CI / lint (push) Failing after 30s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 35s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
216 lines
8.0 KiB
Markdown
216 lines
8.0 KiB
Markdown
# Plugin System — ObsiGate #61
|
|
|
|
## Overview
|
|
|
|
ObsiGate's plugin system enables extending the application with custom
|
|
renderers, search filters, and editor actions — all sandboxed for security.
|
|
|
|
Plugins run in a Web Worker sandbox and communicate with the main app via
|
|
structured `postMessage`. No plugin can access the DOM, localStorage, or
|
|
make network requests without explicit permission.
|
|
|
|
## Quick Start
|
|
|
|
### Installing a Plugin
|
|
|
|
1. Go to **Settings > Plugins** in the sidebar
|
|
2. Click **Install Plugin**
|
|
3. Upload a `.zip` file or a directory containing:
|
|
- `plugin.json` — the manifest
|
|
- Your main JS file (entry point specified in `main`)
|
|
|
|
### Creating a Plugin
|
|
|
|
Use the **View Template** button in Settings > Plugins to get started.
|
|
|
|
## Manifest (plugin.json)
|
|
|
|
```json
|
|
{
|
|
"name": "my-plugin",
|
|
"version": "1.0.0",
|
|
"description": "A brief description of what this plugin does",
|
|
"author": "your-name",
|
|
"main": "index.js",
|
|
"hooks": {
|
|
"onFileRender": "renderFile",
|
|
"onSearchFilter": "filterResults",
|
|
"onEditorAction": "handleAction"
|
|
},
|
|
"permissions": ["read_files"],
|
|
"min_obsigate_version": "2.1.0",
|
|
"license": "MIT",
|
|
"homepage": "https://example.com",
|
|
"repository": "https://github.com/user/plugin"
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
| Field | Type | Description |
|
|
|---------------|--------|-------------------------------------------------|
|
|
| `name` | string | Lowercase alphanumeric + hyphens (e.g. `my-plugin`) |
|
|
| `version` | string | Semantic version (e.g. `1.0.0`, `1.0.0-beta.1`) |
|
|
| `description` | string | Brief description |
|
|
| `author` | string | Author name |
|
|
| `main` | string | Entry point file (relative to plugin root) |
|
|
|
|
### Optional Fields
|
|
|
|
| Field | Type | Default | Description |
|
|
|------------------------|--------|-------------|------------------------------------------|
|
|
| `hooks` | object | `{}` | Map of hook name → handler function name |
|
|
| `permissions` | array | `[]` | Required permissions |
|
|
| `min_obsigate_version` | string | `"2.1.0"` | Minimum ObsiGate version |
|
|
| `license` | string | `"MIT"` | License identifier |
|
|
| `homepage` | string | `null` | Plugin homepage URL |
|
|
| `repository` | string | `null` | Source repository URL |
|
|
|
|
## Available Hooks
|
|
|
|
| Hook | When it fires | Handler signature |
|
|
|-------------------|-----------------------------------|--------------------------------|
|
|
| `onFileRender` | Before a file is rendered | `(ctx) → transformed ctx` |
|
|
| `onSearchFilter` | During search result filtering | `(results) → filtered results` |
|
|
| `onEditorAction` | When editor action is triggered | `(action, state) → result` |
|
|
| `onSidebarItem` | Sidebar item is rendered | `(item) → enhanced item` |
|
|
| `onFileCreate` | After a file is created | `(file) → void` |
|
|
| `onFileDelete` | After a file is deleted | `(file) → void` |
|
|
| `onVaultMount` | When a vault is mounted | `(vault) → void` |
|
|
|
|
## Permissions
|
|
|
|
Plugins must declare the permissions they need. The sandbox enforces these
|
|
restrictions at runtime.
|
|
|
|
| Permission | Description |
|
|
|-----------------------|--------------------------------------------------|
|
|
| `read_files` | Read file contents from the vault |
|
|
| `write_files` | Write/create files in the vault |
|
|
| `read_vault_metadata` | Access vault configuration and metadata |
|
|
| `network_request` | Make HTTP requests to external services |
|
|
| `ui_notify` | Show toast notifications in the UI |
|
|
| `access_clipboard` | Read/write to the system clipboard |
|
|
|
|
## Security Model
|
|
|
|
### Sandbox Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Main App (browser context) │
|
|
│ │
|
|
│ PluginManager │
|
|
│ ├── install/uninstall │
|
|
│ ├── enable/disable │
|
|
│ └── hook dispatch │
|
|
│ │ postMessage (C3) │
|
|
│ ▼ │
|
|
│ ┌─────────────────────────┐ │
|
|
│ │ Web Worker Sandbox │ │
|
|
│ │ (no DOM, no localStorage│ │
|
|
│ │ no network by default) │ │
|
|
│ │ │ │
|
|
│ │ Plugin code executes │ │
|
|
│ │ here with structured │ │
|
|
│ │ message passing only │ │
|
|
│ └─────────────────────────┘ │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
### Security Guarantees
|
|
|
|
- **C1**: Manifest validation (name format, semver, allowed hooks/permissions)
|
|
- **C2**: Vault isolation — plugins are scoped to a single vault
|
|
- **C3**: Web Worker sandbox — no DOM, no `localStorage`, no `importScripts`
|
|
- **C4**: ZIP validation — path traversal, file count, size limits
|
|
- **C5**: Directory validation — manifest + entry point presence
|
|
- **C6**: Permission enforcement at sandbox boundary
|
|
|
|
### Limits
|
|
|
|
- Max 50 plugins per vault
|
|
- Max 500 KB per plugin file
|
|
- Max 100 files per plugin ZIP
|
|
- No dynamic `import()` or `eval()`
|
|
|
|
## Plugin Storage
|
|
|
|
Plugins are installed under `<vault>/.obsigate-plugins/<plugin-name>/`:
|
|
```
|
|
.obsigate-plugins/
|
|
my-plugin/
|
|
plugin.json # manifest
|
|
index.js # entry point
|
|
.disabled # marker file (created when disabled)
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### Backend Endpoints
|
|
|
|
| Method | Endpoint | Auth | Description |
|
|
|--------|---------------------------------|---------|--------------------------|
|
|
| GET | `/api/plugins` | require_auth | List installed plugins |
|
|
| POST | `/api/plugins/install` | require_admin | Install from ZIP |
|
|
| DELETE | `/api/plugins/{name}` | require_admin | Uninstall plugin |
|
|
| POST | `/api/plugins/{name}/enable` | require_admin | Enable plugin |
|
|
| POST | `/api/plugins/{name}/disable` | require_admin | Disable plugin |
|
|
| GET | `/api/plugins/{name}/code/{file}` | require_auth | Get plugin code |
|
|
| GET | `/api/plugins/{name}/hooks` | require_auth | Get plugin hooks |
|
|
| GET | `/api/plugins/template` | require_auth | Get plugin template |
|
|
|
|
### Frontend Module (`plugins.js`)
|
|
|
|
```javascript
|
|
import { PluginManager } from './plugins.js';
|
|
|
|
// Install
|
|
await PluginManager.installPlugin(manifest, code);
|
|
|
|
// Enable/Disable
|
|
await PluginManager.enablePlugin('my-plugin');
|
|
await PluginManager.disablePlugin('my-plugin');
|
|
|
|
// Uninstall
|
|
await PluginManager.uninstallPlugin('my-plugin');
|
|
|
|
// Get code for sandbox
|
|
const code = await PluginManager.getPluginCode('my-plugin', 'index.js');
|
|
|
|
// List
|
|
const plugins = await PluginManager.getCachedPlugins();
|
|
```
|
|
|
|
## Creating Your First Plugin
|
|
|
|
1. Create a directory with `plugin.json`:
|
|
|
|
```json
|
|
{
|
|
"name": "hello-world",
|
|
"version": "1.0.0",
|
|
"description": "My first ObsiGate plugin",
|
|
"author": "you",
|
|
"main": "index.js",
|
|
"hooks": {
|
|
"onFileRender": "render"
|
|
},
|
|
"permissions": ["read_files", "ui_notify"]
|
|
}
|
|
```
|
|
|
|
2. Create `index.js`:
|
|
|
|
```javascript
|
|
export function render(ctx) {
|
|
// Add a custom header to rendered markdown
|
|
if (ctx.content && ctx.path.endsWith('.md')) {
|
|
ctx.content = `> 📝 Plugin: hello-world\n\n${ctx.content}`;
|
|
}
|
|
return ctx;
|
|
}
|
|
```
|
|
|
|
3. Zip the directory and install via Settings > Plugins > Install Plugin.
|