Skip to content
40 changes: 37 additions & 3 deletions bin/ocx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "../src/updat
import { bootRestoreProbe, launcherUsableAfterNpmUpdate, transactionalNpmUpdate } from "../src/update/transactional-install.mjs";
import { manualUpdateFailureGuidance, npmUpdateFailureGuidance } from "../src/update/update-failure-guidance.mjs";
import {
captureCodexCliTransportEnv,
CODEX_CLI_TRANSPORT_ENV_KEYS,
CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS,
codexCliInspectionLookupSeeds,
isCodexCliUpdateInspectionArgv,
} from "../src/update/codex-cli-update-launch-policy.mjs";
import {
Expand Down Expand Up @@ -1192,26 +1195,56 @@ function createNodeLaunchContext() {
: configuredCodexCliPath;
const preBunPath = typeof process.env.PATH === "string" ? process.env.PATH : null;
const preBunPathExt = typeof process.env.PATHEXT === "string" ? process.env.PATHEXT : null;
// HOME and NODE_PATH seed the candidate Node module lookup the update
// inspection models, so they are proof-bound here for the same reason as
// PATH: the live environment could have been rewritten after selection. They
// stay in the inherited environment regardless — the child may need them for
// its own work; only the snapshot copy is optional.
const preBunHome = typeof process.env.HOME === "string" ? process.env.HOME : null;
const preBunNodePath = typeof process.env.NODE_PATH === "string" ? process.env.NODE_PATH : null;
const preBunCodexCliManagerRoots = Object.fromEntries(
CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS.flatMap(name => {
const value = process.env[name];
return typeof value === "string" && value !== "" ? [[name, value]] : [];
}),
);
// Proxy/CA settings are captured once, here, before Bun can add project dotenv
// values. They are part of the required selection snapshot, so they count
// against the context budget below before any optional lookup seed does.
const preBunTransportEnv = captureCodexCliTransportEnv(process.env);
const launchProof = randomBytes(32).toString("base64url");
const launchContext = JSON.stringify({
const serializeContext = lookupSeeds => JSON.stringify({
version: 1,
proof: launchProof,
anthropicEnvSlots: preBunAnthropicSlots,
codexCliInspectionEnv: codexCliUpdateInspection ? {
codexCliPath: preBunCodexCliPath,
path: preBunPath,
pathExt: preBunPathExt,
home: lookupSeeds.home,
nodePath: lookupSeeds.nodePath,
managerRoots: preBunCodexCliManagerRoots,
transportEnv: preBunTransportEnv,
// The published Codex entrypoint selects its native backing by the arch of
// the Node that runs it; a Bun override or PATH fallback can run another
// arch under Rosetta, so this Node launcher's arch is the bound value.
arch: process.arch,
configDir: configDir(),
} : null,
});
// The inspection snapshot above already carries PATH, PATHEXT, and the manager-root slots as
let launchContext = serializeContext({ home: preBunHome, nodePath: preBunNodePath });
// The lookup seeds are optional: a Windows inspection never models the global
// Node lookup, and an oversized NODE_PATH must not push the serialized
// context past the reader's limit — that would discard the whole selection
// snapshot, while dropped seeds merely leave the fallback lane unverifiable
// and fail only the content observation closed.
const lookupSeeds = codexCliInspectionLookupSeeds(
process.platform, preBunHome, preBunNodePath, launchContext.length,
);
if (lookupSeeds.home !== preBunHome || lookupSeeds.nodePath !== preBunNodePath) {
launchContext = serializeContext(lookupSeeds);
}
// The inspection snapshot above already carries PATH, PATHEXT, manager roots and proxy/CA settings as
// proof-bound values, and `inspectCodexCliInstall` reads them from that snapshot rather than
// from the live environment. Inheriting them again would spend the 32,767-character Windows
// environment block twice, so a large-but-valid shell environment could stop the Bun child
Expand All @@ -1224,7 +1257,8 @@ function createNodeLaunchContext() {
const inheritedEnv = { ...process.env };
if (codexCliUpdateInspection) {
const snapshotted = new Set(
["PATH", "PATHEXT", ...CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS].map(name => name.toLowerCase()),
["PATH", "PATHEXT", ...CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS,
...CODEX_CLI_TRANSPORT_ENV_KEYS].map(name => name.toLowerCase()),
);
for (const name of Object.keys(inheritedEnv)) {
if (snapshotted.has(name.toLowerCase())) delete inheritedEnv[name];
Expand Down
8 changes: 8 additions & 0 deletions src/cli/codex-cli-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,19 @@ function inspectionDeps(commandDeps: CodexCliUpdateCommandDeps): CodexCliInstall
return {
env: {
...trusted.managerRoots,
...trusted.transportEnv,
CODEX_CLI_PATH: trusted.codexCliPath ?? undefined,
PATH: trusted.path ?? undefined,
PATHEXT: trusted.pathExt ?? undefined,
// The lookup seeds the inspection models against the launch-time
// capture; ambient values are never consulted.
HOME: trusted.home ?? undefined,
NODE_PATH: trusted.nodePath ?? undefined,
},
configDir: trusted.configDir,
// Content observation picks the platform backing by the Node launcher's
// arch, which can differ from this Bun process's under Rosetta.
arch: trusted.arch ?? undefined,
// The snapshot is proof-bound to the launcher, but the report may only attest
// the environment candidate when the resolver picks the same canonical path —
// managed requires both.
Expand Down
51 changes: 49 additions & 2 deletions src/cli/launcher-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export type TrustedNodeLaunchContext = {
codexCliPath: string | null;
path: string | null;
pathExt: string | null;
home: string | null;
nodePath: string | null;
managerRoots: CodexCliVersionManagerRoots | null;
transportEnv: Readonly<Record<string, string>>;
arch: NodeJS.Architecture | null;
configDir: string;
}> | null;
};
Expand All @@ -50,6 +54,27 @@ function parseVersionManagerRoots(value: unknown): CodexCliVersionManagerRoots |
return Object.freeze(Object.fromEntries(entries)) as CodexCliVersionManagerRoots;
}

function parseTransportEnv(value: unknown): Readonly<Record<string, string>> | undefined {
// Older snapshots convey no transport settings. Never fill them from Bun's ambient env.
if (value === undefined) return Object.freeze({});
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const entries = Object.entries(value);
if (entries.some(([name, setting]) => !CODEX_CLI_TRANSPORT_ENV_KEYS.includes(name.toLowerCase())
|| typeof setting !== "string" || setting.length > 32 * 1024)) return undefined;
return Object.freeze(Object.fromEntries(entries));
}

const NODE_ARCHITECTURES: ReadonlySet<string> = new Set<NodeJS.Architecture>([
"arm", "arm64", "ia32", "loong64", "mips", "mipsel", "ppc64", "riscv64", "s390x", "x64",
]);

function parseLauncherArch(value: unknown): NodeJS.Architecture | null | undefined {
// Older snapshots predate the field; absent stays null (Bun's own arch is the
// pre-existing fallback). Any other non-architecture value is untrusted.
if (value === undefined || value === null) return null;
return typeof value === "string" && NODE_ARCHITECTURES.has(value) ? value as NodeJS.Architecture : undefined;
}

/** Consume the internal proof before normal CLI argument parsing. */
export function initializeNodeLauncherContext(
argv: string[] = process.argv,
Expand All @@ -70,7 +95,7 @@ export function initializeNodeLauncherContext(
delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
trustedContext = null;

if (proofArgs.length !== 1 || !raw || raw.length > 64 * 1024) return null;
if (proofArgs.length !== 1 || !raw || raw.length > NODE_LAUNCH_CONTEXT_MAX_CHARS) return null;
const proof = proofArgs[0]!;
if (!isLaunchProof(proof)) return null;

Expand All @@ -95,22 +120,40 @@ export function initializeNodeLauncherContext(
const managerRoots = inspection && typeof inspection === "object" && !Array.isArray(inspection)
? parseVersionManagerRoots((inspection as Record<string, unknown>).managerRoots)
: null;
const transportEnv = inspection && typeof inspection === "object" && !Array.isArray(inspection)
? parseTransportEnv((inspection as Record<string, unknown>).transportEnv)
: undefined;
const arch = inspection && typeof inspection === "object" && !Array.isArray(inspection)
? parseLauncherArch((inspection as Record<string, unknown>).arch)
: undefined;
const codexCliInspectionEnv = inspection === null || inspection === undefined
? null
: inspection && typeof inspection === "object" && !Array.isArray(inspection)
&& ["codexCliPath", "path", "pathExt"].every(key => {
const value = (inspection as Record<string, unknown>)[key];
return value === null || typeof value === "string";
})
// Older launchers predate the lookup-seed slots; absent stays null so
// the fallback lane simply remains unverifiable rather than untrusted.
&& ["home", "nodePath"].every(key => {
const value = (inspection as Record<string, unknown>)[key];
return value === undefined || value === null || typeof value === "string";
})
&& typeof (inspection as Record<string, unknown>).configDir === "string"
&& (inspection as Record<string, string>).configDir.length > 0
&& (inspection as Record<string, string>).configDir.length <= 32 * 1024
&& managerRoots !== undefined
&& transportEnv !== undefined
&& arch !== undefined
? Object.freeze({
codexCliPath: (inspection as Record<string, string | null>).codexCliPath ?? null,
path: (inspection as Record<string, string | null>).path ?? null,
pathExt: (inspection as Record<string, string | null>).pathExt ?? null,
home: (inspection as Record<string, string | null>).home ?? null,
nodePath: (inspection as Record<string, string | null>).nodePath ?? null,
managerRoots,
transportEnv,
arch,
configDir: (inspection as Record<string, string>).configDir,
})
: undefined;
Expand All @@ -125,4 +168,8 @@ export function initializeNodeLauncherContext(
export function trustedNodeLauncherContext(): TrustedNodeLaunchContext | null {
return trustedContext;
}
import { CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS } from "../update/codex-cli-update-launch-policy.mjs";
import {
CODEX_CLI_TRANSPORT_ENV_KEYS,
CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS,
NODE_LAUNCH_CONTEXT_MAX_CHARS,
} from "../update/codex-cli-update-launch-policy.mjs";
Loading
Loading