fix(runner): pin Claude native runtime

This commit is contained in:
Dotta 2026-09-03 02:55:40 -05:00
parent cfa4e2fbac
commit e9f6a8ae3c
4 changed files with 78 additions and 25 deletions

View File

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

View File

@ -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<VerifiedAcpxRuntimeExecutable | null> {
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<string, unknown>)[
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;`,

View File

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

View File

@ -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",