@bitkyc08/opencodex 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
package/src/codex/shim.ts CHANGED
@@ -1,306 +1,73 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { spawnSync } from "node:child_process";
3
- import { tmpdir } from "node:os";
4
- import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
5
2
  import {
6
3
  chmodSync,
7
- closeSync,
8
4
  existsSync,
9
- fstatSync,
10
5
  lstatSync,
11
- linkSync,
12
- mkdirSync,
13
- mkdtempSync,
14
- openSync,
15
6
  readFileSync,
16
- readdirSync,
17
- readlinkSync,
18
- readSync,
19
7
  renameSync,
20
- rmSync,
21
- rmdirSync,
22
- statSync,
23
- symlinkSync,
24
- type Stats,
25
8
  unlinkSync,
26
9
  writeFileSync,
27
10
  } from "node:fs";
28
- import { getConfigDir } from "../config";
29
- import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "../lib/bun-runtime";
11
+ import { basename, delimiter, dirname, extname, join, posix } from "node:path";
12
+ import { durableBunRuntime } from "../lib/bun-runtime";
30
13
  import type { BunRuntimeSource } from "../lib/bun-runtime";
31
- import { isProcessAlive } from "../lib/process-control";
32
14
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
33
- import { recordOwnedConfigPath } from "../lib/config-ownership";
34
- import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
35
15
  import { isWslRuntime, wslAutomountRoot } from "./home";
36
16
  import { truncateRetainedUtf8 } from "../lib/admission";
17
+ import {
18
+ buildUnixCodexShim,
19
+ buildWindowsCodexShim,
20
+ buildWindowsPowerShellCodexShim,
21
+ gitBashPath,
22
+ SHIM_MARKER,
23
+ UNIX_SHIM_REVISION_MARKER,
24
+ } from "./shim-templates";
25
+ import {
26
+ hasUsableBackingPath,
27
+ isCurrentUnixShimProbe,
28
+ isHealthyShimProbe,
29
+ isVersionManagerOwnedCodexPath,
30
+ restoreWithoutReplacing,
31
+ sameFingerprint,
32
+ sameFingerprintAfterRename,
33
+ sameStableShimPathProbe,
34
+ shimPathFingerprint,
35
+ stableShimPathProbe,
36
+ type ShimPathFingerprint,
37
+ type StableShimPathProbe,
38
+ } from "./shim-fingerprint";
39
+ import {
40
+ fileErrorCode,
41
+ readState,
42
+ readStateResult,
43
+ stateFiles,
44
+ statePath,
45
+ writeState,
46
+ type ShimFileState,
47
+ type ShimState,
48
+ } from "./shim-state-file";
49
+ import {
50
+ CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS,
51
+ MAX_DIAGNOSTIC_VALUE_BYTES,
52
+ probeUnixShimFiles,
53
+ type UnixShimProbeResult,
54
+ } from "./shim-probe";
55
+ import { tryAcquireShimRestoreLock } from "./shim-restore-lock";
56
+
57
+ export { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim } from "./shim-templates";
58
+ export { isVersionManagerOwnedCodexPath } from "./shim-fingerprint";
59
+ export { CODEX_SHIM_STATE_MAX_BYTES } from "./shim-state-file";
60
+ export { setCodexShimProbeHookForTests, setCodexShimProbeShellForTests, setCodexShimProbeObservationMsForTests } from "./shim-probe";
61
+ export type { CodexShimBackingForCommand } from "./shim-inspect";
62
+ export { isLocalAbsoluteInspectionPath, inspectCodexShimBackingForCommand } from "./shim-inspect";
37
63
 
38
- const SHIM_MARKER = "opencodex codex autostart shim";
39
- const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
40
- const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
41
64
  export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
42
- export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
43
- const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
44
- const CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS = 5_000;
45
- const CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS = 1_000;
46
- const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
47
- 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.";
48
- const CODEX_SHIM_INSTALL_PROBE_SCRIPT = `
49
- const { spawn } = require("node:child_process");
50
- const { readFileSync, writeFileSync } = require("node:fs");
51
- const [markerPath, reentryPath, groupPath, stderrPath, launcherShellPath, wrapperPath, timeoutRaw, stderrLimitRaw, stderrDrainRaw, observationRaw] = process.argv.slice(1);
52
- const timeoutMs = Number.parseInt(timeoutRaw, 10);
53
- const stderrLimit = Number.parseInt(stderrLimitRaw, 10);
54
- const stderrDrainMs = Number.parseInt(stderrDrainRaw, 10);
55
- const observationMs = Number.parseInt(observationRaw, 10);
56
- const probeStartedAt = Date.now();
57
- const stderrChunks = [];
58
- let stderrBytes = 0;
59
- let launcher;
60
- let probeLease;
61
- let timer;
62
- let stderrDrainTimer;
63
- let observationTimer;
64
- let reentryPollTimer;
65
- let marker = "";
66
- let finished = false;
67
-
68
- function writeExclusive(path, value) {
69
- writeFileSync(path, value, { flag: "wx", mode: 0o600 });
70
- }
71
-
72
- function appendStderr(value) {
73
- if (stderrBytes >= stderrLimit) return;
74
- const bytes = Buffer.from(value);
75
- const retained = bytes.subarray(0, stderrLimit - stderrBytes);
76
- stderrChunks.push(retained);
77
- stderrBytes += retained.byteLength;
78
- }
79
-
80
- function groupAlive() {
81
- if (!launcher || !launcher.pid) return false;
82
- try {
83
- process.kill(-launcher.pid, 0);
84
- return true;
85
- } catch (error) {
86
- return error && error.code !== "ESRCH";
87
- }
88
- }
89
-
90
- function killGroup() {
91
- if (!launcher || !launcher.pid) return;
92
- try { process.kill(-launcher.pid, "SIGKILL"); } catch (error) {
93
- if (!error || error.code !== "ESRCH") appendStderr(String(error));
94
- }
95
- }
96
-
97
- function setMarker(value) {
98
- if (marker) return;
99
- marker = value;
100
- try { writeExclusive(markerPath, value + "\\n"); } catch (error) { appendStderr(String(error)); }
101
- }
102
65
 
103
- function reentryDetected() {
104
- try { return readFileSync(reentryPath, "utf8").trim() === "recursive"; } catch { return false; }
105
- }
106
-
107
- function checkReentry() {
108
- if (finished || !reentryDetected()) return;
109
- setMarker("recursive");
110
- killGroup();
111
- finish(126);
112
- }
113
-
114
- function finish(status) {
115
- if (finished) return;
116
- finished = true;
117
- if (timer) clearTimeout(timer);
118
- if (stderrDrainTimer) clearTimeout(stderrDrainTimer);
119
- if (observationTimer) clearTimeout(observationTimer);
120
- if (reentryPollTimer) clearInterval(reentryPollTimer);
121
- if (!marker && reentryDetected()) setMarker("recursive");
122
- if (!marker && groupAlive()) {
123
- setMarker("descendants");
124
- killGroup();
125
- }
126
- try { writeExclusive(stderrPath, Buffer.concat(stderrChunks)); } catch { /* parent fails closed */ }
127
- process.exit(marker === "timeout" ? 124 : marker === "descendants" ? 125 : marker === "recursive" ? 126 : status);
128
- }
129
-
130
- function finishAfterStderr(status) {
131
- if (finished) return;
132
- if (timer) {
133
- clearTimeout(timer);
134
- timer = undefined;
135
- }
136
- if (!launcher || !launcher.stderr || !probeLease) {
137
- finish(status);
138
- return;
139
- }
140
- let stderrEnded = launcher.stderr.readableEnded;
141
- let leaseEnded = probeLease.readableEnded;
142
- let observationElapsed = false;
143
- const finishWhenReady = () => {
144
- if (stderrEnded && leaseEnded && observationElapsed) finish(status);
145
- };
146
- launcher.stderr.once("end", () => {
147
- stderrEnded = true;
148
- finishWhenReady();
149
- });
150
- probeLease.once("end", () => {
151
- leaseEnded = true;
152
- finishWhenReady();
153
- });
154
- stderrDrainTimer = setTimeout(() => {
155
- stderrEnded = true;
156
- if (!marker && groupAlive()) {
157
- setMarker("descendants");
158
- killGroup();
159
- finish(125);
160
- return;
161
- }
162
- finishWhenReady();
163
- }, stderrDrainMs);
164
- const remainingObservationMs = Math.max(0, observationMs - (Date.now() - probeStartedAt));
165
- observationTimer = setTimeout(() => {
166
- observationElapsed = true;
167
- if (!leaseEnded) {
168
- setMarker(groupAlive() ? "descendants" : "timeout");
169
- killGroup();
170
- finish(marker === "descendants" ? 125 : 124);
171
- return;
172
- }
173
- finishWhenReady();
174
- }, remainingObservationMs);
175
- finishWhenReady();
176
- }
177
-
178
- try {
179
- launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
180
- detached: true,
181
- env: process.env,
182
- stdio: ["ignore", "ignore", "pipe", "pipe"],
183
- });
184
- if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
185
- probeLease = launcher.stdio[3];
186
- if (!probeLease) throw new Error("Codex shim probe launcher has no descendant lease pipe");
187
- writeExclusive(groupPath, String(launcher.pid) + "\\n");
188
- launcher.stderr.on("data", appendStderr);
189
- reentryPollTimer = setInterval(checkReentry, 10);
190
- launcher.once("error", error => {
191
- appendStderr(String(error));
192
- finishAfterStderr(127);
193
- });
194
- launcher.once("exit", code => finishAfterStderr(Number.isInteger(code) ? code : 127));
195
- timer = setTimeout(() => {
196
- setMarker("timeout");
197
- killGroup();
198
- finish(124);
199
- }, timeoutMs);
200
- } catch (error) {
201
- appendStderr(String(error));
202
- killGroup();
203
- finish(127);
204
- }
205
- `;
206
- const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
207
66
  let lastShimDiscoveryError: string | null = null;
