Plugin API Reference

The field-by-field reference for aoe-plugin.toml, the manifest every Agent of Empires plugin ships. The schema lives in the aoe-plugin-api crate (PluginManifest) and is the source of truth; this page documents it for plugin authors. The host parses the manifest strictly (unknown keys are rejected), so every key here maps to a schema field.

For a guided introduction see Writing Plugins. To scaffold a working plugin, use the starter template:

cookiecutter gh:agent-of-empires/plugin-template

Versioning

A manifest carries two independent version axes.

Key Meaning
api_version The manifest schema version. The current schema is 13. The host rejects a manifest whose api_version is newer than it supports. Bump it as you adopt newer sections (see below).
aoe_version A semver requirement on the host app version, e.g. ">=1.11.0, <2.0.0". The host refuses to install, and skips loading, a plugin whose requirement excludes the running version. Optional; requires api_version >= 4.

Schema additions by api_version: 2 added contributions (commands, keybinds, settings, ui), 3 added the pane UI slot, 4 added status and aoe_version, 5 added screenshots, 6 added a command action, 7 added identity icons, 8 added the composer-action UI slot, 9 added session-driving worker RPCs (see Session-driving RPCs), plugin-private storage, and the dynamic_select / object_list / cron settings widgets, 10 added the tool-card-badge UI slot, 11 added the acp.capabilities.probe RPC + capability, a thinking (thought-level) list on the capability response, the dynamic_multi_select object-list field widget, and an optional project_path (empty = scratch session), extra_project_paths, and a sandbox flag on sessions.create, plus a multiline attribute for string settings fields, 12 grew the pane block vocabulary (see Pane payload): the callout, bar and columns kinds, clickable rows carrying params, header summaries and scrollable bodies on section, disabled / variant / href on action, and a pane-level footer, 13 added the global home-pane UI slot (a host-wide docked pane carrying the same block vocabulary as pane) and the sparkline block kind (a history plot with optional per-sample bands coloring).

Top-level fields

id = "dev.example.my-plugin"
name = "My Plugin"
version = "0.1.0"
api_version = 8
aoe_version = ">=1.11.0, <2.0.0"
description = "What the plugin does."
capabilities = ["runtime.worker"]
Key Type Required Notes
id string yes Plugin id (see Plugin id). Namespaces config, events, and action names.
name string yes Human-readable display name.
version string yes Semantic version of the plugin.
api_version integer yes Manifest schema version, 1 to 13.
description string no Shown in plugin listings. Defaults to empty.
aoe_version string no Host-app semver requirement. Requires api_version >= 4.
capabilities array of string no Runtime grants the worker needs (see Capabilities). Static contributions need none.
screenshots array no Up to 8. Requires api_version >= 5. See Screenshots.
setting_defaults table no Overrides for core host settings, keyed by canonical path (e.g. "theme.idle_decay_minutes"). Resolution is user value, then plugin override, then core default.

Plugin id

A dotted, lowercase ASCII identifier such as dev.example.review-helper. Each dot-separated segment starts with a lowercase letter and may contain digits and hyphens; the whole id is at most 64 bytes. The aoe.* and agent-of-empires.* namespaces are reserved for bundled and officially featured plugins; a community install cannot claim them.

Capabilities

Capabilities gate runtime resource access. They are prompted once at install and pinned to the manifest hash; an update that widens them must be re-approved. Declare only what the worker uses. Static contributions (commands, keybinds, themes, ui, status) need no capability.

