fix(runner): qualify Codex ACP runtime executable

This commit is contained in:
Dotta 2026-09-02 20:23:30 -05:00
parent 805177f7d6
commit 12fdb57ec8
5 changed files with 328 additions and 24 deletions

View File

@ -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: () => {

View File

@ -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(

View File

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

View File

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

View File

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