@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Whether the Codex desktop app's model picker already shows the current catalog (#4761).
3
+ *
4
+ * `ocx system codex-restart` used to quit and relaunch the desktop shell every time, which
5
+ * discards everything that lives only in the client: composer drafts, an unsent picker
6
+ * selection, a pending approval prompt. Upstream's own restart (`codex app-server daemon
7
+ * restart`) restarts only the app-server and lets clients reconnect, and the desktop app
8
+ * already respawns its own app-server child when it exits.
9
+ *
10
+ * The one thing an app-server restart cannot refresh is the picker: the renderer builds
11
+ * the roster once when the shell launches, and `model_catalog_json` is startup-only. So
12
+ * the shell has to restart only when it launched before the catalog it should show was
13
+ * written. The catalog writer skips byte-identical rewrites to keep this mtime honest
14
+ * (`src/codex/catalog/retained-sync.ts`), which is what makes the comparison usable.
15
+ *
16
+ * The mtime only means something for the catalog `config.toml` actually binds. With no
17
+ * `model_catalog_json` the shell shows Codex's own roster, which no local file dates, so
18
+ * an unbound config is `unknown`. `config.toml`'s own mtime is no substitute: injection
19
+ * rewrites it on every sync even when the bytes are identical. A binding changed by THIS
20
+ * request's sync is caught by the caller comparing it across the sync; one changed by an
21
+ * earlier sync over a byte-identical catalog file is not observable here.
22
+ *
23
+ * Anything we cannot read answers `unknown`, and the caller falls back to the full
24
+ * desktop restart: a stale picker the operator asked us to fix is the failure this
25
+ * command exists to prevent, so missing evidence never skips it.
26
+ */
27
+ import { existsSync, readFileSync, statSync } from "node:fs";
28
+ import { activeCodexConfigPath, resolveActiveCodexConfigPath } from "../catalog/parsing";
29
+ import { readRootTomlString } from "../paths";
30
+ import { readProcessStartMsBatch } from "../app-server-processes";
31
+ import { listCodexDesktopShellPids, type DesktopAppRestartIo } from "../desktop-app-restart";
32
+ import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
33
+ import type { CodexDesktopPickerState } from "../../lib/codex-restart-contract";
34
+
35
+ export type DesktopPickerState = CodexDesktopPickerState;
36
+
37
+ /**
38
+ * Pure decision. `shellStartsMs === null` means the shells could not be listed; a null
39
+ * entry means one shell's start time was unreadable.
40
+ *
41
+ * Strictly later, never equal: macOS `ps lstart` has one-second granularity, so a shell
42
+ * that started in the same second as the catalog write may predate it.
43
+ */
44
+ export function classifyDesktopPicker(
45
+ shellStartsMs: readonly (number | null)[] | null,
46
+ catalogMtimeMs: number | null,
47
+ ): DesktopPickerState {
48
+ if (shellStartsMs === null) return "unknown";
49
+ if (shellStartsMs.length === 0) return "not_running";
50
+ if (catalogMtimeMs === null) return "unknown";
51
+ if (shellStartsMs.some(start => start === null)) return "unknown";
52
+ return shellStartsMs.every(start => start! > catalogMtimeMs) ? "current" : "stale";
53
+ }
54
+
55
+ export interface DesktopPickerIo extends DesktopAppRestartIo {
56
+ readStartMs?: (pids: readonly number[]) => Map<number, number | null>;
57
+ /** The catalog `config.toml` binds, or null when it binds none or cannot be read. */
58
+ catalogBinding?: () => string | null;
59
+ catalogMtimeMs?: (path: string) => number | null;
60
+ }
61
+
62
+ /** The resolved `model_catalog_json` path, or null when config.toml binds no catalog. */
63
+ export function readCodexCatalogBinding(): string | null {
64
+ try {
65
+ const configPath = activeCodexConfigPath();
66
+ if (!existsSync(configPath)) return null;
67
+ const path = readRootTomlString(readFileSync(configPath, "utf-8"), "model_catalog_json");
68
+ return path ? resolveActiveCodexConfigPath(path) : null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function defaultCatalogMtimeMs(path: string): number | null {
75
+ try {
76
+ return statSync(path).mtimeMs;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ export function readCodexDesktopPickerState(io: DesktopPickerIo = {}): DesktopPickerState {
83
+ // An armed test process without an injected executor answers `unknown`, which keeps the
84
+ // caller on the desktop-restart seam the test injected. Reading is harmless, but whether
85
+ // the developer's own Codex is open must not decide which branch a test takes.
86
+ if (!io.execFile && isTestHomeGuardArmed()) return "unknown";
87
+ const platform = io.platform ?? process.platform;
88
+ const pids = listCodexDesktopShellPids(io);
89
+ if (pids === null || pids.length === 0) return classifyDesktopPicker(pids, null);
90
+ const starts = (io.readStartMs ?? (list => readProcessStartMsBatch(list, platform)))(pids);
91
+ const binding = (io.catalogBinding ?? readCodexCatalogBinding)();
92
+ return classifyDesktopPicker(
93
+ pids.map(pid => starts.get(pid) ?? null),
94
+ binding === null ? null : (io.catalogMtimeMs ?? defaultCatalogMtimeMs)(binding),
95
+ );
96
+ }
@@ -51,6 +51,8 @@ export interface DesktopProcess {
51
51
  createdAt: string;
52
52
  /** Absolute executable path, used for membership and the shell predicate. */
53
53
  executable: string;
54
+ /** Windows-only private launch context; never included in restart results or logs. */
55
+ commandLine?: string;
54
56
  }
55
57
 
56
58
  export interface DesktopAppAdapter {
@@ -13,6 +13,7 @@
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { sep, win32 } from "node:path";
15
15
  import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsTaskkillExe } from "../../lib/windows-elevation";
16
+ import { activateWindowsCodexCompatibility, assertWindowsCompatibilityContext, captureWindowsCompatibilityContext } from "../desktop-compatibility/windows-package-command";
16
17
  import {
17
18
  isUnderRoot,
18
19
  type DesktopAppAdapter,
@@ -50,8 +51,7 @@ function isMemberExecutable(executable: string, root: string): boolean {
50
51
  * changes between builds, so a literal AUMID would silently stop matching and
51
52
  * then either do nothing or — worse — match a package we did not mean.
52
53
  */
53
- function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
54
- const script = [
54
+ export const WINDOWS_PACKAGE_DISCOVERY_SCRIPT = [
55
55
  "$ErrorActionPreference='SilentlyContinue'",
56
56
  "Import-Module Appx -ErrorAction SilentlyContinue",
57
57
  "$p = Get-AppxPackage -Name OpenAI.Codex",
@@ -60,12 +60,8 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
60
60
  " $p.PackageFamilyName; $p.InstallLocation; \"$($p.PackageFamilyName)!App\"",
61
61
  "}",
62
62
  ].join("; ");
63
- let stdout: string;
64
- try {
65
- stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], POWERSHELL_PROBE_OPTIONS);
66
- } catch {
67
- return null;
68
- }
63
+
64
+ export function parseWindowsDesktopPackage(stdout: string): DesktopAppInstall | null {
69
65
  const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
70
66
  if (lines.length < 3 || lines[0] === "MISS") return null;
71
67
  const [family, installLocation, aumid] = lines;
@@ -73,6 +69,16 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
73
69
  return { id: family, root: installLocation, relaunch: aumid };
74
70
  }
75
71
 
72
+ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
73
+ let stdout: string;
74
+ try {
75
+ stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PACKAGE_DISCOVERY_SCRIPT], POWERSHELL_PROBE_OPTIONS);
76
+ } catch {
77
+ return null;
78
+ }
79
+ return parseWindowsDesktopPackage(stdout);
80
+ }
81
+
76
82
  /**
77
83
  * Only `ChatGPT.exe` processes whose image lives under the discovered install
78
84
  * location AND owned by the current user. The install location alone is not
@@ -100,7 +106,8 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
100
106
  " if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
101
107
  " $owner = if ($o.Domain) { \"$($o.Domain)\\$($o.User)\" } else { $o.User }",
102
108
  " if ($owner -ieq $me) {",
103
- " \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)\"",
109
+ " $encoded=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_.CommandLine))",
110
+ " \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)`t$encoded\"",
104
111
  " }",
