Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cli/codex-cli-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface CodexCliUpdateCommandDeps {
readonly inspectIdentity?: (input: CodexCliInstallationIdentityInput) => Promise<CodexCliInstallationIdentityReport>;
readonly deriveInstallationInput?: (
snapshot: CodexCliInstallationSnapshot,
) => CodexCliInstallationTargetDerivation;
) => Promise<CodexCliInstallationTargetDerivation>;
}

function identitySummary(report: CodexCliInstallationIdentityReport): string[] {
Expand Down Expand Up @@ -148,7 +148,7 @@ export async function handleCodexCliUpdateCommand(
const snapshot = trustedNodeLauncherContext()?.codexCliInspectionEnv;
const derive = deps.deriveInstallationInput
?? (await import("../codex/cli-installation-targets")).deriveCodexCliInstallationInput;
const derived = derive({
const derived = await derive({
codexCliPath: snapshot?.codexCliPath ?? null,
path: snapshot?.path ?? null,
pathExt: snapshot?.pathExt ?? null,
Expand Down
82 changes: 42 additions & 40 deletions src/codex/cli-installation-targets.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { closeSync, existsSync, openSync, readSync } from "node:fs";
import { win32 } from "node:path";
import { SHIM_MARKER } from "./shim-templates";
import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
Expand Down Expand Up @@ -27,53 +26,40 @@ export type CodexCliInstallationTargetDerivation =

export interface CodexCliInstallationTargetDeps {
readonly platform?: NodeJS.Platform;
readonly exists?: (path: string) => boolean;
/** `refused` means the probe could not decide; a PATH scan must stop rather than
* attest a later candidate that the real launcher would never reach. */
readonly exists?: (path: string) => boolean | "refused" | Promise<boolean | "refused">;
/** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
readonly fileContains?: (path: string, marker: string) => boolean;
readonly fileContains?: (path: string, marker: string) =>
boolean | "unavailable" | Promise<boolean | "unavailable">;
}

const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
const SHIM_PROBE_BYTES = 8 * 1024;
const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";

function defaultFileContains(path: string, marker: string): boolean {
let descriptor: number | undefined;
let contains = false;
try {
descriptor = openSync(path, "r");
const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
const count = readSync(descriptor, bytes, 0, bytes.length, 0);
contains = bytes.subarray(0, count).toString("utf8").includes(marker);
} catch {
contains = false;
} finally {
if (descriptor !== undefined) {
try { closeSync(descriptor); } catch { contains = false; }
}
}
return contains;
}

/**
* First match wins, mirroring PATH resolution: directories in order, and within
* each directory every PATHEXT suffix in order (or the exact name when it
* already carries an extension). Skipping a hit to keep scanning would attest
* something other than the launcher that actually resolves.
*/
function scanPath(
async function scanPath(
name: string,
pathValue: string | null | undefined,
pathExt: string | null | undefined,
exists: (path: string) => boolean,
): string | null {
exists: (path: string) => boolean | "refused" | Promise<boolean | "refused">,
): Promise<string | null> {
const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
for (const entry of (pathValue ?? "").split(";")) {
const dir = entry.trim().replace(/^"+|"+$/g, "");
if (!dir) continue;
for (const candidateName of names) {
const candidate = win32.join(dir, candidateName);
if (exists(candidate)) return candidate;
const found = await exists(candidate);
if (found === "refused") return null;
if (found) return candidate;
}
}
return null;
Expand All @@ -86,40 +72,56 @@ function scanPath(
* false identity. No ambient environment is read: without a snapshot the result
* is unavailable rather than silently trusting the child's environment.
*/
export function deriveCodexCliInstallationInput(
export async function deriveCodexCliInstallationInput(
snapshot: CodexCliInstallationSnapshot,
deps: CodexCliInstallationTargetDeps = {},
): CodexCliInstallationTargetDerivation {
): Promise<CodexCliInstallationTargetDerivation> {
if ((deps.platform ?? process.platform) !== "win32") {
return { kind: "unavailable", reason: "unsupported_platform" };
}
const exists = deps.exists ?? existsSync;
const fileContains = deps.fileContains ?? defaultFileContains;
const safeRead = async (path: string, maxBytes: number, prefixOnly = false) => {
const { inspectWindowsInstallationFiles } = await import("./windows-installation-files");
return inspectWindowsInstallationFiles([{ path, maxBytes, metadataOnly: maxBytes === 0, prefixOnly }]);
};
const exists = deps.exists ?? (async (path: string) => {
const result = await safeRead(path, 0);
if (result.kind === "observed") return true;
return result.kind === "refused" ? "refused" : false;
});
const fileContains = deps.fileContains ?? (async (path: string, marker: string) => {
const result = await safeRead(path, SHIM_PROBE_BYTES, true);
if (result.kind !== "observed") return "unavailable";
return Buffer.from(result.files[0]!.bytes).toString("utf8").includes(marker);
});
const configured = snapshot.codexCliPath;

let candidate: string | null;
if (configured) {
if (/^[a-z]:[\\/]/i.test(configured)) {
if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
if (await exists(configured) !== true) return { kind: "unavailable", reason: "candidate_unavailable" };
candidate = win32.normalize(configured);
} else {
if (configured.includes("/") || configured.includes("\\")) {
return { kind: "unavailable", reason: "candidate_unavailable" };
}
candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
candidate = await scanPath(configured, snapshot.path, snapshot.pathExt, exists);
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
}
} else {
candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
candidate = await scanPath("codex", snapshot.path, snapshot.pathExt, exists);
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
}

// An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
// artifact. The renamed original beside it is the file npm wrote.
if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
candidate = backing;
if (/\.cmd$/i.test(candidate)) {
const marker = await fileContains(candidate, SHIM_MARKER);
if (marker === "unavailable") return { kind: "unavailable", reason: "candidate_unavailable" };
if (marker) {
const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
if (await exists(backing) !== true) return { kind: "unavailable", reason: "unsupported_layout" };
candidate = backing;
}
}

const base = win32.basename(candidate).toLowerCase();
Expand All @@ -131,19 +133,19 @@ export function deriveCodexCliInstallationInput(
} else {
return { kind: "unavailable", reason: "unsupported_layout" };
}
if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
if (await exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json")) !== true) {
return { kind: "unavailable", reason: "unsupported_layout" };
}

// The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
let node = win32.join(prefix, "node.exe");
if (!exists(node)) {
const resolved = scanPath("node.exe", snapshot.path, null, exists);
if (await exists(node) !== true) {
const resolved = await scanPath("node.exe", snapshot.path, null, exists);
if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
node = resolved;
}
const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
if (await exists(npmCli) !== true) return { kind: "unavailable", reason: "toolchain_unresolved" };

return {
kind: "derived",
Expand Down
30 changes: 21 additions & 9 deletions src/codex/windows-installation-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface WindowsInstallationFileRequest {
readonly path: string;
readonly maxBytes: number;
readonly hashOnly?: boolean;
/** Validate and hold the path without reading its contents. */
readonly metadataOnly?: boolean;
/** Read at most maxBytes from the start instead of refusing an oversized file. */
readonly prefixOnly?: boolean;
}
export interface WindowsInstallationFileIdentity {
readonly volumeSerial: string;
Expand Down Expand Up @@ -76,6 +80,8 @@ export async function inspectWindowsInstallationFiles(
const parsedPath = request && components(request.path);
if (!parsedPath || !Number.isSafeInteger(request.maxBytes) || request.maxBytes < 0
|| (request.hashOnly !== undefined && typeof request.hashOnly !== "boolean")
|| (request.metadataOnly !== undefined && typeof request.metadataOnly !== "boolean")
|| (request.prefixOnly !== undefined && typeof request.prefixOnly !== "boolean")
|| request.maxBytes > (request.hashOnly ? 256 * MIB : MIB)) return null;
ceiling += request.maxBytes;
return parsedPath;
Expand Down Expand Up @@ -184,29 +190,35 @@ export async function inspectWindowsInstallationFiles(
}
const handle = relativeOpen(parent, names[names.length - 1]!, false);
const identity = inspect(handle, false);
if (identity.size > request.maxBytes) throw new InspectionRefusal("size-limit");
if (!request.metadataOnly && !(request.prefixOnly && !request.hashOnly)
&& identity.size > request.maxBytes) throw new InspectionRefusal("size-limit");
return { request, handle, identity };
});
openedForTests?.();
const observed = files.map(({ request, handle, identity }) => {
const hash = createHash("sha256");
const bytes = request.hashOnly ? new Uint8Array() : new Uint8Array(identity.size);
const chunk = Buffer.alloc(Math.min(MIB, Math.max(1, identity.size)));
if (request.metadataOnly) return { path: request.path, identity, bytes: new Uint8Array(), digest: "" };
const truncated = Boolean(request.prefixOnly) && !request.hashOnly && identity.size > request.maxBytes;
const readLimit = truncated ? request.maxBytes : identity.size;
const bytes = request.hashOnly ? new Uint8Array() : new Uint8Array(readLimit);
const chunk = Buffer.alloc(Math.min(MIB, Math.max(1, readLimit)));
const read = Buffer.alloc(4);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let offset = 0;
while (offset < identity.size) {
const length = Math.min(chunk.length, identity.size - offset);
while (offset < readLimit) {
const length = Math.min(chunk.length, readLimit - offset);
if (!k.ReadFile!(handle, ffi.ptr(chunk), length, ffi.ptr(read), null)) throw new InspectionRefusal("read-failed");
const count = read.readUInt32LE(0);
if (!count || count > length) throw new InspectionRefusal("read-failed");
hash.update(chunk.subarray(0, count));
if (!request.hashOnly) bytes.set(chunk.subarray(0, count), offset);
if (!truncated) hash.update(chunk.subarray(0, count));
offset += count;
}
if (!k.ReadFile!(handle, ffi.ptr(chunk), 1, ffi.ptr(read), null) || read.readUInt32LE(0) !== 0) {
throw new InspectionRefusal("identity-changed");
if (!truncated) {
if (!k.ReadFile!(handle, ffi.ptr(chunk), 1, ffi.ptr(read), null) || read.readUInt32LE(0) !== 0) {
throw new InspectionRefusal("identity-changed");
}
}
return { path: request.path, identity, bytes, digest: hash.digest("hex") };
return { path: request.path, identity, bytes, digest: truncated ? "" : hash.digest("hex") };
});
for (const file of files) {
if (JSON.stringify(inspect(file.handle, false)) !== JSON.stringify(file.identity)) {
Expand Down
4 changes: 3 additions & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ backing, the npm prefix layout, and the Node/npm toolchain beside the resolved n
Configured values containing a path separator must be drive-absolute; otherwise derivation
refuses with `candidate_unavailable` instead of substituting a different PATH candidate. Bare
command names and the unset default continue to resolve only through the captured PATH.
Discovery only proposes paths and never reads ambient state. Four explicit absolute paths
Discovery probes candidates through the same local-volume, reparse-refusing held-handle
reader as final observation, so captured PATH entries cannot trigger network filesystem I/O.
It never reads ambient state. Four explicit absolute paths
remain accepted as an all-or-none override. Only the
standard npm command shim or direct Codex package entry is accepted. The native reader in
`src/codex/windows-installation-files.ts` holds ancestor/file handles for bounded reads and
Expand Down
4 changes: 2 additions & 2 deletions tests/cli/cli-codex-cli-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe("Codex CLI update CLI", () => {
try {
console.log = (...values: unknown[]) => logs.push(values.map(String).join(" "));
expect(await handleCodexCliUpdateCommand(["attest", "--json"], {
deriveInstallationInput: input => {
deriveInstallationInput: async input => {
snapshot = input;
return { kind: "derived", input: { ...identityInput, candidateSource: "selected" as const } };
},
Expand All @@ -92,7 +92,7 @@ describe("Codex CLI update CLI", () => {
try {
console.log = (...values: unknown[]) => logs.push(values.map(String).join(" "));
expect(await handleCodexCliUpdateCommand(["attest", "--json"], {
deriveInstallationInput: () => ({ kind: "unavailable", reason: "candidate_unavailable" }),
deriveInstallationInput: async () => ({ kind: "unavailable", reason: "candidate_unavailable" }),
inspectIdentity: async () => { inspectCalls += 1; return identityReport; },
})).toBe(0);
expect(inspectCalls).toBe(0);
Expand Down
Loading
Loading