@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -0,0 +1,28 @@
1
+ import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
2
+ import type { DesktopExec } from "../desktop-app/types";
3
+ import { activateWindowsCodexCompatibility, validatedCompatibilityPacUrl } from "./windows-package-command";
4
+ export { compatibilityActivationScript, validatedCompatibilityPacUrl } from "./windows-package-command";
5
+
6
+ export interface CompatibilityLaunchIo {
7
+ exec?: DesktopExec;
8
+ platform?: string;
9
+ }
10
+ export type CompatibilityLaunchResult =
11
+ | { status: "started"; pid: number; packageFullName: string }
12
+ | { status: "refused"; reason: "unsupported_platform" | "test_environment" | "package_unavailable" | "app_running" | "process_probe_failed" | "activation_failed" };
13
+
14
+ /** Never quits an app, launches the raw EXE, changes login or installs a watcher. */
15
+ export function launchWindowsCodexCompatibility(pacUrl: string, io: CompatibilityLaunchIo = {}): CompatibilityLaunchResult {
16
+ validatedCompatibilityPacUrl(pacUrl);
17
+ if ((io.platform ?? process.platform) !== "win32") return { status: "refused", reason: "unsupported_platform" };
18
+ if (process.env.OCX_TEST_HOME_GUARD === "1" && !io.exec) return { status: "refused", reason: "test_environment" };
19
+ const exec = io.exec ?? windowsDefaultExec;
20
+ const install = windowsDesktopAppAdapter.discover(exec);
21
+ if (!install) return { status: "refused", reason: "package_unavailable" };
22
+ const processes = windowsDesktopAppAdapter.listProcesses(exec, install);
23
+ if (processes === null) return { status: "refused", reason: "process_probe_failed" };
24
+ if (processes.length > 0) return { status: "refused", reason: "app_running" };
25
+ try {
26
+ return { status: "started", ...activateWindowsCodexCompatibility(exec, install, pacUrl) };
27
+ } catch { return { status: "refused", reason: "activation_failed" }; }
28
+ }
@@ -1831,7 +1831,7 @@ export function countPendingOpencodexHistory(
1831
1831
  : { pendingRows: 0, backupEntries };
1832
1832
  }
