From 12fdb57ec82d18712448ccb8b6d40c8c702c4526 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 2 Sep 2026 20:23:30 -0500 Subject: [PATCH] fix(runner): qualify Codex ACP runtime executable --- .../backends/native-backend-factory.test.ts | 39 ++- .../acpx/installation-integrity.test.ts | 13 + .../drivers/acpx/installation-integrity.ts | 289 +++++++++++++++++- .../drivers/acpx/qualified-profiles.test.ts | 7 + .../src/drivers/acpx/qualified-profiles.ts | 4 +- 5 files changed, 328 insertions(+), 24 deletions(-) diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index 57f4074912..e70ae1ac44 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -91,8 +91,13 @@ function acpxExecution( agentServerVersion: agent === "codex" ? "1.6.2" : agent === "pi" ? "0.0.33" : "0.70.0", agentRuntimePackage: - agent === "pi" ? "@earendil-works/pi-coding-agent" : null, - agentRuntimeVersion: agent === "pi" ? "0.84.2" : null, + agent === "pi" + ? "@earendil-works/pi-coding-agent" + : agent === "codex" + ? "@openai/codex" + : null, + agentRuntimeVersion: + agent === "pi" ? "0.84.2" : agent === "codex" ? "0.148.0" : null, commandDigest: agent === "codex" ? "sha256:94049b3e3c3aee87de62703786e4fa81d031d7bd979f99bdf516d84f28791a79" @@ -168,12 +173,16 @@ function managedExecution( profileId: "profile", region: "us-east-1", accountId: "123456789012", - harnessArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/test", + harnessArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/test", harnessVersion: "1", - endpointArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:endpoint/test", + endpointArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:endpoint/test", endpointQualifier: "1", - agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test", - memoryArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/test", + agentRuntimeArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test", + memoryArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/test", memoryId: "memory", invocationRoleArn: "arn:aws:iam::123456789012:role/runner", contextBucket: "context-bucket", @@ -221,9 +230,9 @@ describe("native backend factory", () => { ); it("requires an explicit runtime root for OpenCode", () => { - expect(() => - createNativeSessionBackend(opencodeExecution()), - ).toThrow("OpenCode native backend requires an instance runtime directory"); + expect(() => createNativeSessionBackend(opencodeExecution())).toThrow( + "OpenCode native backend requires an instance runtime directory", + ); }); it("routes OpenCode through runnerd when a durable transport is supplied", async () => { @@ -247,8 +256,16 @@ describe("native backend factory", () => { }); it.each([ - ["claude_managed" as const, "claude_managed_agents_api", "managed-agents-2026-04-01"], - ["aws_agentcore" as const, "aws_agentcore_harness_api", "aws-agentcore-harness-v1"], + [ + "claude_managed" as const, + "claude_managed_agents_api", + "managed-agents-2026-04-01", + ], + [ + "aws_agentcore" as const, + "aws_agentcore_harness_api", + "aws-agentcore-harness-v1", + ], ])("routes %s through runnerd", async (kind, name, version) => { const backend = createNativeSessionBackend(managedExecution(kind), { codexTransportFactory: () => { 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 85ef93e40c..87e56f64ed 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts @@ -300,6 +300,19 @@ describe("ACPX installation integrity", () => { }); }); + it.runIf(process.platform === "linux" && process.arch === "x64")( + "resolves and pins the qualified Codex native runtime through its transitive packages", + async () => { + const profile = resolveQualifiedAcpxProfile("codex", "gpt-5.6-sol"); + const installation = await verifyQualifiedAcpxInstallation(profile); + expect(installation.agentRuntimePackageJsonPath).toContain( + "/@openai/codex/package.json", + ); + const command = await installation.openCommand(); + await command.close(); + }, + ); + it("rejects package version and executable digest drift", async () => { const fixture = await installationFixture(); await writeFile( diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index 7c07a96c20..aef1e5a449 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -32,6 +32,7 @@ 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 COMMAND_SOURCE_FD = 3; const COMMAND_DIRECTORY_FD = 4; const DEPENDENCY_ANCESTOR_FD_START = 5; @@ -39,6 +40,15 @@ const MAX_DEPENDENCY_ANCESTORS = 64; const PROVIDER_WATCHDOG_HANDSHAKE_TIMEOUT_MS = 2_000; const PROVIDER_GUARDIAN_HANDSHAKE_TIMEOUT_MS = 5_000; +const QUALIFIED_CODEX_LINUX_X64_RUNTIME = Object.freeze({ + 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", +}); + const PROVIDER_LIFETIME_WATCHDOG_SOURCE = ` const fs = require("node:fs"); let reaped = false; @@ -74,13 +84,15 @@ const { spawn } = require("node:child_process"); const WATCHDOG_SOURCE = ${JSON.stringify(PROVIDER_LIFETIME_WATCHDOG_SOURCE)}; const runtimeExecutable = process.env.${VERIFIED_RUNTIME_EXECUTABLE_ENV} || process.execPath; const dependencyAncestorCount = Number.parseInt(process.argv[4], 10); +const providerRuntimeExecutableCount = Number.parseInt(process.argv[8], 10); if (!Number.isSafeInteger(dependencyAncestorCount) || dependencyAncestorCount < 0 || dependencyAncestorCount > ${MAX_DEPENDENCY_ANCESTORS}) throw new Error("ACPX provider dependency ancestry is invalid"); -const OWNER_FD = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount; +if (providerRuntimeExecutableCount !== 0 && providerRuntimeExecutableCount !== 1) throw new Error("ACPX provider runtime executable count is invalid"); +const PROVIDER_RUNTIME_EXECUTABLE_FD = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount; +const OWNER_FD = PROVIDER_RUNTIME_EXECUTABLE_FD + providerRuntimeExecutableCount; const OWNERSHIP_FD = OWNER_FD + 1; const PROVIDER_EXIT_FD = OWNERSHIP_FD + 1; const CREDENTIAL_FENCE_FD_START = PROVIDER_EXIT_FD + 1; const dependencyAncestorFds = Array.from({ length: dependencyAncestorCount }, (_, index) => ${DEPENDENCY_ANCESTOR_FD_START} + index); -const PROVIDER_GUARDIAN_FD = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount; let provider; let watchdog; let reaped = false; @@ -127,7 +139,7 @@ const startProvider = () => { // The provider observes this guardian-owned pipe directly. Kernel EOF // therefore revokes it even when SIGKILL/OOM prevents our JS reap path. // It also inherits both quorum fences until that self-reap completes. - stdio: [0, 1, 2, ${COMMAND_SOURCE_FD}, ${COMMAND_DIRECTORY_FD}, ...dependencyAncestorFds, "pipe", PROVIDER_EXIT_FD, CREDENTIAL_FENCE_FD_START, CREDENTIAL_FENCE_FD_START + 1], + stdio: [0, 1, 2, ${COMMAND_SOURCE_FD}, ${COMMAND_DIRECTORY_FD}, ...dependencyAncestorFds, ...(providerRuntimeExecutableCount === 1 ? [PROVIDER_RUNTIME_EXECUTABLE_FD] : []), "pipe", PROVIDER_EXIT_FD, CREDENTIAL_FENCE_FD_START, CREDENTIAL_FENCE_FD_START + 1], windowsHide: true, }, ); @@ -309,6 +321,19 @@ interface VerifiedAcpxCommandIdentity { changedNanoseconds: string; } +interface VerifiedAcpxRuntimeExecutable { + path: string; + digest: string; + identity: VerifiedAcpxCommandIdentity; +} + +interface AcpxPackageMetadata { + version?: string; + bin?: unknown; + type?: unknown; + optionalDependencies?: unknown; +} + interface VerifiedAcpxDirectoryIdentity { device: string; inode: string; @@ -377,14 +402,20 @@ export async function verifyQualifiedAcpxInstallation( let runtimePackageJsonPath: string | null = null; let runtimePackageFormat: AcpxCommandFormat | null = null; + let runtimePackage: AcpxPackageMetadata | null = null; + let runtimeExecutable: VerifiedAcpxRuntimeExecutable | null = null; if (profile.agentRuntimePackage !== null) { if (profile.agentRuntimeVersion === null) { throw new Error("Qualified ACPX runtime package omitted its version"); } runtimePackageJsonPath = await realpath( - resolvePackageJson(profile.agentRuntimePackage), + resolvePackageJsonFrom( + profile.agentRuntimePackage, + serverPackageJsonPath, + resolvePackageJson, + ), ); - const runtimePackage = await readPackageJson( + runtimePackage = await readPackageJson( runtimePackageJsonPath, profile.agentRuntimePackage, ); @@ -394,6 +425,12 @@ export async function verifyQualifiedAcpxInstallation( ); } runtimePackageFormat = packageModuleFormat(runtimePackage.type); + runtimeExecutable = await verifyQualifiedRuntimeExecutable({ + profile, + runtimePackage, + runtimePackageJsonPath, + resolvePackageJson, + }); } else if (profile.agentRuntimeVersion !== null) { throw new Error("Qualified ACPX runtime version omitted its package"); } @@ -456,9 +493,24 @@ export async function verifyQualifiedAcpxInstallation( ); } let currentDependencyAncestors: FileHandle[] = []; + let currentRuntimeExecutable: FileHandle | null = null; try { currentDependencyAncestors = await openDependencyAncestors(dependencyAncestors); + if (runtimeExecutable !== null) { + const current = await openVerifiedRuntimeExecutable( + runtimeExecutable.path, + runtimeExecutable.digest, + profile.agent, + ); + if (!sameIdentity(current.identity, runtimeExecutable.identity)) { + await current.handle.close(); + throw new Error( + "ACPX provider runtime executable identity changed after verification", + ); + } + currentRuntimeExecutable = current.handle; + } const current = await inspectCommand( commandPath, commandDigest, @@ -480,11 +532,15 @@ export async function verifyQualifiedAcpxInstallation( serverDependencyAncestorCount, serverPackageFormat, dependencyAncestorFormats, + currentRuntimeExecutable, ); } catch (error) { await Promise.all([ currentDirectory.handle.close(), ...currentDependencyAncestors.map((handle) => handle.close()), + ...(currentRuntimeExecutable === null + ? [] + : [currentRuntimeExecutable.close()]), ]); throw error; } @@ -502,10 +558,28 @@ function defaultPackageJsonResolver(packageName: string): string { return createRequire(import.meta.url).resolve(`${packageName}/package.json`); } +function resolvePackageJsonFrom( + packageName: string, + parentPackageJsonPath: string, + resolvePackageJson: AcpxPackageJsonResolver, +): string { + try { + return resolvePackageJson(packageName); + } catch (primaryError) { + try { + return createRequire(parentPackageJsonPath).resolve( + `${packageName}/package.json`, + ); + } catch { + throw primaryError; + } + } +} + async function readPackageJson( packageJsonPath: string, packageName: string, -): Promise<{ version?: string; bin?: unknown; type?: unknown }> { +): Promise { const bytes = await readBoundedRegularFile( packageJsonPath, MAX_PACKAGE_JSON_BYTES, @@ -520,7 +594,89 @@ async function readPackageJson( if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new Error(`ACPX package ${packageName} has invalid package metadata`); } - return value as { version?: string; bin?: unknown; type?: unknown }; + return value as AcpxPackageMetadata; +} + +async function verifyQualifiedRuntimeExecutable(input: { + profile: QualifiedAcpxProfile; + runtimePackage: AcpxPackageMetadata; + runtimePackageJsonPath: string; + resolvePackageJson: AcpxPackageJsonResolver; +}): Promise { + if (input.profile.agent !== "codex") return null; + if ( + input.profile.agentRuntimePackage !== "@openai/codex" || + input.profile.agentRuntimeVersion !== "0.148.0" + ) { + throw new Error("ACPX codex 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", + ); + } + + const optionalDependencies = input.runtimePackage.optionalDependencies; + if ( + typeof optionalDependencies !== "object" || + optionalDependencies === null || + Array.isArray(optionalDependencies) || + (optionalDependencies as Record)[ + QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageName + ] !== QUALIFIED_CODEX_LINUX_X64_RUNTIME.dependencyDeclaration + ) { + throw new Error( + "ACPX codex runtime omitted its qualified Linux executable package", + ); + } + + const executablePackageJsonPath = await realpath( + resolvePackageJsonFrom( + QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageName, + input.runtimePackageJsonPath, + input.resolvePackageJson, + ), + ); + const executablePackage = await readPackageJson( + executablePackageJsonPath, + QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageName, + ); + if ( + executablePackage.version !== + QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageVersion + ) { + throw new Error( + `ACPX codex runtime executable package version mismatch: expected ${QUALIFIED_CODEX_LINUX_X64_RUNTIME.packageVersion}, received ${executablePackage.version ?? "unknown"}`, + ); + } + + const packageDirectory = dirname(executablePackageJsonPath); + const unresolvedExecutablePath = resolve( + packageDirectory, + QUALIFIED_CODEX_LINUX_X64_RUNTIME.relativeExecutable, + ); + if (!isInside(packageDirectory, unresolvedExecutablePath)) { + throw new Error("ACPX codex 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"); + } + const executablePath = resolve( + executableDirectory, + basename(unresolvedExecutablePath), + ); + const verified = await openVerifiedRuntimeExecutable( + executablePath, + QUALIFIED_CODEX_LINUX_X64_RUNTIME.executableDigest, + input.profile.agent, + ); + await verified.handle.close(); + return { + path: executablePath, + digest: QUALIFIED_CODEX_LINUX_X64_RUNTIME.executableDigest, + identity: verified.identity, + }; } async function readBoundedRegularFile( @@ -612,6 +768,96 @@ async function inspectCommand( } } +async function openVerifiedRuntimeExecutable( + executablePath: string, + expectedDigest: string, + agent: string, +): Promise<{ handle: FileHandle; identity: VerifiedAcpxCommandIdentity }> { + const lexicalBefore = await lstat(executablePath, { bigint: true }).catch( + () => null, + ); + if ( + lexicalBefore === null || + lexicalBefore.isSymbolicLink() || + !lexicalBefore.isFile() + ) { + throw new Error( + `ACPX ${agent} runtime executable must be a real regular file`, + ); + } + + let handle: FileHandle; + try { + handle = await open( + executablePath, + verifiedExecutableOpenFlags(process.platform, constants.O_NOFOLLOW), + ); + } catch { + throw new Error( + `ACPX ${agent} runtime executable could not be opened as a no-follow regular file`, + ); + } + + try { + const before = await handle.stat({ bigint: true }); + if ( + !before.isFile() || + before.size < 1n || + before.size > BigInt(MAX_RUNTIME_EXECUTABLE_BYTES) || + (before.mode & 0o111n) === 0n + ) { + throw new Error( + `ACPX ${agent} runtime executable must be a bounded executable file`, + ); + } + const hash = createHash("sha256"); + const buffer = Buffer.alloc(1024 * 1024); + let position = 0; + try { + while (position < Number(before.size)) { + const { bytesRead } = await handle.read( + buffer, + 0, + Math.min(buffer.length, Number(before.size) - position), + position, + ); + if (bytesRead === 0) break; + hash.update(buffer.subarray(0, bytesRead)); + position += bytesRead; + } + } finally { + buffer.fill(0); + } + const after = await handle.stat({ bigint: true }); + const lexicalAfter = await lstat(executablePath, { bigint: true }).catch( + () => null, + ); + const beforeIdentity = fileIdentity(before); + const afterIdentity = fileIdentity(after); + if ( + position !== Number(before.size) || + lexicalAfter === null || + lexicalAfter.isSymbolicLink() || + !lexicalAfter.isFile() || + !sameIdentity(fileIdentity(lexicalBefore), fileIdentity(lexicalAfter)) || + !sameIdentity(fileIdentity(lexicalAfter), afterIdentity) || + !sameIdentity(beforeIdentity, afterIdentity) + ) { + throw new Error( + `ACPX ${agent} runtime executable changed while it was verified`, + ); + } + const digest = `sha256:${hash.digest("hex")}`; + if (digest !== expectedDigest) { + throw new Error(`ACPX ${agent} runtime executable digest mismatch`); + } + return { handle, identity: afterIdentity }; + } catch (error) { + await handle.close(); + throw error; + } +} + /** Fail closed where Node cannot atomically refuse a final symlink component. */ export function verifiedExecutableOpenFlags( platform: NodeJS.Platform, @@ -794,6 +1040,7 @@ function commandLease( serverDependencyAncestorCount: number, serverPackageFormat: AcpxCommandFormat, dependencyAncestorFormats: readonly AcpxCommandFormat[], + providerRuntimeExecutable: FileHandle | null, ): VerifiedAcpxCommandLease { let consumed = false; let directoriesReleased = false; @@ -803,6 +1050,9 @@ function commandLease( await Promise.all([ commandDirectory.close(), ...dependencyAncestors.map((handle) => handle.close()), + ...(providerRuntimeExecutable === null + ? [] + : [providerRuntimeExecutable.close()]), ]); }; const releaseDirectoriesBestEffort = (): void => { @@ -833,8 +1083,13 @@ function commandLease( : format === "module" ? MODULE_SNAPSHOT_BOOTSTRAP : COMMONJS_SNAPSHOT_BOOTSTRAP; - const providerOwnershipFd = - DEPENDENCY_ANCESTOR_FD_START + dependencyAncestors.length + 1; + const providerRuntimeExecutableCount = + providerRuntimeExecutable === null ? 0 : 1; + const providerGuardianFd = + DEPENDENCY_ANCESTOR_FD_START + + dependencyAncestors.length + + providerRuntimeExecutableCount; + const providerOwnershipFd = providerGuardianFd + 1; const providerExitFd = providerOwnershipFd + 1; if ( guarded && @@ -867,6 +1122,7 @@ function commandLease( String(serverDependencyAncestorCount), serverPackageFormat, JSON.stringify(dependencyAncestorFormats), + String(providerRuntimeExecutableCount), ...args, ] : [ @@ -879,6 +1135,7 @@ function commandLease( String(serverDependencyAncestorCount), serverPackageFormat, JSON.stringify(dependencyAncestorFormats), + String(providerRuntimeExecutableCount), ...args, ], { @@ -898,6 +1155,9 @@ function commandLease( "pipe", commandDirectory.fd, ...dependencyAncestors.map((handle) => handle.fd), + ...(providerRuntimeExecutable === null + ? [] + : [providerRuntimeExecutable.fd]), "pipe", "pipe", "pipe", @@ -910,6 +1170,9 @@ function commandLease( "pipe", commandDirectory.fd, ...dependencyAncestors.map((handle) => handle.fd), + ...(providerRuntimeExecutable === null + ? [] + : [providerRuntimeExecutable.fd]), ], }, ); @@ -1127,13 +1390,17 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { "const serverDependencyAncestorCount = Number.parseInt(process.argv[4], 10);", "const serverPackageFormat = process.argv[5];", "const dependencyAncestorFormats = JSON.parse(process.argv[6]);", + "const providerRuntimeExecutableCount = Number.parseInt(process.argv[7], 10);", '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; }', ...(guarded ? [ - `const guardianFd = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount;`, + `const guardianFd = ${DEPENDENCY_ANCESTOR_FD_START} + dependencyAncestorCount + providerRuntimeExecutableCount;`, 'const guardian = fs.createReadStream("", { fd: guardianFd, autoClose: false });', `const reapCurrentProviderProcessGroup = ${reapCurrentProviderProcessGroup.toString()};`, "const killProviderProcess = process.kill.bind(process);", @@ -1155,7 +1422,7 @@ function snapshotBootstrap(format: AcpxCommandFormat, guarded = false): string { "const directoryUrl = pathToFileURL(`${directory}/`).href;", "const pinnedTarget = new URL(commandName, directoryUrl).href;", 'const target = process.platform === "linux" ? pinnedTarget : pathToFileURL(commandPath).href;', - "process.argv.splice(1, 6, fileURLToPath(target));", + "process.argv.splice(1, 7, fileURLToPath(target));", `const dependencyDirectoryUrls = Array.from({ length: dependencyAncestorCount }, (_, index) => pathToFileURL("/proc/self/fd/" + (${DEPENDENCY_ANCESTOR_FD_START} + index) + "/").href);`, 'const canonicalRootUrl = (url) => pathToFileURL(fs.realpathSync(fileURLToPath(url))).href.replace(/\\/?$/, "/");', 'const canonicalDirectoryUrl = process.platform === "linux" ? canonicalRootUrl(directoryUrl) : directoryUrl;', 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 142115db99..95fe6452f7 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.test.ts @@ -23,4 +23,11 @@ describe("qualified ACPX profiles", () => { resolveQualifiedAcpxProfile("codex", "some-other-model"), ).toThrow("requires exact model"); }); + + it("binds Codex ACP to the CLI runtime it launches", () => { + expect(QUALIFIED_ACPX_PROFILES.codex).toMatchObject({ + agentRuntimePackage: "@openai/codex", + agentRuntimeVersion: "0.148.0", + }); + }); }); diff --git a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts index 403a334110..79b287100c 100644 --- a/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts +++ b/packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts @@ -78,8 +78,8 @@ export const QUALIFIED_ACPX_PROFILES: Readonly< agentProfileVersion: 1, agentServerPackage: "@agentclientprotocol/codex-acp", agentServerVersion: "1.6.2", - agentRuntimePackage: null, - agentRuntimeVersion: null, + agentRuntimePackage: "@openai/codex", + agentRuntimeVersion: "0.148.0", commandDigest: "sha256:94049b3e3c3aee87de62703786e4fa81d031d7bd979f99bdf516d84f28791a79", qualificationModel: "gpt-5.6-sol",