# Plugin Development Guide ## Overview Agents Orchestrator plugins are self-contained bundles that run in a sandboxed Web Worker. A plugin consists of a manifest file (`plugin.json`) and an entry point script (`index.js`). Plugins interact with the host application through a message-passing API gated by declared permissions. ## Plugin Anatomy A minimal plugin directory: ``` ~/.config/bterminal/plugins/my-plugin/ plugin.json -- Manifest (required) index.js -- Entry point (required) ``` ### Manifest: plugin.json ```json { "id": "my-plugin", "name": "My Plugin", "version": "1.0.0", "description": "A brief description of what this plugin does.", "author": "Your Name", "entry": "index.js", "permissions": [ "notifications", "tasks" ] } ``` **Required fields:** | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Unique identifier (lowercase, hyphens allowed) | | `name` | `string` | Human-readable display name | | `version` | `string` | Semver version string | | `description` | `string` | Short description (under 200 characters) | | `entry` | `string` | Relative path to entry point script | | `permissions` | `string[]` | List of API permissions the plugin requires | **Optional fields:** | Field | Type | Description | |-------|------|-------------| | `author` | `string` | Plugin author name | | `homepage` | `string` | URL to plugin documentation | | `minVersion` | `string` | Minimum Agents Orchestrator version required | ### Entry Point: index.js The entry point is loaded as a Web Worker module. The `agor` global object provides the plugin API. ```javascript // index.js const { meta, notifications } = agor; console.log(`${meta.id} v${meta.version} loaded`); notifications.send({ title: meta.name, body: 'Plugin activated.', type: 'info', }); ``` ## Web Worker Sandbox Plugins run in an isolated Web Worker context. The sandbox enforces strict boundaries: **Not available:** - Filesystem access (no `fs`, no `Deno.readFile`, no `fetch` to `file://`) - Network access (no `fetch`, no `XMLHttpRequest`, no WebSocket) - DOM access (no `document`, no `window`) - Tauri IPC (no `invoke`, no event listeners) - Dynamic imports (no `import()`, no `importScripts()`) **Available:** - Standard JavaScript built-ins (`JSON`, `Map`, `Set`, `Promise`, `Date`, etc.) - `console.log/warn/error` (routed to host debug output) - `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval` - The `agor` API object (permission-gated) ## Plugin API All API methods are asynchronous and return Promises. Each API namespace requires a corresponding permission declared in `plugin.json`. ### agor.meta Always available (no permission required). ```typescript interface PluginMeta { id: string; // Plugin ID from manifest version: string; // Plugin version from manifest name: string; // Plugin display name } ``` ### agor.palette **Permission:** `"palette"` Register commands in the application command palette. ```typescript // Register a command agor.palette.register({ id: 'my-plugin.greet', label: 'My Plugin: Say Hello', callback: () => { agor.notifications.send({ title: 'Hello', body: 'Greetings from the plugin.', type: 'info', }); }, }); // Unregister a command agor.palette.unregister('my-plugin.greet'); ``` ### agor.messages **Permission:** `"messages"` -- Read agent messages for the active session (read-only). - `agor.messages.list(sessionId)` -- returns `Array<{ role, content, timestamp }>`. - `agor.messages.count(sessionId)` -- returns message count. ### agor.tasks **Permission:** `"tasks"` -- Read/write task board entries (see Tasks-as-KV below). - `agor.tasks.create({ title, description, status })` -- returns task ID. - `agor.tasks.list({ status?, limit? })` -- returns task array. - `agor.tasks.updateStatus(taskId, status)` -- updates status. - `agor.tasks.delete(taskId)` -- removes task. ### agor.events **Permission:** `"events"` -- Subscribe to application events. - `agor.events.on(eventName, callback)` -- returns unsubscribe function. - Events: `agent:status`, `agent:message`, `agent:complete`, `agent:error`. - Each event payload includes `sessionId` plus event-specific fields. ### agor.notifications **Permission:** `"notifications"` -- Send toast notifications. - `agor.notifications.send({ title, body, type })` -- type: info | success | warning | error. ## Permission Model Each API namespace is gated by a permission string. If a plugin calls an API it has not declared in its `permissions` array, the call is rejected with a `PermissionDenied` error. | Permission | API Namespace | Access Level | |------------|---------------|-------------| | `palette` | `agor.palette` | Register/unregister commands | | `messages` | `agor.messages` | Read-only agent messages | | `tasks` | `agor.tasks` | Read/write tasks | | `events` | `agor.events` | Subscribe to events | | `notifications` | `agor.notifications` | Send toast notifications | Permissions are displayed to the user during plugin installation. Users must accept the permission list to proceed. ## Tasks-as-KV Pattern Plugins that need persistent key-value storage use the task system with prefixed keys. This avoids adding a separate storage layer. ### Convention Task titles use the format `plugin:{plugin-id}:{key}`. The task description field holds the value (string, or JSON-serialized for structured data). ```javascript // Store a value await agor.tasks.create({ title: `plugin:${agor.meta.id}:last-run`, description: JSON.stringify({ timestamp: Date.now(), count: 42 }), status: 'done', }); // Retrieve a value const tasks = await agor.tasks.list({ limit: 1 }); const entry = tasks.find(t => t.title === `plugin:${agor.meta.id}:last-run`); const data = JSON.parse(entry.description); ``` ### LRU Cap Plugins should limit their KV entries to avoid unbounded growth. Recommended cap: 100 entries per plugin. When creating a new entry that would exceed the cap, delete the oldest entry first. ### Purge On plugin uninstall, all tasks with the `plugin:{plugin-id}:` prefix are automatically purged. ## Shared Utilities Two helper functions are injected into the Web Worker global scope for common patterns: ### safeGet(obj, path, defaultValue) Safe property access for nested objects. Avoids `TypeError` on undefined intermediate properties. ```javascript const value = safeGet(event, 'data.session.cost', 0); ``` ### safeMsg(template, ...args) String interpolation with type coercion and truncation (max 500 characters per argument). ```javascript const msg = safeMsg('Session {} completed with {} turns', sessionId, turnCount); ``` ## Example Plugin: hello-world ```json { "id": "hello-world", "name": "Hello World", "version": "1.0.0", "description": "Minimal example plugin that greets the user.", "entry": "index.js", "permissions": ["palette", "notifications"] } ``` ```javascript // index.js const { meta, palette, notifications } = agor; palette.register({ id: 'hello-world.greet', label: 'Hello World: Greet', callback: () => { notifications.send({ title: 'Hello', body: `Greetings from ${meta.name} v${meta.version}.`, type: 'info', }); }, }); ``` ## Publishing To publish a plugin to the marketplace: 1. Create a directory in the `agents-orchestrator/agor-plugins` repository under `plugins/{plugin-id}/`. 2. Add the plugin files (`plugin.json`, `index.js`, and any supporting files). 3. Create a `.tar.gz` archive of the plugin directory. 4. Compute the SHA-256 checksum: `sha256sum my-plugin.tar.gz`. 5. Add an entry to `catalog.json` with the download URL, checksum, and metadata. 6. Submit a pull request to the `agor-plugins` repository. The catalog maintainers review the plugin for security (no obfuscated code, reasonable permissions) and functionality before merging. ## Scaffolding Use the scaffolding script to generate a new plugin skeleton: ```bash ./scripts/plugin-init.sh my-plugin "My Plugin" "A description of my plugin" ``` This creates: ``` ~/.config/bterminal/plugins/my-plugin/ plugin.json -- Pre-filled manifest index.js -- Minimal entry point with palette command stub ``` The script prompts for permissions to declare. Generated files include comments explaining each section.