Writing Plugins

This guide takes you from nothing to an installed, running Agent of Empires plugin. For the full manifest schema see the Plugin API Reference; for the architecture and security model see Plugin System Internals. For installing and managing plugins as a user, see Plugins.

A plugin is a directory with an aoe-plugin.toml manifest and, optionally, a worker: an executable the host spawns that speaks JSON-RPC 2.0 over newline-delimited JSON on stdio, in any language. The host does not link your code; the manifest is the contract.

Scaffold from the template

The official starter generates a complete plugin (manifest, worker, tests, CI) in Python, Node, or Rust:

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

Pick a runtime when prompted. The generated project builds, passes its tests, and answers a status command out of the box. The rest of this guide explains what it generated.

The manifest

Every plugin declares identity, what it contributes, and (if it has a worker) how to build and launch it:

id = "dev.example.my-plugin"
name = "My Plugin"
version = "0.1.0"
api_version = 6
aoe_version = ">=1.11.0, <2.0.0"
description = "What the plugin does."

capabilities = ["runtime.worker"]

[[commands]]
id = "status"
title = "My Plugin: status"
description = "Show the status summary."

[[settings]]
key = "enabled"
label = "Enable My Plugin"
type = "boolean"
default = true

[[ui]]
slot = "pane"
id = "my_plugin_pane"

Pick an id outside the reserved aoe.* and agent-of-empires.* namespaces. Set api_version to the schema version you target (currently 6) and aoe_version to the host range you have tested against. Every key is documented in the Plugin API Reference.

Capabilities

A worker requests only the runtime grants it uses. runtime.worker is required to run any code; add net, session.read, notifications, and so on as needed. Static contributions (commands, keybinds, themes, ui, status) need no capability. The user is prompted to grant the exact declared set at install, and the grant is pinned to the manifest hash, so an update that widens capabilities must be re-approved. Keep the list honest and minimal.

The worker

The host spawns the worker, sends one JSON-RPC request per line on stdin, and reads one response per line on stdout. The worker exits when stdin reaches EOF.

A request, and the response your status handler returns:

{"jsonrpc": "2.0", "id": 1, "method": "my-plugin.status", "params": {}}
{"jsonrpc": "2.0", "id": 1, "result": {"ok": true, "message": "running"}}

The host maps a command id to a fully namespaced method, plugin.<id>.<command-id>, so the example above is abbreviated: a worker for dev.example.my-plugin actually receives plugin.dev.example.my-plugin.status. Dispatch on the trailing segment of method so either form works. Return a JSON-RPC error with code -32601 for an unknown method. A message with no id is a notification; do not respond to it.

Build and launch

The worker entrypoint must be plugin-relative, never resolved on the daemon’s PATH. Build into .aoe-build/, which the host excludes from the plugin’s integrity hash, then point command at the built artifact:

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

[[runtime.build]]
command = ["python3", "-m", "venv", ".aoe-build/venv"]
platforms = ["linux", "macos"]

[[runtime.build]]
command = [".aoe-build/venv/bin/pip", "install", "."]
platforms = ["linux", "macos"]

Build steps run once, at install and update, in the user’s interactive shell (where PATH is reliable). A compiled plugin can instead ship a release asset with kind = "release-binary"; see the reference.

Install and test locally

aoe plugin install ./my-plugin     # runs the build steps, prompts for grants
aoe plugin list
aoe plugin update my-plugin         # re-runs build, re-approves changed grants
aoe plugin uninstall my-plugin

Drive the worker by hand before installing, to confirm the protocol:

echo '{"jsonrpc":"2.0","id":1,"method":"my-plugin.status","params":{}}' | <your-worker>

The starter ships a worker-contract test (it spawns the worker, sends a request, and asserts the response) plus its CI. Keep that test green; it is the cheapest guard on the protocol.

Publish

Push a vX.Y.Z tag to cut a GitHub release (the starter’s release workflow does the rest). Users install the latest release with aoe plugin install gh:your-org/my-plugin. To be listed in the Agent of Empires featured index, which lets a plugin claim a verified namespace, open a PR adding your release’s source tree hash to the featured index in the main repository.