Capability Grants
runtime.worker Running any plugin code at all (host RPCs the worker initiates). Any worker needs this.
session.read Reading the attached session.
session.write Mutating the attached session.
config.read Reading host or other-plugin configuration (not the plugin’s own settings).
config.write Writing host or other-plugin configuration.
process.spawn Spawning processes beyond the plugin’s own worker.
net Outbound network access.
fs.read Filesystem reads outside the plugin directory.
fs.write Filesystem writes outside the plugin directory.
clipboard.read Reading the clipboard.
clipboard.write Writing the clipboard.
notifications Posting desktop / TUI notifications.
browser_open Opening a URL in the user’s browser from a command action.
composer.read Reading a click-scoped snapshot of the active ACP composer draft from a composer-action.
composer.write Publishing a host-validated draft edit from a composer-action UI-state payload.
acp.capabilities.read Discovering available agents and their advertised models/modes via acp.capabilities.get (api_version >= 9).
acp.capabilities.probe Triggering a handshake-only catalog probe via acp.capabilities.probe: the host spawns the agent adapter, runs initialize + session/new (no prompt turn, so no tokens), records the advertised models/modes/thought-levels, and tears it down. Distinct from acp.capabilities.read because it spawns a real process (api_version >= 11).
session.create Creating a host-owned structured session via sessions.create (api_version >= 9).
session.prompt Delivering a turn to a session the plugin created via sessions.turn.send, and the initial turn on sessions.create (api_version >= 9).
session.unattended Creating a session in a host-classified unattended approval mode. A distinct, high-severity grant, never implied by session.create or session.prompt (api_version >= 9). See Session-driving RPCs.

A capability this host version does not recognize is rejected, not granted.

Commands

Palette and CLI entries, namespaced by the host as plugin.<id>.<command-id>.

[[commands]]
id = "status"
title = "My Plugin: status"
description = "Show the status summary."
Key Type Required Notes
id string yes Command id. Empty is unaddressable.
title string no Display name.
description string no Help text.
action table no A client-executed action. Requires api_version >= 6 and the browser_open capability.

Command action

[commands.action]
kind = "open-ui-link"
slot = "row-badge"
id = "my_badge"

The only kind is open-ui-link: it opens the href from the plugin’s own (slot, id) UI-state entry in the browser, with no worker round-trip. The (slot, id) pair must match a declared [[ui]] entry on a per-session slot.

Keybinds

[[keybinds]]
command = "status"
key = "Ctrl+Shift+G"
Key Type Required Notes
command string yes Target command id (a plugin or core command).
key string yes Key chord, e.g. Ctrl+Shift+G. Core bindings win a collision.

Settings

Plugin-declared settings, rendered on the TUI and web settings surfaces and stored under [plugins."<id>".settings]. The worker reads them via the config.get host RPC.

[[settings]]
key = "refresh_secs"
label = "Refresh interval (seconds)"
description = "How often the worker polls."
type = "integer"
default = 120
min = 0
max = 86400
advanced = true
Key Type Required Notes
key string yes Setting key, stored under the plugin’s settings table.
label string no Display label.
description string no Help text.
type string no Value type (see below). Defaults to string.
options array of string no Allowed values for select; ignored otherwise.
min / max integer no Inclusive bounds for integer; ignored otherwise.
default any no Declared default. Must match type. Absent means the type’s zero value.
advanced bool no Group under the Advanced fold. Defaults to false.
multiline bool no Render a string field as a multi-line textarea; ignored for other types (api_version >= 11).
option_source string no Host source for a dynamic_select (api_version >= 9).
depends_on array of string no Sibling keys whose values parameterize a dynamic_select (api_version >= 9).
fields array no Item fields of an object_list (api_version >= 9).
item_id_key string no Item field holding each object_list row’s stable id; defaults to _id (host-generated) (api_version >= 9).
min_items / max_items integer no Inclusive item-count bounds for an object_list (api_version >= 9).

Setting types:

type Widget
string Text input (default).
bool (or boolean) Toggle.
integer Number input, bounded by min / max.
select Dropdown over a non-empty options array.
dynamic_select Dropdown whose choices the host resolves from option_source (api_version >= 9).
dynamic_multi_select Multi-select (checkbox list) whose choices the host resolves from option_source; the stored value is an array of chosen values. Object-list item fields only (api_version >= 11).
cron Validated 5-field cron expression text field (api_version >= 9).
object_list A repeatable list of structured items described by fields (api_version >= 9).

Dynamic selects (api_version >= 9)

A dynamic_select renders a dropdown whose options the host resolves at render time, so the plugin never ships a hardcoded list that could drift from the host’s real agents, models, or projects. Set option_source to one of:

