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 +xSearch order (first hit wins):
$XDG_CONFIG_HOME/amber/plugins/<id>~/.config/amber/plugins/<id><workspace>/.amber/plugins/<id>$(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} } }
]
}| Field | Required | Meaning |
|---|---|---|
id | yes | Slug [a-z0-9_]+; directory name, tool prefix, namespace root. |
name | yes | Display name. |
version | yes | Semver. |
protocol_version | yes | Integer. The harness rejects a mismatch (incompatible). |
description | yes | Prompt advertisement: how/when to use the tools, rendered while enabled. |
main | yes | Executable path relative to the plugin directory. |
author / url / license | no | Attribution, shown by /get plugin info <id>. |
settings | no | Default key/value settings; overridden via /set plugin settings <id> <key>=<value>. |
completion | no | A completions.json subtree merged into the command tree on enable. |
tools | no | Tool 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: exitinitialize
// 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 cleanlyTimeouts
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.