agent-orchestrator/docs/plugins/guide-developing.md
Hibryda b6c1d4b6af docs: add 11 new documentation files across all categories
New reference docs:
- agents/ref-btmsg.md: inter-agent messaging schema and CLI
- agents/ref-bttask.md: kanban task board operations
- providers/ref-providers.md: Claude/Codex/Ollama/Aider comparison
- config/ref-settings.md: (already committed)

New guides:
- contributing/dual-repo-workflow.md: community vs commercial repos
- plugins/guide-developing.md: Web Worker sandbox API and publishing

New pro docs:
- pro/features/knowledge-base.md: persistent memory + symbol graph
- pro/features/git-integration.md: context injection + branch policy
- pro/marketplace/README.md: 13 plugins catalog

Split files:
- architecture/data-model.md: from architecture.md (schemas, layout)
- production/hardening.md: from production.md (supervisor, sandbox, WAL)
- production/features.md: from production.md (FTS5, plugins, secrets, audit)
2026-03-17 04:18:05 +01:00

283 lines
8.2 KiB
Markdown

# 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.