@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,73 @@
1
+ import { execFile } from "node:child_process";
2
+ import { win32 } from "node:path";
3
+ import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
4
+ import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
5
+ import { parseWindowsDesktopPackage, WINDOWS_PACKAGE_DISCOVERY_SCRIPT } from "../desktop-app/windows";
6
+ import type { DesktopAppInstall } from "../desktop-app/types";
7
+
8
+ export const DESKTOP_COMPATIBILITY_ASSESSED_VERSION = "26.924.2738.0";
9
+ export const DESKTOP_COMPATIBILITY_ASSESSED_FAMILY = "OpenAI.Codex_2p2nqsd0c76g0";
10
+
11
+ /** Last completed probe outcome. Only public package facts: never paths, keys or account data. */
12
+ export interface DesktopBuildAssessment {
13
+ assessedVersion: string;
14
+ observedVersion: string | null;
15
+ verified: boolean;
16
+ completedAt: number;
17
+ }
18
+
19
+ function assessInstalledPackage(install: DesktopAppInstall): { observedVersion: string | null; verified: boolean } {
20
+ const name = win32.basename(install.root);
21
+ const parts = /^OpenAI\.Codex_(\d+\.\d+\.\d+\.\d+)_(x64|arm64|x86|neutral)_([A-Za-z0-9.-]*)_([A-Za-z0-9]+)$/.exec(name);
22
+ const identified = install.id === DESKTOP_COMPATIBILITY_ASSESSED_FAMILY && install.relaunch === `${install.id}!App`
23
+ && !!parts && `OpenAI.Codex_${parts[4]}` === install.id;
24
+ const observedVersion = identified ? parts![1] : null;
25
+ return { observedVersion, verified: identified && observedVersion === DESKTOP_COMPATIBILITY_ASSESSED_VERSION };
26
+ }
27
+
28
+ /** Resolve only after the exact asynchronous child has closed, including abort/timeout. */
29
+ function queryInstalledPackage(signal: AbortSignal): Promise<DesktopAppInstall | null> {
30
+ if (process.platform !== "win32" || isTestHomeGuardArmed() || signal.aborted) return Promise.resolve(null);
31
+ return new Promise(resolve => {
32
+ let completed = false, closed = false, result: DesktopAppInstall | null = null;
33
+ const finish = () => { if (completed && closed) { signal.removeEventListener("abort", abort); resolve(result); } };
34
+ const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PACKAGE_DISCOVERY_SCRIPT], {
35
+ timeout: 10000, maxBuffer: 65536, encoding: "utf8", windowsHide: true,
36
+ }, (error, stdout) => { result = error ? null : parseWindowsDesktopPackage(stdout); completed = true; finish(); });
37
+ const abort = () => { try { child.kill(); } catch { /* Keep ownership until execFile reports close. */ } };
38
+ child.once("close", () => { closed = true; finish(); });
39
+ signal.addEventListener("abort", abort, { once: true });
40
+ if (signal.aborted) abort();
41
+ });
42
+ }
43
+
44
+ /** Coalesce concurrent fresh probes; never cache a positive verdict across requests. */
45
+ export function createInstalledBuildProbe(query: (signal: AbortSignal) => Promise<DesktopAppInstall | null> = queryInstalledPackage) {
46
+ let pending: Promise<boolean> | null = null, controller: AbortController | null = null, closed = false;
47
+ let lastResult: DesktopBuildAssessment | null = null;
48
+ const record = (install: DesktopAppInstall | null) => {
49
+ const outcome = install ? assessInstalledPackage(install) : { observedVersion: null, verified: false };
50
+ lastResult = { assessedVersion: DESKTOP_COMPATIBILITY_ASSESSED_VERSION, ...outcome, completedAt: Date.now() };
51
+ return outcome.verified;
52
+ };
53
+ return {
54
+ check(): Promise<boolean> {
55
+ if (closed) return Promise.resolve(false);
56
+ if (pending) return pending;
57
+ const current = new AbortController(); controller = current;
58
+ pending = Promise.resolve().then(() => current.signal.aborted ? null : query(current.signal))
59
+ .then(install => closed || current.signal.aborted ? false : record(install))
60
+ .catch(() => closed || current.signal.aborted ? false : record(null))
61
+ .finally(() => { pending = null; controller = null; });
62
+ return pending;
63
+ },
64
+ /** Last completed check only: an aborted or in-flight query never rewrites it. */
65
+ lastResult(): DesktopBuildAssessment | null { return lastResult ? { ...lastResult } : null; },
66
+ async close(): Promise<void> { closed = true; controller?.abort(); await pending; },
67
+ };
68
+ }
69
+
70
+ export async function isAssessedDesktopInstalled(): Promise<boolean> {
71
+ const probe = createInstalledBuildProbe();
72
+ try { return await probe.check(); } finally { await probe.close(); }
73
+ }
@@ -0,0 +1,29 @@
1
+ /** Buffer small JSON only; large bodies pass through without collecting the remainder. */
2
+ export async function boundedJsonBody(body: ReadableStream<Uint8Array> | null, maxBytes = 262144): Promise<{ bytes: Buffer } | { stream: ReadableStream<Uint8Array> }> {
3
+ if (!body) return { bytes: Buffer.alloc(0) };
4
+ const reader = body.getReader();
5
+ const chunks: Uint8Array[] = [];
6
+ let count = 0;
7
+ try {
8
+ while (true) {
9
+ const next = await reader.read();
10
+ if (next.done) { reader.releaseLock(); return { bytes: Buffer.concat(chunks, count) }; }
11
+ chunks.push(next.value); count += next.value.byteLength;
12
+ if (count <= maxBytes && chunks.length < 1024) continue;
13
+ let index = 0;
14
+ return { stream: new ReadableStream<Uint8Array>({
15
+ async pull(controller) {
16
+ if (index < chunks.length) {
17
+ const value = chunks[index]!; chunks[index++] = new Uint8Array(0); controller.enqueue(value); return;
18
+ }
19
+ try {
20
+ const part = await reader.read();
21
+ if (part.done) { reader.releaseLock(); controller.close(); }
22
+ else controller.enqueue(part.value);
23
+ } catch (error) { reader.releaseLock(); controller.error(error); }
24
+ },
25
+ async cancel(reason) { try { await reader.cancel(reason); } finally { reader.releaseLock(); } },
26
+ }) };
27
+ }
28
+ } catch (error) { reader.releaseLock(); throw error; }
29
+ }
@@ -0,0 +1,62 @@
1
+ import { readFileSync, statSync, type BigIntStats } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import type { UsageIdentity } from './usage-controller';
4
+
5
+ type Credential = { identity: UsageIdentity; accessToken: string };
6
+ const equal = (a: UsageIdentity, b: UsageIdentity) => a.id === b.id && a.userId === b.userId && a.plan === b.plan
7
+ && a.credentialGeneration === b.credentialGeneration;
8
+
9
+ /** Local native credentials remain in memory and are never returned by the public reader. */
10
+ export function createNativeIdentityReader(authPath: string, upstreamFetch: typeof fetch = fetch) {
11
+ const readerId = randomUUID(); let generation = 0, previousSnapshot: string | undefined;
12
+ const stamp = (value: BigIntStats) =>
13
+ `${value.dev}:${value.ino}:${value.size}:${value.mtimeNs}:${value.ctimeNs}`;
14
+ const readCredential = (): Credential => {
15
+ const before = statSync(authPath, { bigint: true });
16
+ if (before.size > 1048576n) throw new Error('Unexpected native auth size');
17
+ const text = readFileSync(authPath, 'utf8'), after = statSync(authPath, { bigint: true });
18
+ if (stamp(before) !== stamp(after)) throw new Error('Native credentials changed during read');
19
+ const snapshot = `${stamp(after)}:${createHash('sha256').update(text).digest('hex')}`;
20
+ if (snapshot !== previousSnapshot) { previousSnapshot = snapshot; generation++; }
21
+ const auth = JSON.parse(text);
22
+ if (auth.auth_mode !== 'chatgpt' || typeof auth.tokens?.id_token !== 'string'
23
+ || typeof auth.tokens?.access_token !== 'string' || !auth.tokens.access_token
24
+ || typeof auth.tokens?.account_id !== 'string') throw new Error('Native login required');
25
+ // These local claims are only a binding hint. Activation additionally verifies upstream.
26
+ const claims = JSON.parse(Buffer.from(auth.tokens.id_token.split('.')[1], 'base64url').toString('utf8'))['https://api.openai.com/auth'];
27
+ if (!claims || claims.chatgpt_account_id !== auth.tokens.account_id
28
+ || typeof claims.chatgpt_user_id !== 'string' || !claims.chatgpt_user_id
29
+ || !['plus', 'pro'].includes(claims.chatgpt_plan_type)) throw new Error('Unsupported identity');
30
+ return { identity: { id: auth.tokens.account_id, userId: claims.chatgpt_user_id,
31
+ plan: claims.chatgpt_plan_type, structure: 'personal', credentialGeneration: `${readerId}:${generation}` }, accessToken: auth.tokens.access_token };
32
+ };
33
+ const readCurrentIdentity = async (): Promise<UsageIdentity | null> => {
34
+ try { return readCredential().identity; } catch { return null; }
35
+ };
36
+ const verifyFreshIdentity = async (): Promise<UsageIdentity | null> => {
37
+ try {
38
+ const before = readCredential();
39
+ const response = await upstreamFetch('https://chatgpt.com/backend-api/wham/usage', {
40
+ headers: { authorization: 'Bearer ' + before.accessToken, 'ChatGPT-Account-ID': before.identity.id },
41
+ redirect: 'manual', signal: AbortSignal.timeout(10000),
42
+ });
43
+ if (response.status !== 200 || !response.body) { await response.body?.cancel(); return null; }
44
+ const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0;
45
+ try {
46
+ for (;;) {
47
+ const part = await reader.read(); if (part.done) break;
48
+ size += part.value.length;
49
+ if (size > 65536) { await reader.cancel(); return null; }
50
+ chunks.push(part.value);
51
+ }
52
+ } finally { reader.releaseLock(); }
53
+ const usage = JSON.parse(Buffer.concat(chunks).toString('utf8'));
54
+ const after = readCredential();
55
+ if (!equal(before.identity, after.identity) || before.accessToken !== after.accessToken
56
+ || usage.account_id !== before.identity.id || usage.user_id !== before.identity.userId
57
+ || usage.plan_type !== before.identity.plan) return null;
58
+ return before.identity;
59
+ } catch { return null; }
60
+ };
61
+ return { readCurrentIdentity, verifyFreshIdentity };
62
+ }
@@ -0,0 +1,93 @@
1
+ import { createServer } from "node:https";
2
+ import { Readable, type Duplex } from "node:stream";
3
+ import type { PemKeyPair } from "../../claude/intercept/local-ca";
4
+ import { forwardHeadersForUpstream } from "../../claude/intercept/listener";
5
+ import { dialDesktopUpstream, type DesktopTunnelOptions } from "../../lib/desktop-upstream-tunnel";
6
+
7
+ const ORIGIN = "https://chatgpt.com";
8
+ const STRIP = new Set(["connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length", "alt-svc", "proxy-authenticate"]);
9
+ export interface DesktopRelayOptions {
10
+ leaf: PemKeyPair;
11
+ fetchImpl: typeof fetch;
12
+ /** Test-only TLS peer; the production destination is fixed, never taken from a request. */
13
+ websocketPeer?: { host: string; port: number; ca: string };
14
+ websocketTransport?: Pick<DesktopTunnelOptions, "proxy" | "proxyCa" | "timeoutMs">;
15
+ }
16
+
17
+ /** Transparent HTTP and raw upgraded-socket relay. Only fetchImpl owns usage policy. */
18
+ export async function startDesktopRelay(options: DesktopRelayOptions) {
19
+ const sockets = new Set<Duplex>(), requests = new Set<AbortController>();
20
+ const server = createServer({ cert: options.leaf.certPem, key: options.leaf.keyPem, ALPNProtocols: ["http/1.1"] });
21
+ server.on("connection", socket => { sockets.add(socket); socket.once("close", () => sockets.delete(socket)); });
22
+ const valid = (host: string | undefined, path: string | undefined) =>
23
+ (host?.toLowerCase() === "chatgpt.com" || host?.toLowerCase() === "chatgpt.com:443") && !!path && path.startsWith("/") && !path.startsWith("//");
24
+ server.on("request", (req, res) => {
25
+ if (!valid(req.headers.host, req.url)) { res.writeHead(421); res.end(); return; }
26
+ const abort = new AbortController(); requests.add(abort);
27
+ const finish = () => { requests.delete(abort); abort.abort(); };
28
+ res.once("close", finish); req.once("error", finish);
29
+ void (async () => {
30
+ const headers = new Headers();
31
+ for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i]!, req.rawHeaders[i + 1]!);
32
+ const method = req.method ?? "GET";
33
+ const response = await options.fetchImpl(ORIGIN + req.url, {
34
+ method, headers: forwardHeadersForUpstream(headers), redirect: "manual", signal: abort.signal,
35
+ // Node's and Bun's declarations disagree on BYOB overloads; both implement Web streams.
36
+ body: method === "GET" || method === "HEAD" ? undefined : Readable.toWeb(req) as unknown as ReadableStream<Uint8Array>,
37
+ // @ts-expect-error Node-compatible streaming fetch requires half duplex.
38
+ duplex: "half",
39
+ });
40
+ if (res.destroyed) { await response.body?.cancel(); return; }
41
+ const output: Record<string, string | string[]> = {};
42
+ response.headers.forEach((value, key) => { if (!STRIP.has(key) && key !== "set-cookie") output[key] = value; });
43
+ const cookies = response.headers.getSetCookie(); if (cookies.length) output["set-cookie"] = cookies;
44
+ res.writeHead(response.status, output);
45
+ if (!response.body || method === "HEAD") { await response.body?.cancel(); res.end(); return; }
46
+ const body = Readable.fromWeb(response.body as unknown as import("node:stream/web").ReadableStream<Uint8Array>);
47
+ body.once("error", () => res.destroy()); res.once("close", () => body.destroy()); body.pipe(res);
48
+ })().catch(() => {
49
+ if (res.headersSent) res.destroy(); else { res.writeHead(502, { "Content-Length": "0" }); res.end(); }
50
+ });
51
+ });
52
+ server.on("upgrade", (req, client, head) => {
53
+ if (!valid(req.headers.host, req.url)) { client.end("HTTP/1.1 421 Misdirected Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); return; }
54
+ const peer = options.websocketPeer;
55
+ const abort = new AbortController(); requests.add(abort);
56
+ client.once("close", () => { requests.delete(abort); abort.abort(); });
57
+ client.once("error", () => abort.abort());
58
+ void dialDesktopUpstream({ ...options.websocketTransport, ...(peer ? { target: { host: peer.host, port: peer.port }, ca: peer.ca } : {}), signal: abort.signal }).then(upstream => {
59
+ requests.delete(abort);
60
+ if (client.destroyed || abort.signal.aborted) { upstream.destroy(); return; }
61
+ sockets.add(upstream);
62
+ upstream.once("error", () => client.destroy());
63
+ upstream.once("close", () => { sockets.delete(upstream); client.destroy(); });
64
+ client.once("close", () => upstream.destroy());
65
+ const lines = [`${req.method ?? "GET"} ${req.url} HTTP/1.1`];
66
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
67
+ if (!/^proxy-(?:authorization|connection)$/i.test(req.rawHeaders[i]!)) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
68
+ }
69
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
70
+ if (head.length) upstream.write(head);
71
+ client.pipe(upstream).pipe(client);
72
+ }).catch(() => {
73
+ requests.delete(abort);
74
+ if (!client.destroyed) client.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
75
+ });
76
+ });
77
+ try {
78
+ await new Promise<void>((resolve, reject) => {
79
+ server.once("error", reject);
80
+ server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); });
81
+ });
82
+ } catch (error) { server.close(); throw error; }
83
+ const address = server.address();
84
+ if (!address || typeof address === "string") throw new Error("desktop_compatibility_listener_unavailable");
85
+ let closing: Promise<void> | null = null;
86
+ return { port: address.port, close() {
87
+ if (closing) return closing;
88
+ for (const abort of requests) abort.abort();
89
+ for (const socket of sockets) socket.destroy();
90
+ closing = new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
91
+ server.closeAllConnections(); return closing;
92
+ } };
93
+ }
@@ -0,0 +1,10 @@
1
+ import type { OcxConfig } from "../../types";
2
+
3
+ export interface NativeCompatibilityOwner { hostname: string; port: number; loopbackPort?: number; config: OcxConfig }
4
+ let owner: NativeCompatibilityOwner | null = null;
5
+ /** Bound sockets, not persisted desired ports, identify this process's native routing target. */
6
+ export function bindNativeCompatibilityOwner(value: NativeCompatibilityOwner): () => void {
7
+ owner = value;
8
+ return () => { if (owner === value) owner = null; };
9
+ }
10
+ export function nativeCompatibilityOwner(): NativeCompatibilityOwner | null { return owner; }
@@ -0,0 +1,69 @@
1
+ import { lstatSync, readFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import type { OcxConfig } from "../../types";
5
+ import { nativeCompatibilityOwner, type NativeCompatibilityOwner } from "./routing-binding";
6
+
7
+ const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
8
+ // These are routing inputs, not proof of a conversation's final provider. Their
9
+ // changes revoke the observed context even if the loopback URL stays the same.
10
+ const ROUTING_KEYS = ["providers", "defaultProvider", "defaultModelAliases", "customModels", "combos", "routingProfiles",
11
+ "subagentModelFallback", "subagentModelFallbackByModel", "injectionModel", "compactionRouting", "compactionRecovery",
12
+ "blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels", "codexMainAccountHardLockFallback"] as const satisfies readonly (keyof OcxConfig)[];
13
+ function routingDigest(text: string, owner: NativeCompatibilityOwner): string {
14
+ const native = Bun.TOML.parse(text) as Record<string, unknown>;
15
+ // Desktop refreshes MCP endpoints after launch. Tool transport metadata and TOML
16
+ // formatting do not select the model transport. Keep all other (including unknown)
17
+ // native fields bound, rather than an allowlist that could miss a new routing key.
18
+ const { mcp_servers: _mcpServers, ...nativeContext } = native;
19
+ const projection = Object.fromEntries(ROUTING_KEYS.map(key => [key, owner.config[key]]));
20
+ const serialized = JSON.stringify([nativeContext, owner.hostname, owner.port, owner.loopbackPort, projection], (_key, value) =>
21
+ record(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value);
22
+ if (Buffer.byteLength(serialized) > 1048576) throw new Error("Routing snapshot too large");
23
+ return createHash("sha256").update(serialized).digest("hex");
24
+ }
25
+ /** Validates the effective root/profile routing only; it does not prove a thread's selected provider. */
26
+ export function matchesNativeCompatibilityRouting(text: string, owner: NativeCompatibilityOwner | null): boolean {
27
+ if (!owner || owner.config.codexDesktopAuthless === true || owner.config.runtimeRole === "client") return false;
28
+ try {
29
+ const root: unknown = Bun.TOML.parse(text);
30
+ if (!record(root)) return false;
31
+ let effective = root;
32
+ if (root.profile !== undefined) {
33
+ if (typeof root.profile !== "string" || !record(root.profiles)) return false;
34
+ const profile = root.profiles[root.profile];
35
+ if (!record(profile)) return false;
36
+ effective = { ...root, ...profile };
37
+ }
38
+ if ((effective.model_provider ?? "openai") !== "openai" || effective.forced_login_method === "api") return false;
39
+ if (typeof effective.openai_base_url !== "string") return false;
40
+ const url = new URL(effective.openai_base_url);
41
+ if (url.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password
42
+ || url.search || url.hash || !["/v1", "/v1/"].includes(url.pathname) || !url.port) return false;
43
+ const port = Number(url.port), matchesPort = (value: number | undefined) => Number.isInteger(value) && Number(value) > 0 && value === port;
44
+ // The companion always binds IPv4. localhost can resolve to another address family.
45
+ if (matchesPort(owner.loopbackPort) && url.hostname === "127.0.0.1") return true;
46
+ if (!matchesPort(owner.port)) return false;
47
+ return url.hostname === "127.0.0.1" ? ["127.0.0.1", "0.0.0.0"].includes(owner.hostname)
48
+ : ["::1", "[::1]", "::", "[::]"].includes(owner.hostname);
49
+ } catch { return false; }
50
+ }
51
+ export function createNativeRoutingVerifier(codexHome: string, readOwner = nativeCompatibilityOwner) {
52
+ const path = join(codexHome, "config.toml");
53
+ let baseline: string | undefined, invalidated = false;
54
+ return () => {
55
+ if (invalidated) return false;
56
+ try {
57
+ // Process-level overrides could put the app on another transport despite its TOML.
58
+ if (["CODEX_API_BASE_URL", "CODEX_APP_SERVER_WS_URL", "CODEX_ELECTRON_USER_DATA_PATH", "ELECTRON_RUN_AS_NODE"].some(key => process.env[key]?.trim())) { invalidated = true; return false; }
59
+ const stat = lstatSync(path);
60
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) { invalidated = true; return false; }
61
+ const text = readFileSync(path, "utf8"), owner = readOwner();
62
+ if (!owner || !matchesNativeCompatibilityRouting(text, owner)) { invalidated = true; return false; }
63
+ const current = routingDigest(text, owner);
64
+ if (baseline !== undefined && current !== baseline) { invalidated = true; return false; }
65
+ baseline ??= current;
66
+ return true;
67
+ } catch { invalidated = true; return false; }
68
+ };
69
+ }
@@ -0,0 +1,28 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ interface LaunchContext { pacUrl: string; generation: string; current(): boolean }
4
+ /** Process-local ownership; the management sibling guard owns the cross-instance boundary. */
5
+ let owner: { token: symbol; kind: "runtime" | "certificate"; launch?: LaunchContext } | null = null;
6
+ export function desktopCompatibilityRuntimeActive(): boolean { return owner?.kind === "runtime"; }
7
+ function acquire(kind: "runtime" | "certificate"): () => void {
8
+ if (owner !== null) throw new Error("desktop_compatibility_busy");
9
+ const token = Symbol(); owner = { token, kind };
10
+ return () => { if (owner?.token === token) owner = null; };
11
+ }
12
+ export const acquireDesktopCompatibilityRuntime = () => acquire("runtime");
13
+ export const acquireDesktopCertificateMutation = () => acquire("certificate");
14
+
15
+ /** Published only by a successfully started runtime, never reconstructed from disk or app arguments. */
16
+ export function bindDesktopCompatibilityLaunch(pacUrl: string, current: () => boolean): () => void {
17
+ if (owner?.kind !== "runtime" || owner.launch) throw new Error("desktop_compatibility_launch_owner_unverified");
18
+ const lease = owner, launch = { pacUrl, generation: randomUUID(), current };
19
+ lease.launch = launch;
20
+ return () => { if (lease.launch === launch) delete lease.launch; };
21
+ }
22
+
23
+ /** No key loading, socket discovery, or optional-runtime construction on the ordinary restart path. */
24
+ export function readDesktopCompatibilityLaunch(): { pacUrl: string; generation: string } | null {
25
+ const launch = owner?.kind === "runtime" ? owner.launch : undefined;
26
+ try { return launch?.current() ? { pacUrl: launch.pacUrl, generation: launch.generation } : null; }
27
+ catch { return null; }
28
+ }
@@ -0,0 +1,182 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { getConfigDir } from "../../config/paths";
4
+ import { issueServerLeaf } from "../../claude/intercept/local-ca";
5
+ import { startConnectProxy, type ConnectProxyHandle } from "../../claude/intercept/connect-proxy";
6
+ import { didRunOptionalShutdownHooks, registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
7
+ import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
8
+ import { desktopOutboundFetch, desktopProxyFor } from "../../lib/desktop-proxy-route";
9
+ import { inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, type StoredDesktopAuthority } from "./certificate-store";
10
+ import { inspectWindowsCertificateTrust } from "./windows-certificate-trust";
11
+ import { createNativeIdentityReader } from "./native-identity";
12
+ import { UsageRelayController, type UsageIdentity } from "./usage-controller";
13
+ import { createUsageControlledFetch } from "./usage-controlled-fetch";
14
+ import { startDesktopRelay } from "./relay-listener";
15
+ import { launchWindowsCodexCompatibility } from "./windows-package-launch";
16
+ import { acquireDesktopCompatibilityRuntime, bindDesktopCompatibilityLaunch } from "./runtime-ownership";
17
+ import { createDesktopConnectionStore, type DesktopConnectionStore } from "./connection-store";
18
+ import { createNativeRoutingVerifier } from "./routing-preflight";
19
+ import { createInstalledBuildProbe, type DesktopBuildAssessment } from "./installed-build";
20
+
21
+ export { DESKTOP_COMPATIBILITY_ASSESSED_VERSION, isAssessedDesktopInstalled } from "./installed-build";
22
+ export interface DesktopRuntimeIo {
23
+ platform?: string;
24
+ loadAuthority?: () => Promise<StoredDesktopAuthority>;
25
+ trust?: (authority: StoredDesktopAuthority) => Promise<"trusted" | "not-trusted" | "unknown">;
26
+ identity?: ReturnType<typeof createNativeIdentityReader>;
27
+ buildSupported?: () => boolean | Promise<boolean>;
28
+ /** Internal fixture probe factory; requires testOnly and all sandbox identity/CA seams. */
29
+ testOnlyBuildProbe?: () => ReturnType<typeof createInstalledBuildProbe>;
30
+ upstreamFetch?: typeof fetch;
31
+ connectionStore?: DesktopConnectionStore;
32
+ routingSupported?: () => boolean;
33
+ /** Synthetic-test execution is admitted only with explicit fixture identity and CA seams. */
34
+ testOnly?: boolean;
35
+ }
36
+
37
+ /** Optional runtime. Import/construction/status do not start listeners or touch credentials. */
38
+ export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
39
+ const platform = io.platform ?? process.platform;
40
+ let buildProbe: ReturnType<typeof createInstalledBuildProbe> | undefined;
41
+ let previousBuildAssessment: DesktopBuildAssessment | null = null;
42
+ const buildSupported = io.buildSupported ?? (() => buildProbe?.check() ?? Promise.resolve(false));
43
+ let routingSupported = io.routingSupported ?? (() => false);
44
+ let contextFailure: "build_unverified" | "native_routing_unverified" | null = null;
45
+ const contextValid = async () => {
46
+ try {
47
+ if (!routingSupported()) { contextFailure = "native_routing_unverified"; return false; }
48
+ const supported = await buildSupported();
49
+ contextFailure = !routingSupported() ? "native_routing_unverified" : !supported ? "build_unverified" : null;
50
+ }
51
+ catch { contextFailure = "build_unverified"; }
52
+ return contextFailure === null;
53
+ };
54
+ let phase: "off" | "starting" | "running" | "stopping" | "cleanup-required" = "off";
55
+ let owned: { controller: UsageRelayController; close(): Promise<void>; pacUrl: string; fingerprint: string; deadline: number } | null = null;
56
+ let closing: Promise<void> | null = null;
57
+ let pendingCleanup: (() => Promise<void>) | null = null;
58
+ let releaseOwner: (() => void) | null = null;
59
+ const status = () => ({ phase, supported: platform === "win32", running: owned !== null, contextFailure,
60
+ ...(buildProbe ? { buildAssessment: buildProbe.lastResult() ?? (previousBuildAssessment ? { ...previousBuildAssessment } : null) } : {}),
61
+ ...(owned ? { fingerprint: owned.fingerprint, safetyDeadline: owned.deadline, usage: owned.controller.snapshot() } : {}) });
62
+ async function stop(): Promise<ReturnType<typeof status>> {
63
+ if (phase === "starting") throw new Error("desktop_compatibility_busy");
64
+ if (closing) { await closing; return status(); }
65
+ if (!pendingCleanup) return status();
66
+ phase = "stopping";
67
+ closing = pendingCleanup().then(() => { owned = null; pendingCleanup = null; phase = "off"; releaseOwner?.(); releaseOwner = null; })
68
+ .catch(error => { phase = "cleanup-required"; throw error; }).finally(() => { closing = null; });
69
+ await closing; return status();
70
+ }
71
+ async function start() {
72
+ if (phase !== "off") throw new Error("desktop_compatibility_busy");
73
+ if (platform !== "win32") throw new Error("desktop_compatibility_unsupported");
74
+ if (io.testOnlyBuildProbe && (!io.testOnly || io.buildSupported)) throw new Error("desktop_compatibility_test_environment");
75
+ if ((isTestHomeGuardArmed() || io.testOnlyBuildProbe) && !(io.testOnly && io.identity && io.loadAuthority && io.trust && (io.buildSupported || io.testOnlyBuildProbe) && io.connectionStore && io.routingSupported)) throw new Error("desktop_compatibility_test_environment");
76
+ if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
77
+ try { desktopProxyFor(new URL("https://chatgpt.com")); } catch { throw new Error("desktop_compatibility_egress_proxy_invalid"); }
78
+ releaseOwner = acquireDesktopCompatibilityRuntime(); phase = "starting";
79
+ let relay: Awaited<ReturnType<typeof startDesktopRelay>> | undefined, proxy: ConnectProxyHandle | undefined;
80
+ let pac: ReturnType<typeof Bun.serve> | undefined, timer: ReturnType<typeof setInterval> | undefined;
81
+ let detach: (() => void) | undefined, unbindLaunch: (() => void) | undefined, controller: UsageRelayController | undefined;
82
+ const cleanup = async () => {
83
+ unbindLaunch?.();
84
+ if (timer) clearInterval(timer); detach?.();
85
+ // Abort transport before awaiting stream refresh so a stuck reader cannot retain sockets.
86
+ const results = await Promise.allSettled([pac?.stop(true), proxy?.close(), relay?.close(), controller?.observeOnly(), buildProbe?.close()]);
87
+ if (results.some(result => result.status === "rejected")) throw new Error("desktop_compatibility_cleanup_incomplete");
88
+ };
89
+ pendingCleanup = cleanup;
90
+ try {
91
+ if (!io.buildSupported) {
92
+ // A fresh probe owns this start; diagnostics retain the last completed observation.
93
+ previousBuildAssessment = buildProbe?.lastResult() ?? previousBuildAssessment;
94
+ buildProbe = io.testOnlyBuildProbe ? io.testOnlyBuildProbe() : createInstalledBuildProbe();
95
+ }
96
+ if (!io.routingSupported) routingSupported = createNativeRoutingVerifier((await import("../paths")).getCodexHome());
97
+ if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
98
+ const directory = join(getConfigDir(), "codex-desktop-compatibility");
99
+ if (!io.loadAuthority) {
100
+ const saved = inspectDesktopCompatibilityAuthority(directory);
101
+ if (saved.status === "missing") throw new Error("desktop_compatibility_certificate_not_prepared");
102
+ if (saved.status === "invalid") throw new Error("desktop_compatibility_certificate_invalid");
103
+ if (saved.status === "expired") throw new Error("desktop_compatibility_certificate_expired");
104
+ }
105
+ const authority = await (io.loadAuthority?.() ?? loadDesktopCompatibilityAuthority({ directory }));
106
+ const trust = await (io.trust?.(authority) ?? inspectWindowsCertificateTrust(authority.authority.certPem, authority.fingerprint));
107
+ if (trust !== "trusted") throw new Error("desktop_compatibility_trust_required");
108
+ const connections = io.connectionStore ?? createDesktopConnectionStore(directory);
109
+ const previousConnection = connections.read();
110
+ const deadline = authority.expiresAt - 300_000;
111
+ if (deadline <= Date.now()) throw new Error("desktop_compatibility_certificate_expiring");
112
+ const identity = io.identity ?? createNativeIdentityReader(join((await import("../paths")).getCodexHome(), "auth.json"), desktopOutboundFetch as typeof fetch);
113
+ const account: UsageIdentity | null = await identity.verifyFreshIdentity();
114
+ if (!account) throw new Error("desktop_compatibility_native_identity_unverified");
115
+ if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
116
+ controller = new UsageRelayController(account, identity.readCurrentIdentity, identity.verifyFreshIdentity, Date.now, deadline, 180000, contextValid);
117
+ relay = await startDesktopRelay({ leaf: issueServerLeaf(authority.authority, authority.commonName, ["chatgpt.com"]),
118
+ fetchImpl: createUsageControlledFetch(controller, io.upstreamFetch ?? desktopOutboundFetch as typeof fetch) });
119
+ try { proxy = await startConnectProxy(previousConnection?.connectPort ?? 0, { interceptPort: relay.port, interceptHosts: ["chatgpt.com"], allowedTargets: ["chatgpt.com:443"] }); }
120
+ catch { throw new Error("desktop_compatibility_connection_unavailable"); }
121
+ const runId = previousConnection?.id ?? randomUUID(), proxyPort = proxy.port;
122
+ try { pac = Bun.serve({ hostname: "127.0.0.1", port: previousConnection?.pacPort ?? 0, fetch(req) {
123
+ const url = new URL(req.url);
124
+ if (req.method !== "GET" || req.headers.has("origin") || url.origin !== `http://127.0.0.1:${pac!.port}` || url.pathname !== `/${runId}/proxy.pac`) return new Response(null, { status: 404 });
125
+ return new Response(`function FindProxyForURL(url, host) { return Date.now() < ${deadline} && host.toLowerCase() === "chatgpt.com" && url.indexOf("https:") === 0 ? "PROXY 127.0.0.1:${proxyPort}; DIRECT" : "DIRECT"; }`,
126
+ { headers: { "content-type": "application/x-ns-proxy-autoconfig", "cache-control": "no-store" } });
127
+ } }); } catch { throw new Error("desktop_compatibility_connection_unavailable"); }
128
+ const connection = await connections.publish({ version: 1, id: runId, connectPort: proxyPort, pacPort: pac.port! });
129
+ if (connection.id !== runId || connection.connectPort !== proxyPort || connection.pacPort !== pac.port) throw new Error("desktop_compatibility_connection_changed");
130
+ if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
131
+ const active = controller; let ticking = false, deadlineHandled = false, lastContextCheck = Date.now();
132
+ timer = setInterval(() => {
133
+ if (ticking) return; ticking = true;
134
+ const expired = Date.now() >= deadline;
135
+ const work = (async () => {
136
+ const contextCadence = active.snapshot().mode === "apply" ? 10000 : 60000;
137
+ if (!expired && Date.now() - lastContextCheck >= contextCadence) {
138
+ lastContextCheck = Date.now();
139
+ if (!await contextValid() && active.snapshot().mode === "apply") { await active.observeOnly(); return; }
140
+ }
141
+ await (expired && !deadlineHandled ? (deadlineHandled = true, active.observeOnly()) : active.expireIfNeeded());
142
+ })();
143
+ void work.catch(() => {}).finally(() => { ticking = false; });
144
+ }, 1000); timer.unref();
145
+ owned = { controller, close: cleanup, fingerprint: authority.fingerprint, deadline, pacUrl: `http://127.0.0.1:${pac.port}/${runId}/proxy.pac` };
146
+ phase = "running";
147
+ const launchedBy = owned;
148
+ unbindLaunch = bindDesktopCompatibilityLaunch(owned.pacUrl,
149
+ () => owned === launchedBy && phase === "running" && Date.now() < deadline);
150
+ detach = registerOptionalShutdownHook("codex-desktop-compatibility", () => { void stop().catch(() => {}); });
151
+ return status();
152
+ } catch (error) {
153
+ try { await cleanup(); pendingCleanup = null; phase = "off"; releaseOwner?.(); releaseOwner = null; }
154
+ catch { phase = "cleanup-required"; throw new Error("desktop_compatibility_cleanup_incomplete"); }
155
+ throw error;
156
+ }
157
+ }
158
+ async function apply(accountWideConsent: boolean) {
159
+ const current = owned;
160
+ if (!current || phase !== "running") throw new Error("desktop_compatibility_not_running");
161
+ const generation = current.controller.snapshot().generation, valid = await contextValid();
162
+ if (owned !== current || phase !== "running") throw new Error("desktop_compatibility_not_running");
163
+ if (generation !== current.controller.snapshot().generation) return { accepted: false, reason: "superseded", ...current.controller.snapshot() };
164
+ if (!valid) { await current.controller.observeOnly(); throw new Error(`desktop_compatibility_${contextFailure}`); }
165
+ const result = await current.controller.activate({ scope: "account-ui-compatibility", accountWideConsent });
166
+ if (owned !== current || phase !== "running") { await current.controller.observeOnly(); return { ...result, accepted: false, reason: "runtime-stopped" }; }
167
+ return result;
168
+ }
169
+ return { status, start, stop, apply,
170
+ async observe() { if (owned) await owned.controller.observeOnly(); return status(); },
171
+ async launch() {
172
+ if (!owned || phase !== "running") throw new Error("desktop_compatibility_not_running");
173
+ const current = owned;
174
+ if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
175
+ if (owned !== current || phase !== "running") throw new Error("desktop_compatibility_not_running");
176
+ return launchWindowsCodexCompatibility(current.pacUrl);
177
+ },
178
+ /** Internal native launch context; management status never returns the control endpoint. */
179
+ getPacUrl: () => owned?.pacUrl ?? null,
180
+ };
181
+ }
182
+ export type DesktopCompatibilityRuntime = ReturnType<typeof createDesktopCompatibilityRuntime>;
@@ -0,0 +1,30 @@
1
+ import { createDesktopCompatibilityRuntime, type DesktopCompatibilityRuntime } from "./runtime";
2
+
3
+ /** One runtime for management and automatic startup; shutdown never constructs a new instance. */
4
+ export function createDesktopCompatibilityService(factory: () => DesktopCompatibilityRuntime = createDesktopCompatibilityRuntime) {
5
+ let runtime: DesktopCompatibilityRuntime | undefined;
6
+ let starting: Promise<ReturnType<DesktopCompatibilityRuntime["status"]>> | null = null;
7
+ let shuttingDown = false;
8
+ return {
9
+ get(): DesktopCompatibilityRuntime {
10
+ if (!runtime) {
11
+ const raw = factory();
12
+ runtime = { ...raw, start() {
13
+ if (shuttingDown) return Promise.reject(new Error("desktop_compatibility_stopping"));
14
+ if (starting) return Promise.reject(new Error("desktop_compatibility_busy"));
15
+ starting = raw.start().finally(() => { starting = null; });
16
+ return starting;
17
+ } };
18
+ }
19
+ return runtime;
20
+ },
21
+ async shutdown(): Promise<void> {
22
+ shuttingDown = true;
23
+ try { await starting; } catch { /* failed start owns its cleanup */ }
24
+ if (runtime) await runtime.stop();
25
+ },
26
+ };
27
+ }
28
+ const service = createDesktopCompatibilityService();
29
+ export const getDesktopCompatibilityRuntime = () => service.get();
30
+ export const shutdownDesktopCompatibility = () => service.shutdown();
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readConfigFileSnapshot } from "../../config/diagnostics";
3
+ import { mutatePersistedConfig } from "../../config/persisted-mutation";
4
+ import { desktopCompatibilityConfigError } from "../../config/schema/desktop-compatibility";
5
+ import { adoptPersistedDesktopCompatibility } from "../../config/live-reconcile";
6
+ import type { OcxConfig } from "../../types";
7
+
8
+ export interface DesktopStartupSettings { startOnProxyStart: boolean; revision: string }
9
+ interface SettingsIo { read?: typeof readConfigFileSnapshot; mutate?: typeof mutatePersistedConfig; liveConfig?: OcxConfig }
10
+ const failure = (code: string) => new Error(`desktop_compatibility_settings_${code}`);
11
+ /** A next-process preference: never changes live runtime, trust, app or login state. */
12
+ export function createDesktopStartupSettings(io: SettingsIo = {}) {
13
+ const read = io.read ?? readConfigFileSnapshot, mutate = io.mutate ?? mutatePersistedConfig;
14
+ function inspect() {
15
+ const snapshot = read();
16
+ if (snapshot.diagnostics.source !== "file" || snapshot.raw === undefined) throw failure("unavailable");
17
+ let raw: Record<string, unknown>;
18
+ try { raw = JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")); } catch { throw failure("unavailable"); }
19
+ if (desktopCompatibilityConfigError(raw)) throw failure("invalid");
20
+ const setting = raw.desktopCompatibility as { startOnProxyStart: boolean } | undefined;
21
+ return { setting, status: { startOnProxyStart: setting?.startOnProxyStart === true,
22
+ revision: createHash("sha256").update(JSON.stringify(setting ?? null)).digest("hex") } };
23
+ }
24
+ const status = (): DesktopStartupSettings => inspect().status;
25
+ function set(startOnProxyStart: boolean, expectedRevision: string): DesktopStartupSettings {
26
+ if (typeof startOnProxyStart !== "boolean" || !/^[a-f0-9]{64}$/.test(expectedRevision)) throw failure("invalid_request");
27
+ let outcome;
28
+ try { outcome = mutate(config => {
29
+ // Runs under the existing config lock, including each rebase retry. Unrelated fields
30
+ // may change, but a changed preference must be shown again before this write.
31
+ const current = status();
32
+ if (current.revision !== expectedRevision) throw failure("changed");
33
+ if (current.startOnProxyStart === startOnProxyStart) return { changed: false, value: startOnProxyStart };
34
+ config.desktopCompatibility = { startOnProxyStart };
35
+ return { changed: true, value: startOnProxyStart };
36
+ }); } catch (error) {
37
+ // Publication can succeed before bookkeeping fails. Reconcile a verified disk
38
+ // field without claiming success or replaying the uncertain write.
39
+ if (io.liveConfig) { try { adoptPersistedDesktopCompatibility(io.liveConfig, inspect().setting); } catch { /* unresolved state remains an error */ } }
40
+ throw error;
41
+ }
42
+ if (outcome.status === "unavailable") throw failure("unavailable");
43
+ const actual = inspect();
44
+ if (io.liveConfig) adoptPersistedDesktopCompatibility(io.liveConfig, actual.setting);
45
+ if (actual.status.startOnProxyStart !== startOnProxyStart) throw failure("changed");
46
+ return actual.status;
47
+ }
48
+ return { status, set };
49
+ }
50
+ export type DesktopStartupSettingsService = ReturnType<typeof createDesktopStartupSettings>;