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
src/MeshWeaver.AI.Acp/AcpAgentDefinition.cs— what differs per agent, and the four built-in definitions;AcpInvocation.cs— the spawn and the parameter shapes, pure;AcpTurn.cs— the reader, pure;AcpClient.cs— the process and the JSON-RPC conversation (the one async leaf);AcpChatClient.cs— the reactive composition;AcpSessionMap.cs— the session record;AcpConnectStrategy.cs— the device-code login;AcpHarness.cs— oneIHarnessper definition.src/MeshWeaver.AI.Acp.Test— the definitions, the spawn and the reader against recorded updates, and the process path executed against the fake agent (MeshWeaver.AI.Test.FakeCli,FAKE_CLI_MODE=acp): streaming, tool calls and results, resume with the fresh-session fallback and the record, the instructions + transcript hand-off, noise,AUTH_REQUIRED, the key pass-through and the announced key, permissions by policy, the idle bound, the truncated stream.AcpChatClientE2ETestruns a real agent on demand (ACP_E2E=OpenCodeneeds no login).