option_source Choices
acp.agents ACP-capable agents the host knows and whose adapter is installed on this host. Uninstalled harnesses are not offered.
acp.models Models the selected agent advertised. Needs the agent via depends_on.
acp.modes Approval modes the selected agent advertised. Needs the agent via depends_on.
projects Registered projects (value is the project path).
groups Existing session group paths.

depends_on names sibling keys whose current values parameterize the source; acp.models and acp.modes require the selected agent. When the selected agent’s option catalog has never been discovered, resolving acp.models / acp.modes runs a one-shot handshake probe (see acp.capabilities.probe) to populate it, so the picker self-fills on first open instead of staying empty until the agent has run a live session. Saved ids are advisory: the host revalidates them when a session is actually created, so a model that later disappears from the catalog surfaces as an error at creation, not silently at save.

Object lists (api_version >= 9)

An object_list is a repeatable list of structured records (for example, a cron plugin’s schedule entries), stored on disk as a TOML array of tables under [[plugins."<id>".settings.<key>]]. It is one level deep: each item field is declared in fields and cannot itself be an object_list. Every item carries a stable id under item_id_key (host-generated on add, never changed on edit or reorder) so a worker can track an entry across edits.

[[settings]]
key = "jobs"
label = "Scheduled jobs"
type = "object_list"
item_id_key = "id"
max_items = 50

[[settings.fields]]
key = "agent_id"
label = "Agent"
type = "dynamic_select"
option_source = "acp.agents"
required = true

[[settings.fields]]
key = "model_id"
label = "Model"
type = "dynamic_select"
option_source = "acp.models"
depends_on = ["agent_id"]

[[settings.fields]]
key = "schedule"
label = "Schedule"
type = "cron"
required = true

Each item field takes the same key / label / description / type / options / min / max / default / multiline / option_source / depends_on keys as a top-level setting, plus required (the item must carry a non-empty value). An item field’s type cannot be object_list. An item field may be a dynamic_multi_select (api_version >= 11): like dynamic_select it names an option_source and may depends_on siblings, but its stored value is an array of the chosen option values.

Session-driving RPCs

With api_version >= 9 a worker can discover ACP capabilities and create host-owned structured sessions, the primitives an automation plugin (for example a scheduler) needs. These are worker RPCs, not manifest keys; the host enforces a strict security model around them.

Method Capability Purpose
acp.capabilities.get acp.capabilities.read List agents and their advertised models / modes / thought-levels (never launches an agent; a never-run agent reports catalog_status: undiscovered with empty lists).
acp.capabilities.probe acp.capabilities.probe Populate the catalog for one agent (optional agent_id; otherwise every undiscovered registry agent) via a handshake-only probe, then return the same shape as acp.capabilities.get. Spawns the adapter and runs initialize + session/new with no prompt turn (no tokens); each probe degrades to a no-op on failure. api_version >= 11.
sessions.create session.create (+ session.prompt for an initial turn, + session.unattended for an unattended mode) Create a structured session, optionally with an initial turn and a plugin-scoped idempotency key.
sessions.turn.send session.prompt Deliver a turn to a session this plugin created.
plugin.storage.get / set / cas / remove runtime.worker Plugin-private durable key/value storage (see Plugin storage).

Project selection (api_version >= 11). sessions.create takes an optional project_path and an optional extra_project_paths array. Omitting project_path (or sending it empty) creates a scratch session: a throwaway working directory with no repository, hence no trust anchor. When present, the project_path is the trust-checked primary repo and each extra_project_paths entry is an additional repo of a multi-repo session; combining extras with a scratch session (no project_path) is refused. Every path is canonicalized and existence-checked host-side, fail-closed (capped per call).

Sandbox (api_version >= 11). Set sandbox: true to run the session inside the host’s container sandbox. The host uses its own configured sandbox image; a plugin cannot pick an image. Sandboxing only narrows what the agent can reach, so it needs no grant beyond session.create. The create fails synchronously when no container runtime is installed or running; when one is present the container starts asynchronously after the create returns, so image-pull or startup problems surface on the session later, not as a create error.

Approval-mode classification. The plugin proposes a mode_id; the host decides its security class, never the plugin. A mode is interactive (omitted / adapter default), guarded (a reviewed read-only or plan preset), or unattended (a bypass or auto-write mode, and every mode the host does not recognize, which fail closed to unattended). An unattended mode requires the distinct session.unattended grant on top of session.create.

