@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/shim.ts CHANGED
@@ -1,25 +1,33 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { delimiter, dirname, extname, join, posix } from "node:path";
2
+ import { spawnSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, delimiter, dirname, extname, join, posix } from "node:path";
3
5
  import {
4
6
  chmodSync,
5
7
  closeSync,
6
8
  existsSync,
7
9
  fstatSync,
8
10
  lstatSync,
11
+ linkSync,
9
12
  mkdirSync,
13
+ mkdtempSync,
10
14
  openSync,
11
15
  readFileSync,
12
16
  readdirSync,
17
+ readlinkSync,
13
18
  readSync,
14
19
  renameSync,
20
+ rmSync,
15
21
  rmdirSync,
16
22
  statSync,
23
+ symlinkSync,
17
24
  type Stats,
18
25
  unlinkSync,
19
26
  writeFileSync,
20
27
  } from "node:fs";
21
28
  import { getConfigDir } from "../config";
22
- import { durableBunPath } from "../lib/bun-runtime";
29
+ import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "../lib/bun-runtime";
30
+ import type { BunRuntimeSource } from "../lib/bun-runtime";
23
31
  import { isProcessAlive } from "../lib/process-control";
24
32
  import { serviceApiTokenFilePath } from "../lib/service-secrets";
25
33
  import { recordOwnedConfigPath } from "../lib/config-ownership";
@@ -28,10 +36,173 @@ import { isWslRuntime, wslAutomountRoot } from "./home";
28
36
  import { truncateRetainedUtf8 } from "../lib/admission";
29
37
 
30
38
  const SHIM_MARKER = "opencodex codex autostart shim";
39
+ const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
31
40
  const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
32
41
  export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
33
42
  export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
34
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
+
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
+ `;
35
206
  const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
36
207
  let lastShimDiscoveryError: string | null = null;
37
208
  /** Last human-readable reason discovery returned null (exposed for doctor/tests). */
@@ -120,11 +291,13 @@ export type CodexShimAutoRestoreResult =
120
291
  | { status: "ineligible" | "deferred"; message?: string }
121
292
  | { status: "restored"; message: string };
122
293
 
123
- function cliEntry(): { bun: string; cli: string } {
294
+ function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
124
295
  // Bundled Bun path (survives `ocx update`); all three shim builders
125
296
  // (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
126
297
  // This module lives in src/codex/, the CLI entry in src/cli/index.ts.
127
- return { bun: durableBunPath(), cli: join(import.meta.dir, "..", "cli", "index.ts") };
298
+ // Path and provenance resolve together so the marker always describes this binary.
299
+ const runtime = durableBunRuntime();
300
+ return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "..", "cli", "index.ts") };
128
301
  }
129
302
 
130
303
  function commandNames(name: string): string[] {
@@ -145,6 +318,7 @@ function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
145
318
  try {
146
319
  const content = readFileSync(path, "utf8");
147
320
  if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
321
+ if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
148
322
  if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
149
323
  return true;
150
324
  } catch {
@@ -201,6 +375,12 @@ function sameFingerprint(
201
375
  : false);
202
376
  }
203
377
 
378
+ function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
379
+ // rename changes the outer directory entry ctime on macOS; every other field,
380
+ // including a symlink target fingerprint, must remain identical.
381
+ return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
382
+ }
383
+
204
384
  function stableShimPathProbe(path: string): StableShimPathProbe | null {
205
385
  const before = statFingerprint(path, false);
206
386
  if (!before) return null;
@@ -230,12 +410,73 @@ function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPat
230
410
  return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
231
411
  }
232
412
 
413
+ /**
414
+ * Identity of whatever sits at `path`, read from metadata alone.
415
+ *
416
+ * `stableShimPathProbe` answers a different question: it reads content to decide
417
+ * whether a launcher looks like a healthy shim, and it deliberately returns null
418
+ * for a zero-byte file. That makes it the wrong instrument for rollback
419
+ * bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
420
+ * install moves it aside before writing our wrapper; if the move is recorded
421
+ * without a fingerprint, rollback cannot prove the backup is still the file it
422
+ * set aside and refuses to restore it — the launcher stays lost (#1625).
423
+ *
424
+ * Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
425
+ * re-reads them to reject a path that changed under us, following a symlink to
426
+ * fingerprint its target as well.
427
+ */
428
+ function shimPathFingerprint(path: string): ShimPathFingerprint | null {
429
+ const before = statFingerprint(path, false);
430
+ if (!before) return null;
431
+ if (before.kind !== "symlink") {
432
+ const after = statFingerprint(path, false);
433
+ return after && sameFingerprint(before, after) ? before : null;
434
+ }
435
+ const targetBefore = statFingerprint(path, true);
436
+ if (!targetBefore) return null;
437
+ const targetAfter = statFingerprint(path, true);
438
+ const after = statFingerprint(path, false);
439
+ if (!targetAfter || !after
440
+ || !sameFingerprint(targetBefore, targetAfter)
441
+ || !sameFingerprint(before, after)) return null;
442
+ return { ...before, target: targetBefore };
443
+ }
444
+
445
+ /**
446
+ * Move `from` onto `to` without ever replacing an existing entry.
447
+ *
448
+ * `renameSync` silently clobbers the destination on POSIX, which is wrong for a
449
+ * rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
450
+ * a concurrent installer can publish its own launcher at the original path in
451
+ * between, and the restore would delete it. `link` fails EEXIST instead, which
452
+ * is the no-replace primitive we need and needs no native helper.
453
+ *
454
+ * `link` follows a symlink to its target rather than preserving the link, so a
455
+ * symlink launcher is republished with `symlink`, which is also no-replace: it
456
+ * fails EEXIST on an occupied destination. Checking existence and then renaming
457
+ * would reintroduce exactly the race this function exists to close.
458
+ */
459
+ function restoreWithoutReplacing(from: string, to: string): void {
460
+ const source = lstatSync(from);
461
+ if (source.isSymbolicLink()) {
462
+ symlinkSync(readlinkSync(from), to);
463
+ unlinkSync(from);
464
+ return;
465
+ }
466
+ linkSync(from, to);
467
+ unlinkSync(from);
468
+ }
469
+
233
470
  function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
234
471
  if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
235
472
  const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
236
473
  return platform === "win32" || (mode & 0o111) !== 0;
237
474
  }
238
475
 
476
+ function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
477
+ return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
478
+ }
479
+
239
480
  function hasUsableBackingPath(file: ShimFileState): boolean {
240
481
  return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
241
482
  .some(path => {
@@ -362,15 +603,99 @@ function backupPathFor(path: string): string {
362
603
  return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
363
604
  }
364
605
 
606
+ /**
607
+ * True when a Codex binary lives inside a version manager's install tree.
608
+ *
609
+ * These trees are rewritten in place on upgrade, which destroys both the shim
610
+ * and the sibling .opencodex-real backup it restores from (#2412). The tempting
611
+ * repair — adopt the newly installed binary as a fresh original — is wrong
612
+ * twice: it records a provenance that never happened, and the next upgrade wipes
613
+ * it again, so the repair silently un-repairs on the version manager's schedule.
614
+ *
615
+ * Scope is the three managers named in the report. nvm/fnm/npm-prefix are
616
+ * deliberately excluded: a false positive here refuses a restore that would
617
+ * otherwise be correct.
618
+ */
619
+ export function isVersionManagerOwnedCodexPath(path: string): boolean {
620
+ const normalized = path.replace(/\\/g, "/").toLowerCase();
621
+ return normalized.includes("/mise/installs/")
622
+ || normalized.includes("/mise/shims/")
623
+ || normalized.includes("/.asdf/installs/")
624
+ || normalized.includes("/.asdf/shims/")
625
+ || normalized.includes("/.volta/");
626
+ }
627
+
628
+ /**
629
+ * Why auto-restore refused, in the operator's own terms. Auto-restore used to
630
+ * return a bare `{ status: "ineligible" }`, and the CLI warns only when a
631
+ * message is present, so `ocx start`, `ocx ensure`, and `ocx service repair` all
632
+ * reported success while routing quietly stayed native (#2412, the cause behind
633
+ * the misleading green status in #2411).
634
+ */
635
+ function destroyedShimMessage(file: ShimFileState): string {
636
+ const wrapper = existsSync(file.wrapperPath)
637
+ ? isShim(file.wrapperPath) ? "present but unusable" : "present but not an opencodex shim"
638
+ : "missing";
639
+ const backup = existsSync(file.backupPath) ? "present" : "missing";
640
+ const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
641
+ if (!isVersionManagerOwnedCodexPath(file.wrapperPath)) {
642
+ return `${base} Re-run 'ocx codex-shim install' once the Codex binary is stable.`;
643
+ }
644
+ 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.`;
645
+ }
646
+
365
647
  function shQuote(value: string): string {
366
648
  return `'${value.replace(/'/g, "'\\''")}'`;
367
649
  }