105
112
  " }",
106
113
  " }",
@@ -126,7 +133,9 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
126
133
  }
127
134
 
128
135
  function parseProcessLine(line: string, root: string): DesktopProcess | null {
129
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(line);
136
+ const [listing, encoded, ...extra] = line.split("\t");
137
+ if (extra.length || (encoded && (encoded.length > 65_536 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)))) return null;
138
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(listing ?? "");
130
139
  if (!match) return null;
131
140
  const pid = Number(match[1]);
132
141
  const parentPid = Number(match[2]);
@@ -143,7 +152,8 @@ function parseProcessLine(line: string, root: string): DesktopProcess | null {
143
152
  // Authoritative membership. PowerShell StartsWith already cheap-filtered, but
144
153
  // that test is a string prefix and is how a sibling install would sneak in.
145
154
  if (!isMemberExecutable(executable, root)) return null;
146
- return { pid, parentPid, createdAt, executable };
155
+ return { pid, parentPid, createdAt, executable,
156
+ ...(encoded !== undefined ? { commandLine: Buffer.from(encoded, "base64").toString("utf8") } : {}) };
147
157
  }
148
158
 
149
159
  /**
@@ -215,13 +225,16 @@ export const windowsDesktopAppAdapter: DesktopAppAdapter = {
215
225
  exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(root.pid), "/T", "/F"], POWERSHELL_PROBE_OPTIONS);
216
226
  },
217
227
 
218
- captureRelaunchContext(): Record<string, string> {
219
- // The session is supplied by the shell:AppsFolder launch, so nothing needs
220
- // carrying forward.
221
- return {};
228
+ captureRelaunchContext(_exec, _install, processes): Record<string, string> {
229
+ return captureWindowsCompatibilityContext(processes);
222
230
  },
223
231
 
224
- relaunch(exec, install): void {
232
+ relaunch(exec, install, context): void {
233
+ if (context.codexCompatibilityPacUrl) {
234
+ assertWindowsCompatibilityContext(context);
235
+ activateWindowsCodexCompatibility(exec, install, context.codexCompatibilityPacUrl);
236
+ return;
237
+ }
225
238
  // Throws on failure so the ladder reports relaunch_failed. The old code
226
239
  // returned targets_survived here, which was dishonest: everything HAD died
227
240
  // and it was the relaunch that failed.
@@ -89,6 +89,7 @@ export type DesktopAppRestartReason =
89
89
  | "self_ancestry"
90
90
  | "restart_in_flight"
91
91
  | "handoff_started"
92
+ | "relaunch_context_failed"
92
93
  | "targets_survived"
93
94
  | "relaunch_failed";
94
95
 
@@ -205,6 +206,27 @@ export function listCodexDesktopAppPids(io: DesktopAppRestartIo = {}): number[]
205
206
  return processes === null ? null : processes.map(entry => entry.pid);
206
207
  }
207
208
 
209
+ /**
210
+ * Pids of the running desktop-app ROOT shells, or null when discovery or the probe failed.
211
+ *
212
+ * Read-only. The shell is the process whose renderer built the model picker, so its start
213
+ * time is what tells `performCodexRestart` whether the picker predates the catalog (#4761).
214
+ * `[]` means the probe ran and the app is not running.
215
+ */
216
+ export function listCodexDesktopShellPids(io: DesktopAppRestartIo = {}): number[] | null {
217
+ const platform = io.platform ?? process.platform;
218
+ const selected = ADAPTERS[platform];
219
+ const adapter = io.adapter ?? selected?.adapter;
220
+ if (!adapter) return null;
221
+ const exec = io.execFile ?? selected?.exec;
222
+ if (!exec) return null;
223
+ const install = adapter.discover(exec);
224
+ if (!install) return null;
225
+ const processes = adapter.listProcesses(exec, install);
226
+ if (processes === null) return null;
227
+ return rootShells(processes, install, adapter).map(entry => entry.pid);
228
+ }
229
+
208
230
  export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopAppRestartResult {
209
231
  const skipped = (reason: DesktopAppRestartReason): DesktopAppRestartResult => ({
210
232
  attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason,
@@ -263,7 +285,9 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
263
285
  // graphical session variables, and after termination there is nothing to read them
264
286
  // from. Ordering this wrongly works on macOS and Windows and produces a Linux app
265
287
  // that cannot reach the compositor.
266
- const context = adapter.captureRelaunchContext(exec, install, processes);
288
+ let context: Record<string, string>;
289
+ try { context = adapter.captureRelaunchContext(exec, install, processes); }
290
+ catch { return skipped("relaunch_context_failed"); }
267
291
 
268
292
  const isAlive = io.isAlive ?? defaultIsAlive;
269
293
  const sleep = io.sleep ?? defaultSleep;
@@ -345,4 +369,3 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
345
369
  if (!handedOff) releaseDesktopRestartLock(io.lock);
346
370
  }
347
371
  }
348
-
@@ -0,0 +1,99 @@
1
+ import { join } from "node:path";
2
+ import { getConfigDir } from "../../config/paths";
3
+ import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
4
+ import { ensureDesktopCompatibilityAuthority, inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, renewDesktopCompatibilityAuthority, type DesktopAuthorityInspection, type StoredDesktopAuthority } from "./certificate-store";
5
+ import { createWindowsCertificateTrust, inspectWindowsCertificateTrust, type DesktopCertificateTrust } from "./windows-certificate-trust";
6
+ import { acquireDesktopCertificateMutation, desktopCompatibilityRuntimeActive } from "./runtime-ownership";
7
+
8
+ export interface DesktopCertificateStatus {
9
+ supported: boolean;
10
+ state: "missing" | "invalid" | "expired" | "renewal-required" | "prepared" | "trusted" | "unknown";
11
+ fingerprint?: string;
12
+ expiresAt?: number;
13
+ renewalDue?: boolean;
14
+ trust?: DesktopCertificateTrust;
15
+ busy: "prepare" | "trust" | "remove-trust" | "renew" | null;
16
+ }
17
+
18
+ export interface DesktopCertificateServiceIo {
19
+ platform?: string;
20
+ inspect?: () => DesktopAuthorityInspection;
21
+ prepare?: () => Promise<StoredDesktopAuthority>;
22
+ load?: (allowExpiredForRemoval: boolean) => Promise<StoredDesktopAuthority>;
23
+ renew?: (fingerprint: string) => Promise<StoredDesktopAuthority>;
24
+ readTrust?: (authority: Extract<DesktopAuthorityInspection, { fingerprint: string }>) => Promise<DesktopCertificateTrust>;
25
+ changeTrust?: (authority: StoredDesktopAuthority, action: "trust" | "remove") => Promise<DesktopCertificateTrust>;
26
+ appRunning?: () => Promise<boolean | null>;
27
+ }
28
+
29
+ export class DesktopCertificateServiceError extends Error {
30
+ constructor(readonly code: "unsupported" | "busy" | "not_prepared" | "fingerprint_changed" | "app_running" | "runtime_running" | "app_state_unknown" | "trust_unknown" | "trust_not_applied") {
31
+ super(`desktop_compatibility_${code}`); this.name = "DesktopCertificateServiceError";
32
+ }
33
+ }
34
+
35
+ /** This service owns setup only: no proxy listeners, app restart, account or quota writes. */
36
+ export function createDesktopCertificateService(directory = join(getConfigDir(), "codex-desktop-compatibility"), io: DesktopCertificateServiceIo = {}) {
37
+ const platform = io.platform ?? process.platform;
38
+ const inspect = io.inspect ?? (() => inspectDesktopCompatibilityAuthority(directory));
39
+ const prepare = io.prepare ?? (() => ensureDesktopCompatibilityAuthority({ directory }));
40
+ const load = io.load ?? (allowExpired => loadDesktopCompatibilityAuthority({ directory }, allowExpired));
41
+ const renew = io.renew ?? (fingerprint => renewDesktopCompatibilityAuthority({ directory }, fingerprint));
42
+ const readTrust = io.readTrust ?? (value => inspectWindowsCertificateTrust(value.certPem, value.fingerprint));
43
+ const changeTrust = io.changeTrust ?? ((authority, action) => createWindowsCertificateTrust(authority, authority.fingerprint)[action]());
44
+ const appRunning = io.appRunning ?? (async () => {
45
+ const install = windowsDesktopAppAdapter.discover(windowsDefaultExec);
46
+ if (!install) return null; // Unknown installation is not proof that no app uses the authority.
47
+ const processes = windowsDesktopAppAdapter.listProcesses(windowsDefaultExec, install);
48
+ return processes === null ? null : processes.length > 0;
49
+ });
50
+ let busy: DesktopCertificateStatus["busy"] = null;
51
+ async function status(): Promise<DesktopCertificateStatus> {
52
+ if (platform !== "win32") return { supported: false, state: "missing", busy };
53
+ const stored = inspect();
54
+ if (!("fingerprint" in stored)) return { supported: true, state: stored.status, busy };
55
+ const trusted = await readTrust(stored).catch(() => "unknown" as const);
56
+ return { supported: true, state: stored.status === "expired" ? "expired"
57
+ : stored.status === "renewal-required" ? "renewal-required"
58
+ : trusted === "trusted" ? "trusted" : trusted === "not-trusted" ? "prepared" : "unknown",
59
+ fingerprint: stored.fingerprint, expiresAt: stored.expiresAt, renewalDue: stored.renewalDue, trust: trusted, busy };
60
+ }
61
+ async function action(kind: NonNullable<DesktopCertificateStatus["busy"]>, expectedFingerprint?: string): Promise<DesktopCertificateStatus> {
62
+ if (platform !== "win32") throw new DesktopCertificateServiceError("unsupported");
63
+ if (busy !== null) throw new DesktopCertificateServiceError("busy");
64
+ if (desktopCompatibilityRuntimeActive()) throw new DesktopCertificateServiceError("runtime_running");
65
+ let release: () => void;
66
+ try { release = acquireDesktopCertificateMutation(); } catch { throw new DesktopCertificateServiceError("busy"); }
67
+ busy = kind;
68
+ try {
69
+ if (kind !== "prepare") {
70
+ const before = inspect();
71
+ if (!("fingerprint" in before)) throw new DesktopCertificateServiceError("not_prepared");
72
+ if (before.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
73
+ if (kind === "remove-trust" || kind === "renew") {
74
+ const running = await appRunning();
75
+ if (running === null) throw new DesktopCertificateServiceError("app_state_unknown");
76
+ if (running) throw new DesktopCertificateServiceError("app_running");
77
+ }
78
+ }
79
+ const authority = kind === "prepare" ? await prepare() : await load(kind !== "trust");
80
+ if (kind !== "prepare") {
81
+ if (authority.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
82
+ const current = inspect();
83
+ if (!("fingerprint" in current) || current.fingerprint !== authority.fingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
84
+ const result = await changeTrust(authority, kind === "trust" ? "trust" : "remove");
85
+ if (result === "unknown") throw new DesktopCertificateServiceError("trust_unknown");
86
+ if (result !== (kind === "trust" ? "trusted" : "not-trusted")) throw new DesktopCertificateServiceError("trust_not_applied");
87
+ // Never discard the only removable old identity until OS trust removal is proven.
88
+ // Publication failure leaves its protected envelope intact for an explicit retry.
89
+ if (kind === "renew") await renew(authority.fingerprint);
90
+ }
91
+ busy = null;
92
+ return status();
93
+ } finally { busy = null; release(); }
94
+ }
95
+ return { status, prepare: () => action("prepare"), trust: (fingerprint: string) => action("trust", fingerprint),
96
+ removeTrust: (fingerprint: string) => action("remove-trust", fingerprint), renew: (fingerprint: string) => action("renew", fingerprint) };
97
+ }
98
+
99
+ export type DesktopCertificateService = ReturnType<typeof createDesktopCertificateService>;
@@ -0,0 +1,193 @@
1
+ import { createHash, createPrivateKey, createPublicKey, randomUUID, X509Certificate } from "node:crypto";
2
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { createCertificateAuthority, isServerAuthOnlyCertificate, type LocalInterceptCa } from "../../claude/intercept/local-ca";
5
+ import { withClientLifecycle } from "../../client/lifecycle-lock";
6
+ import { hardenSecretDirAsync, hardenSecretPathAsync } from "../../lib/windows-secret-acl";
7
+ import { windowsAuthorityKeyProtection, type AuthorityKeyProtection } from "./windows-key-protection";
8
+
9
+ const POLICY = "codex-desktop-chatgpt-only/v1";
10
+ const FILE = "authority.json";
11
+ const MAX_FILE = 131_072;
12
+ const VALIDITY_DAYS = 30;
13
+ const DAY = 86_400_000;
14
+ const digest = (value: string) => createHash("sha256").update(value).digest("hex");
15
+
16
+ export class DesktopAuthorityError extends Error {
17
+ constructor(readonly code: "unsafe_path" | "unreadable" | "expired" | "renewal_required" | "protection_failed" | "fingerprint_changed") {
18
+ super(`desktop_compatibility_authority_${code}`); this.name = "DesktopAuthorityError";
19
+ }
20
+ }
21
+
22
+ export interface DesktopAuthorityStoreOptions {
23
+ /** Explicit feature-owned directory; never an OS trust store or existing Claude CA. */
24
+ directory: string;
25
+ now?: () => number;
26
+ /** Test seam; production always uses Windows CurrentUser DPAPI. */
27
+ protection?: AuthorityKeyProtection;
28
+ }
29
+
30
+ export interface StoredDesktopAuthority {
31
+ authority: LocalInterceptCa;
32
+ commonName: string;
33
+ fingerprint: string;
34
+ expiresAt: number;
35
+ renewalDue: boolean;
36
+ reused: boolean;
37
+ }
38
+
39
+ export type DesktopAuthorityInspection =
40
+ | { status: "missing" | "invalid" }
41
+ | { status: "present" | "expired" | "renewal-required"; certPem: string; fingerprint: string; expiresAt: number; renewalDue: boolean };
42
+
43
+ /** Read-only public metadata for status: no key decryption, ACL write, lock or generation. */
44
+ export function inspectDesktopCompatibilityAuthority(directory: string, now = Date.now()): DesktopAuthorityInspection {
45
+ if (!isAbsolute(directory) || !Number.isFinite(now)) return { status: "invalid" };
46
+ const path = join(directory, FILE);
47
+ try {
48
+ if (!pathPresent(directory)) return { status: "missing" };
49
+ assertPath(directory, true);
50
+ if (!pathPresent(path)) return { status: "missing" };
51
+ assertPath(path, false);
52
+ const saved = JSON.parse(readFileSync(path, "utf8"));
53
+ if (saved.version !== 1 || saved.protection !== "windows-current-user-dpapi" || typeof saved.certPem !== "string"
54
+ || typeof saved.sealed !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(saved.sealed) || saved.sealed.length > MAX_FILE) return { status: "invalid" };
55
+ const certificate = new X509Certificate(saved.certPem), expiresAt = Date.parse(certificate.validTo);
56
+ if (!certificate.ca || !Number.isFinite(expiresAt)) return { status: "invalid" };
57
+ return { status: expiresAt <= now ? "expired" : isServerAuthOnlyCertificate(certificate) ? "present" : "renewal-required", certPem: saved.certPem,
58
+ fingerprint: certificate.fingerprint256.replaceAll(":", ""), expiresAt, renewalDue: expiresAt - now <= 7 * DAY };
59
+ } catch { return { status: "invalid" }; }
60
+ }
61
+
62
+ function assertPath(path: string, directory: boolean): void {
63
+ const stat = lstatSync(path);
64
+ if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1 || stat.size > MAX_FILE)) {
65
+ throw new DesktopAuthorityError("unsafe_path");
66
+ }
67
+ }
68
+
69
+ function pathPresent(path: string): boolean {
70
+ try { lstatSync(path); return true; }
71
+ catch (error) {
72
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
73
+ throw new DesktopAuthorityError("unsafe_path");
74
+ }
75
+ }
76
+
77
+ function validatedAuthority(certPem: string, keyPem: string, commonName: string, now: number, reused: boolean, allowExpired = false): StoredDesktopAuthority {
78
+ try {
79
+ const certificate = new X509Certificate(certPem);
80
+ const privateKey = createPrivateKey(keyPem), publicKey = createPublicKey(keyPem);
81
+ if (!certificate.ca || !certificate.checkPrivateKey(privateKey) || !certificate.verify(publicKey)
82
+ || certificate.subject !== certificate.issuer
83
+ || !commonName.startsWith("OpenCodex Codex Desktop ") || !certificate.subject.split("\n").includes(`CN=${commonName}`)) {
84
+ throw new Error("invalid");
85
+ }
86
+ const expiresAt = Date.parse(certificate.validTo), startsAt = Date.parse(certificate.validFrom);
87
+ if (!Number.isFinite(expiresAt) || !Number.isFinite(startsAt) || startsAt > now
88
+ || expiresAt - startsAt > (VALIDITY_DAYS + 1) * DAY) throw new Error("invalid");
89
+ if (expiresAt <= now && !allowExpired) throw new DesktopAuthorityError("expired");
90
+ if (!allowExpired && !isServerAuthOnlyCertificate(certificate)) throw new DesktopAuthorityError("renewal_required");
91
+ return { authority: { certPem, keyPem, privateKey, publicKey }, commonName,
92
+ fingerprint: certificate.fingerprint256.replaceAll(":", ""), expiresAt,
93
+ renewalDue: expiresAt - now <= 7 * DAY, reused };
94
+ } catch (error) {
95
+ if (error instanceof DesktopAuthorityError) throw error;
96
+ throw new DesktopAuthorityError("unreadable");
97
+ }
98
+ }
99
+
100
+ async function readAuthority(path: string, protection: AuthorityKeyProtection, now: number, allowExpired = false): Promise<StoredDesktopAuthority> {
101
+ assertPath(path, false);
102
+ await hardenSecretPathAsync(path, { required: true });
103
+ assertPath(path, false);
104
+ let cleartext: Uint8Array | undefined;
105
+ try {
106
+ const saved = JSON.parse(readFileSync(path, "utf8"));
107
+ if (saved.version !== 1 || saved.protection !== "windows-current-user-dpapi"
108
+ || typeof saved.certPem !== "string" || typeof saved.sealed !== "string"
109
+ || !/^[A-Za-z0-9+/]+={0,2}$/.test(saved.sealed) || saved.sealed.length > MAX_FILE) throw new Error("invalid");
110
+ cleartext = await protection.unprotect(Buffer.from(saved.sealed, "base64"));
111
+ const secret = JSON.parse(Buffer.from(cleartext).toString("utf8"));
112
+ if (secret.policy !== POLICY || secret.certSha256 !== digest(saved.certPem)
113
+ || typeof secret.keyPem !== "string" || typeof secret.commonName !== "string") throw new Error("invalid");
114
+ return validatedAuthority(saved.certPem, secret.keyPem, secret.commonName, now, true, allowExpired);
115
+ } catch (error) {
116
+ if (error instanceof DesktopAuthorityError) throw error;
117
+ // A corrupt or foreign-user key never silently regenerates a new root or trust prompt.
118
+ throw new DesktopAuthorityError("unreadable");
119
+ } finally { cleartext?.fill(0); }
120
+ }
121
+
122
+ /** Loads only existing state. Expired or legacy-purpose keys may be loaded solely for trust removal/renewal. */
123
+ export async function loadDesktopCompatibilityAuthority(options: DesktopAuthorityStoreOptions, allowExpiredForRemoval = false): Promise<StoredDesktopAuthority> {
124
+ if (!isAbsolute(options.directory)) throw new DesktopAuthorityError("unsafe_path");
125
+ const now = options.now?.() ?? Date.now();
126
+ if (!Number.isFinite(now)) throw new DesktopAuthorityError("unreadable");
127
+ assertPath(options.directory, true);
128
+ const path = join(options.directory, FILE);
129
+ if (!pathPresent(path)) throw new DesktopAuthorityError("unreadable");
130
+ return readAuthority(path, options.protection ?? windowsAuthorityKeyProtection, now, allowExpiredForRemoval);
131
+ }
132
+
133
+ /** Reuses one user-protected root across restarts; never installs, rotates or removes trust. */
134
+ export async function ensureDesktopCompatibilityAuthority(options: DesktopAuthorityStoreOptions): Promise<StoredDesktopAuthority> {
135
+ return publishAuthority(options);
136
+ }
137
+
138
+ /** Deliberate replacement after the caller has removed OS trust and stopped consumers. */
139
+ export async function renewDesktopCompatibilityAuthority(options: DesktopAuthorityStoreOptions, expectedFingerprint: string): Promise<StoredDesktopAuthority> {
140
+ if (!/^[A-F0-9]{64}$/.test(expectedFingerprint)) throw new DesktopAuthorityError("fingerprint_changed");
141
+ return publishAuthority(options, expectedFingerprint);
142
+ }
143
+
144
+ async function publishAuthority(options: DesktopAuthorityStoreOptions, expectedFingerprint?: string): Promise<StoredDesktopAuthority> {
145
+ if (!isAbsolute(options.directory)) throw new DesktopAuthorityError("unsafe_path");
146
+ if (!options.protection && process.platform !== "win32") throw new Error("desktop_compatibility_windows_required");
147
+ const now = options.now?.() ?? Date.now();
148
+ if (!Number.isFinite(now)) throw new DesktopAuthorityError("unreadable");
149
+ const protection = options.protection ?? windowsAuthorityKeyProtection;
150
+ mkdirSync(options.directory, { recursive: true, mode: 0o700 });
151
+ assertPath(options.directory, true);
152
+ await hardenSecretDirAsync(options.directory, { required: true });
153
+ return withClientLifecycle(async () => {
154
+ assertPath(options.directory, true);
155
+ const path = join(options.directory, FILE);
156
+ let original: string | undefined;
157
+ if (expectedFingerprint !== undefined) {
158
+ if (!pathPresent(path)) throw new DesktopAuthorityError("fingerprint_changed");
159
+ const previous = await readAuthority(path, protection, now, true);
160
+ if (previous.fingerprint !== expectedFingerprint) throw new DesktopAuthorityError("fingerprint_changed");
161
+ original = readFileSync(path, "utf8");
162
+ } else if (pathPresent(path)) return readAuthority(path, protection, now);
163
+ const commonName = `OpenCodex Codex Desktop ${randomUUID()}`;
164
+ const authority = createCertificateAuthority({ commonName, validityDays: VALIDITY_DAYS,
165
+ permittedDnsNames: ["chatgpt.com"], excludeAllIpAddresses: true, serverAuthOnly: true });
166
+ const cleartext = Buffer.from(JSON.stringify({ policy: POLICY, certSha256: digest(authority.certPem), commonName, keyPem: authority.keyPem }));
167
+ let sealed: Uint8Array;
168
+ try { sealed = await protection.protect(cleartext); }
169
+ catch { throw new DesktopAuthorityError("protection_failed"); }
170
+ finally { cleartext.fill(0); }
171
+ if (sealed.byteLength === 0 || sealed.byteLength > 65_536) throw new DesktopAuthorityError("protection_failed");
172
+ const contents = JSON.stringify({ version: 1, protection: "windows-current-user-dpapi", certPem: authority.certPem,
173
+ sealed: Buffer.from(sealed).toString("base64") });
174
+ const temporary = join(options.directory, `authority-${randomUUID()}.tmp`);
175
+ let created = false;
176
+ try {
177
+ const fd = openSync(temporary, "wx", 0o600); created = true;
178
+ try { writeFileSync(fd, contents); fsyncSync(fd); } finally { closeSync(fd); }
179
+ await hardenSecretPathAsync(temporary, { required: true });
180
+ assertPath(temporary, false); assertPath(options.directory, true);
181
+ // Verify the new encrypted envelope before replacing the only recovery source.
182
+ await readAuthority(temporary, protection, now);
183
+ if (original !== undefined) {
184
+ assertPath(path, false);
185
+ if (readFileSync(path, "utf8") !== original) throw new DesktopAuthorityError("fingerprint_changed");
186
+ } else if (pathPresent(path)) throw new DesktopAuthorityError("unsafe_path");
187
+ renameSync(temporary, path); created = false;
188
+ // Read back the actual persisted pair before reporting a successful setup.
189
+ const persisted = await readAuthority(path, protection, now);
190
+ return { ...persisted, reused: false };
191
+ } finally { if (created) unlinkSync(temporary); }
192
+ }, { lockPath: join(options.directory, "authority-publication.sqlite") });
193
+ }
@@ -0,0 +1,87 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, fsyncSync, linkSync, lstatSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { withClientLifecycle } from "../../client/lifecycle-lock";
5
+ import { hardenSecretDirAsync, hardenSecretPathAsync } from "../../lib/windows-secret-acl";
6
+
7
+ export interface DesktopConnectionIdentity {
8
+ version: 1;
9
+ id: string;
10
+ connectPort: number;
11
+ pacPort: number;
12
+ }
13
+ export interface DesktopConnectionStore {
14
+ read(): DesktopConnectionIdentity | null;
15
+ publish(value: DesktopConnectionIdentity): Promise<DesktopConnectionIdentity>;
16
+ }
17
+ const FILE = "connection.json";
18
+ const fail = (reason: "invalid" | "changed" | "cleanup_required", cause?: unknown) => new Error(`desktop_compatibility_connection_${reason}`, { cause });
19
+ const port = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 1024 && Number(value) <= 65535;
20
+ function validate(value: unknown): DesktopConnectionIdentity {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw fail("invalid");
22
+ const row = value as Record<string, unknown>;
23
+ if (Object.keys(row).length !== 4 || row.version !== 1 || typeof row.id !== "string"
24
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(row.id)
25
+ || !port(row.connectPort) || !port(row.pacPort) || row.connectPort === row.pacPort) throw fail("invalid");
26
+ return { version: 1, id: row.id, connectPort: row.connectPort, pacPort: row.pacPort };
27
+ }
28
+ function assertPath(path: string, directory: boolean): void {
29
+ const stat = lstatSync(path);
30
+ if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1 || stat.size > 4096)) throw fail("invalid");
31
+ }
32
+ function present(path: string): boolean {
33
+ try { lstatSync(path); return true; }
34
+ catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false; throw fail("invalid"); }
35
+ }
36
+ function same(left: DesktopConnectionIdentity, right: DesktopConnectionIdentity): boolean {
37
+ return left.version === right.version && left.id === right.id && left.connectPort === right.connectPort && left.pacPort === right.pacPort;
38
+ }
39
+
40
+ /** Public endpoint identity only. No token, account, private key or expiry is persisted here. */
41
+ export function createDesktopConnectionStore(directory: string): DesktopConnectionStore {
42
+ if (!isAbsolute(directory)) throw fail("invalid");
43
+ const path = join(directory, FILE);
44
+ const read = (): DesktopConnectionIdentity | null => {
45
+ try {
46
+ if (!present(directory)) return null;
47
+ assertPath(directory, true);
48
+ if (!present(path)) return null;
49
+ assertPath(path, false);
50
+ return validate(JSON.parse(readFileSync(path, "utf8")));
51
+ } catch { throw fail("invalid"); }
52
+ };
53
+ const verifyExisting = (value: DesktopConnectionIdentity): DesktopConnectionIdentity | null => {
54
+ const existing = read();
55
+ if (existing && !same(existing, value)) throw fail("changed");
56
+ return existing;
57
+ };
58
+ return { read, async publish(input) {
59
+ const value = validate(input);
60
+ const existing = verifyExisting(value); if (existing) return existing;
61
+ // A trusted authority must already own this directory. Never create a replacement home.
62
+ assertPath(directory, true); await hardenSecretDirAsync(directory, { required: true });
63
+ return withClientLifecycle(async () => {
64
+ const raced = verifyExisting(value); if (raced) return raced;
65
+ const temporary = join(directory, `connection-${randomUUID()}.tmp`);
66
+ let created = false;
67
+ try {
68
+ const fd = openSync(temporary, "wx", 0o600); created = true;
69
+ try { writeFileSync(fd, JSON.stringify(value)); fsyncSync(fd); } finally { closeSync(fd); }
70
+ await hardenSecretPathAsync(temporary, { required: true });
71
+ assertPath(directory, true); assertPath(temporary, false);
72
+ // Create-only publication cannot overwrite another process's endpoint identity.
73
+ try { linkSync(temporary, path); }
74
+ catch (error) {
75
+ const winner = verifyExisting(value); if (!winner) throw error;
76
+ }
77
+ unlinkSync(temporary); created = false;
78
+ const published = verifyExisting(value); if (!published) throw fail("changed");
79
+ return published;
80
+ } catch (error) {
81
+ try { if (created && present(temporary)) unlinkSync(temporary); }
82
+ catch (cleanupError) { throw fail("cleanup_required", new AggregateError([error, cleanupError], "Endpoint publication and cleanup failed")); }
83
+ throw error;
84
+ }
85
+ }, { lockPath: join(directory, "connection-publication.sqlite") });
86
+ } };
87
+ }