208
67
  /** Last human-readable reason discovery returned null (exposed for doctor/tests). */
209
68
  export function lastCodexDiscoveryError(): string | null {
210
69
  return lastShimDiscoveryError;
211
70
  }
212
- const CODEX_INTERNAL_COMMANDS = [
213
- "app-server",
214
- "archive",
215
- "apply",
216
- "cloud",
217
- "completion",
218
- "debug",
219
- "delete",
220
- "doctor",
221
- "exec-server",
222
- "features",
223
- "fork",
224
- "help",
225
- "login",
226
- "logout",
227
- "mcp",
228
- "plugin",
229
- "sandbox",
230
- "unarchive",
231
- "update",
232
- ];
233
-
234
- // Codex accepts global options before a subcommand. The shim must skip the value belonging to
235
- // these options before it decides which first positional token is the real subcommand. Keep this
236
- // list aligned with `codex --help`; `--option=value` and attached short forms stay one token.
237
- const CODEX_GLOBAL_OPTIONS_WITH_VALUE = [
238
- "-c", "--config",
239
- "--enable", "--disable",
240
- "--remote", "--remote-auth-token-env",
241
- "-i", "--image",
242
- "-m", "--model",
243
- "--local-provider",
244
- "-p", "--profile",
245
- "-s", "--sandbox",
246
- "-C", "--cd",
247
- "--add-dir",
248
- "-a", "--ask-for-approval",
249
- ];
250
-
251
- interface ShimState {
252
- platform: NodeJS.Platform;
253
- wrapperPath: string;
254
- originalPath: string;
255
- backupPath: string;
256
- wrappers?: ShimFileState[];
257
- }
258
-
259
- interface ShimFileState {
260
- wrapperPath: string;
261
- originalPath: string;
262
- backupPath: string;
263
- realPath?: string;
264
- preserveOnly?: boolean;
265
- }
266
-
267
- export type CodexShimBackingForCommand =
268
- | Readonly<{ status: "not-tracked" }>
269
- | Readonly<{
270
- status: "matched";
271
- selectedRole: "wrapper" | "backing";
272
- backingPath: string;
273
- backingKind: "backup" | "real";
274
- }>
275
- | Readonly<{
276
- status: "unknown";
277
- reason:
278
- | "state_invalid"
279
- | "platform_mismatch"
280
- | "ambiguous_match"
281
- | "preserve_only"
282
- | "backing_missing"
283
- | "backing_mismatch"
284
- | "binding_unavailable"
285
- | "wrapper_unhealthy"
286
- | "version_manager_refused";
287
- }>;
288
-
289
- interface ShimPathFingerprint {
290
- dev: number;
291
- ino: number;
292
- kind: "file" | "symlink";
293
- mode: number;
294
- size: number;
295
- mtimeMs: number;
296
- ctimeMs: number;
297
- target?: Omit<ShimPathFingerprint, "target">;
298
- }
299
-
300
- interface StableShimPathProbe {
301
- fingerprint: ShimPathFingerprint;
302
- prefix: string;
303
- }
304
71
 
305
72
  interface InstallCodexShimInternalOptions {
306
73
  expectedReplacements?: ReadonlyMap<string, ShimPathFingerprint>;
@@ -348,166 +115,6 @@ function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
348
115
  }
349
116
  }
350
117
 
