Files
fusion/docs/acp-contract.md
gsxdsm a9815fb1ff FN-6457: add ACP ask runner and bundled Claude bridge setup
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
2026-06-15 02:30:41 -07:00

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.
  • stderr is 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 acpEnvAllowList allow-list only (inherited process.env is 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.1 is pinned under the ACP runtime plugin and the sentinel binary name resolves to this plugin's own node_modules/.bin shim, not to a PATH-selected substitute.
  • The read-only ask posture uses tools: "readonly", acpArgs: [], and leaves acpFsRead / acpFsWrite off. 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: HOME is forwarded so the underlying claude can read ~/.claude auth/session state, and PATH is forwarded for sub-executable resolution. ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, and inherited process.env are not forwarded.
  • checkSetup treats 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 run claude once 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.
  • promptWithFallback surfaces ACP stopReason to the runner. Planning tolerates an absent stop reason, but validation treats abnormal/truncated stops such as max_tokens and cancelled as error regardless of any recovered JSON.
  • Validator prose fallback is constrained: prose can infer fail or blocked, but never pass. A pass requires clean structured JSON (verdict:"pass" or passed:true) from a clean turn.

Readiness = the initialize handshake

There is no --version probe. Readiness is the protocol handshake itself:

  1. Spawn the agent subprocess.
  2. Send initialize { protocolVersion: 1, clientCapabilities: { fs } } under a timeout (default 30s — research flagged Gemini-on-macOS OAuth and Claude-adapter session/new stalls).
  3. The agent responds with its integer protocolVersion, agentCapabilities, and authMethods.
  4. 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 AbortSignal in the runtime contract; teardown enters via an unawaited synchronous dispose() plus the process-registry kill. The registry SIGKILL is the authoritative no-orphan / no-deadlock guarantee; a best-effort session/cancel + pending-permission drain runs first when timing allows but is opportunistic.

Sources