diff --git a/package.json b/package.json index 7d383fe100..744719a708 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "acpx@0.12.0": "patches/acpx@0.12.0.patch", "acpx@0.13.1": "patches/acpx@0.13.1.patch", "@agentclientprotocol/claude-agent-acp@0.70.0": "patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch", + "@agentclientprotocol/claude-agent-acp@0.73.0": "patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch", "@agentclientprotocol/codex-acp@1.6.2": "patches/@agentclientprotocol__codex-acp@1.6.2.patch" }, "overrides": { diff --git a/packages/adapters/claude-local/package.json b/packages/adapters/claude-local/package.json index b6bdb2c895..69e902d00b 100644 --- a/packages/adapters/claude-local/package.json +++ b/packages/adapters/claude-local/package.json @@ -53,7 +53,7 @@ "probe:quota:raw": "pnpm exec tsx src/cli/quota-probe.ts --json --raw-cli" }, "dependencies": { - "@agentclientprotocol/claude-agent-acp": "^0.70.0", + "@agentclientprotocol/claude-agent-acp": "^0.73.0", "@anthropic-ai/sdk": "0.121.0", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/packages/adapters/claude-local/src/index.ts b/packages/adapters/claude-local/src/index.ts index 35de6e36ac..703a69eef1 100644 --- a/packages/adapters/claude-local/src/index.ts +++ b/packages/adapters/claude-local/src/index.ts @@ -6,6 +6,7 @@ export const SANDBOX_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code export const models = [ { id: "claude-opus-4-8", label: "Claude Opus 4.8" }, { id: "claude-sonnet-5", label: "Claude Sonnet 5" }, + { id: "claude-fable-5-1", label: "Claude Fable 5.1" }, { id: "claude-fable-5", label: "Claude Fable 5" }, { id: "claude-mythos-5", label: "Claude Mythos 5" }, { id: "claude-opus-5", label: "Claude Opus 5" }, diff --git a/packages/adapters/claude-local/src/server/acp.test.ts b/packages/adapters/claude-local/src/server/acp.test.ts index 0406d345a5..3ec97c6813 100644 --- a/packages/adapters/claude-local/src/server/acp.test.ts +++ b/packages/adapters/claude-local/src/server/acp.test.ts @@ -514,6 +514,30 @@ describe("claude_local ACP lane", () => { expect(settings.permissions.allow).toEqual(expect.arrayContaining(["Bash(curl:*)", "Bash(env)"])); }); + it("passes the exact configured Fable 5.1 ID through ANTHROPIC_MODEL on the ACP lane", async () => { + const root = await makeTempRoot("paperclip-claude-acp-fable51-"); + const meta: AdapterInvocationMeta[] = []; + const execute = createClaudeAcpExecutor({ + createRuntime: (options: FakeRuntimeOptions) => new FakeRuntime(options) as never, + }); + + const result = await execute(buildContext(root, { + config: { + engine: "acp", + cwd: root, + stateDir: path.join(root, "state"), + model: "claude-fable-5-1", + promptTemplate: "Do the assigned work.", + }, + onMeta: async (payload: AdapterInvocationMeta) => { + meta.push(payload); + }, + })); + + expect(result.exitCode).toBe(0); + expect(meta[0]?.env?.ANTHROPIC_MODEL).toBe("claude-fable-5-1"); + }); + it("creates the ACP session on the in-sandbox workspace cwd for runner-backed remote runs", async () => { const root = await makeTempRoot("paperclip-claude-acp-remote-cwd-"); const localCwd = path.join(root, "worktree"); diff --git a/packages/adapters/claude-local/src/server/cli-capabilities.ts b/packages/adapters/claude-local/src/server/cli-capabilities.ts index fb345b3010..5fa3ba7cb5 100644 --- a/packages/adapters/claude-local/src/server/cli-capabilities.ts +++ b/packages/adapters/claude-local/src/server/cli-capabilities.ts @@ -4,6 +4,13 @@ import path from "node:path"; const effortFlagSupportCache = new Map>(); +export const CLAUDE_FABLE_5_1_MIN_CLI_VERSION = "2.1.251"; + +const CLAUDE_FABLE_5_1_MODEL_IDS = new Set([ + "claude-fable-5-1", + "us.anthropic.claude-fable-5-1", +]); + export function claudeCommandLooksLike(command: string, expected = "claude"): boolean { const base = path.basename(command).toLowerCase(); return base === expected || base === `${expected}.cmd` || base === `${expected}.exe`; @@ -33,6 +40,78 @@ function cacheKeyForTarget(command: string, target: AdapterExecutionTarget | nul ].join(":"); } +export function minimumClaudeCliVersionForModel(model: string): string | null { + return CLAUDE_FABLE_5_1_MODEL_IDS.has(model.trim()) + ? CLAUDE_FABLE_5_1_MIN_CLI_VERSION + : null; +} + +export function parseClaudeCliVersion(output: string): string | null { + return output.match(/\b(\d+)\.(\d+)\.(\d+)\b/)?.[0] ?? null; +} + +export function claudeCliVersionAtLeast(version: string, minimum: string): boolean { + const parsedVersion = version.split(".").map(Number); + const parsedMinimum = minimum.split(".").map(Number); + if ( + parsedVersion.length !== 3 || + parsedMinimum.length !== 3 || + parsedVersion.some((part) => !Number.isInteger(part) || part < 0) || + parsedMinimum.some((part) => !Number.isInteger(part) || part < 0) + ) { + return false; + } + + for (let index = 0; index < parsedMinimum.length; index += 1) { + if (parsedVersion[index] !== parsedMinimum[index]) { + return parsedVersion[index] > parsedMinimum[index]; + } + } + return true; +} + +async function probeClaudeCommandVersion(input: { + runId: string; + command: string; + target: AdapterExecutionTarget | null | undefined; + cwd: string; + env: Record; + timeoutSec: number; + graceSec: number; +}): Promise { + const version = await runAdapterExecutionTargetProcess( + input.runId, + input.target, + input.command, + ["--version"], + { + cwd: input.cwd, + env: input.env, + timeoutSec: Math.max(1, Math.min(input.timeoutSec, 20)), + graceSec: Math.max(1, Math.min(input.graceSec, 5)), + onLog: async () => {}, + }, + ); + + if (version.timedOut || version.exitCode !== 0) return null; + return parseClaudeCliVersion(`${version.stdout}\n${version.stderr}`); +} + +export async function readClaudeCommandVersion(input: { + runId: string; + command: string; + target: AdapterExecutionTarget | null | undefined; + cwd: string; + env: Record; + timeoutSec: number; + graceSec: number; +}): Promise { + // Do not cache this probe: an operator may upgrade Claude Code while the + // Paperclip server is running, and the next Test/run should recover without + // requiring a server restart. + return probeClaudeCommandVersion(input).catch(() => null); +} + async function probeClaudeCommandSupportsEffortFlag(input: { runId: string; command: string; diff --git a/packages/adapters/claude-local/src/server/execute.remote.test.ts b/packages/adapters/claude-local/src/server/execute.remote.test.ts index c0cd542205..ec2eb60f5b 100644 --- a/packages/adapters/claude-local/src/server/execute.remote.test.ts +++ b/packages/adapters/claude-local/src/server/execute.remote.test.ts @@ -13,15 +13,17 @@ const { syncDirectoryToSsh, startAdapterExecutionTargetPaperclipBridge, } = vi.hoisted(() => ({ - runChildProcess: vi.fn(async (): Promise => ({ + runChildProcess: vi.fn(async (_runId: string, _command: string, args: string[]): Promise => ({ exitCode: 0, signal: null, timedOut: false, - stdout: [ - JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-1", model: "claude-sonnet" }), - JSON.stringify({ type: "assistant", session_id: "claude-session-1", message: { content: [{ type: "text", text: "hello" }] } }), - JSON.stringify({ type: "result", session_id: "claude-session-1", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } }), - ].join("\n"), + stdout: args.includes("--version") + ? "2.1.251 (Claude Code)\n" + : [ + JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-1", model: "claude-sonnet" }), + JSON.stringify({ type: "assistant", session_id: "claude-session-1", message: { content: [{ type: "text", text: "hello" }] } }), + JSON.stringify({ type: "result", session_id: "claude-session-1", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } }), + ].join("\n"), stderr: "", pid: 123, startedAt: new Date().toISOString(), @@ -76,12 +78,14 @@ vi.mock("@paperclipai/adapter-utils/execution-target", async () => { }); import { execute } from "./execute.js"; +import { resetClaudeCliCapabilitiesCacheForTests } from "./cli-capabilities.js"; describe("claude remote execution", () => { const cleanupDirs: string[] = []; afterEach(async () => { vi.clearAllMocks(); + resetClaudeCliCapabilitiesCacheForTests(); while (cleanupDirs.length > 0) { const dir = cleanupDirs.pop(); if (!dir) continue; @@ -403,4 +407,126 @@ describe("claude remote execution", () => { expect(result.errorCode).toBe("duplex_channel_lost"); }); + describe("CLI-lane model pass-through", () => { + async function executeWithModel(prefix: string, config: Record) { + const rootDir = await mkdtemp(path.join(os.tmpdir(), prefix)); + cleanupDirs.push(rootDir); + const workspaceDir = path.join(rootDir, "workspace"); + await mkdir(workspaceDir, { recursive: true }); + + const result = await execute({ + runId: "run-model-passthrough", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Claude Coder", + adapterType: "claude_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: "claude", + ...config, + }, + context: { + paperclipWorkspace: { + cwd: workspaceDir, + source: "project_primary", + }, + }, + executionTransport: { + remoteExecution: { + host: "127.0.0.1", + port: 2222, + username: "fixture", + remoteWorkspacePath: "/remote/workspace", + remoteCwd: "/remote/workspace", + privateKey: "PRIVATE KEY", + knownHosts: "[127.0.0.1]:2222 ssh-ed25519 AAAA", + strictHostKeyChecking: true, + }, + }, + onLog: async () => {}, + }); + + const call = runChildProcess.mock.calls.find((candidate) => + (candidate[2] as string[]).includes("--print"), + ) as unknown as [string, string, string[]] | undefined; + return { args: call?.[2] ?? [], result }; + } + + it("passes the exact configured Fable 5.1 ID as --model on the CLI lane", async () => { + const { args } = await executeWithModel("paperclip-claude-model-direct-", { + model: "claude-fable-5-1", + }); + + const modelFlag = args.indexOf("--model"); + expect(modelFlag).toBeGreaterThanOrEqual(0); + expect(args[modelFlag + 1]).toBe("claude-fable-5-1"); + }); + + it("passes the Bedrock-native Fable 5.1 ID as --model under Bedrock auth", async () => { + const { args } = await executeWithModel("paperclip-claude-model-bedrock-", { + model: "us.anthropic.claude-fable-5-1", + env: { CLAUDE_CODE_USE_BEDROCK: "1" }, + }); + + const modelFlag = args.indexOf("--model"); + expect(modelFlag).toBeGreaterThanOrEqual(0); + expect(args[modelFlag + 1]).toBe("us.anthropic.claude-fable-5-1"); + }); + + it("skips --model for a direct Anthropic ID under Bedrock auth", async () => { + const { args } = await executeWithModel("paperclip-claude-model-bedrock-skip-", { + model: "claude-fable-5-1", + env: { CLAUDE_CODE_USE_BEDROCK: "1" }, + }); + + expect(args).not.toContain("--model"); + }); + + it("rejects Fable 5.1 before launch when the CLI is older than 2.1.251", async () => { + runChildProcess.mockResolvedValueOnce({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "2.1.247 (Claude Code)\n", + stderr: "", + pid: 123, + startedAt: new Date().toISOString(), + }); + + const { args, result } = await executeWithModel("paperclip-claude-model-old-cli-", { + model: "claude-fable-5-1", + }); + + expect(args).toEqual([]); + expect(result.errorCode).toBe("claude_cli_version_incompatible"); + expect(result.errorMessage).toContain("requires Claude Code 2.1.251 or newer"); + expect(result.resultJson).toMatchObject({ + requiredClaudeCodeVersion: "2.1.251", + detectedClaudeCodeVersion: "2.1.247", + }); + }); + + it("leaves Fable compatibility to explicitly configured custom CLI wrappers", async () => { + const { args, result } = await executeWithModel("paperclip-claude-model-wrapper-", { + command: "/opt/paperclip/claude-wrapper", + model: "claude-fable-5-1", + }); + + expect(args).toContain("--model"); + expect(args).toContain("claude-fable-5-1"); + expect(result.errorCode).not.toBe("claude_cli_version_incompatible"); + expect(runChildProcess.mock.calls.some((call) => + (call[2] as string[]).includes("--version"), + )).toBe(false); + }); + }); + }); diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index ea8e34b297..0f24419c9f 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -81,7 +81,13 @@ import { resolveSharedClaudeConfigDir, writePaperclipClaudeMcpConfig, } from "./claude-config.js"; -import { claudeCommandSupportsEffortFlag } from "./cli-capabilities.js"; +import { + claudeCliVersionAtLeast, + claudeCommandLooksLike, + claudeCommandSupportsEffortFlag, + minimumClaudeCliVersionForModel, + readClaudeCommandVersion, +} from "./cli-capabilities.js"; import { resolveClaudeDesiredSkillNames } from "./skills.js"; import { isBedrockModelId } from "./models.js"; import { prepareClaudePromptBundle } from "./prompt-cache.js"; @@ -863,6 +869,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise { expect(JSON.stringify(spawnedEnv)).not.toContain("caller-proxy"); }); + it("warns without executing when runtime PATH selects a different local Claude executable", async () => { + const runtimeDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-cli-runtime-path-")); + const runtimeClaudePath = path.join(runtimeDir, "claude"); + await writeFile(runtimeClaudePath, "#!/bin/sh\nexit 0\n"); + await chmod(runtimeClaudePath, 0o755); + + try { + probeResult.value = { exitCode: 0, stdout: "2.1.251 (Claude Code)\n", stderr: "" }; + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { + engine: "cli", + command: "claude", + model: "claude-fable-5-1", + env: { PATH: runtimeDir }, + }, + executionTarget: null, + environmentName: null, + }); + + expect(result.status).toBe("warn"); + expect(result.checks).toContainEqual(expect.objectContaining({ + code: "claude_cli_version_probe_mismatch", + level: "warn", + })); + expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled(); + } finally { + await rm(runtimeDir, { recursive: true, force: true }); + } + }); + it("names the local host target on every result", async () => { probeResult.value = { exitCode: 0, stdout: successStdout, stderr: "" }; diff --git a/packages/adapters/claude-local/src/server/test.remote.test.ts b/packages/adapters/claude-local/src/server/test.remote.test.ts index 3c7f5d5fd1..b8cc0b36d6 100644 --- a/packages/adapters/claude-local/src/server/test.remote.test.ts +++ b/packages/adapters/claude-local/src/server/test.remote.test.ts @@ -49,6 +49,7 @@ vi.mock("@paperclipai/adapter-utils/execution-target", async () => { import { testEnvironment } from "./test.js"; import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js"; +import { resetClaudeCliCapabilitiesCacheForTests } from "./cli-capabilities.js"; const sandboxTarget: AdapterExecutionTarget = { kind: "remote", @@ -94,6 +95,7 @@ const loginRequiredStdout = [ afterEach(() => { vi.clearAllMocks(); + resetClaudeCliCapabilitiesCacheForTests(); }); describe("claude sandbox auth-missing check", () => { @@ -137,3 +139,40 @@ describe("claude sandbox auth-missing check", () => { expect(result.checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(false); }); }); + +describe("claude CLI model compatibility check", () => { + it("fails before the hello probe when Fable 5.1 is configured with an older CLI", async () => { + probeResult.value = { + exitCode: 0, + stdout: "2.1.247 (Claude Code)\n", + stderr: "", + }; + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { + engine: "cli", + command: "claude", + model: "claude-fable-5-1", + }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + expect(result.status).toBe("fail"); + expect(result.checks).toContainEqual(expect.objectContaining({ + code: "claude_cli_version_incompatible", + level: "error", + detail: "Detected Claude Code 2.1.247.", + })); + expect(runAdapterExecutionTargetProcess).toHaveBeenCalledTimes(1); + const versionCall = runAdapterExecutionTargetProcess.mock.calls[0] as unknown as [ + string, + AdapterExecutionTarget, + string, + string[], + ]; + expect(versionCall[3]).toEqual(["--version"]); + }); +}); diff --git a/packages/adapters/claude-local/src/server/test.ts b/packages/adapters/claude-local/src/server/test.ts index f98dc2fb2c..7d98e2ff2a 100644 --- a/packages/adapters/claude-local/src/server/test.ts +++ b/packages/adapters/claude-local/src/server/test.ts @@ -16,6 +16,7 @@ import { ensureAdapterExecutionTargetDirectory, runAdapterExecutionTargetProcess, resolveAdapterExecutionTargetCwd, + resolveAdapterExecutionTargetCommandForLogs, } from "@paperclipai/adapter-utils/execution-target"; import { detectClaudeLoginRequired, @@ -23,7 +24,13 @@ import { isClaudeTransientUpstreamError, parseClaudeStreamJson, } from "./parse.js"; -import { claudeCommandLooksLike, claudeCommandSupportsEffortFlag } from "./cli-capabilities.js"; +import { + claudeCliVersionAtLeast, + claudeCommandLooksLike, + claudeCommandSupportsEffortFlag, + minimumClaudeCliVersionForModel, + readClaudeCommandVersion, +} from "./cli-capabilities.js"; import { isBedrockModelId } from "./models.js"; import { buildClaudeProbePermissionArgs } from "./permissions.js"; import { prepareSandboxClaudeProbeRuntime } from "./claude-config.js"; @@ -47,6 +54,14 @@ function isNonEmpty(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } +function localExecutablesMatch( + trustedCommand: string | null, + runtimeCommand: string | null, +): boolean { + if (!trustedCommand || !runtimeCommand) return false; + return trustedCommand === runtimeCommand; +} + export async function testEnvironment( ctx: AdapterEnvironmentTestContext, ): Promise { @@ -130,8 +145,17 @@ export async function testEnvironment( })), ); const runtimeEnv = ensurePathInEnv({ ...process.env, ...env }); + let localRuntimeCommand: string | null = null; try { await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv); + if (!targetIsRemote) { + localRuntimeCommand = await resolveAdapterExecutionTargetCommandForLogs( + command, + target, + cwd, + runtimeEnv, + ); + } checks.push({ code: "claude_command_resolvable", level: "info", @@ -214,7 +238,62 @@ export async function testEnvironment( check.code !== "claude_command_unresolvable" && check.code !== "claude_managed_config_dir_failed", ); - if (canRunProbe) { + let configuredModelIsCompatible = true; + const configuredModel = asString(config.model, "").trim(); + const minimumCliVersion = + claudeCommandLooksLike(command, "claude") && + (!hasBedrock || isBedrockModelId(configuredModel)) + ? minimumClaudeCliVersionForModel(configuredModel) + : null; + const versionProbeCommand = localProbe?.command ?? (targetIsRemote ? command : null); + const versionProbeMatchesRuntime = targetIsRemote || localExecutablesMatch( + localProbe?.command ?? null, + localRuntimeCommand, + ); + if ( + canRunProbe && + minimumCliVersion && + versionProbeCommand && + !versionProbeMatchesRuntime + ) { + configuredModelIsCompatible = false; + checks.push({ + code: "claude_cli_version_probe_mismatch", + level: "warn", + message: + "Skipped Fable 5.1 readiness probing because the runtime PATH selects a different Claude executable than the trusted local Test probe.", + hint: + "Ensure the runtime-selected Claude Code is 2.1.251 or newer. Execution will verify that exact executable before launch.", + }); + } else if (canRunProbe && minimumCliVersion && versionProbeCommand) { + const versionProbeEnv = localProbe?.env ?? env; + const detectedCliVersion = await readClaudeCommandVersion({ + runId, + command: versionProbeCommand, + target, + cwd, + env: versionProbeEnv, + timeoutSec: 45, + graceSec: 5, + }); + if ( + !detectedCliVersion || + !claudeCliVersionAtLeast(detectedCliVersion, minimumCliVersion) + ) { + configuredModelIsCompatible = false; + checks.push({ + code: "claude_cli_version_incompatible", + level: "error", + message: `Claude Fable 5.1 requires Claude Code ${minimumCliVersion} or newer on the CLI lane.`, + detail: detectedCliVersion + ? `Detected Claude Code ${detectedCliVersion}.` + : "Could not determine the installed Claude Code version.", + hint: "Upgrade Claude Code or restore the default ACP lane, then retry the Test.", + }); + } + } + + if (canRunProbe && configuredModelIsCompatible) { if (!claudeCommandLooksLike(command, "claude")) { checks.push({ code: "claude_hello_probe_skipped_custom_command", @@ -233,7 +312,7 @@ export async function testEnvironment( hint: "Install the `claude` CLI on the Paperclip host, then retry the Test.", }); } else { - const model = asString(config.model, "").trim(); + const model = configuredModel; const effort = asString(config.effort, "").trim(); const chrome = asBoolean(config.chrome, false); const maxTurns = asNumber(config.maxTurnsPerRun, 0); diff --git a/patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch b/patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch new file mode 100644 index 0000000000..f84471a742 --- /dev/null +++ b/patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch @@ -0,0 +1,51 @@ +diff --git a/dist/acp-agent.js b/dist/acp-agent.js +--- a/dist/acp-agent.js ++++ b/dist/acp-agent.js +@@ -2917,10 +2917,20 @@ + cost: { + amount: message.total_cost_usd, + currency: "USD", + }, +- ...(message.origin && { +- _meta: { "_claude/origin": message.origin }, +- }), ++ _meta: { ++ ...(message.origin && { "_claude/origin": message.origin }), ++ // ACP's core Usage shape exposes context occupancy and ++ // cost, but not the billable input/output split. Keep the ++ // SDK's bounded aggregate in extension metadata so ACPX can ++ // normalize it without persisting a raw provider message. ++ usage: { ++ input_tokens: message.usage.input_tokens, ++ output_tokens: message.usage.output_tokens, ++ cache_read_input_tokens: message.usage.cache_read_input_tokens, ++ cache_creation_input_tokens: message.usage.cache_creation_input_tokens, ++ }, ++ }, + }, + }); + } +@@ -5307,8 +5317,12 @@ + const options = { + systemPrompt, + settingSources: ["user", "project", "local"], + ...(thinking !== undefined && { thinking }), + ...userProvidedOptions, ++ // Paperclip Runner owns the complete session context. Its isolated ++ // user root contains only assigned skills; project/local settings ++ // would reintroduce host prompts, plugins, skills, and MCP servers. ++ ...(process.env.PAPERCLIP_ACPX_ISOLATED_CONTEXT === "1" && { settingSources: ["user"] }), + ...(settings && { settings }), + env, + // Override certain fields that must be controlled by ACP +@@ -5317,7 +5331,9 @@ + includePartialMessages: true, + forwardSubagentText, + mcpServers: { +- ...(userProvidedOptions?.mcpServers || {}), ++ ...(process.env.PAPERCLIP_ACPX_ISOLATED_CONTEXT === "1" ++ ? {} ++ : (userProvidedOptions?.mcpServers || {})), + ...mcpServers, + ...(fileChangeAuditSupport + ? { [FILE_CHANGE_AUDIT_SERVER_NAME]: fileChangeAuditSupport.mcpServer } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bef4999a34..1a903feeef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,4 +20,5 @@ patchedDependencies: acpx@0.12.0: patches/acpx@0.12.0.patch acpx@0.13.1: patches/acpx@0.13.1.patch '@agentclientprotocol/claude-agent-acp@0.70.0': patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch + '@agentclientprotocol/claude-agent-acp@0.73.0': patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch '@agentclientprotocol/codex-acp@1.6.2': patches/@agentclientprotocol__codex-acp@1.6.2.patch diff --git a/server/src/__tests__/adapter-models.test.ts b/server/src/__tests__/adapter-models.test.ts index d60a73a353..3b84c652a6 100644 --- a/server/src/__tests__/adapter-models.test.ts +++ b/server/src/__tests__/adapter-models.test.ts @@ -66,6 +66,7 @@ describe("adapter model listing", () => { // Newer flagship models are offered, but Opus 4.8 stays the default (first) option. expect(models[0]?.id).toBe("claude-opus-4-8"); expect(models.some((model) => model.id === "claude-sonnet-5")).toBe(true); + expect(models.some((model) => model.id === "claude-fable-5-1")).toBe(true); expect(models.some((model) => model.id === "claude-fable-5")).toBe(true); expect(models.some((model) => model.id === "claude-mythos-5")).toBe(true); // Opus 5 is a current GA flagship and must be offered even when live discovery is unavailable. @@ -130,6 +131,36 @@ describe("adapter model listing", () => { expect(models).toEqual(claudeFallbackModels); }); + it("does not duplicate claude-fable-5-1 when discovery returns the identical ID", async () => { + process.env.ANTHROPIC_API_KEY = "sk-ant-test"; + vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ + data: [{ id: "claude-fable-5-1", display_name: "Claude Fable 5.1" }], + }), + } as Response); + + const models = await listAdapterModels("claude_local"); + + expect(models.filter((model) => model.id === "claude-fable-5-1")).toHaveLength(1); + // Curated fallbacks discovery did not return are still merged in. + expect(models.some((model) => model.id === "claude-fable-5")).toBe(true); + expect(models.some((model) => model.id === "claude-opus-4-8")).toBe(true); + }); + + it("exposes the Bedrock-native Fable 5.1 ID (never the direct ID) in Bedrock mode", async () => { + process.env.CLAUDE_CODE_USE_BEDROCK = "1"; + const fetchSpy = vi.spyOn(globalThis, "fetch"); + + const models = await listAdapterModels("claude_local"); + + // The Bedrock default (first entry) is unchanged. + expect(models[0]?.id).toBe("us.anthropic.claude-opus-4-8-v1"); + expect(models.some((model) => model.id === "us.anthropic.claude-fable-5-1")).toBe(true); + expect(models.some((model) => model.id === "claude-fable-5-1")).toBe(false); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + it("loads codex models dynamically and merges fallback options", async () => { process.env.OPENAI_API_KEY = "sk-test"; const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({