Repository trust is enforced regardless of grants. A session against a repository whose hooks need approval is refused even with session.unattended; a plugin cannot pre-approve repository trust. See Unattended sessions for the full model.

Ownership. sessions.turn.send only reaches a session the calling plugin created; a plugin cannot deliver turns to a user’s or another plugin’s session.

Idempotency. sessions.create accepts an idempotency_key scoped to the plugin: retrying with the same key and payload returns the existing session (created: false); a different payload under the same key is a conflict.

Limits. Per plugin: 20 session creates per hour, 5 active plugin-created sessions, 120 turns per hour. Exceeding a limit returns a rate_limited / concurrency_limited error. Disabling the plugin stops all of its automation.

Settings-change events. After a settings write the host sends the plugin’s worker a plugin.settings.changed notification carrying { revision, changed_keys }; the worker re-reads the affected values via config.get (whose response includes the current revision). Polling config.get remains a fallback for a worker that was down when the write landed.

Plugin storage

A worker has a host-backed, private key/value store, namespaced by its plugin id, that survives daemon and worker restarts (it is not the install directory, which an upgrade can replace). No capability beyond runtime.worker is needed: a plugin can only reach its own namespace.

Method Params Returns
plugin.storage.get { key } { value } (null if absent)
plugin.storage.set { key, value } {}
plugin.storage.cas { key, expected, value } { swapped, current }
plugin.storage.remove { key } { removed }

Quotas per plugin: 64 keys, 256-byte keys, 64 KiB values. cas (compare-and-swap) enables safe concurrent updates: the write applies only when the stored value equals expected.

UI slots

Declares the host-rendered slots the worker pushes state into via the ui.state.set host RPC.

[[ui]]
slot = "pane"
id = "my_pane"
Key Type Required Notes
slot string yes One of the slot names below. Unknown slots are rejected.
id string no Addressing id for (slot, id) state pushes. Required to be non-empty when a command action targets it.
Slot Scope Renders
status-bar global A segment in the dashboard status bar.
card global A card on the dashboard overview.
sort-key global A named sort option over a row-column value.
filter-facet global A named filter over a row-column value.
row-badge per-session A badge on the session row.
row-column per-session A text column on the session row.
detail-badge per-session A badge in the session detail view.
pane per-session A dockable tool-window pane (requires api_version >= 3). See Pane payload.
home-pane global A host-wide docked pane on the dashboard overview and the structured-view pane overlay, carrying the same block vocabulary as pane but session-less (requires api_version >= 13). Several plugins’ home panes stack in snapshot order.
settings-page global A full page under Settings, using the same block vocabulary as pane (requires api_version >= 10).
composer-action per-session A button beside the ACP composer controls (requires api_version >= 8).
tool-card-badge per-session A pill on a transcript MCP or skill tool-call card, matched by target (requires api_version >= 10).
notification n/a A transient notification pushed via ui.notify; gated by the notifications capability, not a slot declaration.

Pane payload

A pane entry renders a dockable tool-window. The worker pushes it with ui.state.set:

{
  "title": "GitHub",
  "default_location": "right",
  "icon": "git-branch",
  "blocks": [{ "kind": "heading", "text": "GitHub" }],
  "footer": { "text": "refreshed 12:07", "value": "blocked", "tone": "danger", "icon": "refresh-cw" }
}
Key Type Notes
title string Shown on the dock tab.
body string The simple form: plain text, used only when blocks is absent.
blocks array The block list (below). Takes precedence over body.
default_location string right or bottom. The dock it first opens in; the user can move it after.
icon string Lucide name for the activity-bar / dock-tab icon. A manifest icon_asset outranks it.
footer table A status line pinned below the scrolling block list: text left, tone-colored value right, plus an optional icon. Requires api_version >= 12.

The whole payload is capped at 64 KiB. Everything else on the entry is validated strictly, but blocks is stored as opaque JSON: each surface renders the kinds it knows and silently drops the rest. That is the forward-compatibility contract, and it cuts both ways. A new kind needs no host change, and an older host will render nothing for it, so a pane whose layout depends on a newer kind should say so with api_version (and aoe_version) rather than degrade silently.

