From 72f8ea982ec6374241e57a17b3810e68e16c14a2 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Fri, 23 Oct 2026 14:37:46 +0900 Subject: [PATCH 1/5] feat(codex): prove explicit Windows npm installations for read-only plans Port the Windows-plan hunks of 44353b9a0f, 86ad5a00d3 and c291ff89e2 onto the 7586 POSIX stack head 4c2230821f. A proof-captured absolute standard npm codex.cmd shim, its global-prefix package, prefix-local Node/npm and the prefix-local optional backing are bound by held-handle identity re-verification; the plan then reports applicable: false with refusal windows_apply_disabled. PATH-only selection and separate-prefix Node stay refused; Windows apply remains disabled. The independent cli-installation-targets refused-probe fix (#7577) is intentionally not included. --- docs-site/src/content/docs/reference/cli.md | 11 +- .../src/content/docs/reference/cli/agents.md | 21 +- .../test-layout/layout-codex-collision.json | 1 + src/codex/cli-install-provenance.ts | 25 +- src/codex/cli-installation-identity.ts | 2 +- src/codex/cli-installation-limits.ts | 7 + src/codex/cli-update-artifacts.ts | 2 +- src/codex/cli-update-plan.ts | 32 ++- src/codex/cli-windows-plan-provenance.ts | 151 +++++++++++ src/codex/windows-installation-files.ts | 10 +- structure/codex-cli-update.md | 37 ++- structure/runtime.md | 2 +- ...dex-cli-windows-installation-files.test.ts | 26 +- .../codex-cli-windows-plan.test.ts | 255 ++++++++++++++++++ tests/fixtures/test-layout-expected.json | 1 + 15 files changed, 534 insertions(+), 49 deletions(-) create mode 100644 src/codex/cli-installation-limits.ts create mode 100644 src/codex/cli-windows-plan-provenance.ts create mode 100644 tests/codex-integration/codex-cli-windows-plan.test.ts diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 77086aa31bd..8bc2ef73168 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -152,16 +152,19 @@ manifest can then establish `managed: true` when it proves ownership and, if a c The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`, and `selectionAttested`. Inspecting the configured candidate requires a trusted published-launcher context; a direct Bun/source launch has no such proof, ignores ambient and persisted candidate state, and may report -`candidate_unavailable` on POSIX or `windows_inspection_deferred` on Windows. On Windows this first slice performs no candidate or configuration filesystem I/O: -only a proof-captured absolute environment candidate can receive lexical app-bundle or version-manager labels; -every other Windows candidate fails closed. The command does not install or repair software, execute +`candidate_unavailable` on POSIX or `windows_inspection_deferred` on Windows. Windows x64 also supports +a proof-captured absolute standard npm `codex.cmd` with prefix-local Node/npm and a verified in-prefix +platform backing. It uses reparse-refusing held handles; other Windows layouts remain deferred or unmanaged. +The command does not install or repair software, execute Codex or npm, control a running process, or write configuration/cache state. `ocx system codex-cli-update plan --json` adds a read-only update decision for an attested POSIX npm-global installation. It resolves `latest` through the official npm registry, validates the exact version and SHA-512 integrity, and refuses active sessions or incomplete process observation. Registry queries use isolated temporary npm configuration, cache and logs with best-effort cleanup. -No software is installed and no process is signalled. Windows returns `windows_inspection_deferred`. +No software is installed and no process is signalled. A supported Windows installation can produce a +deterministic read-only plan with `applicable: false` and `refusal: windows_apply_disabled`. +PATH-only selection, separate-prefix Node and OpenCodex autostart wrappers remain deferred. See [planning details](/reference/cli/agents/#read-only-codex-cli-update-plan). For a reviewed POSIX non-shim npm-global plan, `ocx system codex-cli-update apply --plan-id --yes` diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index a1dda3cc351..d7ed6d13b2d 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -1159,9 +1159,9 @@ resolver selects the same canonical environment candidate without executing it. can then establish `managed: true` when it proves ownership and, if a candidate version is reported, that version matches the manifest; persisted candidates remain unattested. The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`, and `selectionAttested`. Inspecting the configured candidate requires a trusted published-launcher context; a direct Bun/source launch has no such proof, ignores ambient and persisted candidate state, and may report -`candidate_unavailable` on POSIX. On Windows this first slice performs no candidate or configuration filesystem I/O: -only a proof-captured absolute environment candidate can receive lexical app-bundle or version-manager labels; -every other Windows candidate fails closed. Because that slice never consults persisted state, a Windows run +`candidate_unavailable` on POSIX. On Windows x64, a proof-captured absolute standard npm `codex.cmd` +can be managed when held local handles bind its package, prefix-local Node/npm and the first in-prefix +platform backing. Other Windows layouts remain deferred or unmanaged. Windows never consults persisted state, so a run with no captured environment candidate reports `windows_inspection_deferred` rather than `candidate_unavailable`: the command cannot observe whether a Codex CLI is installed, so it reports the deferral instead of asserting that no candidate exists. The command does not execute Codex or a package manager, repair a shim, @@ -1178,7 +1178,9 @@ ocx system codex-cli-update plan --channel latest --json ``` Run through the published Node launcher with the intended `CODEX_CLI_PATH` in the shell environment. -The plan requires an attested POSIX npm-global installation and a linked package manifest version. +The plan requires an attested npm-global installation and a linked package manifest version. +Windows supports only an explicit absolute standard npm `codex.cmd` with prefix-local Node/npm; +PATH-only selection, separate-prefix Node, OpenCodex autostart wrappers and global/vendor fallback stay deferred. It queries the official npm registry for an exact version, SHA-512 integrity and same-origin tarball, using the launcher's captured PATH and isolated temporary npm configuration, cache and logs. Proxy settings and `NODE_EXTRA_CA_CERTS` remain supported; cleanup is best-effort. No Codex command runs. @@ -1186,10 +1188,13 @@ settings and `NODE_EXTRA_CA_CERTS` remain supported; cleanup is best-effort. No An applicable plan reports `applicable: true` and a deterministic `planId` binding platform, installation-root/content digests, installed version, channel and exact main/platform artifacts. The plan is never persisted. It refuses downgrades, already-current versions, active Codex sessions and incomplete -process observation. Windows returns `windows_inspection_deferred` before registry/process queries. +process observation. Supported Windows metadata queries use the observed Node/npm under matching +held handles. A completed Windows read-only plan reports a deterministic ID, `applicable: false` +and `refusal: windows_apply_disabled`; active/unknown sessions still refuse earlier. Unsupported +Windows layouts return `windows_inspection_deferred` before registry/process queries. A refusal exits 0: inspect `applicable` and `refusal`. The printed install argv is indicative; this -command installs nothing, changes no application state and never signals a session. A plan ID alone -is not installation authorization: apply requires separate explicit confirmation. +command installs nothing, changes no application state and never signals a session. Windows apply +remains disabled, and a plan ID alone is not installation authorization: apply requires separate explicit confirmation. #### Confirmed POSIX Codex CLI apply and job status @@ -1234,7 +1239,7 @@ ocx system codex-cli-update attest --candidate --npm-prefix isAppBundledCodexPath(path, platform) || isCodexCliUpdateVersionManagerPath(path, platform) + || isWithinConfiguredVersionManagerRoot(path, managerRoots, platform)); } const configDir = deps.configDir ?? getConfigDir(); if (!isSafeLocalInspectionPath(configDir, deps)) { diff --git a/src/codex/cli-installation-identity.ts b/src/codex/cli-installation-identity.ts index da06ad91738..2c9dcd6ac08 100644 --- a/src/codex/cli-installation-identity.ts +++ b/src/codex/cli-installation-identity.ts @@ -113,7 +113,7 @@ function manifest(file: ObservedInstallationFile, name: string, bin: string, exp } /** Only the standard npm cmd-shim is linked here; arbitrary wrappers remain unverified. */ -function npmCodexCommandShim(bytes: Uint8Array): boolean { +export function npmCodexCommandShim(bytes: Uint8Array): boolean { const expected = `@ECHO off GOTO start :find_dp0 diff --git a/src/codex/cli-installation-limits.ts b/src/codex/cli-installation-limits.ts new file mode 100644 index 00000000000..60b0868e3de --- /dev/null +++ b/src/codex/cli-installation-limits.ts @@ -0,0 +1,7 @@ +/** + * Shared finite observation bound, independent of platform/native API modules. + * Published npm backings exceed 256 MiB: Linux 0.163.0 is 296,865,480 bytes + * and Windows 0.153.4 is 295,408,944 bytes. Both readers hash native files in + * bounded chunks; 1 GiB limits read time without allocating the whole file. + */ +export const MAX_NATIVE_BACKING_BYTES = 1024 * 1024 * 1024; diff --git a/src/codex/cli-update-artifacts.ts b/src/codex/cli-update-artifacts.ts index 7c660610a30..8afe16b8c72 100644 --- a/src/codex/cli-update-artifacts.ts +++ b/src/codex/cli-update-artifacts.ts @@ -52,7 +52,7 @@ export function backingReference(main: Metadata, installName: string): { name: s export function codexCliArtifactSet(main: Metadata, query: (spec: string) => Metadata, platform: NodeJS.Platform, arch: string): CodexCliArtifactSet { const layout = codexCliPlatformBacking(platform, arch); - if (!layout || platform === "win32" || main.name !== "@openai/codex") throw new Error("artifact_platform_unverified"); + if (!layout || main.name !== "@openai/codex") throw new Error("artifact_platform_unverified"); const mainArtifact = artifact(main, "@openai/codex"); const reference = backingReference(main, layout.package); const backing = artifact(query(reference.spec), layout.package); diff --git a/src/codex/cli-update-plan.ts b/src/codex/cli-update-plan.ts index 70cc0cb8499..c30e50f2c36 100644 --- a/src/codex/cli-update-plan.ts +++ b/src/codex/cli-update-plan.ts @@ -9,6 +9,7 @@ import { npmInvocation } from "../update/npm-invocation.mjs"; import { CODEX_CLI_TRANSPORT_ENV_KEYS } from "../update/codex-cli-update-launch-policy.mjs"; import { CODEX_CLI_REGISTRY, isSha512Integrity, codexCliArtifactSet, codexCliArtifactSetDigest, type CodexCliArtifactSet } from "./cli-update-artifacts"; export { CODEX_CLI_REGISTRY } from "./cli-update-artifacts"; +import { windowsCodexCliPlanEvidence, withWindowsCodexCliPlanToolchain } from "./cli-windows-plan-provenance"; import { inspectCodexCliInstall, type CodexCliInstallKind, @@ -50,6 +51,7 @@ export type CodexCliUpdateChannel = "latest"; export type CodexCliUpdateRefusal = | "windows_inspection_deferred" + | "windows_apply_disabled" | "not_managed" | "installed_version_unverified" | "install_content_unverified" @@ -82,7 +84,7 @@ export interface CodexCliUpdatePlan { readonly channel: CodexCliUpdateChannel; readonly applicable: boolean; readonly refusal: CodexCliUpdateRefusal | null; - /** Present only for an applicable plan; there is nothing to quote for a refusal. */ + /** Also present for a proven Windows read-only plan whose apply remains disabled. */ readonly planId: string | null; readonly provenance: CodexCliInstallKind; readonly managed: boolean; @@ -434,12 +436,16 @@ export function resolveCodexCliUpdateTarget( /** Resolve both exact archives, including the publisher's platform-package alias. */ export function resolveCodexCliUpdateArtifactTarget(channel: CodexCliUpdateChannel, spawn: typeof spawnSync = spawnSync, boundEnv?: NodeJS.ProcessEnv, - platform: NodeJS.Platform = process.platform, arch: string = process.arch): CodexCliUpdateTarget { + platform: NodeJS.Platform = process.platform, arch: string = process.arch, + windowsToolchain?: { readonly node: string; readonly npmCli: string }): CodexCliUpdateTarget { let isolation: NpmConfigIsolation | undefined; try { isolation = createNpmConfigIsolation(undefined, boundEnv); const query = (spec: string) => { - const invocation = isolatedNpmTarget(["view", spec, "--json"], isolation!); + const args = isolatedNpmArgs(["view", spec, "--json"], isolation!); + const invocation = windowsToolchain + ? { bin: windowsToolchain.node, args: [windowsToolchain.npmCli, ...args], options: { cwd: isolation!.dir, env: isolation!.env } } + : isolatedNpmTarget(["view", spec, "--json"], isolation!); if (!invocation) throw new Error("npm unavailable"); const result = spawn(invocation.bin, invocation.args, { encoding: "utf8", timeout: REGISTRY_TIMEOUT_MS, maxBuffer: 1024 * 1024, windowsHide: true, ...invocation.options }); @@ -507,11 +513,10 @@ export async function createCodexCliUpdatePlan(deps: CodexCliUpdatePlanDeps = {} // pays the bounded content read; a bare ownership check leaves it unobserved. const report = await inspect({ ...(deps.inspectionDeps ?? {}), observeInstallContent: true }); - // Phase 1 performs no candidate filesystem I/O at all on Windows, so there is no - // ownership evidence to build a plan on. This stays dormant until the handle-bound - // provenance layer exists; pretending otherwise would require exactly the reads phase 1 - // refused to perform. - if (platform === "win32" || report.reason === "windows_inspection_deferred") { + // Windows planning requires private evidence from the production handle-bound + // predicate. A synthetic/copyable public managed flag cannot open this lane. + const windowsEvidence = windowsCodexCliPlanEvidence(report); + if ((platform === "win32" && !windowsEvidence) || report.reason === "windows_inspection_deferred") { return refusedPlan("windows_inspection_deferred", report, channel, null, NOT_EVALUATED); } if (!report.managed || report.provenance !== "npm-global") { @@ -539,8 +544,11 @@ export async function createCodexCliUpdatePlan(deps: CodexCliUpdatePlanDeps = {} // { PATH: "" } form, so this resolution cannot silently fall back to ambient. const resolveTarget = deps.resolveTarget ?? (ch => resolveCodexCliUpdateArtifactTarget(ch, deps.spawnProcess ?? spawnSync, deps.inspectionDeps?.env, - platform, deps.inspectionDeps?.arch ?? process.arch)); - const target = resolveTarget(channel); + platform, windowsEvidence?.arch ?? deps.inspectionDeps?.arch ?? process.arch, windowsEvidence ?? undefined)); + const target = windowsEvidence + ? await withWindowsCodexCliPlanToolchain(report, () => resolveTarget(channel)) + ?? Object.freeze({ kind: "unresolved" as const, reason: "Windows toolchain identity changed" }) + : resolveTarget(channel); if (target.kind !== "resolved") { return refusedPlan("target_unresolved", report, channel, target, NOT_EVALUATED); } @@ -575,8 +583,8 @@ export async function createCodexCliUpdatePlan(deps: CodexCliUpdatePlanDeps = {} schemaVersion: CODEX_CLI_UPDATE_SCHEMA_VERSION, package: CODEX_CLI_PACKAGE, channel, - applicable: true, - refusal: null, + applicable: platform !== "win32", + refusal: platform === "win32" ? "windows_apply_disabled" : null, planId: codexCliUpdatePlanId({ platform, provenance: report.provenance, diff --git a/src/codex/cli-windows-plan-provenance.ts b/src/codex/cli-windows-plan-provenance.ts new file mode 100644 index 00000000000..0f7b5d5c055 --- /dev/null +++ b/src/codex/cli-windows-plan-provenance.ts @@ -0,0 +1,151 @@ +import { createHash } from "node:crypto"; +import { win32 } from "node:path"; +import { parseStrictSemver } from "../lib/strict-semver"; +import { npmCodexCommandShim } from "./cli-installation-identity"; +import { MAX_NATIVE_BACKING_BYTES } from "./cli-installation-limits"; +import { deriveCodexCliInstallationInput } from "./cli-installation-targets"; +import type { CodexCliInstallProvenanceDeps, CodexCliInstallReport } from "./cli-install-provenance"; +import { resolveCodexRuntime } from "./runtime"; +import { inspectWindowsInstallationFiles, type WindowsInstallationFilesResult } from "./windows-installation-files"; + +/** Private plan evidence. Explicit `attest` observations never enter this map. */ +interface WindowsPlanEvidence { readonly node: string; readonly npmCli: string; readonly arch: "x64" | "arm64"; + readonly nodeEvidence: string; readonly npmEvidence: string } +const evidence = new WeakMap(); +export const windowsCodexCliPlanEvidence = (report: CodexCliInstallReport): WindowsPlanEvidence | null => evidence.get(report) ?? null; +const hash = (value: string): string => createHash("sha256").update(value).digest("hex"); +type File = Extract["files"][number]; +const id = (file: File): string => JSON.stringify([file.path, file.identity, file.digest]); +const sameWindowsPath = (left: string, right: string): boolean => win32.resolve(left).toLowerCase() === win32.resolve(right).toLowerCase(); +/** Metadata queries execute only while the observed Node/npm and ancestors deny replacement. */ +export async function withWindowsCodexCliPlanToolchain(report: CodexCliInstallReport, run: () => T): Promise { + const toolchain = evidence.get(report); + if (!toolchain) return null; + let result: T | null = null; + const observed = await inspectWindowsInstallationFiles([ + { path: toolchain.node, maxBytes: 128 * 1024 * 1024, hashOnly: true }, + { path: toolchain.npmCli, maxBytes: 256 * 1024 }, + ], files => { + if (id(files[0]!) !== toolchain.nodeEvidence || id(files[1]!) !== toolchain.npmEvidence) return false; + result = run(); return true; + }); + return observed.kind === "observed" ? result : null; +} +const json = (file: File): Record | null => { + try { const item = JSON.parse(Buffer.from(file.bytes).toString("utf8")); + return item && typeof item === "object" && !Array.isArray(item) ? item : null; + } catch { return null; } +}; +const entry = (item: Record, name: string): unknown => typeof item.bin === "string" ? item.bin + : item.bin && typeof item.bin === "object" ? (item.bin as Record)[name] : null; +function peArch(bytes: Uint8Array): "x64" | "arm64" | null { + const b = Buffer.from(bytes); + if (b.length < 64 || b.toString("ascii", 0, 2) !== "MZ") return null; + const offset = b.readUInt32LE(60); + if (offset > b.length - 6 || b.toString("ascii", offset, offset + 4) !== "PE\0\0") return null; + return b.readUInt16LE(offset + 4) === 0x8664 ? "x64" : b.readUInt16LE(offset + 4) === 0xaa64 ? "arm64" : null; +} + +/** + * Read-only supported subset: a proof-captured absolute standard npm cmd-shim + * override, its global-prefix package, prefix-local Node/npm, and the prefix-local + * optional backing. PATH-only Node selection remains refused until the snapshot + * proves the target cmd-shim's cwd and search policy. + */ +export async function inspectWindowsCodexCliPlanInstall( + deps: CodexCliInstallProvenanceDeps, fallback: CodexCliInstallReport, excluded: (path: string) => boolean, +): Promise { + const env = deps.env ?? {}; + const candidate = env.CODEX_CLI_PATH?.trim(); + if (!deps.selectionAttested || !candidate || !/^[a-z]:[\\/]/i.test(candidate) + || win32.basename(candidate) !== "codex.cmd" + || (deps.selectedCommand != null && deps.selectedCommand !== candidate)) return fallback; + const prefix = win32.dirname(candidate); + const localNodePath = win32.join(prefix, "node.exe"); + const localNode = await inspectWindowsInstallationFiles([{ path: localNodePath, maxBytes: 0, metadataOnly: true }]); + if (localNode.kind === "refused" && localNode.reason !== "not-found") return fallback; + const selectedPath = env.PATH; + const selectedPathExt = env.PATHEXT; + const derivation = await deriveCodexCliInstallationInput({ + codexCliPath: candidate, path: selectedPath ?? null, pathExt: selectedPathExt ?? null, + }, { platform: "win32" }); + if (derivation.kind !== "derived" || derivation.input.candidate !== candidate + || derivation.input.npmPrefix !== prefix) return fallback; + const node = derivation.input.node; + const npmCli = derivation.input.npmCli; + const npmRoot = win32.dirname(win32.dirname(npmCli)); + const pathNode = !sameWindowsPath(node, localNodePath); + if (pathNode) return fallback; + const root = win32.join(prefix, "node_modules", "@openai", "codex"); + const manifest = win32.join(root, "package.json"); + const npmManifest = win32.join(npmRoot, "package.json"); + const bin = win32.join(root, "bin", "codex.js"); + const paths = [candidate, node, npmCli, root]; + if (prefix === win32.parse(prefix).root || paths.some(excluded)) return fallback; + // Only already handle-observed files can pass the real resolver's existence + // predicate. The explicit override wins before saved-state/shim/PATH candidates. + const selected = () => resolveCodexRuntime({ platform: "win32", env, configDir: deps.configDir, + probeVersion: false, discoverAlternatives: false, existsSync: path => path === candidate, + readFileSync: () => { throw new Error("state not required for explicit override"); }, + readdirSync: () => [], statSync: () => { throw new Error("discovery disabled"); }, + }).runtime.command === candidate && env.CODEX_CLI_PATH?.trim() === candidate; + try { + const first = await inspectWindowsInstallationFiles([ + { path: manifest, maxBytes: 256 * 1024 }, { path: npmManifest, maxBytes: 256 * 1024 }, + { path: node, maxBytes: 4096, prefixOnly: true }, + ]); + if (first.kind !== "observed") return fallback; + const initial = new Map(first.files.map(file => [file.path, file])); + const main = json(initial.get(manifest)!); const npm = json(initial.get(npmManifest)!); + const arch = peArch(initial.get(node)!.bytes); + if (!main || main.name !== "@openai/codex" || typeof main.version !== "string" || !parseStrictSemver(main.version) + || entry(main, "codex") !== "bin/codex.js" || !npm || npm.name !== "npm" + || typeof npm.version !== "string" || !parseStrictSemver(npm.version) || entry(npm, "npm") !== "bin/npm-cli.js" + || !arch || (deps.arch && deps.arch !== arch)) return fallback; + const pkg = `@openai/codex-win32-${arch}`; + const triple = arch === "x64" ? "x86_64-pc-windows-msvc" : "aarch64-pc-windows-msvc"; + // Node's nearest-first package lookup, bounded to this independent prefix. + const lanes = [win32.join(root, "bin", "node_modules"), win32.join(root, "node_modules"), + win32.join(prefix, "node_modules", "@openai", "node_modules"), win32.join(prefix, "node_modules")]; + const backingSelection = async (): Promise => { + for (const lane of lanes) { + const path = win32.join(lane, pkg, "package.json"); + const probe = await inspectWindowsInstallationFiles([{ path, maxBytes: 0, metadataOnly: true }]); + if (probe.kind === "observed") return path; + if (probe.reason !== "not-found") return null; + } + return null; // No global module lookup or vendor fallback adoption. + }; + const backingManifest = await backingSelection(); + if (!backingManifest) return fallback; + const native = win32.join(win32.dirname(backingManifest), "vendor", triple, "bin", "codex.exe"); + const requests = [manifest, npmManifest, candidate, bin, npmCli, backingManifest].map(path => ({ path, maxBytes: 256 * 1024 })); + const final = await inspectWindowsInstallationFiles([...requests, + { path: node, maxBytes: 128 * 1024 * 1024, hashOnly: true }, + { path: native, maxBytes: MAX_NATIVE_BACKING_BYTES, hashOnly: true }, + ], async () => selected() && env.PATH === selectedPath && env.PATHEXT === selectedPathExt + && await deriveCodexCliInstallationInput({ codexCliPath: candidate, + path: selectedPath ?? null, pathExt: selectedPathExt ?? null }, { platform: "win32" }) + .then(current => current.kind === "derived" && current.input.candidate === candidate + && current.input.npmPrefix === prefix && current.input.node === node && current.input.npmCli === npmCli) + && await backingSelection() === backingManifest); + if (final.kind !== "observed") return fallback; + const files = new Map(final.files.map(file => [file.path, file])); + if (!npmCodexCommandShim(files.get(candidate)!.bytes)) return fallback; + for (const path of [manifest, npmManifest]) if (id(initial.get(path)!) !== id(files.get(path)!)) return fallback; + if (JSON.stringify(initial.get(node)!.identity) !== JSON.stringify(files.get(node)!.identity)) return fallback; + const backing = json(files.get(backingManifest)!); + if (!backing || "exports" in backing || ![pkg, "@openai/codex"].includes(String(backing.name)) + || ![main.version, `${main.version}-win32-${arch}`].includes(String(backing.version))) return fallback; + const content = hash(JSON.stringify(["windows-npm-read-only-plan-v2", arch, prefix, candidate, node, npmCli, + ...final.files.map(file => [file.path, file.identity, file.digest])])); + const report: CodexCliInstallReport = Object.freeze({ ...fallback, managed: true, selectionAttested: true, + provenance: "npm-global", reason: "managed_npm_global", packageVersion: main.version, + versionEvidence: Object.freeze({ kind: "package-manifest" }), installDigest: hash(prefix), + installContentDigest: content, shim: Object.freeze({ status: "not-tracked", backingKind: null }), + evidence: Object.freeze(["package_manifest", "package_manifest_digest", "install_content_digest", "global_npm_layout"] as const), + }); + evidence.set(report, Object.freeze({ node, npmCli, arch, nodeEvidence: id(files.get(node)!), npmEvidence: id(files.get(npmCli)!) })); + return report; + } catch { return fallback; } +} diff --git a/src/codex/windows-installation-files.ts b/src/codex/windows-installation-files.ts index e1f3cef5691..4fb026f220c 100644 --- a/src/codex/windows-installation-files.ts +++ b/src/codex/windows-installation-files.ts @@ -1,5 +1,6 @@ import { createHash } from "node:crypto"; import type { Library, Symbols } from "bun:ffi"; +import { MAX_NATIVE_BACKING_BYTES } from "./cli-installation-limits"; const KERNEL_SYMBOLS = { GetDriveTypeW: { args: ["ptr"], returns: "u32" }, @@ -53,6 +54,9 @@ class InspectionRefusal extends Error { constructor(readonly reason: Refusal) { super(reason); } } const MIB = 1024 * 1024; +// One native backing, the unchanged production Node bound, and at most twelve +// bounded text requests. This is an I/O ceiling; hash-only buffers stay empty. +const MAX_REQUEST_BYTES = MAX_NATIVE_BACKING_BYTES + (128 + 12) * MIB; const REPARSE = 0x400; const DIRECTORY = 0x10; const INVALID_HANDLE = 0xffffffffffffffffn; @@ -81,6 +85,7 @@ function components(path: string): { drive: string; names: string[] } | null { */ export async function inspectWindowsInstallationFiles( requests: readonly WindowsInstallationFileRequest[], + finalize?: (files: Extract["files"]) => boolean | Promise, ): Promise { if (!Array.isArray(requests) || requests.length === 0 || requests.length > 12) { return { kind: "refused", reason: "invalid-request" }; @@ -92,11 +97,11 @@ export async function inspectWindowsInstallationFiles( || (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; + || request.maxBytes > (request.hashOnly ? MAX_NATIVE_BACKING_BYTES : MIB)) return null; ceiling += request.maxBytes; return parsedPath; }); - if (parsed.some(path => path === null) || ceiling > 300 * MIB) { + if (parsed.some(path => path === null) || ceiling > MAX_REQUEST_BYTES) { return { kind: "refused", reason: "invalid-request" }; } if (process.platform !== "win32" || process.arch !== "x64") { @@ -230,6 +235,7 @@ export async function inspectWindowsInstallationFiles( } return { path: request.path, identity, bytes, digest: truncated ? "" : hash.digest("hex") }; }); + if (finalize && !await finalize(observed)) throw new InspectionRefusal("identity-changed"); for (const file of files) { if (JSON.stringify(inspect(file.handle, false)) !== JSON.stringify(file.identity)) { throw new InspectionRefusal("identity-changed"); diff --git a/structure/codex-cli-update.md b/structure/codex-cli-update.md index 48869fc87f2..122a743bdb6 100644 --- a/structure/codex-cli-update.md +++ b/structure/codex-cli-update.md @@ -41,8 +41,8 @@ observes linkage at that boundary and does not prevent later symlink mutation. A no bound content is refused install_content_unverified before any registry query. The digest is observation only — it carries no execution guarantee — so the apply phase re-observes rather than rely on it. The observation is opt-in: the plan path requests it because the planId -binds it, while a bare check leaves the digest null rather than paying the bounded read. -Selection observation uses the runtime resolver with provenance's bounded no-follow, nonblocking state reader and local executable-path policy. Windows returns before invoking the resolver; its deferred inspection performs no candidate or configuration I/O. +binds it, while a bare POSIX check leaves the digest null rather than paying the bounded read. +Selection observation uses the runtime resolver with provenance's bounded no-follow, nonblocking state reader and local executable-path policy. Windows supports the restricted handle-bound plan predicate below; other Windows layouts remain deferred. Its child environment drops npm configuration overrides, named Node startup/output controls, the `LD_*`/`DYLD_*` loader families and the OpenSSL startup configuration and provider/engine search variables; proxy settings and `NODE_EXTRA_CA_CERTS` remain supported. @@ -56,6 +56,39 @@ Linux per-process `status`/`cmdline` read failures invalidate a complete observa the restart/kill path skips unreadable entries while retaining independently verified same-user app-server targets. This plan neither installs a runtime nor signals an existing Codex session. Windows enumeration/installation limits remain unchanged. +## Windows read-only plan predicate + +`src/codex/cli-windows-plan-provenance.ts` admits only a proof-captured, drive-absolute +`CODEX_CLI_PATH` naming the exact standard npm `codex.cmd` grammar. The global npm +package prefix comes from the shim directory and its `node_modules` tree. This plan +currently requires prefix-local `node.exe` and npm. Derivation may propose a PATH Node only +when the local Node probe proves `not-found`; a refused/reparse probe never counts as absent. +The production plan still refuses that external candidate: a standard npm cmd-shim runs bare +`node` when its local Node is absent, and CMD can prefer the invocation working directory. +The proof-bound launcher snapshot has PATH/PATHEXT but no target-shim invocation +cwd/search-result evidence, so a separate Node cannot be selected safely yet. The production runtime resolver confirms the explicit Codex override's +priority without saved-state reads or probes. A PE header binds Node's architecture; +the package manifest/bin, npm manifest/bin, Node and the first in-prefix platform +package plus native backing are observed together. Backing lookup follows the +nearest-first Node lanes and is repeated while the files and ancestors remain held. +Native handles refuse reparse points, writers and rename/delete sharing; initial +manifest and Node identities must match the final observation. Text stays bounded +to 256 KiB and Node to 128 MiB. The native backing uses the same finite 1 GiB +`MAX_NATIVE_BACKING_BYTES` as POSIX, defined in `src/codex/cli-installation-limits.ts`. +The native reader bounds aggregate requests to that backing plus 128 MiB Node +and twelve 1 MiB text requests; hash-only bytes are never buffered whole. + +PATH-only or separate-prefix Node selection, autostart wrappers, global/vendor fallback, +app/Store/MSIX bundles and version-manager ownership remain unverified and unmanaged. +The explicit `attest` report stays observation-only. Only the production plan predicate +creates private plan evidence; copying a public report cannot enable Windows planning. +This observes linkage and content, not publisher authenticity or a whole-tree guarantee. +Metadata queries use the observed Node/npm paths under freshly matching held handles, +the existing isolated npm configuration/cache, and the pinned registry. No installer runs. +A completed Windows read-only plan has a deterministic ID and `applicable: false`, +`refusal: windows_apply_disabled`; active/unknown sessions still refuse earlier. +The independent Windows apply entrypoint remains refused before inspection or lease creation. + ## Explicit POSIX apply `src/cli/codex-cli-update.ts` accepts `apply --plan-id <32 lowercase hex> --yes` with optional JSON output. diff --git a/structure/runtime.md b/structure/runtime.md index 3224ed2d5d1..caffb2dcad2 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. 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. The separate production Windows check/plan predicate is described in [Windows read-only planning](codex-cli-update.md#windows-read-only-plan-predicate); unsupported layouts retain deferred inspection. 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-windows-installation-files.test.ts b/tests/codex-integration/codex-cli-windows-installation-files.test.ts index 2f8919591fc..4182e0d9c98 100644 --- a/tests/codex-integration/codex-cli-windows-installation-files.test.ts +++ b/tests/codex-integration/codex-cli-windows-installation-files.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { createHash } from "node:crypto"; -import { closeSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, symlinkSync, writeFileSync, writeSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { closeSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, symlinkSync, truncateSync, writeFileSync, writeSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -9,6 +10,7 @@ import { setWindowsInstallationFilesOpenedForTests, } from "../../src/codex/windows-installation-files"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { MAX_NATIVE_BACKING_BYTES } from "../../src/codex/cli-installation-limits"; const nativeTest = process.platform === "win32" && process.arch === "x64" ? test : test.skip; let fixture = ""; @@ -44,9 +46,11 @@ describe("explicit Windows installation file snapshot", () => { expect(await inspectWindowsInstallationFiles(Array.from({ length: 13 }, () => ({ path: "C:\\package.json", maxBytes: 10 })))) .toEqual({ kind: "refused", reason: "invalid-request" }); expect(await inspectWindowsInstallationFiles([ - { path: "C:\\node.exe", maxBytes: 256 * 1024 * 1024, hashOnly: true }, - { path: "C:\\other.exe", maxBytes: 256 * 1024 * 1024, hashOnly: true }, + { path: "C:\\native.exe", maxBytes: MAX_NATIVE_BACKING_BYTES, hashOnly: true }, + { path: "C:\\other.exe", maxBytes: MAX_NATIVE_BACKING_BYTES, hashOnly: true }, ])).toEqual({ kind: "refused", reason: "invalid-request" }); + expect(await inspectWindowsInstallationFiles([{ path: "C:\\native.exe", maxBytes: MAX_NATIVE_BACKING_BYTES + 1, hashOnly: true }])) + .toEqual({ kind: "refused", reason: "invalid-request" }); }); nativeTest("reports absent leaf and ancestor as not-found", async () => { @@ -162,6 +166,22 @@ describe("explicit Windows installation file snapshot", () => { expect(file.digest).toBe(""); }); + nativeTest("one byte over the native limit refuses before reads and releases its handles", async () => { + const path = join(fixture, "oversized-native.fixture"); + writeFileSync(path, ""); + expect(process.env.SystemRoot).toBeDefined(); + const sparse = spawnSync(join(process.env.SystemRoot!, "System32", "fsutil.exe"), ["sparse", "setflag", path], + { windowsHide: true, encoding: "utf8", timeout: 5_000 }); + expect(sparse.error).toBeUndefined(); expect(sparse.status).toBe(0); + truncateSync(path, MAX_NATIVE_BACKING_BYTES + 1); + let opened = 0; + setWindowsInstallationFilesOpenedForTests(() => { opened++; }); + expect(await inspectWindowsInstallationFiles([{ path, maxBytes: MAX_NATIVE_BACKING_BYTES, hashOnly: true }])) + .toEqual({ kind: "refused", reason: "size-limit" }); + expect(opened).toBe(0); + renameSync(path, path + ".released"); + }); + nativeTest("prefixOnly returns a bounded prefix of an oversized file without a digest", async () => { const path = join(fixture, "wrapper.cmd"); const prefix = Buffer.from("@echo off\r\nrem marker\r\n"); diff --git a/tests/codex-integration/codex-cli-windows-plan.test.ts b/tests/codex-integration/codex-cli-windows-plan.test.ts new file mode 100644 index 00000000000..5f960682767 --- /dev/null +++ b/tests/codex-integration/codex-cli-windows-plan.test.ts @@ -0,0 +1,255 @@ +import { afterEach, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, renameSync, symlinkSync, truncateSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { inspectCodexCliInstall, MAX_NATIVE_BACKING_BYTES } from "../../src/codex/cli-install-provenance"; +import { applyCodexCliUpdate } from "../../src/codex/cli-update-apply"; +import { createCodexCliUpdatePlan } from "../../src/codex/cli-update-plan"; +import { handleCodexCliUpdateRoutes } from "../../src/server/management/codex-cli-update-routes"; +import type { ManagementContext } from "../../src/server/management/context"; +import { setWindowsInstallationFilesOpenedForTests } from "../../src/codex/windows-installation-files"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { repoPath, repoRoot } from "../helpers/repo-root"; +import { handleCodexCliUpdateCommand } from "../../src/cli/codex-cli-update"; +import { initializeNodeLauncherContext, NODE_LAUNCH_CONTEXT_ENV, NODE_LAUNCH_PROOF_PREFIX } from "../../src/cli/launcher-context"; + +const native = process.platform === "win32" && process.arch === "x64" ? test : test.skip; +const roots: string[] = []; +afterEach(() => { + initializeNodeLauncherContext(["bun", "cli"], {}); + setWindowsInstallationFilesOpenedForTests(); + for (const root of roots.splice(0)) removeTreeWithRetry(root); +}); +// npm's independent cmd-shim fixture; no candidate is ever executed. +const cmd = ['@ECHO off', 'GOTO start', ':find_dp0', 'SET dp0=%~dp0', 'EXIT /b', ':start', 'SETLOCAL', + 'CALL :find_dp0', '', 'IF EXIST "%dp0%\\node.exe" (', ' SET "_prog=%dp0%\\node.exe"', ') ELSE (', + ' SET "_prog=node"', ' SET PATHEXT=%PATHEXT:;.JS;=;%', ')', '', + 'endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\@openai\\codex\\bin\\codex.js" %*', +].join('\r\n'); +function fixture(separateNodeToolchain = false) { + const root = mkdtempSync(join(tmpdir(), "ocx-windows-plan-")); roots.push(root); + const prefix = join(root, "npm"); + const toolchainPrefix = separateNodeToolchain ? join(root, "nodejs") : prefix; + const npmRoot = join(toolchainPrefix, "node_modules", "npm"); + const packageRoot = join(prefix, "node_modules", "@openai", "codex"); + // Closest Node lookup lane: there is no earlier ancestor package to shadow it. + const backing = join(packageRoot, "bin", "node_modules", "@openai", "codex-win32-x64"); + const files = new Map([ + [join(prefix, "codex.cmd"), cmd], + [join(prefix, "codex.opencodex-real.cmd"), "stale unselected shim backup"], + [join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "1.2.3", bin: { codex: "bin/codex.js" } })], + [join(packageRoot, "bin", "codex.js"), 'throw new Error("Codex must never execute");'], + [join(backing, "package.json"), JSON.stringify({ name: "@openai/codex", version: "1.2.3-win32-x64" })], + [join(backing, "vendor", "x86_64-pc-windows-msvc", "bin", "codex.exe"), "synthetic backing bytes"], + [join(npmRoot, "package.json"), JSON.stringify({ name: "npm", version: "11.0.0", bin: { npm: "bin/npm-cli.js" } })], + [join(npmRoot, "bin", "npm-cli.js"), 'throw new Error("npm must never execute");'], + ]); + const pe = Buffer.alloc(128); pe.write('MZ'); pe.writeUInt32LE(64, 60); pe.write('PE\0\0', 64); pe.writeUInt16LE(0x8664, 68); + files.set(join(toolchainPrefix, "node.exe"), pe); + for (const [path, value] of files) { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, value); } + const deps = { env: { CODEX_CLI_PATH: join(prefix, "codex.cmd"), PATH: toolchainPrefix, PATHEXT: ".EXE;.CMD" }, + configDir: join(root, "absent-home"), + selectionAttested: true, arch: "x64" as const, observeInstallContent: true }; + return { root, prefix, toolchainPrefix, npmRoot, packageRoot, backing, files, deps }; +} + +native("production Windows inspector binds selected npm shim, package, backing and toolchain with no candidate execution", async () => { + const f = fixture(); const before = [...f.files].map(([path]) => readFileSync(path)); + const report = await inspectCodexCliInstall(f.deps); + expect(report).toMatchObject({ managed: true, selectionAttested: true, provenance: "npm-global", reason: "managed_npm_global", packageVersion: "1.2.3" }); + expect(report.installContentDigest).toMatch(/^[a-f0-9]{64}$/); + expect((await inspectCodexCliInstall(f.deps)).installContentDigest).toBe(report.installContentDigest); + expect(JSON.stringify(report)).not.toContain(f.root); + expect([...f.files].map(([path]) => readFileSync(path))).toEqual(before); +}); + +native("a production plan is deterministic while Windows apply stays explicitly disabled", async () => { + const f = fixture(); + const deps = { inspectionDeps: f.deps, resolveTarget: () => ({ kind: "resolved" as const, version: "1.2.4", integrity: "sha512-" + Buffer.alloc(64, 1).toString("base64") }), + scanProcesses: () => ({ kind: "observed" as const, processes: [] }) }; + const first = await createCodexCliUpdatePlan(deps); + expect(first).toMatchObject({ managed: true, applicable: false, refusal: "windows_apply_disabled", installedVersion: "1.2.3", targetVersion: "1.2.4" }); + expect(first.planId).toMatch(/^[a-f0-9]{32}$/); + expect((await createCodexCliUpdatePlan(deps)).planId).toBe(first.planId); + writeFileSync(join(f.backing, "vendor", "x86_64-pc-windows-msvc", "bin", "codex.exe"), "changed backing bytes"); + expect((await createCodexCliUpdatePlan(deps)).planId).not.toBe(first.planId); +}); + +native("a Windows plan id with applicable=false cannot enter a management apply job", async () => { + const f = fixture(); + const deps = { inspectionDeps: f.deps, + resolveTarget: () => ({ kind: "resolved" as const, version: "1.2.4", integrity: "sha512-" + Buffer.alloc(64, 1).toString("base64") }), + scanProcesses: () => ({ kind: "observed" as const, processes: [] }) }; + const plan = await createCodexCliUpdatePlan(deps); + expect(plan.planId).toMatch(/^[a-f0-9]{32}$/); + expect(plan).toMatchObject({ applicable: false, refusal: "windows_apply_disabled" }); + + const oldHome = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = f.deps.configDir; + try { + const request = new Request("http://localhost/api/codex/cli-update/apply", { method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ requestId: "windows-nonapplicable-plan-001", planId: plan.planId, confirmed: true }) }); + const ctx: ManagementContext = { req: request, url: new URL(request.url), + config: { port: 10100, providers: {} } as import("../../src/types").OcxConfig, + deps: {}, version: "test", principal: "gui-session", trustedLoopbackIngress: true, guiSessionIssuance: null, + convergeCodexCatalog: async () => ({ status: "skipped", reason: "not-requested", retryable: false }), + syncClaudeAgentDefsBestEffort: async () => {} }; + const response = await handleCodexCliUpdateRoutes(ctx); + expect(response?.status).toBe(503); + expect(await response?.json()).toEqual({ error: "unsupported_store" }); + + let leaseAttempts = 0, installAttempts = 0; + const apply = await applyCodexCliUpdate(plan.planId!, { platform: "win32", inspectionDeps: f.deps, + acquireLease: () => { leaseAttempts += 1; return null; }, + install: async () => { installAttempts += 1; return { terminal: true, exitCode: 0 }; } }); + expect(apply).toMatchObject({ outcome: "refused", reason: "windows_inspection_deferred" }); + expect(leaseAttempts).toBe(0); + expect(installAttempts).toBe(0); + } finally { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = oldHome; + } +}); + +native("a published-size Windows npm backing binds; one past the shared finite limit refuses", async () => { + const f = fixture(); + const binary = join(f.backing, "vendor", "x86_64-pc-windows-msvc", "bin", "codex.exe"); + // An NTFS sparse file has the installed 0.153.4-win32-x64 backing's logical + // size without copying its bytes or allocating a dense 295 MB fixture. + writeFileSync(binary, ""); + expect(process.env.SystemRoot).toBeDefined(); + const sparse = spawnSync(join(process.env.SystemRoot!, "System32", "fsutil.exe"), ["sparse", "setflag", binary], + { windowsHide: true, encoding: "utf8", timeout: 5_000 }); + expect(sparse.error).toBeUndefined(); expect(sparse.status).toBe(0); + truncateSync(binary, 295_408_944); + const report = await inspectCodexCliInstall(f.deps); + expect(report.managed).toBe(true); expect(report.installContentDigest).toMatch(/^[a-f0-9]{64}$/); + truncateSync(binary, MAX_NATIVE_BACKING_BYTES + 1); + expect((await inspectCodexCliInstall(f.deps)).managed).toBe(false); +}, 30_000); + +native("unattested, mismatched selection, app/Store/version-manager and unverified launchers remain unmanaged", async () => { + const f = fixture(); + for (const deps of [ { ...f.deps, selectionAttested: false }, { ...f.deps, selectedCommand: join(f.root, "other.cmd") }, + ...["WindowsApps", "OpenAI/Codex/bin/app", ".nvm", "standalone"].map(dir => ({ ...f.deps, + env: { ...f.deps.env, CODEX_CLI_PATH: join(f.root, dir, "codex.exe") } })), + ]) expect((await inspectCodexCliInstall(deps)).managed).toBe(false); + writeFileSync(join(f.prefix, "codex.cmd"), cmd + "\r\necho retargeted"); + expect((await inspectCodexCliInstall(f.deps)).managed).toBe(false); +}); + +native("a junction prefix is refused and prefix retarget attempts fail throughout the coherent observation", async () => { + const f = fixture(); const alias = join(f.root, "alias"); symlinkSync(f.prefix, alias, "junction"); + expect((await inspectCodexCliInstall({ ...f.deps, env: { ...f.deps.env, CODEX_CLI_PATH: join(alias, "codex.cmd") } })).managed).toBe(false); + let blocked = 0; + setWindowsInstallationFilesOpenedForTests(() => { try { renameSync(f.prefix, f.prefix + "-moved"); } catch { blocked++; } }); + expect((await inspectCodexCliInstall(f.deps)).managed).toBe(true); + expect(blocked).toBeGreaterThan(0); +}); + +native("a selected override changing during observation refuses the original prefix", async () => { + const f = fixture(); + setWindowsInstallationFilesOpenedForTests(() => { f.deps.env.CODEX_CLI_PATH = join(f.root, "other", "codex.cmd"); }); + expect((await inspectCodexCliInstall(f.deps)).managed).toBe(false); +}); + +native("npm toolchain drift after inspection refuses metadata without executing the replacement", async () => { + const f = fixture(); let queries = 0; + const plan = await createCodexCliUpdatePlan({ inspectionDeps: f.deps, inspect: async deps => { + const report = await inspectCodexCliInstall(deps); expect(report.managed).toBe(true); + writeFileSync(join(f.npmRoot, "bin", "npm-cli.js"), "throw new Error('replacement');"); + return report; + }, resolveTarget: () => { queries++; throw new Error("must not query changed toolchain"); } }); + expect(queries).toBe(0); expect(plan.refusal).toBe("target_unresolved"); +}); + +native("a separate PATH Node is refused without proof of the target cmd-shim's search result", async () => { + const f = fixture(true); + expect((await inspectCodexCliInstall({ ...f.deps, selectionAttested: false })).managed).toBe(false); + const report = await inspectCodexCliInstall(f.deps); + expect(report).toMatchObject({ managed: false, installContentDigest: null }); + let targetQueries = 0; + const plan = await createCodexCliUpdatePlan({ inspectionDeps: f.deps, + resolveTarget: () => { targetQueries++; throw new Error("unproven Node must not query registry"); } }); + expect(targetQueries).toBe(0); + expect(plan.managed).toBe(false); + expect(plan.refusal).toBe("windows_inspection_deferred"); + expect(JSON.stringify(plan)).not.toContain(f.root); +}); + +native("synthetic CMD lookup selects a current-directory node.cmd ahead of PATH", () => { + const root = mkdtempSync(join(tmpdir(), "ocx-cmd-search-")); roots.push(root); + const current = join(root, "current"); const pathDir = join(root, "path"); + mkdirSync(current); mkdirSync(pathDir); + writeFileSync(join(current, "node.cmd"), "@echo off\r\necho fixture-current-directory\r\n"); + writeFileSync(join(pathDir, "node.cmd"), "@echo off\r\necho fixture-path\r\n"); + const systemRoot = process.env.SystemRoot!; + const cmdExe = join(systemRoot, "System32", "cmd.exe"); + const run = spawnSync(cmdExe, ["/d", "/c", "node"], { cwd: current, encoding: "utf8", windowsHide: true, + timeout: 5_000, env: { SystemRoot: systemRoot, COMSPEC: cmdExe, PATH: pathDir, + PATHEXT: ".COM;.EXE;.BAT;.CMD" } }); + expect(run.error).toBeUndefined(); + expect(run.status).toBe(0); + expect(run.stdout).toContain("fixture-current-directory"); + expect(run.stdout).not.toContain("fixture-path"); +}); + +native("a refused or reparse prefix-local Node is not treated as absent for PATH fallback", async () => { + const f = fixture(true); + mkdirSync(join(f.prefix, "node.exe")); + const report = await inspectCodexCliInstall(f.deps); + expect(report.managed).toBe(false); + expect(report.installContentDigest).toBeNull(); + + const g = fixture(true); + const target = join(g.root, "node-target"); mkdirSync(target); + symlinkSync(target, join(g.prefix, "node.exe"), "junction"); + const reparse = await inspectCodexCliInstall(g.deps); + expect(reparse.managed).toBe(false); + expect(reparse.installContentDigest).toBeNull(); +}); + +native("actual CLI handler reaches production inspector and plan from the proof-bound selection", async () => { + const f = fixture(); const proof = "W".repeat(43); + initializeNodeLauncherContext(["bun", "cli", `${NODE_LAUNCH_PROOF_PREFIX}${proof}`], { + [NODE_LAUNCH_CONTEXT_ENV]: JSON.stringify({ version: 1, proof, anthropicEnvSlots: [], + codexCliInspectionEnv: { codexCliPath: f.deps.env.CODEX_CLI_PATH, path: f.prefix, + pathExt: ".EXE;.CMD", managerRoots: {}, configDir: f.deps.configDir, arch: "x64" } }), + }); + let reached = false; + expect(await handleCodexCliUpdateCommand(["plan", "--json"], { createPlan: async deps => { + const plan = await createCodexCliUpdatePlan({ ...deps, + resolveTarget: () => ({ kind: "resolved", version: "1.2.4", integrity: "sha512-" + Buffer.alloc(64, 1).toString("base64") }), + scanProcesses: () => ({ kind: "observed", processes: [] }), + }); + expect(plan.managed).toBe(true); expect(plan.planId).toMatch(/^[a-f0-9]{32}$/); + expect(plan.refusal).toBe("windows_apply_disabled"); reached = true; return plan; + } })).toBe(0); + expect(reached).toBe(true); +}); + +native("published Node CLI uses the production inspector and observed toolchain for read-only metadata", () => { + const f = fixture(); const hostNode = Bun.which("node"); expect(hostNode).not.toBeNull(); + const node = join(f.toolchainPrefix, "node.exe"); copyFileSync(hostNode!, node); + const integrity = "sha512-" + Buffer.alloc(64, 1).toString("base64"); + const main = { name: "@openai/codex", version: "1.2.4", optionalDependencies: { "@openai/codex-win32-x64": "1.2.4" }, + dist: { integrity, tarball: "https://registry.npmjs.org/@openai/codex/-/codex-1.2.4.tgz" } }; + const backing = { name: "@openai/codex-win32-x64", version: "1.2.4", dist: { integrity, + tarball: "https://registry.npmjs.org/@openai/codex-win32-x64/-/codex-win32-x64-1.2.4.tgz" } }; + // Controlled registry protocol fixture; no real npm installation or network request. + writeFileSync(join(f.npmRoot, "bin", "npm-cli.js"), + `const spec = process.argv[3]; console.log(JSON.stringify(spec.startsWith('@openai/codex-win32-x64@') ? ${JSON.stringify(backing)} : ${JSON.stringify(main)}));`); + const run = spawnSync(node, [repoPath("bin", "ocx.mjs"), "system", "codex-cli-update", "plan", "--json"], { + cwd: repoRoot(), encoding: "utf8", windowsHide: true, timeout: 45_000, + env: { ...process.env, CODEX_CLI_PATH: f.deps.env.CODEX_CLI_PATH, OPENCODEX_HOME: f.deps.configDir, + OPENCODEX_BUN_PATH: process.execPath, OCX_NO_DESKTOP_HANDOFF: "1" }, + }); + expect(run.error).toBeUndefined(); expect(run.status).toBe(0); + const plan = JSON.parse(run.stdout); + expect(plan.managed).toBe(true); expect(plan.targetVersion).toBe("1.2.4"); + expect(plan.targetIntegrity).toBe(integrity); + expect(plan.applicable).toBe(false); + expect(["blocked_active_session", "blocked_process_state_unknown", "windows_apply_disabled"]).toContain(plan.refusal); + expect(JSON.stringify(plan)).not.toContain(f.root); +}, 60_000); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 2e44270c870..be96044d4ad 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1394,6 +1394,7 @@ "management-desktop-compatibility-runtime-routes.test.ts": "server", "management-desktop-compatibility-settings.test.ts": "server", "optional-desktop-upstream.test.ts": "lib", + "codex-cli-windows-plan.test.ts": "codex-integration", "opaque-proxy-tunnel.test.ts": "lib", "opaque-proxy-tunnel-protocol.test.ts": "lib", "server-desktop-compatibility-startup.test.ts": "server", From 5bb78cc250ba2c892b8d22e721843896335a0afd Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Sat, 24 Oct 2026 13:36:20 +0900 Subject: [PATCH 2/5] fix(codex): keep bare Windows check content-observation opt-in A bare check now requests the entrypoint, npm CLI, Node and native files metadata-only: the same handles stay open and identity-bound, but zero content bytes are streamed and installContentDigest stays null. The plan path still opts in, so the plan-bound digest and evidence are unchanged. CodeRabbit review on #7596 comment 4365760303. --- src/codex/cli-windows-plan-provenance.ts | 25 ++++++++++++---- src/codex/windows-installation-files.ts | 11 ++++++- structure/codex-cli-update.md | 6 +++- structure/runtime.md | 12 ++++---- .../codex-cli-windows-plan.test.ts | 30 ++++++++++++++++++- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/codex/cli-windows-plan-provenance.ts b/src/codex/cli-windows-plan-provenance.ts index 0f7b5d5c055..fc0def35e8d 100644 --- a/src/codex/cli-windows-plan-provenance.ts +++ b/src/codex/cli-windows-plan-provenance.ts @@ -56,6 +56,9 @@ export async function inspectWindowsCodexCliPlanInstall( deps: CodexCliInstallProvenanceDeps, fallback: CodexCliInstallReport, excluded: (path: string) => boolean, ): Promise { const env = deps.env ?? {}; + // The bounded content read is opt-in: a bare ownership check holds the same + // reparse-refusing handles and identity set but streams no file content. + const observe = deps.observeInstallContent === true; const candidate = env.CODEX_CLI_PATH?.trim(); if (!deps.selectionAttested || !candidate || !/^[a-z]:[\\/]/i.test(candidate) || win32.basename(candidate) !== "codex.cmd" @@ -119,10 +122,19 @@ export async function inspectWindowsCodexCliPlanInstall( const backingManifest = await backingSelection(); if (!backingManifest) return fallback; const native = win32.join(win32.dirname(backingManifest), "vendor", triple, "bin", "codex.exe"); - const requests = [manifest, npmManifest, candidate, bin, npmCli, backingManifest].map(path => ({ path, maxBytes: 256 * 1024 })); + // Ownership requires the manifests, shim and backing manifest bytes plus the + // bounded Node header read above; entrypoint, npm CLI, Node and native + // contents only feed the opt-in digest, so they stay metadata-held without it. + const owned = (path: string) => ({ path, maxBytes: 256 * 1024 }); + const bound = (path: string) => observe ? { path, maxBytes: 256 * 1024 } : { path, maxBytes: 0, metadataOnly: true }; + const requests = [owned(manifest), owned(npmManifest), owned(candidate), bound(bin), bound(npmCli), owned(backingManifest)]; const final = await inspectWindowsInstallationFiles([...requests, - { path: node, maxBytes: 128 * 1024 * 1024, hashOnly: true }, - { path: native, maxBytes: MAX_NATIVE_BACKING_BYTES, hashOnly: true }, + observe + ? { path: node, maxBytes: 128 * 1024 * 1024, hashOnly: true } + : { path: node, maxBytes: 0, metadataOnly: true }, + observe + ? { path: native, maxBytes: MAX_NATIVE_BACKING_BYTES, hashOnly: true } + : { path: native, maxBytes: 0, metadataOnly: true }, ], async () => selected() && env.PATH === selectedPath && env.PATHEXT === selectedPathExt && await deriveCodexCliInstallationInput({ codexCliPath: candidate, path: selectedPath ?? null, pathExt: selectedPathExt ?? null }, { platform: "win32" }) @@ -137,13 +149,14 @@ export async function inspectWindowsCodexCliPlanInstall( const backing = json(files.get(backingManifest)!); if (!backing || "exports" in backing || ![pkg, "@openai/codex"].includes(String(backing.name)) || ![main.version, `${main.version}-win32-${arch}`].includes(String(backing.version))) return fallback; - const content = hash(JSON.stringify(["windows-npm-read-only-plan-v2", arch, prefix, candidate, node, npmCli, - ...final.files.map(file => [file.path, file.identity, file.digest])])); + const content = observe ? hash(JSON.stringify(["windows-npm-read-only-plan-v2", arch, prefix, candidate, node, npmCli, + ...final.files.map(file => [file.path, file.identity, file.digest])])) : null; const report: CodexCliInstallReport = Object.freeze({ ...fallback, managed: true, selectionAttested: true, provenance: "npm-global", reason: "managed_npm_global", packageVersion: main.version, versionEvidence: Object.freeze({ kind: "package-manifest" }), installDigest: hash(prefix), installContentDigest: content, shim: Object.freeze({ status: "not-tracked", backingKind: null }), - evidence: Object.freeze(["package_manifest", "package_manifest_digest", "install_content_digest", "global_npm_layout"] as const), + evidence: Object.freeze(["package_manifest", "package_manifest_digest", + ...(content ? ["install_content_digest" as const] : []), "global_npm_layout"] as const), }); evidence.set(report, Object.freeze({ node, npmCli, arch, nodeEvidence: id(files.get(node)!), npmEvidence: id(files.get(npmCli)!) })); return report; diff --git a/src/codex/windows-installation-files.ts b/src/codex/windows-installation-files.ts index 4fb026f220c..ff66cf5a6d1 100644 --- a/src/codex/windows-installation-files.ts +++ b/src/codex/windows-installation-files.ts @@ -65,6 +65,11 @@ let openedForTests: (() => void) | undefined; export function setWindowsInstallationFilesOpenedForTests(callback?: () => void): void { openedForTests = callback; } +let readForTests: ((path: string, bytesRead: number) => void) | undefined; +/** Runs once per request with its streamed content bytes (0 for metadata-only), for read accounting. */ +export function setWindowsInstallationFilesReadForTests(callback?: (path: string, bytesRead: number) => void): void { + readForTests = callback; +} function components(path: string): { drive: string; names: string[] } | null { if (typeof path !== "string" || path.length > 8192 || !/^[A-Za-z]:[\\/]/.test(path)) return null; @@ -212,7 +217,10 @@ export async function inspectWindowsInstallationFiles( openedForTests?.(); const observed = files.map(({ request, handle, identity }) => { const hash = createHash("sha256"); - if (request.metadataOnly) return { path: request.path, identity, bytes: new Uint8Array(), digest: "" }; + if (request.metadataOnly) { + readForTests?.(request.path, 0); + 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); @@ -233,6 +241,7 @@ export async function inspectWindowsInstallationFiles( throw new InspectionRefusal("identity-changed"); } } + readForTests?.(request.path, offset); return { path: request.path, identity, bytes, digest: truncated ? "" : hash.digest("hex") }; }); if (finalize && !await finalize(observed)) throw new InspectionRefusal("identity-changed"); diff --git a/structure/codex-cli-update.md b/structure/codex-cli-update.md index a298a0c93f1..683db8a3d79 100644 --- a/structure/codex-cli-update.md +++ b/structure/codex-cli-update.md @@ -41,7 +41,7 @@ observes linkage at that boundary and does not prevent later symlink mutation. A no bound content is refused install_content_unverified before any registry query. The digest is observation only — it carries no execution guarantee — so the apply phase re-observes rather than rely on it. The observation is opt-in: the plan path requests it because the planId -binds it, while a bare POSIX check leaves the digest null rather than paying the bounded read. +binds it, while a bare check leaves the digest null rather than paying the bounded read. Selection observation uses the runtime resolver with provenance's bounded no-follow, nonblocking state reader and local executable-path policy. Windows supports the restricted handle-bound plan predicate below; other Windows layouts remain deferred. Its child environment drops npm configuration overrides, named Node startup/output controls, the `LD_*`/`DYLD_*` loader families and the OpenSSL startup configuration and @@ -77,6 +77,10 @@ to 256 KiB and Node to 128 MiB. The native backing uses the same finite 1 GiB `MAX_NATIVE_BACKING_BYTES` as POSIX, defined in `src/codex/cli-installation-limits.ts`. The native reader bounds aggregate requests to that backing plus 128 MiB Node and twelve 1 MiB text requests; hash-only bytes are never buffered whole. +Only the opt-in content observation streams the entrypoint, npm CLI, Node and native bytes. The +plan path sets it, so the plan-bound `installContentDigest` is preserved; a bare check holds the +same files — Node and native included — open and identity-checked metadata-only, with zero content +bytes read and a null `installContentDigest`. PATH-only or separate-prefix Node selection, autostart wrappers, global/vendor fallback, app/Store/MSIX bundles and version-manager ownership remain unverified and unmanaged. diff --git a/structure/runtime.md b/structure/runtime.md index b12c951890c..d89444ab76c 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -313,12 +313,12 @@ package-registry request and reads bounded provenance evidence for the configure package metadata, and shim binding. The proof-bound launcher snapshot does not attest successful Codex execution; POSIX selection is attested when the runtime resolver selects the same canonical environment candidate without executing it. A linked npm-global manifest can then establish managed ownership; persisted candidates stay unattested. A manifest-owned POSIX npm-global install can also report an opt-in bounded content digest over the manifest, declared entrypoint, and the platform backing files at the layout's modeled locations; it is an observation claim, not execution proof. -On Windows this first slice performs no candidate/configuration filesystem I/O: it preserves only proof-captured -absolute environment candidates for lexical app-bundle/version-manager reporting and otherwise fails closed. That -fail-closed result records which observation was missing: a run with no proof-captured environment candidate reports -`windows_inspection_deferred`, because persisted selection is never consulted there and the command cannot claim that -no Codex CLI exists; a captured candidate whose path is not lexically eligible reports `candidate_path_unavailable`. -POSIX keeps `candidate_unavailable` for an unobserved candidate. +On Windows x64, a proof-captured absolute standard npm `codex.cmd` with prefix-local Node/npm and verified in-prefix +platform backing is inspected under reparse-refusing held handles; the opt-in content digest stays null here. +Other layouts keep the earlier fail-closed shape: only proof-captured absolute environment candidates receive +lexical app-bundle/version-manager reporting. A run with no captured candidate reports `windows_inspection_deferred` +(persisted selection is never consulted, so it cannot claim no Codex CLI exists); an ineligible captured path reports +`candidate_path_unavailable`. POSIX keeps `candidate_unavailable` for an unobserved candidate. This check does not admit a runtime or authorize an install. The command exposes no private mutation authority and does not query a registry, execute Codex/npm, install, repair, stop, restart, or change configuration/cache state. diff --git a/tests/codex-integration/codex-cli-windows-plan.test.ts b/tests/codex-integration/codex-cli-windows-plan.test.ts index 5f960682767..eac0fb43dcc 100644 --- a/tests/codex-integration/codex-cli-windows-plan.test.ts +++ b/tests/codex-integration/codex-cli-windows-plan.test.ts @@ -8,7 +8,7 @@ import { applyCodexCliUpdate } from "../../src/codex/cli-update-apply"; import { createCodexCliUpdatePlan } from "../../src/codex/cli-update-plan"; import { handleCodexCliUpdateRoutes } from "../../src/server/management/codex-cli-update-routes"; import type { ManagementContext } from "../../src/server/management/context"; -import { setWindowsInstallationFilesOpenedForTests } from "../../src/codex/windows-installation-files"; +import { setWindowsInstallationFilesOpenedForTests, setWindowsInstallationFilesReadForTests } from "../../src/codex/windows-installation-files"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoPath, repoRoot } from "../helpers/repo-root"; import { handleCodexCliUpdateCommand } from "../../src/cli/codex-cli-update"; @@ -19,6 +19,7 @@ const roots: string[] = []; afterEach(() => { initializeNodeLauncherContext(["bun", "cli"], {}); setWindowsInstallationFilesOpenedForTests(); + setWindowsInstallationFilesReadForTests(); for (const root of roots.splice(0)) removeTreeWithRetry(root); }); // npm's independent cmd-shim fixture; no candidate is ever executed. @@ -64,6 +65,33 @@ native("production Windows inspector binds selected npm shim, package, backing a expect([...f.files].map(([path]) => readFileSync(path))).toEqual(before); }); +native("a bare Windows check honors the bounded content-observation opt-in", async () => { + const f = fixture(); + // Production check context: selectionAttested without observeInstallContent. + const deps = { env: f.deps.env, configDir: f.deps.configDir, + selectionAttested: f.deps.selectionAttested, arch: f.deps.arch }; + const reads: { path: string; bytes: number }[] = []; + setWindowsInstallationFilesReadForTests((path, bytes) => reads.push({ path, bytes })); + const report = await inspectCodexCliInstall(deps); + expect(report).toMatchObject({ managed: true, selectionAttested: true, provenance: "npm-global", + reason: "managed_npm_global", packageVersion: "1.2.3", installContentDigest: null }); + expect(report.evidence).not.toContain("install_content_digest"); + // The same files stay opened, held and identity-bound; only the content streams stop. + const bytesFor = (suffix: string) => reads.filter(read => read.path.endsWith(suffix)).map(read => read.bytes); + expect(bytesFor("codex.exe")).toEqual([0]); + for (const suffix of ["codex.js", "npm-cli.js"]) { + const fileReads = bytesFor(suffix); + expect(fileReads.length).toBeGreaterThan(0); + expect(fileReads.every(bytes => bytes === 0)).toBe(true); + } + const nodeReads = bytesFor("node.exe"); + expect(nodeReads.length).toBeGreaterThan(0); + expect(nodeReads.filter(bytes => bytes > 0)).toEqual([128]); + // Only bounded ownership reads remain: nothing exceeds the 4 KiB Node header bound. + expect(Math.max(0, ...reads.map(read => read.bytes))).toBeLessThanOrEqual(4096); + expect(reads.some(read => read.path.endsWith("codex.cmd") && read.bytes > 0)).toBe(true); +}); + native("a production plan is deterministic while Windows apply stays explicitly disabled", async () => { const f = fixture(); const deps = { inspectionDeps: f.deps, resolveTarget: () => ({ kind: "resolved" as const, version: "1.2.4", integrity: "sha512-" + Buffer.alloc(64, 1).toString("base64") }), From 4183b1265765caf2df18707f58d0565a0afbf4ff Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Sat, 24 Oct 2026 13:36:24 +0900 Subject: [PATCH 3/5] docs(cli): align localized update-check wording with Windows x64 support The seven localized CLI pages still claimed Windows defers inspection without reading candidates. Align each check paragraph with the English contract: supported x64 npm installs are inspected under reparse-refusing held handles, and a supported Windows install can produce a deterministic read-only plan refused with windows_apply_disabled. CodeRabbit review on #7596 comment 4365760298. --- docs-site/src/content/docs/fr/reference/cli.md | 2 +- docs-site/src/content/docs/fr/reference/cli/agents.md | 2 +- docs-site/src/content/docs/ja/reference/cli.md | 2 +- docs-site/src/content/docs/ja/reference/cli/agents.md | 2 +- docs-site/src/content/docs/ko/reference/cli.md | 2 +- docs-site/src/content/docs/ko/reference/cli/agents.md | 2 +- docs-site/src/content/docs/ru/reference/cli.md | 2 +- docs-site/src/content/docs/ru/reference/cli/agents.md | 2 +- docs-site/src/content/docs/tr/reference/cli.md | 2 +- docs-site/src/content/docs/tr/reference/cli/agents.md | 2 +- docs-site/src/content/docs/zh-cn/reference/cli.md | 2 +- docs-site/src/content/docs/zh-cn/reference/cli/agents.md | 2 +- docs-site/src/content/docs/zh-tw/reference/cli.md | 2 +- docs-site/src/content/docs/zh-tw/reference/cli/agents.md | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/cli.md b/docs-site/src/content/docs/fr/reference/cli.md index 2563cdc0de9..015a3e3aafe 100644 --- a/docs-site/src/content/docs/fr/reference/cli.md +++ b/docs-site/src/content/docs/fr/reference/cli.md @@ -17,7 +17,7 @@ Exécutez `ocx help` (ou `ocx --help` / `ocx -h`) pour afficher l’aide génér Les commandes de gestion communiquent avec l’API de gestion du proxy actif. Elles s’appuient sur le port d’exécution enregistré et sur des contrôles d’identité, plutôt que sur un second chemin de configuration. Un proxy arrêté ou inaccessible est représenté par une réponse HTTP 503 et entraîne un code de sortie CLI non nul. Les commandes explicitement documentées comme des opérations de configuration hors ligne peuvent, quant à elles, valider et modifier le fichier de configuration sans proxy actif. -`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows diffère l’inspection sans lire les fichiers du candidat ni la configuration. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan). +`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows x64 prend aussi en charge un `codex.cmd` npm standard absolu lié à la preuve, avec Node/npm locaux au préfixe et une plateforme de support vérifiée dans le préfixe, en utilisant des handles maintenus qui refusent la réanalyse. Les autres dispositions Windows restent différées ou non gérées. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Une installation Windows prise en charge peut produire un plan en lecture seule déterministe avec `applicable: false` et `refusal: windows_apply_disabled` ; la sélection par PATH seul, un Node de préfixe distinct et les wrappers de démarrage automatique OpenCodex restent différés. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan). Pour observer une installation Windows x64, consultez [`attest`](/fr/reference/cli/agents/). Sans chemins explicites, la commande observe le candidat sélectionné identifié par l’instantané du lanceur lié à la preuve ; aucune autorité de mise à jour ni sélection du runtime n’est attestée. diff --git a/docs-site/src/content/docs/fr/reference/cli/agents.md b/docs-site/src/content/docs/fr/reference/cli/agents.md index 1337a76d0ce..e788fa47bbe 100644 --- a/docs-site/src/content/docs/fr/reference/cli/agents.md +++ b/docs-site/src/content/docs/fr/reference/cli/agents.md @@ -287,7 +287,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows diffère l’inspection sans lire les fichiers du candidat ni la configuration. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan). +`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows x64 prend aussi en charge un `codex.cmd` npm standard absolu lié à la preuve, avec Node/npm locaux au préfixe et une plateforme de support vérifiée dans le préfixe, en utilisant des handles maintenus qui refusent la réanalyse. Les autres dispositions Windows restent différées ou non gérées. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Une installation Windows prise en charge peut produire un plan en lecture seule déterministe avec `applicable: false` et `refusal: windows_apply_disabled` ; la sélection par PATH seul, un Node de préfixe distinct et les wrappers de démarrage automatique OpenCodex restent différés. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan). Sous Windows, une commande simple capturée comme `CODEX_CLI_PATH=codex`, un chemin distant ou un chemin de périphérique produit plutôt `candidate_path_unavailable`. Le candidat a été capturé, mais son chemin ne convient pas à cette inspection. diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index c766a1cdfaf..4762c9a1fad 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -20,7 +20,7 @@ opencodex CLI は `ocx` です。最初のコマンド名でディスパッチ 管理コマンドは、2 番目の構成パスを維持するのではなく、記録されたランタイム ポートと ID チェックを使用して、稼働中のプロキシの管理 API をラウンドトリップします。停止したプロキシまたは到達不能なプロキシは HTTP 503 として表され、ゼロ以外の CLI 終了が生成されます。オフライン構成操作として明示的に文書化されているコマンドは、代わりに、稼働中のプロキシを使用せずに設定ファイルを検証および編集できます。 -`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows は候補や設定ファイルを読まずに検査を延期します。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。 +`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows x64 は、証明にバインドされた絶対パスの標準 npm `codex.cmd` とプレフィックスローカルの Node/npm、検証済みのインプレフィックスプラットフォームバッキングもサポートし、リパースを拒否する保持ハンドルを使用します。他の Windows レイアウトは引き続き延期または非管理です。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。サポートされている Windows インストールは `applicable: false` と `refusal: windows_apply_disabled` を持つ決定論的な読み取り専用 plan を生成でき、PATH のみの選択・別プレフィックスの Node・OpenCodex 自動起動ラッパーは引き続き延期されます。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。 Windows x64 インストールの観測は [`attest` コマンド](/ja/reference/cli/agents/)を参照してください。明示パスを省略すると、証明にバインドされたランチャースナップショットが特定した選択候補を観測します。更新権限やランタイム選択は証明しません。 diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index ebe43edfffc..fa91fed7720 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -218,7 +218,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows は候補や設定ファイルを読まずに検査を延期します。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。 +`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows x64 は、証明にバインドされた絶対パスの標準 npm `codex.cmd` とプレフィックスローカルの Node/npm、検証済みのインプレフィックスプラットフォームバッキングもサポートし、リパースを拒否する保持ハンドルを使用します。他の Windows レイアウトは引き続き延期または非管理です。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。サポートされている Windows インストールは `applicable: false` と `refusal: windows_apply_disabled` を持つ決定論的な読み取り専用 plan を生成でき、PATH のみの選択・別プレフィックスの Node・OpenCodex 自動起動ラッパーは引き続き延期されます。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。 Windows で `CODEX_CLI_PATH=codex` のような単純なコマンド名、リモートパス、デバイスパスが候補としてキャプチャされた場合は、`candidate_path_unavailable` を報告します。候補は取得されていますが、そのパスはこの検査の対象になりません。 diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index 3abe8fbd115..9b02fb11fcd 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -17,7 +17,7 @@ opencodex CLI는 `ocx`입니다. 첫 번째 명령 이름으로 분기하며, `s 관리 명령은 기록된 런타임 포트와 신원 검사를 사용해 살아 있는 프록시의 management API와 왕복 통신하며, 두 번째 설정 경로를 따로 두지 않습니다. 멈췄거나 닿을 수 없는 프록시는 HTTP 503으로 표시되며 CLI는 0이 아닌 종료 코드를 반환합니다. 명시적으로 오프라인 설정 작업으로 문서화된 명령은 라이브 프록시 없이 설정 파일을 검증하고 수정할 수 있습니다. -`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows는 후보나 설정 파일을 읽지 않고 검사를 연기합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요. +`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows x64는 증명에 바인딩된 절대 표준 npm `codex.cmd`와 프리픽스 로컬 Node/npm, 검증된 인프리픽스 플랫폼 백킹도 지원하며 reparse를 거부하는 보유 핸들을 사용합니다. 다른 Windows 레이아웃은 계속 연기되거나 관리되지 않습니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. 지원되는 Windows 설치는 `applicable: false`와 `refusal: windows_apply_disabled`를 가진 결정적 읽기 전용 plan을 생성할 수 있으며 PATH 전용 선택·분리된 프리픽스 Node·OpenCodex 자동 시작 래퍼는 계속 연기됩니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요. Windows x64 설치 관측은 [`attest` 명령](/ko/reference/cli/agents/)을 참조하세요. 명시적 경로 없이 증명에 바인딩된 런처 스냅샷이 식별한 선택 후보를 관측하며, 업데이트 권한이나 런타임 선택을 증명하지 않습니다. diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index c8bb7e53731..8fc893a9890 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -256,7 +256,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows는 후보나 설정 파일을 읽지 않고 검사를 연기합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요. +`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows x64는 증명에 바인딩된 절대 표준 npm `codex.cmd`와 프리픽스 로컬 Node/npm, 검증된 인프리픽스 플랫폼 백킹도 지원하며 reparse를 거부하는 보유 핸들을 사용합니다. 다른 Windows 레이아웃은 계속 연기되거나 관리되지 않습니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. 지원되는 Windows 설치는 `applicable: false`와 `refusal: windows_apply_disabled`를 가진 결정적 읽기 전용 plan을 생성할 수 있으며 PATH 전용 선택·분리된 프리픽스 Node·OpenCodex 자동 시작 래퍼는 계속 연기됩니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요. Windows에서 `CODEX_CLI_PATH=codex` 같은 단순 명령 이름이나 원격 경로·장치 경로가 후보로 캡처되면 `candidate_path_unavailable`을 보고합니다. 후보는 캡처됐지만 해당 경로가 이 검사 대상에 적합하지 않은 경우입니다. diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index da9ff0c9096..aca2b049ee2 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -31,7 +31,7 @@ runtime port и проверку identity, а не поддерживая вто явно документированные как offline-операции с конфигурацией, вместо этого могут валидировать и редактировать файл конфигурации без живого прокси. -`check` не обращается к реестру и не запускает Codex. В POSIX `selectionAttested` может быть `true`, если разрешитель выбирает тот же канонический путь, что и кандидат окружения, зафиксированный опубликованным средством запуска. Связанный манифест npm-global может подтвердить `managed: true`, если он доказывает владение и, когда версия кандидата сообщается, она совпадает с версией манифеста; сохранённые кандидаты остаются неаттестованными. Windows откладывает проверку, не читая файлы кандидата и конфигурацию. Прямой запуск через Bun или исходный код не даёт доказательства средства запуска. `plan` обращается к официальному реестру с изолированными временными настройками, кешем и журналами npm, отказывая при активных сессиях или неполном наблюдении процессов. Он ничего не устанавливает и не посылает сигналы. См. [полное описание команд и причин отказа (англ.)](/reference/cli/agents/#read-only-codex-cli-update-plan). +`check` не обращается к реестру и не запускает Codex. В POSIX `selectionAttested` может быть `true`, если разрешитель выбирает тот же канонический путь, что и кандидат окружения, зафиксированный опубликованным средством запуска. Связанный манифест npm-global может подтвердить `managed: true`, если он доказывает владение и, когда версия кандидата сообщается, она совпадает с версией манифеста; сохранённые кандидаты остаются неаттестованными. Windows x64 также поддерживает привязанный к доказательству абсолютный стандартный npm `codex.cmd` с локальными для префикса Node/npm и проверенной внутрипрефиксной платформенной подложкой, используя удерживаемые дескрипторы, отклоняющие reparse. Остальные раскладки Windows остаются отложенными или неуправляемыми. Прямой запуск через Bun или исходный код не даёт доказательства средства запуска. `plan` обращается к официальному реестру с изолированными временными настройками, кешем и журналами npm, отказывая при активных сессиях или неполном наблюдении процессов. Он ничего не устанавливает и не посылает сигналы. Поддерживаемая установка Windows может создать детерминированный план только для чтения с `applicable: false` и `refusal: windows_apply_disabled`; выбор только по PATH, Node из отдельного префикса и обёртки автозапуска OpenCodex остаются отложенными. См. [полное описание команд и причин отказа (англ.)](/reference/cli/agents/#read-only-codex-cli-update-plan). Для наблюдения установки Windows x64 см. [`attest`](/ru/reference/cli/agents/). Без явных путей команда наблюдает выбранного кандидата, определённого привязанным к доказательству снимком средства запуска; полномочия на обновление и выбор runtime не подтверждаются. diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 1c24581fb08..1656aca847b 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -270,7 +270,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check` не обращается к реестру и не запускает Codex. В POSIX `selectionAttested` может быть `true`, если разрешитель выбирает тот же канонический путь, что и кандидат окружения, зафиксированный опубликованным средством запуска. Связанный манифест npm-global может подтвердить `managed: true`, если он доказывает владение и, когда версия кандидата сообщается, она совпадает с версией манифеста; сохранённые кандидаты остаются неаттестованными. Windows откладывает проверку, не читая файлы кандидата и конфигурацию. Прямой запуск через Bun или исходный код не даёт доказательства средства запуска. `plan` обращается к официальному реестру с изолированными временными настройками, кешем и журналами npm, отказывая при активных сессиях или неполном наблюдении процессов. Он ничего не устанавливает и не посылает сигналы. См. [полное описание команд и причин отказа (англ.)](/reference/cli/agents/#read-only-codex-cli-update-plan). +`check` не обращается к реестру и не запускает Codex. В POSIX `selectionAttested` может быть `true`, если разрешитель выбирает тот же канонический путь, что и кандидат окружения, зафиксированный опубликованным средством запуска. Связанный манифест npm-global может подтвердить `managed: true`, если он доказывает владение и, когда версия кандидата сообщается, она совпадает с версией манифеста; сохранённые кандидаты остаются неаттестованными. Windows x64 также поддерживает привязанный к доказательству абсолютный стандартный npm `codex.cmd` с локальными для префикса Node/npm и проверенной внутрипрефиксной платформенной подложкой, используя удерживаемые дескрипторы, отклоняющие reparse. Остальные раскладки Windows остаются отложенными или неуправляемыми. Прямой запуск через Bun или исходный код не даёт доказательства средства запуска. `plan` обращается к официальному реестру с изолированными временными настройками, кешем и журналами npm, отказывая при активных сессиях или неполном наблюдении процессов. Он ничего не устанавливает и не посылает сигналы. Поддерживаемая установка Windows может создать детерминированный план только для чтения с `applicable: false` и `refusal: windows_apply_disabled`; выбор только по PATH, Node из отдельного префикса и обёртки автозапуска OpenCodex остаются отложенными. См. [полное описание команд и причин отказа (англ.)](/reference/cli/agents/#read-only-codex-cli-update-plan). В Windows захваченная команда без полного пути, например `CODEX_CLI_PATH=codex`, удалённый путь или путь устройства возвращает `candidate_path_unavailable`. Кандидат захвачен, но его путь не подходит для этой проверки. diff --git a/docs-site/src/content/docs/tr/reference/cli.md b/docs-site/src/content/docs/tr/reference/cli.md index e8bcea6d8fc..9422b3821da 100644 --- a/docs-site/src/content/docs/tr/reference/cli.md +++ b/docs-site/src/content/docs/tr/reference/cli.md @@ -36,7 +36,7 @@ yönetim API'sine gidiş-dönüş yapar. Durdurulmuş veya erişilemeyen bir pro yapılandırma işlemleri olarak açıkça belgelenen komutlar, bunun yerine canlı bir proxy olmadan yapılandırma dosyasını doğrulayabilir ve düzenleyebilir. -`check` kayıt defterini sorgulamaz ve Codex’i çalıştırmaz. POSIX üzerinde çözümleyicinin seçtiği kanonik yol, yayımlanmış başlatıcının yakaladığı ortam adayıyla aynıysa `selectionAttested: true` olabilir. Bağlantılı npm-global paket bildirimi sahipliği doğrularsa ve aday sürüm bildirildiyse bu sürüm bildirimdeki sürümle eşleşirse `managed: true` olabilir; kalıcı adaylar doğrulanmaz. Windows, aday veya yapılandırma dosyalarını okumadan incelemeyi erteler. Doğrudan Bun/kaynak çalıştırması başlatıcı kanıtı sağlamaz. `plan`, yalıtılmış geçici npm yapılandırması, önbelleği ve günlükleriyle resmî kayıt defterini sorgular; etkin oturumları veya eksik süreç gözlemini reddeder. Yazılım kurmaz ve süreçlere sinyal göndermez. [Komutların ve ret nedenlerinin tam açıklamasına (İngilizce)](/reference/cli/agents/#read-only-codex-cli-update-plan) bakın. +`check` kayıt defterini sorgulamaz ve Codex’i çalıştırmaz. POSIX üzerinde çözümleyicinin seçtiği kanonik yol, yayımlanmış başlatıcının yakaladığı ortam adayıyla aynıysa `selectionAttested: true` olabilir. Bağlantılı npm-global paket bildirimi sahipliği doğrularsa ve aday sürüm bildirildiyse bu sürüm bildirimdeki sürümle eşleşirse `managed: true` olabilir; kalıcı adaylar doğrulanmaz. Windows x64 ayrıca kanıta bağlı mutlak standart npm `codex.cmd` ile önek-yerel Node/npm ve önek içinde doğrulanmış platform desteğini destekler; yeniden ayrıştırmayı reddeden tutulan tanıtıcılar kullanır. Diğer Windows düzenleri ertelenmiş veya yönetimsiz kalır. Doğrudan Bun/kaynak çalıştırması başlatıcı kanıtı sağlamaz. `plan`, yalıtılmış geçici npm yapılandırması, önbelleği ve günlükleriyle resmî kayıt defterini sorgular; etkin oturumları veya eksik süreç gözlemini reddeder. Yazılım kurmaz ve süreçlere sinyal göndermez. Desteklenen bir Windows kurulumu `applicable: false` ve `refusal: windows_apply_disabled` ile belirleyici bir salt okunur plan üretebilir; yalnızca PATH seçimi, ayrı önekli Node ve OpenCodex otomatik başlatma sarmalayıcıları ertelenmiş kalır. [Komutların ve ret nedenlerinin tam açıklamasına (İngilizce)](/reference/cli/agents/#read-only-codex-cli-update-plan) bakın. Windows x64 kurulum gözlemi için [`attest`](/tr/reference/cli/agents/) komutuna bakın. Açık yollar verilmezse komut, kanıta bağlı başlatıcı anlık görüntüsünün belirlediği seçili adayı gözlemler; güncelleme yetkisi veya çalışma zamanı seçimi doğrulanmaz. diff --git a/docs-site/src/content/docs/tr/reference/cli/agents.md b/docs-site/src/content/docs/tr/reference/cli/agents.md index 132a02705d3..6a52461ec5e 100644 --- a/docs-site/src/content/docs/tr/reference/cli/agents.md +++ b/docs-site/src/content/docs/tr/reference/cli/agents.md @@ -322,7 +322,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check` kayıt defterini sorgulamaz ve Codex’i çalıştırmaz. POSIX üzerinde çözümleyicinin seçtiği kanonik yol, yayımlanmış başlatıcının yakaladığı ortam adayıyla aynıysa `selectionAttested: true` olabilir. Bağlantılı npm-global paket bildirimi sahipliği doğrularsa ve aday sürüm bildirildiyse bu sürüm bildirimdeki sürümle eşleşirse `managed: true` olabilir; kalıcı adaylar doğrulanmaz. Windows, aday veya yapılandırma dosyalarını okumadan incelemeyi erteler. Doğrudan Bun/kaynak çalıştırması başlatıcı kanıtı sağlamaz. `plan`, yalıtılmış geçici npm yapılandırması, önbelleği ve günlükleriyle resmî kayıt defterini sorgular; etkin oturumları veya eksik süreç gözlemini reddeder. Yazılım kurmaz ve süreçlere sinyal göndermez. [Komutların ve ret nedenlerinin tam açıklamasına (İngilizce)](/reference/cli/agents/#read-only-codex-cli-update-plan) bakın. +`check` kayıt defterini sorgulamaz ve Codex’i çalıştırmaz. POSIX üzerinde çözümleyicinin seçtiği kanonik yol, yayımlanmış başlatıcının yakaladığı ortam adayıyla aynıysa `selectionAttested: true` olabilir. Bağlantılı npm-global paket bildirimi sahipliği doğrularsa ve aday sürüm bildirildiyse bu sürüm bildirimdeki sürümle eşleşirse `managed: true` olabilir; kalıcı adaylar doğrulanmaz. Windows x64 ayrıca kanıta bağlı mutlak standart npm `codex.cmd` ile önek-yerel Node/npm ve önek içinde doğrulanmış platform desteğini destekler; yeniden ayrıştırmayı reddeden tutulan tanıtıcılar kullanır. Diğer Windows düzenleri ertelenmiş veya yönetimsiz kalır. Doğrudan Bun/kaynak çalıştırması başlatıcı kanıtı sağlamaz. `plan`, yalıtılmış geçici npm yapılandırması, önbelleği ve günlükleriyle resmî kayıt defterini sorgular; etkin oturumları veya eksik süreç gözlemini reddeder. Yazılım kurmaz ve süreçlere sinyal göndermez. Desteklenen bir Windows kurulumu `applicable: false` ve `refusal: windows_apply_disabled` ile belirleyici bir salt okunur plan üretebilir; yalnızca PATH seçimi, ayrı önekli Node ve OpenCodex otomatik başlatma sarmalayıcıları ertelenmiş kalır. [Komutların ve ret nedenlerinin tam açıklamasına (İngilizce)](/reference/cli/agents/#read-only-codex-cli-update-plan) bakın. Windows üzerinde `CODEX_CLI_PATH=codex` gibi yalın bir komut, uzak yol veya aygıt yolu aday olarak yakalanırsa `candidate_path_unavailable` bildirilir. Aday yakalanmıştır; ancak yolu bu inceleme için uygun değildir. diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index c6e89a3b4fa..80e9204f399 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -17,7 +17,7 @@ opencodex 的 CLI 是 `ocx`。它会根据第一个命令名进行分发;文 管理命令会通过实时代理的管理 API 往返调用,使用记录下来的运行时端口和身份检查,而不是维护第二条配置路径。已停止或不可达的代理会被表示为 HTTP 503,并导致 CLI 以非零状态退出。明确标注为离线配置操作的命令,则可以在没有实时代理的情况下验证并编辑配置文件。 -`check` 不查询注册表,也不运行 Codex。在 POSIX 上,当已发布启动器捕获的环境候选项与运行时解析器选择的规范路径一致时,`selectionAttested` 可以为 `true`;如果关联的 npm-global 软件包清单证明了所有权,`managed` 可以为 `true`。持久化候选项仍不获认证。Windows 不读取候选项或配置文件,而是推迟检查。直接通过 Bun 或源码运行时没有启动器证明。`plan` 使用隔离的临时 npm 配置、缓存和日志查询官方注册表,并拒绝活动会话或不完整的进程观测。它不安装软件,也不向进程发送信号。请参阅[完整命令和拒绝原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 +`check` 不查询注册表,也不运行 Codex。在 POSIX 上,当已发布启动器捕获的环境候选项与运行时解析器选择的规范路径一致时,`selectionAttested` 可以为 `true`;如果关联的 npm-global 软件包清单证明了所有权,`managed` 可以为 `true`。持久化候选项仍不获认证。Windows x64 还支持经证明绑定的绝对路径标准 npm `codex.cmd`,以及前缀本地 Node/npm 和已验证的前缀内平台支持组件,并使用拒绝重解析的持有句柄。其他 Windows 布局仍然推迟或不受管理。直接通过 Bun 或源码运行时没有启动器证明。`plan` 使用隔离的临时 npm 配置、缓存和日志查询官方注册表,并拒绝活动会话或不完整的进程观测。它不安装软件,也不向进程发送信号。受支持的 Windows 安装可以生成 `applicable: false` 且 `refusal: windows_apply_disabled` 的确定性只读 plan;仅 PATH 选择、独立前缀的 Node 和 OpenCodex 自启动包装器仍然推迟。请参阅[完整命令和拒绝原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 有关 Windows x64 安装观察,请参阅 [`attest` 命令](/zh-cn/reference/cli/agents/)。未提供显式路径时,该命令观察由受证明约束的启动器快照识别出的已选候选项;它不授予更新权限,也不证明运行时选择。 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index f3b95ff0f58..868a1553d63 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -222,7 +222,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check` 不查询注册表,也不运行 Codex。在 POSIX 上,当已发布启动器捕获的环境候选项与运行时解析器选择的规范路径一致时,`selectionAttested` 可以为 `true`;如果关联的 npm-global 软件包清单证明了所有权,`managed` 可以为 `true`。持久化候选项仍不获认证。Windows 不读取候选项或配置文件,而是推迟检查。直接通过 Bun 或源码运行时没有启动器证明。`plan` 使用隔离的临时 npm 配置、缓存和日志查询官方注册表,并拒绝活动会话或不完整的进程观测。它不安装软件,也不向进程发送信号。请参阅[完整命令和拒绝原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 +`check` 不查询注册表,也不运行 Codex。在 POSIX 上,当已发布启动器捕获的环境候选项与运行时解析器选择的规范路径一致时,`selectionAttested` 可以为 `true`;如果关联的 npm-global 软件包清单证明了所有权,`managed` 可以为 `true`。持久化候选项仍不获认证。Windows x64 还支持经证明绑定的绝对路径标准 npm `codex.cmd`,以及前缀本地 Node/npm 和已验证的前缀内平台支持组件,并使用拒绝重解析的持有句柄。其他 Windows 布局仍然推迟或不受管理。直接通过 Bun 或源码运行时没有启动器证明。`plan` 使用隔离的临时 npm 配置、缓存和日志查询官方注册表,并拒绝活动会话或不完整的进程观测。它不安装软件,也不向进程发送信号。受支持的 Windows 安装可以生成 `applicable: false` 且 `refusal: windows_apply_disabled` 的确定性只读 plan;仅 PATH 选择、独立前缀的 Node 和 OpenCodex 自启动包装器仍然推迟。请参阅[完整命令和拒绝原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 在 Windows 上,如果捕获到 `CODEX_CLI_PATH=codex` 这样的裸命令、远程路径或设备路径,则报告 `candidate_path_unavailable`。这些情况下候选项已被捕获,但其路径不适用于此检查。 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli.md b/docs-site/src/content/docs/zh-tw/reference/cli.md index acfdece2371..ad1e14b5035 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli.md @@ -28,7 +28,7 @@ opencodex 的命令列工具是 `ocx`。它依第一個命令名稱分派,有 設定路徑。停止或無法連線的代理以 HTTP 503 呈現,並產生非零的 CLI 離開碼。明確記載為 離線設定操作的命令,可以在沒有執行中代理的情況下驗證與編輯設定檔。 -`check` 不查詢註冊表,也不執行 Codex。在 POSIX 上,當已發布啟動器擷取的環境候選項與執行階段解析器選擇的正規路徑相同時,`selectionAttested` 可以為 `true`;若關聯的 npm-global 套件資訊清單證明了所有權,`managed` 可以為 `true`。持久化候選項仍不獲認證。Windows 不讀取候選項或設定檔,而是延後檢查。直接透過 Bun 或原始碼執行時沒有啟動器證明。`plan` 使用隔離的暫存 npm 設定、快取和日誌查詢官方註冊表,並拒絕活動工作階段或不完整的程序觀測。它不安裝軟體,也不向程序傳送訊號。請參閱[完整命令與拒絕原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 +`check` 不查詢註冊表,也不執行 Codex。在 POSIX 上,當已發布啟動器擷取的環境候選項與執行階段解析器選擇的正規路徑相同時,`selectionAttested` 可以為 `true`;若關聯的 npm-global 套件資訊清單證明了所有權,`managed` 可以為 `true`。持久化候選項仍不獲認證。Windows x64 也支援經證明繫結的絕對路徑標準 npm `codex.cmd`,以及前綴本機 Node/npm 和已驗證的前綴內平台支援元件,並使用拒絕重新剖析的持有控制代碼。其他 Windows 配置仍然延後或不受管理。直接透過 Bun 或原始碼執行時沒有啟動器證明。`plan` 使用隔離的暫存 npm 設定、快取和日誌查詢官方註冊表,並拒絕活動工作階段或不完整的程序觀測。它不安裝軟體,也不向程序傳送訊號。受支援的 Windows 安裝可產生 `applicable: false` 且 `refusal: windows_apply_disabled` 的確定性唯讀 plan;僅 PATH 選取、獨立前綴的 Node 和 OpenCodex 自動啟動包裝器仍然延後。請參閱[完整命令與拒絕原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 觀測 Windows x64 安裝的方式請參閱 [`attest` 命令](/zh-tw/reference/cli/agents/)。未提供明確路徑時,該命令會觀測由受證明約束的啟動器快照識別出的已選候選項;它不授予更新權限,也不證明執行階段選擇。 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md index 4728a5f131b..5ab8a27cdef 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md @@ -223,7 +223,7 @@ ocx system settings --stream-mode eager-relay ocx system codex-cli-update check --json ``` -`check` 不查詢註冊表,也不執行 Codex。在 POSIX 上,當已發布啟動器擷取的環境候選項與執行階段解析器選擇的正規路徑相同時,`selectionAttested` 可以為 `true`;若關聯的 npm-global 套件資訊清單證明了所有權,`managed` 可以為 `true`。持久化候選項仍不獲認證。Windows 不讀取候選項或設定檔,而是延後檢查。直接透過 Bun 或原始碼執行時沒有啟動器證明。`plan` 使用隔離的暫存 npm 設定、快取和日誌查詢官方註冊表,並拒絕活動工作階段或不完整的程序觀測。它不安裝軟體,也不向程序傳送訊號。請參閱[完整命令與拒絕原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 +`check` 不查詢註冊表,也不執行 Codex。在 POSIX 上,當已發布啟動器擷取的環境候選項與執行階段解析器選擇的正規路徑相同時,`selectionAttested` 可以為 `true`;若關聯的 npm-global 套件資訊清單證明了所有權,`managed` 可以為 `true`。持久化候選項仍不獲認證。Windows x64 也支援經證明繫結的絕對路徑標準 npm `codex.cmd`,以及前綴本機 Node/npm 和已驗證的前綴內平台支援元件,並使用拒絕重新剖析的持有控制代碼。其他 Windows 配置仍然延後或不受管理。直接透過 Bun 或原始碼執行時沒有啟動器證明。`plan` 使用隔離的暫存 npm 設定、快取和日誌查詢官方註冊表,並拒絕活動工作階段或不完整的程序觀測。它不安裝軟體,也不向程序傳送訊號。受支援的 Windows 安裝可產生 `applicable: false` 且 `refusal: windows_apply_disabled` 的確定性唯讀 plan;僅 PATH 選取、獨立前綴的 Node 和 OpenCodex 自動啟動包裝器仍然延後。請參閱[完整命令與拒絕原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。 在 Windows 上,如果擷取到 `CODEX_CLI_PATH=codex` 這類單純命令名稱、遠端路徑或裝置路徑,則回報 `candidate_path_unavailable`。這些情況已有擷取的候選項,但其路徑不適用於此檢查。 From fd24f1658167d09fedb3afc4196b411c61c44827 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Sat, 24 Oct 2026 13:55:52 +0900 Subject: [PATCH 4/5] fix(codex): report windows_apply_disabled for direct Windows apply refusal A direct applyCodexCliUpdate on Windows misreported windows_inspection_deferred, the check-time code for layouts that cannot be inspected, while the plan path refuses with windows_apply_disabled. Align the guard and add the code to CODEX_CLI_JOB_REASONS so a persisted refusal is not rewritten to job_result_unknown. CodeRabbit review on #7596 comment 4365760310. --- src/codex/cli-update-apply.ts | 2 +- src/codex/cli-update-job-execution.ts | 2 +- tests/cli/cli-codex-cli-update.test.ts | 5 +++++ tests/codex-integration/codex-cli-update-artifacts.test.ts | 2 +- tests/codex-integration/codex-cli-windows-plan.test.ts | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/codex/cli-update-apply.ts b/src/codex/cli-update-apply.ts index 24a6e5c71d3..df44ed80836 100644 --- a/src/codex/cli-update-apply.ts +++ b/src/codex/cli-update-apply.ts @@ -147,7 +147,7 @@ export async function applyCodexCliUpdate(planId: string, deps: CodexCliApplyDep output = { outcome, reason, planId, installedVersion, targetVersion, installerExitCode: exitCode, leaseRetained: keepLease, verificationPid, cleanupErrors: [] }; try { if (!/^[0-9a-f]{32}$/.test(planId)) return result("refused", "invalid_plan_id"); - if ((deps.platform ?? process.platform) === "win32") return result("refused", "windows_inspection_deferred"); + if ((deps.platform ?? process.platform) === "win32") return result("refused", "windows_apply_disabled"); if (deps.signal?.aborted) return result("refused", "cancelled"); const inspect = deps.inspect ?? inspectCodexCliInstall; const first = await inspect(freshInspection(deps)); diff --git a/src/codex/cli-update-job-execution.ts b/src/codex/cli-update-job-execution.ts index e3ae63ed765..7d3eac94d08 100644 --- a/src/codex/cli-update-job-execution.ts +++ b/src/codex/cli-update-job-execution.ts @@ -1,7 +1,7 @@ import type { CodexCliApplyResult } from "./cli-update-apply"; import type { CodexCliUpdateJobDto } from "./cli-update-job"; -export const CODEX_CLI_JOB_REASONS: ReadonlySet = new Set(["invalid_plan_id", "windows_inspection_deferred", "cancelled", "mutation_evidence_unverified", +export const CODEX_CLI_JOB_REASONS: ReadonlySet = new Set(["invalid_plan_id", "windows_inspection_deferred", "windows_apply_disabled", "cancelled", "mutation_evidence_unverified", "installation_prefix_changed", "installation_prefix_unsupported", "installation_lease_unavailable", "installation_busy", "plan_refused", "plan_changed", "target_artifacts_unverified", "installation_changed", "npm_unavailable", "blocked_process_state_unknown", "blocked_active_session", "installer_state_unknown", "version_process_state_unknown", "version_readback_unverified", "verified_target", diff --git a/tests/cli/cli-codex-cli-update.test.ts b/tests/cli/cli-codex-cli-update.test.ts index 0156df8cef5..6268f23f1d6 100644 --- a/tests/cli/cli-codex-cli-update.test.ts +++ b/tests/cli/cli-codex-cli-update.test.ts @@ -297,6 +297,11 @@ describe("Codex CLI job persistence failures", () => { expect(safe).toMatchObject({ outcome: "ambiguous", reason: "job_result_unknown", leaseRetained: true, cleanupErrors: [] }); expect(JSON.stringify(safe)).not.toContain("private/"); expect(normalizeCodexCliJobResult(result)).toEqual(result); + // The Windows refusal is a first-class job reason: it survives normalization + // instead of collapsing into job_result_unknown. + expect(normalizeCodexCliJobResult({ ...result, outcome: "refused", + reason: "windows_apply_disabled", leaseRetained: false })) + .toMatchObject({ outcome: "refused", reason: "windows_apply_disabled" }); }); test("a missing generation returns null without running the engine", async () => { diff --git a/tests/codex-integration/codex-cli-update-artifacts.test.ts b/tests/codex-integration/codex-cli-update-artifacts.test.ts index 6e1e761b419..3afc95cd9d6 100644 --- a/tests/codex-integration/codex-cli-update-artifacts.test.ts +++ b/tests/codex-integration/codex-cli-update-artifacts.test.ts @@ -303,6 +303,6 @@ describe("Codex CLI apply consumer", () => { test("Windows apply refuses before inspecting or installing", async () => { let calls = 0; const result = await applyCodexCliUpdate("a".repeat(32), { platform: "win32", inspect: async () => { calls++; throw new Error("not reached"); } }); - expect(result.reason).toBe("windows_inspection_deferred"); expect(calls).toBe(0); + expect(result.reason).toBe("windows_apply_disabled"); expect(calls).toBe(0); }); }); diff --git a/tests/codex-integration/codex-cli-windows-plan.test.ts b/tests/codex-integration/codex-cli-windows-plan.test.ts index eac0fb43dcc..9636ab66509 100644 --- a/tests/codex-integration/codex-cli-windows-plan.test.ts +++ b/tests/codex-integration/codex-cli-windows-plan.test.ts @@ -132,7 +132,7 @@ native("a Windows plan id with applicable=false cannot enter a management apply const apply = await applyCodexCliUpdate(plan.planId!, { platform: "win32", inspectionDeps: f.deps, acquireLease: () => { leaseAttempts += 1; return null; }, install: async () => { installAttempts += 1; return { terminal: true, exitCode: 0 }; } }); - expect(apply).toMatchObject({ outcome: "refused", reason: "windows_inspection_deferred" }); + expect(apply).toMatchObject({ outcome: "refused", reason: "windows_apply_disabled" }); expect(leaseAttempts).toBe(0); expect(installAttempts).toBe(0); } finally { From 8caa5288dfff88b8a6f4bba2ecce9e017d06941e Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Sat, 24 Oct 2026 14:14:59 +0900 Subject: [PATCH 5/5] test(codex): hold Windows toolchain through async metadata resolution --- .../codex-cli-windows-plan.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/codex-integration/codex-cli-windows-plan.test.ts b/tests/codex-integration/codex-cli-windows-plan.test.ts index 9636ab66509..0b0f59dba75 100644 --- a/tests/codex-integration/codex-cli-windows-plan.test.ts +++ b/tests/codex-integration/codex-cli-windows-plan.test.ts @@ -6,6 +6,7 @@ import { dirname, join } from "node:path"; import { inspectCodexCliInstall, MAX_NATIVE_BACKING_BYTES } from "../../src/codex/cli-install-provenance"; import { applyCodexCliUpdate } from "../../src/codex/cli-update-apply"; import { createCodexCliUpdatePlan } from "../../src/codex/cli-update-plan"; +import { withWindowsCodexCliPlanToolchain } from "../../src/codex/cli-windows-plan-provenance"; import { handleCodexCliUpdateRoutes } from "../../src/server/management/codex-cli-update-routes"; import type { ManagementContext } from "../../src/server/management/context"; import { setWindowsInstallationFilesOpenedForTests, setWindowsInstallationFilesReadForTests } from "../../src/codex/windows-installation-files"; @@ -192,6 +193,41 @@ native("npm toolchain drift after inspection refuses metadata without executing expect(queries).toBe(0); expect(plan.refusal).toBe("target_unresolved"); }); +native("the observed toolchain denies replacement until an asynchronous resolver settles", async () => { + const f = fixture(); const report = await inspectCodexCliInstall(f.deps); + expect(report.managed).toBe(true); + const node = join(f.toolchainPrefix, "node.exe"); + const npmCli = join(f.npmRoot, "bin", "npm-cli.js"); + const moved = f.prefix + "-moved"; + let enter!: () => void, release!: () => void; + const entered = new Promise(resolve => { enter = resolve; }); + const gate = new Promise(resolve => { release = resolve; }); + let settled = false; + const held = withWindowsCodexCliPlanToolchain(report, async () => { + enter(); await gate; return "resolved"; + }).then(value => { settled = true; return value; }); + let result: string | null; + try { + await Promise.race([entered, held.then(() => { throw new Error("toolchain resolver was not entered"); })]); + // A sync finalize returning an unawaited Promise can close its handles in a + // microtask; cross an event-loop turn before checking the still-pending gate. + await new Promise(resolve => { setImmediate(resolve); }); + expect(settled).toBe(false); + for (const path of [node, npmCli]) expect(() => writeFileSync(path, "replacement")).toThrow(); + expect(() => renameSync(f.prefix, moved)).toThrow(); + } finally { + release(); result = await held; + } + expect(result).toBe("resolved"); + expect(settled).toBe(true); + for (const path of [node, npmCli]) { + writeFileSync(path, "replacement"); + expect(readFileSync(path, "utf8")).toBe("replacement"); + } + renameSync(f.prefix, moved); + expect(readFileSync(join(moved, "node_modules", "npm", "bin", "npm-cli.js"), "utf8")).toBe("replacement"); +}); + native("a separate PATH Node is refused without proof of the target cmd-shim's search result", async () => { const f = fixture(true); expect((await inspectCodexCliInstall({ ...f.deps, selectionAttested: false })).managed).toBe(false);