From 62d12d1ab7f18ebbbb21836e54c01aade034d5c7 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Fri, 23 Oct 2026 10:43:14 +0900 Subject: [PATCH] fix(codex): refuse uncertain prefix-local Node observations --- src/codex/cli-installation-targets.ts | 6 ++++- structure/runtime.md | 2 +- .../codex-cli-installation-targets.test.ts | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/codex/cli-installation-targets.ts b/src/codex/cli-installation-targets.ts index e0c5d088f72..1dc1933c8cf 100644 --- a/src/codex/cli-installation-targets.ts +++ b/src/codex/cli-installation-targets.ts @@ -141,10 +141,14 @@ export async function deriveCodexCliInstallationInput( // The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH. let node = win32.join(prefix, "node.exe"); - if (await exists(node) !== true) { + const localNode = await exists(node); + if (localNode === false) { const resolved = await scanPath("node.exe", snapshot.path, null, exists); if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" }; node = resolved; + } else if (localNode !== true) { + // A refused local probe is not proof of absence and must not fall through to PATH. + return { kind: "unavailable", reason: "toolchain_unresolved" }; } const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js"); if (await exists(npmCli) !== true) return { kind: "unavailable", reason: "toolchain_unresolved" }; diff --git a/structure/runtime.md b/structure/runtime.md index 5255939fa24..ce86d5a9293 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -95,7 +95,7 @@ The prefilter is only an optimization, not final process-membership authority. ## Explicit Codex CLI installation observation -`src/cli/codex-cli-update.ts` dispatches the opt-in Windows x64 `attest` operation to `src/codex/cli-installation-identity.ts`. With no options, `src/codex/cli-installation-targets.ts` derives the four inputs from the proof-bound launcher snapshot: the configured candidate or the first codex on the captured PATH, an OpenCodex wrapper resolving to its renamed npm backing, the npm prefix layout, and the Node/npm toolchain beside the resolved node.exe. 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 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 rejects reparse points, conflicting writers and unsupported paths/platforms. Its path-free report binds file identities and bytes to this observation, not a durable update permission. `installationIdentityObserved` can be true; `selectionAttested`, `managed` and `applyAllowed` remain false. Supplied Node identity does not prove launcher selection, effective npm config, past installer identity or tool authenticity. No package-registry request, installation, config write or process control occurs. Existing Windows `check` retains zero candidate/config filesystem I/O. A reported refusal can exit 0; consumers inspect `status`. +`src/cli/codex-cli-update.ts` dispatches the opt-in Windows x64 `attest` operation to `src/codex/cli-installation-identity.ts`. With no options, `src/codex/cli-installation-targets.ts` derives the four inputs from the proof-bound launcher snapshot: the configured candidate or the first codex on the captured PATH, an OpenCodex wrapper resolving to its renamed npm backing, the npm prefix layout, and the Node/npm toolchain beside the resolved node.exe. 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 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. A refused or unavailable prefix-local node.exe probe is not absence evidence, so derivation refuses with `toolchain_unresolved` instead of attesting a PATH-resolved Node. 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 rejects reparse points, conflicting writers and unsupported paths/platforms. Its path-free report binds file identities and bytes to this observation, not a durable update permission. `installationIdentityObserved` can be true; `selectionAttested`, `managed` and `applyAllowed` remain false. Supplied Node identity does not prove launcher selection, effective npm config, past installer identity or tool authenticity. No package-registry request, installation, config write or process control occurs. Existing Windows `check` retains zero candidate/config filesystem I/O. A reported refusal can exit 0; consumers inspect `status`. The local account CLI and pool credential resolver share the [Orca source-owned import contract](codex-home.md#orca-source-owned-account-import): importing does not perform OAuth, and runtime credential resolution rereads the owned source. diff --git a/tests/codex-integration/codex-cli-installation-targets.test.ts b/tests/codex-integration/codex-cli-installation-targets.test.ts index 485f0a77d1b..1538fe90833 100644 --- a/tests/codex-integration/codex-cli-installation-targets.test.ts +++ b/tests/codex-integration/codex-cli-installation-targets.test.ts @@ -131,6 +131,33 @@ describe("selected Codex CLI installation target derivation", () => { }); }); + test("a refused prefix-local Node probe does not fall through to a PATH Node", async () => { + const files = fixtureFiles(); const base = depsFor(files); const probes: string[] = []; + const result = await deriveCodexCliInstallationInput( + snapshot({ codexCliPath: PREFIX + "\\codex.cmd", path: "C:\\empty;" + NODE_DIR }), + { ...base, exists: (path: string) => { + probes.push(path.toLowerCase()); + return path.toLowerCase() === (PREFIX + "\\node.exe").toLowerCase() ? "refused" : base.exists(path); + } }, + ); + expect(result).toEqual({ kind: "unavailable", reason: "toolchain_unresolved" }); + expect(probes).not.toContain((NODE_DIR + "\\node.exe").toLowerCase()); + }); + + test("an unavailable prefix-local Node volume is not absence evidence for a PATH Node", async () => { + const files = fixtureFiles(); const base = depsFor(files); const probes: string[] = []; + const result = await deriveCodexCliInstallationInput( + snapshot({ codexCliPath: PREFIX + "\\codex.cmd", path: "C:\\empty;" + NODE_DIR }), + { ...base, exists: (path: string) => { + probes.push(path.toLowerCase()); + return path.toLowerCase() === (PREFIX + "\\node.exe").toLowerCase() + ? "volume-unavailable" : base.exists(path); + } }, + ); + expect(result).toEqual({ kind: "unavailable", reason: "toolchain_unresolved" }); + expect(probes).not.toContain((NODE_DIR + "\\node.exe").toLowerCase()); + }); + test("an earlier PATH codex.exe is the selected launcher and refuses the layout", async () => { const files = fixtureFiles(); files.delete((PREFIX + "\\codex.cmd").toLowerCase());