368
650
 
369
- export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, tokenFile = serviceApiTokenFilePath()): string {
651
+ // Provenance is required rather than defaulted: a default would let a caller pass an
652
+ // override binary and silently label it something else, which is precisely the
653
+ // path/marker disagreement this feature exists to prevent.
654
+ //
655
+ // The marker is scoped to the `ensure` invocation in every flavor below and is never
656
+ // exported into the shim's own environment. A shim wraps the real `codex`, so an
657
+ // exported marker would be inherited by Codex and everything it spawns — a shell that
658
+ // then ran a *different* Bun directly would carry a provenance describing a binary it
659
+ // is not executing.
660
+ export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
370
661
  const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
371
662
  const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
372
663
  return `#!/usr/bin/env sh
373
664
  # ${SHIM_MARKER}
665
+ # ${UNIX_SHIM_REVISION_MARKER}
666
+ if [ "\${OCX_SHIM_PROBE:-}" = "1" ]; then
667
+ if [ "\${OCX_SHIM_PROBE_ACTIVE:-}" = "1" ]; then
668
+ if [ -n "\${OCX_SHIM_PROBE_REENTRY_PATH:-}" ]; then
669
+ (umask 077; printf '%s\n' recursive > "$OCX_SHIM_PROBE_REENTRY_PATH") 2>/dev/null || true
670
+ fi
671
+ printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
672
+ exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
673
+ fi
674
+ OCX_SHIM_PROBE_ACTIVE=1
675
+ export OCX_SHIM_PROBE_ACTIVE
676
+ fi
677
+ if [ "\${OCX_SHIM_ACTIVE_PID:-}" = "$$" ]; then
678
+ printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
679
+ exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
680
+ fi
681
+ case "\${OCX_SHIM_ACTIVE_DEPTH:-0}" in
682
+ 0)
683
+ OCX_SHIM_ACTIVE_DEPTH=1
684
+ ;;
685
+ 1)
686
+ OCX_SHIM_ACTIVE_DEPTH=2
687
+ ;;
688
+ *)
689
+ printf '%s\n' ${shQuote(CODEX_SHIM_REENTRY_DIAGNOSTIC)} >&2
690
+ exit ${CODEX_SHIM_REENTRY_EXIT_CODE}
691
+ ;;
692
+ esac
693
+ # Dynamic launchers such as mise exec -- codex may resolve the command name
694
+ # back to this wrapper. An exec chain keeps the same PID. A legitimate nested
695
+ # Codex invocation may enter once with a new PID; repeated child-process
696
+ # redispatch reaches depth 2 and is rejected before it can form an infinite chain.
697
+ OCX_SHIM_ACTIVE_PID=$$
698
+ export OCX_SHIM_ACTIVE_PID OCX_SHIM_ACTIVE_DEPTH
374
699
  if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
375
700
  OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
376
701
  export OPENCODEX_API_AUTH_TOKEN
@@ -406,7 +731,7 @@ case "$ocx_subcommand" in
406
731
  ;;
407
732
  *)
408
733
  if [ -z "$OCX_SHIM_BYPASS" ]; then
409
- ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
734
+ ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
410
735
  fi
411
736
  ;;
412
737
  esac
@@ -414,6 +739,239 @@ exec ${shQuote(realCodexPath)} "$@"
414
739
  `;
415
740
  }
416
741
 
