Harness interop — protocol seams, not vendor adapters
Ozzy Dev orchestrates harnesses; it does not marry one. Two protocol seams — ACP outward to engines, MCP inward to tools — admit every current and future agent that speaks them, so no proprietary vendor ever becomes load-bearing.
Landed. ADR-013 Phase 3 shipped a single AcpEngineAdapter on the UNCHANGED EngineAdapter seam. Because the same seam now hosts two transport families (cli.* and acp.*), the seam is proven generic at N=2 — the empirical bar this page committed to in v0.1 for calling a seam
generic rather than an alias. acp.auggie is LIVE-verified against Auggie 0.24.0
(worktree containment, router-layer diff capture, clean shutdown, honest cost/model
tombstones); acp.gemini-cli is wired + fixture-replay-verified, live run gated on
Google auth. Config version: harness-router@0.0.3-phase3.
1 · The principle and why it exists
AGENTS.md §2 is verbatim-in-spirit the rule the console enforces: a proprietary tool’s name must never appear in a type union, a config enum, a registry key, an
adapter class name, or a benchmark arm id. Vendors are instances configured behind a
generic contract — an acp engine adapter, never an auggie adapter; an external-docs expert, never a Context7Expert. The vendor’s endpoint,
auth env var and model live in config ({ command, args, env, cwd } in registry entries),
never in the type system. A vendor name IS allowed as data: an adapter-id slug (acp.auggie), a benchmark arm label, a config value. Naming what you measured is honest reporting; naming
a seam after what you measured is lock-in. Closed-source, server-side, credit-priced
dependencies are things we benchmark and learn from; they never become load-bearing.
The generic seam costs nothing at design time and is unbounded to retrofit later.
The enforcement test we adopted is the one the code cannot fake: a seam is not proven generic until a SECOND instance runs through it. One vendor behind a generic interface is an alias. Two vendors is a contract. As of ADR-013 Phase
3 the EngineAdapter seam clears that bar for transport families, not just vendor instances — cli.* and acp.* share one seam without a single type-level branch.
An earlier draft of our own benchmark plan proposed an AuggieExpert class and
widening ExpertId with a 'auggie' literal. The audit found zero coupling in
the shipped code — the drift was in the plan, not the codebase. The plan was
rewritten around an external-retriever expert configured by endpoint /
auth-env-var / model. The rule above exists so that near-miss cannot become a hit — and Phase
3 shipped AcpEngineAdapter, not AuggieEngineAdapter, because of it.
2 · The two seams, and where the boundary sits
ACP is the engine seam — we drive an agent. MCP is the tool seam — an agent consumes our tools. They stack: a
client (Zed, or our harness) speaks ACP outward to an agent; the agent internally speaks MCP
inward to its tool servers. session/new even accepts an mcpServers[] array so the client can inject MCP endpoints into the agent for a session. An open RFD (“MCP-over-ACP”)
proposes tunneling MCP frames through ACP channels — reinforcing that they are complementary layers,
not rivals.
Protocol mechanics (v1, exact)
- Wire — JSON-RPC 2.0 with two message types (methods and notifications). camelCase JSON keys; snake_case for discriminator string values.
- Transport (stable) — stdin / stdout NDJSON, UTF-8; the client spawns the
agent as a subprocess; no sockets, no daemons. LSP-style header framing is NOT required; SDK examples use plain NDJSON.
- Remote transport — documented as work in progress; a Transports Working Group is stabilizing streamable HTTP + WebSocket per an RFD.
- Capability negotiation — driven by the
initializeresponse:agentCapabilities,clientCapabilities,authMethods,agentInfo, negotiatedprotocolVersion(uint16, bumps only on breaking). - Extensibility —
_metafield on every object; custom methods must be underscore-prefixed; custom capabilities advertised atinitialize.
Method surface (baseline, capability-gated where noted)
- initialize — negotiate protocolVersion (uint16, bumps only on breaking), exchange clientCapabilities / agentCapabilities, receive authMethods.
- authenticate — invoked with an authMethodId returned by initialize; env-var-authed agents skip this entirely.
- session/new — required; takes an absolute cwd, optional mcpServers[], optional additionalDirectories[]; returns sessionId, optional configOptions, optional modes.
- session/prompt — the core turn; prompt is ContentBlock[] gated by PromptCapabilities; returns a stopReason.
- session/update — streaming notification channel: plan, agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update, current_mode_update, usage_update, available_commands_update, session_info_update, next_edit_suggestion.
- session/request_permission — agent → client; the client owns dangerous ops.
- session/cancel — notification; agent must respond in-flight with stopReason: "cancelled".
- fs/read_text_file, fs/write_text_file, terminal/* — client-side, capability-gated.
- elicitation/create + elicitation/complete — form or url mode.
- session/load, session/resume, session/list, session/close, session/delete, session/set_mode, session/set_config_option — all capability-gated.
ACP vs MCP — axis by axis
- Direction — ACP: Editor / client ↔ agent MCP: Agent ↔ tools / resources
- Trust owner — ACP: Client owns dangerous ops (fs, terminal, permissions) MCP: Server exposes ops; the LLM decides
- Multi-turn sessions — ACP: First-class (session/*) MCP: Not modeled — sessions are the host’s concern
- Permissions — ACP: First-class (session/request_permission) MCP: Not modeled
- Diffs — ACP: First-class (diff content variant on tool_call.content[]) MCP: Not modeled
- Cost / usage — ACP: First-class (usage_update with ISO-4217 cost) MCP: Not modeled
- Streaming — ACP: session/update notifications MCP: Notifications + SSE / streamable HTTP
Where ACP stops: it does not carry agent-to-agent handoff, cloud auth flows
(only an authenticate shim + URL elicitation), sandbox provisioning, or
model-provider routing. Sub-agent orchestration is out of scope (the proxy-chain RFD is early).
Our orchestrator / subagent model runs outside ACP.
3 · ACP ecosystem inventory
- Governance — Apache-2.0, created by Zed Industries (repo 2025-06-23), lead maintainer Sergey Ignatov (JetBrains); cross-vendor stewardship, public RFD process. NOT foundation-donated (contrast MCP → Linux Foundation).
- Spec maturity — v1 is the stable protocol; Rust and TypeScript SDKs at 1.0.0. v2 is published as DRAFT with breaking restructuring of the prompt lifecycle, permission requests, tool-call updates, plus first-class diff-file-states and next-edit-suggestions.
- Native clients — Zed made ACP the headline of its 1.0 release (2026-04-29); JetBrains built it into IntelliJ / PyCharm / GoLand / WebStorm / RubyMine / PhpStorm / RustRover (since Dec 2025) and ships the auth-verified registry. Devin Desktop and AWS Kiro also native.
- First-party agents (upstream, not bridges) — Gemini CLI (gemini --acp, Google, Apache-2.0), Auggie (auggie --acp, Augment Code, closed core + registry shim), GitHub Copilot CLI (Microsoft / GitHub, closed, public preview Jan 2026), Cursor (closed, March 2026), OpenCode (sst, MIT), Goose (Block, Apache-2.0), Qwen Code (Alibaba), Kimi CLI (Moonshot), Kiro CLI (AWS), Junie (JetBrains), Mistral Vibe, plus 20+ more in the curated registry.
- Community bridges (not upstream at the vendor) — zed-industries/claude-agent-acp wraps the Claude Agent SDK; zed-industries/codex-acp wraps OpenAI Codex CLI. Native-vs-bridge is the distinction that matters when validating the seam.
- Registry — agentclientprotocol/registry (Apache-2.0) auto-updates hourly from npm / PyPI / GitHub Releases; live JSON at cdn.agentclientprotocol.com/registry/v1/latest/registry.json. 38+ auth-verified agents at time of writing.
- Structural weakness — Microsoft / VS Code have not adopted ACP first-party; a community extension (formulahendry/vscode-acp) fills the gap. GitHub Copilot CLI shipping ACP is Microsoft-adjacent but does not imply VS Code proper will follow.
4 · Proving the seam — the instance ladder
The seam was proven by running the same generic acp engine adapter against agents chosen
for orthogonal auth, capability and provenance profiles. Status below is as of ADR-013 Phase 3.
- Instance 1 — acp.auggie (LIVE-verified) Auggie 0.24.0 driven end-to-end through the generic acp adapter under the harness router: worktree-scoped containment (path escapes rejected loudly), router-layer diff capture, clean shutdown on stopReason, honest cost/model TOMBSTONES because Auggie 0.24.0 emits no usage_update — recorded as tombstones rather than guessed. Exercises the auth-via-elicitation path and Auggie-specific sessionModes (architect / code / ask).
- Instance 2 — acp.gemini-cli (fixture-replay only) Google first-party, Apache-2.0; the ACP TypeScript SDK docs cite it as THE reference production implementation. Adapter is wired and passes fixture-replay against captured session/prompt turns; a live run is gated on Google auth (GOOGLE_API_KEY env-var auth path, no authenticate roundtrip). When it fires end-to-end it will exercise a genuinely different slice of the seam — no authenticate step, PromptCapabilities.image = true, its own sessionModes set — and demonstrate ACP compliance rather than Auggie compliance.
- Instance 3 (queued) — Claude Agent via zed-industries/claude-agent-acp A community-maintained bridge, not upstream Anthropic — which is exactly why it is third, not second (using a bridge as the primary proof would confuse "the seam works" with "the Zed bridge works"). Still the single most important agent our users will want, exercises the third auth style (Anthropic OAuth via URL elicitation, distinct from both Auggie and Gemini), and regression-tests the shared Zed-SDK bridge code path the whole community depends on.
- Explicit non-recommendations Codex CLI (zed-industries/codex-acp) — same bridge code path as Claude Agent, low diversity value. Copilot CLI — closed-source, opaque failures make debugging our own seam painful. OpenCode / Goose — excellent OSS but similar capability profile to Auggie, low differentiation as the "prove generic" instance.
The landed adapter — what shipped in Phase 3
The load-bearing architectural result of Phase 3 is not a new capability — it is the ABSENCE of
one. The EngineAdapter seam did not change to host ACP. A single new adapter class on
the existing seam was sufficient, which is the operational meaning of “the seam is generic.”
- One adapter class — AcpEngineAdapter — implementing the SAME EngineAdapter interface the cli.* adapters implement. Zero new seam types; zero new vendor identifiers in the type system. The seam absorbed a second transport family without changing shape.
- Adapter id refined from a bare "acp" literal to "acp.<slug>" (acp.auggie, acp.gemini-cli). Vendor identity is a slug on a config row, never a class name or type-union member. The test is unchanged from AGENTS.md §2: deleting a vendor requires a config change, not a code change.
- Wire: JSON-RPC 2.0 over NEWLINE-DELIMITED stdio (NDJSON). NOT LSP-style Content-Length framing — anyone extending this will get it wrong by default. Parser resyncs on newline; stray non-JSON stdout is logged loudly, never crashes the loop.
- Config version: harness-router@0.0.3-phase3. config_version is receipts-visible so every run records the exact adapter contract that produced it.
- Registry membership (accuracy): the harness MCP registry currently lists cli.claude-code and acp.auggie ONLY. acp.gemini-cli is wired and fixture-replay-verified but NOT registered — it enters the registry when the live run gate (Google auth) is passed.
Headless AcpDriver — the client half we own
ACP puts the client in charge of dangerous operations. Our driver is headless — no editor UI to delegate to — so every client-side responsibility is discharged programmatically, and each discharge is classified rather than heuristic.
- Permission prompts (session/request_permission) — auto-answered with allow_once. Headless drivers cannot show a UI; the router honours the request through containment discipline instead of interactive consent. No promptable, no interactive fallback.
- Filesystem — fs/read_text_file and fs/write_text_file honoured ONLY inside the session cwd (worktree containment). A path escape (../, absolute path outside cwd, symlink target outside cwd) is REJECTED LOUDLY as a classified failure — never silently declined and never partially served.
- Failure classification — timeout, spawn failure, JSON-RPC error, and stopReason ≠ end_turn are recorded as DISTINCT failure classes on the receipt. Bucketing them into one "engine error" would erase the signal the router uses to tune per-arm timeouts and retry policy (retries are absent; a failure is a failure).
- Cost / model reporting — usage_update is optional in the protocol and absent in some agent builds (Auggie 0.24.0 emits none). The driver records cost and model as TOMBSTONES ("no usage_update observed") rather than guessing or averaging — honest missing beats a fabricated number.
- Shutdown — session/cancel followed by process close on stopReason; a hung agent is killed at the budget deadline and the kill is receipted. No lingering child processes.
5 · The inversion pattern — the architecturally correct integration
The direction most vendor-integration writeups reach for — ozzydev absorbs the vendor —
has the roles inverted. The architecturally correct integration is the mirror image: the vendor’s harness becomes a client of OUR MCPs. An MCP-client harness pointed at ozzydev-search / ozzydev-write inherits our D8 rubric, our verify
ladder, our rollback and our signed receipts. It requires zero ozzydev code — only
configuration.
It is also the only real test of the claim in our own AGENTS.md that the stack is harness-agnostic — a claim that to date has been exercised only by Kilo and Cursor. Adding a third and fourth MCP-client harness (Zed with our MCPs registered; Claude Code with the same) is the workspace-level counterpart to the ACP instance ladder.
A write-enabled external harness needs OS-level containment — a dedicated
worktree plus a sandbox or container — because a harness’s own workspace flag is not a
filesystem boundary. A read-only harness consuming ozzydev-search is safe today with no extra containment; that is the correct starting
point for the second-harness proof.
6 · Adapter guidance and sharp edges
The mitigation rule is one sentence: branch on capabilities from the initialize response; never hardcode a vendor’s shape. Everything below is a corollary.
- NDJSON framing quirks — some agents (older Gemini CLI builds, certain Auggie builds) emit stray non-JSON warnings to stdout interleaved with JSON-RPC frames. Parser resyncs on newline, logs the noise loudly, does not crash.
- Capability drift — PromptCapabilities differ per agent (image support: Claude yes, some CLIs no). Adapter degrades content blocks based on the initialize response, never on hardcoded assumptions about a specific vendor.
- cwd is absolute — every session-creating method requires an absolute cwd; worktree and Windows paths need care.
- session/load replay — agents that advertise loadSession re-stream the entire history through session/update on load; do not double-render.
- Cancellation exception leaks — API SDKs inside the agent may throw on abort and propagate as JSON-RPC errors instead of stopReason: "cancelled". Treat "error after cancel-in-flight" as cancelled.
- Auth passthrough — env-var-based auth is the common case (ANTHROPIC_API_KEY, GOOGLE_API_KEY, OPENAI_API_KEY); some agents use authMethods with URL elicitation. Never persist tokens in the registry entry; source from the console Secrets vault (ADR-006).
- Unstable sessionModes ids — capability advertised but mode ids drift across vendor releases; render capability-driven UI, do not enumerate against a hardcoded list.
- Optional usage_update cost fields — some agents report tokens only with no cost fields; the receipt schema treats cost as optional.
- Remote transport in flight — Transports Working Group is stabilizing streamable HTTP + WebSocket per an RFD; today’s local stdio transport is the only stable one. Cloud-hosted agents cannot yet speak ACP over a supported transport.
- MCP-over-ACP RFD — proposes tunneling MCP transport frames through ACP channels, reinforcing that the two seams stack, not compete. session/new already accepts mcpServers[] so the client can inject MCP endpoints into the agent for a session.
Conformance state
- No dedicated protocol-compliance-suite repo exists yet — filing one would be a clean upstream contribution.
- The de-facto conformance mechanisms are: the registry CI (which spawns each agent, runs initialize, validates authMethods), the .protocol-matrix/ capability matrix in the registry repo, running the agent inside a Zed dev build, and the runnable reference agent + client in agentclientprotocol/typescript-sdk src/examples/.
- Our own smoke gate (steps 1–4 are the "generic seam works" minimum, 5–7 are the capability differentiators): (1) spawn + initialize + protocolVersion assertion; (2) session/new with temp cwd + sessionId assertion; (3) trivial session/prompt receives ≥1 agent_message_chunk and a stopReason within N seconds; (4) mid-turn session/cancel yields stopReason: "cancelled" (never an error); (5) fs.readTextFile advertisement routes read requests to us; (6) agentCapabilities matrix matches our registry declaration (drift detector); (7) graceful shutdown ≤ 5s.
7 · Risks and open questions
- v2 draft breaking changes (headline risk) — restructures prompt lifecycle, permission requests, tool-call updates; agents will upgrade at different rates. Mitigation: target v1 explicitly, parse agentCapabilities.protocolVersion from initialize, gate v2 code paths on capability rather than fork the adapter.
- Microsoft / VS Code non-adoption — if Microsoft ships a competing protocol (VS Code Agent APIs are already extensible in-house), the ecosystem could fragment. Mitigation: the acp engine adapter is one of many under the generic Engine seam; a future vscode-agent adapter is a bounded cost.
- Governance risk — not foundation-donated; still Zed-steered with a JetBrains lead maintainer. Mitigation: Apache-2.0 makes a fork always possible; monitor RFDs for narrow-vendor changes.
- Capability drift across vendors — sessionModes ids unstable, usage_update cost optional, stdio noise. Mitigation: capability-driven UI + loud-classify unknowns; never hardcode a vendor shape.
- Sub-agent / delegation not modeled — the "Agent Extensions via ACP Proxies" RFD is early. Our orchestrator/subagent model runs outside ACP today. Mitigation: keep the subagent contract independent of ACP.
- Registry auth-lock-in — the curated registry only accepts agents that expose authMethods; some env-var-authed OSS agents are not listed but can still be run manually. Mitigation: support both registry-driven and manual configuration; do not gate on registry membership.
- Open question — which conformance target we validate against: our own smoke gate (steps 1–7), the ACP TS SDK reference client, or a Zed dev-build harness. Recommendation: our own gate as CI, TS SDK client as unit fixture, Zed dev-build as manual pre-release check.
References
_WORKSPACE-DOCS/RESEARCH-ACP-2026-07-28.md— the full ACP research: protocol reference, implementations inventory, ACP-vs-MCP boundary, adapter guidance, risks (24 primary and secondary sources).- agentclientprotocol.com — spec home;
/protocol/v1/overview.md,/protocol/v1/schema.md,/protocol/v1/prompt-turn.md,/get-started/agents.md. - agentclientprotocol/agent-client-protocol (Apache-2.0, spec repo) · agentclientprotocol/registry · registry.json live feed.
- Zed — Bring your own agent · JetBrains ACP hub · JetBrains — ACP agent registry.
- Gemini CLI — ACP mode · zed-industries/claude-agent-acp · zed-industries/codex-acp.
- AGENTS.md §2 (integrate protocols, not vendors) · AGENTS.md §4a (concurrent orchestrators) · ADR-007 (search MCP) · ADR-008 (write MCP) · ADR-009 (signed receipts) · ADR-013 Phase 3 (acp EngineAdapter — the landed adapter on the unchanged seam).