From 022bee5842aeab01cfd18cdbccd153cc2b4db4e4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 15 Jun 2026 11:34:36 -0700 Subject: [PATCH] =?UTF-8?q?feat(acp):=20U11/KTD10=20=E2=80=94=20publish=20?= =?UTF-8?q?bundled=20bridge=20path=20on=20plugin=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acp-runtime plugin's onLoad now publishes the identity-pinned bundled claude-code-cli-acp path to FUSION_CLAUDE_ACP_BRIDGE (when unset), so the pi-claude-cli kill-switch resolves the bridge WITHOUT a manual env var — no engine->plugin static coupling. Publishes the path only; the ACP transport stays OFF until an operator sets FUSION_CLAUDE_ACP=1 (rollout gate). Explicit env override wins; resolver is pinned to the plugin's node_modules/.bin shim. 204/204 plugin tests green (3 new KTD10 tests); typecheck clean. Co-Authored-By: Claude Opus 4.8 --- .../src/__tests__/index.test.ts | 33 +++++++++++++++++++ .../fusion-plugin-acp-runtime/src/index.ts | 21 +++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) 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: {