feat(acp): U11/KTD10 — publish bundled bridge path on plugin load

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 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-15 11:34:36 -07:00
parent 239bec74c4
commit 022bee5842
2 changed files with 53 additions and 1 deletions

View File

@@ -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");
});
});

View File

@@ -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: {