@bitkyc08/opencodex 2.12.0 → 2.13.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/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/shim.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
3
5
|
import {
|
|
4
6
|
chmodSync,
|
|
5
7
|
closeSync,
|
|
@@ -7,11 +9,13 @@ import {
|
|
|
7
9
|
fstatSync,
|
|
8
10
|
lstatSync,
|
|
9
11
|
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
10
13
|
openSync,
|
|
11
14
|
readFileSync,
|
|
12
15
|
readdirSync,
|
|
13
16
|
readSync,
|
|
14
17
|
renameSync,
|
|
18
|
+
rmSync,
|
|
15
19
|
rmdirSync,
|
|
16
20
|
statSync,
|
|
17
21
|
type Stats,
|
|
@@ -29,10 +33,173 @@ import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
|
29
33
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
30
34
|
|
|
31
35
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
36
|
+
const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
|
|
32
37
|
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
33
38
|
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
34
39
|
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
35
40
|
const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
|
|
41
|
+
const CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS = 5_000;
|
|
42
|
+
const CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS = 1_000;
|
|
43
|
+
const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
|
|
44
|
+
const CODEX_SHIM_REENTRY_DIAGNOSTIC = "opencodex: saved Codex launcher resolved back to the autostart shim; run ocx codex-shim uninstall and reinstall Codex before enabling codexAutoStart.";
|
|
45
|
+
const CODEX_SHIM_INSTALL_PROBE_SCRIPT = `
|
|
46
|
+
const { spawn } = require("node:child_process");
|
|
47
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
48
|
+
const [markerPath, reentryPath, groupPath, stderrPath, launcherShellPath, wrapperPath, timeoutRaw, stderrLimitRaw, stderrDrainRaw, observationRaw] = process.argv.slice(1);
|
|
49
|
+
const timeoutMs = Number.parseInt(timeoutRaw, 10);
|
|
50
|
+
const stderrLimit = Number.parseInt(stderrLimitRaw, 10);
|
|
51
|
+
const stderrDrainMs = Number.parseInt(stderrDrainRaw, 10);
|
|
52
|
+
const observationMs = Number.parseInt(observationRaw, 10);
|
|
53
|
+
const probeStartedAt = Date.now();
|
|
54
|
+
const stderrChunks = [];
|
|
55
|
+
let stderrBytes = 0;
|
|
56
|
+
let launcher;
|
|
57
|
+
let probeLease;
|
|
58
|
+
let timer;
|
|
59
|
+
let stderrDrainTimer;
|
|
60
|
+
let observationTimer;
|
|
61
|
+
let reentryPollTimer;
|
|
62
|
+
let marker = "";
|
|
63
|
+
let finished = false;
|
|
64
|
+
|
|
65
|
+
function writeExclusive(path, value) {
|
|
66
|
+
writeFileSync(path, value, { flag: "wx", mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function appendStderr(value) {
|
|
70
|
+
if (stderrBytes >= stderrLimit) return;
|
|
71
|
+
const bytes = Buffer.from(value);
|
|
72
|
+
const retained = bytes.subarray(0, stderrLimit - stderrBytes);
|
|
73
|
+
stderrChunks.push(retained);
|
|
74
|
+
stderrBytes += retained.byteLength;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function groupAlive() {
|
|
78
|
+
if (!launcher || !launcher.pid) return false;
|
|
79
|
+
try {
|
|
80
|
+
process.kill(-launcher.pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return error && error.code !== "ESRCH";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function killGroup() {
|
|
88
|
+
if (!launcher || !launcher.pid) return;
|
|
89
|
+
try { process.kill(-launcher.pid, "SIGKILL"); } catch (error) {
|
|
90
|
+
if (!error || error.code !== "ESRCH") appendStderr(String(error));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setMarker(value) {
|
|
95
|
+
if (marker) return;
|
|
96
|
+
marker = value;
|
|
97
|
+
try { writeExclusive(markerPath, value + "\\n"); } catch (error) { appendStderr(String(error)); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reentryDetected() {
|
|
101
|
+
try { return readFileSync(reentryPath, "utf8").trim() === "recursive"; } catch { return false; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkReentry() {
|
|
105
|
+
if (finished || !reentryDetected()) return;
|
|
106
|
+
setMarker("recursive");
|
|
107
|
+
killGroup();
|
|
108
|
+
finish(126);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function finish(status) {
|
|
112
|
+
if (finished) return;
|
|
113
|
+
finished = true;
|
|
114
|
+
if (timer) clearTimeout(timer);
|
|
115
|
+
if (stderrDrainTimer) clearTimeout(stderrDrainTimer);
|
|
116
|
+
if (observationTimer) clearTimeout(observationTimer);
|
|
117
|
+
if (reentryPollTimer) clearInterval(reentryPollTimer);
|
|
118
|
+
if (!marker && reentryDetected()) setMarker("recursive");
|
|
119
|
+
if (!marker && groupAlive()) {
|
|
120
|
+
setMarker("descendants");
|
|
121
|
+
killGroup();
|
|
122
|
+
}
|
|
123
|
+
try { writeExclusive(stderrPath, Buffer.concat(stderrChunks)); } catch { /* parent fails closed */ }
|
|
124
|
+
process.exit(marker === "timeout" ? 124 : marker === "descendants" ? 125 : marker === "recursive" ? 126 : status);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function finishAfterStderr(status) {
|
|
128
|
+
if (finished) return;
|
|
129
|
+
if (timer) {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
timer = undefined;
|
|
132
|
+
}
|
|
133
|
+
if (!launcher || !launcher.stderr || !probeLease) {
|
|
134
|
+
finish(status);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let stderrEnded = launcher.stderr.readableEnded;
|
|
138
|
+
let leaseEnded = probeLease.readableEnded;
|
|
139
|
+
let observationElapsed = false;
|
|
140
|
+
const finishWhenReady = () => {
|
|
141
|
+
if (stderrEnded && leaseEnded && observationElapsed) finish(status);
|
|
142
|
+
};
|
|
143
|
+
launcher.stderr.once("end", () => {
|
|
144
|
+
stderrEnded = true;
|
|
145
|
+
finishWhenReady();
|
|
146
|
+
});
|
|
147
|
+
probeLease.once("end", () => {
|
|
148
|
+
leaseEnded = true;
|
|
149
|
+
finishWhenReady();
|
|
150
|
+
});
|
|
151
|
+
stderrDrainTimer = setTimeout(() => {
|
|
152
|
+
stderrEnded = true;
|
|
153
|
+
if (!marker && groupAlive()) {
|
|
154
|
+
setMarker("descendants");
|
|
155
|
+
killGroup();
|
|
156
|
+
finish(125);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
finishWhenReady();
|
|
160
|
+
}, stderrDrainMs);
|
|
161
|
+
const remainingObservationMs = Math.max(0, observationMs - (Date.now() - probeStartedAt));
|
|
162
|
+
observationTimer = setTimeout(() => {
|
|
163
|
+
observationElapsed = true;
|
|
164
|
+
if (!leaseEnded) {
|
|
165
|
+
setMarker(groupAlive() ? "descendants" : "timeout");
|
|
166
|
+
killGroup();
|
|
167
|
+
finish(marker === "descendants" ? 125 : 124);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
finishWhenReady();
|
|
171
|
+
}, remainingObservationMs);
|
|
172
|
+
finishWhenReady();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
|
|
177
|
+
detached: true,
|
|
178
|
+
env: process.env,
|
|
179
|
+
stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
180
|
+
});
|
|
181
|
+
if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
|
|
182
|
+
probeLease = launcher.stdio[3];
|
|
183
|
+
if (!probeLease) throw new Error("Codex shim probe launcher has no descendant lease pipe");
|
|
184
|
+
writeExclusive(groupPath, String(launcher.pid) + "\\n");
|
|
185
|
+
launcher.stderr.on("data", appendStderr);
|
|
186
|
+
reentryPollTimer = setInterval(checkReentry, 10);
|
|
187
|
+
launcher.once("error", error => {
|
|
188
|
+
appendStderr(String(error));
|
|
189
|
+
finishAfterStderr(127);
|
|
190
|
+
});
|
|
191
|
+
launcher.once("exit", code => finishAfterStderr(Number.isInteger(code) ? code : 127));
|
|
192
|
+
timer = setTimeout(() => {
|
|
193
|
+
setMarker("timeout");
|
|
194
|
+
killGroup();
|
|
195
|
+
finish(124);
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
appendStderr(String(error));
|
|
199
|
+
killGroup();
|
|
200
|
+
finish(127);
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
36
203
|
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
37
204
|
let lastShimDiscoveryError: string | null = null;
|
|
38
205
|
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
@@ -148,6 +315,7 @@ function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
|
148
315
|
try {
|
|
149
316
|
const content = readFileSync(path, "utf8");
|
|
150
317
|
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
318
|
+
if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
|
|
151
319
|
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
152
320
|
return true;
|
|
153
321
|
} catch {
|
|
@@ -204,6 +372,12 @@ function sameFingerprint(
|
|
|
204
372
|
: false);
|
|
205
373
|
}
|
|
206
374
|
|
|
375
|
+
function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
|
|
376
|
+
// rename changes the outer directory entry ctime on macOS; every other field,
|
|
377
|
+
// including a symlink target fingerprint, must remain identical.
|
|
378
|
+
return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
|
|
379
|
+
}
|
|
380
|
+
|
|
207
381
|
function stableShimPathProbe(path: string): StableShimPathProbe | null {
|
|
208
382
|
const before = statFingerprint(path, false);
|
|
209
383
|
if (!before) return null;
|
|
@@ -239,6 +413,10 @@ function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platfor
|
|
|
239
413
|
return platform === "win32" || (mode & 0o111) !== 0;
|
|
240
414
|
}
|
|
241
415
|
|
|
416
|
+
function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
|
|
417
|
+
return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
|
|
418
|
+
}
|
|
419
|
+
|
|
242
420
|
function hasUsableBackingPath(file: ShimFileState): boolean {
|
|
243
421
|
return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
|
|
244
422
|
.some(path => {
|
|
@@ -383,6 +561,40 @@ export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPa
|
|
|
383
561
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
384
562
|
return `#!/usr/bin/env sh
|
|
385
563
|
# ${SHIM_MARKER}
|
|
564
|
+
# ${UNIX_SHIM_REVISION_MARKER}
|
|
565
|
+
if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
|
|
566
|
+
if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
|
|
567
|
+
if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
|
|
568
|
+
(umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
|
|
569
|
+
fi
|
|
570
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
571
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
572
|
+
fi
|
|
573
|
+
OCX_SHIM_PROBE_ACTIVE=1
|
|
574
|
+
export OCX_SHIM_PROBE_ACTIVE
|
|
575
|
+
fi
|
|
576
|
+
if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
|
|
577
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
578
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
579
|
+
fi
|
|
580
|
+
case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
|
|
581
|
+
0)
|
|
582
|
+
OCX_SHIM_ACTIVE_DEPTH=1
|
|
583
|
+
;;
|
|
584
|
+
1)
|
|
585
|
+
OCX_SHIM_ACTIVE_DEPTH=2
|
|
586
|
+
;;
|
|
587
|
+
*)
|
|
588
|
+
printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
|
|
589
|
+
exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
|
|
590
|
+
;;
|
|
591
|
+
esac
|
|
592
|
+
# Dynamic launchers such as mise exec -- codex may resolve the command name
|
|
593
|
+
# back to this wrapper. An exec chain keeps the same PID. A legitimate nested
|
|
594
|
+
# Codex invocation may enter once with a new PID; repeated child-process
|
|
595
|
+
# redispatch reaches depth 2 and is rejected before it can form an infinite chain.
|
|
596
|
+
OCX_SHIM_ACTIVE_PID=$$
|
|
597
|
+
export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
|
|
386
598
|
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
387
599
|
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
388
600
|
export OPENCODEX_API_AUTH_TOKEN
|
|
@@ -426,6 +638,219 @@ exec ${shQuote(realCodexPath)} "$@"
|
|
|
426
638
|
`;
|
|
427
639
|
}
|
|
428
640
|
|
|
641
|
+
type UnixShimProbeResult = "cleanup" | "descendants" | "failed" | "recursive" | "timeout" | null;
|
|
642
|
+
|
|
643
|
+
let codexShimProbeHookForTests: (() => void) | null = null;
|
|
644
|
+
let codexShimProbeShellForTests: string | null = null;
|
|
645
|
+
let codexShimGuardedWriteHookForTests: (() => void) | null = null;
|
|
646
|
+
let codexShimFreshWriteHookForTests: (() => void) | null = null;
|
|
647
|
+
let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
648
|
+
|
|
649
|
+
/** Narrow deterministic seam for transaction rollback tests. */
|
|
650
|
+
export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
|
|
651
|
+
codexShimProbeHookForTests = hook;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Selects a POSIX shell only for cross-shell probe regression tests. */
|
|
655
|
+
export function setCodexShimProbeShellForTests(path: string | null): void {
|
|
656
|
+
codexShimProbeShellForTests = path;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Shortens the successful-launcher observation window only for focused tests. */
|
|
660
|
+
export function setCodexShimProbeObservationMsForTests(value: number | null): void {
|
|
661
|
+
codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Narrow deterministic seam for guarded partial-write rollback tests. */
|
|
665
|
+
export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
|
|
666
|
+
codexShimGuardedWriteHookForTests = hook;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Narrow deterministic seam for fresh-install partial-write rollback tests. */
|
|
670
|
+
export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): void {
|
|
671
|
+
codexShimFreshWriteHookForTests = hook;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function readProbeMetadata(path: string, maxBytes: number): string | null {
|
|
675
|
+
try {
|
|
676
|
+
if (!existsSync(path)) return "";
|
|
677
|
+
const stat = lstatSync(path);
|
|
678
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
679
|
+
return readFileSync(path, "utf8").trim();
|
|
680
|
+
} catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
686
|
+
if (process.platform === "win32") return null;
|
|
687
|
+
const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
|
|
688
|
+
const markerPath = join(probeDir, "result");
|
|
689
|
+
const reentryPath = join(probeDir, "reentry");
|
|
690
|
+
const groupPath = join(probeDir, "group");
|
|
691
|
+
const stderrPath = join(probeDir, "stderr");
|
|
692
|
+
const env: NodeJS.ProcessEnv = {
|
|
693
|
+
...process.env,
|
|
694
|
+
OCX_SHIM_BYPASS: "1",
|
|
695
|
+
OCX_SHIM_PROBE: "1",
|
|
696
|
+
OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
|
|
697
|
+
};
|
|
698
|
+
delete env.OCX_SHIM_ACTIVE_PID;
|
|
699
|
+
delete env.OCX_SHIM_ACTIVE_DEPTH;
|
|
700
|
+
delete env.OCX_SHIM_PROBE_ACTIVE;
|
|
701
|
+
let groupId = 0;
|
|
702
|
+
try {
|
|
703
|
+
chmodSync(probeDir, 0o700);
|
|
704
|
+
const result = spawnSync(process.execPath, [
|
|
705
|
+
"-e",
|
|
706
|
+
CODEX_SHIM_INSTALL_PROBE_SCRIPT,
|
|
707
|
+
markerPath,
|
|
708
|
+
reentryPath,
|
|
709
|
+
groupPath,
|
|
710
|
+
stderrPath,
|
|
711
|
+
codexShimProbeShellForTests ?? "/bin/sh",
|
|
712
|
+
wrapperPath,
|
|
713
|
+
String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
|
|
714
|
+
String(MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
715
|
+
String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
|
|
716
|
+
String(codexShimProbeObservationMs),
|
|
717
|
+
], {
|
|
718
|
+
encoding: "utf8",
|
|
719
|
+
env,
|
|
720
|
+
timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
|
|
721
|
+
killSignal: "SIGKILL",
|
|
722
|
+
});
|
|
723
|
+
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
|
724
|
+
const marker = readProbeMetadata(markerPath, 64);
|
|
725
|
+
const reentryMarker = readProbeMetadata(reentryPath, 64);
|
|
726
|
+
const groupText = readProbeMetadata(groupPath, 64);
|
|
727
|
+
const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
728
|
+
groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
|
|
729
|
+
if (marker === null || reentryMarker === null || groupText === null || launcherStderr === null
|
|
730
|
+
|| !Number.isInteger(groupId) || groupId <= 0) return "cleanup";
|
|
731
|
+
const groupSurvived = unixProcessGroupAlive(groupId);
|
|
732
|
+
if (timedOut || marker || reentryMarker || groupSurvived) {
|
|
733
|
+
try {
|
|
734
|
+
terminateUnixProcessGroup(groupId);
|
|
735
|
+
} catch {
|
|
736
|
+
return "cleanup";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (result.error && !timedOut) return "cleanup";
|
|
740
|
+
if (timedOut || marker === "timeout") return "timeout";
|
|
741
|
+
if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
|
|
742
|
+
if (reentryMarker !== "") return "cleanup";
|
|
743
|
+
if (marker === "descendants") return "descendants";
|
|
744
|
+
if (groupSurvived) return "descendants";
|
|
745
|
+
if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
|
|
746
|
+
return "recursive";
|
|
747
|
+
}
|
|
748
|
+
if (result.status !== 0) return "failed";
|
|
749
|
+
return null;
|
|
750
|
+
} catch {
|
|
751
|
+
if (Number.isInteger(groupId) && groupId > 0) {
|
|
752
|
+
try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
|
|
753
|
+
}
|
|
754
|
+
return "cleanup";
|
|
755
|
+
} finally {
|
|
756
|
+
try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
|
|
761
|
+
if (process.platform === "win32") return null;
|
|
762
|
+
codexShimProbeHookForTests?.();
|
|
763
|
+
return files
|
|
764
|
+
.filter(file => !file.preserveOnly)
|
|
765
|
+
.map(file => probeUnixShimInstall(file.wrapperPath))
|
|
766
|
+
.find(result => result !== null) ?? null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function unixProcessGroupAlive(groupId: number): boolean {
|
|
770
|
+
try {
|
|
771
|
+
process.kill(-groupId, 0);
|
|
772
|
+
return true;
|
|
773
|
+
} catch (error) {
|
|
774
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function terminateUnixProcessGroup(groupId: number): void {
|
|
779
|
+
try {
|
|
780
|
+
process.kill(-groupId, "SIGKILL");
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
783
|
+
}
|
|
784
|
+
const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
|
|
785
|
+
while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
|
|
786
|
+
if (unixProcessGroupAlive(groupId)) {
|
|
787
|
+
throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
interface FreshShimInstallJournalEntry {
|
|
792
|
+
target: ShimFileState;
|
|
793
|
+
movedOriginalFingerprint?: ShimPathFingerprint;
|
|
794
|
+
originalMovedToBackup: boolean;
|
|
795
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
796
|
+
wrapperWriteStarted: boolean;
|
|
797
|
+
/** dev/ino of the file our `writeShim()` created, recorded before anything can fail. */
|
|
798
|
+
writtenWrapperInode?: { dev: number; ino: number };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry[]): void {
|
|
802
|
+
const errors: Error[] = [];
|
|
803
|
+
for (const entry of [...journal].reverse()) {
|
|
804
|
+
const target = entry.target;
|
|
805
|
+
let sourceOccupied = false;
|
|
806
|
+
let ownsWrapperNow = false;
|
|
807
|
+
try {
|
|
808
|
+
const wrapper = stableShimPathProbe(target.wrapperPath);
|
|
809
|
+
// Ownership is the inode our own write created. There is no marker-text
|
|
810
|
+
// fallback: the markers are public, so a concurrent updater's wrapper carries
|
|
811
|
+
// them too, and treating that as proof is how we would delete a file we never
|
|
812
|
+
// wrote. An in-place truncation of our file keeps the inode and is still ours
|
|
813
|
+
// to clean up; a replacement renamed over it has a different inode and is not.
|
|
814
|
+
const ownsWrapper = !target.preserveOnly
|
|
815
|
+
&& entry.wrapperWriteStarted
|
|
816
|
+
&& wrapper !== null
|
|
817
|
+
&& wrapperInodeIsOurs(wrapper, entry.writtenWrapperInode, entry.writtenWrapperFingerprint);
|
|
818
|
+
ownsWrapperNow = ownsWrapper;
|
|
819
|
+
if (ownsWrapper) unlinkSync(target.wrapperPath);
|
|
820
|
+
else {
|
|
821
|
+
try {
|
|
822
|
+
lstatSync(target.originalPath);
|
|
823
|
+
sourceOccupied = true;
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch (error) {
|
|
829
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
if (entry.originalMovedToBackup && existsSync(target.backupPath)) {
|
|
833
|
+
const movedOriginal = stableShimPathProbe(target.backupPath);
|
|
834
|
+
if (!movedOriginal || !entry.movedOriginalFingerprint
|
|
835
|
+
|| !sameFingerprint(movedOriginal.fingerprint, entry.movedOriginalFingerprint)) {
|
|
836
|
+
throw new Error("Codex shim fresh-install backup changed during rollback");
|
|
837
|
+
}
|
|
838
|
+
if (sourceOccupied) {
|
|
839
|
+
// Something else occupies the source path. Dropping the backup is correct
|
|
840
|
+
// only when that something is a file we own; when a concurrent updater
|
|
841
|
+
// owns it, this backup is the user's real launcher and deleting it would
|
|
842
|
+
// lose the command entirely. Keep it in that case — a stray
|
|
843
|
+
// `codex.opencodex-real` is recoverable, a deleted launcher is not.
|
|
844
|
+
if (ownsWrapperNow) unlinkSync(target.backupPath);
|
|
845
|
+
} else renameSync(target.backupPath, target.originalPath);
|
|
846
|
+
}
|
|
847
|
+
} catch (error) {
|
|
848
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
|
|
852
|
+
}
|
|
853
|
+
|
|
429
854
|
function windowsBatchValue(value: string): string {
|
|
430
855
|
return value
|
|
431
856
|
.replace(/%/g, "%%")
|
|
@@ -626,7 +1051,16 @@ function gitBashPath(path: string): string {
|
|
|
626
1051
|
return path.replace(/\\/g, "/");
|
|
627
1052
|
}
|
|
628
1053
|
|
|
629
|
-
|
|
1054
|
+
/**
|
|
1055
|
+
* Write the wrapper and return the identity of the inode this call created, or
|
|
1056
|
+
* `undefined` where the platform still writes the destination in place.
|
|
1057
|
+
*
|
|
1058
|
+
* Callers must derive ownership from the returned identity rather than from a
|
|
1059
|
+
* later `stat` of `wrapperPath`: the shim markers are public, so a concurrent
|
|
1060
|
+
* updater's wrapper can carry them, and a replacement landing between the write
|
|
1061
|
+
* and the observation is otherwise indistinguishable from our own file.
|
|
1062
|
+
*/
|
|
1063
|
+
function writeShim(wrapperPath: string, realCodexPath: string): { dev: number; ino: number } | undefined {
|
|
630
1064
|
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
631
1065
|
if (process.platform === "win32") {
|
|
632
1066
|
const lower = wrapperPath.toLowerCase();
|
|
@@ -644,12 +1078,83 @@ function writeShim(wrapperPath: string, realCodexPath: string): void {
|
|
|
644
1078
|
"utf8",
|
|
645
1079
|
);
|
|
646
1080
|
}
|
|
1081
|
+
return undefined;
|
|
647
1082
|
} else {
|
|
648
|
-
|
|
649
|
-
|
|
1083
|
+
// Stage the wrapper as its own inode and rename it into place, so ownership
|
|
1084
|
+
// comes from the write itself rather than from observing the path afterwards.
|
|
1085
|
+
// Writing the destination directly leaves a window in which a concurrent
|
|
1086
|
+
// updater can replace the file between our write and our fingerprint; we would
|
|
1087
|
+
// then adopt that replacement as ours and unlink it during rollback, deleting
|
|
1088
|
+
// an executable we never wrote.
|
|
1089
|
+
// Hidden and non-executable while staged, so a crash between the write and the
|
|
1090
|
+
// rename cannot leave an executable `codex*` artifact that a glob or a shell
|
|
1091
|
+
// completion would surface.
|
|
1092
|
+
const staged = join(dirname(wrapperPath), `.${basename(wrapperPath)}.opencodex-staging.${process.pid}.${randomUUID()}`);
|
|
1093
|
+
let renamed = false;
|
|
1094
|
+
try {
|
|
1095
|
+
// "wx" fails if the staging path somehow exists, so we never inherit a file.
|
|
1096
|
+
writeFileSync(staged, buildUnixCodexShim(realCodexPath, bun, cli, bunRuntimeSource), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
1097
|
+
const stagedStat = lstatSync(staged);
|
|
1098
|
+
chmodSync(staged, 0o755);
|
|
1099
|
+
renameSync(staged, wrapperPath);
|
|
1100
|
+
renamed = true;
|
|
1101
|
+
// rename() preserves dev/ino and updates ctime, so identity is the inode
|
|
1102
|
+
// pair, captured from the file we created rather than from the destination.
|
|
1103
|
+
return { dev: stagedStat.dev, ino: stagedStat.ino };
|
|
1104
|
+
} finally {
|
|
1105
|
+
if (!renamed) {
|
|
1106
|
+
try { unlinkSync(staged); } catch { /* best-effort: nothing to clean up */ }
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
650
1109
|
}
|
|
651
1110
|
}
|
|
652
1111
|
|
|
1112
|
+
/**
|
|
1113
|
+
* The fingerprint to record as "we wrote this", or `undefined` when the file at
|
|
1114
|
+
* `wrapperPath` is not the inode `writeShim()` created. Returning `undefined`
|
|
1115
|
+
* makes every rollback path treat the file as someone else's and leave it alone.
|
|
1116
|
+
*/
|
|
1117
|
+
function ownedWrapperFingerprint(
|
|
1118
|
+
wrapperPath: string,
|
|
1119
|
+
written: { dev: number; ino: number } | undefined,
|
|
1120
|
+
): ShimPathFingerprint | undefined {
|
|
1121
|
+
const probe = stableShimPathProbe(wrapperPath);
|
|
1122
|
+
if (!probe) return undefined;
|
|
1123
|
+
// Platforms that still write in place (Windows) have no staged identity; fall
|
|
1124
|
+
// back to the marker check they have always used.
|
|
1125
|
+
if (!written) return probe.prefix.includes(SHIM_MARKER) ? probe.fingerprint : undefined;
|
|
1126
|
+
if (probe.fingerprint.dev !== written.dev || probe.fingerprint.ino !== written.ino) return undefined;
|
|
1127
|
+
return probe.fingerprint;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Whether the file now at the wrapper path is one this transaction may unlink.
|
|
1132
|
+
*
|
|
1133
|
+
* The inode our write created is the authority. It stays ours through an in-place
|
|
1134
|
+
* truncation — a partial write we must clean up — and stops being ours the moment
|
|
1135
|
+
* someone renames a different file over the path, which is exactly the concurrent
|
|
1136
|
+
* updater we must not delete. Where no inode was recorded (Windows writes the
|
|
1137
|
+
* destination directly), fall back to the exact fingerprint recorded at the time.
|
|
1138
|
+
*/
|
|
1139
|
+
function wrapperInodeIsOurs(
|
|
1140
|
+
wrapper: StableShimPathProbe,
|
|
1141
|
+
written: { dev: number; ino: number } | undefined,
|
|
1142
|
+
recorded: ShimPathFingerprint | undefined,
|
|
1143
|
+
): boolean {
|
|
1144
|
+
// A different inode is conclusive: someone renamed their own file over ours.
|
|
1145
|
+
if (written && (wrapper.fingerprint.dev !== written.dev || wrapper.fingerprint.ino !== written.ino)) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
// Same inode is not sufficient on its own — an in-place truncation (shell `>`,
|
|
1149
|
+
// writeFileSync) keeps it while replacing the contents. When we recorded a full
|
|
1150
|
+
// fingerprint, require it to still match; that covers our own partial write,
|
|
1151
|
+
// whose fingerprint we record before anything can fail.
|
|
1152
|
+
if (recorded !== undefined) return sameFingerprint(wrapper.fingerprint, recorded);
|
|
1153
|
+
// No recorded fingerprint: only the inode identity we captured at write time can
|
|
1154
|
+
// speak for us, and there is nothing else to distinguish this file.
|
|
1155
|
+
return written !== undefined;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
653
1158
|
function stateFiles(state: ShimState): ShimFileState[] {
|
|
654
1159
|
return state.wrappers?.length
|
|
655
1160
|
? state.wrappers
|
|
@@ -684,12 +1189,37 @@ function refreshShimFile(file: ShimFileState): boolean {
|
|
|
684
1189
|
}
|
|
685
1190
|
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
686
1191
|
if (file.wrapperPath !== file.originalPath) return false;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
return
|
|
1192
|
+
const replacement = stableShimPathProbe(file.wrapperPath);
|
|
1193
|
+
if (!replacement) return false;
|
|
1194
|
+
return applyGuardedRefreshTransaction([{
|
|
1195
|
+
file,
|
|
1196
|
+
expectedReplacement: replacement.fingerprint,
|
|
1197
|
+
sourcePath: file.wrapperPath,
|
|
1198
|
+
}]);
|
|
690
1199
|
}
|
|
691
1200
|
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
692
1201
|
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1202
|
+
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1203
|
+
if (!writtenWrapper || !writtenWrapper.prefix.includes(SHIM_MARKER)) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1207
|
+
let probeError: Error | null = null;
|
|
1208
|
+
try {
|
|
1209
|
+
unsafe = probeUnixShimFiles([file]);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1212
|
+
}
|
|
1213
|
+
const currentWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1214
|
+
const wrapperChangedDuringProbe = !currentWrapper
|
|
1215
|
+
|| !sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint);
|
|
1216
|
+
if (unsafe !== null || probeError || wrapperChangedDuringProbe) {
|
|
1217
|
+
if (currentWrapper && sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint)) {
|
|
1218
|
+
unlinkSync(file.wrapperPath);
|
|
1219
|
+
}
|
|
1220
|
+
if (probeError) throw probeError;
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
693
1223
|
return true;
|
|
694
1224
|
}
|
|
695
1225
|
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
@@ -709,7 +1239,9 @@ interface GuardedRefreshOperation {
|
|
|
709
1239
|
interface GuardedRefreshJournalEntry {
|
|
710
1240
|
operation: GuardedRefreshOperation;
|
|
711
1241
|
stagedOldBackupPath?: string;
|
|
1242
|
+
movedReplacementFingerprint?: ShimPathFingerprint;
|
|
712
1243
|
replacementMovedToBackup: boolean;
|
|
1244
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
713
1245
|
wrapperWriteStarted: boolean;
|
|
714
1246
|
}
|
|
715
1247
|
|
|
@@ -901,14 +1433,35 @@ function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]):
|
|
|
901
1433
|
}
|
|
902
1434
|
};
|
|
903
1435
|
for (const entry of [...journal].reverse()) {
|
|
1436
|
+
let sourceOccupied = false;
|
|
904
1437
|
attempt(() => {
|
|
905
|
-
|
|
1438
|
+
const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
|
|
1439
|
+
// No marker-text fallback: the markers are public, so a concurrent updater's
|
|
1440
|
+
// wrapper carries them too. No recorded inode identity means not ours.
|
|
1441
|
+
const ownsWrapper = entry.wrapperWriteStarted
|
|
1442
|
+
&& wrapper !== null
|
|
1443
|
+
&& entry.writtenWrapperFingerprint !== undefined
|
|
1444
|
+
&& sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1445
|
+
if (ownsWrapper) {
|
|
906
1446
|
unlinkSync(entry.operation.file.wrapperPath);
|
|
1447
|
+
} else {
|
|
1448
|
+
try {
|
|
1449
|
+
lstatSync(entry.operation.sourcePath);
|
|
1450
|
+
sourceOccupied = true;
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
|
|
1453
|
+
}
|
|
907
1454
|
}
|
|
908
1455
|
});
|
|
909
1456
|
attempt(() => {
|
|
910
1457
|
if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
|
|
911
|
-
|
|
1458
|
+
const movedReplacement = stableShimPathProbe(entry.operation.file.backupPath);
|
|
1459
|
+
if (!movedReplacement || !entry.movedReplacementFingerprint
|
|
1460
|
+
|| !sameFingerprint(movedReplacement.fingerprint, entry.movedReplacementFingerprint)) {
|
|
1461
|
+
throw new Error("Codex shim guarded refresh backup changed during rollback");
|
|
1462
|
+
}
|
|
1463
|
+
if (sourceOccupied) unlinkSync(entry.operation.file.backupPath);
|
|
1464
|
+
else renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
|
|
912
1465
|
}
|
|
913
1466
|
});
|
|
914
1467
|
attempt(() => {
|
|
@@ -928,6 +1481,8 @@ function applyGuardedRefreshTransaction(
|
|
|
928
1481
|
const journal: GuardedRefreshJournalEntry[] = [];
|
|
929
1482
|
let applyError: Error | null = null;
|
|
930
1483
|
let fingerprintMismatch = false;
|
|
1484
|
+
let unsafeLauncher = false;
|
|
1485
|
+
let wrapperChangedDuringProbe = false;
|
|
931
1486
|
const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
|
|
932
1487
|
|
|
933
1488
|
for (const [index, operation] of operations.entries()) {
|
|
@@ -951,15 +1506,43 @@ function applyGuardedRefreshTransaction(
|
|
|
951
1506
|
}
|
|
952
1507
|
renameSync(operation.sourcePath, operation.file.backupPath);
|
|
953
1508
|
entry.replacementMovedToBackup = true;
|
|
1509
|
+
const movedReplacement = stableShimPathProbe(operation.file.backupPath);
|
|
1510
|
+
if (!movedReplacement) throw new Error("Codex shim guarded refresh could not fingerprint the staged launcher");
|
|
1511
|
+
entry.movedReplacementFingerprint = movedReplacement.fingerprint;
|
|
954
1512
|
entry.wrapperWriteStarted = true;
|
|
955
|
-
writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
1513
|
+
const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
1514
|
+
// Claim our own partial write before the hook can fail (see fresh install).
|
|
1515
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
1516
|
+
codexShimGuardedWriteHookForTests?.();
|
|
1517
|
+
// Re-check: unset means a concurrent writer owns the path now, so rollback
|
|
1518
|
+
// must leave it alone.
|
|
1519
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
|
|
1520
|
+
if (!entry.writtenWrapperFingerprint && !writtenInode) {
|
|
1521
|
+
throw new Error("Codex shim guarded refresh could not fingerprint the generated wrapper");
|
|
1522
|
+
}
|
|
956
1523
|
} catch (error) {
|
|
957
1524
|
applyError = error instanceof Error ? error : new Error(String(error));
|
|
958
1525
|
break;
|
|
959
1526
|
}
|
|
960
1527
|
}
|
|
961
1528
|
|
|
962
|
-
if (!fingerprintMismatch && !applyError
|
|
1529
|
+
if (!fingerprintMismatch && !applyError) {
|
|
1530
|
+
try {
|
|
1531
|
+
unsafeLauncher = probeUnixShimFiles(operations.map(operation => operation.file)) !== null;
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (!fingerprintMismatch && !applyError && !unsafeLauncher) {
|
|
1538
|
+
wrapperChangedDuringProbe = journal.some(entry => {
|
|
1539
|
+
const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
|
|
1540
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1541
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (!fingerprintMismatch && !applyError && !unsafeLauncher && !wrapperChangedDuringProbe && commitState) {
|
|
963
1546
|
try {
|
|
964
1547
|
commitState();
|
|
965
1548
|
} catch (error) {
|
|
@@ -967,7 +1550,7 @@ function applyGuardedRefreshTransaction(
|
|
|
967
1550
|
}
|
|
968
1551
|
}
|
|
969
1552
|
|
|
970
|
-
if (fingerprintMismatch || applyError) {
|
|
1553
|
+
if (fingerprintMismatch || applyError || unsafeLauncher || wrapperChangedDuringProbe) {
|
|
971
1554
|
const rollbackErrors = rollbackGuardedRefresh(journal);
|
|
972
1555
|
if (applyError || rollbackErrors.length > 0) {
|
|
973
1556
|
throw new AggregateError(
|
|
@@ -990,10 +1573,186 @@ function applyGuardedRefreshTransaction(
|
|
|
990
1573
|
return true;
|
|
991
1574
|
}
|
|
992
1575
|
|
|
1576
|
+
interface ObsoleteUnixShimJournalEntry {
|
|
1577
|
+
file: ShimFileState;
|
|
1578
|
+
stagedWrapperPath: string;
|
|
1579
|
+
priorWrapperFingerprint: ShimPathFingerprint;
|
|
1580
|
+
backingFingerprint: ShimPathFingerprint;
|
|
1581
|
+
writtenWrapperFingerprint?: ShimPathFingerprint;
|
|
1582
|
+
wrapperWriteStarted: boolean;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function rollbackObsoleteUnixShimRefresh(journal: readonly ObsoleteUnixShimJournalEntry[]): Error[] {
|
|
1586
|
+
const errors: Error[] = [];
|
|
1587
|
+
const attempt = (operation: () => void): void => {
|
|
1588
|
+
try {
|
|
1589
|
+
operation();
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
for (const entry of [...journal].reverse()) {
|
|
1595
|
+
attempt(() => {
|
|
1596
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1597
|
+
const ownsWrapper = entry.wrapperWriteStarted
|
|
1598
|
+
&& wrapper !== null
|
|
1599
|
+
&& (entry.writtenWrapperFingerprint
|
|
1600
|
+
? sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
|
|
1601
|
+
: wrapper.prefix.includes(UNIX_SHIM_REVISION_MARKER));
|
|
1602
|
+
if (ownsWrapper) unlinkSync(entry.file.wrapperPath);
|
|
1603
|
+
});
|
|
1604
|
+
attempt(() => {
|
|
1605
|
+
if (!existsSync(entry.stagedWrapperPath)) return;
|
|
1606
|
+
if (existsSync(entry.file.wrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1607
|
+
else renameSync(entry.stagedWrapperPath, entry.file.wrapperPath);
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
return errors;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
type ObsoleteUnixShimRefreshResult =
|
|
1614
|
+
| { installed: true; message: string }
|
|
1615
|
+
| { installed: false; deferred: boolean; message: string };
|
|
1616
|
+
|
|
1617
|
+
function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnixShimRefreshResult {
|
|
1618
|
+
if (process.platform === "win32") {
|
|
1619
|
+
return { installed: false, deferred: false, message: "Codex autostart shim is already current." };
|
|
1620
|
+
}
|
|
1621
|
+
const candidates = files.filter(file => {
|
|
1622
|
+
if (file.preserveOnly || file.wrapperPath !== file.originalPath || !existsSync(file.wrapperPath)) return false;
|
|
1623
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1624
|
+
return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
|
|
1625
|
+
});
|
|
1626
|
+
if (candidates.length === 0) {
|
|
1627
|
+
return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const journal: ObsoleteUnixShimJournalEntry[] = [];
|
|
1631
|
+
const transactionId = `${process.pid}-${randomUUID()}`;
|
|
1632
|
+
let applyError: Error | null = null;
|
|
1633
|
+
for (const [index, file] of candidates.entries()) {
|
|
1634
|
+
const wrapper = stableShimPathProbe(file.wrapperPath);
|
|
1635
|
+
const backing = stableShimPathProbe(file.backupPath);
|
|
1636
|
+
if (!wrapper || !wrapper.prefix.includes(SHIM_MARKER) || isCurrentUnixShimProbe(wrapper) || !backing) {
|
|
1637
|
+
applyError = new Error("Codex autostart shim upgrade inputs changed before regeneration");
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
const entry: ObsoleteUnixShimJournalEntry = {
|
|
1641
|
+
file,
|
|
1642
|
+
stagedWrapperPath: `${file.wrapperPath}.upgrade-${transactionId}-${index}`,
|
|
1643
|
+
priorWrapperFingerprint: wrapper.fingerprint,
|
|
1644
|
+
backingFingerprint: backing.fingerprint,
|
|
1645
|
+
wrapperWriteStarted: false,
|
|
1646
|
+
};
|
|
1647
|
+
journal.push(entry);
|
|
1648
|
+
try {
|
|
1649
|
+
renameSync(file.wrapperPath, entry.stagedWrapperPath);
|
|
1650
|
+
const stagedWrapper = stableShimPathProbe(entry.stagedWrapperPath);
|
|
1651
|
+
if (!stagedWrapper
|
|
1652
|
+
|| !sameFingerprintAfterRename(stagedWrapper.fingerprint, entry.priorWrapperFingerprint)) {
|
|
1653
|
+
throw new Error("Codex autostart shim upgrade could not fingerprint the staged wrapper");
|
|
1654
|
+
}
|
|
1655
|
+
entry.wrapperWriteStarted = true;
|
|
1656
|
+
const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
1657
|
+
const writtenWrapper = stableShimPathProbe(file.wrapperPath);
|
|
1658
|
+
if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) {
|
|
1659
|
+
throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper");
|
|
1660
|
+
}
|
|
1661
|
+
// Identity is the inode we created; a replacement that landed since the
|
|
1662
|
+
// rename leaves this unset so rollback treats the file as someone else's.
|
|
1663
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(file.wrapperPath, writtenInode);
|
|
1664
|
+
} catch (error) {
|
|
1665
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1671
|
+
let probeError: Error | null = null;
|
|
1672
|
+
if (!applyError) {
|
|
1673
|
+
try {
|
|
1674
|
+
unsafe = probeUnixShimFiles(candidates);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const changedDuringProbe = !applyError && !probeError && journal.some(entry => {
|
|
1680
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1681
|
+
const backing = stableShimPathProbe(entry.file.backupPath);
|
|
1682
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1683
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
|
|
1684
|
+
|| !backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
if (applyError || probeError || changedDuringProbe) {
|
|
1688
|
+
const rollbackErrors = rollbackObsoleteUnixShimRefresh(journal);
|
|
1689
|
+
if (applyError || probeError || rollbackErrors.length > 0) {
|
|
1690
|
+
throw new AggregateError(
|
|
1691
|
+
[...(applyError ? [applyError] : []), ...(probeError ? [probeError] : []), ...rollbackErrors],
|
|
1692
|
+
"Codex autostart shim upgrade failed",
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (unsafe) {
|
|
1699
|
+
const cleanupErrors: Error[] = [];
|
|
1700
|
+
for (const entry of [...journal].reverse()) {
|
|
1701
|
+
try {
|
|
1702
|
+
const wrapper = stableShimPathProbe(entry.file.wrapperPath);
|
|
1703
|
+
if (!wrapper || !entry.writtenWrapperFingerprint
|
|
1704
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)) {
|
|
1705
|
+
throw new Error("Codex autostart shim upgrade lost wrapper ownership before removal");
|
|
1706
|
+
}
|
|
1707
|
+
const backing = stableShimPathProbe(entry.file.backupPath);
|
|
1708
|
+
if (!backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint)) {
|
|
1709
|
+
throw new Error("Codex autostart shim upgrade backing launcher changed before restoration");
|
|
1710
|
+
}
|
|
1711
|
+
unlinkSync(entry.file.wrapperPath);
|
|
1712
|
+
renameSync(entry.file.backupPath, entry.file.originalPath);
|
|
1713
|
+
if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (cleanupErrors.length > 0) {
|
|
1719
|
+
throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade safety removal failed");
|
|
1720
|
+
}
|
|
1721
|
+
if (existsSync(statePath())) unlinkSync(statePath());
|
|
1722
|
+
return {
|
|
1723
|
+
installed: false,
|
|
1724
|
+
deferred: false,
|
|
1725
|
+
message: "Removed an obsolete Codex autostart shim because its saved launcher failed current validation. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.",
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const cleanupErrors: Error[] = [];
|
|
1730
|
+
for (const entry of journal) {
|
|
1731
|
+
try {
|
|
1732
|
+
if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade cleanup failed");
|
|
1738
|
+
return {
|
|
1739
|
+
installed: true,
|
|
1740
|
+
message: `Upgraded Codex autostart shim at ${candidates.map(file => file.wrapperPath).join(", ")} and validated the saved launcher.`,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
|
|
993
1744
|
function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
|
|
994
1745
|
const existing = readState();
|
|
995
1746
|
if (existing) {
|
|
996
1747
|
const files = stateFiles(existing);
|
|
1748
|
+
if (!options.expectedReplacements && process.platform !== "win32") {
|
|
1749
|
+
const hasObsoleteShim = files.some(file => {
|
|
1750
|
+
if (file.preserveOnly) return false;
|
|
1751
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1752
|
+
return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
|
|
1753
|
+
});
|
|
1754
|
+
if (hasObsoleteShim) return refreshObsoleteUnixShims(files);
|
|
1755
|
+
}
|
|
997
1756
|
if (options.expectedReplacements) {
|
|
998
1757
|
const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
|
|
999
1758
|
if (!operations || operations.length === 0) {
|
|
@@ -1060,9 +1819,92 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1060
1819
|
for (const target of targets) {
|
|
1061
1820
|
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
1062
1821
|
}
|
|
1822
|
+
const freshJournal: FreshShimInstallJournalEntry[] = [];
|
|
1823
|
+
let freshApplyError: Error | null = null;
|
|
1063
1824
|
for (const target of targets) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1825
|
+
const entry: FreshShimInstallJournalEntry = {
|
|
1826
|
+
target,
|
|
1827
|
+
originalMovedToBackup: false,
|
|
1828
|
+
wrapperWriteStarted: false,
|
|
1829
|
+
};
|
|
1830
|
+
freshJournal.push(entry);
|
|
1831
|
+
try {
|
|
1832
|
+
if (existsSync(target.originalPath)) {
|
|
1833
|
+
renameSync(target.originalPath, target.backupPath);
|
|
1834
|
+
entry.originalMovedToBackup = true;
|
|
1835
|
+
if (process.platform !== "win32") {
|
|
1836
|
+
const movedOriginal = stableShimPathProbe(target.backupPath);
|
|
1837
|
+
if (!movedOriginal) throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
|
|
1838
|
+
entry.movedOriginalFingerprint = movedOriginal.fingerprint;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (!target.preserveOnly) {
|
|
1842
|
+
entry.wrapperWriteStarted = true;
|
|
1843
|
+
const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
|
|
1844
|
+
entry.writtenWrapperInode = writtenInode;
|
|
1845
|
+
codexShimFreshWriteHookForTests?.();
|
|
1846
|
+
if (process.platform !== "win32") {
|
|
1847
|
+
if (!writtenInode) throw new Error("Codex shim fresh install could not fingerprint the generated wrapper");
|
|
1848
|
+
entry.writtenWrapperFingerprint = ownedWrapperFingerprint(target.wrapperPath, writtenInode);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
freshApplyError = error instanceof Error ? error : new Error(String(error));
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (process.platform !== "win32") {
|
|
1857
|
+
if (freshApplyError) {
|
|
1858
|
+
try {
|
|
1859
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1860
|
+
} catch (rollbackError) {
|
|
1861
|
+
throw new AggregateError([freshApplyError, rollbackError], "Codex shim installation and rollback failed");
|
|
1862
|
+
}
|
|
1863
|
+
throw freshApplyError;
|
|
1864
|
+
}
|
|
1865
|
+
let unsafe: UnixShimProbeResult = null;
|
|
1866
|
+
let probeError: Error | null = null;
|
|
1867
|
+
try {
|
|
1868
|
+
unsafe = probeUnixShimFiles(targets);
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
probeError = error instanceof Error ? error : new Error(String(error));
|
|
1871
|
+
}
|
|
1872
|
+
if (probeError) {
|
|
1873
|
+
try {
|
|
1874
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1875
|
+
} catch (rollbackError) {
|
|
1876
|
+
throw new AggregateError([probeError, rollbackError], "Codex shim probe and install rollback failed");
|
|
1877
|
+
}
|
|
1878
|
+
throw probeError;
|
|
1879
|
+
}
|
|
1880
|
+
const wrapperChangedDuringProbe = freshJournal.some(entry => {
|
|
1881
|
+
if (entry.target.preserveOnly) return false;
|
|
1882
|
+
const wrapper = stableShimPathProbe(entry.target.wrapperPath);
|
|
1883
|
+
return !wrapper || !entry.writtenWrapperFingerprint
|
|
1884
|
+
|| !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
|
|
1885
|
+
});
|
|
1886
|
+
if (unsafe || wrapperChangedDuringProbe) {
|
|
1887
|
+
rollbackFreshShimInstall(freshJournal);
|
|
1888
|
+
const reason = wrapperChangedDuringProbe
|
|
1889
|
+
? "the generated wrapper changed during its validation probe"
|
|
1890
|
+
: unsafe === "recursive"
|
|
1891
|
+
? "the saved launcher resolved back to the generated shim"
|
|
1892
|
+
: unsafe === "timeout"
|
|
1893
|
+
? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
|
|
1894
|
+
: unsafe === "descendants"
|
|
1895
|
+
? "the saved launcher left background descendants running after --version"
|
|
1896
|
+
: unsafe === "cleanup"
|
|
1897
|
+
? "the saved launcher's probe process group could not be terminated cleanly"
|
|
1898
|
+
: "the saved launcher failed its --version probe";
|
|
1899
|
+
return {
|
|
1900
|
+
installed: false,
|
|
1901
|
+
message: wrapperChangedDuringProbe
|
|
1902
|
+
? `Refusing Codex autostart shim because ${reason}. The concurrent launcher was preserved, and your previous launcher was kept alongside it as \`<codex>.opencodex-real\`; retry after the Codex update finishes, and remove that backup once you are satisfied the launcher on PATH is the one you want.`
|
|
1903
|
+
: `Refusing Codex autostart shim because ${reason}. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.`,
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
} else if (freshApplyError) {
|
|
1907
|
+
throw freshApplyError;
|
|
1066
1908
|
}
|
|
1067
1909
|
writeState(primaryState(targets));
|
|
1068
1910
|
return {
|
|
@@ -1095,6 +1937,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1095
1937
|
|
|
1096
1938
|
const files = stateFiles(state);
|
|
1097
1939
|
const replacementProbes = new Map<string, StableShimPathProbe>();
|
|
1940
|
+
const obsoleteShimProbes = new Map<string, StableShimPathProbe>();
|
|
1098
1941
|
const seen = new Set<string>();
|
|
1099
1942
|
let healthyCount = 0;
|
|
1100
1943
|
for (const file of files) {
|
|
@@ -1109,15 +1952,19 @@ export function autoRestoreCodexShim(options: {
|
|
|
1109
1952
|
if (!probe) return { status: "deferred" };
|
|
1110
1953
|
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
1111
1954
|
if (!isHealthyShimProbe(probe, state.platform)) return { status: "ineligible" };
|
|
1955
|
+
if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
|
|
1956
|
+
obsoleteShimProbes.set(file.wrapperPath, probe);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1112
1959
|
healthyCount += 1;
|
|
1113
1960
|
continue;
|
|
1114
1961
|
}
|
|
1115
1962
|
replacementProbes.set(file.wrapperPath, probe);
|
|
1116
1963
|
}
|
|
1117
1964
|
|
|
1118
|
-
if (replacementProbes.size === 0) return { status: "healthy" };
|
|
1965
|
+
if (replacementProbes.size === 0 && obsoleteShimProbes.size === 0) return { status: "healthy" };
|
|
1119
1966
|
if (!options.enabled()) return { status: "disabled" };
|
|
1120
|
-
if (files.length > 1 && healthyCount > 0) {
|
|
1967
|
+
if (files.length > 1 && (healthyCount > 0 || (replacementProbes.size > 0 && obsoleteShimProbes.size > 0))) {
|
|
1121
1968
|
return {
|
|
1122
1969
|
status: "deferred",
|
|
1123
1970
|
message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
|
|
@@ -1129,6 +1976,19 @@ export function autoRestoreCodexShim(options: {
|
|
|
1129
1976
|
try {
|
|
1130
1977
|
options.afterRestoreLockAcquired?.();
|
|
1131
1978
|
(options.stabilitySleep ?? Bun.sleepSync)(CODEX_SHIM_REPLACEMENT_STABLE_MS);
|
|
1979
|
+
if (obsoleteShimProbes.size > 0) {
|
|
1980
|
+
for (const [path, firstProbe] of obsoleteShimProbes) {
|
|
1981
|
+
const secondProbe = stableShimPathProbe(path);
|
|
1982
|
+
if (!secondProbe || isCurrentUnixShimProbe(secondProbe)
|
|
1983
|
+
|| !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
|
|
1984
|
+
}
|
|
1985
|
+
const result = refreshObsoleteUnixShims(files);
|
|
1986
|
+
return result.installed
|
|
1987
|
+
? { status: "restored", message: result.message }
|
|
1988
|
+
: result.deferred
|
|
1989
|
+
? { status: "deferred", message: result.message }
|
|
1990
|
+
: { status: "ineligible", message: result.message };
|
|
1991
|
+
}
|
|
1132
1992
|
const expectedReplacements = new Map<string, ShimPathFingerprint>();
|
|
1133
1993
|
for (const [path, firstProbe] of replacementProbes) {
|
|
1134
1994
|
const secondProbe = stableShimPathProbe(path);
|