@bitkyc08/opencodex 2.36.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.
- package/bin/ocx.mjs +69 -10
- package/gui/dist/assets/index-C14iCj_Q.js +112 -0
- package/gui/dist/assets/index-D7PIz7_g.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/package.json +1 -1
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/adapters/kiro-constants.ts +36 -2
- package/src/adapters/kiro.ts +13 -2
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/claude.ts +12 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +3 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +10 -4
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +20 -1
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +489 -30
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/shim.ts +204 -18
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +84 -48
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +25 -9
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/responses/spill-store.ts +334 -29
- package/src/responses/state.ts +488 -7
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +7 -2
- package/src/server/startup-action-control.ts +3 -2
- package/src/service.ts +594 -33
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +3 -2
- package/src/update/job.ts +10 -11
- package/gui/dist/assets/index-Cy7Z_pl0.css +0 -1
- package/gui/dist/assets/index-DO8liQVL.js +0 -112
|
@@ -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
|
+
}
|