@bitkyc08/opencodex 2.37.0 → 2.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/bin/ocx.mjs +69 -10
  2. package/gui/dist/assets/{index-CowztZdo.js → index-C14iCj_Q.js} +13 -13
  3. package/gui/dist/assets/index-D7PIz7_g.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/aside.svg +3 -0
  6. package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
  7. package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
  8. package/gui/dist/provider-icons/openclaw.svg +54 -0
  9. package/gui/dist/provider-icons/prime-agent.svg +21 -0
  10. package/gui/dist/provider-icons/zcode.svg +219 -0
  11. package/package.json +1 -1
  12. package/src/adapters/cursor/protobuf-request.ts +4 -1
  13. package/src/adapters/cursor/tool-definitions.ts +36 -4
  14. package/src/cli/capabilities.ts +14 -0
  15. package/src/cli/codex-cli-update.ts +96 -0
  16. package/src/cli/codex-shim-autorestore.ts +3 -0
  17. package/src/cli/export-command.ts +18 -17
  18. package/src/cli/help.ts +2 -2
  19. package/src/cli/index.ts +3 -2
  20. package/src/cli/launcher-context.ts +53 -2
  21. package/src/cli/opencode.ts +126 -33
  22. package/src/cli/registry.ts +16 -10
  23. package/src/cli/system-command.ts +6 -1
  24. package/src/clients/config-export.ts +293 -28
  25. package/src/codex/account-store.ts +10 -4
  26. package/src/codex/autostart-health.ts +3 -3
  27. package/src/codex/catalog/provider-fetch.ts +20 -1
  28. package/src/codex/catalog/sync.ts +4 -3
  29. package/src/codex/cli-install-provenance.ts +795 -0
  30. package/src/codex/convergence.ts +4 -3
  31. package/src/codex/credential-mutation-epoch.ts +11 -0
  32. package/src/codex/main-account.ts +2 -0
  33. package/src/codex/model-entitlements.ts +430 -27
  34. package/src/codex/native-profile-manager.ts +4 -0
  35. package/src/codex/reset-credit-operation-ledger.ts +1411 -0
  36. package/src/codex/reset-credit-recovery.ts +20 -2
  37. package/src/codex/shim.ts +204 -18
  38. package/src/codex/user-identity.ts +2 -1
  39. package/src/config/paths.ts +18 -3
  40. package/src/config.ts +23 -0
  41. package/src/generated/compatibility-version.json +81 -45
  42. package/src/integrations/registry.ts +112 -0
  43. package/src/integrations/state.ts +67 -5
  44. package/src/integrations/writer.ts +25 -9
  45. package/src/lib/bounded-subprocess.ts +36 -0
  46. package/src/lib/strict-semver.ts +47 -0
  47. package/src/lib/windows-elevation.ts +32 -1
  48. package/src/lib/windows-secret-acl.ts +47 -25
  49. package/src/lib/windows-service-mutation-lock.ts +133 -0
  50. package/src/lib/windows-user-principal.ts +15 -17
  51. package/src/responses/spill-store.ts +334 -29
  52. package/src/responses/state.ts +488 -7
  53. package/src/server/index.ts +4 -3
  54. package/src/server/lifecycle.ts +5 -1
  55. package/src/server/management/model-rows.ts +11 -2
  56. package/src/server/management/provider-routes.ts +4 -0
  57. package/src/server/management/system-restart.ts +5 -5
  58. package/src/server/management-api.ts +7 -2
  59. package/src/server/startup-action-control.ts +3 -2
  60. package/src/service.ts +594 -33
  61. package/src/sidecar/candidates.ts +1 -1
  62. package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
  63. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  64. package/src/update/index.ts +3 -2
  65. package/src/update/job.ts +10 -11
  66. package/gui/dist/assets/index-jqE_VOKI.css +0 -1