742
+ type UnixShimProbeResult = "cleanup" | "descendants" | "failed" | "recursive" | "timeout" | null;
743
+
744
+ let codexShimProbeHookForTests: (() => void) | null = null;
745
+ let codexShimProbeShellForTests: string | null = null;
746
+ let codexShimGuardedWriteHookForTests: (() => void) | null = null;
747
+ let codexShimFreshWriteHookForTests: (() => void) | null = null;
748
+ let codexShimRollbackRestoreHookForTests: ((target: ShimFileState) => void) | null = null;
749
+ let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
750
+
751
+ /** Narrow deterministic seam for transaction rollback tests. */
752
+ export function setCodexShimProbeHookForTests(hook: (() => void) | null): void {
753
+ codexShimProbeHookForTests = hook;
754
+ }
755
+
756
+ /** Selects a POSIX shell only for cross-shell probe regression tests. */
757
+ export function setCodexShimProbeShellForTests(path: string | null): void {
758
+ codexShimProbeShellForTests = path;
759
+ }
760
+
761
+ /** Shortens the successful-launcher observation window only for focused tests. */
762
+ export function setCodexShimProbeObservationMsForTests(value: number | null): void {
763
+ codexShimProbeObservationMs = value ?? CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS;
764
+ }
765
+
766
+ /** Narrow deterministic seam for guarded partial-write rollback tests. */
767
+ export function setCodexShimGuardedWriteHookForTests(hook: (() => void) | null): void {
768
+ codexShimGuardedWriteHookForTests = hook;
769
+ }
770
+
771
+ /** Narrow deterministic seam for fresh-install partial-write rollback tests. */
772
+ export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): void {
773
+ codexShimFreshWriteHookForTests = hook;
774
+ }
775
+
776
+ /**
777
+ * @internal Test-only seam for the rollback restore race.
778
+ *
779
+ * The window this closes opens after `sourceOccupied` is sampled and closes when
780
+ * the backup is republished, so no earlier hook can reach it: publishing from
781
+ * the fresh-write hook makes `sourceOccupied` true and skips the restore
782
+ * entirely.
783
+ */
784
+ export function setCodexShimRollbackRestoreHookForTests(
785
+ hook: ((target: ShimFileState) => void) | null,
786
+ ): void {
787
+ codexShimRollbackRestoreHookForTests = hook;
788
+ }
789
+
790
+ function readProbeMetadata(path: string, maxBytes: number): string | null {
791
+ try {
792
+ if (!existsSync(path)) return "";
793
+ const stat = lstatSync(path);
794
+ if (!stat.isFile() || stat.size > maxBytes) return null;
795
+ return readFileSync(path, "utf8").trim();
796
+ } catch {
797
+ return null;
798
+ }
799
+ }
800
+
801
+ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
802
+ if (process.platform === "win32") return null;
803
+ const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
804
+ const markerPath = join(probeDir, "result");
805
+ const reentryPath = join(probeDir, "reentry");
806
+ const groupPath = join(probeDir, "group");
807
+ const stderrPath = join(probeDir, "stderr");
808
+ const env: NodeJS.ProcessEnv = {
809
+ ...process.env,
810
+ OCX_SHIM_BYPASS: "1",
811
+ OCX_SHIM_PROBE: "1",
812
+ OCX_SHIM_PROBE_REENTRY_PATH: reentryPath,
813
+ };
814
+ delete env.OCX_SHIM_ACTIVE_PID;
815
+ delete env.OCX_SHIM_ACTIVE_DEPTH;
816
+ delete env.OCX_SHIM_PROBE_ACTIVE;
817
+ let groupId = 0;
818
+ try {
819
+ chmodSync(probeDir, 0o700);
820
+ const result = spawnSync(process.execPath, [
821
+ "-e",
822
+ CODEX_SHIM_INSTALL_PROBE_SCRIPT,
823
+ markerPath,
824
+ reentryPath,
825
+ groupPath,
826
+ stderrPath,
827
+ codexShimProbeShellForTests ?? "/bin/sh",
828
+ wrapperPath,
829
+ String(CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS),
830
+ String(MAX_DIAGNOSTIC_VALUE_BYTES),
831
+ String(CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS),
832
+ String(codexShimProbeObservationMs),
833
+ ], {
834
+ encoding: "utf8",
835
+ env,
836
+ timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
837
+ killSignal: "SIGKILL",
838
+ });
839
+ const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
840
+ const marker = readProbeMetadata(markerPath, 64);
841
+ const reentryMarker = readProbeMetadata(reentryPath, 64);
842
+ const groupText = readProbeMetadata(groupPath, 64);
843
+ const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
844
+ groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
845
+ if (marker === null || reentryMarker === null || groupText === null || launcherStderr === null
846
+ || !Number.isInteger(groupId) || groupId <= 0) return "cleanup";
847
+ const groupSurvived = unixProcessGroupAlive(groupId);
848
+ if (timedOut || marker || reentryMarker || groupSurvived) {
849
+ try {
850
+ terminateUnixProcessGroup(groupId);
851
+ } catch {
852
+ return "cleanup";
853
+ }
854
+ }
855
+ if (result.error && !timedOut) return "cleanup";
856
+ if (timedOut || marker === "timeout") return "timeout";
857
+ if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
858
+ if (reentryMarker !== "") return "cleanup";
859
+ if (marker === "descendants") return "descendants";
860
+ if (groupSurvived) return "descendants";
861
+ if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
862
+ return "recursive";
863
+ }
864
+ if (result.status !== 0) return "failed";
865
+ return null;
866
+ } catch {
867
+ if (Number.isInteger(groupId) && groupId > 0) {
868
+ try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
869
+ }
870
+ return "cleanup";
871
+ } finally {
872
+ try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
873
+ }
874
+ }
875
+
876
+ function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
877
+ if (process.platform === "win32") return null;
878
+ codexShimProbeHookForTests?.();
879
+ return files
880
+ .filter(file => !file.preserveOnly)
881
+ .map(file => probeUnixShimInstall(file.wrapperPath))
882
+ .find(result => result !== null) ?? null;
883
+ }
884
+
885
+ function unixProcessGroupAlive(groupId: number): boolean {
886
+ try {
887
+ process.kill(-groupId, 0);
888
+ return true;
889
+ } catch (error) {
890
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
891
+ }
892
+ }
893
+
894
+ function terminateUnixProcessGroup(groupId: number): void {
895
+ try {
896
+ process.kill(-groupId, "SIGKILL");
897
+ } catch (error) {
898
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
899
+ }
900
+ const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
901
+ while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
902
+ if (unixProcessGroupAlive(groupId)) {
903
+ throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
904
+ }
905
+ }
906
+
907
+ interface FreshShimInstallJournalEntry {
908
+ target: ShimFileState;
909
+ movedOriginalFingerprint?: ShimPathFingerprint;
910
+ originalMovedToBackup: boolean;
911
+ writtenWrapperFingerprint?: ShimPathFingerprint;
912
+ wrapperWriteStarted: boolean;
913
+ /** dev/ino of the file our `writeShim()` created, recorded before anything can fail. */
914
+ writtenWrapperInode?: { dev: number; ino: number };
915
+ }
916
+
917
+ function rollbackFreshShimInstall(journal: readonly FreshShimInstallJournalEntry[]): void {
918
+ const errors: Error[] = [];
919
+ for (const entry of [...journal].reverse()) {
920
+ const target = entry.target;
921
+ let sourceOccupied = false;
922
+ let ownsWrapperNow = false;
923
+ try {
924
+ const wrapper = stableShimPathProbe(target.wrapperPath);
925
+ // Ownership is the inode our own write created. There is no marker-text
926
+ // fallback: the markers are public, so a concurrent updater's wrapper carries
927
+ // them too, and treating that as proof is how we would delete a file we never
928
+ // wrote. An in-place truncation of our file keeps the inode and is still ours
929
+ // to clean up; a replacement renamed over it has a different inode and is not.
930
+ const ownsWrapper = !target.preserveOnly
931
+ && entry.wrapperWriteStarted
932
+ && wrapper !== null
933
+ && wrapperInodeIsOurs(wrapper, entry.writtenWrapperInode, entry.writtenWrapperFingerprint);
934
+ ownsWrapperNow = ownsWrapper;
935
+ if (ownsWrapper) unlinkSync(target.wrapperPath);
936
+ else {
937
+ try {
938
+ lstatSync(target.originalPath);
939
+ sourceOccupied = true;
940
+ } catch (error) {
941
+ if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
942
+ }
943
+ }
944
+ } catch (error) {
945
+ errors.push(error instanceof Error ? error : new Error(String(error)));
946
+ }
947
+ try {
948
+ if (entry.originalMovedToBackup && existsSync(target.backupPath)) {
949
+ const movedOriginal = shimPathFingerprint(target.backupPath);
950
+ if (!movedOriginal || !entry.movedOriginalFingerprint
951
+ || !sameFingerprint(movedOriginal, entry.movedOriginalFingerprint)) {
952
+ throw new Error("Codex shim fresh-install backup changed during rollback");
953
+ }
954
+ if (sourceOccupied) {
955
+ // Something else occupies the source path. Dropping the backup is correct
956
+ // only when that something is a file we own; when a concurrent updater
957
+ // owns it, this backup is the user's real launcher and deleting it would
958
+ // lose the command entirely. Keep it in that case — a stray
959
+ // `codex.opencodex-real` is recoverable, a deleted launcher is not.
960
+ if (ownsWrapperNow) unlinkSync(target.backupPath);
961
+ } else {
962
+ // No-replace: sourceOccupied was sampled earlier, so a concurrent
963
+ // installer may have published a launcher at the original path since.
964
+ codexShimRollbackRestoreHookForTests?.(target);
965
+ restoreWithoutReplacing(target.backupPath, target.originalPath);
966
+ }
967
+ }
968
+ } catch (error) {
969
+ errors.push(error instanceof Error ? error : new Error(String(error)));
970
+ }
971
+ }
972
+ if (errors.length > 0) throw new AggregateError(errors, "Codex shim install validation rollback failed");
973
+ }
974
+
417
975
  function windowsBatchValue(value: string): string {
418
976
  return value
419
977
  .replace(/%/g, "%%")
@@ -431,7 +989,7 @@ function windowsBatchSet(name: string, value: string): string {
431
989
  return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
432
990
  }
433
991
 
434
- export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
992
+ export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
435
993
  const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
436
994
  const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
437
995
  return `@echo off\r
@@ -462,7 +1020,11 @@ if "%~1"=="" goto ensure_ocx\r
462
1020
  shift\r
463
1021
  goto scan_codex_args\r
464
1022
  :ensure_ocx\r
1023
+ setlocal\r
1024
+ ${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
1025
+ ${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
465
1026
  "%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
1027
+ endlocal\r
466
1028
  :run_codex\r
467
1029
  "%OCX_REAL_CODEX%" %*\r
468
1030
  `;
@@ -472,7 +1034,7 @@ function psString(value: string): string {
472
1034
  return `'${value.replace(/'/g, "''")}'`;
473
1035
  }
474
1036
 
475
- export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
1037
+ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
476
1038
  const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
477
1039
  const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
478
1040
  const tokenFile = serviceApiTokenFilePath();
@@ -497,7 +1059,17 @@ foreach ($argValue in $args) {
497
1059
  }
498
1060
  $skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
499
1061
  if (-not $skipEnsure) {
500
- & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null
1062
+ $priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
1063
+ $priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
1064
+ $env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
1065
+ $env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
1066
+ try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
1067
+ finally {
1068
+ if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
1069
+ else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
1070
+ if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
1071
+ else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
1072
+ }
501
1073
  }
502
1074
  & ${psString(realCodexPath)} @args
503
1075
  exit $LASTEXITCODE
@@ -600,28 +1172,108 @@ function gitBashPath(path: string): string {
600
1172
  return path.replace(/\\/g, "/");
601
1173
  }
602
1174
 
603
- function writeShim(wrapperPath: string, realCodexPath: string): void {
604
- const { bun, cli } = cliEntry();
1175
+ /**
1176
+ * Write the wrapper and return the identity of the inode this call created, or
1177
+ * `undefined` where the platform still writes the destination in place.
1178
+ *
1179
+ * Callers must derive ownership from the returned identity rather than from a
1180
+ * later `stat` of `wrapperPath`: the shim markers are public, so a concurrent
1181
+ * updater's wrapper can carry them, and a replacement landing between the write
1182
+ * and the observation is otherwise indistinguishable from our own file.
1183
+ */
1184
+ function writeShim(wrapperPath: string, realCodexPath: string): { dev: number; ino: number } | undefined {
1185
+ const { bun, bunRuntimeSource, cli } = cliEntry();
605
1186
  if (process.platform === "win32") {
606
1187
  const lower = wrapperPath.toLowerCase();
607
1188
  if (lower.endsWith(".ps1")) {
608
1189
  // UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
609
1190
  // codepage, which mangles non-ASCII paths embedded in the shim.
610
- writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
1191
+ writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
611
1192
  } else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
612
- writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
1193
+ writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
613
1194
  } else {
614
1195
  // Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
615
1196
  writeFileSync(
616
1197
  wrapperPath,
617
- buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
1198
+ buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), bunRuntimeSource, gitBashPath(serviceApiTokenFilePath())),
618
1199
  "utf8",
619
1200
  );
620
1201
  }
1202
+ return undefined;
621
1203
  } else {
622
- writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
623
- chmodSync(wrapperPath, 0o755);
1204
+ // Stage the wrapper as its own inode and rename it into place, so ownership
1205
+ // comes from the write itself rather than from observing the path afterwards.
1206
+ // Writing the destination directly leaves a window in which a concurrent
1207
+ // updater can replace the file between our write and our fingerprint; we would
1208
+ // then adopt that replacement as ours and unlink it during rollback, deleting
1209
+ // an executable we never wrote.
1210
+ // Hidden and non-executable while staged, so a crash between the write and the
1211
+ // rename cannot leave an executable `codex*` artifact that a glob or a shell
1212
+ // completion would surface.
1213
+ const staged = join(dirname(wrapperPath), `.${basename(wrapperPath)}.opencodex-staging.${process.pid}.${randomUUID()}`);
1214
+ let renamed = false;
1215
+ try {
1216
+ // "wx" fails if the staging path somehow exists, so we never inherit a file.
1217
+ writeFileSync(staged, buildUnixCodexShim(realCodexPath, bun, cli, bunRuntimeSource), { encoding: "utf8", flag: "wx", mode: 0o600 });
1218
+ const stagedStat = lstatSync(staged);
1219
+ chmodSync(staged, 0o755);
1220
+ renameSync(staged, wrapperPath);
1221
+ renamed = true;
1222
+ // rename() preserves dev/ino and updates ctime, so identity is the inode
1223
+ // pair, captured from the file we created rather than from the destination.
1224
+ return { dev: stagedStat.dev, ino: stagedStat.ino };
1225
+ } finally {
1226
+ if (!renamed) {
1227
+ try { unlinkSync(staged); } catch { /* best-effort: nothing to clean up */ }
1228
+ }
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ /**
1234
+ * The fingerprint to record as "we wrote this", or `undefined` when the file at
1235
+ * `wrapperPath` is not the inode `writeShim()` created. Returning `undefined`
1236
+ * makes every rollback path treat the file as someone else's and leave it alone.
1237
+ */
1238
+ function ownedWrapperFingerprint(
1239
+ wrapperPath: string,
1240
+ written: { dev: number; ino: number } | undefined,
1241
+ ): ShimPathFingerprint | undefined {
1242
+ const probe = stableShimPathProbe(wrapperPath);
1243
+ if (!probe) return undefined;
1244
+ // Platforms that still write in place (Windows) have no staged identity; fall
1245
+ // back to the marker check they have always used.
1246
+ if (!written) return probe.prefix.includes(SHIM_MARKER) ? probe.fingerprint : undefined;
1247
+ if (probe.fingerprint.dev !== written.dev || probe.fingerprint.ino !== written.ino) return undefined;
1248
+ return probe.fingerprint;
1249
+ }
1250
+
1251
+ /**
1252
+ * Whether the file now at the wrapper path is one this transaction may unlink.
1253
+ *
1254
+ * The inode our write created is the authority. It stays ours through an in-place
1255
+ * truncation — a partial write we must clean up — and stops being ours the moment
1256
+ * someone renames a different file over the path, which is exactly the concurrent
1257
+ * updater we must not delete. Where no inode was recorded (Windows writes the
1258
+ * destination directly), fall back to the exact fingerprint recorded at the time.
1259
+ */
1260
+ function wrapperInodeIsOurs(
1261
+ wrapper: StableShimPathProbe,
1262
+ written: { dev: number; ino: number } | undefined,
1263
+ recorded: ShimPathFingerprint | undefined,
1264
+ ): boolean {
1265
+ // A different inode is conclusive: someone renamed their own file over ours.
1266
+ if (written && (wrapper.fingerprint.dev !== written.dev || wrapper.fingerprint.ino !== written.ino)) {
1267
+ return false;
624
1268
  }
1269
+ // Same inode is not sufficient on its own — an in-place truncation (shell `>`,
1270
+ // writeFileSync) keeps it while replacing the contents. When we recorded a full
1271
+ // fingerprint, require it to still match; that covers our own partial write,
1272
+ // whose fingerprint we record before anything can fail.
1273
+ if (recorded !== undefined) return sameFingerprint(wrapper.fingerprint, recorded);
1274
+ // No recorded fingerprint: only the inode identity we captured at write time can
1275
+ // speak for us, and there is nothing else to distinguish this file.
1276
+ return written !== undefined;
625
1277
  }
626
1278
 
627
1279
  function stateFiles(state: ShimState): ShimFileState[] {
@@ -658,12 +1310,37 @@ function refreshShimFile(file: ShimFileState): boolean {
658
1310
  }
659
1311
  if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
660
1312
  if (file.wrapperPath !== file.originalPath) return false;
661
- replaceOwnedBackup(file.wrapperPath, file.backupPath);
662
- writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
663
- return true;
1313
+ const replacement = stableShimPathProbe(file.wrapperPath);
1314
+ if (!replacement) return false;
1315
+ return applyGuardedRefreshTransaction([{
1316
+ file,
1317
+ expectedReplacement: replacement.fingerprint,
1318
+ sourcePath: file.wrapperPath,
1319
+ }]);
664
1320
  }
665
1321
  if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
666
1322
  writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
1323
+ const writtenWrapper = stableShimPathProbe(file.wrapperPath);
1324
+ if (!writtenWrapper || !writtenWrapper.prefix.includes(SHIM_MARKER)) {
1325
+ return false;
1326
+ }
1327
+ let unsafe: UnixShimProbeResult = null;
1328
+ let probeError: Error | null = null;
1329
+ try {
1330
+ unsafe = probeUnixShimFiles([file]);
1331
+ } catch (error) {
1332
+ probeError = error instanceof Error ? error : new Error(String(error));
1333
+ }
1334
+ const currentWrapper = stableShimPathProbe(file.wrapperPath);
1335
+ const wrapperChangedDuringProbe = !currentWrapper
1336
+ || !sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint);
1337
+ if (unsafe !== null || probeError || wrapperChangedDuringProbe) {
1338
+ if (currentWrapper && sameFingerprint(currentWrapper.fingerprint, writtenWrapper.fingerprint)) {
1339
+ unlinkSync(file.wrapperPath);
1340
+ }
1341
+ if (probeError) throw probeError;
1342
+ return false;
1343
+ }
667
1344
  return true;
668
1345
  }