The settings-page slot takes the same block vocabulary, minus default_location and footer (a full page is not docked, so neither has anything to attach to).

Block kinds

kind Required Optional
heading text
note text tone
divider
row one of label / value / prefix / icon / avatar sublabel, tone, value_tone, color, href, tooltip, mono, selected, badges, method, params
section title, children, value, value_tone, badges, icon, tone, boxed, scroll, collapsible, collapsed
callout one of title / detail icon, tone, color, actions
bar segments caption
sparkline values max, tone, bands, caption (requires api_version >= 13)
columns children
action label, plus one of method / href / disabled icon, tone, tooltip, variant
comment one of author / body path, line, resolved, href

tone is one of neutral / info / success / warn / danger. color is a validated #rgb / #rrggbb literal for a hue no tone names (a merged PR’s purple); anything else is ignored.

row lays out at most two lines: prefix (mono, tone-tinted) and label lead the first with value pinned right, and sublabel leads the second with badges pinned right. value_tone colors the trailing token independently of the row, for a status glyph beside a neutral scalar such as a timestamp. mono monospaces the row’s own text. Each entry in badges is { text?, icon?, tone?, tooltip? } and renders as a compact glyph or token, not a pill.

A method makes the row body a button that fires that worker method; an href alongside it becomes a separate trailing open-externally link, so a selectable row can still link out. With href alone the whole row is the link. selected marks the row as the pane’s current subject.

section groups children. The header takes a right-pinned value summary or a run of badges (count pills). boxed draws it as a bordered card, scroll caps the body height so a long list scrolls inside the section instead of pushing the rest of the pane away, and collapsible folds it via a native <details> (collapsed sets the initial state).

callout is a tone-bordered verdict card: a glyph, a title, a detail paragraph, and its own actions laid out full width. Use it for the one thing the pane is telling the user; use a section for a list.

bar is a proportional stacked bar over segments, each { value, tone?, color?, label? }. Segments without a positive numeric value are dropped, and a bar left with nothing renders nothing. caption sits beneath it.

sparkline plots values (an array of numbers, oldest first) as a compact history line. max fixes the top of the scale (default: the largest value), so a series plots against a stable ceiling instead of auto-scaling each refresh; a single tone colors the whole line. bands is a list of { at, tone } thresholds that recolor each sample by the highest band its value reaches, for a green/amber/red pressure line. caption sits beneath. An empty values renders nothing.

columns lays its children side by side in equal fractions. A single child spans the full width, so eliding one card collapses the row cleanly rather than leaving a gap.

action forwards method to the worker (see Pane actions). With href and no method it is a link-out button instead, for something the host cannot do itself. disabled renders it inert, which is how a blocked state reads without pretending to be clickable; a disabled action never navigates either. variant: "primary" gives the brand-filled treatment.

Pane actions

Clicking an action block, or a row carrying a method, POSTs to /api/plugins/{id}/action with { method, params, session_id }. params is the block’s own params object, forwarded verbatim, so one method can serve every row in a list:

{ "kind": "row", "label": "warn when daemon is stale", "prefix": "#3231",
  "method": "github.select_pr", "params": { "pr": "o/r#3231" } }

The host merges in the authoritative session_id (a plugin cannot spoof it) and delivers the call to the worker as a fire-and-forget JSON-RPC notification: there is no reply and no return value. The worker does its work and re-pushes its UI state; the clicked control spins until the plugin’s UI revision moves, with a 15s timeout fallback. Actions are read-write-mode only and are not passphrase gated, so treat every method as reachable by anyone who can use the dashboard.

The native TUI renders panes read-only for now: it draws the text of every kind above (dropping icons, hrefs and tooltips, and stacking columns) but cannot fire an action, so action blocks appear as inert [action] <label> labels.

Composer action payload

A composer-action entry renders a host-owned button in the web dashboard ACP composer. The worker pushes it with ui.state.set:

{
  "label": "Dictate",
  "method": "dictation.start",
  "icon": "mic",
  "tooltip": "Start dictation",
  "tone": "info",
  "disabled": false
}

