diff --git a/plugins/fusion-plugin-acp-runtime/src/__tests__/index.test.ts b/plugins/fusion-plugin-acp-runtime/src/__tests__/index.test.ts index 331e5967e1..843fb68de5 100644 --- a/plugins/fusion-plugin-acp-runtime/src/__tests__/index.test.ts +++ b/plugins/fusion-plugin-acp-runtime/src/__tests__/index.test.ts @@ -133,3 +133,36 @@ describe("resolveCliSettings", () => { expect(ask.allowUnrestricted).toBe(false); }); }); + +describe("KTD10 — onLoad publishes the bundled bridge path (Route A)", () => { + const savedBridge = process.env.FUSION_CLAUDE_ACP_BRIDGE; + const savedFlag = process.env.FUSION_CLAUDE_ACP; + afterEach(() => { + if (savedBridge === undefined) delete process.env.FUSION_CLAUDE_ACP_BRIDGE; + else process.env.FUSION_CLAUDE_ACP_BRIDGE = savedBridge; + if (savedFlag === undefined) delete process.env.FUSION_CLAUDE_ACP; + else process.env.FUSION_CLAUDE_ACP = savedFlag; + }); + const fakeCtx = () => ({ settings: {}, logger: { info: () => undefined, warn: () => undefined } }); + + it("publishes the bundled bridge path to FUSION_CLAUDE_ACP_BRIDGE when unset", () => { + delete process.env.FUSION_CLAUDE_ACP_BRIDGE; + plugin.hooks?.onLoad?.(fakeCtx() as never); + expect(process.env.FUSION_CLAUDE_ACP_BRIDGE).toBeDefined(); + expect(isAbsolute(process.env.FUSION_CLAUDE_ACP_BRIDGE!)).toBe(true); + expect(process.env.FUSION_CLAUDE_ACP_BRIDGE).toContain("node_modules/.bin/claude-code-cli-acp"); + }); + + it("does NOT enable the transport — FUSION_CLAUDE_ACP stays unset (kill-switch off)", () => { + delete process.env.FUSION_CLAUDE_ACP; + delete process.env.FUSION_CLAUDE_ACP_BRIDGE; + plugin.hooks?.onLoad?.(fakeCtx() as never); + expect(process.env.FUSION_CLAUDE_ACP).toBeUndefined(); + }); + + it("respects an explicit FUSION_CLAUDE_ACP_BRIDGE override", () => { + process.env.FUSION_CLAUDE_ACP_BRIDGE = "/custom/bridge/path"; + plugin.hooks?.onLoad?.(fakeCtx() as never); + expect(process.env.FUSION_CLAUDE_ACP_BRIDGE).toBe("/custom/bridge/path"); + }); +}); diff --git a/plugins/fusion-plugin-acp-runtime/src/index.ts b/plugins/fusion-plugin-acp-runtime/src/index.ts index 057aaadd84..c998af80ea 100644 --- a/plugins/fusion-plugin-acp-runtime/src/index.ts +++ b/plugins/fusion-plugin-acp-runtime/src/index.ts @@ -1,6 +1,6 @@ import { definePlugin } from "@fusion/plugin-sdk"; import type { FusionPlugin, PluginRuntimeFactory, PluginRuntimeManifestMetadata } from "@fusion/plugin-sdk"; -import { resolveCliSettings } from "./cli-spawn.js"; +import { resolveCliSettings, resolveBundledClaudeBridgeBinary } from "./cli-spawn.js"; import { AcpRuntimeAdapter } from "./runtime-adapter.js"; import { killAllProcesses } from "./process-manager.js"; import { setupHooks, setupManifest } from "./setup.js"; @@ -49,6 +49,25 @@ const plugin: FusionPlugin = definePlugin({ "will be auto-approved under an allow-all policy. Prefer an approval-required policy.", ); } + // KTD10 (Route A): publish the bundled `claude-code-cli-acp` bridge path + // process-wide so the pi-claude-cli provider's kill-switch can resolve it + // WITHOUT a manual FUSION_CLAUDE_ACP_BRIDGE env var. This only PUBLISHES the + // path — the ACP transport stays OFF until an operator sets + // FUSION_CLAUDE_ACP=1 (the rollout gate). An explicit env override wins, and + // the resolver is identity-pinned to the plugin-owned node_modules/.bin shim + // so a same-named global binary cannot replace the reviewed bridge. + if (!process.env.FUSION_CLAUDE_ACP_BRIDGE) { + const resolved = resolveBundledClaudeBridgeBinary(); + if (resolved.kind === "resolved") { + process.env.FUSION_CLAUDE_ACP_BRIDGE = resolved.path; + ctx.logger.info( + "ACP Runtime: published bundled Claude bridge path for Route A " + + "(transport stays off until FUSION_CLAUDE_ACP=1).", + ); + } else { + ctx.logger.info(`ACP Runtime: bundled Claude bridge not resolved (${resolved.reason}); Route A unavailable.`); + } + } }, }, runtime: {