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)
8.2 KiB
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
{
"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.
// 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, noDeno.readFile, nofetchtofile://) - Network access (no
fetch, noXMLHttpRequest, no WebSocket) - DOM access (no
document, nowindow) - Tauri IPC (no
invoke, no event listeners) - Dynamic imports (no
import(), noimportScripts())
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
agorAPI 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).
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.
// 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)-- returnsArray<{ 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
sessionIdplus 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).
// 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.
const value = safeGet(event, 'data.session.cost', 0);
safeMsg(template, ...args)
String interpolation with type coercion and truncation (max 500 characters per argument).
const msg = safeMsg('Session {} completed with {} turns', sessionId, turnCount);
Example Plugin: hello-world
{
"id": "hello-world",
"name": "Hello World",
"version": "1.0.0",
"description": "Minimal example plugin that greets the user.",
"entry": "index.js",
"permissions": ["palette", "notifications"]
}
// 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:
- Create a directory in the
agents-orchestrator/agor-pluginsrepository underplugins/{plugin-id}/. - Add the plugin files (
plugin.json,index.js, and any supporting files). - Create a
.tar.gzarchive of the plugin directory. - Compute the SHA-256 checksum:
sha256sum my-plugin.tar.gz. - Add an entry to
catalog.jsonwith the download URL, checksum, and metadata. - Submit a pull request to the
agor-pluginsrepository.
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:
./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.