From e9f6a8ae3caa3383a7dae62fb65fe94522bf5ba4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 3 Sep 2026 02:55:40 -0500 Subject: [PATCH] fix(runner): pin Claude native runtime --- .../acpx/installation-integrity.test.ts | 7 ++ .../drivers/acpx/installation-integrity.ts | 85 ++++++++++++++----- .../drivers/acpx/qualified-profiles.test.ts | 7 ++ .../src/drivers/acpx/qualified-profiles.ts | 4 +- 4 files changed, 78 insertions(+), 25 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts index 4a808a6969..cecf96be27 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts @@ -499,6 +499,8 @@ describe("ACPX installation integrity", () => { ]); const profile = { ...resolveQualifiedAcpxProfile("claude", "claude-sonnet-5"), + agentRuntimePackage: null, + agentRuntimeVersion: null, commandDigest: `sha256:${createHash("sha256").update(command).digest("hex")}`, }; const installation = await verifyQualifiedAcpxInstallation( @@ -542,6 +544,8 @@ describe("ACPX installation integrity", () => { verifyQualifiedAcpxInstallation( { ...resolveQualifiedAcpxProfile("claude", "claude-sonnet-5"), + agentRuntimePackage: null, + agentRuntimeVersion: null, commandDigest: fixture.profile.commandDigest, }, (packageName) => @@ -562,6 +566,9 @@ describe("ACPX installation integrity", () => { expect(installation.agentServerPackageJsonPath).toContain( "/@agentclientprotocol/claude-agent-acp/package.json", ); + expect(installation.agentRuntimePackageJsonPath).toContain( + "/@anthropic-ai/claude-agent-sdk/package.json", + ); await (await installation.openCommand()).close(); }, ); diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index e5a8fcfaf3..65678a8937 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -33,21 +33,38 @@ import { const MAX_PACKAGE_JSON_BYTES = 256 * 1024; const MAX_AGENT_COMMAND_BYTES = 16 * 1024 * 1024; -const MAX_RUNTIME_EXECUTABLE_BYTES = 256 * 1024 * 1024; +const MAX_RUNTIME_EXECUTABLE_BYTES = 384 * 1024 * 1024; const COMMAND_SOURCE_FD = 3; const COMMAND_DIRECTORY_FD = 4; const DEPENDENCY_ANCESTOR_FD_START = 5; const MAX_DEPENDENCY_ANCESTORS = 64; const PROVIDER_WATCHDOG_HANDSHAKE_TIMEOUT_MS = 2_000; const PROVIDER_GUARDIAN_HANDSHAKE_TIMEOUT_MS = 5_000; +const VERIFIED_PROVIDER_RUNTIME_TARGET_ENV = + "PAPERCLIP_ACPX_VERIFIED_PROVIDER_RUNTIME_TARGET"; + +const QUALIFIED_CLAUDE_LINUX_X64_RUNTIME = Object.freeze({ + runtimePackageName: "@anthropic-ai/claude-agent-sdk", + runtimePackageVersion: "0.3.232", + packageName: "@anthropic-ai/claude-agent-sdk-linux-x64", + packageVersion: "0.3.232", + dependencyDeclaration: "0.3.232", + relativeExecutable: "claude", + executableDigest: + "sha256:61d23f8749136907d586d5b11831ea8a5234d4c1dea40a5e55c33b52e204c6d1", + environmentVariable: "CLAUDE_CODE_EXECUTABLE", +}); const QUALIFIED_CODEX_LINUX_X64_RUNTIME = Object.freeze({ + runtimePackageName: "@openai/codex", + runtimePackageVersion: "0.148.0", packageName: "@openai/codex-linux-x64", packageVersion: "0.148.0-linux-x64", dependencyDeclaration: "npm:@openai/codex@0.148.0-linux-x64", relativeExecutable: "vendor/x86_64-unknown-linux-musl/bin/codex", executableDigest: "sha256:ac2cfed85fb647d61e0150b8548102b330e4799d9d81ad5d354de701edf6b074", + environmentVariable: "CODEX_PATH", }); // Claude's ACP server is not a self-contained bundle: its entrypoint imports @@ -445,6 +462,7 @@ interface VerifiedAcpxRuntimeExecutable { path: string; digest: string; identity: VerifiedAcpxCommandIdentity; + environmentVariable: "CLAUDE_CODE_EXECUTABLE" | "CODEX_PATH"; } interface AcpxPackageMetadata { @@ -530,7 +548,7 @@ export async function verifyQualifiedAcpxInstallation( throw new Error("Qualified ACPX runtime package omitted its version"); } runtimePackageJsonPath = await realpath( - resolvePackageJson(profile.agentRuntimePackage), + resolvePackageJson(profile.agentRuntimePackage, serverPackageJsonPath), ); runtimePackage = await readPackageJson( runtimePackageJsonPath, @@ -768,16 +786,24 @@ async function verifyQualifiedRuntimeExecutable(input: { runtimePackageJsonPath: string; resolvePackageJson: AcpxPackageJsonResolver; }): Promise { - if (input.profile.agent !== "codex") return null; + const qualification = + input.profile.agent === "claude" + ? QUALIFIED_CLAUDE_LINUX_X64_RUNTIME + : input.profile.agent === "codex" + ? QUALIFIED_CODEX_LINUX_X64_RUNTIME + : null; + if (qualification === null) return null; if ( - input.profile.agentRuntimePackage !== "@openai/codex" || - input.profile.agentRuntimeVersion !== "0.148.0" + input.profile.agentRuntimePackage !== qualification.runtimePackageName || + input.profile.agentRuntimeVersion !== qualification.runtimePackageVersion ) { - throw new Error("ACPX codex runtime does not match its qualified profile"); + throw new Error( + `ACPX ${input.profile.agent} runtime does not match its qualified profile`, + ); } if (process.platform !== "linux" || process.arch !== "x64") { throw new Error( - "ACPX codex verified runtime executable requires qualified Linux x64", + `ACPX ${input.profile.agent} verified runtime executable requires qualified Linux x64`, ); } @@ -787,41 +813,45 @@ async function verifyQualifiedRuntimeExecutable(input: { optionalDependencies === null || Array.isArray(optionalDependencies) || (optionalDependencies as Record)[ - QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageName - ] !== QUALIFIED_CODEX_LINUX_X64_RUNTIME.dependencyDeclaration + qualification.packageName + ] !== qualification.dependencyDeclaration ) { throw new Error( - "ACPX codex runtime omitted its qualified Linux executable package", + `ACPX ${input.profile.agent} runtime omitted its qualified Linux executable package`, ); } const executablePackageJsonPath = await realpath( - input.resolvePackageJson(QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageName), + input.resolvePackageJson( + qualification.packageName, + input.runtimePackageJsonPath, + ), ); const executablePackage = await readPackageJson( executablePackageJsonPath, - QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageName, + qualification.packageName, ); - if ( - executablePackage.version !== - QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageVersion - ) { + if (executablePackage.version !== qualification.packageVersion) { throw new Error( - `ACPX codex runtime executable package version mismatch: expected ${QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageVersion}, received ${executablePackage.version ?? "unknown"}`, + `ACPX ${input.profile.agent} runtime executable package version mismatch: expected ${qualification.packageVersion}, received ${executablePackage.version ?? "unknown"}`, ); } const packageDirectory = dirname(executablePackageJsonPath); const unresolvedExecutablePath = resolve( packageDirectory, - QUALIFIED_CODEX_LINUX_X64_RUNTIME.relativeExecutable, + qualification.relativeExecutable, ); if (!isInside(packageDirectory, unresolvedExecutablePath)) { - throw new Error("ACPX codex runtime executable escapes its package"); + throw new Error( + `ACPX ${input.profile.agent} runtime executable escapes its package`, + ); } const executableDirectory = await realpath(dirname(unresolvedExecutablePath)); if (!isInsideOrEqual(packageDirectory, executableDirectory)) { - throw new Error("ACPX codex runtime executable escapes its package"); + throw new Error( + `ACPX ${input.profile.agent} runtime executable escapes its package`, + ); } const executablePath = resolve( executableDirectory, @@ -829,14 +859,15 @@ async function verifyQualifiedRuntimeExecutable(input: { ); const verified = await openVerifiedRuntimeExecutable( executablePath, - QUALIFIED_CODEX_LINUX_X64_RUNTIME.executableDigest, + qualification.executableDigest, input.profile.agent, ); await verified.handle.close(); return { path: executablePath, - digest: QUALIFIED_CODEX_LINUX_X64_RUNTIME.executableDigest, + digest: qualification.executableDigest, identity: verified.identity, + environmentVariable: qualification.environmentVariable, }; } @@ -1267,6 +1298,12 @@ function commandLease( const runtimeExecutable = verifiedRuntimeExecutable(); const environment = sanitizedNodeEnvironment(options.env); environment[VERIFIED_RUNTIME_EXECUTABLE_ENV] = runtimeExecutable; + if (providerRuntimeExecutable === null) { + delete environment[VERIFIED_PROVIDER_RUNTIME_TARGET_ENV]; + } else { + environment[VERIFIED_PROVIDER_RUNTIME_TARGET_ENV] = + providerRuntimeExecutable.environmentVariable; + } child = spawnChildProcess( runtimeExecutable, guarded @@ -1552,13 +1589,15 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { "const serverPackageFormat = process.argv[5];", "const dependencyAncestorFormats = JSON.parse(process.argv[6]);", "const providerRuntimeExecutableCount = Number.parseInt(process.argv[7], 10);", + `const providerRuntimeEnvironmentVariable = process.env.${VERIFIED_PROVIDER_RUNTIME_TARGET_ENV};`, + `delete process.env.${VERIFIED_PROVIDER_RUNTIME_TARGET_ENV};`, 'if (process.platform !== "linux") throw new Error("ACPX provider relative module loading requires Linux descriptor-pinned paths");', `if (!Number.isSafeInteger(dependencyAncestorCount) || dependencyAncestorCount < 0 || dependencyAncestorCount > ${MAX_DEPENDENCY_ANCESTORS}) throw new Error("ACPX provider dependency ancestry is invalid");`, 'if (!Number.isSafeInteger(serverDependencyAncestorCount) || serverDependencyAncestorCount < 0 || serverDependencyAncestorCount > dependencyAncestorCount) throw new Error("ACPX provider package ancestry is invalid");', 'if ((serverPackageFormat !== "module" && serverPackageFormat !== "commonjs") || !Array.isArray(dependencyAncestorFormats) || dependencyAncestorFormats.length !== dependencyAncestorCount || dependencyAncestorFormats.some((value) => value !== "module" && value !== "commonjs")) throw new Error("ACPX provider package formats are invalid");', 'if (providerRuntimeExecutableCount !== 0 && providerRuntimeExecutableCount !== 1) throw new Error("ACPX provider runtime executable count is invalid");', `const providerRuntimeExecutableFd = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount;`, - 'if (providerRuntimeExecutableCount === 1) { fs.fstatSync(providerRuntimeExecutableFd); process.env.CODEX_PATH = "/proc/" + process.pid + "/fd/" + providerRuntimeExecutableFd; }', + 'if (providerRuntimeExecutableCount === 1) { if (providerRuntimeEnvironmentVariable !== "CODEX_PATH" && providerRuntimeEnvironmentVariable !== "CLAUDE_CODE_EXECUTABLE") throw new Error("ACPX provider runtime environment target is invalid"); fs.fstatSync(providerRuntimeExecutableFd); process.env[providerRuntimeEnvironmentVariable] = "/proc/" + process.pid + "/fd/" + providerRuntimeExecutableFd; } else if (providerRuntimeEnvironmentVariable !== undefined) throw new Error("ACPX provider runtime environment target is unexpected");', ...(guarded ? [ `const guardianFd = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount + providerRuntimeExecutableCount;`, diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts index 95fe6452f7..3b8fa9135c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -30,4 +30,11 @@ describe("qualified ACPX profiles", () => { agentRuntimeVersion: "0.148.0", }); }); + + it("binds Claude ACP to the SDK and native CLI runtime it launches", () => { + expect(QUALIFIED_ACPX_PROFILES.claude).toMatchObject({ + agentRuntimePackage: "@anthropic-ai/claude-agent-sdk", + agentRuntimeVersion: "0.3.232", + }); + }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index 3d28a9467b..7a84fc6411 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -62,8 +62,8 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< agentProfileVersion: 1, agentServerPackage: "@agentclientprotocol/claude-agent-acp", agentServerVersion: "0.70.0", - agentRuntimePackage: null, - agentRuntimeVersion: null, + agentRuntimePackage: "@anthropic-ai/claude-agent-sdk", + agentRuntimeVersion: "0.3.232", commandDigest: "sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a", qualificationModel: "claude-sonnet-5",