351
- function readShimProbePrefix(path: string): string {
352
- const fd = openSync(path, "r");
353
- try {
354
- const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
355
- const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
356
- return buffer.toString("utf8", 0, bytesRead);
357
- } finally {
358
- closeSync(fd);
359
- }
360
- }
361
-
362
- function statFingerprint(path: string, follow: boolean): Omit<ShimPathFingerprint, "target"> | null {
363
- try {
364
- const stat = follow ? statSync(path) : lstatSync(path);
365
- if (follow ? !stat.isFile() : (!stat.isFile() && !stat.isSymbolicLink())) return null;
366
- return {
367
- dev: stat.dev,
368
- ino: stat.ino,
369
- kind: stat.isSymbolicLink() ? "symlink" : "file",
370
- mode: stat.mode,
371
- size: stat.size,
372
- mtimeMs: stat.mtimeMs,
373
- ctimeMs: stat.ctimeMs,
374
- };
375
- } catch {
376
- return null;
377
- }
378
- }
379
-
380
- function sameFingerprint(
381
- left: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
382
- right: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
383
- ): boolean {
384
- return left.dev === right.dev
385
- && left.ino === right.ino
386
- && left.kind === right.kind
387
- && left.mode === right.mode
388
- && left.size === right.size
389
- && left.mtimeMs === right.mtimeMs
390
- && left.ctimeMs === right.ctimeMs
391
- && (!("target" in left) || !("target" in right)
392
- ? true
393
- : left.target === undefined && right.target === undefined
394
- ? true
395
- : left.target !== undefined && right.target !== undefined
396
- ? sameFingerprint(left.target, right.target)
397
- : false);
398
- }
399
-
400
- function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
401
- // rename changes the outer directory entry ctime on macOS; every other field,
402
- // including a symlink target fingerprint, must remain identical.
403
- return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
404
- }
405
-
406
- function stableShimPathProbe(path: string): StableShimPathProbe | null {
407
- const before = statFingerprint(path, false);
408
- if (!before) return null;
409
- const targetBefore = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
410
- if (before.kind === "symlink" && !targetBefore) return null;
411
- let prefix: string;
412
- try {
413
- prefix = readShimProbePrefix(path);
414
- } catch {
415
- return null;
416
- }
417
- const targetAfter = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
418
- const after = statFingerprint(path, false);
419
- if (!after || !sameFingerprint(before, after)) return null;
420
- if (before.kind === "symlink") {
421
- if (!targetBefore || !targetAfter || !sameFingerprint(targetBefore, targetAfter)) return null;
422
- }
423
- const fingerprint: ShimPathFingerprint = {
424
- ...before,
425
- ...(targetBefore ? { target: targetBefore } : {}),
426
- };
427
- const contentSize = fingerprint.target?.size ?? fingerprint.size;
428
- return contentSize > 0 ? { fingerprint, prefix } : null;
429
- }
430
-
431
- function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean {
432
- return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
433
- }
434
-
435
- /**
436
- * Identity of whatever sits at `path`, read from metadata alone.
437
- *
438
- * `stableShimPathProbe` answers a different question: it reads content to decide
439
- * whether a launcher looks like a healthy shim, and it deliberately returns null
440
- * for a zero-byte file. That makes it the wrong instrument for rollback
441
- * bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
442
- * install moves it aside before writing our wrapper; if the move is recorded
443
- * without a fingerprint, rollback cannot prove the backup is still the file it
444
- * set aside and refuses to restore it — the launcher stays lost (#1625).
445
- *
446
- * Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
447
- * re-reads them to reject a path that changed under us, following a symlink to
448
- * fingerprint its target as well.
449
- */
450
- function shimPathFingerprint(path: string): ShimPathFingerprint | null {
451
- const before = statFingerprint(path, false);
452
- if (!before) return null;
453
- if (before.kind !== "symlink") {
454
- const after = statFingerprint(path, false);
455
- return after && sameFingerprint(before, after) ? before : null;
456
- }
457
- const targetBefore = statFingerprint(path, true);
458
- if (!targetBefore) return null;
459
- const targetAfter = statFingerprint(path, true);
460
- const after = statFingerprint(path, false);
461
- if (!targetAfter || !after
462
- || !sameFingerprint(targetBefore, targetAfter)
463
- || !sameFingerprint(before, after)) return null;
464
- return { ...before, target: targetBefore };
465
- }
466
-
467
- /**
468
- * Move `from` onto `to` without ever replacing an existing entry.
469
- *
470
- * `renameSync` silently clobbers the destination on POSIX, which is wrong for a
471
- * rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
472
- * a concurrent installer can publish its own launcher at the original path in
473
- * between, and the restore would delete it. `link` fails EEXIST instead, which
474
- * is the no-replace primitive we need and needs no native helper.
475
- *
476
- * `link` follows a symlink to its target rather than preserving the link, so a
477
- * symlink launcher is republished with `symlink`, which is also no-replace: it
478
- * fails EEXIST on an occupied destination. Checking existence and then renaming
479
- * would reintroduce exactly the race this function exists to close.
480
- */
481
- function restoreWithoutReplacing(from: string, to: string): void {
482
- const source = lstatSync(from);
483
- if (source.isSymbolicLink()) {
484
- symlinkSync(readlinkSync(from), to);
485
- unlinkSync(from);
486
- return;
487
- }
488
- linkSync(from, to);
489
- unlinkSync(from);
490
- }
491
-
492
- function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
493
- if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
494
- const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
495
- return platform === "win32" || (mode & 0o111) !== 0;
496
- }
497
-
498
- function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
499
- return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
500
- }
501
-
502
- function hasUsableBackingPath(file: ShimFileState): boolean {
503
- return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
504
- .some(path => {
505
- if (!path) return false;
506
- const fingerprint = statFingerprint(path, true);
507
- return fingerprint !== null && fingerprint.size > 0;
508
- });
509
- }
510
-
511
118
  /**
512
119
  * A PATH entry that reaches Windows through WSL drive interop
513
120
  * (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
@@ -625,33 +232,6 @@ function backupPathFor(path: string): string {
625
232
  return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
626
233
  }
627
234
 
628
- /**
629
- * True when a Codex binary lives inside a version manager's install tree.
630
- *
631
- * These trees are rewritten in place on upgrade, which destroys both the shim
632
- * and the sibling .opencodex-real backup it restores from (#2412). The tempting
633
- * repair — adopt the newly installed binary as a fresh original — is wrong
634
- * twice: it records a provenance that never happened, and the next upgrade wipes
635
- * it again, so the repair silently un-repairs on the version manager's schedule.
636
- *
637
- * Scope is the three managers named in the report. nvm/fnm/npm-prefix are
638
- * deliberately excluded: a false positive here refuses a restore that would
639
- * otherwise be correct.
640
- */
641
- export function isVersionManagerOwnedCodexPath(
642
- path: string,
643
- platform: NodeJS.Platform = process.platform,
644
- ): boolean {
645
- const normalized = (platform === "win32"
646
- ? win32.normalize(path).replace(/\\/g, "/")
647
- : posix.normalize(path)).toLowerCase();
648
- return normalized.includes("/mise/installs/")
649
- || normalized.includes("/mise/shims/")
650
- || normalized.includes("/.asdf/installs/")
651
- || normalized.includes("/.asdf/shims/")
652
- || normalized.includes("/.volta/");
653
- }
654
-
655
235
  /**
656
236
  * Why auto-restore refused, in the operator's own terms. Auto-restore used to
657
237
  * return a bare `{ status: "ineligible" }`, and the CLI warns only when a
@@ -671,159 +251,9 @@ function destroyedShimMessage(file: ShimFileState): string {
671
251
  return `${base} This Codex binary is owned by a version manager (mise/asdf/volta), so opencodex will not wrap it as a new original — the next upgrade would overwrite the shim and its backup again. Route through Codex instead with 'ocx start', and use 'ocx service install' for autostart.`;
672
252
  }
673
253
 
674
- function shQuote(value: string): string {
675
- return `'${value.replace(/'/g, "'\\''")}'`;
676
- }
677
-
678
- // Provenance is required rather than defaulted: a default would let a caller pass an
679
- // override binary and silently label it something else, which is precisely the
680
- // path/marker disagreement this feature exists to prevent.
681
- //
682
- // The marker is scoped to the `ensure` invocation in every flavor below and is never
683
- // exported into the shim's own environment. A shim wraps the real `codex`, so an
684
- // exported marker would be inherited by Codex and everything it spawns — a shell that
685
- // then ran a *different* Bun directly would carry a provenance describing a binary it
686
- // is not executing.
687
- export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
688
- const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
689
- const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
690
- return `#!/usr/bin/env sh
691
- # ${SHIM_MARKER}
692
- # ${UNIX_SHIM_REVISION_MARKER}
693
- if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
694
- if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
695
- if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
696
- (umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
697
- fi
698
- printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
699
- exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
700
- fi
701
- OCX_SHIM_PROBE_ACTIVE=1
702
- export OCX_SHIM_PROBE_ACTIVE
703
- fi
704
- if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
705
- printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
706
- exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
707
- fi
708
- case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
709
- 0)
710
- OCX_SHIM_ACTIVE_DEPTH=1
711
- ;;
712
- 1)
713
- OCX_SHIM_ACTIVE_DEPTH=2
714
- ;;
715
- *)
716
- printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
717
- exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
718
- ;;
719
- esac
720
- # Dynamic launchers such as mise exec -- codex may resolve the command name
721
- # back to this wrapper. An exec chain keeps the same PID. A legitimate nested
722
- # Codex invocation may enter once with a new PID; repeated child-process
723
- # redispatch reaches depth 2 and is rejected before it can form an infinite chain.
724
- OCX_SHIM_ACTIVE_PID=$$
725
- export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
726
- if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
727
- OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
728
- export OPENCODEX_API_AUTH_TOKEN
729
- fi
730
- ocx_subcommand=""
731
- ocx_skip_next=0
732
- for ocx_arg in "$@"; do
733
- if [ "$ocx_skip_next" -eq 1 ]; then
734
- ocx_skip_next=0
735
- continue
736
- fi
737
- case "$ocx_arg" in
738
- --)
739
- break
740
- ;;
741
- ${valueOptions})
742
- ocx_skip_next=1
743
- ;;
744
- --help|-h|--version|-V)
745
- ocx_subcommand="$ocx_arg"
746
- break
747
- ;;
748
- -*)
749
- ;;
750
- *)
751
- ocx_subcommand="$ocx_arg"
752
- break
753
- ;;
754
- esac
755
- done
756
- case "$ocx_subcommand" in
757
- ${internalCommands}|--help|-h|--version|-V)
758
- ;;
759
- *)
760
- if [ -z "$OCX_SHIM_BYPASS" ]; then
761
- ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
762
- fi
763
- ;;
764
- esac
765
- exec ${shQuote(realCodexPath)} "$@"
766
- `;
767
- }
768
-
769
- type UnixShimProbeCleanupPhase = "marker" | "reentry" | "group" | "stderr" | "group-id" | "termination" | "spawn" | "exception";
770
- interface UnixShimProbeCleanup {
771
- kind: "cleanup";
772
- phase: UnixShimProbeCleanupPhase;
773
- code: string;
774
- status: number | null;
775
- signal: string;
776
- }
777
- type UnixShimProbeResult = UnixShimProbeCleanup | "descendants" | "failed" | "recursive" | "timeout" | null;
778
-
779
- const SHIM_PROBE_ERROR_CODES = new Set([
780
- "EACCES", "EAGAIN", "EBADF", "ECANCELED", "EINTR", "EIO", "EMFILE", "ENFILE",
781
- "ENOENT", "ENOEXEC", "ENOMEM", "ENOSPC", "EPERM", "EPIPE", "ESRCH", "ETIMEDOUT", "ETXTBSY",
782
- ]);
783
- const SHIM_PROBE_SIGNALS = new Set([
784
- "SIGABRT", "SIGBUS", "SIGHUP", "SIGILL", "SIGINT", "SIGKILL", "SIGPIPE", "SIGQUIT",
785
- "SIGSEGV", "SIGTERM", "SIGTRAP", "SIGXCPU", "SIGXFSZ",
786
- ]);
787
-
788
- /** Diagnostics cross a CLI boundary: never stringify arbitrary errors or metadata. */
789
- function shimProbeCleanup(
790
- phase: UnixShimProbeCleanupPhase, error?: unknown, status?: unknown, signal?: unknown,
791
- ): UnixShimProbeCleanup {
792
- let code = error === undefined ? "none" : "unknown";
793
- if (error !== null && typeof error === "object") {
794
- try {
795
- const value = Object.getOwnPropertyDescriptor(error, "code")?.value;
796
- if (typeof value === "string" && SHIM_PROBE_ERROR_CODES.has(value)) code = value;
797
- } catch { /* hostile accessors/proxies cannot turn diagnostics into an exception */ }
798
- }
799
- return {
800
- kind: "cleanup", phase, code,
801
- status: typeof status === "number" && Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
802
- signal: typeof signal === "string" && SHIM_PROBE_SIGNALS.has(signal) ? signal : "none",
803
- };
804
- }
805
-
806
- let codexShimProbeHookForTests: (() => void) | null = null;
807
- let codexShimProbeShellForTests: string | null = null;
808
254
  let codexShimGuardedWriteHookForTests: (() => void) | null = null;
