feat(acp): enable Route A via experimental flag (claudeCliAcp), default ON

Replace the manual FUSION_CLAUDE_ACP env enable with an experimental feature
switch. `experimentalFeatures.claudeCliAcp` is ON by default (off only when
explicitly set false); the engine translates it into the FUSION_CLAUDE_ACP
dispatch the pi-claude-cli provider reads, at registerExtensionProviders time.

- Still fail-closed: with no bridge path published (acp-runtime plugin absent),
  the provider falls back to `claude -p`.
- Explicit FUSION_CLAUDE_ACP env always wins (operator / test override).
- New testable helper claude-acp-enable.ts (6/6 tests); flag documented in the
  core experimentalFeatures doc.

So with the acp-runtime plugin installed, Claude CLI now routes through the ACP
bridge by default; set experimentalFeatures.claudeCliAcp=false to force `-p`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-15 12:14:20 -07:00
parent 642780220e
commit 45184aef7e
4 changed files with 92 additions and 4 deletions

View File

@@ -3170,9 +3170,14 @@ export interface GlobalSettings {
* "another-experiment": false
* }
*
* Default: workflow columns, graph executor, dual-observe, and authoritative
* interpreter flags enabled; operators may explicitly set individual flags
* false while rollout controls remain available. */
* Default: workflow columns, graph executor, dual-observe, authoritative
* interpreter, and `claudeCliAcp` flags enabled; operators may explicitly set
* individual flags false while rollout controls remain available.
*
* `claudeCliAcp` (default ON): routes the Claude CLI provider through the
* `claude-code-cli-acp` ACP bridge instead of `claude -p`. Effective only when
* the acp-runtime plugin is installed (it publishes the bundled bridge path);
* otherwise the provider fails closed to `-p`. Set false to force `-p`. */
experimentalFeatures?: Record<string, boolean>;
/** Per-adapter CLI-agent launch configuration (CLI Agent Executor, U15).
* Keyed by adapter id (e.g. `"claude-code"`, `"codex"`, `"generic"`). Each

View File

@@ -0,0 +1,38 @@
import { describe, it, expect } from "vitest";
import { claudeAcpExperimentalEnabled, applyClaudeAcpEnable } from "../claude-acp-enable.js";
describe("claudeAcpExperimentalEnabled — default ON", () => {
it("is ON when no settings / no experimentalFeatures", () => {
expect(claudeAcpExperimentalEnabled(undefined)).toBe(true);
expect(claudeAcpExperimentalEnabled({})).toBe(true);
expect(claudeAcpExperimentalEnabled({ experimentalFeatures: {} })).toBe(true);
});
it("is ON when explicitly true", () => {
expect(claudeAcpExperimentalEnabled({ experimentalFeatures: { claudeCliAcp: true } })).toBe(true);
});
it("is OFF only when explicitly false", () => {
expect(claudeAcpExperimentalEnabled({ experimentalFeatures: { claudeCliAcp: false } })).toBe(false);
});
});
describe("applyClaudeAcpEnable — translates the flag to FUSION_CLAUDE_ACP", () => {
it("sets FUSION_CLAUDE_ACP=1 when enabled (default ON) and env unset", () => {
const env: NodeJS.ProcessEnv = {};
expect(applyClaudeAcpEnable({}, env)).toBe(true);
expect(env.FUSION_CLAUDE_ACP).toBe("1");
});
it("does NOT set the env when the flag is explicitly false", () => {
const env: NodeJS.ProcessEnv = {};
expect(applyClaudeAcpEnable({ experimentalFeatures: { claudeCliAcp: false } }, env)).toBe(false);
expect(env.FUSION_CLAUDE_ACP).toBeUndefined();
});
it("honors an explicit env override (operator/test wins over the flag)", () => {
const off: NodeJS.ProcessEnv = { FUSION_CLAUDE_ACP: "0" };
expect(applyClaudeAcpEnable({}, off)).toBe(false); // flag default-on, but env says off
expect(off.FUSION_CLAUDE_ACP).toBe("0");
const on: NodeJS.ProcessEnv = { FUSION_CLAUDE_ACP: "1" };
expect(applyClaudeAcpEnable({ experimentalFeatures: { claudeCliAcp: false } }, on)).toBe(true);
expect(on.FUSION_CLAUDE_ACP).toBe("1");
});
});

View File

@@ -0,0 +1,36 @@
/**
* Route A enable resolution (experimental, DEFAULT ON).
*
* The `pi-claude-cli` provider drives Claude through the `claude-code-cli-acp`
* ACP bridge instead of `claude -p` when BOTH hold at dispatch time:
* 1. `FUSION_CLAUDE_ACP=1` (this module sets it from the experimental flag), and
* 2. a bridge path is resolvable (the acp-runtime plugin publishes
* `FUSION_CLAUDE_ACP_BRIDGE` on load — KTD10; absent → fail-closed to `-p`).
*
* The user-facing switch is `experimentalFeatures.claudeCliAcp`: ON unless the
* user explicitly sets it to `false`. An explicit `FUSION_CLAUDE_ACP` env value
* always wins (operator / test override) — see {@link applyClaudeAcpEnable}.
*/
/** True unless `experimentalFeatures.claudeCliAcp === false` (default ON). */
export function claudeAcpExperimentalEnabled(
globalSettings: Record<string, unknown> | undefined,
): boolean {
const exp = ((globalSettings ?? {}).experimentalFeatures ?? {}) as Record<string, unknown>;
return exp.claudeCliAcp !== false;
}
/**
* Translate the experimental flag into the `FUSION_CLAUDE_ACP` dispatch the
* provider reads. No-op when the env var is already set (explicit override wins),
* so operators/tests keep full control. Returns the resolved enabled state.
*/
export function applyClaudeAcpEnable(
globalSettings: Record<string, unknown> | undefined,
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (typeof env.FUSION_CLAUDE_ACP === "string") return env.FUSION_CLAUDE_ACP === "1";
const enabled = claudeAcpExperimentalEnabled(globalSettings);
if (enabled) env.FUSION_CLAUDE_ACP = "1";
return enabled;
}

View File

@@ -57,6 +57,7 @@ import {
type SkillSelectionContext,
} from "./skill-resolver.js";
import { isContextLimitError } from "./context-limit-detector.js";
import { applyClaudeAcpEnable } from "./claude-acp-enable.js";
import { createFusionAuthStorage, getModelRegistryModelsPath } from "./auth-storage.js";
import { piLog, extensionsLog } from "./logger.js";
import { readCustomProviders } from "./custom-providers.js";
@@ -1368,10 +1369,18 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis
try {
const agentDir = getPackageManagerAgentDir();
const settingsView = createReadOnlyPiSettingsView(cwd, agentDir);
// Route A enable (experimental, DEFAULT ON): translate
// experimentalFeatures.claudeCliAcp into the FUSION_CLAUDE_ACP dispatch the
// pi-claude-cli provider reads. Still fail-closed — with no bridge path
// published (acp-runtime plugin absent), the provider falls back to `-p`.
applyClaudeAcpEnable(settingsView.getGlobalSettings() as Record<string, unknown>);
const packageManager = new DefaultPackageManager({
cwd,
agentDir,
settingsManager: createReadOnlyPiSettingsView(cwd, agentDir) as any,
settingsManager: settingsView as any,
});
const resolvedPaths = await packageManager.resolve();
const packageExtensionPaths = resolvedPaths.extensions