## Summary Fixes the `session.subscribe is not a function` crash that breaks every engine workflow step (Plan Review, Code Review) executed by an ACP agent (Hermes ACP, Prime, Grok). Introduced by the ACP custom-tools bridge (#3476) in the sense that it made the gap reachable: ACP sessions stream through the bridging client handler onto `callbacks` and never exposed the pi-style `subscribe()` that the engine's AgentSession contract promises. Two production call sites call it unconditionally: - `packages/engine/src/executor/execute-workflow-step.ts` (workflow steps — Plan Review / Code Review) - `packages/engine/src/pi.ts` fallback wiring (`wireFallbackHooks`, `promptableSession.subscribe`) (`reviewer.ts` guards with `typeof session.subscribe === "function"`; the other paths do not.) Real-world symptom (verified on 0.77.0-beta.6 with a Hermes ACP agent): every Plan Review attempt fails before producing a verdict: ``` [pre-merge] Workflow step failed: Plan Review outcome: Plan Review failed before producing a verdict: session.subscribe is not a function Plan Review provider retry budget exhausted ``` ## Fix Fix at the seam rather than guarding every call site forever: - `AcpRuntimeAdapter.createSession` wraps the raw callbacks so each forwarded text/thinking/tool event is **also** replayed to subscribers as the pi-shaped event consumers parse (`message_update` + `assistantMessageEvent.{text_delta,thinking_delta}`, `tool_execution_start/end`) - exposes `session.subscribe(handler)` returning an unsubscribe function; dispose clears subscribers - merges engine `taskEnv` into the subprocess env behind the existing allow-list trust boundary (KTD6b preserved — only allow-listed keys forward, task values win) - original callback delivery unchanged; subscriber exceptions are isolated so a faulty consumer can't break the streaming bridge ## Testing - New regression tests in `runtime-adapter.test.ts` against the real echo-agent fixture (`ACP_FIXTURE_RICH_PROMPT=1`): - subscribe replays text/thinking/tool events as pi-shaped events - unsubscribe stops delivery - dual delivery: original `onText` callback AND subscriber both fire - Full plugin suite: 255 passed (21 files) - `tsc --noEmit` clean for the plugin - changeset for `@runfusion/fusion` (patch, bugfix) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added session event subscriptions with unsubscribe support. * Streamed text, thinking, and tool updates are replayed in a consistent event format. * Added support for task-specific environment values when launching subprocesses. * **Bug Fixes** * Ensured streamed events reach both existing callbacks and subscribers. * Isolated subscriber errors so they do not interrupt other handlers. * Session cleanup now stops further subscription notifications. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
@fusion-plugin-examples/acp-runtime
A Fusion runtime plugin that drives any external Agent Client Protocol (ACP) agent over JSON-RPC/stdio. One integration unlocks every ACP-compatible agent (Gemini CLI, the Claude Code ACP adapter, and any future agent that speaks the protocol) through the standard protocol instead of a bespoke per-CLI integration.
Selected via runtimeId: "acp". Installed on demand (experimental) — see the
Fusion plugin catalog (fn plugin install fusion-plugin-acp-runtime).
Security posture
The ACP agent is an untrusted subprocess that calls back into Fusion for permissions and filesystem access. This plugin enforces a defense-in-depth floor:
- Per-category permission gating. Each
session/request_permissionis classified by tool kind into a Fusion action category and checked against the live permission policy — never a preset shortcut.allow_onceonly (never a persisted blanket grant). Unmappable kinds and missing policy default-deny. - Unrestricted-risk acknowledgement (
acpAllowUnrestricted). Because the shipped default policy isunrestricted(allow-all), a blanketallowon a sensitive category is escalated to approval unless the user explicitly setsacpAllowUnrestricted: true. Prefer running the ACP runtime under anapproval-requiredpolicy. - Filesystem jail.
fs/read_text_file/fs/write_text_fileare opt-in (acpFsRead/acpFsWrite, writes default OFF), confined to the sessioncwdby a real symlink-resolving jail (realpath +O_NOFOLLOW), with a deny-list for secrets (.env,*.pem, …) and git internals (.git/**). Writes are gated through thefile_write_deletepermission category. - Untrusted-input bounds. Streamed output is sanitized (ANSI/control strip) and bounded (per-turn + per-chunk caps; bounded tool-call correlation map).
- Subprocess isolation. The agent env is built from an allow-list
(
acpEnvAllowList) — inheritedprocess.envis not forwarded.
Not sandboxed in v1: the agent's own process/network syscalls run with Fusion's user privileges (OS-level sandboxing is recommended future work).
Settings
| Key | Default | Meaning |
|---|---|---|
acpBinaryPath |
acp-agent |
Agent binary to spawn |
acpArgs |
[] |
Args that launch the agent in ACP/stdio mode (e.g. ["--acp"]) |
acpModel |
— | Optional model identifier reported via describeModel |
acpFsRead |
false |
Advertise/register fs/read_text_file |
acpFsWrite |
false |
Advertise/register fs/write_text_file (gated) |
acpEnvAllowList |
[] |
Env var names forwarded to the agent subprocess |
acpAllowUnrestricted |
false |
Acknowledge the untrusted-agent risk under an allow-all policy |
Upstream / third-party integration evidence
Per AGENTS.md (External-integration evidence):
- Protocol homepage / docs: https://agentclientprotocol.com
- Upstream protocol repo: https://github.com/agentclientprotocol/agent-client-protocol
- TypeScript SDK repo: https://github.com/agentclientprotocol/typescript-sdk
- Dependency (npm):
@agentclientprotocol/sdk— https://www.npmjs.com/package/@agentclientprotocol/sdk - Pinned release:
0.24.0(Apache-2.0) - Tarball: https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.24.0.tgz
- Integrity (sha512):
sha512-vvu9appvGvfYstBj19C6NCepV6SvUhY5VRv60KUZ4XzhTah/olOYul5Zo4C+x2enyshMSvgB2mm/OEmrsHaSmA== - Agent binaries driven: user-supplied ACP agents (e.g.
gemini --acp) and the bundled Claude bridge below. User-configured agents remainupstream-pending-verificationper agent.
Bundled Claude ACP bridge evidence
- Canonical upstream repo URL: https://github.com/moabualruz/claude-code-cli-acp
- Docs / homepage URL: https://github.com/moabualruz/claude-code-cli-acp#readme
- Release / download URL: npm package
claude-code-cli-acp(version0.1.1) — https://www.npmjs.com/package/claude-code-cli-acp - Binary / CLI name:
claude-code-cli-acp - Checksum:
sha512-qpfRGOXkOs9mqI7oumsGistWisyXcCC0r7ng7wdLvGMIORdzHjmUUa+94Jftgr/NYAVnAUe6N7kimD8PaO3D5g==(frompnpm-lock.yamlforclaude-code-cli-acp@0.1.1) - Pinned-commit spot-review: tag
v0.1.1points to commitc93f4f4ca449f451d9f3b7db536caf4060883da9(annotated tagca33404fc1128d6a88a55b248f042f70b4bc9f9a, unsigned). License: Apache-2.0. Behavior reviewed for this integration: runsclaudethrough a PTY, reads transcript JSONL, exposes an ACP server over stdio, and requires@anthropic-ai/claude-codeinstalled + authenticated.
See docs/acp-contract.md for the launch/readiness contract and failure taxonomy.