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
8.0 KiB
8.0 KiB
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
- Go to Settings > Plugins in the sidebar
- Click Install Plugin
- Upload a
.zipfile 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)
{
"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, noimportScripts - 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()oreval()
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)
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
- Create a directory with
plugin.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"]
}
- Create
index.js:
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;
}
- Zip the directory and install via Settings > Plugins > Install Plugin.