Files
fusion/plugins/fusion-plugin-acp-runtime
Timoteo 47dd536e36 fix: expose session.subscribe on ACP runtime sessions for engine workflow steps (#3501)
## 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 -->
2026-08-21 19:30:31 -07:00
..
2026-08-21 07:44:32 -07:00
2026-08-21 07:44:32 -07:00

@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_permission is classified by tool kind into a Fusion action category and checked against the live permission policy — never a preset shortcut. allow_once only (never a persisted blanket grant). Unmappable kinds and missing policy default-deny.
  • Unrestricted-risk acknowledgement (acpAllowUnrestricted). Because the shipped default policy is unrestricted (allow-all), a blanket allow on a sensitive category is escalated to approval unless the user explicitly sets acpAllowUnrestricted: true. Prefer running the ACP runtime under an approval-required policy.
  • Filesystem jail. fs/read_text_file / fs/write_text_file are opt-in (acpFsRead / acpFsWrite, writes default OFF), confined to the session cwd by a real symlink-resolving jail (realpath + O_NOFOLLOW), with a deny-list for secrets (.env, *.pem, …) and git internals (.git/**). Writes are gated through the file_write_delete permission 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) — inherited process.env is 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):

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 (version 0.1.1) — https://www.npmjs.com/package/claude-code-cli-acp
  • Binary / CLI name: claude-code-cli-acp
  • Checksum: sha512-qpfRGOXkOs9mqI7oumsGistWisyXcCC0r7ng7wdLvGMIORdzHjmUUa+94Jftgr/NYAVnAUe6N7kimD8PaO3D5g== (from pnpm-lock.yaml for claude-code-cli-acp@0.1.1)
  • Pinned-commit spot-review: tag v0.1.1 points to commit c93f4f4ca449f451d9f3b7db536caf4060883da9 (annotated tag ca33404fc1128d6a88a55b248f042f70b4bc9f9a, unsigned). License: Apache-2.0. Behavior reviewed for this integration: runs claude through a PTY, reads transcript JSONL, exposes an ACP server over stdio, and requires @anthropic-ai/claude-code installed + authenticated.

See docs/acp-contract.md for the launch/readiness contract and failure taxonomy.