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

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, 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).

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

// 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:

  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:

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