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
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:
+215
@@ -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
@@ -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** |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user