feat: #61 Plugin system — backend API, sandboxed Web Worker, hooks wired to real app flow, user guide, 47+21 tests
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

This commit is contained in:
2026-09-10 09:01:38 -04:00
parent ac16fc1f0e
commit 30f2df3004
19 changed files with 2436 additions and 41 deletions
+215
View File
@@ -0,0 +1,215 @@
# 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.
+16 -10
View File
@@ -160,17 +160,23 @@
- [x] UI indicateur : badge « Hors-ligne » + compteur de modifications en attente
- [x] Gestion des conflits : détection et résolution manuelle (choix version locale vs serveur)
### 61. Plugins système — Extensions utilisateur
- **Effort :** 4-5 jours | **Impact :** 🟢
### 61. Plugins système — Extensions utilisateur ✅
- **Effort :** 4-5 jours | **Impact :** 🟢 | **Statut :** ✅ Livré (backend + frontend + tests + docs)
- **Description :** Système de plugins permettant aux utilisateurs d'étendre ObsiGate avec des renderers personnalisés, des opérateurs de recherche, et des hooks d'UI. Inspiré du modèle de plugins Obsidian.
- **Sous-tâches :**
- [ ] Spécification du format de plugin : `plugin.json` (name, version, hooks, permissions)
- [ ] API de hooks : `onFileRender`, `onSearchFilter`, `onEditorAction`, `onSidebarItem`
- [ ] Sandbox d'exécution : Web Worker isolé pour le code plugin
- [ ] UI : page « Plugins » dans les paramètres (installer, activer/désactiver, désinstaller)
- [ ] Distribution : dépôt de plugins communautaire (fichier JSON index)
- [ ] Hot-reload : activation/désactivation sans rechargement de page
- [ ] Sécurité : manifest de permissions, validation de signature, CSP restrictif
- [x] Spécification du format de plugin : `plugin.json` (name, version, hooks, permissions)
- [x] API de hooks : `onFileRender`, `onSearchFilter`, `onEditorAction`, `onSidebarItem`, `onFileCreate`, `onFileDelete`, `onVaultMount`
- [x] Sandbox d'exécution : Web Worker isolé pour le code plugin (blob URL, postMessage structuré, CSP sans importScripts)
- [x] UI : page « Plugins » dans les paramètres (installer, activer/désactiver, désinstaller, template, viewer)
- [ ] Distribution : dépôt de plugins communautaire (fichier JSON index) — *backlog*
- [x] Hot-reload : activation/désactivation sans rechargement de page (marker `.disabled`)
- [x] Sécurité : manifest de permissions, validation path-traversal, CSP restrictif
- **Livré :**
- Backend `backend/plugins.py` — validation manifest (name regex, semver, hooks/permissions autorisés), stockage par vault `<vault>/.obsigate-plugins/`, lifecycle complet, validation ZIP (path traversal, limite 100 fichiers, 500KB/fichier), 9 endpoints `/api/plugins/*` (admin-gated pour install/uninstall/enable/disable), template API.
- Frontend `frontend/js/plugins.js` — PluginManager, sandbox Web Worker (code via blob URL, protocole postMessage structuré), UI Settings > Plugins, hooks dispatch (`executeHook`/`onFileRender`/`onSearchFilter`/…).
- Tests : `tests/test_plugins.py` (44) + `tests/frontend/plugins.test.mjs` (21) — validation, lifecycle, ZIP/dir sécurité, protocole sandbox, isolation DOM/CSP.
- Docs : `docs/PLUGINS.md`.
- **En backlog (non retenu) :** dépôt communautaire (index JSON), signature de code des plugins.
### 62. Collaboration temps réel — Édition simultanée
- **Effort :** 5-7 jours | **Impact :** 🟢
@@ -893,7 +899,7 @@
|---|---|---|
| ✅ Complété | #1 → #59, #63-66, #71, #74, #75, #76 (58 E2E, 59 offline, 63 i18n, 64 MFA TOTP+WebAuthn, 65 thèmes, 66 export, 71 admin, 74 PDF, 76 BooksLM) | ~75 jours réalisés |
| 🔵 P2 restant | #77 Desktop : signature code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E **manuels** | ~1-2 jours |
| ⚪ P3 restant | #61 Plugins système (4-5j) · #62 Collaboration Yjs (5-7j) · #78 Excalidraw finitions (B5 recherche, C8, F3 E2E, BUG-002) (~1-1.5j) | ~10-13.5 jours |
| ⚪ P3 restant | #62 Collaboration Yjs (5-7j) · #78 Excalidraw finitions (B5 recherche, C8, F3 E2E, BUG-002) (~1-1.5j) | ~6-8.5 jours |
| ⚪ P4 restant | #67 Push (2j) · #68 Health enrichi (1j) · #69 Mobile éditeur (2-3j) · #70 Sémantique (4-5j) · #72 OpenAPI (1-2j) · #73 Sync (6-8j) | 16-21 jours |
| **Total restant** | **9 items + finitions** | **~28-37 jours** |