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; the host parses it strictly, so unknown keys are rejected and every key here maps to a schema field. For a guided introduction, see Writing Plugins.

Versioning

A manifest carries two independent version axes.

Key Meaning
api_version The manifest schema version. The current schema is 14. The host rejects a manifest whose api_version is newer than it supports.
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.

Each key below notes the api_version it needs. Target the newest schema your plugin uses, and set aoe_version to the host range you have tested.

Top-level fields

id = "dev.example.my-plugin"
name = "My Plugin"
version = "0.1.0"
api_version = 13
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 14.
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, so an update that widens them must be re-approved. Declare only what the worker uses; static contributions (commands, keybinds, themes, ui, status) need none.

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.

A command action is a client-executed action instead of a worker call. The only kind is open-ui-link, which opens the href from the plugin’s own (slot, id) UI-state entry in the browser, with no worker round-trip; that pair must match a declared [[ui]] entry on a per-session slot.

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

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)"
type = "integer"
default = 120
min = 0
max = 86400
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).
string_list Freeform add/remove list of user-typed strings; no closed option set. Top-level setting or object-list item field (api_version >= 14).

Dynamic selects (api_version >= 9)

A dynamic_select’s options are resolved by the host at render time, so the plugin never ships a 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 values parameterize the source, which acp.models and acp.modes require. When that agent’s catalog has never been discovered, resolving them runs a one-shot handshake probe (see acp.capabilities.probe), so the picker self-fills on first open. Saved ids are advisory: the host revalidates at session creation, so a model that later disappears surfaces as an error then rather than silently at save.

Object lists (api_version >= 9)

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

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

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

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

An item field takes the same keys as a top-level setting (key, label, description, type, options, min, max, default, multiline, option_source, depends_on) plus required. It may be a dynamic_multi_select (api_version >= 11), whose stored value is an array of the chosen option values, or a string_list (api_version >= 14), whose stored value is an array of freeform user-typed strings.

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 (the trust-checked primary repo) and extra_project_paths (the other repos of a multi-repo session). Omitting project_path creates a scratch session: a throwaway directory with no repository, so extras alongside it are refused. Every path is canonicalized and existence-checked host-side, fail-closed and capped per call.

Sandbox (api_version >= 11). sandbox: true runs the session in the host’s container sandbox, on the host’s own configured image. It only narrows what the agent can reach, so it needs no grant beyond session.create. The create fails synchronously when no runtime is installed, but the container starts asynchronously, so image-pull problems surface on the session later.

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

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

Ownership. sessions.turn.send reaches only a session the calling plugin created.

Busy sessions. A turn aimed at an agent already running a non-steerable turn (or cancelling, or compacting) is refused with a retryable agent_busy rather than dropped. A stopped or dormant session is not busy: the host wakes it the way a user prompt does, closes any turn the previous worker left open, resumes the worker, and waits. An archived or trashed session is never woken; the turn fails with session_archived or session_trashed.

Idempotency. sessions.create takes a plugin-scoped idempotency_key: retrying with the same key and payload returns the existing session (created: false), while a different payload under that key is a conflict.

Limits. Per plugin: 20 creates per hour, 5 active plugin-created sessions, 120 turns per hour, reported as rate_limited or concurrency_limited. Disabling the plugin stops all of its automation.

Settings-change events. After a settings write the host notifies the worker with plugin.settings.changed carrying { revision, changed_keys }; the worker re-reads those values with config.get, whose response carries the current revision. Polling that method is the fallback for a worker that was down.

Plugin storage

A worker has a host-backed private key/value store, namespaced by its plugin id, that survives daemon and worker restarts (unlike the install directory, which an upgrade can replace). It needs no capability beyond runtime.worker, since 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.
composer-action per-session A button beside the ACP composer controls (requires api_version >= 8).
notification n/a A transient notification pushed via ui.notify; gated by the notifications capability, not a slot declaration.

Badge payload

status-bar, row-badge and detail-badge take either one badge, { text?, icon?, tone?, href?, tooltip? } (status-bar and detail-badge require text or items), or an items list of such badges that replaces the top-level fields. items: [] clears the badge.

Give items a shared group to collapse them into one chip that shows one item at a time. Clicking or tapping it advances to the next item and wraps around, for example a usage badge cycling 5h, 7d and opus values:

{ "items": [
  { "text": "5h 40%", "group": "usage" },
  { "text": "7d 12%", "group": "usage" },
  { "text": "stale", "tone": "warn" }
] }

Items without a group stay separate chips, and each distinct group cycles independently. The position is kept per browser tab and is never sent to the worker. A cycling chip ignores href; a group with a single item renders as a normal chip. The TUI cannot click and shows the first item with text.

Pane payload

A pane entry renders a dockable tool-window, pushed 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 payload is capped at 64 KiB. Everything but blocks is validated strictly; blocks is opaque JSON, and each surface renders the kinds it knows and drops the rest. That forward-compatibility contract cuts both ways: a new kind needs no host change, but an older host renders nothing for it, so a pane that depends on a newer kind should say so with api_version and aoe_version.

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.

An href renders as a link only when it is an http(s) URL or a path starting with a single / and containing no backslash, tab or line break. A link to a dashboard route navigates in place; any other link opens in a new tab, including a path that normalizes to //host such as /..//evil.com, which opens on the dashboard’s own origin.

row lays out at most two lines: prefix (mono, tone-tinted) and label lead the first with value pinned right; sublabel leads the second with badges ({ text?, icon?, tone?, tooltip? }) pinned right. value_tone colors the trailing token independently of the row, and mono monospaces the row’s text. A method makes the row body a button firing that worker method, and an href alongside it becomes a separate trailing link-out; with href alone the whole row is the link. selected marks the row as the pane’s current subject.

section groups children, with a right-pinned value summary or badges in its header. boxed draws a bordered card, scroll caps the body height so a long list scrolls inside the section, and collapsible folds it via a native <details> (collapsed sets the initial state).

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

bar stacks segments ({ value, tone?, color?, label? }) proportionally; segments without a positive value are dropped and a bar left with nothing renders nothing. sparkline plots values (oldest first) as a history line, with max fixing the top of the scale so a series does not auto-scale each refresh and bands ({ at, tone } thresholds) recoloring each sample by the highest band it reaches. Both take a caption beneath.

columns lays its children out in equal fractions, and a single child spans the full width, so eliding one card collapses the row cleanly.

action forwards method to the worker (see Pane actions); with href and no method it is a link-out button. disabled renders it inert and non-navigating, and 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 as a fire-and-forget JSON-RPC notification: no reply, no return value. The worker does its work and re-pushes its UI state, and the clicked control spins until the plugin’s UI revision moves, with a 15s timeout. 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 TUI renders panes read-only: it draws the text of every kind (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 dashboard’s ACP composer, pushed with ui.state.set. label and method are required; icon, tooltip, tone, and disabled are optional.

{ "label": "Dictate", "method": "dictation.start", "icon": "mic" }

On click the dashboard POSTs method to /api/plugins/{id}/action with the active session_id. With composer.read the forwarded params also carry { "composer": { "text", "selection_start", "selection_end" } }, a click-scoped snapshot of the draft; without it the server strips that snapshot before forwarding.

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

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

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

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, in one of two kinds. Omit it for a static, metadata-only plugin.

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