label and method are required. On click, the dashboard POSTs method to /api/plugins/{id}/action with the active session_id. When the plugin has composer.read, the forwarded params include:

{
  "composer": {
    "text": "current draft",
    "selection_start": 0,
    "selection_end": 5
  }
}

Without composer.read, the server strips that snapshot before forwarding the action to the worker.

To mutate the draft, include a draft_operation in the pushed payload. This requires composer.write.

{
  "label": "Dictate",
  "method": "dictation.start",
  "draft_operation": {
    "kind": "insert-text",
    "id": "transcript-1",
    "text": "Hello from dictation."
  }
}

kind is insert-text, replace-selection, or set-text. id must be stable and non-empty; the web dashboard applies each operation id once so a persistent UI-state entry cannot replay the edit on every poll.

Tool-card badge payload

A tool-card-badge entry attaches provenance pills to transcript tool-call cards. Declare one slot id per session and push a single entry whose items list carries every badge you want; the host matches each item to a card by its target. target.kind is mcp or skill and target.name is the raw MCP server name or skill name (matched exactly, not canonicalized), since an MCP server and a skill can share a name. Requires api_version >= 10.

{
  "items": [
    { "target": { "kind": "mcp", "name": "github" }, "text": "Company", "tone": "info", "icon": "building-2" },
    { "target": { "kind": "skill", "name": "deploy" }, "text": "Verified" }
  ]
}

Each item needs text or icon (a badge with neither renders nothing) and a non-empty target name; tone and tooltip are optional. Empty items: [] clears the plugin’s badges. Rendered in the web dashboard; the native TUI ignores this slot for now.

Status

Status segments the plugin contributes, consumed by the status surface. Requires api_version >= 4.

[[status]]
id = "pr_state"
label = "PR state"
Key Type Required Notes
id string yes Stable segment id.
label string no Human-readable text.

Themes

[[themes]]
name = "My Theme"
path = "themes/my-theme.toml"
Key Type Required Notes
name string yes Theme name in the picker. Must not collide with a builtin.
path string yes Theme TOML path, relative to the plugin directory.

Screenshots

Up to 8 marketplace screenshots, shown in the plugin detail view. Requires api_version >= 5.

[[screenshots]]
path = "assets/screenshots/overview.png"
alt = "The plugin's pane showing live status."
caption = "Live status in the pane."
Key Type Required Notes
path string yes Repository-relative image path. No URL scheme, no leading separator, no ..; must be PNG, JPEG, GIF, or WebP.
alt string yes Accessible description; non-empty.
caption string no Caption shown beneath the image.

Runtime

The worker the host spawns and supervises. Omit it for a static, metadata-only plugin. Two kinds.

Command

The host runs the build steps at install or update, then launches command.

[runtime]
kind = "command"
command = [".aoe-build/venv/bin/my-plugin-worker"]

[[runtime.build]]
command = ["python3", "-m", "venv", ".aoe-build/venv"]
platforms = ["linux", "macos"]
Key Type Required Notes
command array of string yes argv. Plugin-relative by default (must contain a path separator, never absolute) so the daemon’s PATH never decides whether the worker launches. With system = true it must instead be a bare program name resolved on PATH.
system bool no Resolve command[0] on the host PATH (for genuine system tools only). Defaults to false.
build array no Ordered build steps, run once at install or update inside the plugin directory, in the user’s interactive shell.

Build into .aoe-build/ (the host’s build-output directory); the host excludes it from the plugin tree hash, so a venv, node_modules, or target/ there does not break integrity verification.

Build step

Key Type Required Notes
command array of string yes argv, same resolution policy as the launch command.
platforms array of string no Restrict to OS names: linux, macos, windows. Empty runs on all.

Release binary

The host downloads a release asset instead of building from source.

[runtime]
kind = "release-binary"
asset = "my-plugin-${target}.tar.gz"
bin = "my-plugin-worker"
Key Type Required Notes
asset string yes Asset-name template; ${os}, ${arch}, ${target} are substituted before matching the release.
bin string no Executable path inside the extracted archive. Omit to run the downloaded asset directly (a raw, non-archive binary).