Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/codex/cli-installation-targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions tests/codex-integration/codex-cli-installation-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading