Route ACP-backed planning and validation through a read-only ask-once runner with a pinned Claude bridge foundation. - Add askAcpOnce for single-turn ACP sessions with timeout handling, JSON recovery, clean stop validation, and disposal. - Refactor validation seams to use ACP runtime prompts and require structured pass verdicts. - Resolve the Claude ACP bridge from the plugin bundle and add setup checks for identity, environment, probing, and auth readiness. - Document the ACP Route B plan and update tests for validator, session, runtime, and plugin setup behavior. Files changed: CONCEPTS.md | 6 + docs/acp-contract.md | 36 ++ .../2026-06-14-001-feat-claude-acp-runtime-plan.md | 465 +++++++++++++++++++++ .../engine/src/__tests__/cli-agent-ask.test.ts | 104 +++++ .../src/__tests__/cli-agent-validator.test.ts | 137 +++--- .../src/__tests__/interactive-ai-session.test.ts | 96 +++-- packages/engine/src/agent-runtime.ts | 6 +- packages/engine/src/cli-agent-ask.ts | 120 ++++++ packages/engine/src/cli-agent-validator.ts | 65 ++- .../cli-agent/__tests__/one-shot-session.test.ts | 16 +- packages/engine/src/cli-agent/one-shot-session.ts | 17 +- packages/engine/src/index.ts | 8 +- packages/engine/src/interactive-ai-session.ts | 33 +- plugins/fusion-plugin-acp-runtime/AGENTS.md | 14 + plugins/fusion-plugin-acp-runtime/CHANGELOG.md | 6 + plugins/fusion-plugin-acp-runtime/README.md | 13 +- plugins/fusion-plugin-acp-runtime/package.json | 3 +- .../src/__tests__/index.test.ts | 51 ++- .../src/__tests__/process-manager.test.ts | 32 +- .../src/__tests__/runtime-adapter.test.ts | 4 +- .../src/__tests__/setup.test.ts | 71 ++++ plugins/fusion-plugin-acp-runtime/src/cli-spawn.ts | 95 ++++- plugins/fusion-plugin-acp-runtime/src/index.ts | 16 +- .../src/process-manager.ts | 26 +- .../src/runtime-adapter.ts | 11 +- plugins/fusion-plugin-acp-runtime/src/setup.ts | 104 +++++ plugins/fusion-plugin-acp-runtime/src/types.ts | 6 +- pnpm-lock.yaml | 139 ++++-- 28 files changed, 1502 insertions(+), 198 deletions(-) Fusion-Task-Id: FN-6457 Fusion-Task-Lineage: a3364ed7-cb28-4a2b-b898-6ccd0d95fb92
5.1 KiB
ACP (Agent Client Protocol) Runtime Contract
Date: 2026-06-03
Launch/readiness contract and failure taxonomy for fusion-plugin-acp-runtime,
which drives any external Agent Client Protocol
agent over JSON-RPC/stdio. Mirrors the shape of docs/cursor-cli-contract.md.
Transport
- Newline-delimited JSON-RPC 2.0 over stdio (no Content-Length framing).
Provided by
@agentclientprotocol/sdk(ndJsonStream+ClientSideConnection). - The client (Fusion) launches the agent as a subprocess with piped stdio. The agent's stdin is the JSON-RPC output stream; its stdout is the input stream.
stderris captured (redacted) for diagnostics, never parsed as protocol.
Invocation and binary detection
- Unlike a single-vendor CLI, ACP is a protocol — the agent binary + ACP-mode
flag are user-configured:
acpBinaryPath— e.g.gemini,npx, or an absolute path.acpArgs— the flag(s) that put the agent in ACP/stdio mode, e.g.["--acp"].
- The subprocess environment is built from the
acpEnvAllowListallow-list only (inheritedprocess.envis not forwarded — the agent is untrusted).
Claude bridge ask profile (Route B)
Route-B planning and validator asks use the acp runtime with the bundled
claude-code-cli-acp bridge instead of claude -p:
claude-code-cli-acp@0.1.1is pinned under the ACP runtime plugin and the sentinel binary name resolves to this plugin's ownnode_modules/.binshim, not to a PATH-selected substitute.- The read-only ask posture uses
tools: "readonly",acpArgs: [], and leavesacpFsRead/acpFsWriteoff. Route A's tool-bearing provider path remains deferred and is not implied by this profile. - The Claude bridge env allow-list is intentionally narrow:
HOMEis forwarded so the underlyingclaudecan read~/.claudeauth/session state, andPATHis forwarded for sub-executable resolution.ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN, and inheritedprocess.envare not forwarded. checkSetuptreats the bridge as installed only when the resolved binary is plugin-owned, the ACP handshake succeeds, and no Claude auth hint is returned. Auth-needed statuses tell the operator to runclaudeonce to authenticate.
askAcpOnce prose → JSON recovery contract
The engine-side askAcpOnce runner creates one readonly ACP session, accumulates
all onText deltas into text, runs one promptWithFallback turn, optionally
recovers the trailing JSON object via extractJsonObjects, and disposes the
session in finally. Its shape is deliberately close to the old one-shot result:
- Success:
{ ok: true, text, parsed?, stopReason? }. - Failure:
{ ok: false, reason, message, text?, stopReason? }for session creation errors, turn errors, timeouts, and abnormal stops. promptWithFallbacksurfaces ACPstopReasonto the runner. Planning tolerates an absent stop reason, but validation treats abnormal/truncated stops such asmax_tokensandcancelledaserrorregardless of any recovered JSON.- Validator prose fallback is constrained: prose can infer
failorblocked, but neverpass. A pass requires clean structured JSON (verdict:"pass"orpassed:true) from a clean turn.
Readiness = the initialize handshake
There is no --version probe. Readiness is the protocol handshake itself:
- Spawn the agent subprocess.
- Send
initialize { protocolVersion: 1, clientCapabilities: { fs } }under a timeout (default 30s — research flagged Gemini-on-macOS OAuth and Claude-adaptersession/newstalls). - The agent responds with its integer
protocolVersion,agentCapabilities, andauthMethods. - The client compares the integer protocol version; an unsupported version is a hard failure (do not assume the agent errors first).
fs capabilities are advertised only when acpFsRead/acpFsWrite are
enabled (writes default OFF).
Failure taxonomy (probe.ts AcpProbeReason)
| Reason | Trigger |
|---|---|
ok |
Handshake completed (with authRequired: true when authMethods is non-empty) |
missing_binary |
Spawn ENOENT (binary not found, code 127) |
spawn_error |
Other spawn failure |
handshake_timeout |
initialize did not complete within the bound (code 124) |
incompatible_protocol |
Agent negotiated an unsupported integer protocol version |
unauthenticated |
Agent requires an auth method the client cannot satisfy |
Lifecycle / teardown
- The engine has no
AbortSignalin the runtime contract; teardown enters via an unawaited synchronousdispose()plus the process-registry kill. The registry SIGKILL is the authoritative no-orphan / no-deadlock guarantee; a best-effortsession/cancel+ pending-permission drain runs first when timing allows but is opportunistic.
Sources
- https://agentclientprotocol.com (introduction, schema, transports, initialization, tool-calls)
@agentclientprotocol/sdkv0.24.0 — https://www.npmjs.com/package/@agentclientprotocol/sdk- Validation: the SDK example echo agent (CI) + an in-repo controllable fixture
(
src/__tests__/fixtures/echo-agent.mjs); Gemini CLI / Claude-adapter for manual e2e.