# 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 `/.obsigate-plugins//`: ``` .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.