Per-harness plugins: each exposes the framework in the harness's own plugin API — registered agent tools and/or commands, display, and skills — by shelling the framework CLIs (agent-app for build/evolve, a2app for operate). The shared logic lives once in integration-starter/ (the engine); each plugin is a thin binding to a real harness API. Harnesses with no plugin use the universal ../skills/ route: the skills tell the agent to run agent-app <dir> open, and the CLI opens the app itself (harness-declared opener, else the OS browser, always printing the URL) — so a launched app reaches the user with no plugin code at all.
| Directory | Harness | Language / API | What it registers |
|---|---|---|---|
| integration-starter/ | — (shared engine + template) | TypeScript | The 17 build+operate tools, skills, display, runA2App, and registerHarnessProfile; every plugin below uses it |
| openclaw/ | OpenClaw | TypeScript, definePluginEntry; pnpm build stages a self-contained installable dist/ |
An "Agent Apps" manager tab (browser-style tabs per app: embedded app view, launch/pause/delete, a per-app session side panel, and a build form that starts the agent run directly) + /agent-app chat command + agent-app CLI + the six skills; no agent tools (skills + CLIs are the agent surface); ships openclaw.plugin.json |
| hermes/ | Hermes | Python, plugin.yaml + register(ctx) |
11 tools via ctx.register_tool(...) |
| dsh/ | deepseek-harness | TypeScript Cordis (peer deps cordis, @deepseek-ai/dsh-tools) |
14 tools via ctx.tools.register(defineTool(...)); a browser iframe renderer |
| craftbot/ | CraftBot | Python, @action decorator (agent_core) |
11 actions in the agent_app action set |
| claude-code/ | Claude Code | MCP (stdio JSON-RPC 2.0) | 14 tools via a real MCP server; register with claude mcp add |
| ../.claude-plugin/ | Claude Code | Plugin marketplace manifest (no code) | The /agent-app launcher command (../commands/) + the six ../skills/, installable with /plugin. No build step, so it works from a clean checkout; add the MCP server above for the tool surface |
Each plugin builds against its harness's SDK in that harness's toolchain (the TypeScript ones declare it as a peer dependency; the Python ones load inside the harness). None uses a shell — record field values reach the CLI as literal arguments. Two binaries (framework spec 5.1): set A2APP_CLI for the a2app operate client and AGENT_APP_CLI for the agent-app build/evolve binary. Each plugin routes a verb to its owner, so a build verb never reaches the operate client.
agent-app <dir> serve returns the app's URL; getting a human in front of it is a
harness capability, not a lifecycle step, so it is resolved in a fixed order:
- The agent's own browser tool — a harness that gave the model one (a built-in
browser pane, a Chrome extension). Unreachable from a child process, so the
agent passes
open --print-onlyand opens the URL with its own tool. - The harness plugin — a plugin implementing
registerDisplayembeds the app in its own UI.registerA2AppPlugindetects this and suppresses the CLI opener, so the user gets an embedded view rather than a stray browser window. AGENT_APP_OPEN_CMD— whitespace-separated argv the CLI runs with the URL appended (code --open-url,wslview, a script). Never shell-parsed.- The OS browser —
start/open/xdg-open, suppressed whenCIorSSH_CONNECTIONis set or Linux has no display (WSL excepted, it has an opener). - The printed URL — always, on every path above. This is the real floor: every harness renders a loopback URL as clickable text.
Only 3–5 are the CLI's; 1 and 2 belong to the caller. "opened": false is not an
error — the app is running and the URL is valid, which is what the caller asked.
The other direction, and the same shape: an app puts work on its queue, and
getting an agent to pick it up is a harness capability, not a protocol step. It
resolves deepest-first (agent-app <dir> bridge reports which rung a machine
lands on, and framework/cli/ documents the whole ladder):
- An inbound endpoint — the harness already serves an HTTP route that starts a run. Deepest, and rare; most harnesses have no such API.
- A headless CLI —
claude -p,codex exec,gemini -p,aider --message. Universal, and the default: the framework ships profiles for these, so a machine with one installed needs no plugin and no configuration at all. - A gateway — a local process the framework starts and then posts to.
- Polling — the harness cannot be triggered, so it runs
a2app <app> tasks next --wait 60000in a loop of its own and takes work that way. Nothing needs to reach into it. - Nothing — bi-directional operation is not supported on that machine, and the CLI says so rather than starting a service that delivers nothing.
A plugin's job here is one file. If its harness offers rung 1 or 3 — an
endpoint, or a gateway it can start — or a headless CLI the framework has no
built-in profile for, the plugin writes that route into
$A2APP_HOME/harnesses.json (~/.a2app/harnesses.json) when it loads, and
every app on the machine can use it. An entry replaces a same-id built-in
outright, so a plugin also uses this to correct a headless invocation whose flags
have changed:
Store the name of the variable holding a token, never the token — this file is hand-edited and pasted around. List every route the harness offers in any order; the ladder picks. A plugin whose harness already has a built-in profile and nothing deeper can do nothing here; any other harness is invisible to the bridge until its plugin registers it.
The file is the user's, so a plugin only ever adds its own profile, and only
when no profile with that id exists: it never overwrites an entry (a user's edit
survives), never touches another harness, never sets default, and never
rewrites a file it cannot parse. Which harness the bridge drives stays the
user's choice — bridge start --harness <id>, A2APP_HARNESS, or "default".
registerHarnessProfile implements
exactly that. What the plugins here register:
| Plugin | Profile | Route |
|---|---|---|
| claude-code/ | claude, replacing the built-in |
claude -p {prompt} --permission-mode dontAsk --allowedTools "Bash(a2app *)". The built-in passes no permission flags, and print mode denies every Bash call that would prompt, so the agent could not run a2app at all |
| openclaw/ | openclaw |
openclaw agent exec {prompt}: one embedded turn in the app's directory, alongside a running gateway |
| dsh/ | dsh |
dsh --profile headless {prompt} |
| pi/ | pi |
pi -p {prompt}, written by a Pi extension |
What a plugin must not do is start a bridge on a user's behalf. A bridge lets an app start agent runs, which is a capability a person grants once, knowingly, for an app they chose.
{ "version": 1, "harnesses": [ { "id": "myharness", "routes": [ { "mode": "inbound", "url": "http://127.0.0.1:7000/run", "health": "http://127.0.0.1:7000/up", "tokenEnv": "MYHARNESS_TOKEN" }, { "mode": "headless", "command": "myharness", "args": ["run", "--prompt", "{prompt}"] } ] } ] }