669
1346
  if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
@@ -683,7 +1360,9 @@ interface GuardedRefreshOperation {
683
1360
  interface GuardedRefreshJournalEntry {
684
1361
  operation: GuardedRefreshOperation;
685
1362
  stagedOldBackupPath?: string;
1363
+ movedReplacementFingerprint?: ShimPathFingerprint;
686
1364
  replacementMovedToBackup: boolean;
1365
+ writtenWrapperFingerprint?: ShimPathFingerprint;
687
1366
  wrapperWriteStarted: boolean;
688
1367
  }
689
1368
 
@@ -875,14 +1554,35 @@ function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]):
875
1554
  }
876
1555
  };
877
1556
  for (const entry of [...journal].reverse()) {
1557
+ let sourceOccupied = false;
878
1558
  attempt(() => {
879
- if (entry.wrapperWriteStarted && existsSync(entry.operation.file.wrapperPath)) {
1559
+ const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
1560
+ // No marker-text fallback: the markers are public, so a concurrent updater's
1561
+ // wrapper carries them too. No recorded inode identity means not ours.
1562
+ const ownsWrapper = entry.wrapperWriteStarted
1563
+ && wrapper !== null
1564
+ && entry.writtenWrapperFingerprint !== undefined
1565
+ && sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
1566
+ if (ownsWrapper) {
880
1567
  unlinkSync(entry.operation.file.wrapperPath);
1568
+ } else {
1569
+ try {
1570
+ lstatSync(entry.operation.sourcePath);
1571
+ sourceOccupied = true;
1572
+ } catch (error) {
1573
+ if (fileErrorCode(error) !== "ENOENT") sourceOccupied = true;
1574
+ }
881
1575
  }
882
1576
  });
883
1577
  attempt(() => {
884
1578
  if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
885
- renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
1579
+ const movedReplacement = stableShimPathProbe(entry.operation.file.backupPath);
1580
+ if (!movedReplacement || !entry.movedReplacementFingerprint
1581
+ || !sameFingerprint(movedReplacement.fingerprint, entry.movedReplacementFingerprint)) {
1582
+ throw new Error("Codex shim guarded refresh backup changed during rollback");
1583
+ }
1584
+ if (sourceOccupied) unlinkSync(entry.operation.file.backupPath);
1585
+ else renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
886
1586
  }
887
1587
  });
888
1588
  attempt(() => {
@@ -902,6 +1602,8 @@ function applyGuardedRefreshTransaction(
902
1602
  const journal: GuardedRefreshJournalEntry[] = [];
903
1603
  let applyError: Error | null = null;
904
1604
  let fingerprintMismatch = false;
1605
+ let unsafeLauncher = false;
1606
+ let wrapperChangedDuringProbe = false;
905
1607
  const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
906
1608
 
907
1609
  for (const [index, operation] of operations.entries()) {
@@ -925,15 +1627,43 @@ function applyGuardedRefreshTransaction(
925
1627
  }
926
1628
  renameSync(operation.sourcePath, operation.file.backupPath);
927
1629
  entry.replacementMovedToBackup = true;
1630
+ const movedReplacement = stableShimPathProbe(operation.file.backupPath);
1631
+ if (!movedReplacement) throw new Error("Codex shim guarded refresh could not fingerprint the staged launcher");
1632
+ entry.movedReplacementFingerprint = movedReplacement.fingerprint;
928
1633
  entry.wrapperWriteStarted = true;
929
- writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
1634
+ const writtenInode = writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
1635
+ // Claim our own partial write before the hook can fail (see fresh install).
1636
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
1637
+ codexShimGuardedWriteHookForTests?.();
1638
+ // Re-check: unset means a concurrent writer owns the path now, so rollback
1639
+ // must leave it alone.
1640
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(operation.file.wrapperPath, writtenInode);
1641
+ if (!entry.writtenWrapperFingerprint && !writtenInode) {
1642
+ throw new Error("Codex shim guarded refresh could not fingerprint the generated wrapper");
1643
+ }
930
1644
  } catch (error) {
931
1645
  applyError = error instanceof Error ? error : new Error(String(error));
932
1646
  break;
933
1647
  }
934
1648
  }
935
1649
 
936
- if (!fingerprintMismatch && !applyError && commitState) {
1650
+ if (!fingerprintMismatch && !applyError) {
1651
+ try {
1652
+ unsafeLauncher = probeUnixShimFiles(operations.map(operation => operation.file)) !== null;
1653
+ } catch (error) {
1654
+ applyError = error instanceof Error ? error : new Error(String(error));
1655
+ }
1656
+ }
1657
+
1658
+ if (!fingerprintMismatch && !applyError && !unsafeLauncher) {
1659
+ wrapperChangedDuringProbe = journal.some(entry => {
1660
+ const wrapper = stableShimPathProbe(entry.operation.file.wrapperPath);
1661
+ return !wrapper || !entry.writtenWrapperFingerprint
1662
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
1663
+ });
1664
+ }
1665
+
1666
+ if (!fingerprintMismatch && !applyError && !unsafeLauncher && !wrapperChangedDuringProbe && commitState) {
937
1667
  try {
938
1668
  commitState();
939
1669
  } catch (error) {
@@ -941,7 +1671,7 @@ function applyGuardedRefreshTransaction(
941
1671
  }
942
1672
  }
943
1673
 
944
- if (fingerprintMismatch || applyError) {
1674
+ if (fingerprintMismatch || applyError || unsafeLauncher || wrapperChangedDuringProbe) {
945
1675
  const rollbackErrors = rollbackGuardedRefresh(journal);
946
1676
  if (applyError || rollbackErrors.length > 0) {
947
1677
  throw new AggregateError(
@@ -964,10 +1694,186 @@ function applyGuardedRefreshTransaction(
964
1694
  return true;
965
1695
  }
966
1696
 
1697
+ interface ObsoleteUnixShimJournalEntry {
1698
+ file: ShimFileState;
1699
+ stagedWrapperPath: string;
1700
+ priorWrapperFingerprint: ShimPathFingerprint;
1701
+ backingFingerprint: ShimPathFingerprint;
1702
+ writtenWrapperFingerprint?: ShimPathFingerprint;
1703
+ wrapperWriteStarted: boolean;
1704
+ }
1705
+
1706
+ function rollbackObsoleteUnixShimRefresh(journal: readonly ObsoleteUnixShimJournalEntry[]): Error[] {
1707
+ const errors: Error[] = [];
1708
+ const attempt = (operation: () => void): void => {
1709
+ try {
1710
+ operation();
1711
+ } catch (error) {
1712
+ errors.push(error instanceof Error ? error : new Error(String(error)));
1713
+ }
1714
+ };
1715
+ for (const entry of [...journal].reverse()) {
1716
+ attempt(() => {
1717
+ const wrapper = stableShimPathProbe(entry.file.wrapperPath);
1718
+ const ownsWrapper = entry.wrapperWriteStarted
1719
+ && wrapper !== null
1720
+ && (entry.writtenWrapperFingerprint
1721
+ ? sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
1722
+ : wrapper.prefix.includes(UNIX_SHIM_REVISION_MARKER));
1723
+ if (ownsWrapper) unlinkSync(entry.file.wrapperPath);
1724
+ });
1725
+ attempt(() => {
1726
+ if (!existsSync(entry.stagedWrapperPath)) return;
1727
+ if (existsSync(entry.file.wrapperPath)) unlinkSync(entry.stagedWrapperPath);
1728
+ else renameSync(entry.stagedWrapperPath, entry.file.wrapperPath);
1729
+ });
1730
+ }
1731
+ return errors;
1732
+ }
1733
+
1734
+ type ObsoleteUnixShimRefreshResult =
1735
+ | { installed: true; message: string }
1736
+ | { installed: false; deferred: boolean; message: string };
1737
+
1738
+ function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnixShimRefreshResult {
1739
+ if (process.platform === "win32") {
1740
+ return { installed: false, deferred: false, message: "Codex autostart shim is already current." };
1741
+ }
1742
+ const candidates = files.filter(file => {
1743
+ if (file.preserveOnly || file.wrapperPath !== file.originalPath || !existsSync(file.wrapperPath)) return false;
1744
+ const probe = stableShimPathProbe(file.wrapperPath);
1745
+ return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
1746
+ });
1747
+ if (candidates.length === 0) {
1748
+ return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
1749
+ }
1750
+
1751
+ const journal: ObsoleteUnixShimJournalEntry[] = [];
1752
+ const transactionId = `${process.pid}-${randomUUID()}`;
1753
+ let applyError: Error | null = null;
1754
+ for (const [index, file] of candidates.entries()) {
1755
+ const wrapper = stableShimPathProbe(file.wrapperPath);
1756
+ const backing = stableShimPathProbe(file.backupPath);
1757
+ if (!wrapper || !wrapper.prefix.includes(SHIM_MARKER) || isCurrentUnixShimProbe(wrapper) || !backing) {
1758
+ applyError = new Error("Codex autostart shim upgrade inputs changed before regeneration");
1759
+ break;
1760
+ }
1761
+ const entry: ObsoleteUnixShimJournalEntry = {
1762
+ file,
1763
+ stagedWrapperPath: `${file.wrapperPath}.upgrade-${transactionId}-${index}`,
1764
+ priorWrapperFingerprint: wrapper.fingerprint,
1765
+ backingFingerprint: backing.fingerprint,
1766
+ wrapperWriteStarted: false,
1767
+ };
1768
+ journal.push(entry);
1769
+ try {
1770
+ renameSync(file.wrapperPath, entry.stagedWrapperPath);
1771
+ const stagedWrapper = stableShimPathProbe(entry.stagedWrapperPath);
1772
+ if (!stagedWrapper
1773
+ || !sameFingerprintAfterRename(stagedWrapper.fingerprint, entry.priorWrapperFingerprint)) {
1774
+ throw new Error("Codex autostart shim upgrade could not fingerprint the staged wrapper");
1775
+ }
1776
+ entry.wrapperWriteStarted = true;
1777
+ const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
1778
+ const writtenWrapper = stableShimPathProbe(file.wrapperPath);
1779
+ if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) {
1780
+ throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper");
1781
+ }
1782
+ // Identity is the inode we created; a replacement that landed since the
1783
+ // rename leaves this unset so rollback treats the file as someone else's.
1784
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(file.wrapperPath, writtenInode);
1785
+ } catch (error) {
1786
+ applyError = error instanceof Error ? error : new Error(String(error));
1787
+ break;
1788
+ }
1789
+ }
1790
+
1791
+ let unsafe: UnixShimProbeResult = null;
1792
+ let probeError: Error | null = null;
1793
+ if (!applyError) {
1794
+ try {
1795
+ unsafe = probeUnixShimFiles(candidates);
1796
+ } catch (error) {
1797
+ probeError = error instanceof Error ? error : new Error(String(error));
1798
+ }
1799
+ }
1800
+ const changedDuringProbe = !applyError && !probeError && journal.some(entry => {
1801
+ const wrapper = stableShimPathProbe(entry.file.wrapperPath);
1802
+ const backing = stableShimPathProbe(entry.file.backupPath);
1803
+ return !wrapper || !entry.writtenWrapperFingerprint
1804
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)
1805
+ || !backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint);
1806
+ });
1807
+
1808
+ if (applyError || probeError || changedDuringProbe) {
1809
+ const rollbackErrors = rollbackObsoleteUnixShimRefresh(journal);
1810
+ if (applyError || probeError || rollbackErrors.length > 0) {
1811
+ throw new AggregateError(
1812
+ [...(applyError ? [applyError] : []), ...(probeError ? [probeError] : []), ...rollbackErrors],
1813
+ "Codex autostart shim upgrade failed",
1814
+ );
1815
+ }
1816
+ return { installed: false, deferred: true, message: "Codex autostart shim upgrade deferred because tracked launchers changed." };
1817
+ }
1818
+
1819
+ if (unsafe) {
1820
+ const cleanupErrors: Error[] = [];
1821
+ for (const entry of [...journal].reverse()) {
1822
+ try {
1823
+ const wrapper = stableShimPathProbe(entry.file.wrapperPath);
1824
+ if (!wrapper || !entry.writtenWrapperFingerprint
1825
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint)) {
1826
+ throw new Error("Codex autostart shim upgrade lost wrapper ownership before removal");
1827
+ }
1828
+ const backing = stableShimPathProbe(entry.file.backupPath);
1829
+ if (!backing || !sameFingerprint(backing.fingerprint, entry.backingFingerprint)) {
1830
+ throw new Error("Codex autostart shim upgrade backing launcher changed before restoration");
1831
+ }
1832
+ unlinkSync(entry.file.wrapperPath);
1833
+ renameSync(entry.file.backupPath, entry.file.originalPath);
1834
+ if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
1835
+ } catch (error) {
1836
+ cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
1837
+ }
1838
+ }
1839
+ if (cleanupErrors.length > 0) {
1840
+ throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade safety removal failed");
1841
+ }
1842
+ if (existsSync(statePath())) unlinkSync(statePath());
1843
+ return {
1844
+ installed: false,
1845
+ deferred: false,
1846
+ message: "Removed an obsolete Codex autostart shim because its saved launcher failed current validation. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.",
1847
+ };
1848
+ }
1849
+
1850
+ const cleanupErrors: Error[] = [];
1851
+ for (const entry of journal) {
1852
+ try {
1853
+ if (existsSync(entry.stagedWrapperPath)) unlinkSync(entry.stagedWrapperPath);
1854
+ } catch (error) {
1855
+ cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
1856
+ }
1857
+ }
1858
+ if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex autostart shim upgrade cleanup failed");
1859
+ return {
1860
+ installed: true,
1861
+ message: `Upgraded Codex autostart shim at ${candidates.map(file => file.wrapperPath).join(", ")} and validated the saved launcher.`,
1862
+ };
1863
+ }
1864
+
967
1865
  function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
968
1866
  const existing = readState();
969
1867
  if (existing) {
970
1868
  const files = stateFiles(existing);
1869
+ if (!options.expectedReplacements && process.platform !== "win32") {
1870
+ const hasObsoleteShim = files.some(file => {
1871
+ if (file.preserveOnly) return false;
1872
+ const probe = stableShimPathProbe(file.wrapperPath);
1873
+ return probe !== null && probe.prefix.includes(SHIM_MARKER) && !isCurrentUnixShimProbe(probe);
1874
+ });
1875
+ if (hasObsoleteShim) return refreshObsoleteUnixShims(files);
1876
+ }
971
1877
  if (options.expectedReplacements) {
972
1878
  const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
973
1879
  if (!operations || operations.length === 0) {
@@ -1034,9 +1940,107 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
1034
1940
  for (const target of targets) {
1035
1941
  if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
1036
1942
  }
1943
+ const freshJournal: FreshShimInstallJournalEntry[] = [];
1944
+ let freshApplyError: Error | null = null;
1037
1945
  for (const target of targets) {
1038
- if (existsSync(target.originalPath)) renameSync(target.originalPath, target.backupPath);
1039
- if (!target.preserveOnly) writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
1946
+ const entry: FreshShimInstallJournalEntry = {
1947
+ target,
1948
+ originalMovedToBackup: false,
1949
+ wrapperWriteStarted: false,
1950
+ };
1951
+ freshJournal.push(entry);
1952
+ try {
1953
+ if (existsSync(target.originalPath)) {
1954
+ renameSync(target.originalPath, target.backupPath);
1955
+ entry.originalMovedToBackup = true;
1956
+ // Metadata-only, and before the content probe: an empty or otherwise
1957
+ // unprobeable launcher must still be restorable during rollback.
1958
+ //
1959
+ // Only Unix reaches the rollback path (Windows rethrows freshApplyError
1960
+ // without rolling back), so only Unix may treat a missing fingerprint as
1961
+ // fatal. Throwing here on Windows would abort AFTER the original moved,
1962
+ // stranding the launcher at its backup path with nothing to restore it.
1963
+ const movedOriginalFingerprint = shimPathFingerprint(target.backupPath);
1964
+ if (movedOriginalFingerprint) entry.movedOriginalFingerprint = movedOriginalFingerprint;
1965
+ if (process.platform !== "win32") {
1966
+ if (!movedOriginalFingerprint) {
1967
+ throw new Error("Codex shim fresh install could not fingerprint the staged launcher");
1968
+ }
1969
+ const movedOriginal = stableShimPathProbe(target.backupPath);
1970
+ // A content probe still runs where it can, purely as a consistency
1971
+ // check: disagreement means the file moved under us mid-install.
1972
+ if (movedOriginal && !sameFingerprint(movedOriginal.fingerprint, movedOriginalFingerprint)) {
1973
+ throw new Error("Codex shim fresh install staged launcher changed while being fingerprinted");
1974
+ }
1975
+ }
1976
+ }
1977
+ if (!target.preserveOnly) {
1978
+ entry.wrapperWriteStarted = true;
1979
+ const writtenInode = writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
1980
+ entry.writtenWrapperInode = writtenInode;
1981
+ codexShimFreshWriteHookForTests?.();
1982
+ if (process.platform !== "win32") {
1983
+ if (!writtenInode) throw new Error("Codex shim fresh install could not fingerprint the generated wrapper");
1984
+ entry.writtenWrapperFingerprint = ownedWrapperFingerprint(target.wrapperPath, writtenInode);
1985
+ }
1986
+ }
1987
+ } catch (error) {
1988
+ freshApplyError = error instanceof Error ? error : new Error(String(error));
1989
+ break;
1990
+ }
1991
+ }
1992
+ if (process.platform !== "win32") {
1993
+ if (freshApplyError) {
1994
+ try {
1995
+ rollbackFreshShimInstall(freshJournal);
1996
+ } catch (rollbackError) {
1997
+ throw new AggregateError([freshApplyError, rollbackError], "Codex shim installation and rollback failed");
1998
+ }
1999
+ throw freshApplyError;
2000
+ }
2001
+ let unsafe: UnixShimProbeResult = null;
2002
+ let probeError: Error | null = null;
2003
+ try {
2004
+ unsafe = probeUnixShimFiles(targets);
2005
+ } catch (error) {
2006
+ probeError = error instanceof Error ? error : new Error(String(error));
2007
+ }
2008
+ if (probeError) {
2009
+ try {
2010
+ rollbackFreshShimInstall(freshJournal);
2011
+ } catch (rollbackError) {
2012
+ throw new AggregateError([probeError, rollbackError], "Codex shim probe and install rollback failed");
2013
+ }
2014
+ throw probeError;
2015
+ }
2016
+ const wrapperChangedDuringProbe = freshJournal.some(entry => {
2017
+ if (entry.target.preserveOnly) return false;
2018
+ const wrapper = stableShimPathProbe(entry.target.wrapperPath);
2019
+ return !wrapper || !entry.writtenWrapperFingerprint
2020
+ || !sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint);
2021
+ });
2022
+ if (unsafe || wrapperChangedDuringProbe) {
2023
+ rollbackFreshShimInstall(freshJournal);
2024
+ const reason = wrapperChangedDuringProbe
2025
+ ? "the generated wrapper changed during its validation probe"
2026
+ : unsafe === "recursive"
2027
+ ? "the saved launcher resolved back to the generated shim"
2028
+ : unsafe === "timeout"
2029
+ ? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
2030
+ : unsafe === "descendants"
2031
+ ? "the saved launcher left background descendants running after --version"
2032
+ : unsafe === "cleanup"
2033
+ ? "the saved launcher's probe process group could not be terminated cleanly"
2034
+ : "the saved launcher failed its --version probe";
2035
+ return {
2036
+ installed: false,
2037
+ message: wrapperChangedDuringProbe
2038
+ ? `Refusing Codex autostart shim because ${reason}. The concurrent launcher was preserved, and your previous launcher was kept alongside it as \`<codex>.opencodex-real\`; retry after the Codex update finishes, and remove that backup once you are satisfied the launcher on PATH is the one you want.`
2039
+ : `Refusing Codex autostart shim because ${reason}. The original launcher was restored; reinstall Codex as a concrete executable before enabling codexAutoStart.`,
2040
+ };
2041
+ }
2042
+ } else if (freshApplyError) {
2043
+ throw freshApplyError;
1040
2044
  }
1041
2045
  writeState(primaryState(targets));
1042
2046
  return {
@@ -1069,29 +2073,46 @@ export function autoRestoreCodexShim(options: {
1069
2073
 
1070
2074
  const files = stateFiles(state);
1071
2075
  const replacementProbes = new Map<string, StableShimPathProbe>();
2076
+ const obsoleteShimProbes = new Map<string, StableShimPathProbe>();
1072
2077
  const seen = new Set<string>();
1073
2078
  let healthyCount = 0;
1074
2079
  for (const file of files) {
1075
2080
  if (seen.has(file.wrapperPath)) return { status: "ineligible" };
1076
2081
  seen.add(file.wrapperPath);
1077
2082
  if (file.preserveOnly) {
1078
- if (!existsSync(file.backupPath) || existsSync(file.originalPath)) return { status: "ineligible" };
2083
+ if (!existsSync(file.backupPath) || existsSync(file.originalPath)) {
2084
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2085
+ }
1079
2086
  continue;
1080
2087
  }
1081
- if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) return { status: "ineligible" };
2088
+ if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) {
2089
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2090
+ }
1082
2091
  const probe = stableShimPathProbe(file.wrapperPath);
1083
2092
  if (!probe) return { status: "deferred" };
1084
2093
  if (probe.prefix.includes(SHIM_MARKER)) {
1085
- if (!isHealthyShimProbe(probe, state.platform)) return { status: "ineligible" };
2094
+ if (!isHealthyShimProbe(probe, state.platform)) {
2095
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2096
+ }
2097
+ if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
2098
+ obsoleteShimProbes.set(file.wrapperPath, probe);
2099
+ continue;
2100
+ }
1086
2101
  healthyCount += 1;
1087
2102
  continue;
1088
2103
  }
2104
+ // A surviving backup would otherwise let the replacement path below wrap the
2105
+ // version manager's NEW binary as a fresh original — the same adoption the
2106
+ // missing-backup case refuses, arriving through the back door.
2107
+ if (isVersionManagerOwnedCodexPath(file.wrapperPath)) {
2108
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2109
+ }
1089
2110
  replacementProbes.set(file.wrapperPath, probe);
1090
2111
  }
1091
2112
 
1092
- if (replacementProbes.size === 0) return { status: "healthy" };
2113
+ if (replacementProbes.size === 0 && obsoleteShimProbes.size === 0) return { status: "healthy" };
1093
2114
  if (!options.enabled()) return { status: "disabled" };
1094
- if (files.length > 1 && healthyCount > 0) {
2115
+ if (files.length > 1 && (healthyCount > 0 || (replacementProbes.size > 0 && obsoleteShimProbes.size > 0))) {
1095
2116
  return {
1096
2117
  status: "deferred",
1097
2118
  message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
@@ -1103,6 +2124,19 @@ export function autoRestoreCodexShim(options: {
1103
2124
  try {
1104
2125
  options.afterRestoreLockAcquired?.();
1105
2126
  (options.stabilitySleep ?? Bun.sleepSync)(CODEX_SHIM_REPLACEMENT_STABLE_MS);
2127
+ if (obsoleteShimProbes.size > 0) {
2128
+ for (const [path, firstProbe] of obsoleteShimProbes) {
2129
+ const secondProbe = stableShimPathProbe(path);
2130
+ if (!secondProbe || isCurrentUnixShimProbe(secondProbe)
2131
+ || !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
2132
+ }
2133
+ const result = refreshObsoleteUnixShims(files);
2134
+ return result.installed
2135
+ ? { status: "restored", message: result.message }
2136
+ : result.deferred
2137
+ ? { status: "deferred", message: result.message }
2138
+ : { status: "ineligible", message: result.message };
2139
+ }
1106
2140
  const expectedReplacements = new Map<string, ShimPathFingerprint>();
1107
2141
  for (const [path, firstProbe] of replacementProbes) {
1108
2142
  const secondProbe = stableShimPathProbe(path);