@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
linkSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readSync,
|
|
8
|
+
readlinkSync,
|
|
9
|
+
statSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { posix, win32 } from "node:path";
|
|
14
|
+
import { SHIM_MARKER, UNIX_SHIM_REVISION_MARKER } from "./shim-templates";
|
|
15
|
+
import type { ShimFileState } from "./shim-state-file";
|
|
16
|
+
|
|
17
|
+
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
18
|
+
|
|
19
|
+
interface ShimPathFingerprint {
|
|
20
|
+
dev: number;
|
|
21
|
+
ino: number;
|
|
22
|
+
kind: "file" | "symlink";
|
|
23
|
+
mode: number;
|
|
24
|
+
size: number;
|
|
25
|
+
mtimeMs: number;
|
|
26
|
+
ctimeMs: number;
|
|
27
|
+
target?: Omit<ShimPathFingerprint, "target">;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StableShimPathProbe {
|
|
31
|
+
fingerprint: ShimPathFingerprint;
|
|
32
|
+
prefix: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readShimProbePrefix(path: string): string {
|
|
36
|
+
const fd = openSync(path, "r");
|
|
37
|
+
try {
|
|
38
|
+
const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
|
|
39
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
40
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
41
|
+
} finally {
|
|
42
|
+
closeSync(fd);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function statFingerprint(path: string, follow: boolean): Omit<ShimPathFingerprint, "target"> | null {
|
|
47
|
+
try {
|
|
48
|
+
const stat = follow ? statSync(path) : lstatSync(path);
|
|
49
|
+
if (follow ? !stat.isFile() : (!stat.isFile() && !stat.isSymbolicLink())) return null;
|
|
50
|
+
return {
|
|
51
|
+
dev: stat.dev,
|
|
52
|
+
ino: stat.ino,
|
|
53
|
+
kind: stat.isSymbolicLink() ? "symlink" : "file",
|
|
54
|
+
mode: stat.mode,
|
|
55
|
+
size: stat.size,
|
|
56
|
+
mtimeMs: stat.mtimeMs,
|
|
57
|
+
ctimeMs: stat.ctimeMs,
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sameFingerprint(
|
|
65
|
+
left: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
66
|
+
right: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
67
|
+
): boolean {
|
|
68
|
+
return left.dev === right.dev
|
|
69
|
+
&& left.ino === right.ino
|
|
70
|
+
&& left.kind === right.kind
|
|
71
|
+
&& left.mode === right.mode
|
|
72
|
+
&& left.size === right.size
|
|
73
|
+
&& left.mtimeMs === right.mtimeMs
|
|
74
|
+
&& left.ctimeMs === right.ctimeMs
|
|
75
|
+
&& (!("target" in left) || !("target" in right)
|
|
76
|
+
? true
|
|
77
|
+
: left.target === undefined && right.target === undefined
|
|
78
|
+
? true
|
|
79
|
+
: left.target !== undefined && right.target !== undefined
|
|
80
|
+
? sameFingerprint(left.target, right.target)
|
|
81
|
+
: false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
|
|
85
|
+
// rename changes the outer directory entry ctime on macOS; every other field,
|
|
86
|
+
// including a symlink target fingerprint, must remain identical.
|
|
87
|
+
return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stableShimPathProbe(path: string): StableShimPathProbe | null {
|
|
91
|
+
const before = statFingerprint(path, false);
|
|
92
|
+
if (!before) return null;
|
|
93
|
+
const targetBefore = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
94
|
+
if (before.kind === "symlink" && !targetBefore) return null;
|
|
95
|
+
let prefix: string;
|
|
96
|
+
try {
|
|
97
|
+
prefix = readShimProbePrefix(path);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const targetAfter = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
102
|
+
const after = statFingerprint(path, false);
|
|
103
|
+
if (!after || !sameFingerprint(before, after)) return null;
|
|
104
|
+
if (before.kind === "symlink") {
|
|
105
|
+
if (!targetBefore || !targetAfter || !sameFingerprint(targetBefore, targetAfter)) return null;
|
|
106
|
+
}
|
|
107
|
+
const fingerprint: ShimPathFingerprint = {
|
|
108
|
+
...before,
|
|
109
|
+
...(targetBefore ? { target: targetBefore } : {}),
|
|
110
|
+
};
|
|
111
|
+
const contentSize = fingerprint.target?.size ?? fingerprint.size;
|
|
112
|
+
return contentSize > 0 ? { fingerprint, prefix } : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean {
|
|
116
|
+
return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Identity of whatever sits at `path`, read from metadata alone.
|
|
121
|
+
*
|
|
122
|
+
* `stableShimPathProbe` answers a different question: it reads content to decide
|
|
123
|
+
* whether a launcher looks like a healthy shim, and it deliberately returns null
|
|
124
|
+
* for a zero-byte file. That makes it the wrong instrument for rollback
|
|
125
|
+
* bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
|
|
126
|
+
* install moves it aside before writing our wrapper; if the move is recorded
|
|
127
|
+
* without a fingerprint, rollback cannot prove the backup is still the file it
|
|
128
|
+
* set aside and refuses to restore it — the launcher stays lost (#1625).
|
|
129
|
+
*
|
|
130
|
+
* Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
|
|
131
|
+
* re-reads them to reject a path that changed under us, following a symlink to
|
|
132
|
+
* fingerprint its target as well.
|
|
133
|
+
*/
|
|
134
|
+
function shimPathFingerprint(path: string): ShimPathFingerprint | null {
|
|
135
|
+
const before = statFingerprint(path, false);
|
|
136
|
+
if (!before) return null;
|
|
137
|
+
if (before.kind !== "symlink") {
|
|
138
|
+
const after = statFingerprint(path, false);
|
|
139
|
+
return after && sameFingerprint(before, after) ? before : null;
|
|
140
|
+
}
|
|
141
|
+
const targetBefore = statFingerprint(path, true);
|
|
142
|
+
if (!targetBefore) return null;
|
|
143
|
+
const targetAfter = statFingerprint(path, true);
|
|
144
|
+
const after = statFingerprint(path, false);
|
|
145
|
+
if (!targetAfter || !after
|
|
146
|
+
|| !sameFingerprint(targetBefore, targetAfter)
|
|
147
|
+
|| !sameFingerprint(before, after)) return null;
|
|
148
|
+
return { ...before, target: targetBefore };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Move `from` onto `to` without ever replacing an existing entry.
|
|
153
|
+
*
|
|
154
|
+
* `renameSync` silently clobbers the destination on POSIX, which is wrong for a
|
|
155
|
+
* rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
|
|
156
|
+
* a concurrent installer can publish its own launcher at the original path in
|
|
157
|
+
* between, and the restore would delete it. `link` fails EEXIST instead, which
|
|
158
|
+
* is the no-replace primitive we need and needs no native helper.
|
|
159
|
+
*
|
|
160
|
+
* `link` follows a symlink to its target rather than preserving the link, so a
|
|
161
|
+
* symlink launcher is republished with `symlink`, which is also no-replace: it
|
|
162
|
+
* fails EEXIST on an occupied destination. Checking existence and then renaming
|
|
163
|
+
* would reintroduce exactly the race this function exists to close.
|
|
164
|
+
*/
|
|
165
|
+
function restoreWithoutReplacing(from: string, to: string): void {
|
|
166
|
+
const source = lstatSync(from);
|
|
167
|
+
if (source.isSymbolicLink()) {
|
|
168
|
+
symlinkSync(readlinkSync(from), to);
|
|
169
|
+
unlinkSync(from);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
linkSync(from, to);
|
|
173
|
+
unlinkSync(from);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
|
|
177
|
+
if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
|
|
178
|
+
const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
|
|
179
|
+
return platform === "win32" || (mode & 0o111) !== 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
|
|
183
|
+
return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function hasUsableBackingPath(file: ShimFileState): boolean {
|
|
187
|
+
return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
|
|
188
|
+
.some(path => {
|
|
189
|
+
if (!path) return false;
|
|
190
|
+
const fingerprint = statFingerprint(path, true);
|
|
191
|
+
return fingerprint !== null && fingerprint.size > 0;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* True when a Codex binary lives inside a version manager's install tree.
|
|
197
|
+
*
|
|
198
|
+
* These trees are rewritten in place on upgrade, which destroys both the shim
|
|
199
|
+
* and the sibling .opencodex-real backup it restores from (#2412). The tempting
|
|
200
|
+
* repair — adopt the newly installed binary as a fresh original — is wrong
|
|
201
|
+
* twice: it records a provenance that never happened, and the next upgrade wipes
|
|
202
|
+
* it again, so the repair silently un-repairs on the version manager's schedule.
|
|
203
|
+
*
|
|
204
|
+
* Scope is the three managers named in the report. nvm/fnm/npm-prefix are
|
|
205
|
+
* deliberately excluded: a false positive here refuses a restore that would
|
|
206
|
+
* otherwise be correct.
|
|
207
|
+
*/
|
|
208
|
+
export function isVersionManagerOwnedCodexPath(
|
|
209
|
+
path: string,
|
|
210
|
+
platform: NodeJS.Platform = process.platform,
|
|
211
|
+
): boolean {
|
|
212
|
+
const normalized = (platform === "win32"
|
|
213
|
+
? win32.normalize(path).replace(/\\/g, "/")
|
|
214
|
+
: posix.normalize(path)).toLowerCase();
|
|
215
|
+
return normalized.includes("/mise/installs/")
|
|
216
|
+
|| normalized.includes("/mise/shims/")
|
|
217
|
+
|| normalized.includes("/.asdf/installs/")
|
|
218
|
+
|| normalized.includes("/.asdf/shims/")
|
|
219
|
+
|| normalized.includes("/.volta/");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type { ShimPathFingerprint, StableShimPathProbe };
|
|
223
|
+
export { statFingerprint, sameFingerprint, sameFingerprintAfterRename, stableShimPathProbe, sameStableShimPathProbe, shimPathFingerprint, restoreWithoutReplacing, isHealthyShimProbe, isCurrentUnixShimProbe, hasUsableBackingPath };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import { extname, join, posix, win32 } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
import { fileErrorCode, readStateResult, stateFiles } from "./shim-state-file";
|
|
5
|
+
import {
|
|
6
|
+
isHealthyShimProbe,
|
|
7
|
+
isVersionManagerOwnedCodexPath,
|
|
8
|
+
shimPathFingerprint,
|
|
9
|
+
stableShimPathProbe,
|
|
10
|
+
statFingerprint,
|
|
11
|
+
type ShimPathFingerprint,
|
|
12
|
+
} from "./shim-fingerprint";
|
|
13
|
+
import { gitBashPath, psString, shQuote, windowsBatchSet } from "./shim-templates";
|
|
14
|
+
|
|
15
|
+
export type CodexShimBackingForCommand =
|
|
16
|
+
| Readonly<{ status: "not-tracked" }>
|
|
17
|
+
| Readonly<{
|
|
18
|
+
status: "matched";
|
|
19
|
+
selectedRole: "wrapper" | "backing";
|
|
20
|
+
backingPath: string;
|
|
21
|
+
backingKind: "backup" | "real";
|
|
22
|
+
}>
|
|
23
|
+
| Readonly<{
|
|
24
|
+
status: "unknown";
|
|
25
|
+
reason:
|
|
26
|
+
| "state_invalid"
|
|
27
|
+
| "platform_mismatch"
|
|
28
|
+
| "ambiguous_match"
|
|
29
|
+
| "preserve_only"
|
|
30
|
+
| "backing_missing"
|
|
31
|
+
| "backing_mismatch"
|
|
32
|
+
| "binding_unavailable"
|
|
33
|
+
| "wrapper_unhealthy"
|
|
34
|
+
| "version_manager_refused";
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export function isLocalAbsoluteInspectionPath(path: string, platform: NodeJS.Platform): boolean {
|
|
38
|
+
if (platform !== "win32") return posix.isAbsolute(path);
|
|
39
|
+
const normalized = path.replace(/\//g, "\\");
|
|
40
|
+
// UNC and device namespaces can initiate remote I/O while a nominally local
|
|
41
|
+
// inspection is resolving user-controlled paths. Root-relative paths are
|
|
42
|
+
// drive-context dependent, so require an explicit local drive as well.
|
|
43
|
+
return win32.isAbsolute(path)
|
|
44
|
+
&& /^[a-z]:\\/i.test(normalized)
|
|
45
|
+
&& !normalized.startsWith("\\\\");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function windowsShimInspectionIsDeferred(platform: NodeJS.Platform): boolean {
|
|
49
|
+
return platform === "win32";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolve one selected command through already-recorded shim state, without repair. */
|
|
53
|
+
export function inspectCodexShimBackingForCommand(
|
|
54
|
+
selectedCommand: string,
|
|
55
|
+
platform: NodeJS.Platform = process.platform,
|
|
56
|
+
configDir: string = getConfigDir(),
|
|
57
|
+
): CodexShimBackingForCommand {
|
|
58
|
+
// Pathname prechecks cannot prevent a writable Windows ancestor from being
|
|
59
|
+
// replaced with a remote reparse point before the later state/fingerprint
|
|
60
|
+
// reads. Keep the exported read-only helper fail-closed until those reads are
|
|
61
|
+
// performed through a handle-bound Windows provenance layer.
|
|
62
|
+
if (windowsShimInspectionIsDeferred(platform)) {
|
|
63
|
+
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
64
|
+
}
|
|
65
|
+
if (!isLocalAbsoluteInspectionPath(configDir, platform)) {
|
|
66
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
67
|
+
}
|
|
68
|
+
const stateFile = join(configDir, "codex-shim.json");
|
|
69
|
+
try {
|
|
70
|
+
const stateEntry = lstatSync(stateFile);
|
|
71
|
+
if (stateEntry.isSymbolicLink()) {
|
|
72
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (fileErrorCode(error) !== "ENOENT") {
|
|
76
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const result = readStateResult(stateFile);
|
|
80
|
+
if (!result.state) {
|
|
81
|
+
return result.present
|
|
82
|
+
? Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const })
|
|
83
|
+
: Object.freeze({ status: "not-tracked" as const });
|
|
84
|
+
}
|
|
85
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
86
|
+
const samePath = (left: string, right: string): boolean => {
|
|
87
|
+
const normalizedLeft = pathApi.resolve(left);
|
|
88
|
+
const normalizedRight = pathApi.resolve(right);
|
|
89
|
+
return platform === "win32"
|
|
90
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
91
|
+
: normalizedLeft === normalizedRight;
|
|
92
|
+
};
|
|
93
|
+
const files = stateFiles(result.state);
|
|
94
|
+
if (files.some(file => !file.wrapperPath || !file.originalPath || !file.backupPath
|
|
95
|
+
|| ![file.wrapperPath, file.originalPath, file.backupPath, file.realPath]
|
|
96
|
+
.filter((path): path is string => typeof path === "string")
|
|
97
|
+
.every(path => isLocalAbsoluteInspectionPath(path, platform)))) {
|
|
98
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
99
|
+
}
|
|
100
|
+
const wrapperKeys = files.map(file => platform === "win32"
|
|
101
|
+
? pathApi.resolve(file.wrapperPath).toLowerCase()
|
|
102
|
+
: pathApi.resolve(file.wrapperPath));
|
|
103
|
+
if (new Set(wrapperKeys).size !== wrapperKeys.length) {
|
|
104
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
105
|
+
}
|
|
106
|
+
const selectedFingerprint = shimPathFingerprint(selectedCommand);
|
|
107
|
+
if (!selectedFingerprint) {
|
|
108
|
+
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
109
|
+
}
|
|
110
|
+
const selectedIdentity = selectedFingerprint.target ?? selectedFingerprint;
|
|
111
|
+
const sameEffectiveIdentity = (fingerprint: ShimPathFingerprint | null): boolean => {
|
|
112
|
+
if (!fingerprint) return false;
|
|
113
|
+
const identity = fingerprint.target ?? fingerprint;
|
|
114
|
+
return identity.dev === selectedIdentity.dev && identity.ino === selectedIdentity.ino;
|
|
115
|
+
};
|
|
116
|
+
const matches = files.flatMap(file => {
|
|
117
|
+
const backingPath = file.realPath ?? file.backupPath;
|
|
118
|
+
const roles: Array<"wrapper" | "backing"> = [];
|
|
119
|
+
if (samePath(file.wrapperPath, selectedCommand)
|
|
120
|
+
|| sameEffectiveIdentity(shimPathFingerprint(file.wrapperPath))) {
|
|
121
|
+
roles.push("wrapper");
|
|
122
|
+
}
|
|
123
|
+
if (samePath(backingPath, selectedCommand)
|
|
124
|
+
|| sameEffectiveIdentity(shimPathFingerprint(backingPath))) {
|
|
125
|
+
roles.push("backing");
|
|
126
|
+
}
|
|
127
|
+
return roles.map(selectedRole => ({ file, backingPath, selectedRole }));
|
|
128
|
+
});
|
|
129
|
+
if (matches.length === 0) return Object.freeze({ status: "not-tracked" as const });
|
|
130
|
+
if (result.state.platform !== platform) {
|
|
131
|
+
return Object.freeze({ status: "unknown" as const, reason: "platform_mismatch" as const });
|
|
132
|
+
}
|
|
133
|
+
if (matches.length !== 1) {
|
|
134
|
+
return Object.freeze({ status: "unknown" as const, reason: "ambiguous_match" as const });
|
|
135
|
+
}
|
|
136
|
+
const { file, backingPath, selectedRole } = matches[0]!;
|
|
137
|
+
if (file.preserveOnly === true) {
|
|
138
|
+
return Object.freeze({ status: "unknown" as const, reason: "preserve_only" as const });
|
|
139
|
+
}
|
|
140
|
+
const backing = statFingerprint(backingPath, true);
|
|
141
|
+
if (!backing || backing.size <= 0 || samePath(backingPath, file.wrapperPath)) {
|
|
142
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_missing" as const });
|
|
143
|
+
}
|
|
144
|
+
const wrapperProbe = stableShimPathProbe(file.wrapperPath);
|
|
145
|
+
if (!wrapperProbe || !isHealthyShimProbe(wrapperProbe, result.state.platform)) {
|
|
146
|
+
return Object.freeze({
|
|
147
|
+
status: "unknown" as const,
|
|
148
|
+
reason: isVersionManagerOwnedCodexPath(file.wrapperPath)
|
|
149
|
+
? "version_manager_refused" as const
|
|
150
|
+
: "wrapper_unhealthy" as const,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const wrapperIdentity = wrapperProbe.fingerprint.target ?? wrapperProbe.fingerprint;
|
|
154
|
+
if (backing.dev === wrapperIdentity.dev && backing.ino === wrapperIdentity.ino) {
|
|
155
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
156
|
+
}
|
|
157
|
+
const wrapperExt = extname(file.wrapperPath).toLowerCase();
|
|
158
|
+
const invokesBacking = platform !== "win32"
|
|
159
|
+
? wrapperProbe.prefix.includes(`exec ${shQuote(backingPath)} "$@"`)
|
|
160
|
+
: wrapperExt === ".cmd" || wrapperExt === ".bat"
|
|
161
|
+
? wrapperProbe.prefix.includes(windowsBatchSet("OCX_REAL_CODEX", backingPath))
|
|
162
|
+
&& wrapperProbe.prefix.includes('"%OCX_REAL_CODEX%" %*')
|
|
163
|
+
: wrapperExt === ".ps1"
|
|
164
|
+
? wrapperProbe.prefix.includes(`& ${psString(backingPath)} @args`)
|
|
165
|
+
: wrapperProbe.prefix.includes(`exec ${shQuote(gitBashPath(backingPath))} "$@"`);
|
|
166
|
+
if (!invokesBacking) {
|
|
167
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
status: "matched" as const,
|
|
171
|
+
selectedRole,
|
|
172
|
+
backingPath,
|
|
173
|
+
backingKind: file.realPath !== undefined ? "real" as const : "backup" as const,
|
|
174
|
+
});
|
|
175
|
+
}
|