@@ -0,0 +1,795 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants as fsConstants,
5
+ existsSync,
6
+ fstatSync,
7
+ lstatSync,
8
+ openSync,
9
+ readSync,
10
+ realpathSync,
11
+ statSync,
12
+ } from "node:fs";
13
+ import { posix, win32 } from "node:path";
14
+ import { getConfigDir } from "../config";
15
+ import { parseStrictSemver } from "../lib/strict-semver";
16
+ import { CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS } from "../update/codex-cli-update-launch-policy.mjs";
17
+ import { isSpawnableCodexCandidate } from "./exec-invocation";
18
+ import {
19
+ codexRuntimeStatePath,
20
+ parsePersistedCodexRuntime,
21
+ } from "./runtime";
22
+ import {
23
+ inspectCodexShimBackingForCommand,
24
+ isLocalAbsoluteInspectionPath,
25
+ isVersionManagerOwnedCodexPath,
26
+ type CodexShimBackingForCommand,
27
+ } from "./shim";
28
+
29
+ const CODEX_PACKAGE = "@openai/codex";
30
+ const MAX_MANIFEST_BYTES = 256 * 1024;
31
+ const MAX_RUNTIME_STATE_BYTES = 256 * 1024;
32
+ const MAX_MANIFEST_ANCESTORS = 12;
33
+
34
+ export type CodexCliInstallKind =
35
+ | "npm-global"
36
+ | "app-bundle"
37
+ | "version-manager"
38
+ | "standalone-unverified"
39
+ | "unknown";
40
+
41
+ export type CodexCliInstallReason =
42
+ | "candidate_unavailable"
43
+ | "candidate_path_unavailable"
44
+ | "candidate_path_unsafe"
45
+ | "windows_inspection_deferred"
46
+ | "shim_state_unknown"
47
+ | "shim_update_deferred"
48
+ | "app_bundle"
49
+ | "version_manager_owned"
50
+ | "npm_global_unverified"
51
+ | "selection_unattested"
52
+ | "version_mismatch"
53
+ | "unverified_standalone"
54
+ | "inspection_failed";
55
+
56
+ export type CodexCliCandidateSource = "environment" | "persisted";
57
+ export type CodexCliInstallEvidence =
58
+ | "canonical_path"
59
+ | "app_bundle_path"
60
+ | "version_manager_path"
61
+ | "package_manifest"
62
+ | "package_manifest_digest"
63
+ | "shim_backing"
64
+ | "global_npm_layout";
65
+
66
+ export interface ReadOnlyCodexRuntimeCandidate {
67
+ readonly command: string;
68
+ readonly version: string | null;
69
+ readonly evidence: CodexCliCandidateSource;
70
+ }
71
+
72
+ export interface CodexCliInstallReport {
73
+ readonly schemaVersion: 1;
74
+ readonly candidateAvailable: boolean;
75
+ readonly candidateVersion: string | null;
76
+ readonly candidateSource: CodexCliCandidateSource | null;
77
+ readonly selectionAttested: boolean;
78
+ readonly versionEvidence: Readonly<{
79
+ kind: "package-manifest" | "advisory-runtime" | "unavailable";
80
+ }>;
81
+ readonly provenance: CodexCliInstallKind;
82
+ readonly managed: boolean;
83
+ readonly reason: CodexCliInstallReason;
84
+ readonly location: string | null;
85
+ readonly packageVersion: string | null;
86
+ readonly shim: Readonly<{
87
+ status: "not-tracked" | "matched" | "unknown";
88
+ backingKind: "backup" | "real" | null;
89
+ }>;
90
+ readonly evidence: readonly CodexCliInstallEvidence[];
91
+ }
92
+
93
+ export interface CodexCliInstallProvenanceDeps {
94
+ readonly env?: NodeJS.ProcessEnv;
95
+ readonly platform?: NodeJS.Platform;
96
+ readonly configDir?: string;
97
+ readonly exists?: (path: string) => boolean;
98
+ readonly lstat?: typeof lstatSync;
99
+ readonly stat?: typeof statSync;
100
+ readonly readFile?: (path: string) => Buffer;
101
+ readonly boundedFileReadMode?: "native-hardened" | "injected-test";
102
+ readonly realpath?: (path: string) => string;
103
+ readonly inspectShim?: (
104
+ command: string,
105
+ platform: NodeJS.Platform,
106
+ configDir: string,
107
+ ) => CodexShimBackingForCommand;
108
+ }
109
+
110
+ interface PackageManifestEvidence {
111
+ readonly path: string;
112
+ readonly root: string;
113
+ readonly binPath: string;
114
+ readonly version: string;
115
+ readonly digest: string;
116
+ }
117
+
118
+ function sha256(domain: string, value: string | Uint8Array): string {
119
+ return createHash("sha256")
120
+ .update(domain, "utf8")
121
+ .update("\0", "utf8")
122
+ .update(value)
123
+ .digest("hex");
124
+ }
125
+
126
+ function validatedVersion(value: string | null | undefined): string | null {
127
+ if (typeof value !== "string" || value !== value.trim()) return null;
128
+ return parseStrictSemver(value, 96)?.raw ?? null;
129
+ }
130
+
131
+ function publicExecutableLocation(path: string, platform: NodeJS.Platform): string {
132
+ const raw = pathTools(platform).basename(path).toLowerCase();
133
+ const safe = ["codex", "codex.exe", "codex.cmd", "codex.bat", "codex.com", "codex.js"].includes(raw)
134
+ ? raw : "codex";
135
+ return `<path>/${safe}`;
136
+ }
137
+
138
+ function freezeReport(report: CodexCliInstallReport): CodexCliInstallReport {
139
+ Object.freeze(report.shim);
140
+ Object.freeze(report.evidence);
141
+ return Object.freeze(report);
142
+ }
143
+
144
+ function unknownReport(
145
+ reason: CodexCliInstallReason,
146
+ candidate?: ReadOnlyCodexRuntimeCandidate,
147
+ extra: Partial<Pick<CodexCliInstallReport, "location">> = {},
148
+ ): CodexCliInstallReport {
149
+ return freezeReport({
150
+ schemaVersion: 1,
151
+ candidateAvailable: Boolean(candidate),
152
+ candidateVersion: candidate?.version ?? null,
153
+ candidateSource: candidate?.evidence ?? null,
154
+ selectionAttested: false,
155
+ versionEvidence: Object.freeze({
156
+ kind: candidate?.version ? "advisory-runtime" as const : "unavailable" as const,
157
+ }),
158
+ provenance: "unknown",
159
+ managed: false,
160
+ reason,
161
+ location: extra.location ?? null,
162
+ packageVersion: null,
163
+ shim: Object.freeze({ status: "not-tracked", backingKind: null }),
164
+ evidence: Object.freeze([]),
165
+ });
166
+ }
167
+
168
+ function unknownWindowsReport(
169
+ reason: CodexCliInstallReason,
170
+ candidate?: ReadOnlyCodexRuntimeCandidate,
171
+ extra: Partial<Pick<CodexCliInstallReport, "location">> = {},
172
+ ): CodexCliInstallReport {
173
+ return freezeReport({
174
+ ...unknownReport(reason, candidate, extra),
175
+ shim: Object.freeze({ status: "unknown", backingKind: null }),
176
+ });
177
+ }
178
+
179
+ function readPersistedCandidate(
180
+ deps: CodexCliInstallProvenanceDeps,
181
+ ): ReadOnlyCodexRuntimeCandidate | null {
182
+ // A pathname-only Windows read cannot prove that a writable ancestor stayed
183
+ // local and non-reparse between validation and open. PR1 therefore accepts
184
+ // only the proof-captured environment candidate on Windows; persisted-state
185
+ // inspection requires the later handle-bound Windows provenance layer.
186
+ if ((deps.platform ?? process.platform) === "win32") return null;
187
+ const configDir = deps.configDir ?? getConfigDir();
188
+ if (!isSafeLocalInspectionPath(configDir, deps)) return null;
189
+ try {
190
+ const statePath = codexRuntimeStatePath(configDir);
191
+ const bytes = readBoundedFile(statePath, MAX_RUNTIME_STATE_BYTES, deps);
192
+ if (!bytes) return null;
193
+ const parsed = parsePersistedCodexRuntime(
194
+ bytes,
195
+ );
196
+ if (!parsed) return null;
197
+ return {
198
+ command: parsed.command,
199
+ version: validatedVersion(parsed.selectedVersion),
200
+ evidence: "persisted",
201
+ };
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Observe configured candidate evidence without launching Codex, creating a
209
+ * probe home, or persisting a replacement selection.
210
+ */
211
+ export function observeCodexRuntimeCandidateReadOnly(
212
+ deps: CodexCliInstallProvenanceDeps = {},
213
+ ): ReadOnlyCodexRuntimeCandidate | null {
214
+ const env = deps.env ?? process.env;
215
+ const configured = env.CODEX_CLI_PATH?.trim();
216
+ if (configured) {
217
+ return {
218
+ command: configured,
219
+ version: null,
220
+ evidence: "environment",
221
+ };
222
+ }
223
+ return readPersistedCandidate(deps);
224
+ }
225
+
226
+ function pathTools(platform: NodeJS.Platform): typeof posix | typeof win32 {
227
+ return platform === "win32" ? win32 : posix;
228
+ }
229
+
230
+ function isWindowsPlatform(platform: NodeJS.Platform): boolean {
231
+ return platform === "win32";
232
+ }
233
+
234
+ function isSafeLocalInspectionPath(
235
+ path: string,
236
+ deps: CodexCliInstallProvenanceDeps,
237
+ ): boolean {
238
+ const platform = deps.platform ?? process.platform;
239
+ return isLocalAbsoluteInspectionPath(path, platform);
240
+ }
241
+
242
+ function caseInsensitiveEnv(env: NodeJS.ProcessEnv, name: string): string | undefined {
243
+ const entry = Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase());
244
+ return entry?.[1];
245
+ }
246
+
247
+ function resolveCandidateCommandPath(
248
+ command: string,
249
+ deps: CodexCliInstallProvenanceDeps,
250
+ ): string | null {
251
+ const platform = deps.platform ?? process.platform;
252
+ // Windows inspection returns before this resolver until the next slice can
253
+ // bind command resolution and wrapper reads to stable filesystem handles.
254
+ if (isWindowsPlatform(platform)) return null;
255
+ const exists = deps.exists ?? existsSync;
256
+ const lstat = deps.lstat ?? lstatSync;
257
+ const stat = deps.stat ?? statSync;
258
+ const usable = (path: string): boolean => {
259
+ if (!isSafeLocalInspectionPath(path, deps)) return false;
260
+ try {
261
+ const entry = lstat(path);
262
+ if (!exists(path) || (!entry.isFile() && !entry.isSymbolicLink()) || !isSpawnableCodexCandidate(path, platform)) return false;
263
+ if (platform !== "win32") {
264
+ const target = stat(path);
265
+ if (!target.isFile() || (target.mode & 0o111) === 0) return false;
266
+ }
267
+ return true;
268
+ } catch {
269
+ return false;
270
+ }
271
+ };
272
+ const env = deps.env ?? process.env;
273
+ const tools = pathTools(platform);
274
+ const explicit = tools.isAbsolute(command) || command.includes("/") || command.includes("\\");
275
+ if (explicit) return usable(command) ? command : null;
276
+ const pathValue = env.PATH ?? "";
277
+ const names = [command];
278
+ for (const entry of pathValue.split(tools.delimiter)) {
279
+ // Empty and relative entries name the current working directory. Either can
280
+ // shadow a later absolute hit and therefore makes the candidate path unknown.
281
+ if (!entry || !isSafeLocalInspectionPath(entry, deps)) return null;
282
+ for (const name of names) {
283
+ const candidate = tools.join(entry, name);
284
+ if (usable(candidate)) return candidate;
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+
290
+ function canonicalize(path: string, deps: CodexCliInstallProvenanceDeps): string | null {
291
+ if (!isSafeLocalInspectionPath(path, deps)) return null;
292
+ try {
293
+ const canonical = (deps.realpath ?? realpathSync.native)(path);
294
+ return isSafeLocalInspectionPath(canonical, deps) ? canonical : null;
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+
300
+ function normalizePath(path: string, platform: NodeJS.Platform): string {
301
+ const slashNormalized = platform === "win32" ? path.replace(/\\/g, "/") : path;
302
+ const normalized = platform !== "win32" && /^\/+$/u.test(slashNormalized)
303
+ ? "/"
304
+ : slashNormalized.replace(/\/+$/, "");
305
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
306
+ }
307
+
308
+ function samePath(left: string, right: string, platform: NodeJS.Platform): boolean {
309
+ return normalizePath(left, platform) === normalizePath(right, platform);
310
+ }
311
+
312
+ export function isAppBundledCodexPath(path: string, platform: NodeJS.Platform): boolean {
313
+ const normalized = normalizePath(path, platform);
314
+ if (platform === "win32") {
315
+ return normalized.includes("/windowsapps/")
316
+ || normalized.includes("/microsoft/windowsapps/")
317
+ || normalized.includes("/packages/openai.codex_");
318
+ }
319
+ if (platform === "darwin") return /[.]app\/contents\//i.test(normalized);
320
+ return normalized.startsWith("/snap/") || normalized.includes("/flatpak/app/");
321
+ }
322
+
323
+ /** Updater ownership is intentionally broader than shim-repair refusal. */
324
+ export function isCodexCliUpdateVersionManagerPath(
325
+ path: string,
326
+ platform: NodeJS.Platform = process.platform,
327
+ ): boolean {
328
+ const normalized = (platform === "win32"
329
+ ? win32.normalize(path).replace(/\\/g, "/")
330
+ : posix.normalize(path)).toLowerCase();
331
+ if (isVersionManagerOwnedCodexPath(normalized, platform)) return true;
332
+ return normalized.includes("/.nvm/")
333
+ || normalized.includes("/nvm/versions/")
334
+ || /\/nvm\/v?\d+(?:[.]\d+){1,2}(?:\/|$)/.test(normalized)
335
+ || normalized.includes("/.proto/")
336
+ || normalized.includes("/proto/tools/")
337
+ || normalized.includes("/.nodenv/")
338
+ || normalized.includes("/nodenv/versions/")
339
+ || normalized.includes("/.nvs/")
340
+ || normalized.includes("/nvs/node/")
341
+ || normalized.includes("/.fnm/")
342
+ || normalized.includes("/fnm/node-versions/")
343
+ || normalized.includes("/fnm_multishells/")
344
+ || (platform === "win32" && (
345
+ normalized.includes("/scoop/apps/")
346
+ || normalized.includes("/scoop/shims/")
347
+ ));
348
+ }
349
+
350
+ function configuredVersionManagerRoots(
351
+ env: NodeJS.ProcessEnv,
352
+ platform: NodeJS.Platform,
353
+ deps: CodexCliInstallProvenanceDeps,
354
+ ): readonly string[] {
355
+ const roots: string[] = [];
356
+ for (const name of CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS) {
357
+ const raw = platform === "win32" ? caseInsensitiveEnv(env, name) : env[name];
358
+ if (!raw || !isLocalAbsoluteInspectionPath(raw, platform)) continue;
359
+ // Windows roots are advisory lexical labels only in this first slice. Do
360
+ // not resolve or open them until the handle-bound Windows layer exists.
361
+ if (platform === "win32") {
362
+ roots.push(normalizePath(win32.normalize(raw), platform));
363
+ continue;
364
+ }
365
+ const canonical = isSafeLocalInspectionPath(raw, deps) ? canonicalize(raw, deps) : null;
366
+ if (canonical) roots.push(normalizePath(canonical, platform));
367
+ }
368
+ return Object.freeze([...new Set(roots)]);
369
+ }
370
+
371
+ function isWithinConfiguredVersionManagerRoot(path: string, roots: readonly string[], platform: NodeJS.Platform): boolean {
372
+ const candidate = normalizePath(path, platform);
373
+ return roots.some(root => {
374
+ const filesystemRoot = root === "/" || (platform === "win32" && /^[a-z]:$/i.test(root));
375
+ return candidate === root || (!filesystemRoot && candidate.startsWith(`${root}/`));
376
+ });
377
+ }
378
+
379
+ function readBoundedFile(
380
+ path: string,
381
+ maxBytes: number,
382
+ deps: CodexCliInstallProvenanceDeps,
383
+ ): Buffer | null {
384
+ if (!isSafeLocalInspectionPath(path, deps)) return null;
385
+ // Production inspection accepts only a direct regular file. This prevents a
386
+ // persisted-state or manifest symlink from silently redirecting a nominally
387
+ // local check. Virtual filesystem tests may omit lstat and retain their
388
+ // injected stat/read behavior.
389
+ const inspectLexical = deps.lstat
390
+ ?? (deps.stat === undefined && deps.readFile === undefined ? lstatSync : null);
391
+ let lexicalBefore: ReturnType<typeof lstatSync> | null = null;
392
+ if (inspectLexical) {
393
+ try {
394
+ lexicalBefore = inspectLexical(path);
395
+ if (lexicalBefore.isSymbolicLink() || !lexicalBefore.isFile()) return null;
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+ const useInjectedReader = deps.boundedFileReadMode === "injected-test";
401
+ if (!useInjectedReader) {
402
+ let fd: number | null = null;
403
+ try {
404
+ const flags = process.platform === "win32"
405
+ ? fsConstants.O_RDONLY
406
+ : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
407
+ fd = openSync(path, flags);
408
+ const before = fstatSync(fd);
409
+ if (!before.isFile() || before.size > maxBytes) return null;
410
+ const bytes = Buffer.allocUnsafe(before.size);
411
+ let offset = 0;
412
+ while (offset < bytes.length) {
413
+ const count = readSync(fd, bytes, offset, bytes.length - offset, offset);
414
+ if (count <= 0) return null;
415
+ offset += count;
416
+ }
417
+ const extra = Buffer.allocUnsafe(1);
418
+ if (readSync(fd, extra, 0, 1, offset) !== 0) return null;
419
+ const after = fstatSync(fd);
420
+ const lexicalAfter = inspectLexical ? inspectLexical(path) : null;
421
+ if (lexicalAfter?.isSymbolicLink()) return null;
422
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
423
+ || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
424
+ || (lexicalBefore !== null && (lexicalAfter === null || lexicalAfter.isSymbolicLink()
425
+ || lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
426
+ || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino
427
+ || lexicalAfter.size !== after.size || lexicalAfter.mtimeMs !== after.mtimeMs
428
+ || lexicalAfter.ctimeMs !== after.ctimeMs))) return null;
429
+ return bytes;
430
+ } catch {
431
+ return null;
432
+ } finally {
433
+ if (fd !== null) closeSync(fd);
434
+ }
435
+ }
436
+ if (!deps.stat || !deps.readFile) return null;
437
+ const stat = deps.stat;
438
+ const read = deps.readFile;
439
+ try {
440
+ const before = stat(path);
441
+ if (!before.isFile() || before.size > maxBytes) return null;
442
+ const bytes = read(path);
443
+ const after = stat(path);
444
+ const lexicalAfter = inspectLexical ? inspectLexical(path) : null;
445
+ if (
446
+ bytes.byteLength !== before.size
447
+ || before.dev !== after.dev
448
+ || before.ino !== after.ino
449
+ || before.size !== after.size
450
+ || before.mtimeMs !== after.mtimeMs
451
+ || (lexicalBefore !== null && (lexicalAfter === null || lexicalAfter.isSymbolicLink()
452
+ || lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
453
+ || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino))
454
+ ) return null;
455
+ return bytes;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+
461
+ function manifestCandidates(
462
+ logicalPath: string,
463
+ canonicalPath: string,
464
+ platform: NodeJS.Platform,
465
+ ): string[] {
466
+ const tools = pathTools(platform);
467
+ const candidates: string[] = [];
468
+ let cursor = tools.dirname(canonicalPath);
469
+ for (let depth = 0; depth < MAX_MANIFEST_ANCESTORS; depth += 1) {
470
+ candidates.push(tools.join(cursor, "package.json"));
471
+ const parent = tools.dirname(cursor);
472
+ if (parent === cursor) break;
473
+ cursor = parent;
474
+ }
475
+ const binDir = tools.dirname(logicalPath);
476
+ if (platform === "win32") {
477
+ candidates.push(tools.join(binDir, "node_modules", "@openai", "codex", "package.json"));
478
+ } else {
479
+ const prefix = tools.dirname(binDir);
480
+ candidates.push(tools.join(prefix, "lib", "node_modules", "@openai", "codex", "package.json"));
481
+ candidates.push(tools.join(prefix, "node_modules", "@openai", "codex", "package.json"));
482
+ }
483
+ return [...new Set(candidates)];
484
+ }
485
+
486
+ function manifestBinPath(bin: unknown): string | null {
487
+ const raw = typeof bin === "string"
488
+ ? bin
489
+ : bin && typeof bin === "object" && !Array.isArray(bin)
490
+ ? (bin as Record<string, unknown>).codex
491
+ : null;
492
+ if (typeof raw !== "string") return null;
493
+ const normalized = raw.replace(/\\/g, "/").replace(/^\.\//, "");
494
+ return normalized === "bin/codex.js" ? normalized : null;
495
+ }
496
+
497
+ function findCodexPackageManifest(
498
+ logicalPath: string,
499
+ canonicalPath: string,
500
+ deps: CodexCliInstallProvenanceDeps,
501
+ ): PackageManifestEvidence | null {
502
+ const platform = deps.platform ?? process.platform;
503
+ for (const candidate of manifestCandidates(logicalPath, canonicalPath, platform)) {
504
+ const bytes = readBoundedFile(candidate, MAX_MANIFEST_BYTES, deps);
505
+ if (!bytes) continue;
506
+ try {
507
+ const value = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
508
+ const version = validatedVersion(typeof value.version === "string" ? value.version : null);
509
+ if (value.name !== CODEX_PACKAGE || !version) continue;
510
+ const binPath = manifestBinPath(value.bin);
511
+ if (!binPath) continue;
512
+ const root = canonicalize(pathTools(platform).dirname(candidate), deps);
513
+ const path = canonicalize(candidate, deps);
514
+ if (!root || !path) continue;
515
+ return {
516
+ path,
517
+ root,
518
+ binPath,
519
+ version,
520
+ digest: sha256("codex-cli-package-manifest-v1", bytes),
521
+ };
522
+ } catch {
523
+ continue;
524
+ }
525
+ }
526
+ return null;
527
+ }
528
+
529
+ function launcherIsLinkedToManifest(
530
+ canonicalOwnershipPath: string,
531
+ manifest: PackageManifestEvidence,
532
+ deps: CodexCliInstallProvenanceDeps,
533
+ ): boolean {
534
+ const platform = deps.platform ?? process.platform;
535
+ const tools = pathTools(platform);
536
+ const entrypoint = canonicalize(tools.join(manifest.root, ...manifest.binPath.split("/")), deps);
537
+ if (!entrypoint) return false;
538
+ const normalizedRoot = normalizePath(manifest.root, platform);
539
+ const normalizedEntrypoint = normalizePath(entrypoint, platform);
540
+ if (!normalizedEntrypoint.startsWith(`${normalizedRoot}/`)) return false;
541
+ if (samePath(canonicalOwnershipPath, entrypoint, platform)) return true;
542
+ return false;
543
+ }
544
+
545
+ function isProvenGlobalNpmLayout(
546
+ launcherPath: string,
547
+ packageRoot: string,
548
+ platform: NodeJS.Platform,
549
+ deps: CodexCliInstallProvenanceDeps,
550
+ ): boolean {
551
+ const tools = pathTools(platform);
552
+ const launcherName = tools.basename(launcherPath).toLowerCase();
553
+ if (launcherName !== (platform === "win32" ? "codex.cmd" : "codex")) return false;
554
+ const root = normalizePath(packageRoot, platform);
555
+ // Keep the POSIX launcher itself lexical because npm commonly installs it as
556
+ // a symlink into the package. Canonicalize only its parent so a symlinked or
557
+ // case-aliased npm prefix is compared against the canonical manifest root.
558
+ const launcherParent = tools.dirname(launcherPath);
559
+ const canonicalLauncherParent = platform === "win32"
560
+ ? launcherParent
561
+ : canonicalize(launcherParent, deps);
562
+ if (!canonicalLauncherParent) return false;
563
+ const launcherDir = normalizePath(canonicalLauncherParent, platform);
564
+ const suffix = "/node_modules/@openai/codex";
565
+ if (!root.endsWith(suffix)) return false;
566
+ const beforeNodeModules = root.slice(0, -suffix.length);
567
+ if (platform === "win32") return launcherDir === beforeNodeModules;
568
+ if (!beforeNodeModules.endsWith("/lib")) return false;
569
+ const prefix = beforeNodeModules.slice(0, -"/lib".length);
570
+ return launcherDir === `${prefix}/bin`;
571
+ }
572
+
573
+ function shimReport(shim: CodexShimBackingForCommand): CodexCliInstallReport["shim"] {
574
+ if (shim.status === "matched") {
575
+ return Object.freeze({ status: "matched" as const, backingKind: shim.backingKind });
576
+ }
577
+ if (shim.status === "unknown") {
578
+ return Object.freeze({ status: "unknown" as const, backingKind: null });
579
+ }
580
+ return Object.freeze({ status: "not-tracked" as const, backingKind: null });
581
+ }
582
+
583
+ /** Inspect ownership of one configured Codex CLI candidate, without mutation. */
584
+ export async function inspectCodexCliInstall(
585
+ deps: CodexCliInstallProvenanceDeps = {},
586
+ ): Promise<CodexCliInstallReport> {
587
+ const platform = deps.platform ?? process.platform;
588
+ const candidate = observeCodexRuntimeCandidateReadOnly(deps);
589
+ if (!candidate) {
590
+ return isWindowsPlatform(platform)
591
+ ? unknownWindowsReport("candidate_unavailable")
592
+ : unknownReport("candidate_unavailable");
593
+ }
594
+
595
+ const env = deps.env ?? process.env;
596
+ if (platform === "win32") {
597
+ // This first slice never opens a candidate-controlled Windows pathname.
598
+ // A boolean precheck followed by lstat/realpath/open is raceable when an
599
+ // ancestor can be replaced with a remote reparse point. Preserve only
600
+ // lexical, report-only classifications until a handle-bound inspector is
601
+ // introduced; every result remains unattested and unmanaged.
602
+ const lexicalCandidatePath = win32.isAbsolute(candidate.command)
603
+ && isLocalAbsoluteInspectionPath(candidate.command, platform)
604
+ ? win32.normalize(candidate.command)
605
+ : null;
606
+ if (!lexicalCandidatePath) {
607
+ return unknownWindowsReport("candidate_path_unavailable", candidate);
608
+ }
609
+ const managerRoots = configuredVersionManagerRoots(env, platform, deps);
610
+ const appBundle = isAppBundledCodexPath(lexicalCandidatePath, platform);
611
+ const versionManager = isCodexCliUpdateVersionManagerPath(lexicalCandidatePath, platform)
612
+ || isWithinConfiguredVersionManagerRoot(lexicalCandidatePath, managerRoots, platform);
613
+ if (appBundle || versionManager) {
614
+ return freezeReport({
615
+ schemaVersion: 1,
616
+ candidateAvailable: true,
617
+ candidateVersion: candidate.version,
618
+ candidateSource: candidate.evidence,
619
+ selectionAttested: false,
620
+ versionEvidence: Object.freeze({
621
+ kind: candidate.version ? "advisory-runtime" as const : "unavailable" as const,
622
+ }),
623
+ provenance: appBundle ? "app-bundle" : "version-manager",
624
+ managed: false,
625
+ reason: appBundle ? "app_bundle" : "version_manager_owned",
626
+ location: publicExecutableLocation(lexicalCandidatePath, platform),
627
+ packageVersion: null,
628
+ shim: Object.freeze({ status: "unknown", backingKind: null }),
629
+ evidence: Object.freeze([appBundle ? "app_bundle_path" : "version_manager_path"]),
630
+ });
631
+ }
632
+ return unknownWindowsReport("windows_inspection_deferred", candidate, {
633
+ location: publicExecutableLocation(lexicalCandidatePath, platform),
634
+ });
635
+ }
636
+ const configDir = deps.configDir ?? getConfigDir();
637
+ if (!isSafeLocalInspectionPath(configDir, deps)) {
638
+ return unknownReport("shim_state_unknown", candidate);
639
+ }
640
+ const candidatePath = resolveCandidateCommandPath(candidate.command, deps);
641
+ if (!candidatePath) {
642
+ return unknownReport("candidate_path_unavailable", candidate);
643
+ }
644
+ const canonicalCandidatePath = canonicalize(candidatePath, deps);
645
+ if (!canonicalCandidatePath) {
646
+ return unknownReport("candidate_path_unsafe", candidate);
647
+ }
648
+ const inspectShim = deps.inspectShim ?? inspectCodexShimBackingForCommand;
649
+ const shim = inspectShim(candidatePath, platform, configDir);
650
+ if (shim.status === "unknown") {
651
+ return freezeReport({
652
+ ...unknownReport("shim_state_unknown", candidate, {
653
+ location: publicExecutableLocation(canonicalCandidatePath, platform),
654
+ }),
655
+ shim: shimReport(shim),
656
+ });
657
+ }
658
+ if (shim.status === "matched") {
659
+ return freezeReport({
660
+ ...unknownReport("shim_update_deferred", candidate, {
661
+ location: publicExecutableLocation(canonicalCandidatePath, platform),
662
+ }),
663
+ provenance: "standalone-unverified",
664
+ shim: shimReport(shim),
665
+ evidence: Object.freeze(["canonical_path", "shim_backing"] as const),
666
+ });
667
+ }
668
+
669
+ const ownershipPath = candidatePath;
670
+ const canonicalOwnershipPath = canonicalize(ownershipPath, deps);
671
+ if (!canonicalOwnershipPath) {
672
+ return unknownReport("candidate_path_unsafe", candidate);
673
+ }
674
+ const location = publicExecutableLocation(canonicalCandidatePath, platform);
675
+ const canonicalPathSet = [canonicalCandidatePath, canonicalOwnershipPath];
676
+ if (canonicalPathSet.some(path => isAppBundledCodexPath(path, platform))) {
677
+ return freezeReport({
678
+ schemaVersion: 1,
679
+ candidateAvailable: true,
680
+ candidateVersion: candidate.version,
681
+ candidateSource: candidate.evidence,
682
+ selectionAttested: false,
683
+ versionEvidence: Object.freeze({
684
+ kind: candidate.version ? "advisory-runtime" as const : "unavailable" as const,
685
+ }),
686
+ provenance: "app-bundle",
687
+ managed: false,
688
+ reason: "app_bundle",
689
+ location,
690
+ packageVersion: null,
691
+ shim: shimReport(shim),
692
+ evidence: Object.freeze(["canonical_path", "app_bundle_path"]),
693
+ });
694
+ }
695
+ const configuredManagerRoots = configuredVersionManagerRoots(env, platform, deps);
696
+ if (canonicalPathSet.some(path => isCodexCliUpdateVersionManagerPath(path, platform)
697
+ || isWithinConfiguredVersionManagerRoot(path, configuredManagerRoots, platform))) {
698
+ return freezeReport({
699
+ schemaVersion: 1,
700
+ candidateAvailable: true,
701
+ candidateVersion: candidate.version,
702
+ candidateSource: candidate.evidence,
703
+ selectionAttested: false,
704
+ versionEvidence: Object.freeze({
705
+ kind: candidate.version ? "advisory-runtime" as const : "unavailable" as const,
706
+ }),
707
+ provenance: "version-manager",
708
+ managed: false,
709
+ reason: "version_manager_owned",
710
+ location,
711
+ packageVersion: null,
712
+ shim: shimReport(shim),
713
+ evidence: Object.freeze(["canonical_path", "version_manager_path"]),
714
+ });
715
+ }
716
+ if (/codex[.]opencodex-real(?:[.](?:cmd|bat|exe))?$/i.test(pathTools(platform).basename(candidatePath))) {
717
+ return freezeReport({
718
+ ...unknownReport("shim_state_unknown", candidate, { location }),
719
+ shim: shimReport(shim),
720
+ });
721
+ }
722
+
723
+ const manifest = findCodexPackageManifest(ownershipPath, canonicalOwnershipPath, deps);
724
+ if (manifest) {
725
+ // POSIX npm launchers are commonly symlinks into the package, so their
726
+ // lexical prefix is the ownership evidence instead.
727
+ const global = isProvenGlobalNpmLayout(
728
+ ownershipPath,
729
+ manifest.root,
730
+ platform,
731
+ deps,
732
+ );
733
+ const linked = launcherIsLinkedToManifest(
734
+ canonicalOwnershipPath,
735
+ manifest,
736
+ deps,
737
+ );
738
+ const manifestOwned = global && linked;
739
+ const versionMatches = candidate.version === null || candidate.version === manifest.version;
740
+ const reason: CodexCliInstallReason = !global || !linked
741
+ ? "npm_global_unverified"
742
+ : !versionMatches ? "version_mismatch" : "selection_unattested";
743
+ return freezeReport({
744
+ schemaVersion: 1,
745
+ candidateAvailable: true,
746
+ candidateVersion: candidate.version,
747
+ candidateSource: candidate.evidence,
748
+ selectionAttested: false,
749
+ versionEvidence: Object.freeze({
750
+ kind: manifestOwned && candidate.version !== null && versionMatches
751
+ ? "package-manifest" as const
752
+ : candidate.version !== null ? "advisory-runtime" as const : "unavailable" as const,
753
+ }),
754
+ provenance: manifestOwned ? "npm-global" : "standalone-unverified",
755
+ managed: false,
756
+ reason,
757
+ location,
758
+ packageVersion: manifest.version,
759
+ shim: shimReport(shim),
760
+ evidence: Object.freeze<CodexCliInstallEvidence[]>([
761
+ "canonical_path", "package_manifest", "package_manifest_digest",
762
+ ...(manifestOwned ? ["global_npm_layout" as const] : []),
763
+ ]),
764
+ });
765
+ }
766
+
767
+ const stat = deps.stat ?? statSync;
768
+ if (!isSafeLocalInspectionPath(canonicalOwnershipPath, deps)) {
769
+ return unknownReport("candidate_path_unsafe", candidate);
770
+ }
771
+ try {
772
+ if (!stat(canonicalOwnershipPath).isFile()) {
773
+ return unknownReport("candidate_path_unsafe", candidate);
774
+ }
775
+ } catch {
776
+ return unknownReport("inspection_failed", candidate);
777
+ }
778
+ return freezeReport({
779
+ schemaVersion: 1,
780
+ candidateAvailable: true,
781
+ candidateVersion: candidate.version,
782
+ candidateSource: candidate.evidence,
783
+ selectionAttested: false,
784
+ versionEvidence: Object.freeze({
785
+ kind: candidate.version ? "advisory-runtime" as const : "unavailable" as const,
786
+ }),
787
+ provenance: "standalone-unverified",
788
+ managed: false,
789
+ reason: "unverified_standalone",
790
+ location,
791
+ packageVersion: null,
792
+ shim: shimReport(shim),
793
+ evidence: Object.freeze(["canonical_path"]),
794
+ });
795
+ }