How the harness drives an ACP agent

The Agent Client Protocol (ACP) is the JSON-RPC-over-stdio protocol Zed defined for editor ↔ agent conversations, and it is what most coding agents now expose as their embeddable face — the registry lists forty-odd. T3 Code drives Cursor, Grok Build and Antigravity through it; MeshWeaver drives those three and OpenCode through ONE harness implementation (MeshWeaver.AI.Acp), one IHarness per agent. This page is the record of the shape, why it is shaped that way, and what was measured on 2026-09-12 against Cursor CLI 2026.09.08, Grok 1.0.30, OpenCode 1.18.30 and Antigravity ACP server 1.1.1 (all protocol version 1).

The conversation

initialize {protocolVersion: 1, clientCapabilities: {fs: {readTextFile: false, writeTextFile: false}, terminal: false}, clientInfo}
[authenticate {methodId}]                    — only when a stored key must be announced (Antigravity's gemini-api-key)
session/resume {sessionId, cwd, mcpServers}  — when the agent advertises sessionCapabilities.resume
  | session/load {sessionId, …}              — else, when it advertises loadSession (replays history first — dropped)
  | session/new {cwd, mcpServers}            — no session yet, or the resume was refused
session/prompt {sessionId, prompt: [{type: text, text}]}
  … session/update {update: {sessionUpdate: agent_message_chunk | agent_thought_chunk | tool_call |
                    tool_call_update | usage_update | available_commands_update | current_mode_update | …}}
  … session/request_permission {toolCall: {kind, …}, options: [{optionId, kind: allow_once|allow_always|reject_once|reject_always}]}
→ {stopReason: end_turn | max_tokens | max_turn_requests | refusal | cancelled, usage?}

The client declares no file-system and no terminal capability: the agents' own tools do that work inside the agent process; the mesh — not the portal's disk — is the workspace, reached through the MCP server handed to every session (mcpServers: [{name: "meshweaver", type: "http", url, headers: [{name: "Authorization", value: "Bearer …"}]}]), which acts as the user under the user's own bearer token. The session's cwd is the shared skills workspace, whose AGENTS.md every one of these agents reads natively.

One agent session per mesh thread

The agent mints the session id on session/new; the harness records it under the user's agent home (meshweaver/sessions/<sha256 prefix>.txt, beside the agent's own session store) and resumes it every later round with only the new message. Every probed agent advertises loadSession; Grok, OpenCode and Antigravity also advertise sessionCapabilities.resume, which — unlike a load — does not replay the history. A refused resume forgets the record and starts a fresh session in the same round, handed the mesh thread's earlier turns as a bounded transcript and the agent's instructions at the head of the first prompt (ACP has no system-prompt seam).

The updates the reader understands

Recorded from OpenCode's free model; the reader (AcpTurn) takes exactly these and ignores the rest:

Update What the harness does with it
agent_message_chunk The streamed text, chunk by chunk.
agent_thought_chunk Reasoning content, streamed.
tool_call A FunctionCallContent — the agent's title as the name when it is an identifier (bash, read_file), else its kind; rawInput as the arguments.
tool_call_update with status completed / failed The FunctionResultContent: the content parts' text (a diff part as a patch header), else rawOutput; failed becomes the fault. A completion for a call never announced synthesizes the call first; a call announced already finished yields both.
usage_update, available_commands_update, current_mode_update Recorded (context usage, the agent's slash-commands, its mode); nothing yielded.
the prompt response The stop reason, and the usage as a UsageContent when the agent reports one.
anything else Nothing. A plan, a user_message_chunk replayed by a load, an agent's _meta extension notification (_x.ai/mcp/servers_updated), a non-JSON line, whatever a newer agent adds — none may fault the round.

session/request_permission is answered in-line, so the agent never waits on a human that is not there: the first allow_once option (else any allow) when every request is auto-approved or the tool's kind is on the allowed list — read, search, fetch, think, other by default — otherwise the first reject_once (else any reject); cancelled when the agent offered no usable option.

A headless way in for every agent

Every probed agent answers initialize without a login and refuses session/new with the protocol's AUTH_REQUIRED (-32000 Authentication required) until it has one. The registry's authentication rules know two kinds — agent auth, where the agent opens a browser on the machine it runs on, and terminal auth, an interactive TUI — and neither works on a headless portal pod. So each agent needs a way in that does:

Agent Headless login Machine-only login
Cursor CURSOR_API_KEY (a pasted key) agent login (browser)
Grok Build grok login --device-auth (a device code, scraped and awaited) or XAI_API_KEY grok login --oauth
OpenCode none needed on its free Zen models; OPENCODE_API_KEY for the paid ones opencode auth login (TUI, other providers' keys)
Google Antigravity GEMINI_API_KEY + authenticate {methodId: gemini-api-key} Google login (browser)

A pasted key is stored as the harness's credential node and passed through the agent's key variable on every round (the portal's own copy of every such variable is cleared from the child environment first). A device-code login is the CLI's own process under the user's home; once it exits 0 the harness writes its marker (meshweaver/login.json) so the login-status probe and the round's pre-flight check answer without spawning the agent. On the local mesh — the machine the user works on — there is no per-user root, the agent runs under the user's own home, and the agent's own browser login is simply used.

Per-user isolation is the HOME override

Every ACP agent keeps its login and its sessions under $HOME (or the XDG directories beneath it) — there is no CLAUDE_CONFIG_DIR / CODEX_HOME equivalent common to them — so each user runs each agent under HOME = {ConfigDirRoot}/{userId}/{agent} (with XDG_*_HOME pointed beneath it), the way T3 Code isolates Antigravity's profile. The root is the same users volume as Claude Code and Codex.

Where it lives and how it is tested

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.