contenox
Browse docs/

The ACP client library and its verification harnesses

libacp implements the Agent Client Protocol (ACP) v1 for both roles of the conversation:

  • Agent sidelibacp.AgentSideConnection serving a libacp.Agent. This is the upward direction: runtime/acpsvc implements the production agent that editors (Zed, beam, the VS Code bridge) drive via contenox acp.
  • Client sidelibacp.ClientSideConnection driving a libacp.Client. This is the downward direction: contenox itself opening sessions on other ACP agents (see the blueprint in blueprints/acp/acp-client-engine.md). The client exposes every agent-bound method (Initialize, NewSession, Prompt, CancelPrompt, session config options, ext-method passthrough) as outbound calls, receives streamed session/update notifications in wire order, and answers the agent’s reverse calls (session/request_permission, fs/*, terminal/*).

The package documentation (libacp/doc.go) carries a compact end-to-end client example. libacp/acpexec provides the subprocess-over-stdio transport both roles use to reach a peer binary.

E2E harnesses against the Rust reference SDK

The in-repo tests exercise both halves against each other (in-process fakes, plus a production loopback in runtime/acpsvc/client_loopback_test.go that runs the real acpsvc agent against the real ClientSideConnection). Two additional, opt-in harnesses validate each role against independently implemented peers from the reference Rust SDK (github.com/agentclientprotocol/rust-sdk):

TargetValidatesPeer binary
make acp-conformanceour agent side (libacp/cmd/acp-stub-agent via AgentSideConnection)acp-validator, a conformance-checking ACP client
make acp-client-e2eour client side (ClientSideConnection over acpexec)testy, the SDK’s deterministic test agent

Both targets skip-or-fail cleanly when their binary env var is unset:

  • ACP_TESTY_BIN / ACP_MCP_ECHO_BIN — build from a rust-sdk checkout with cargo build -p agent-client-protocol-test --bins; the binaries land at <checkout>/target/debug/testy and <checkout>/target/debug/mcp-echo-server. (ACP_MCP_ECHO_BIN only gates the MCP pass-down test, which skips on its own if unset.)
  • ACP_VALIDATOR_BIN — the validator is not part of the SDK; its source is vendored in tools/acp-validator/ in this repository. It depends on the SDK’s agent-client-protocol crate by relative path, so copy it next to a rust-sdk checkout and cargo build there (the README has the exact steps). ACP_YOPO_BIN (optional, additional client) builds from the rust-sdk’s src/yopo.

The composed host e2e (registry → agenthost → live turn)

One layer above the wire-dispatch harnesses, make acp-host-e2e validates the runtime’s client-host composition end to end: an agents row created and resolved through the real registry service, spawned and driven by runtime/agenthost.DriveTurn (initialize → session/new → session/prompt → teardown), with the streamed reply asserted on the caller’s harness. This is the harness scoped by blueprints/acp/agent-servers-and-client-e2e.md. Servers, each isolating something different:

ServerGateAsserts
acp-stub-agent (hermetic)none — runs in plain go testdeterministic “ack” turn, update ordering through the harness seam
contenox self-loopback (contenox acp built in-test, driving a no-model chain fixture)none (skips under -short)byte-exact fixture reply through registry → host → real chain
testyACP_TESTY_BINdeterministic echo/greet through the composed path
Claude Code (via claude-code-acp)ACP_CLAUDE_ACP_BIN — never CI; needs Claude credentialsturn shape only: end_turn plus displayable output from a real, foreign production agent

The user-facing twin of this harness is contenox agent check <name>: it drives the same DriveTurn path against any registered agent and streams the reply — the way to verify an agent right after contenox agent add.

MCP forwarding and the agent’s command surface

An agent row’s mcp_servers config field is an explicit, per-agent allowlist of registered MCP server names (contenox mcp list) forwarded to that agent in ACP session/new — the mirror of what runtime/acpsvc consumes when contenox is on the agent side of the same exchange. The host (agenthost.ResolveForwardedMcpServers + DriveTurn) resolves names loudly (a missing name fails the turn rather than silently shrinking the agent’s declared context), filters by the agent’s initialize-advertised mcpCapabilities (stdio is baseline; http/sse gated), and reports forwarded-vs-dropped on the TurnResult. Contenox-side auth synthesis (authToken/authEnvKey/oauth/injectParams) is never translated into the payload; forwarding a server at all is the consent boundary. The composed pass-down is pinned by TestHostE2E_Testy_McpPassDownThroughComposedPath (testy connects to the forwarded mcp-echo-server and lists its tools).

The slash commands a hosted agent advertises (available_commands_update) are recorded by the harness (RecordingHarness.AvailableCommands) and printed by agent check. Merging them with contenox’s own acpsvc command set is deliberately not built here — it belongs to the future re-exposure layer (front-end ↔ contenox-as-agent ↔ hosted agent), where acpsvc’s leading-slash interception is the natural merge point and a collision policy is required (a real case: claude-code-acp and acpsvc both advertise /compact).

Esc to close