809
255
  let codexShimFreshWriteHookForTests: (() => void) | null = null;
810
256
  let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
811
- let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
812
-
813
- /** Narrow deterministic seam for transaction rollback tests. */
814
- export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
815
- codexShimProbeHookForTests = hook;
816
- }
817
-
818
- /** Selects a POSIX shell only for cross-shell probe regression tests. */
819
- export function setCodexShimProbeShellForTests(path: string | null): void {
820
- codexShimProbeShellForTests = path;
821
- }
822
-
823
- /** Shortens the successful-launcher observation window only for focused tests. */
824
- export function setCodexShimProbeObservationMsForTests(value: number | null): void {
825
- codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
826
- }
827
257
 
828
258
  /** Narrow deterministic seam for guarded partial-write rollback tests. */
829
259
  export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
@@ -849,136 +279,6 @@ export function setCodexShimRollbackRestoreHookForTests(
849
279
  codexShimRollbackRestoreHookForTests = hook;
850
280
  }
851
281
 
852
- function readProbeMetadata(path: string, maxBytes: number): string | null {
853
- try {
854
- if (!existsSync(path)) return "";
855
- const stat = lstatSync(path);
856
- if (!stat.isFile() || stat.size > maxBytes) return null;
857
- return readFileSync(path, "utf8").trim();
858
- } catch {
859
- return null;
860
- }
861
- }
862
-
863
- function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
864
- if (process.platform === "win32") return null;
865
- const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
866
- const markerPath = join(probeDir, "result");
867
- const reentryPath = join(probeDir, "reentry");
868
- const groupPath = join(probeDir, "group");
869
- const stderrPath = join(probeDir, "stderr");
870
- const env: NodeJS.ProcessEnv = {
871
- ...process.env,
872
- OCX_SHIM_BYPASS: "1",
873
- OCX_SHIM_PROBE: "1",
874
- OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
875
- };
876
- delete env.OCX_SHIM_ACTIVE_PID;
877
- delete env.OCX_SHIM_ACTIVE_DEPTH;
878
- delete env.OCX_SHIM_PROBE_ACTIVE;
879
- let groupId = 0;
880
- let probeStatus: unknown;
881
- let probeSignal: unknown;
882
- try {
883
- chmodSync(probeDir, 0o700);
884
- const result = spawnSync(process.execPath, [
885
- "-e",
886
- CODEX_SHIM_INSTALL_PROBE_SCRIPT,
887
- markerPath,
888
- reentryPath,
889
- groupPath,
890
- stderrPath,
891
- codexShimProbeShellForTests ?? "/bin/sh",
892
- wrapperPath,
893
- String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
894
- String(MAX_DIAGNOSTIC_VALUE_BYTES),
895
- String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
896
- String(codexShimProbeObservationMs),
897
- ], {
898
- encoding: "utf8",
899
- env,
900
- timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
901
- killSignal: "SIGKILL",
902
- });
903
- probeStatus = result.status;
904
- probeSignal = result.signal;
905
- const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
906
- const marker = readProbeMetadata(markerPath, 64);
907
- const reentryMarker = readProbeMetadata(reentryPath, 64);
908
- const groupText = readProbeMetadata(groupPath, 64);
909
- const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
910
- groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
911
- if (marker === null) return shimProbeCleanup("marker", result.error, probeStatus, probeSignal);
912
- if (reentryMarker === null) return shimProbeCleanup("reentry", result.error, probeStatus, probeSignal);
913
- if (groupText === null) return shimProbeCleanup("group", result.error, probeStatus, probeSignal);
914
- if (launcherStderr === null) return shimProbeCleanup("stderr", result.error, probeStatus, probeSignal);
915
- if (!Number.isInteger(groupId) || groupId <= 0) return shimProbeCleanup("group-id", result.error, probeStatus, probeSignal);
916
- const groupSurvived = unixProcessGroupAlive(groupId);
917
- if (timedOut || marker || reentryMarker || groupSurvived) {
918
- try {
919
- terminateUnixProcessGroup(groupId);
920
- } catch (error) {
921
- return shimProbeCleanup("termination", error, probeStatus, probeSignal);
922
- }
923
- }
924
- if (result.error && !timedOut) return shimProbeCleanup("spawn", result.error, probeStatus, probeSignal);
925
- if (timedOut || marker === "timeout") return "timeout";
926
- if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
927
- if (reentryMarker !== "") return shimProbeCleanup("reentry", undefined, probeStatus, probeSignal);
928
- if (marker === "descendants") return "descendants";
929
- if (groupSurvived) return "descendants";
930
- if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
931
- return "recursive";
932
- }
933
- if (result.status !== 0) return "failed";
934
- return null;
935
- } catch (error) {
936
- if (Number.isInteger(groupId) && groupId > 0) {
937
- try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
938
- }
939
- return shimProbeCleanup("exception", error, probeStatus, probeSignal);
940
- } finally {
941
- try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
942
- }
943
- }
944
-
945
- function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
946
- if (process.platform === "win32") return null;
947
- codexShimProbeHookForTests?.();
948
- return files
949
- .filter(file => !file.preserveOnly)
950
- .map(file => probeUnixShimInstall(file.wrapperPath))
951
- .find(result => result !== null) ?? null;
952
- }
953
-
954
- function unixProcessGroupAlive(groupId: number): boolean {
955
- try {
956
- process.kill(-groupId, 0);
957
- return true;
958
- } catch (error) {
959
- return (error as NodeJS.ErrnoException).code !== "ESRCH";
960
- }
961
- }
962
-
963
- function terminateUnixProcessGroup(groupId: number): void {
964
- let permissionError: unknown;
965
- try {
966
- process.kill(-groupId, "SIGKILL");
967
- } catch (error) {
968
- const code = (error as NodeJS.ErrnoException).code;
969
- if (code === "EPERM") permissionError = error;
970
- else if (code !== "ESRCH") throw error;
971
- }
972
- // A concurrently exiting group can briefly reject a second signal. Only
973
- // observed disappearance clears that uncertainty; never send another signal.
974
- const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
975
- while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
976
- if (unixProcessGroupAlive(groupId)) {
977
- if (permissionError) throw permissionError;
978
- throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
979
- }
980
- }
981
-
982
282
  interface FreshShimInstallJournalEntry {
983
283
  target: ShimFileState;
984
284
  movedOriginalFingerprint?: ShimPathFingerprint;
@@ -1047,374 +347,6 @@ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry
1047
347
  if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
1048
348
  }
