@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
@@ -0,0 +1,367 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { tmpdir } from "node:os";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ rmSync,
10
+ } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { CODEX_SHIM_REENTRY_EXIT_CODE, CODEX_SHIM_REENTRY_DIAGNOSTIC } from "./shim-templates";
13
+ import type { ShimFileState } from "./shim-state-file";
14
+
15
+ const CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS = 5_000;
16
+ const CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS = 1_000;
17
+
18
+ const CODEX_SHIM_INSTALL_PROBE_SCRIPT = `
19
+ const { spawn } = require("node:child_process");
20
+ const { readFileSync, writeFileSync } = require("node:fs");
21
+ const [markerPath, reentryPath, groupPath, stderrPath, launcherShellPath, wrapperPath, timeoutRaw, stderrLimitRaw, stderrDrainRaw, observationRaw] = process.argv.slice(1);
22
+ const timeoutMs = Number.parseInt(timeoutRaw, 10);
23
+ const stderrLimit = Number.parseInt(stderrLimitRaw, 10);
24
+ const stderrDrainMs = Number.parseInt(stderrDrainRaw, 10);
25
+ const observationMs = Number.parseInt(observationRaw, 10);
26
+ const probeStartedAt = Date.now();
27
+ const stderrChunks = [];
28
+ let stderrBytes = 0;
29
+ let launcher;
30
+ let probeLease;
31
+ let timer;
32
+ let stderrDrainTimer;
33
+ let observationTimer;
34
+ let reentryPollTimer;
35
+ let marker = "";
36
+ let finished = false;
37
+
38
+ function writeExclusive(path, value) {
39
+ writeFileSync(path, value, { flag: "wx", mode: 0o600 });
40
+ }
41
+
42
+ function appendStderr(value) {
43
+ if (stderrBytes >= stderrLimit) return;
44
+ const bytes = Buffer.from(value);
45
+ const retained = bytes.subarray(0, stderrLimit - stderrBytes);
46
+ stderrChunks.push(retained);
47
+ stderrBytes += retained.byteLength;
48
+ }
49
+
50
+ function groupAlive() {
51
+ if (!launcher || !launcher.pid) return false;
52
+ try {
53
+ process.kill(-launcher.pid, 0);
54
+ return true;
55
+ } catch (error) {
56
+ return error && error.code !== "ESRCH";
57
+ }
58
+ }
59
+
60
+ function killGroup() {
61
+ if (!launcher || !launcher.pid) return;
62
+ try { process.kill(-launcher.pid, "SIGKILL"); } catch (error) {
63
+ if (!error || error.code !== "ESRCH") appendStderr(String(error));
64
+ }
65
+ }
66
+
67
+ function setMarker(value) {
68
+ if (marker) return;
69
+ marker = value;
70
+ try { writeExclusive(markerPath, value + "\\n"); } catch (error) { appendStderr(String(error)); }
71
+ }
72
+
73
+ function reentryDetected() {
74
+ try { return readFileSync(reentryPath, "utf8").trim() === "recursive"; } catch { return false; }
75
+ }
76
+
77
+ function checkReentry() {
78
+ if (finished || !reentryDetected()) return;
79
+ setMarker("recursive");
80
+ killGroup();
81
+ finish(126);
82
+ }
83
+
84
+ function finish(status) {
85
+ if (finished) return;
86
+ finished = true;
87
+ if (timer) clearTimeout(timer);
88
+ if (stderrDrainTimer) clearTimeout(stderrDrainTimer);
89
+ if (observationTimer) clearTimeout(observationTimer);
90
+ if (reentryPollTimer) clearInterval(reentryPollTimer);
91
+ if (!marker && reentryDetected()) setMarker("recursive");
92
+ if (!marker && groupAlive()) {
93
+ setMarker("descendants");
94
+ killGroup();
95
+ }
96
+ try { writeExclusive(stderrPath, Buffer.concat(stderrChunks)); } catch { /* parent fails closed */ }
97
+ process.exit(marker === "timeout" ? 124 : marker === "descendants" ? 125 : marker === "recursive" ? 126 : status);
98
+ }
99
+
100
+ function finishAfterStderr(status) {
101
+ if (finished) return;
102
+ if (timer) {
103
+ clearTimeout(timer);
104
+ timer = undefined;
105
+ }
106
+ if (!launcher || !launcher.stderr || !probeLease) {
107
+ finish(status);
108
+ return;
109
+ }
110
+ let stderrEnded = launcher.stderr.readableEnded;
111
+ let leaseEnded = probeLease.readableEnded;
112
+ let observationElapsed = false;
113
+ const finishWhenReady = () => {
114
+ if (stderrEnded && leaseEnded && observationElapsed) finish(status);
115
+ };
116
+ launcher.stderr.once("end", () => {
117
+ stderrEnded = true;
118
+ finishWhenReady();
119
+ });
120
+ probeLease.once("end", () => {
121
+ leaseEnded = true;
122
+ finishWhenReady();
123
+ });
124
+ stderrDrainTimer = setTimeout(() => {
125
+ stderrEnded = true;
126
+ if (!marker && groupAlive()) {
127
+ setMarker("descendants");
128
+ killGroup();
129
+ finish(125);
130
+ return;
131
+ }
132
+ finishWhenReady();
133
+ }, stderrDrainMs);
134
+ const remainingObservationMs = Math.max(0, observationMs - (Date.now() - probeStartedAt));
135
+ observationTimer = setTimeout(() => {
136
+ observationElapsed = true;
137
+ if (!leaseEnded) {
138
+ setMarker(groupAlive() ? "descendants" : "timeout");
139
+ killGroup();
140
+ finish(marker === "descendants" ? 125 : 124);
141
+ return;
142
+ }
143
+ finishWhenReady();
144
+ }, remainingObservationMs);
145
+ finishWhenReady();
146
+ }
147
+
148
+ try {
149
+ launcher = spawn(launcherShellPath, [wrapperPath, "--version"], {
150
+ detached: true,
151
+ env: process.env,
152
+ stdio: ["ignore", "ignore", "pipe", "pipe"],
153
+ });
154
+ if (!launcher.pid) throw new Error("Codex shim probe launcher has no pid");
155
+ probeLease = launcher.stdio[3];
156
+ if (!probeLease) throw new Error("Codex shim probe launcher has no descendant lease pipe");
157
+ writeExclusive(groupPath, String(launcher.pid) + "\\n");
158
+ launcher.stderr.on("data", appendStderr);
159
+ reentryPollTimer = setInterval(checkReentry, 10);
160
+ launcher.once("error", error => {
161
+ appendStderr(String(error));
162
+ finishAfterStderr(127);
163
+ });
164
+ launcher.once("exit", code => finishAfterStderr(Number.isInteger(code) ? code : 127));
165
+ timer = setTimeout(() => {
166
+ setMarker("timeout");
167
+ killGroup();
168
+ finish(124);
169
+ }, timeoutMs);
170
+ } catch (error) {
171
+ appendStderr(String(error));
172
+ killGroup();
173
+ finish(127);
174
+ }
175
+ `;
176
+ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
177
+
178
+ type UnixShimProbeCleanupPhase = "marker" | "reentry" | "group" | "stderr" | "group-id" | "termination" | "spawn" | "exception";
179
+ interface UnixShimProbeCleanup {
180
+ kind: "cleanup";
181
+ phase: UnixShimProbeCleanupPhase;
182
+ code: string;
183
+ status: number | null;
184
+ signal: string;
185
+ }
186
+ type UnixShimProbeResult = UnixShimProbeCleanup | "descendants" | "failed" | "recursive" | "timeout" | null;
187
+
188
+ const SHIM_PROBE_ERROR_CODES = new Set([
189
+ "EACCES", "EAGAIN", "EBADF", "ECANCELED", "EINTR", "EIO", "EMFILE", "ENFILE",
190
+ "ENOENT", "ENOEXEC", "ENOMEM", "ENOSPC", "EPERM", "EPIPE", "ESRCH", "ETIMEDOUT", "ETXTBSY",
191
+ ]);
192
+ const SHIM_PROBE_SIGNALS = new Set([
193
+ "SIGABRT", "SIGBUS", "SIGHUP", "SIGILL", "SIGINT", "SIGKILL", "SIGPIPE", "SIGQUIT",
194
+ "SIGSEGV", "SIGTERM", "SIGTRAP", "SIGXCPU", "SIGXFSZ",
195
+ ]);
196
+
197
+ /** Diagnostics cross a CLI boundary: never stringify arbitrary errors or metadata. */
198
+ function shimProbeCleanup(
199
+ phase: UnixShimProbeCleanupPhase, error?: unknown, status?: unknown, signal?: unknown,
200
+ ): UnixShimProbeCleanup {
201
+ let code = error === undefined ? "none" : "unknown";
202
+ if (error !== null && typeof error === "object") {
203
+ try {
204
+ const value = Object.getOwnPropertyDescriptor(error, "code")?.value;
205
+ if (typeof value === "string" && SHIM_PROBE_ERROR_CODES.has(value)) code = value;
206
+ } catch { /* hostile accessors/proxies cannot turn diagnostics into an exception */ }
207
+ }
208
+ return {
209
+ kind: "cleanup", phase, code,
210
+ status: typeof status === "number" && Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
211
+ signal: typeof signal === "string" && SHIM_PROBE_SIGNALS.has(signal) ? signal : "none",
212
+ };
213
+ }
214
+
215
+ let codexShimProbeHookForTests: (() => void) | null = null;
216
+ let codexShimProbeShellForTests: string | null = null;
217
+
218
+ let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
219
+
220
+ /** Narrow deterministic seam for transaction rollback tests. */
221
+ export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
222
+ codexShimProbeHookForTests = hook;
223
+ }
224
+
225
+ /** Selects a POSIX shell only for cross-shell probe regression tests. */
226
+ export function setCodexShimProbeShellForTests(path: string | null): void {
227
+ codexShimProbeShellForTests = path;
228
+ }
229
+
230
+ /** Shortens the successful-launcher observation window only for focused tests. */
231
+ export function setCodexShimProbeObservationMsForTests(value: number | null): void {
232
+ codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
233
+ }
234
+
235
+ function readProbeMetadata(path: string, maxBytes: number): string | null {
236
+ try {
237
+ if (!existsSync(path)) return "";
238
+ const stat = lstatSync(path);
239
+ if (!stat.isFile() || stat.size > maxBytes) return null;
240
+ return readFileSync(path, "utf8").trim();
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
247
+ if (process.platform === "win32") return null;
248
+ const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
249
+ const markerPath = join(probeDir, "result");
250
+ const reentryPath = join(probeDir, "reentry");
251
+ const groupPath = join(probeDir, "group");
252
+ const stderrPath = join(probeDir, "stderr");
253
+ const env: NodeJS.ProcessEnv = {
254
+ ...process.env,
255
+ OCX_SHIM_BYPASS: "1",
256
+ OCX_SHIM_PROBE: "1",
257
+ OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
258
+ };
259
+ delete env.OCX_SHIM_ACTIVE_PID;
260
+ delete env.OCX_SHIM_ACTIVE_DEPTH;
261
+ delete env.OCX_SHIM_PROBE_ACTIVE;
262
+ let groupId = 0;
263
+ let probeStatus: unknown;
264
+ let probeSignal: unknown;
265
+ try {
266
+ chmodSync(probeDir, 0o700);
267
+ const result = spawnSync(process.execPath, [
268
+ "-e",
269
+ CODEX_SHIM_INSTALL_PROBE_SCRIPT,
270
+ markerPath,
271
+ reentryPath,
272
+ groupPath,
273
+ stderrPath,
274
+ codexShimProbeShellForTests ?? "/bin/sh",
275
+ wrapperPath,
276
+ String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
277
+ String(MAX_DIAGNOSTIC_VALUE_BYTES),
278
+ String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
279
+ String(codexShimProbeObservationMs),
280
+ ], {
281
+ encoding: "utf8",
282
+ env,
283
+ timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
284
+ killSignal: "SIGKILL",
285
+ });
286
+ probeStatus = result.status;
287
+ probeSignal = result.signal;
288
+ const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
289
+ const marker = readProbeMetadata(markerPath, 64);
290
+ const reentryMarker = readProbeMetadata(reentryPath, 64);
291
+ const groupText = readProbeMetadata(groupPath, 64);
292
+ const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
293
+ groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
294
+ if (marker === null) return shimProbeCleanup("marker", result.error, probeStatus, probeSignal);
295
+ if (reentryMarker === null) return shimProbeCleanup("reentry", result.error, probeStatus, probeSignal);
296
+ if (groupText === null) return shimProbeCleanup("group", result.error, probeStatus, probeSignal);
297
+ if (launcherStderr === null) return shimProbeCleanup("stderr", result.error, probeStatus, probeSignal);
298
+ if (!Number.isInteger(groupId) || groupId <= 0) return shimProbeCleanup("group-id", result.error, probeStatus, probeSignal);
299
+ const groupSurvived = unixProcessGroupAlive(groupId);
300
+ if (timedOut || marker || reentryMarker || groupSurvived) {
301
+ try {
302
+ terminateUnixProcessGroup(groupId);
303
+ } catch (error) {
304
+ return shimProbeCleanup("termination", error, probeStatus, probeSignal);
305
+ }
306
+ }
307
+ if (result.error && !timedOut) return shimProbeCleanup("spawn", result.error, probeStatus, probeSignal);
308
+ if (timedOut || marker === "timeout") return "timeout";
309
+ if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
310
+ if (reentryMarker !== "") return shimProbeCleanup("reentry", undefined, probeStatus, probeSignal);
311
+ if (marker === "descendants") return "descendants";
312
+ if (groupSurvived) return "descendants";
313
+ if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
314
+ return "recursive";
315
+ }
316
+ if (result.status !== 0) return "failed";
317
+ return null;
318
+ } catch (error) {
319
+ if (Number.isInteger(groupId) && groupId > 0) {
320
+ try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
321
+ }
322
+ return shimProbeCleanup("exception", error, probeStatus, probeSignal);
323
+ } finally {
324
+ try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
325
+ }
326
+ }
327
+
328
+ function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
329
+ if (process.platform === "win32") return null;
330
+ codexShimProbeHookForTests?.();
331
+ return files
332
+ .filter(file => !file.preserveOnly)
333
+ .map(file => probeUnixShimInstall(file.wrapperPath))
334
+ .find(result => result !== null) ?? null;
335
+ }
336
+
337
+ function unixProcessGroupAlive(groupId: number): boolean {
338
+ try {
339
+ process.kill(-groupId, 0);
340
+ return true;
341
+ } catch (error) {
342
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
343
+ }
344
+ }
345
+
346
+ function terminateUnixProcessGroup(groupId: number): void {
347
+ let permissionError: unknown;
348
+ try {
349
+ process.kill(-groupId, "SIGKILL");
350
+ } catch (error) {
351
+ const code = (error as NodeJS.ErrnoException).code;
352
+ if (code === "EPERM") permissionError = error;
353
+ else if (code !== "ESRCH") throw error;
354
+ }
355
+ // A concurrently exiting group can briefly reject a second signal. Only
356
+ // observed disappearance clears that uncertainty; never send another signal.
357
+ const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
358
+ while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
359
+ if (unixProcessGroupAlive(groupId)) {
360
+ if (permissionError) throw permissionError;
361
+ throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
362
+ }
363
+ }
364
+
365
+ export { CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS, MAX_DIAGNOSTIC_VALUE_BYTES };
366
+ export type { UnixShimProbeResult };
367
+ export { probeUnixShimFiles };
@@ -0,0 +1,169 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ fstatSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ openSync,
9
+ readdirSync,
10
+ rmdirSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ type Stats,
14
+ } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { getConfigDir } from "../config";
17
+ import { isProcessAlive } from "../lib/process-control";
18
+ import { sameFingerprint, stableShimPathProbe, type ShimPathFingerprint } from "./shim-fingerprint";
19
+ import { fileErrorCode } from "./shim-state-file";
20
+
21
+ const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
22
+
23
+ interface ShimRestoreLock {
24
+ release(): void;
25
+ }
26
+
27
+ interface ShimRestoreLockRecord {
28
+ version: 1;
29
+ token: string;
30
+ pid: number;
31
+ createdAt: number;
32
+ }
33
+
34
+ interface ShimRestoreLockSnapshot {
35
+ record: ShimRestoreLockRecord;
36
+ ownerPath: string;
37
+ lockIdentity: Pick<Stats, "dev" | "ino">;
38
+ fingerprint: ShimPathFingerprint;
39
+ }
40
+
41
+ function restoreLockPath(): string {
42
+ return join(getConfigDir(), "codex-shim.autorestore.lock");
43
+ }
44
+
45
+ function sameFileIdentity(left: Pick<Stats, "dev" | "ino">, right: Pick<Stats, "dev" | "ino">): boolean {
46
+ return left.dev === right.dev && left.ino === right.ino;
47
+ }
48
+
49
+ function readShimRestoreLockSnapshot(path: string): ShimRestoreLockSnapshot | null {
50
+ let lockIdentity: Stats;
51
+ let entries: string[];
52
+ try {
53
+ lockIdentity = lstatSync(path);
54
+ if (!lockIdentity.isDirectory()) return null;
55
+ entries = readdirSync(path);
56
+ } catch {
57
+ return null;
58
+ }
59
+ if (entries.length !== 1 || !entries[0].endsWith(".json")) return null;
60
+ const ownerPath = join(path, entries[0]);
61
+ const probe = stableShimPathProbe(ownerPath);
62
+ if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
63
+ try {
64
+ const value = JSON.parse(probe.prefix) as Partial<ShimRestoreLockRecord>;
65
+ if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
66
+ || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
67
+ || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
68
+ if (entries[0] !== `${value.token}.json`) return null;
69
+ const currentLockIdentity = lstatSync(path);
70
+ if (!currentLockIdentity.isDirectory() || !sameFileIdentity(lockIdentity, currentLockIdentity)) return null;
71
+ return {
72
+ record: value as ShimRestoreLockRecord,
73
+ ownerPath,
74
+ lockIdentity,
75
+ fingerprint: probe.fingerprint,
76
+ };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function sameShimRestoreLock(left: ShimRestoreLockSnapshot, right: ShimRestoreLockSnapshot): boolean {
83
+ return left.record.token === right.record.token
84
+ && sameFileIdentity(left.lockIdentity, right.lockIdentity)
85
+ && sameFingerprint(left.fingerprint, right.fingerprint);
86
+ }
87
+
88
+ function reclaimStaleRestoreLock(path: string, beforeDelete?: () => void): boolean {
89
+ const observed = readShimRestoreLockSnapshot(path);
90
+ if (!observed) return false;
91
+ const createdAt = Math.max(observed.record.createdAt, observed.fingerprint.mtimeMs);
92
+ if (Date.now() - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
93
+ if (isProcessAlive(observed.record.pid)) return false;
94
+ const current = readShimRestoreLockSnapshot(path);
95
+ if (!current || !sameShimRestoreLock(observed, current)) return false;
96
+ beforeDelete?.();
97
+ try {
98
+ // The token is part of the owner filename. Even if the lock directory is
99
+ // replaced after the comparison, this unlink cannot target a successor's
100
+ // differently named owner record.
101
+ unlinkSync(observed.ownerPath);
102
+ rmdirSync(path);
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreLock | null {
110
+ const dir = getConfigDir();
111
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
112
+ const path = restoreLockPath();
113
+ for (let attempt = 0; attempt < 2; attempt += 1) {
114
+ let fd: number | null = null;
115
+ let identity: Stats | null = null;
116
+ let createdDirectory = false;
117
+ const record: ShimRestoreLockRecord = {
118
+ version: 1,
119
+ token: `${process.pid}-${Date.now()}-${randomUUID()}`,
120
+ pid: process.pid,
121
+ createdAt: Date.now(),
122
+ };
123
+ const ownerPath = join(path, `${record.token}.json`);
124
+ try {
125
+ mkdirSync(path, { mode: 0o700 });
126
+ createdDirectory = true;
127
+ fd = openSync(ownerPath, "wx", 0o600);
128
+ identity = fstatSync(fd);
129
+ writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf8");
130
+ identity = fstatSync(fd);
131
+ let released = false;
132
+ return {
133
+ release(): void {
134
+ if (released) return;
135
+ released = true;
136
+ try { closeSync(fd!); } catch { /* stale recovery handles an uncertain lock */ }
137
+ try {
138
+ const current = readShimRestoreLockSnapshot(path);
139
+ if (identity && current && current.record.token === record.token
140
+ && sameFileIdentity(identity, current.fingerprint)) {
141
+ unlinkSync(ownerPath);
142
+ rmdirSync(path);
143
+ }
144
+ } catch { /* stale recovery handles release failures */ }
145
+ },
146
+ };
147
+ } catch (error) {
148
+ if (fd !== null) {
149
+ try { closeSync(fd); } catch { /* best-effort close before ownership cleanup */ }
150
+ try {
151
+ const current = readShimRestoreLockSnapshot(path);
152
+ if (identity && current && current.record.token === record.token
153
+ && sameFileIdentity(identity, current.fingerprint)) {
154
+ unlinkSync(ownerPath);
155
+ rmdirSync(path);
156
+ }
157
+ } catch { /* leave an uncertain lock for stale recovery */ }
158
+ } else if (createdDirectory) {
159
+ try { rmdirSync(path); } catch { /* another owner exists or cleanup is uncertain */ }
160
+ }
161
+ if (fileErrorCode(error) !== "EEXIST") throw error;
162
+ if (attempt === 0 && reclaimStaleRestoreLock(path, beforeStaleDelete)) continue;
163
+ return null;
164
+ }
165
+ }
166
+ return null;
167
+ }
168
+
169
+ export { tryAcquireShimRestoreLock, reclaimStaleRestoreLock };
@@ -0,0 +1,151 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ fstatSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readSync,
9
+ writeFileSync,
10
+ type Stats,
11
+ } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { getConfigDir } from "../config";
14
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
15
+
16
+ export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
17
+
18
+ interface ShimState {
19
+ platform: NodeJS.Platform;
20
+ wrapperPath: string;
21
+ originalPath: string;
22
+ backupPath: string;
23
+ wrappers?: ShimFileState[];
24
+ }
25
+
26
+ interface ShimFileState {
27
+ wrapperPath: string;
28
+ originalPath: string;
29
+ backupPath: string;
30
+ realPath?: string;
31
+ preserveOnly?: boolean;
32
+ }
33
+
34
+ interface ShimStateReadResult {
35
+ state: ShimState | null;
36
+ present: boolean;
37
+ warning?: string;
38
+ }
39
+
40
+ function fileErrorCode(error: unknown): string | undefined {
41
+ return error && typeof error === "object" && "code" in error
42
+ ? String((error as { code?: unknown }).code)
43
+ : undefined;
44
+ }
45
+
46
+ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
47
+ let lexicalBefore: Stats;
48
+ try {
49
+ lexicalBefore = lstatSync(path);
50
+ if (lexicalBefore.isSymbolicLink() || !lexicalBefore.isFile()) {
51
+ return { warning: `Codex shim state is not a direct regular file at ${path}; auto-restore skipped.` };
52
+ }
53
+ } catch (error) {
54
+ if (fileErrorCode(error) === "ENOENT") return null;
55
+ return { warning: `Codex shim state could not be inspected at ${path}.` };
56
+ }
57
+ let fd: number;
58
+ try {
59
+ fd = openSync(path, "r");
60
+ } catch (error) {
61
+ if (fileErrorCode(error) === "ENOENT") return null;
62
+ return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
63
+ }
64
+ try {
65
+ const before = fstatSync(fd);
66
+ if (!before.isFile()) return { warning: `Codex shim state is not a regular file at ${path}; auto-restore skipped.` };
67
+ if (before.size > maxBytes) {
68
+ return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
69
+ }
70
+ const buffer = Buffer.allocUnsafe(before.size);
71
+ let offset = 0;
72
+ while (offset < buffer.length) {
73
+ const bytesRead = readSync(fd, buffer, offset, buffer.length - offset, offset);
74
+ if (bytesRead === 0) return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
75
+ offset += bytesRead;
76
+ }
77
+ const extra = Buffer.allocUnsafe(1);
78
+ if (readSync(fd, extra, 0, 1, offset) !== 0) {
79
+ return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
80
+ }
81
+ const after = fstatSync(fd);
82
+ let lexicalAfter: Stats;
83
+ try {
84
+ lexicalAfter = lstatSync(path);
85
+ } catch {
86
+ return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
87
+ }
88
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
89
+ || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
90
+ || lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
91
+ || lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino) {
92
+ return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
93
+ }
94
+ return { bytes: buffer, content: buffer.toString("utf8") };
95
+ } finally {
96
+ closeSync(fd);
97
+ }
98
+ }
99
+
100
+ function readStateResult(path = statePath()): ShimStateReadResult {
101
+ const bounded = readBoundedRegularFile(path, CODEX_SHIM_STATE_MAX_BYTES);
102
+ if (!bounded) return { state: null, present: false };
103
+ if ("warning" in bounded) return { state: null, present: true, warning: bounded.warning };
104
+ try {
105
+ const value = JSON.parse(bounded.content) as unknown;
106
+ if (!value || typeof value !== "object") return { state: null, present: true };
107
+ const state = value as Record<string, unknown>;
108
+ if (typeof state.platform !== "string") return { state: null, present: true };
109
+ const validFile = (item: unknown): item is ShimFileState => {
110
+ if (!item || typeof item !== "object") return false;
111
+ const file = item as Record<string, unknown>;
112
+ return typeof file.wrapperPath === "string"
113
+ && typeof file.originalPath === "string"
114
+ && typeof file.backupPath === "string"
115
+ && (file.realPath === undefined || typeof file.realPath === "string")
116
+ && (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
117
+ };
118
+ if (state.wrappers !== undefined) {
119
+ if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null, present: true };
120
+ } else if (!validFile(state)) {
121
+ return { state: null, present: true };
122
+ }
123
+ return { state: state as unknown as ShimState, present: true };
124
+ } catch {
125
+ return { state: null, present: true };
126
+ }
127
+ }
128
+
129
+ function readState(): ShimState | null {
130
+ return readStateResult().state;
131
+ }
132
+
133
+ function statePath(): string {
134
+ return join(getConfigDir(), "codex-shim.json");
135
+ }
136
+
137
+ function writeState(state: ShimState): void {
138
+ const path = statePath();
139
+ recordOwnedConfigPath(getConfigDir(), path);
140
+ if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
141
+ writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
142
+ }
143
+
144
+ function stateFiles(state: ShimState): ShimFileState[] {
145
+ return state.wrappers?.length
146
+ ? state.wrappers
147
+ : [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
148
+ }
149
+
150
+ export type { ShimState, ShimFileState };
151
+ export { fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };