fix: retain Codex sandbox resources after npm platform installation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 11:17:44 -05:00
1 parent f17c8c3bbf
commit 6baec5181f
7 files changed
+179 -11

No files matched your search

@@ -54,6 +54,9 @@ The existing installer checks must prove both native and legacy version selectio
and the runtime must still verify the selected executable's version and digest.
Direct/source runtimes retain their own dependency declarations. A CI credential
test fixture also reserves its ports before making its unchanged denial assertion.
Installed-consumer verification also checks the native vendor sandbox read roots
and their rendered isolation configuration; a version probe alone does not prove
that Codex can invoke its native resources for shell tools.
Reopen packaging and installed-consumer verification for this correction.
The green checks at `36d9eb0c3c6b9ca5234432b97349dc7317cfa916` are historical;
+2
View File
@@ -73,6 +73,8 @@ belong to the published server manifest; the bundled JavaScript wrapper delegate
platform installation there so npm can keep native and legacy versions separate.
The wrapper code and patched ACP bridge stay unchanged. Docker images
preinstall their qualified runtime dependencies during the image build.
Codex sandbox read roots include the separately installed native vendor resources
without granting access to enclosing npm directories or credential homes.
Release assembly checks the source revision and Paperclip runner binary
identities before publication. This packaging does not change agent defaults or
the selected runner of an existing agent.
@@ -632,7 +632,7 @@ export async function verifyQualifiedAcpxInstallation(
): Promise<VerifiedAcpxInstallation> {
// Keep the selected package authority available when its bundled Codex
// wrapper delegates platform installation to the published server manifest.
if (resolvePackageJson === defaultPackageJsonResolver && process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT !== undefined) {
if (profile.agent === "codex" && resolvePackageJson === defaultPackageJsonResolver && process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT !== undefined) {
resolvePackageJson = createAcpxPackageJsonResolver(
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT,
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST,
@@ -32,7 +32,7 @@ describe("Codex security configuration", () => {
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { resolvePinnedCodexCommand } from ${JSON.stringify(new URL("./codex-command.ts", import.meta.url).href)};
import { codexExecutableReadOnlyRoots } from ${JSON.stringify(new URL("./codex-security-config.ts", import.meta.url).href)};
import { codexExecutableReadOnlyRoots, createIsolatedCodexAppServerArgs } from ${JSON.stringify(new URL("./codex-security-config.ts", import.meta.url).href)};
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperclip-pinned-codex-test-")));
const issuer = join(root, "node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/codex/codex-command.js");
const adapter = join(root, "node_modules/@paperclipai/adapter-codex-local");
@@ -51,7 +51,9 @@ describe("Codex security configuration", () => {
writeFileSync(join(bridge, "package.json"), JSON.stringify({ name: "@agentclientprotocol/codex-acp", version: "1.6.2" }));
writeFileSync(join(codex, "package.json"), JSON.stringify(metadata));
mkdirSync(vendor, { recursive: true });
writeFileSync(join(platform, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.160.0-" + process.platform + "-" + process.arch }));
const platformMetadata = { name: "@openai/codex", version: "0.160.0-" + process.platform + "-" + process.arch,
os: [process.platform], cpu: [process.arch] };
writeFileSync(join(platform, "package.json"), JSON.stringify(platformMetadata));
writeFileSync(executable, "#!" + process.execPath + "\\nif (process.argv.slice(2).join(' ') !== '--version') process.exit(9); console.log('codex-cli 0.160.0');\\n", { mode: 0o755 });
assert.equal(resolvePinnedCodexCommand(issuer), executable);
const roots = codexExecutableReadOnlyRoots({ PATH: "/missing-codex-command" }, executable);
@@ -77,6 +79,67 @@ describe("Codex security configuration", () => {
assert.throws(() => resolvePinnedCodexCommand(issuer), /runtime unavailable/);
rmSync(codex, { recursive: true });
assert.throws(() => resolvePinnedCodexCommand(issuer), /runtime unavailable.*Reinstall/);
// npm installs the host alias declared by the published server while
// its bundled JS wrapper deliberately omits optionalDependencies.
const server = join(root, "node_modules/@paperclipai/server");
const serverManifest = join(server, "package.json");
const normalized = join(server, "node_modules/@openai/codex");
const normalizedManifest = join(normalized, "package.json");
const normalizedCommand = join(normalized, "bin/codex.js");
const publishedBridgeManifest = join(server, "node_modules/@agentclientprotocol/codex-acp/package.json");
const publishedBridgeMetadata = { name: "@agentclientprotocol/codex-acp", version: "1.6.2" };
const serverMetadata = { name: "@paperclipai/server", dependencies: { "@agentclientprotocol/codex-acp": "1.6.2" },
optionalDependencies: metadata.optionalDependencies };
const normalizedMetadata = { ...metadata, optionalDependencies: undefined };
mkdirSync(dirname(normalizedCommand), { recursive: true });
mkdirSync(dirname(publishedBridgeManifest), { recursive: true });
writeFileSync(publishedBridgeManifest, JSON.stringify(publishedBridgeMetadata));
writeFileSync(serverManifest, JSON.stringify(serverMetadata));
writeFileSync(normalizedManifest, JSON.stringify(normalizedMetadata));
writeFileSync(normalizedCommand, "#!" + process.execPath + "\\n", { mode: 0o755 });
const normalizedRoots = () => codexExecutableReadOnlyRoots({ PATH: "/missing-codex-command" }, normalizedCommand);
assert.ok(normalizedRoots().includes(vendor), "The normalized published wrapper must retain its selected native sandbox resources");
const sandbox = createIsolatedCodexAppServerArgs({ HOME: "/private-provider-home" }, normalizedRoots()).join("\\n");
assert.ok(sandbox.includes(JSON.stringify(vendor) + '=\"read\"'), "Native sandbox resources must reach the actual read-only permission configuration");
assert.ok(sandbox.includes('\"/private-provider-home\"=\"none\"'), "The provider home remains denied");
for (const ancestor of [platform, dirname(platform), join(root, "node_modules"), server, root]) {
assert.ok(!normalizedRoots().includes(ancestor), "Platform discovery must not grant enclosing npm or server roots");
}
for (const [path, changed] of [
[serverManifest, { ...serverMetadata, optionalDependencies: {} }],
[serverManifest, { ...serverMetadata, optionalDependencies: { [platformName]: "npm:@openai/codex@0.159.0-" + process.platform + "-" + process.arch } }],
[serverManifest, { ...serverMetadata, dependencies: { "@agentclientprotocol/codex-acp": "1.5.0" } }],
[publishedBridgeManifest, { ...publishedBridgeMetadata, version: "1.5.0" }],
[publishedBridgeManifest, { ...publishedBridgeMetadata, name: "unqualified-bridge" }],
[normalizedManifest, { ...normalizedMetadata, version: "0.159.0" }],
[normalizedManifest, { ...normalizedMetadata, optionalDependencies: {} }],
[join(platform, "package.json"), { ...platformMetadata, name: "unqualified-native" }],
[join(platform, "package.json"), { ...platformMetadata, version: "0.159.0-" + process.platform + "-" + process.arch }],
[join(platform, "package.json"), { ...platformMetadata, os: ["unsupported-os"] }],
[join(platform, "package.json"), { ...platformMetadata, cpu: ["unsupported-cpu"] }],
]) {
writeFileSync(path, JSON.stringify(changed));
assert.ok(!normalizedRoots().includes(vendor), "Unqualified metadata must not grant native resources");
writeFileSync(serverManifest, JSON.stringify(serverMetadata));
writeFileSync(publishedBridgeManifest, JSON.stringify(publishedBridgeMetadata));
writeFileSync(normalizedManifest, JSON.stringify(normalizedMetadata));
writeFileSync(join(platform, "package.json"), JSON.stringify(platformMetadata));
}
const outsideVendor = join(root, "outside-vendor");
mkdirSync(outsideVendor);
rmSync(vendor, { recursive: true });
symlinkSync(outsideVendor, vendor);
assert.ok(!normalizedRoots().includes(outsideVendor), "A vendor symlink must not escape the selected platform package");
rmSync(vendor); mkdirSync(vendor);
const outsidePlatform = join(root, "outside-platform");
mkdirSync(join(outsidePlatform, "vendor"), { recursive: true });
writeFileSync(join(outsidePlatform, "package.json"), JSON.stringify(platformMetadata));
rmSync(platform, { recursive: true }); symlinkSync(outsidePlatform, platform);
assert.ok(!normalizedRoots().includes(join(outsidePlatform, "vendor")), "An alias symlink must not grant a foreign package");
rmSync(platform); mkdirSync(vendor, { recursive: true });
writeFileSync(join(platform, "package.json"), JSON.stringify(platformMetadata));
} finally { rmSync(root, { recursive: true, force: true }); }
process.stdout.write("PINNED_CODEX_ISOLATION_VERIFIED");
`;
@@ -1,7 +1,8 @@
import { configuredEnvironmentKeys } from "../../configured-environment.js";
import { resolve, isAbsolute, join, dirname, delimiter } from "node:path";
import { existsSync, realpathSync, readFileSync, statSync } from "node:fs";
import { resolve, isAbsolute, join, dirname, delimiter, relative, sep } from "node:path";
import { closeSync, constants, existsSync, fstatSync, openSync, realpathSync, readFileSync, statSync } from "node:fs";
import { createRequire } from "node:module";
import { QUALIFIED_ACPX_PROFILES } from "../acpx/qualified-profiles.js";
import {
githubCredentialEnvironmentKeys,
@@ -54,8 +55,12 @@ export function codexExecutableReadOnlyRoots(source: NodeJS.ProcessEnv, command
// The npm entrypoint launches the platform package's native executable,
// which Codex invokes again inside bwrap when starting each shell command.
const platformPackage = `@openai/codex-${process.platform}-${process.arch}`;
if (manifest.optionalDependencies?.[platformPackage]) {
const platformManifest = createRequire(manifestPath).resolve(`${platformPackage}/package.json`);
const normalizedPlatformManifest = manifest.optionalDependencies === undefined
? normalizedCodexPlatformManifest(manifestPath, platformPackage)
: undefined;
if (manifest.optionalDependencies?.[platformPackage] || normalizedPlatformManifest) {
const platformManifest = normalizedPlatformManifest
?? createRequire(manifestPath).resolve(`${platformPackage}/package.json`);
const packageRoot = realpathSync(dirname(platformManifest));
const vendor = realpathSync(resolve(packageRoot, "vendor"));
if (vendor.startsWith(`${packageRoot}/`)) add(vendor);
@@ -68,6 +73,77 @@ export function codexExecutableReadOnlyRoots(source: NodeJS.ProcessEnv, command
return [...roots];
}
/** The published server, rather than its bundled JS wrapper, declares npm's host package. */
function normalizedCodexPlatformManifest(runtimeManifest: string, platformPackage: string): string | undefined {
const profile = QUALIFIED_ACPX_PROFILES.codex;
const runtime = readCodexPackageManifest(runtimeManifest);
if (runtime.name !== profile.agentRuntimePackage || runtime.version !== profile.agentRuntimeVersion
|| runtime.optionalDependencies !== undefined) return undefined;
const expectedVersion = `${profile.agentRuntimeVersion}-${process.platform}-${process.arch}`;
const expectedDeclaration = `npm:${profile.agentRuntimePackage}@${expectedVersion}`;
for (let root = dirname(dirname(runtimeManifest)), depth = 0; depth < 24; depth += 1) {
const serverManifest = resolve(root, "package.json");
if (existsSync(serverManifest)) {
const server = readCodexPackageManifest(serverManifest);
if (server.name === "@paperclipai/server") {
if (server.dependencies?.[profile.agentServerPackage] !== profile.agentServerVersion
|| server.optionalDependencies?.[platformPackage] !== expectedDeclaration
|| !codexPathInside(resolve(root, "node_modules"), runtimeManifest)) return undefined;
const bridgeManifest = createRequire(serverManifest).resolve(`${profile.agentServerPackage}/package.json`);
if (!codexPathInside(resolve(root, "node_modules"), bridgeManifest)
|| realpathSync(bridgeManifest) !== bridgeManifest) return undefined;
const bridge = readCodexPackageManifest(bridgeManifest);
if (bridge.name !== profile.agentServerPackage || bridge.version !== profile.agentServerVersion
|| createRequire(bridgeManifest).resolve(`${profile.agentRuntimePackage}/package.json`) !== runtimeManifest) return undefined;
const runtimeSelection = createRequire(runtimeManifest).resolve(`${platformPackage}/package.json`);
const selected = createRequire(serverManifest).resolve(`${platformPackage}/package.json`);
if (runtimeSelection !== selected || realpathSync(selected) !== selected) return undefined;
const slot = (createRequire(serverManifest).resolve.paths(platformPackage) ?? []).some(directory => {
try {
return (dirname(directory) === root || codexPathInside(dirname(directory), root))
&& realpathSync(directory) === directory
&& selected === resolve(directory, platformPackage, "package.json")
&& realpathSync(dirname(selected)) === dirname(selected);
} catch { return false; }
});
if (!slot) return undefined;
const native = readCodexPackageManifest(selected);
if ((native.name !== platformPackage && native.name !== profile.agentRuntimePackage)
|| native.version !== expectedVersion
|| !Array.isArray(native.os) || !native.os.includes(process.platform)
|| !Array.isArray(native.cpu) || !native.cpu.includes(process.arch)) return undefined;
return selected;
}
}
const parent = dirname(root);
if (parent === root) break;
root = parent;
}
return undefined;
}
function codexPathInside(root: string, path: string): boolean {
const value = relative(root, path);
return value !== "" && value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
}
function readCodexPackageManifest(path: string): Record<string, unknown> & {
dependencies?: Record<string, unknown>;
optionalDependencies?: Record<string, unknown>;
} {
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const before = fstatSync(descriptor, { bigint: true });
if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > 256n * 1024n) throw new Error("Unsafe Codex package manifest");
const bytes = readFileSync(descriptor), after = fstatSync(descriptor, { bigint: true });
if (bytes.length !== Number(before.size) || before.dev !== after.dev || before.ino !== after.ino
|| before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs) throw new Error("Codex package manifest changed during lookup");
const value: unknown = JSON.parse(bytes.toString("utf8"));
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Codex package manifest");
return value as ReturnType<typeof readCodexPackageManifest>;
} finally { closeSync(descriptor); }
}
export const CODEX_SKILLLESS_PERMISSION_PROFILE =
"paperclip-runner-workspace-only";
export const CODEX_PLANNING_PERMISSION_PROFILE =
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@@ -55,12 +55,13 @@ test('verification never elevates PR-controlled provisioning or cleanup on the h
});
test('the installed Codex probe exercises the public export and rejects incomplete or mismatched packages', () => {
const root = mkdtempSync(join(tmpdir(), 'installed-codex-probe-'));
const root = realpathSync(mkdtempSync(join(tmpdir(), 'installed-codex-probe-')));
try {
const consumer = join(root, 'consumer'); mkdirSync(consumer);
const target = `${process.platform}-${process.arch}`;
const platformPath = `node_modules/@openai/codex-${target}`;
const platform = join(consumer, platformPath); mkdirSync(platform, { recursive: true });
const nativeVendor = join(platform, 'vendor'); mkdirSync(nativeVendor);
const manifest = { name: '@openai/codex', version: `0.160.0-${target}`, os: [process.platform], cpu: [process.arch] };
writeFileSync(join(platform, 'package.json'), JSON.stringify(manifest));
const entry = { name: manifest.name, version: manifest.version,
@@ -71,6 +72,10 @@ test('the installed Codex probe exercises the public export and rejects incomple
const vendor = join(server, 'dist/vendor/paperclip-runner'), drivers = join(vendor, 'drivers/acpx'); mkdirSync(drivers, { recursive: true });
writeFileSync(join(server, 'package.json'), JSON.stringify({ name: '@paperclipai/server', type: 'module' }));
const integrity = join(drivers, 'installation-integrity.js');
const security = join(vendor, 'drivers/codex/codex-security-config.js'); mkdirSync(join(vendor, 'drivers/codex'));
const securitySource = `export const codexExecutableReadOnlyRoots = () => [${JSON.stringify(nativeVendor)}];
export const createIsolatedCodexAppServerArgs = (source, roots) => roots.map(root => JSON.stringify(root) + '=\"read\"');`;
writeFileSync(security, securitySource);
const moduleSource = `import assert from 'node:assert/strict'; import { writeFileSync } from 'node:fs';
export function createAcpxPackageJsonResolver(root, manifest) { assert.equal(root, ${JSON.stringify(server)}); assert.equal(manifest, root + '/package.json'); return () => 'selected-authority'; }
export async function verifyQualifiedAcpxInstallation(profile, resolver) { assert.deepEqual(profile, { agent: 'codex', model: 'gpt-5.4' }); assert.equal(resolver(), 'selected-authority');
@@ -91,6 +96,7 @@ test('the installed Codex probe exercises the public export and rejects incomple
assert.equal(JSON.parse(result.stdout).codexConsumerPlatform, target);
assert.equal(JSON.parse(result.stdout).codexQualifiedInstallationVerified, true);
assert.equal(JSON.parse(result.stdout).codexCommandLeaseVerified, true);
assert.equal(JSON.parse(result.stdout).codexNativeSandboxResourcesVerified, true);
assert.equal(readFileSync(join(server, 'lease-closed'), 'utf8'), 'ok');
assert.equal(JSON.parse(result.stdout).providerCalls, 0);
const legacyPath = 'node_modules/legacy-codex-adapter/node_modules/@openai/codex-' + target;
@@ -116,6 +122,13 @@ test('the installed Codex probe exercises the public export and rejects incomple
writeFileSync(index, "export const resolvePinnedCodexCommand = () => { throw new Error('Bundled Codex runtime version mismatch'); };");
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /Bundled Codex runtime version mismatch/);
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(command)};`);
writeFileSync(security, securitySource.replace(JSON.stringify(nativeVendor), '"/missing-vendor-root"'));
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /vendor directory must be readable/);
writeFileSync(security, securitySource.replace(JSON.stringify(nativeVendor), `${JSON.stringify(nativeVendor)}, ${JSON.stringify(consumer)}`));
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /must not grant enclosing npm/);
writeFileSync(security, securitySource.replace("'=\"read\"'", "'=\"write\"'"));
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /retain the selected native executable resources/);
writeFileSync(security, securitySource);
const emptySlot = join(server, 'node_modules/@openai/codex-' + target);
mkdirSync(emptySlot, { recursive: true });
result = run(); assert.equal(result.status, 0, result.stderr);
+13 -2
View File
@@ -126,7 +126,18 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
const vendor = dirname(index);
const integrityPath = join(vendor, 'drivers/acpx/installation-integrity.js');
const profilesPath = join(vendor, 'drivers/acpx/qualified-profiles.js');
assert.ok(contained(integrityPath) && contained(profilesPath), 'Codex integrity modules must come from the installed consumer');
const securityPath = join(vendor, 'drivers/codex/codex-security-config.js');
assert.ok(contained(integrityPath) && contained(profilesPath) && contained(securityPath), 'Codex verification modules must come from the installed consumer');
const { codexExecutableReadOnlyRoots, createIsolatedCodexAppServerArgs } = await import(pathToFileURL(securityPath).href);
const platformRoot = realpathSync(dirname(selectedPlatformManifest));
const nativeVendor = realpathSync(join(platformRoot, 'vendor'));
assert.ok(contained(nativeVendor) && nativeVendor.startsWith(platformRoot + sep), 'Codex vendor resources must remain inside their installed platform package');
const sandboxSource = { HOME: '/private-provider-home', PATH: '/usr/local/bin:/usr/bin:/bin' };
const readRoots = codexExecutableReadOnlyRoots(sandboxSource, command);
assert.ok(readRoots.includes(nativeVendor), 'Selected native Codex vendor directory must be readable inside its sandbox');
assert.ok(!readRoots.includes(platformRoot) && !readRoots.includes(root) && !readRoots.includes(sandboxSource.HOME), 'Codex sandbox must not grant enclosing npm or credential-home access');
const sandboxArgs = createIsolatedCodexAppServerArgs(sandboxSource, readRoots).join('\\n');
assert.ok(sandboxArgs.includes(JSON.stringify(nativeVendor) + '="read"'), 'Codex sandbox configuration must retain the selected native executable resources');
const { createAcpxPackageJsonResolver, verifyQualifiedAcpxInstallation } = await import(pathToFileURL(integrityPath).href);
const { resolveQualifiedAcpxProfile } = await import(pathToFileURL(profilesPath).href);
const serverRoot = resolve(vendor, '../../..');
@@ -136,7 +147,7 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
const lease = await installation.openCommand();
await lease.close();
console.log(JSON.stringify({ pinnedCodexCommandVerified: true, pinnedCodexVersion: ${JSON.stringify(expectedVersion)},
...provenance, codexSelectedPlatformPath: selectedPlatform.path, codexSelectedPlatformVersion: selectedMetadata.version, codexQualifiedInstallationVerified: true, codexCommandLeaseVerified: true, providerCalls: 0 }));
...provenance, codexSelectedPlatformPath: selectedPlatform.path, codexSelectedPlatformVersion: selectedMetadata.version, codexNativeSandboxResourcesVerified: true, codexQualifiedInstallationVerified: true, codexCommandLeaseVerified: true, providerCalls: 0 }));
`;
}