1049
349
 
1050
- function windowsBatchValue(value: string): string {
1051
- return value
1052
- .replace(/%/g, "%%")
1053
- .replace(/\^/g, "^^")
1054
- .replace(/"/g, "")
1055
- .replace(/[\r\n]/g, "");
1056
- }
1057
-
1058
- function windowsBatchSet(name: string, value: string): string {
1059
- // Paths are rewritten to %USERPROFILE%-style env indirection: cmd.exe parses .cmd
1060
- // files in the OEM codepage, so a literal non-ASCII profile prefix (Korean/Chinese
1061
- // usernames) written as UTF-8 turns to mojibake. The env token expands natively in
1062
- // the right codepage at parse time; no `chcp` here — this shim runs in the USER's
1063
- // console and must not leak a codepage change into it.
1064
- return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
1065
- }
1066
-
1067
- export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
1068
- const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
1069
- const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
1070
- return `@echo off\r
1071
- rem ${SHIM_MARKER}\r
1072
- setlocal\r
1073
- ${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
1074
- ${windowsBatchSet("OCX_BUN", bunPath)}\r
1075
- ${windowsBatchSet("OCX_CLI", cliPath)}\r
1076
- ${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
1077
- if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
1078
- if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
1079
- goto scan_codex_args\r
1080
- :scan_codex_args\r
1081
- if "%~1"=="" goto ensure_ocx\r
1082
- if "%~1"=="--" goto ensure_ocx\r
1083
- ${valueOptionChecks}\r
1084
- ${internalCommandChecks}\r
1085
- if /I "%~1"=="--help" goto run_codex\r
1086
- if /I "%~1"=="-h" goto run_codex\r
1087
- if /I "%~1"=="--version" goto run_codex\r
1088
- if /I "%~1"=="-V" goto run_codex\r
1089
- set "OCX_SCAN_ARG=%~1"\r
1090
- if "%OCX_SCAN_ARG:~0,1%"=="-" goto shift_codex_arg\r
1091
- goto ensure_ocx\r
1092
- :skip_option_value\r
1093
- shift\r
1094
- if "%~1"=="" goto ensure_ocx\r
1095
- :shift_codex_arg\r
1096
- shift\r
1097
- goto scan_codex_args\r
1098
- :ensure_ocx\r
1099
- setlocal\r
1100
- ${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
1101
- ${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
1102
- "%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
1103
- endlocal\r
1104
- :run_codex\r
1105
- "%OCX_REAL_CODEX%" %*\r
1106
- `;
1107
- }
1108
-
1109
- function psString(value: string): string {
1110
- return `'${value.replace(/'/g, "''")}'`;
1111
- }
1112
-
1113
- export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
1114
- const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
1115
- const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
1116
- const tokenFile = serviceApiTokenFilePath();
1117
- return `#!/usr/bin/env pwsh
1118
- # ${SHIM_MARKER}
1119
- $hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN
1120
- $priorApiAuthToken = $env:OPENCODEX_API_AUTH_TOKEN
1121
- try {
1122
- if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
1123
- $env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
1124
- }
1125
- $internalCommands = @(${internalCommands})
1126
- $valueOptions = @(${valueOptions})
1127
- $subcommand = ""
1128
- $skipNext = $false
1129
- foreach ($argValue in $args) {
1130
- $argText = [string]$argValue
1131
- if ($skipNext) { $skipNext = $false; continue }
1132
- if ($argText -eq "--") { break }
1133
- if ($valueOptions -contains $argText) { $skipNext = $true; continue }
1134
- if (@("--help", "-h", "--version", "-V") -contains $argText) { $subcommand = $argText; break }
1135
- if ($argText.StartsWith("-")) { continue }
1136
- $subcommand = $argText
1137
- break
1138
- }
1139
- $skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
1140
- if (-not $skipEnsure) {
1141
- $priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
1142
- $priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
1143
- $env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
1144
- $env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
1145
- try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
1146
- finally {
1147
- if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
1148
- else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
1149
- if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
1150
- else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
1151
- }
1152
- }
1153
- & ${psString(realCodexPath)} @args
1154
- $codexExitCode = $LASTEXITCODE
1155
- } finally {
1156
- if ($hadApiAuthToken) { $env:OPENCODEX_API_AUTH_TOKEN = $priorApiAuthToken }
1157
- else { Remove-Item Env:\\OPENCODEX_API_AUTH_TOKEN -ErrorAction SilentlyContinue }
1158
- }
1159
- exit $codexExitCode
1160
- `;
1161
- }
1162
-
1163
- interface ShimStateReadResult {
1164
- state: ShimState | null;
1165
- present: boolean;
1166
- warning?: string;
1167
- }
1168
-
1169
- function fileErrorCode(error: unknown): string | undefined {
1170
- return error && typeof error === "object" && "code" in error
1171
- ? String((error as { code?: unknown }).code)
1172
- : undefined;
1173
- }
1174
-
1175
- function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
1176
- let lexicalBefore: Stats;
1177
- try {
1178
- lexicalBefore = lstatSync(path);
1179
- if (lexicalBefore.isSymbolicLink() || !lexicalBefore.isFile()) {
1180
- return { warning: `Codex shim state is not a direct regular file at ${path}; auto-restore skipped.` };
1181
- }
1182
- } catch (error) {
1183
- if (fileErrorCode(error) === "ENOENT") return null;
1184
- return { warning: `Codex shim state could not be inspected at ${path}.` };
1185
- }
1186
- let fd: number;
1187
- try {
1188
- fd = openSync(path, "r");
1189
- } catch (error) {
1190
- if (fileErrorCode(error) === "ENOENT") return null;
1191
- return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
1192
- }
1193
- try {
1194
- const before = fstatSync(fd);
1195
- if (!before.isFile()) return { warning: `Codex shim state is not a regular file at ${path}; auto-restore skipped.` };
1196
- if (before.size > maxBytes) {
1197
- return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
1198
- }
1199
- const buffer = Buffer.allocUnsafe(before.size);
1200
- let offset = 0;
1201
- while (offset < buffer.length) {
1202
- const bytesRead = readSync(fd, buffer, offset, buffer.length - offset, offset);
1203
- if (bytesRead === 0) return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
1204
- offset += bytesRead;
1205
- }
1206
- const extra = Buffer.allocUnsafe(1);
1207
- if (readSync(fd, extra, 0, 1, offset) !== 0) {
1208
- return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
1209
- }
1210
- const after = fstatSync(fd);
1211
- let lexicalAfter: Stats;
1212
- try {
1213
- lexicalAfter = lstatSync(path);
1214
- } catch {
1215
- return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
1216
- }
1217
- if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
1218
- || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
1219
- || lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
1220
- || lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino) {
1221
- return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
1222
- }
1223
- return { bytes: buffer, content: buffer.toString("utf8") };
1224
- } finally {
1225
- closeSync(fd);
1226
- }
1227
- }
1228
-
1229
- function readStateResult(path = statePath()): ShimStateReadResult {
1230
- const bounded = readBoundedRegularFile(path, CODEX_SHIM_STATE_MAX_BYTES);
1231
- if (!bounded) return { state: null, present: false };
1232
- if ("warning" in bounded) return { state: null, present: true, warning: bounded.warning };
1233
- try {
1234
- const value = JSON.parse(bounded.content) as unknown;
1235
- if (!value || typeof value !== "object") return { state: null, present: true };
1236
- const state = value as Record<string, unknown>;
1237
- if (typeof state.platform !== "string") return { state: null, present: true };
1238
- const validFile = (item: unknown): item is ShimFileState => {
1239
- if (!item || typeof item !== "object") return false;
1240
- const file = item as Record<string, unknown>;
1241
- return typeof file.wrapperPath === "string"
1242
- && typeof file.originalPath === "string"
1243
- && typeof file.backupPath === "string"
1244
- && (file.realPath === undefined || typeof file.realPath === "string")
1245
- && (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
1246
- };
1247
- if (state.wrappers !== undefined) {
1248
- if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null, present: true };
1249
- } else if (!validFile(state)) {
1250
- return { state: null, present: true };
1251
- }
1252
- return { state: state as unknown as ShimState, present: true };
1253
- } catch {
1254
- return { state: null, present: true };
1255
- }
1256
- }
1257
-
1258
- function readState(): ShimState | null {
1259
- return readStateResult().state;
1260
- }
1261
-
1262
- export function isLocalAbsoluteInspectionPath(path: string, platform: NodeJS.Platform): boolean {
1263
- if (platform !== "win32") return posix.isAbsolute(path);
1264
- const normalized = path.replace(/\//g, "\\");
1265
- // UNC and device namespaces can initiate remote I/O while a nominally local
1266
- // inspection is resolving user-controlled paths. Root-relative paths are
1267
- // drive-context dependent, so require an explicit local drive as well.
1268
- return win32.isAbsolute(path)
1269
- && /^[a-z]:\\/i.test(normalized)
1270
- && !normalized.startsWith("\\\\");
1271
- }
1272
-
1273
- function windowsShimInspectionIsDeferred(platform: NodeJS.Platform): boolean {
1274
- return platform === "win32";
1275
- }
1276
-
1277
- /** Resolve one selected command through already-recorded shim state, without repair. */
1278
- export function inspectCodexShimBackingForCommand(
1279
- selectedCommand: string,
1280
- platform: NodeJS.Platform = process.platform,
1281
- configDir: string = getConfigDir(),
1282
- ): CodexShimBackingForCommand {
1283
- // Pathname prechecks cannot prevent a writable Windows ancestor from being
1284
- // replaced with a remote reparse point before the later state/fingerprint
1285
- // reads. Keep the exported read-only helper fail-closed until those reads are
1286
- // performed through a handle-bound Windows provenance layer.
1287
- if (windowsShimInspectionIsDeferred(platform)) {
1288
- return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
1289
- }
1290
- if (!isLocalAbsoluteInspectionPath(configDir, platform)) {
1291
- return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
1292
- }
1293
- const stateFile = join(configDir, "codex-shim.json");
1294
- try {
1295
- const stateEntry = lstatSync(stateFile);
1296
- if (stateEntry.isSymbolicLink()) {
1297
- return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
1298
- }
1299
- } catch (error) {
1300
- if (fileErrorCode(error) !== "ENOENT") {
1301
- return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
1302
- }
1303
- }
1304
- const result = readStateResult(stateFile);
1305
- if (!result.state) {
1306
- return result.present
1307
- ? Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const })
1308
- : Object.freeze({ status: "not-tracked" as const });
1309
- }
1310
- const pathApi = platform === "win32" ? win32 : posix;
1311
- const samePath = (left: string, right: string): boolean => {
1312
- const normalizedLeft = pathApi.resolve(left);
1313
- const normalizedRight = pathApi.resolve(right);
1314
- return platform === "win32"
1315
- ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
1316
- : normalizedLeft === normalizedRight;
1317
- };
1318
- const files = stateFiles(result.state);
1319
- if (files.some(file => !file.wrapperPath || !file.originalPath || !file.backupPath
1320
- || ![file.wrapperPath, file.originalPath, file.backupPath, file.realPath]
1321
- .filter((path): path is string => typeof path === "string")
1322
- .every(path => isLocalAbsoluteInspectionPath(path, platform)))) {
1323
- return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
1324
- }
1325
- const wrapperKeys = files.map(file => platform === "win32"
1326
- ? pathApi.resolve(file.wrapperPath).toLowerCase()
1327
- : pathApi.resolve(file.wrapperPath));
1328
- if (new Set(wrapperKeys).size !== wrapperKeys.length) {
1329
- return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
1330
- }
1331
- const selectedFingerprint = shimPathFingerprint(selectedCommand);
1332
- if (!selectedFingerprint) {
1333
- return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
1334
- }
1335
- const selectedIdentity = selectedFingerprint.target ?? selectedFingerprint;
1336
- const sameEffectiveIdentity = (fingerprint: ShimPathFingerprint | null): boolean => {
1337
- if (!fingerprint) return false;
1338
- const identity = fingerprint.target ?? fingerprint;
1339
- return identity.dev === selectedIdentity.dev && identity.ino === selectedIdentity.ino;
1340
- };
1341
- const matches = files.flatMap(file => {
1342
- const backingPath = file.realPath ?? file.backupPath;
1343
- const roles: Array<"wrapper" | "backing"> = [];
1344
- if (samePath(file.wrapperPath, selectedCommand)
1345
- || sameEffectiveIdentity(shimPathFingerprint(file.wrapperPath))) {
1346
- roles.push("wrapper");
1347
- }
1348
- if (samePath(backingPath, selectedCommand)
1349
- || sameEffectiveIdentity(shimPathFingerprint(backingPath))) {
1350
- roles.push("backing");
1351
- }
1352
- return roles.map(selectedRole => ({ file, backingPath, selectedRole }));
1353
- });
1354
- if (matches.length === 0) return Object.freeze({ status: "not-tracked" as const });
1355
- if (result.state.platform !== platform) {
1356
- return Object.freeze({ status: "unknown" as const, reason: "platform_mismatch" as const });
1357
- }
1358
- if (matches.length !== 1) {
1359
- return Object.freeze({ status: "unknown" as const, reason: "ambiguous_match" as const });
1360
- }
1361
- const { file, backingPath, selectedRole } = matches[0]!;
1362
- if (file.preserveOnly === true) {
1363
- return Object.freeze({ status: "unknown" as const, reason: "preserve_only" as const });
1364
- }
1365
- const backing = statFingerprint(backingPath, true);
1366
- if (!backing || backing.size <= 0 || samePath(backingPath, file.wrapperPath)) {
1367
- return Object.freeze({ status: "unknown" as const, reason: "backing_missing" as const });
1368
- }
1369
- const wrapperProbe = stableShimPathProbe(file.wrapperPath);
1370
- if (!wrapperProbe || !isHealthyShimProbe(wrapperProbe, result.state.platform)) {
1371
- return Object.freeze({
1372
- status: "unknown" as const,
1373
- reason: isVersionManagerOwnedCodexPath(file.wrapperPath)
1374
- ? "version_manager_refused" as const
1375
- : "wrapper_unhealthy" as const,
1376
- });
1377
- }
1378
- const wrapperIdentity = wrapperProbe.fingerprint.target ?? wrapperProbe.fingerprint;
1379
- if (backing.dev === wrapperIdentity.dev && backing.ino === wrapperIdentity.ino) {
1380
- return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
1381
- }
1382
- const wrapperExt = extname(file.wrapperPath).toLowerCase();
1383
- const invokesBacking = platform !== "win32"
1384
- ? wrapperProbe.prefix.includes(`exec ${shQuote(backingPath)} "$@"`)
1385
- : wrapperExt === ".cmd" || wrapperExt === ".bat"
1386
- ? wrapperProbe.prefix.includes(windowsBatchSet("OCX_REAL_CODEX", backingPath))
1387
- && wrapperProbe.prefix.includes('"%OCX_REAL_CODEX%" %*')
1388
- : wrapperExt === ".ps1"
1389
- ? wrapperProbe.prefix.includes(`& ${psString(backingPath)} @args`)
1390
- : wrapperProbe.prefix.includes(`exec ${shQuote(gitBashPath(backingPath))} "$@"`);
1391
- if (!invokesBacking) {
1392
- return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
1393
- }
1394
- return Object.freeze({
1395
- status: "matched" as const,
1396
- selectedRole,
1397
- backingPath,
1398
- backingKind: file.realPath !== undefined ? "real" as const : "backup" as const,
1399
- });
1400
- }
1401
-
1402
- function statePath(): string {
1403
- return join(getConfigDir(), "codex-shim.json");
1404
- }
1405
-
1406
- function writeState(state: ShimState): void {
1407
- const path = statePath();
1408
- recordOwnedConfigPath(getConfigDir(), path);
1409
- if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1410
- writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
1411
- }
1412
-
1413
- /** Git-Bash accepts `C:/...` but not backslashed paths inside sh scripts. */
1414
- function gitBashPath(path: string): string {
1415
- return path.replace(/\\/g, "/");
1416
- }
1417
-
1418
350
  /**
1419
351
  * Write the wrapper and return the identity of the inode this call created, or
1420
352
  * `undefined` where the platform still writes the destination in place.
@@ -1519,12 +451,6 @@ function wrapperInodeIsOurs(
1519
451
  return written !== undefined;
1520
452
  }
1521
453
 
1522
- function stateFiles(state: ShimState): ShimFileState[] {
1523
- return state.wrappers?.length
1524
- ? state.wrappers
1525
- : [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
1526
- }
1527
-
1528
454
  function primaryState(files: ShimFileState[]): ShimState {
1529
455
  const first = files[0]!;
1530
456
  return { platform: process.platform, ...first, wrappers: files };
@@ -1611,152 +537,6 @@ interface GuardedRefreshJournalEntry {
1611
537
 
1612
538
  let guardedRefreshTransactionId = 0;
1613
539
 
1614
- interface ShimRestoreLock {
1615
- release(): void;
1616
- }
1617
-
1618
- interface ShimRestoreLockRecord {
1619
- version: 1;
1620
- token: string;
1621
- pid: number;
1622
- createdAt: number;
1623
- }
1624
-
1625
- interface ShimRestoreLockSnapshot {
1626
- record: ShimRestoreLockRecord;
1627
- ownerPath: string;
1628
- lockIdentity: Pick<Stats, "dev" | "ino">;
1629
- fingerprint: ShimPathFingerprint;
1630
- }
1631
-
1632
- function restoreLockPath(): string {
1633
- return join(getConfigDir(), "codex-shim.autorestore.lock");
1634
- }
1635
-
1636
- function sameFileIdentity(left: Pick<Stats, "dev" | "ino">, right: Pick<Stats, "dev" | "ino">): boolean {
1637
- return left.dev === right.dev && left.ino === right.ino;
1638
- }
1639
-
1640
- function readShimRestoreLockSnapshot(path: string): ShimRestoreLockSnapshot | null {
1641
- let lockIdentity: Stats;
1642
- let entries: string[];
1643
- try {
1644
- lockIdentity = lstatSync(path);
1645
- if (!lockIdentity.isDirectory()) return null;
1646
- entries = readdirSync(path);
1647
- } catch {
1648
- return null;
1649
- }
1650
- if (entries.length !== 1 || !entries[0].endsWith(".json")) return null;
1651
- const ownerPath = join(path, entries[0]);
1652
- const probe = stableShimPathProbe(ownerPath);
1653
- if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
1654
- try {
1655
- const value = JSON.parse(probe.prefix) as Partial<ShimRestoreLockRecord>;
1656
- if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
1657
- || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
1658
- || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
1659
- if (entries[0] !== `${value.token}.json`) return null;
1660
- const currentLockIdentity = lstatSync(path);
1661
- if (!currentLockIdentity.isDirectory() || !sameFileIdentity(lockIdentity, currentLockIdentity)) return null;
1662
- return {
1663
- record: value as ShimRestoreLockRecord,
1664
- ownerPath,
1665
- lockIdentity,
1666
- fingerprint: probe.fingerprint,
1667
- };
1668
- } catch {
1669
- return null;
1670
- }
1671
- }
1672
-
1673
- function sameShimRestoreLock(left: ShimRestoreLockSnapshot, right: ShimRestoreLockSnapshot): boolean {
1674
- return left.record.token === right.record.token
1675
- && sameFileIdentity(left.lockIdentity, right.lockIdentity)
1676
- && sameFingerprint(left.fingerprint, right.fingerprint);
1677
- }
1678
-
1679
- function reclaimStaleRestoreLock(path: string, beforeDelete?: () => void): boolean {
1680
- const observed = readShimRestoreLockSnapshot(path);
1681
- if (!observed) return false;
1682
- const createdAt = Math.max(observed.record.createdAt, observed.fingerprint.mtimeMs);
1683
- if (Date.now() - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
1684
- if (isProcessAlive(observed.record.pid)) return false;
1685
- const current = readShimRestoreLockSnapshot(path);
1686
- if (!current || !sameShimRestoreLock(observed, current)) return false;
1687
- beforeDelete?.();
1688
- try {
1689
- // The token is part of the owner filename. Even if the lock directory is
1690
- // replaced after the comparison, this unlink cannot target a successor's
1691
- // differently named owner record.
1692
- unlinkSync(observed.ownerPath);
1693
- rmdirSync(path);
1694
- return true;
1695
- } catch {
1696
- return false;
1697
- }
1698
- }
1699
-
1700
- function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreLock | null {
1701
- const dir = getConfigDir();
1702
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
1703
- const path = restoreLockPath();
1704
- for (let attempt = 0; attempt < 2; attempt += 1) {
1705
- let fd: number | null = null;
1706
- let identity: Stats | null = null;
1707
- let createdDirectory = false;
1708
- const record: ShimRestoreLockRecord = {
1709
- version: 1,
1710
- token: `${process.pid}-${Date.now()}-${randomUUID()}`,
1711
- pid: process.pid,
1712
- createdAt: Date.now(),
1713
- };
1714
- const ownerPath = join(path, `${record.token}.json`);
1715
- try {
1716
- mkdirSync(path, { mode: 0o700 });
1717
- createdDirectory = true;
1718
- fd = openSync(ownerPath, "wx", 0o600);
1719
- identity = fstatSync(fd);
1720
- writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf8");
1721
- identity = fstatSync(fd);
1722
- let released = false;
1723
- return {
1724
- release(): void {
1725
- if (released) return;
1726
- released = true;
1727
- try { closeSync(fd!); } catch { /* stale recovery handles an uncertain lock */ }
1728
- try {
1729
- const current = readShimRestoreLockSnapshot(path);
1730
- if (identity && current && current.record.token === record.token
1731
- && sameFileIdentity(identity, current.fingerprint)) {
1732
- unlinkSync(ownerPath);
1733
- rmdirSync(path);
1734
- }
1735
- } catch { /* stale recovery handles release failures */ }
1736
- },
1737
- };
1738
- } catch (error) {
1739
- if (fd !== null) {
1740
- try { closeSync(fd); } catch { /* best-effort close before ownership cleanup */ }
1741
- try {
1742
- const current = readShimRestoreLockSnapshot(path);
1743
- if (identity && current && current.record.token === record.token
1744
- && sameFileIdentity(identity, current.fingerprint)) {
1745
- unlinkSync(ownerPath);
1746
- rmdirSync(path);
1747
- }
1748
- } catch { /* leave an uncertain lock for stale recovery */ }
1749
- } else if (createdDirectory) {
1750
- try { rmdirSync(path); } catch { /* another owner exists or cleanup is uncertain */ }
1751
- }
1752
- if (fileErrorCode(error) !== "EEXIST") throw error;
1753
- if (attempt === 0 && reclaimStaleRestoreLock(path, beforeStaleDelete)) continue;
1754
- return null;
1755
- }
1756
- }
1757
- return null;
1758
- }
1759
-
1760
540
  function planGuardedRefreshTransaction(
1761
541
  files: readonly ShimFileState[],
1762
542
  expectedReplacements: ReadonlyMap<string, ShimPathFingerprint>,