1833
1833
  try {
1834
- const db = new Database(stateDbPath, { readonly: true });
1834
+ const db = openCodexStateForPreflight(stateDbPath);
1835
1835
  try {
1836
1836
  db.exec("PRAGMA busy_timeout = 100");
1837
1837
  // Prove the expected history schema is readable without counting unowned routed rows.
@@ -1,7 +1,7 @@
1
1
  import type { AtomicWriteHooks } from "../../config/atomic-write";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { withConfigMutationLockSync } from "../../config";
4
- import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
4
+ import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, removeConfigWriteEntry, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
5
5
  import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "../paths";
6
6
  import { getCodexHome } from "../paths";
7
7
  import { codexHomeIsAbsent } from "../codex-home-owner";
@@ -16,6 +16,9 @@ export function publishCodexArtifact<T>(path: string, held: LockHandle | undefin
16
16
  if (!held) throw new Error("Codex artifact publication requires a held configuration lock");
17
17
  return publishConfigWriteTarget(CODEX_CONFIG_PATH, held, path, run);
18
18
  }
19
+ export function removeCodexProfileEntry(held: LockHandle): void {
20
+ removeConfigWriteEntry(CODEX_CONFIG_PATH, held, CODEX_PROFILE_PATH);
21
+ }
19
22
  /** Courtesy cleanup re-reads provider ownership inside the same config section. */
20
23
  export function cleanExternalProviderJournal(beforeClientWrite?: () => void): void {
21
24
  if (codexHomeIsAbsent(getCodexHome())) return;
@@ -1,8 +1,8 @@
1
- import { beginCodexWriteSection, publishCodexArtifact } from "./config-write-section";
2
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
1
+ import { beginCodexWriteSection, publishCodexArtifact, removeCodexProfileEntry } from "./config-write-section";
2
+ import { existsSync, lstatSync, readFileSync } from "node:fs";
3
3
  import { normalizeStructuralWhitespace, rootAssignmentKey, rootSourceLines, sourceAssignment, sourceText } from "../toml-source-lines";
4
4
  import { atomicWriteFile } from "../../config";
5
- import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld } from "../config-write-lock";
5
+ import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld, withConfigWriteRecovery } from "../config-write-lock";
6
6
  import type { LockHandle } from "../config-write-lock";
7
7
  import {
8
8
  REALTIME_WS_BASE_URL_KEY,
@@ -206,15 +206,26 @@ export function removeCodexConfig(
206
206
  }
207
207
  const locked = withConfigWriteLockHeld(CODEX_CONFIG_PATH, options.heldConfigWriteLock, held => {
208
208
  beginCodexWriteSection(held);
209
+ if (!options.preserveProfile) {
210
+ try {
211
+ if (lstatSync(CODEX_PROFILE_PATH).isSymbolicLink()) {
212
+ return { success: false, message: "Codex profile is a symbolic link; configuration and recovery evidence were preserved." };
213
+ }
214
+ } catch (error) {
215
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
216
+ }
217
+ }
209
218
  if (!existsSync(CODEX_CONFIG_PATH)) {
210
219
  if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
211
- publishCodexArtifact(CODEX_PROFILE_PATH, held, (path, hooks) => { hooks.validateBeforeRename?.(path); unlinkSync(path); hooks.afterRename?.(path); });
220
+ removeCodexProfileEntry(held);
212
221
  return {
213
222
  success: true,
214
223
  message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
215
224
  };
216
225
  }
217
- const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
226
+ const preimage = readFileSync(CODEX_CONFIG_PATH);
227
+ return withConfigWriteRecovery(CODEX_CONFIG_PATH, held, () => {
228
+ const rawContent = preimage.toString("utf-8");
218
229
  // Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
219
230
  // The unchanged fast path compares in LF space so an untouched file is never rewritten.
220
231
  const eol = dominantEol(rawContent);
@@ -248,7 +259,7 @@ export function removeCodexConfig(
248
259
  publishCodexArtifact(CODEX_CONFIG_PATH, held, (path, hooks) => atomicWriteFile(path, applyEol(finalContent, eol), undefined, hooks));
249
260
  }
250
261
  if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
251
- publishCodexArtifact(CODEX_PROFILE_PATH, held, (path, hooks) => { hooks.validateBeforeRename?.(path); unlinkSync(path); hooks.afterRename?.(path); });
262
+ removeCodexProfileEntry(held);
252
263
  const retainedNote = retainedBlock === null
253
264
  ? ""
254
265
  : " Kept [model_providers.opencodex] so conversations already tagged opencodex still open;"
@@ -272,6 +283,7 @@ export function removeCodexConfig(
272
283
  message: removedMessage,
273
284
  ...(retainedBlock === null ? {} : { retainedProviderTable: retainedBlock.replace(/\n+$/, "").split("\n") }),
274
285
  };
286
+ }, { preimage, requireMatchingPreimage: true });
275
287
  });
276
288
  if (!locked.ok) return { success: false, retryable: locked.error === "locked", message: configWriteLockFailureMessage(locked) };
277
289
  return locked.value;
@@ -1,7 +1,7 @@
1
1
  import { beginCodexWriteSection, publishCodexArtifact } from "./inject/config-write-section";
2
2
  import { createHash } from "node:crypto";
3
3
  import type { AtomicWriteHooks } from "../config/atomic-write";
4
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
4
+ import { existsSync, lstatSync, readFileSync, unlinkSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { atomicWriteFile } from "../config";
7
7
  import { assertCodexHomeOwner, codexHomeIsAbsent, CODEX_HOME_JOURNAL_FILE, CodexHomeOwnerRefusal, opencodexHomeForInjection, readCodexHomeJournal, type CodexHomeOwnerRefusalReason } from "./codex-home-owner";
@@ -347,6 +347,21 @@ export function restoreJournalState(
347
347
  };
348
348
  }
349
349
 
350
+ // A matching byte hash does not authorize following a newly substituted profile link.
351
+ // Refuse the complete pair before restoring config or deleting its recovery journal.
352
+ let profileIsSymlink = false;
353
+ if (profileUnchanged && (!comparison.profileAlreadyOriginal || comparison.originalProfile === null)) {
354
+ try { profileIsSymlink = lstatSync(CODEX_PROFILE_PATH).isSymbolicLink(); }
355
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
356
+ }
357
+ if (profileIsSymlink) {
358
+ return {
359
+ configRestored: comparison.configAlreadyOriginal, profileRestored: false,
360
+ configRewritten: false, profileRewritten: false,
361
+ configChanged: !configUnchanged, profileChanged: false,
362
+ complete: false, unverified: false, profileRestoreFailed: true as const,
363
+ };
364
+ }
350
365
  assertCodexHomeOwner(CODEX_HOME);
351
366
  let configRestored = comparison.configAlreadyOriginal;
352
367
  let profileRestored = comparison.profileAlreadyOriginal;
@@ -31,6 +31,12 @@ import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch
31
31
  import { withNativeMainExclusiveClaim } from "./native-main-claim";
32
32
  import { resolveNativeProfileContext } from "./native-profile-store";
33
33
  import { isNativeMainTrafficBlocked } from "./native-profile-startup";
34
+ import { loadConfig } from "../config";
35
+ import {
36
+ requestCodexAuthStatus,
37
+ resolveNativeMainRefreshMode,
38
+ type CodexAuthStatus,
39
+ } from "./native-main-refresh-mode";
34
40
 
35
41
  export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
36
42
 
@@ -48,13 +54,11 @@ type MainAuthJsonCredential = {
48
54
  path: string;
49
55
  rawSha256: string;
50
56
  /**
51
- * Filesystem identity of the file the hash was taken from (#2999).
52
- *
53
- * A content hash cannot tell "unchanged" from "replaced with a file that happens to
54
- * hash the same", and more importantly it is read at a different instant than the
55
- * rename. Carrying dev+ino lets the pre-rename guard ask the sharper question: is this
56
- * still the same file, not merely one with the same bytes. `null` when the target could
57
- * not be stat'ed, which is treated as "cannot prove identity" rather than "matches".
57
+ * Filesystem identity of the file the hash was taken from. It catches a writer that
58
+ * replaces auth.json by rename with identical bytes. It does not see Codex's own writes:
59
+ * Codex rewrites auth.json in place (truncate + write), which keeps dev and ino, so only
60
+ * the content hash notices those, and only once the new bytes are visible (#2999).
61
+ * `null` when the target could not be stat'ed, which fails closed.
58
62
  */
59
63
  identity: { dev: number; ino: number } | null;
60
64
  root: Record<string, unknown>;
@@ -66,6 +70,8 @@ type MainAuthJsonCredential = {
66
70
 
67
71
  export interface NativeMainRefreshDependencies {
68
72
  refreshToken?: (refreshToken: string, options: { signal: AbortSignal }) => Promise<OAuthCredentials>;
73
+ /** Codex-mode seam: one app-server `getAuthStatus` call. Used only when Codex owns the refresh. */
74
+ codexAuthStatus?: (options: { refresh: boolean; signal: AbortSignal }) => Promise<CodexAuthStatus>;
69
75
  signal?: AbortSignal;
70
76
  /** Metadata-only refresh must not retract a concurrent traffic reauth quarantine. */
71
77
  preserveReauth?: boolean;
@@ -264,10 +270,9 @@ function assertMainAuthJsonSnapshotUnchanged(expected: MainAuthJsonCredential):
264
270
  if (!current || current.path !== expected.path || current.rawSha256 !== expected.rawSha256) {
265
271
  throw new MainAuthJsonChangedDuringRefreshError();
266
272
  }
267
- // Identity, not just content (#2999). A writer can land between this check and the
268
- // rename, and rename(2) replaces unconditionally - so the narrower the question asked
269
- // here, the smaller the window where a Codex login gets silently overwritten. An
270
- // unreadable identity on either side fails closed: unprovable is not the same as equal.
273
+ // Also refuse a rename-based replacement with identical bytes. An unreadable identity on
274
+ // either side fails closed. This narrows the window; rename(2) still replaces
275
+ // unconditionally after this check returns.
271
276
  assertMainAuthJsonIdentityUnchanged(expected);
272
277
  }
273
278
 
@@ -288,9 +293,7 @@ function persistRefreshedMainAuthJson(
288
293
  assertNotRealCodexHomeUnderTest(resolveCodexHomeDir());
289
294
  const accessToken = refreshed.access;
290
295
  const refreshToken = refreshed.refresh || expected.refreshToken!;
291
- const chatgptAccountId = refreshed.accountId
292
- ?? extractAccountId(undefined, accessToken)
293
- ?? expected.chatgptAccountId;
296
+ const { chatgptAccountId } = inFlightMainAccountToken(expected, refreshed);
294
297
  const tokens = {
295
298
  ...expected.tokens,
296
299
  access_token: accessToken,
@@ -475,6 +478,12 @@ async function resolveMainAccountToken(
475
478
  const signal = dependencies.signal
476
479
  ? AbortSignal.any([dependencies.signal, refreshTimeout])
477
480
  : refreshTimeout;
481
+ // An injected exchange is the guarded writer by definition. Otherwise Codex refreshes its own
482
+ // login whenever a supported binary exists, unless the operator chose "opencodex" (#2999).
483
+ const refreshMode = dependencies.refreshToken
484
+ ? null
485
+ : await resolveNativeMainRefreshMode(loadConfig());
486
+ if (signal.aborted) throw signal.reason;
478
487
  const lockKey = refreshGrantFingerprintForToken(initial.refreshToken);
479
488
  // Two locks, because they guard two different things that live in two different
480
489
  // homes. `withCodexRefreshFileLock` is keyed on the grant fingerprint and lives
@@ -508,6 +517,11 @@ async function resolveMainAccountToken(
508
517
  && mainAccessTokenFresh(locked.accessToken, Date.now(), MAIN_TOKEN_REFRESH_SKEW_MS)) {
509
518
  return { accessToken: locked.accessToken!, chatgptAccountId: locked.chatgptAccountId };
510
519
  }
520
+ if (refreshMode?.mode === "codex") {
521
+ const command = refreshMode.command!;
522
+ return refreshThroughCodex(locked, rejectedAccessToken, dependencies, refreshTimeout,
523
+ dependencies.codexAuthStatus ?? (options => requestCodexAuthStatus(command, options)));
524
+ }
511
525
  const refresh = dependencies.refreshToken ?? refreshNativeMainGrant;
512
526
  let refreshed: OAuthCredentials;
513
527
  if (signal.aborted) throw signal.reason;
@@ -539,7 +553,19 @@ async function resolveMainAccountToken(
539
553
  // Once the exchange starts, only its own timeout may abort it: a successful
540
554
  // rotating grant retires the old refresh token. Save the only live grant before
541
555
  // honoring caller cancellation, while retaining the external-writer guards.
542
- const result = persistRefreshedMainAuthJson(locked, refreshed);
556
+ let result: { accessToken: string; chatgptAccountId: string };
557
+ try {
558
+ result = persistRefreshedMainAuthJson(locked, refreshed);
559
+ } catch (cause) {
560
+ if (!(cause instanceof MainAuthJsonChangedDuringRefreshError)) throw cause;
561
+ // Another writer owns auth.json now (#2999). The exchange already succeeded, so
562
+ // failing the request would waste it. Serve the new access token to this request
563
+ // only: leave the file as the other writer left it, keep nothing as the main
564
+ // credential, and do not retire the grant.
565
+ noteNativeMainRefresh("publish-refused-served-in-flight");
566
+ if (dependencies.signal?.aborted) throw dependencies.signal.reason;
567
+ return inFlightMainAccountToken(locked, refreshed);
568
+ }
543
569
  // A permitted in-flight success speaks only for its own profile and grant.
544
570
  clearMainRefreshGrantRejection(mainRefreshGrantKey(locked)!);
545
571
  if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
@@ -556,6 +582,64 @@ async function resolveMainAccountToken(
556
582
  }
557
583
  }
558
584
 
585
+ /** The refreshed bearer for one request whose save was refused; never stored anywhere. */
586
+ function inFlightMainAccountToken(
587
+ expected: MainAuthJsonCredential,
588
+ refreshed: OAuthCredentials,
589
+ ): { accessToken: string; chatgptAccountId: string } {
590
+ return {
591
+ accessToken: refreshed.access,
592
+ chatgptAccountId: refreshed.accountId
593
+ ?? extractAccountId(undefined, refreshed.access)
594
+ ?? expected.chatgptAccountId,
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Codex mode (#2999): Codex refreshes and stores its own login; OpenCodex only uses the bearer.
600
+ *
601
+ * auth.json is not read again after the call. In keyring mode Codex deletes the file once the
602
+ * keyring save succeeds, so the RPC answer is the only place the new token exists for us.
603
+ * The call runs on the refresh timeout alone: killing app-server mid-exchange could retire
604
+ * the grant before Codex saves its successor.
605
+ */
606
+ async function refreshThroughCodex(
607
+ locked: MainAuthJsonCredential,
608
+ rejectedAccessToken: string | undefined,
609
+ dependencies: NativeMainRefreshDependencies,
610
+ refreshTimeout: AbortSignal,
611
+ authStatus: NonNullable<NativeMainRefreshDependencies["codexAuthStatus"]>,
612
+ ): Promise<{ accessToken: string; chatgptAccountId: string }> {
613
+ let status: CodexAuthStatus;
614
+ try {
615
+ status = await authStatus({ refresh: true, signal: refreshTimeout });
616
+ } catch (cause) {
617
+ noteNativeMainRefresh("codex-unavailable");
618
+ throw new MainAccountTokenRefreshError("transient", { cause });
619
+ }
620
+ if (status.authMethod === "chatgpt" && !status.authToken) {
621
+ // The only permanent-failure signal the response carries: Codex withholds the token once
622
+ // its refresh was refused for good. The specific OAuth code is not on the wire.
623
+ noteNativeMainRefresh("codex-reauth");
624
+ markMainRefreshGrantRejected(mainRefreshGrantKey(locked)!);
625
+ throw new MainAccountTokenRefreshError("reauth");
626
+ }
627
+ const token = status.authToken;
628
+ if (status.authMethod !== "chatgpt" || !token) {
629
+ noteNativeMainRefresh("codex-no-chatgpt-login");
630
+ throw new MainAccountTokenRefreshError("transient");
631
+ }
632
+ // A transient Codex refresh failure still answers with its cached bearer.
633
+ if (token === rejectedAccessToken || !mainAccessTokenFresh(token, Date.now(), 0)) {
634
+ noteNativeMainRefresh("codex-transient");
635
+ throw new MainAccountTokenRefreshError("transient");
636
+ }
637
+ noteNativeMainRefresh("codex-ok");
638
+ if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
639
+ if (dependencies.signal?.aborted) throw dependencies.signal.reason;
640
+ return { accessToken: token, chatgptAccountId: extractAccountId(undefined, token) ?? locked.chatgptAccountId };
641
+ }
642
+
559
643
  /** Refresh the CLI-owned native credential before upstream I/O and publish it atomically. */
560
644
  export function getValidMainAccountToken(
561
645
  dependencies: NativeMainRefreshDependencies = {},
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Who refreshes the native main ChatGPT credential (#2999).
3
+ *
4
+ * Codex owns `CODEX_HOME/auth.json`. It rewrites the file in place and serializes its own
5
+ * refreshes only inside one process, so any second writer can race it, and a lost race
6
+ * retires the grant. In "codex" mode OpenCodex never writes the file: it asks the Codex
7
+ * binary to refresh through `app-server` `getAuthStatus` and uses the bearer that call
8
+ * returns. In "opencodex" mode the guarded writer in `main-account.ts` refreshes as before.
9
+ *
10
+ * The default is "codex" only when a version-checked binary is available. Without one, or
11
+ * when the operator sets `nativeMainRefresh: "opencodex"`, the guarded writer stays.
12
+ */
13
+ import { codexExecInvocation } from "./exec-invocation";
14
+ import { resolveCodexHomeDir } from "./home";
15
+ import { compareCodexVersions, displayCodexRuntimePath, resolveCodexRuntimeAsync } from "./runtime";
16
+ import type { ResolveCodexRuntimeResult } from "./runtime";
17
+ import { isTestHomeGuardArmed } from "../lib/test-home-guard";
18
+ import type { OcxConfig } from "../types/config";
19
+
20
+ export type NativeMainRefreshMode = "codex" | "opencodex";
21
+
22
+ /**
23
+ * Supported floor for app-server `getAuthStatus` with `includeToken`/`refreshToken` that
24
+ * withholds the token after a permanent refresh failure, the only reauth signal on the wire.
25
+ * Releases from about 0.118 already withhold it; 0.130.0 is the first with the handler in
26
+ * `request_processors/account_processor.rs` and is chosen conservatively.
27
+ */
28
+ export const CODEX_AUTH_STATUS_MIN_VERSION = "0.130.0";
29
+
30
+ export type NativeMainRefreshReason =
31
+ | "codex-compatible"
32
+ | "configured-opencodex"
33
+ | "codex-missing"
34
+ | "codex-too-old"
35
+ | "test-guard";
36
+
37
+ export interface NativeMainRefreshResolution {
38
+ readonly mode: NativeMainRefreshMode;
39
+ readonly configured: NativeMainRefreshMode | "auto";
40
+ readonly reason: NativeMainRefreshReason;
41
+ readonly command?: string;
42
+ readonly version?: string | null;
43
+ }
44
+
45
+ export interface CodexAuthStatus {
46
+ readonly authMethod: string | null;
47
+ readonly authToken: string | null;
48
+ readonly requiresOpenaiAuth: boolean | null;
49
+ }
50
+
51
+ export class CodexAuthStatusError extends Error {
52
+ constructor(message: string, options?: ErrorOptions) {
53
+ super(message, options);
54
+ this.name = "CodexAuthStatusError";
55
+ }
56
+ }
57
+
58
+ let resolutionForTests: NativeMainRefreshResolution | null = null;
59
+
60
+ /** Test seam: pin the resolution so no test spawns the developer's real Codex binary. */
61
+ export function setNativeMainRefreshResolutionForTests(value: NativeMainRefreshResolution | null): void {
62
+ resolutionForTests = value;
63
+ }
64
+
65
+ function configuredMode(config: Pick<OcxConfig, "nativeMainRefresh"> | null | undefined): NativeMainRefreshMode | "auto" {
66
+ const value = config?.nativeMainRefresh;
67
+ return value === "codex" || value === "opencodex" ? value : "auto";
68
+ }
69
+
70
+ export async function resolveNativeMainRefreshMode(
71
+ config: Pick<OcxConfig, "nativeMainRefresh"> | null | undefined,
72
+ deps: { resolveRuntime?: () => Promise<ResolveCodexRuntimeResult> } = {},
73
+ ): Promise<NativeMainRefreshResolution> {
74
+ if (resolutionForTests) return resolutionForTests;
75
+ const configured = configuredMode(config);
76
+ if (configured === "opencodex") return { mode: "opencodex", configured, reason: "configured-opencodex" };
77
+ // Tests run against throwaway CODEX_HOMEs; delegating would spawn the real binary.
78
+ if (!deps.resolveRuntime && isTestHomeGuardArmed()) return { mode: "opencodex", configured, reason: "test-guard" };
79
+ const resolved = await (deps.resolveRuntime ?? (() => resolveCodexRuntimeAsync({ discoverAlternatives: false })))();
80
+ const { command, version, source } = resolved.runtime;
81
+ if (source === "fallback" && version === null) {
82
+ return { mode: "opencodex", configured, reason: "codex-missing" };
83
+ }
84
+ if (version === null || compareCodexVersions(version, CODEX_AUTH_STATUS_MIN_VERSION) < 0) {
85
+ return { mode: "opencodex", configured, reason: "codex-too-old", command, version };
86
+ }
87
+ return { mode: "codex", configured, reason: "codex-compatible", command, version };
88
+ }
89
+
90
+ /**
91
+ * One `getAuthStatus` round trip against a short-lived `codex app-server` on stdio.
92
+ *
93
+ * `model_provider="openai"` is forced because an authless Desktop injection sets
94
+ * `requires_openai_auth = false` on the OpenCodex provider, and then the server reports no
95
+ * credential at all. The process inherits CODEX_HOME so it reads the same login (file or
96
+ * keyring) Codex itself uses.
97
+ */
98
+ export async function requestCodexAuthStatus(
99
+ command: string,
100
+ options: { refresh: boolean; signal: AbortSignal; codexHome?: string },
101
+ ): Promise<CodexAuthStatus> {
102
+ if (options.signal.aborted) throw options.signal.reason;
103
+ const invocation = codexExecInvocation(command, ["-c", 'model_provider="openai"', "app-server"]);
104
+ let proc: ReturnType<typeof Bun.spawn<"pipe", "pipe", "ignore">>;
105
+ try {
106
+ proc = Bun.spawn([invocation.file, ...invocation.args], {
107
+ env: { ...process.env, CODEX_HOME: options.codexHome ?? resolveCodexHomeDir() },
108
+ stdin: "pipe",
109
+ stdout: "pipe",
110
+ stderr: "ignore",
111
+ windowsVerbatimArguments: invocation.options.windowsVerbatimArguments,
112
+ windowsHide: true,
113
+ });
114
+ } catch (cause) {
115
+ throw new CodexAuthStatusError("could not start codex app-server", { cause });
116
+ }
117
+ // Abort settles the call by itself. Waiting for stdout to close would hang on a child that
118
+ // ignores SIGTERM or a launcher descendant that keeps the pipe open.
119
+ let onAbort = () => {};
120
+ const aborted = new Promise<never>((_, reject) => {
121
+ onAbort = () => reject(options.signal.reason);
122
+ options.signal.addEventListener("abort", onAbort, { once: true });
123
+ });
124
+ aborted.catch(() => { /* observed through the race below */ });
125
+ try {
126
+ const send = (message: unknown) => proc.stdin.write(JSON.stringify(message) + "\n");
127
+ send({ id: 1, method: "initialize", params: { clientInfo: { name: "opencodex", title: null, version: "0" } } });
128
+ send({ method: "initialized" });
129
+ send({ id: 2, method: "getAuthStatus", params: { includeToken: true, refreshToken: options.refresh } });
130
+ await Promise.race([proc.stdin.flush(), aborted]);
131
+ const response = await Promise.race([readResponse(proc.stdout, 2), aborted]);
132
+ if (options.signal.aborted) throw options.signal.reason;
133
+ return parseAuthStatus(response);
134
+ } catch (cause) {
135
+ if (options.signal.aborted) throw options.signal.reason;
136
+ if (cause instanceof CodexAuthStatusError) throw cause;
137
+ throw new CodexAuthStatusError("codex app-server getAuthStatus failed", { cause });
138
+ } finally {
139
+ options.signal.removeEventListener("abort", onAbort);
140
+ stopAppServer(proc);
141
+ }
142
+ }
143
+
144
+ const APP_SERVER_KILL_GRACE_MS = 2_000;
145
+
146
+ /** SIGTERM now, SIGKILL if the child is still alive after a grace period; never awaited. */
147
+ function stopAppServer(proc: { stdin: { end(): unknown }; kill(signal?: number | NodeJS.Signals): void; exited: Promise<number> }): void {
148
+ try { proc.stdin.end(); } catch { /* already closed */ }
149
+ let exited = false;
150
+ void proc.exited.then(() => { exited = true; }, () => { exited = true; });
151
+ try { proc.kill("SIGTERM"); } catch { /* already gone */ }
152
+ const timer = setTimeout(() => {
153
+ if (!exited) try { proc.kill("SIGKILL"); } catch { /* already gone */ }
154
+ }, APP_SERVER_KILL_GRACE_MS);
155
+ (timer as { unref?: () => void }).unref?.();
156
+ }
157
+
158
+ const MAX_APP_SERVER_LINE_BYTES = 1_048_576;
159
+
160
+ async function readResponse(stream: ReadableStream<Uint8Array>, id: number): Promise<Record<string, unknown>> {
161
+ const decoder = new TextDecoder();
162
+ let buffered = "";
163
+ for await (const chunk of stream) {
164
+ buffered += decoder.decode(chunk, { stream: true });
165
+ let newline: number;
166
+ while ((newline = buffered.indexOf("\n")) >= 0) {
167
+ const line = buffered.slice(0, newline).trim();
168
+ buffered = buffered.slice(newline + 1);
169
+ if (!line) continue;
170
+ let message: unknown;
171
+ try { message = JSON.parse(line); } catch { continue; }
172
+ if (!message || typeof message !== "object") continue;
173
+ const record = message as Record<string, unknown>;
174
+ if (record.id !== id) continue;
175
+ if (record.error !== undefined) throw new CodexAuthStatusError("codex app-server rejected getAuthStatus");
176
+ if (!record.result || typeof record.result !== "object") {
177
+ throw new CodexAuthStatusError("codex app-server returned no getAuthStatus result");
178
+ }
179
+ return record.result as Record<string, unknown>;
180
+ }
181
+ if (buffered.length > MAX_APP_SERVER_LINE_BYTES) throw new CodexAuthStatusError("codex app-server line too long");
182
+ }
183
+ throw new CodexAuthStatusError("codex app-server exited before answering getAuthStatus");
184
+ }
185
+
186
+ function parseAuthStatus(result: Record<string, unknown>): CodexAuthStatus {
187
+ const authMethod = typeof result.authMethod === "string" ? result.authMethod : null;
188
+ const authToken = typeof result.authToken === "string" && result.authToken ? result.authToken : null;
189
+ const requiresOpenaiAuth = typeof result.requiresOpenaiAuth === "boolean" ? result.requiresOpenaiAuth : null;
190
+ return { authMethod, authToken, requiresOpenaiAuth };
191
+ }
192
+
193
+ /** `ocx doctor` lines for the active refresh owner. Never includes a credential. */
194
+ export function formatNativeMainRefreshDoctorLines(resolution: NativeMainRefreshResolution): string[] {
195
+ const configured = resolution.configured === "auto" ? "auto" : `nativeMainRefresh: "${resolution.configured}"`;
196
+ const binary = resolution.command
197
+ ? `${displayCodexRuntimePath(resolution.command)} (${resolution.version ?? "unknown"})`
198
+ : "none";
199
+ switch (resolution.reason) {
200
+ case "codex-compatible":
201
+ return [` ok codex: Codex refreshes the main login through app-server (${binary}; ${configured})`];
202
+ case "configured-opencodex":
203
+ return [` -- opencodex: OpenCodex refreshes auth.json with its guarded writer (${configured})`];
204
+ case "test-guard":
205
+ return [" -- opencodex: test guard is armed; Codex delegation is disabled"];
206
+ case "codex-missing":
207
+ return [
208
+ ` !! opencodex: no Codex binary found, so OpenCodex refreshes auth.json itself (${configured})`,
209
+ ` Install Codex ${CODEX_AUTH_STATUS_MIN_VERSION} or newer to let Codex own its login.`,
210
+ ];
211
+ case "codex-too-old":
212
+ return [
213
+ ` !! opencodex: Codex ${binary} is older than ${CODEX_AUTH_STATUS_MIN_VERSION}, so OpenCodex refreshes auth.json itself (${configured})`,
214
+ " Update Codex to let Codex own its login.",
215
+ ];
216
+ }
217
+ }
@@ -20,6 +20,7 @@ import { withNativeMainExclusiveClaim } from "./native-main-claim";
20
20
  import { scrubNativeMainAuthTempResidues } from "./native-main-auth-temp";
21
21
  import { NATIVE_STAGE_SWEEP_INTERVAL_MS } from "./native-profile-stage-store";
22
22
  import type { NativeCodexOwnership } from "../integrations/native/ownership-preflight";
23
+ import { isPassiveRead } from "../lib/passive-read";
23
24
 
24
25
  export type NativeMainStartupGateSnapshot =
25
26
  | { status: "ready"; homeId: string | null }
@@ -701,7 +702,9 @@ export async function releaseNativeMainStartupLifecycle(server: object): Promise
701
702
 
702
703
  export function isNativeMainTrafficBlocked(): boolean {
703
704
  const reason = activeServiceOwnershipBlockReason();
704
- if (reason !== null && reprobeServiceOwnership(reason)) {
705
+ // A passive read (a Remote Link Child's, #6959) reports the gate as it stands: re-inspecting
706
+ // service ownership can activate the native-main lifecycle and its recovery on this machine.
707
+ if (reason !== null && !isPassiveRead() && reprobeServiceOwnership(reason)) {
705
708
  // The host became ownable after boot (#2108): the fence lifts here rather than
706
709
  // waiting for the restart the reporter had to perform by hand.
707
710
  return activeServiceOwnershipBlockReason() !== null || snapshot.status === "blocked";
@@ -318,7 +318,7 @@ export function recoverIfNeeded(
318
318
  };
319
319
  }
320
320
  try {
321
- publishConfigWrite(expectedTargets.configPath, held, (destination, hooks) => restore(destination, record.preConfigBytes, hooks));
321
+ publishConfigWrite(expectedTargets.configPath, held, (destination, hooks) => restore(destination, record.preConfigBytes, expectingImage(hooks, record.postConfig)));
322
322
  } catch (error) {
323
323
  return {
324
324
  ok: false,
@@ -336,7 +336,7 @@ export function recoverIfNeeded(
336
336
  };
337
337
  }
338
338
  try {
339
- publishConfigWriteTarget(expectedTargets.configPath, held, expectedTargets.storePath, (destination, hooks) => restore(destination, record.preStoreBytes, hooks));
339
+ publishConfigWriteTarget(expectedTargets.configPath, held, expectedTargets.storePath, (destination, hooks) => restore(destination, record.preStoreBytes, expectingImage(hooks, record.postStore)));
340
340
  } catch (error) {
341
341
  return {
342
342
  ok: false,
@@ -363,3 +363,21 @@ function restore(path: string, bytes: string | null, hooks: AtomicWriteHooks): v
363
363
  if (bytes === null) durableDelete(path, hooks);
364
364
  else durableWrite(path, bytes, hooks);
365
365
  }
366
+
367
+ /**
368
+ * Bind a write to the image the caller classified. The rename retry hooks check
369
+ * path and inode identity only; an in-place edit by another process during a
370
+ * Windows sharing-violation retry keeps the inode, so without this re-read a
371
+ * later attempt would replace newer bytes. Checked before every attempt.
372
+ */
373
+ export function expectingImage(hooks: AtomicWriteHooks, expectedHash: string): AtomicWriteHooks {
374
+ return {
375
+ ...hooks,
376
+ validateBeforeRename: (targetPath: string) => {
377
+ hooks.validateBeforeRename?.(targetPath);
378
+ if (hashBytes(readOrNull(targetPath)) !== expectedHash) {
379
+ throw new Error(targetPath + " changed during the write; refusing to overwrite it. Files and recovery evidence were preserved.");
380
+ }
381
+ },
382
+ };
383
+ }
@@ -119,3 +119,52 @@ export function decodeTomlBasicString(literal: string): string | null {
119
119
  }
120
120
  return out;
121
121
  }
122
+
123
+ /** Shared scalar assignment matching; quoted and escaped keys retain their spelling. */
124
+ export function matchKeyHead(line: string, key: string): { prefix: string; rest: string } | null {
125
+ const m = /^\s*("(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*=\s*/.exec(line);
126
+ if (!m) return null;
127
+ const token = m[1]!;
128
+ const decoded = token.startsWith('"') ? decodeTomlBasicString(token)
129
+ : token.startsWith("'") ? token.slice(1, -1) : token;
130
+ return decoded === key ? { prefix: m[0], rest: line.slice(m[0].length) } : null;
131
+ }
132
+
133
+ /**
134
+ * For each line, whether it begins outside every string and composite span, or
135
+ * `null` when a span never closes and the scope of later lines is unknown.
136
+ *
137
+ * Line matchers for assignments and table headers must only look at lines that
138
+ * start at top level: `"model_instructions_file" = "x"` written inside a
139
+ * `"""…"""` value or a multi-line array is prose, not syntax. Readers and
140
+ * editors share this so a write and the snapshot read back agree.
141
+ */
142
+ export function lexicalLineStarts(lines: readonly string[]): boolean[] | null {
143
+ const starts: boolean[] = [];
144
+ let open: string | null = null;
145
+ let depth = 0;
146
+ for (const line of lines) {
147
+ starts.push(open === null && depth === 0);
148
+ for (let i = 0; i < line.length; i += 1) {
149
+ const char = line[i]!;
150
+ if (open !== null) {
151
+ if (char === "\\" && (open === '"' || open === '"""')) { i += 1; continue; }
152
+ if (char !== open[0]) continue;
153
+ if (open.length === 1) { open = null; continue; }
154
+ let run = 0;
155
+ while (line[i + run] === char) run += 1;
156
+ // Up to two quotes may sit inside the value just before the closing three.
157
+ if (run >= 3) open = null;
158
+ i += run - 1;
159
+ } else if (char === "#") break;
160
+ else if (char === '"' || char === "'") {
161
+ open = line.startsWith(char.repeat(3), i) ? char.repeat(3) : char;
162
+ i += open.length - 1;
163
+ } else if (char === "[" || char === "{") depth += 1;
164
+ else if (char === "]" || char === "}") { depth -= 1; if (depth < 0) return null; }
165
+ }
166
+ // Single-line strings cannot cross a newline.
167
+ if (open !== null && open.length === 1) return null;
168
+ }
169
+ return open === null && depth === 0 ? starts : null;
170
+ }