External plugins

External plugins are self-contained executables that extend amber with new tools. The harness never links plugin code: every plugin is a separate process speaking newline-delimited JSON-RPC 2.0 over stdio, so a plugin crash cannot take down the app and protocol changes ship as plugin updates without touching the core.

They contribute tools only. For anything else, providers, status segments, panels, prompt blocks, event hooks, use acore plugin.

Layout and discovery

~/.config/amber/plugins/hello/
├── manifest.json      # id, name, version, protocol_version, main, tools[], completion{}
└── hello-plugin       # executable, chmod +x

Search order (first hit wins):

  1. $XDG_CONFIG_HOME/amber/plugins/<id>
  2. ~/.config/amber/plugins/<id>
  3. <workspace>/.amber/plugins/<id>
  4. $(datadir)/amber/plugins/<id> (system-shipped)

Manifest

{
  "id": "hello",
  "name": "Hello",
  "version": "1.0.0",
  "protocol_version": 1,
  "author": "you",
  "url": "https://example.com",
  "license": "MIT",
  "description": "How and when to use this plugin, advertised to the agent.",
  "main": "hello-plugin",
  "settings": { "greeting": "Hello" },
  "completion": {
    "hello": {
      "help": "one-line drawer description",
      "man": "Manual page: use case, usage, examples.",
      "children": {
        "greet": { "help": "Greet someone", "man": "Usage: /hello greet <name>" }
      }
    }
  },
  "tools": [
    { "name": "greet",
      "description": "Greet someone by name.",
      "schema": { "name": {"type": "string", "required": true} } }
  ]
}
FieldRequiredMeaning
idyesSlug [a-z0-9_]+; directory name, tool prefix, namespace root.
nameyesDisplay name.
versionyesSemver.
protocol_versionyesInteger. The harness rejects a mismatch (incompatible).
descriptionyesPrompt advertisement: how/when to use the tools, rendered while enabled.
mainyesExecutable path relative to the plugin directory.
author / url / licensenoAttribution, shown by /get plugin info <id>.
settingsnoDefault key/value settings; overridden via /set plugin settings <id> <key>=<value>.
completionnoA completions.json subtree merged into the command tree on enable.
toolsnoTool definitions, registered as plugin_<id>_<name>.

Protocol

One JSON object per line on stdin/stdout, UTF-8. All messages carry an id except notifications.

sequenceDiagram
    autonumber
    participant H as amber harness
    participant PM as PluginManager
    participant P as plugin process

    H->>PM: enable(id)
    PM->>PM: discover manifest.json
    Note over H,PM: enabled - tools registered, namespace merged

    H->>PM: tool call
    PM->>P: spawn executable
    PM->>P: initialize protocol_version, settings, workspace
    P-->>PM: protocol_version, ok

    alt compatible
        PM->>P: tool.call name, args
        P-->>PM: ok, output, meta
        PM-->>H: tool envelope
    else version mismatch or ok false
        Note over H,P: unregistered - incompatible
    end

    H->>PM: disable(id)
    PM->>P: shutdown
    P-->>PM: exit
Discovery, spawn on the first tool call, the version handshake, and the shutdown notification.

initialize

// harness -> plugin
{"id": 1, "method": "initialize", "params": {
  "protocol_version": 1,
  "settings": { "greeting": "Hello" },
  "workspace": "/path/to/workspace"}}

// plugin -> harness
{"id": 1, "result": {"protocol_version": 1, "ok": true}}

A protocol_version mismatch or "ok": false marks the plugin incompatible and unregisters it.

tool.call

// harness -> plugin
{"id": 2, "method": "tool.call",
 "params": {"name": "greet", "args": {"name": "amber"}}}

// plugin -> harness  (the standard tool envelope)
{"id": 2, "result": {"ok": true, "output": "Hello, amber!", "meta": {}}}

// errors are returned, never thrown:
{"id": 2, "result": {"ok": false, "output": "ERROR: ..."}}

The result is the same tool envelope the built-in tools use, so the agent reads a plugin result exactly like a bash orsearch result.

shutdown

{"method": "shutdown"}   // notification; the plugin exits cleanly

Timeouts

A tool call that produces no line within 60 s is killed and reported as a timeout. Partial output is lost, plugins must emit one line per request.

Administration

/get plugin list | info <id> | settings <id> [key]
/set plugin on <id> | off <id> | install <path|url> | uninstall <id>
/set plugin settings <id> <key>=<value>

install takes a tar.gz (local path or http(s) URL) containingmanifest.json and the executable; it is validated and staged to~/.config/amber/plugins/<id>/, left disabled until/set plugin on <id>. System-shipped plugins cannot be uninstalled.

Security

  • Plugin code is untrusted but runs with the user's privileges, like the harness. Install only plugins you trust; /get plugin info <id> shows author/url/license before enabling.
  • All path arguments pass through workspace confinement before the plugin sees them.
  • Output is capped at 64 KiB per tool result.
  • Tools are namespaced plugin_<id>_<name>, so approval rules can target them by name.

Known limitation

Command subtrees contributed by external plugins render in the completion drawer but cannot execute: no handler is registered forplugin.* actions. The command-contribution capability was removed (nothing produced or consumed one). Expose behaviour through tools instead.

Full protocol: docs/spec/plugins/README.md.

amber 0.5.8Apache 2.0C++17
utf-8ncursesJacek Trefon www.trefon.com2026