Route ACP-backed planning and validation through a read-only ask-once runner with a pinned Claude bridge foundation. - Add askAcpOnce for single-turn ACP sessions with timeout handling, JSON recovery, clean stop validation, and disposal. - Refactor validation seams to use ACP runtime prompts and require structured pass verdicts. - Resolve the Claude ACP bridge from the plugin bundle and add setup checks for identity, environment, probing, and auth readiness. - Document the ACP Route B plan and update tests for validator, session, runtime, and plugin setup behavior. Files changed: CONCEPTS.md | 6 + docs/acp-contract.md | 36 ++ .../2026-06-14-001-feat-claude-acp-runtime-plan.md | 465 +++++++++++++++++++++ .../engine/src/__tests__/cli-agent-ask.test.ts | 104 +++++ .../src/__tests__/cli-agent-validator.test.ts | 137 +++--- .../src/__tests__/interactive-ai-session.test.ts | 96 +++-- packages/engine/src/agent-runtime.ts | 6 +- packages/engine/src/cli-agent-ask.ts | 120 ++++++ packages/engine/src/cli-agent-validator.ts | 65 ++- .../cli-agent/__tests__/one-shot-session.test.ts | 16 +- packages/engine/src/cli-agent/one-shot-session.ts | 17 +- packages/engine/src/index.ts | 8 +- packages/engine/src/interactive-ai-session.ts | 33 +- plugins/fusion-plugin-acp-runtime/AGENTS.md | 14 + plugins/fusion-plugin-acp-runtime/CHANGELOG.md | 6 + plugins/fusion-plugin-acp-runtime/README.md | 13 +- plugins/fusion-plugin-acp-runtime/package.json | 3 +- .../src/__tests__/index.test.ts | 51 ++- .../src/__tests__/process-manager.test.ts | 32 +- .../src/__tests__/runtime-adapter.test.ts | 4 +- .../src/__tests__/setup.test.ts | 71 ++++ plugins/fusion-plugin-acp-runtime/src/cli-spawn.ts | 95 ++++- plugins/fusion-plugin-acp-runtime/src/index.ts | 16 +- .../src/process-manager.ts | 26 +- .../src/runtime-adapter.ts | 11 +- plugins/fusion-plugin-acp-runtime/src/setup.ts | 104 +++++ plugins/fusion-plugin-acp-runtime/src/types.ts | 6 +- pnpm-lock.yaml | 139 ++++-- 28 files changed, 1502 insertions(+), 198 deletions(-) Fusion-Task-Id: FN-6457 Fusion-Task-Lineage: a3364ed7-cb28-4a2b-b898-6ccd0d95fb92
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { dirname, relative, resolve, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { PluginContext, PluginSetupCheckResult, PluginSetupHooks, PluginSetupManifest } from "@fusion/plugin-sdk";
|
|
import { CLAUDE_CODE_CLI_ACP_BINARY, bundledClaudeBridgeBinPath, resolveClaudeBridgeAskSettings } from "./cli-spawn.js";
|
|
import { buildSpawnEnv } from "./process-manager.js";
|
|
import { probeAcpReadiness, type AcpProbeStatus, type ProbeOptions } from "./probe.js";
|
|
|
|
export const setupManifest: PluginSetupManifest = {
|
|
binaryName: CLAUDE_CODE_CLI_ACP_BINARY,
|
|
description: "Claude Code ACP bridge used by Fusion's read-only ask path",
|
|
channel: "beta",
|
|
defaultTimeoutMs: 30_000,
|
|
};
|
|
|
|
export interface CheckAcpSetupDeps {
|
|
probe?: (opts: ProbeOptions) => Promise<AcpProbeStatus>;
|
|
pluginRoot?: string;
|
|
}
|
|
|
|
const MAX_PROBE_TIMEOUT_MS = 30_000;
|
|
|
|
function isInside(parent: string, child: string): boolean {
|
|
const rel = relative(resolve(parent), resolve(child));
|
|
return rel === "" || (!rel.startsWith("..") && !rel.includes(`..${sep}`));
|
|
}
|
|
|
|
function defaultPluginRoot(): string {
|
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
}
|
|
|
|
export function validateBundledBridgeIdentity(binaryPath: string, pluginRoot = defaultPluginRoot()): string | undefined {
|
|
const expectedBin = resolve(bundledClaudeBridgeBinPath(pluginRoot));
|
|
const expectedNodeModules = resolve(pluginRoot, "node_modules");
|
|
if (resolve(binaryPath) !== expectedBin && !isInside(expectedNodeModules, binaryPath)) {
|
|
return `Resolved ${CLAUDE_CODE_CLI_ACP_BINARY} must come from this plugin's node_modules, got ${binaryPath}`;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function statusFromProbe(probe: AcpProbeStatus, binaryPath: string): PluginSetupCheckResult {
|
|
if (probe.ok) {
|
|
if (probe.authRequired) {
|
|
return {
|
|
status: "error",
|
|
binaryPath,
|
|
error: "Claude authentication required: run `claude` once to authenticate before using the ACP bridge.",
|
|
};
|
|
}
|
|
return { status: "installed", binaryPath };
|
|
}
|
|
|
|
if (probe.reason === "missing_binary") {
|
|
return {
|
|
status: "not-installed",
|
|
error: `Install bundled dependency ${CLAUDE_CODE_CLI_ACP_BINARY}@0.1.1 and run pnpm install for this plugin.`,
|
|
};
|
|
}
|
|
if (probe.reason === "unauthenticated") {
|
|
return {
|
|
status: "error",
|
|
binaryPath,
|
|
error: "Claude authentication required: run `claude` once to authenticate before using the ACP bridge.",
|
|
};
|
|
}
|
|
return { status: "error", binaryPath, error: probe.detail ?? `ACP readiness failed: ${probe.reason}` };
|
|
}
|
|
|
|
export async function checkSetup(
|
|
ctx: PluginContext,
|
|
deps: CheckAcpSetupDeps = {},
|
|
): Promise<PluginSetupCheckResult> {
|
|
const settings = resolveClaudeBridgeAskSettings(ctx.settings as Record<string, unknown> | undefined);
|
|
if (settings.binaryResolution?.kind === "not_resolved") {
|
|
return {
|
|
status: "not-installed",
|
|
error: settings.binaryResolution.reason ?? `Install ${CLAUDE_CODE_CLI_ACP_BINARY}@0.1.1`,
|
|
};
|
|
}
|
|
|
|
const identityError = validateBundledBridgeIdentity(settings.binaryPath, deps.pluginRoot);
|
|
if (identityError) {
|
|
return { status: "error", error: identityError, binaryPath: settings.binaryPath };
|
|
}
|
|
|
|
let env: NodeJS.ProcessEnv;
|
|
try {
|
|
env = buildSpawnEnv(settings.envAllowList, { required: settings.requiredEnv });
|
|
} catch (err) {
|
|
return { status: "error", binaryPath: settings.binaryPath, error: err instanceof Error ? err.message : String(err) };
|
|
}
|
|
|
|
const probe = await (deps.probe ?? probeAcpReadiness)({
|
|
binaryPath: settings.binaryPath,
|
|
args: settings.args,
|
|
cwd: process.cwd(),
|
|
env,
|
|
timeoutMs: MAX_PROBE_TIMEOUT_MS,
|
|
});
|
|
return statusFromProbe(probe, settings.binaryPath);
|
|
}
|
|
|
|
export const setupHooks: PluginSetupHooks = {
|
|
checkSetup,
|
|
};
|