@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/bridge/sse.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { anthropicProgressItems } from "../responses/anthropic-progress";
1
2
  import type {
2
3
  AdapterEvent,
4
+ AnthropicServerBlocks,
3
5
  OcxMessagePhase,
4
6
  OcxProviderContinuationState,
5
7
  OcxProviderOpaqueToolCallMetadata,
@@ -12,7 +14,7 @@ import {
12
14
  classifyError,
13
15
  cyberPolicyErrorType,
14
16
  CYBER_POLICY_ERROR_CODE,
15
- isCyberPolicyCode,
17
+ isTerminalRefusalCode,
16
18
  type OcxErrorPayload,
17
19
  } from "../lib/errors";
18
20
  import { redactSecretString } from "../lib/redact";
@@ -50,6 +52,7 @@ import {
50
52
  } from "../lib/translator-budget";
51
53
  import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
52
54
  import type { OutputItem, StringChunks } from "./internal";
55
+ import { terminalRefusalFailure } from "./terminal-refusal";
53
56
 
54
57
  function sseEvent(name: string, data: Record<string, unknown>): string {
55
58
  return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
@@ -88,6 +91,7 @@ export function bridgeToResponsesSSE(
88
91
  * response.completed — codex-rs collect_compaction_output requires exactly one.
89
92
  */
90
93
  compaction?: boolean;
94
+ compactionRetention?: { encode(summary: string): string; commit(): void };
91
95
  /** One-shot: first non-empty text/thinking/raw-reasoning/tool-input delta observed. */
92
96
  onFirstOutput?: () => void;
93
97
  onTerminal?: (status: ResponsesTerminalStatus) => void;
@@ -120,6 +124,12 @@ export function bridgeToResponsesSSE(
120
124
  * would reach the client under the invented name instead of the normalized one.
121
125
  */
122
126
  enforceDeclaredToolNames?: boolean;
127
+ /**
128
+ * Deliver a non-retryable upstream refusal as `response.failed` with a terminal-refusal code
129
+ * instead of `response.incomplete` (see `./terminal-refusal`). Set only for the Responses
130
+ * inbound wire: the chat and Anthropic encoders read the incomplete to report a filter stop.
131
+ */
132
+ terminalRefusalAsFailed?: boolean;
123
133
  /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
124
134
  toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
125
135
  /**
@@ -676,12 +686,15 @@ export function bridgeToResponsesSSE(
676
686
  // leaves a "Searching the web" spinner spinning forever.
677
687
  // `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
678
688
  // downstream translators (claude outbound) can fill web_search_tool_result content.
679
- const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
689
+ // `anthropic` (Anthropic's server-side call) rides verbatim as `anthropic_blocks`, for the same
690
+ // claude outbound translator; a web_fetch makes the cell a page open.
691
+ const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[], anthropic?: AnthropicServerBlocks) => {
680
692
  if (!currentWebSearch) return;
681
693
  const item = {
682
694
  type: "web_search_call", id: currentWebSearch.itemId, status,
683
- action: webSearchAction(queries),
695
+ action: webSearchAction(queries, anthropic),
684
696
  ...(sources && sources.length > 0 ? { sources } : {}),
697
+ ...(anthropic ? { anthropic_blocks: anthropic } : {}),
685
698
  };
686
699
  emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
687
700
  retainFinishedItem(item as OutputItem);
@@ -841,6 +854,7 @@ export function bridgeToResponsesSSE(
841
854
  // its compaction UI renders nothing mid-turn, so nothing is lost visually.
842
855
  if (options?.compaction) {
843
856
  if (event.type === "text_delta") {
857
+ if (event.anthropicThinkingSignature) continue;
844
858
  compaction = appendString(
845
859
  compaction,
846
860
  event.text,
@@ -871,6 +885,29 @@ export function bridgeToResponsesSSE(
871
885
  break;
872
886
  }
873
887
  case "text_delta": {
888
+ if (event.anthropicThinkingSignature) {
889
+ rawReasoningForNextToolCall = "";
890
+ closeCurrentMessage("commentary");
891
+ closeCurrentReasoning();
892
+ closeCurrentRawReasoning();
893
+ flushHiddenRawReasoning();
894
+ flushHiddenReasoningEnvelope();
895
+ if (currentToolCall) closeCurrentToolCall();
896
+ for (const item of anthropicProgressItems(event.text, event.anthropicThinkingSignature, budget)) {
897
+ emit("response.output_item.added", { output_index: outputIndex, item: { ...item, ...(item.type === "message" ? { status: "in_progress", content: [] } : {}) } });
898
+ if (item.type === "message") {
899
+ const part = item.content![0];
900
+ emit("response.content_part.added", { item_id: item.id, output_index: outputIndex, content_index: 0, part: { ...part, text: "" } });
901
+ emit("response.output_text.delta", { item_id: item.id, output_index: outputIndex, content_index: 0, delta: event.text });
902
+ emit("response.output_text.done", { item_id: item.id, output_index: outputIndex, content_index: 0, text: event.text });
903
+ emit("response.content_part.done", { item_id: item.id, output_index: outputIndex, content_index: 0, part });
904
+ }
905
+ emit("response.output_item.done", { output_index: outputIndex, item });
906
+ retainFinishedItem(item as OutputItem);
907
+ outputIndex++;
908
+ }
909
+ break;
910
+ }
874
911
  if (currentReasoning) closeCurrentReasoning();
875
912
  if (currentRawReasoning) closeCurrentRawReasoning();
876
913
  flushHiddenRawReasoning();
@@ -1195,7 +1232,7 @@ export function bridgeToResponsesSSE(
1195
1232
  currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
1196
1233
  }
1197
1234
  const safeSources = safeWebSearchSources(event.sources);
1198
- closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
1235
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources, event.anthropic);
1199
1236
  // Queue this search's sources for the next assistant message (dedup by URL).
1200
1237
  if (safeSources.length > 0) {
1201
1238
  for (const source of safeSources) {
@@ -1233,7 +1270,7 @@ export function bridgeToResponsesSSE(
1233
1270
  // Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
1234
1271
  const item = {
1235
1272
  type: "compaction", id: `cmp_${uuid()}`,
1236
- encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
1273
+ encrypted_content: event.compactionEncryptedContent ?? (options?.compactionRetention?.encode(joinChunks(compaction)) ?? encodeCompactionSummary(joinChunks(compaction))),
1237
1274
  };
1238
1275
  emit("response.output_item.done", { output_index: outputIndex, item });
1239
1276
  retainFinishedItem(item as OutputItem, event.compactionEncryptedContent ? 0 : compaction.bytes);
@@ -1271,6 +1308,7 @@ export function bridgeToResponsesSSE(
1271
1308
  emit("response.completed", {
1272
1309
  response,
1273
1310
  });
1311
+ if (!closed && !terminated && options?.compaction && !event.compactionEncryptedContent) options.compactionRetention?.commit();
1274
1312
  reportTerminal("completed");
1275
1313
  }
1276
1314
  terminalEvent = true;
@@ -1287,12 +1325,25 @@ export function bridgeToResponsesSSE(
1287
1325
  flushHiddenReasoningEnvelope();
1288
1326
  options?.onUsage?.(event.usage);
1289
1327
  await awaitThoughtSignatureDurability();
1328
+ const refusal = options?.terminalRefusalAsFailed ? terminalRefusalFailure(event) : undefined;
1329
+ if (refusal) {
1330
+ emit("response.failed", {
1331
+ response: {
1332
+ ...responseSnapshot("failed", finishedItems, event.endTurn),
1333
+ usage: responsesUsage(event.usage),
1334
+ ...refusal,
1335
+ },
1336
+ });
1337
+ reportTerminal("failed");
1338
+ terminalEvent = true;
1339
+ break;
1340
+ }
1290
1341
  emit("response.incomplete", {
1291
1342
  response: {
1292
1343
  ...responseSnapshot("incomplete", finishedItems, event.endTurn),
1293
1344
  usage: responsesUsage(event.usage),
1294
1345
  incomplete_details: {
1295
- reason: event.reason,
1346
+ reason: truncationReasonFor(event.reason) ?? event.reason,
1296
1347
  ...(event.message ? { message: event.message } : {}),
1297
1348
  ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
1298
1349
  },
@@ -1325,7 +1376,7 @@ export function bridgeToResponsesSSE(
1325
1376
  ...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
1326
1377
  error: failure.error,
1327
1378
  last_error: failure.error,
1328
- ...(isCyberPolicyCode(failure.error.code)
1379
+ ...(isTerminalRefusalCode(failure.error.code)
1329
1380
  ? { retryable: false }
1330
1381
  : event.retryable !== undefined ? { retryable: event.retryable } : {}),
1331
1382
  },
@@ -1366,7 +1417,7 @@ export function bridgeToResponsesSSE(
1366
1417
  ...responseSnapshot("failed", finishedItems),
1367
1418
  error: failure,
1368
1419
  last_error: failure,
1369
- ...(isCyberPolicyCode(failure.code) ? { retryable: false } : {}),
1420
+ ...(isTerminalRefusalCode(failure.code) ? { retryable: false } : {}),
1370
1421
  },
1371
1422
  });
1372
1423
  reportTerminal("failed");
@@ -0,0 +1,59 @@
1
+ import type { AdapterEvent } from "../types";
2
+ import type { OcxErrorPayload } from "../lib/errors";
3
+ import { redactSecretString } from "../lib/redact";
4
+
5
+ /**
6
+ * The terminal-refusal code Codex ends a turn on. It is a member of `TERMINAL_REFUSAL_CODES`
7
+ * in `src/lib/errors.ts`, and the one that says "this prompt, as sent, is refused" without
8
+ * claiming a specific policy family the upstream never named.
9
+ */
10
+ const TERMINAL_REFUSAL_FAILED_CODE = "invalid_prompt";
11
+
12
+ interface TerminalRefusalFailure {
13
+ error: OcxErrorPayload;
14
+ last_error: OcxErrorPayload;
15
+ retryable: false;
16
+ }
17
+
18
+ /**
19
+ * Client-visible copy: what happened, and that resending the same context cannot help. It points
20
+ * at a new conversation rather than a new turn, because a later turn replays the refused context.
21
+ */
22
+ function terminalRefusalMessage(detail: string | undefined): string {
23
+ const reason = detail?.trim() ? ` (${redactSecretString(detail.trim())})` : "";
24
+ return `The upstream model refused this request${reason}. `
25
+ + "Retrying with the same conversation context will not succeed; change the request or start a new conversation.";
26
+ }
27
+
28
+ /**
29
+ * An adapter `incomplete` that is a non-retryable upstream refusal: a content filter carrying an
30
+ * explicit `retryable: false` verdict. A filter without the verdict, an output cap, a stall and an
31
+ * adapter EOF do not qualify. The sidecar loops read this too: a refused iteration ends the turn,
32
+ * so a sidecar call it carried is neither run nor answered with another model request.
33
+ */
34
+ export function isTerminalRefusalIncomplete(event: AdapterEvent | undefined): boolean {
35
+ return event?.type === "incomplete" && event.reason === "content_filter" && event.retryable === false;
36
+ }
37
+
38
+ /**
39
+ * `response.failed` fields for an adapter `incomplete` that is a non-retryable upstream refusal
40
+ * (#4312), or `undefined` when the event is anything else. The Anthropic and Kiro adapters are
41
+ * the ones that emit it today.
42
+ *
43
+ * codex-rs maps every `response.incomplete` to a retryable stream error and never reads
44
+ * `incomplete_details.retryable`, so a refusal delivered that way is re-sent through the whole
45
+ * stream retry budget. It only ends the turn on `response.failed` carrying a terminal-refusal
46
+ * `error.code`. Only an explicit `retryable: false` content filter qualifies: a filter without
47
+ * the verdict, an output cap, a stall and an adapter EOF stay `response.incomplete`.
48
+ */
49
+ export function terminalRefusalFailure(
50
+ event: Extract<AdapterEvent, { type: "incomplete" }>,
51
+ ): TerminalRefusalFailure | undefined {
52
+ if (!isTerminalRefusalIncomplete(event)) return undefined;
53
+ const error: OcxErrorPayload = {
54
+ type: "invalid_request_error",
55
+ code: TERMINAL_REFUSAL_FAILED_CODE,
56
+ message: terminalRefusalMessage(event.message),
57
+ };
58
+ return { error, last_error: error, retryable: false };
59
+ }
@@ -1,4 +1,4 @@
1
- import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, type BigIntStats } from "node:fs";
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, type BigIntStats } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
@@ -84,9 +84,20 @@ export function readDesktopLibraryText(path: string): string {
84
84
  return readDesktopLibraryBytes(path).toString("utf8");
85
85
  }
86
86
 
87
+ /** Only an absent directory entry is optional; dangling links and failed reads still refuse. */
88
+ export function readOptionalDesktopLibraryBytes(path: string): Buffer | undefined {
89
+ try { return readDesktopLibraryBytes(path); }
90
+ catch (error) {
91
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /** Read optional verified metadata; a present file must contain an entries array. */
87
97
  export function parseMetadata(path: string): Desktop3pMetadata {
88
- if (!existsSync(path)) return { entries: [] };
89
- const parsed = JSON.parse(readDesktopLibraryText(path)) as Partial<Desktop3pMetadata>;
98
+ const bytes = readOptionalDesktopLibraryBytes(path);
99
+ if (bytes === undefined) return { entries: [] };
100
+ const parsed = JSON.parse(bytes.toString("utf8")) as Partial<Desktop3pMetadata>;
90
101
  if (!Array.isArray(parsed.entries)) throw new Error("Claude Desktop 3P _meta.json has no entries array");
91
102
  return { ...parsed, entries: parsed.entries };
92
103
  }
@@ -120,11 +131,25 @@ export const OPENCODEX_DESKTOP_PROFILE_KEYS = new Set([
120
131
  "inferenceModels",
121
132
  ]);
122
133
 
123
- export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
124
- if (!existsSync(path)) return {};
125
- const parsed = JSON.parse(readDesktopLibraryText(path)) as unknown;
134
+ export interface DesktopProfileSnapshot {
135
+ /** Verified bytes of the existing profile; undefined only when the entry is absent. */
136
+ bytes?: Buffer;
137
+ foreign: Record<string, unknown>;
138
+ }
139
+
140
+ /** Capture verified bytes for key preservation, backup and rollback; publication revalidates them. */
141
+ export function readDesktopProfileSnapshot(path: string): DesktopProfileSnapshot {
142
+ const bytes = readOptionalDesktopLibraryBytes(path);
143
+ if (bytes === undefined) return { foreign: {} };
144
+ const parsed = JSON.parse(bytes.toString("utf8")) as unknown;
126
145
  if (!isRecord(parsed)) throw new Error("Claude Desktop 3P profile is not a JSON object");
127
- return Object.fromEntries(
146
+ const foreign = Object.fromEntries(
128
147
  Object.entries(parsed).filter(([key]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)),
129
148
  );
149
+ return { bytes, foreign };
150
+ }
151
+
152
+ /** Preserve settings outside the gateway-owned keys from a verified profile. */
153
+ export function readDesktopProfileForeignKeys(path: string): Record<string, unknown> {
154
+ return readDesktopProfileSnapshot(path).foreign;
130
155
  }
@@ -5,15 +5,16 @@ import { applyRemoteDesktopStore, inspectRemoteDesktopCleanup, restoreRemoteDesk
5
5
  import { canonicalDirectory } from "./desktop-remote-store-io";
6
6
  import {
7
7
  resolveDesktop3pConfigLibraryPath, parseMetadata, SAFE_DESKTOP_PROFILE_ID, isRecord,
8
- isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileForeignKeys, readDesktopLibraryText, readDesktopLibraryBytes,
8
+ isOwnedDesktopEntry, isOwnedDesktopGatewayEntry, profilePath, readDesktopProfileSnapshot, readDesktopLibraryText, readDesktopLibraryBytes,
9
+ readOptionalDesktopLibraryBytes,
9
10
  type Desktop3pConfigLibraryOptions, type Desktop3pMetadata, type Desktop3pMetadataEntry,
10
11
  } from "./desktop-3p-library";
11
12
  export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
12
13
  import { createHash, randomUUID } from "node:crypto";
13
- import { copyFileSync, existsSync, mkdirSync, lstatSync, unlinkSync } from "node:fs";
14
+ import { existsSync, mkdirSync, lstatSync, unlinkSync, writeFileSync } from "node:fs";
14
15
  import { join } from "node:path";
15
16
  import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
16
- import { atomicWriteFileNoFollow } from "../config/atomic-write";
17
+ import { atomicWriteFileNoFollow, atomicWriteFileStreamed } from "../config/atomic-write";
17
18
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
18
19
  import type { OcxClaudeDesktopProfile } from "../types";
19
20
  import {
@@ -800,6 +801,7 @@ function desktopRefreshPathsSafe(libraryPath: string, configPath?: string): bool
800
801
  } catch { return false; }
801
802
  }
802
803
 
804
+ /** Publish a gateway profile from a verified snapshot, rolling it back if metadata fails. */
803
805
  function writeDesktop3pConfigWithGenerator(
804
806
  generate: () => object,
805
807
  refreshOnly?: { requiredId: string; expectedFingerprint: string; admit: () => boolean },
@@ -828,8 +830,8 @@ function writeDesktop3pConfigWithGenerator(
828
830
  : [...metadata.entries, entry];
829
831
 
830
832
  const generated = generate();
831
- const preserved = readDesktopProfileForeignKeys(configPath);
832
- const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
833
+ const previous = readDesktopProfileSnapshot(configPath);
834
+ const configJson = JSON.stringify({ ...previous.foreign, ...generated }, null, 2) + "\n";
833
835
  const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
834
836
  if (refreshOnly) {
835
837
  atomicWriteFileNoFollow(configPath, configJson, undefined, {
@@ -847,12 +849,31 @@ function writeDesktop3pConfigWithGenerator(
847
849
  });
848
850
  return { written: true, path: configPath, fingerprint };
849
851
  }
850
- const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
852
+ atomicReplaceDesktopConfig(configPath, configJson, previous.bytes);
851
853
  try {
852
854
  atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
853
855
  } catch (metaError) {
854
- // Rollback: restore the backed-up config if metadata write fails.
855
- if (backupPath && existsSync(backupPath)) copyFileSync(backupPath, configPath);
856
+ // Roll back from the verified bytes in memory: reopening the backup could meet a swapped
857
+ // entry and leave the new profile beside the old metadata. A profile this call created is
858
+ // unreferenced once metadata failed, so remove it rather than orphan one per retry.
859
+ // Rollback is ownership-aware: it acts only while the live profile still holds the bytes
860
+ // this call published, checked by the same comparison that guarded publication.
861
+ const base = metaError instanceof Error ? metaError.message : String(metaError);
862
+ const published = Buffer.from(configJson);
863
+ let owned = false;
864
+ const stillOwned = () => { owned = false; assertDesktopProfileUnchanged(configPath, published); owned = true; };
865
+ try {
866
+ stillOwned();
867
+ if (previous.bytes !== undefined) writeDesktopConfigBytes(configPath, previous.bytes, stillOwned);
868
+ else unlinkSync(configPath);
869
+ } catch (rollbackError) {
870
+ if (!owned) {
871
+ // Another writer replaced the profile after publication; its bytes win over our rollback.
872
+ throw new Error(`${base} (profile rollback skipped: profile changed after publish)`, { cause: metaError });
873
+ }
874
+ const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
875
+ throw new AggregateError([metaError, rollbackError], `${base} (profile rollback also failed: ${detail})`, { cause: metaError });
876
+ }
856
877
  throw metaError;
857
878
  }
858
879
  return { written: true, path: configPath, fingerprint };
@@ -862,14 +883,35 @@ function writeDesktop3pConfigWithGenerator(
862
883
  }
863
884
  }
864
885
 
865
- /** Backup an existing owned config then atomically replace it. Exported for failure-path tests. */
886
+ /**
887
+ * Compare the live profile with the caller's verified bytes before backup and before replacement.
888
+ * Refuse drift without retrying; backup and rollback continue to use the original verified bytes.
889
+ * `previous` is undefined only for an absent profile, which leaves any older backup alone.
890
+ * Exported for failure-path tests.
891
+ */
866
892
  export function atomicReplaceDesktopConfig(
867
893
  path: string,
868
894
  content: string,
869
- writer: (path: string, content: string) => void = atomicWriteFile,
895
+ previous: Buffer | undefined,
896
+ writer: typeof atomicWriteFileNoFollow = atomicWriteFileNoFollow,
870
897
  ): { backupPath?: string } {
898
+ const validateBeforeRename = () => assertDesktopProfileUnchanged(path, previous);
899
+ validateBeforeRename();
871
900
  const backupPath = `${path}.bak`;
872
- if (existsSync(path)) copyFileSync(path, backupPath);
873
- writer(path, content);
874
- return existsSync(backupPath) ? { backupPath } : {};
901
+ if (previous !== undefined) writeDesktopConfigBytes(backupPath, previous);
902
+ writer(path, content, undefined, { validateBeforeRename });
903
+ return previous === undefined ? {} : { backupPath };
904
+ }
905
+
906
+ /** Single compare-and-swap check: the live profile must still hold `expected` (undefined = absent). */
907
+ function assertDesktopProfileUnchanged(path: string, expected: Buffer | undefined): void {
908
+ const current = readOptionalDesktopLibraryBytes(path);
909
+ if (expected === undefined ? current !== undefined : current === undefined || !expected.equals(current)) {
910
+ throw new Error("desktop_profile_changed");
911
+ }
912
+ }
913
+
914
+ /** Publish the verified bytes without reopening the source or decoding its backup contents. */
915
+ function writeDesktopConfigBytes(path: string, bytes: Buffer, validateBeforeRename?: () => void): void {
916
+ atomicWriteFileStreamed(path, descriptor => { writeFileSync(descriptor, bytes); }, validateBeforeRename ? { validateBeforeRename } : {});
875
917
  }
@@ -25,7 +25,7 @@ import type { OcxConfig } from "../types";
25
25
  import { claudeConfigDir } from "./auth-detect";
26
26
  import { inspectDesktop3pConfigLibrary } from "./desktop-3p";
27
27
  import { claudeInterceptCaCertPath, ensureLocalInterceptCa } from "./intercept/local-ca";
28
- import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/runtime";
28
+ import { claudeInterceptEnabled, claudeInterceptProxyPort } from "./intercept/port-policy";
29
29
  import { ensureClaudeInterceptProxyToken, readClaudeInterceptProxyToken } from "./intercept/proxy-auth";
30
30
  import {
31
31
  applyClaudeInterceptSettings,
@@ -0,0 +1,4 @@
1
+ /** Platforms with a Desktop egress profile and an OS trust adapter for the picker. */
2
+ export function supportsDesktopPicker(platform: NodeJS.Platform = process.platform): boolean {
3
+ return platform === "darwin" || platform === "win32";
4
+ }
@@ -23,6 +23,7 @@ import {
23
23
  type DesktopPickerProfileOptions,
24
24
  } from "./desktop-picker-profile";
25
25
  import { resolveClaudeDesktopMode, observeClaudeDesktopMode } from "./desktop-first-party";
26
+ import { supportsDesktopPicker } from "./desktop-picker-platform";
26
27
 
27
28
  /** Desktop no longer selects the picker profile, so removing the CA's trust cannot strand it. */
28
29
  function profileReleased(profile: DesktopPickerProfileInspection): boolean {
@@ -77,6 +78,8 @@ export interface DesktopPickerControllerDeps {
77
78
  persistPreference: (value: boolean) => boolean;
78
79
  /** Bound picker CONNECT proxy port (Desktop's egress), or null when it is not running. */
79
80
  proxyPort: () => number | null;
81
+ /** Bind a dormant relay on explicit enable, under the controller's mutation lock. */
82
+ ensureProxy?: () => Promise<void>;
80
83
  configDir: string;
81
84
  persistentAuthority?: { store?: PickerCaStore };
82
85
  security?: SecurityRunner;
@@ -91,7 +94,7 @@ function profileOptions(deps: DesktopPickerControllerDeps): DesktopPickerProfile
91
94
  }
92
95
 
93
96
  function statusReasonForConfig(config: OcxConfig, platform: NodeJS.Platform, proxyBound: boolean): DesktopPickerReason {
94
- if (platform !== "darwin") return "unsupported_platform";
97
+ if (!supportsDesktopPicker(platform)) return "unsupported_platform";
95
98
  if (resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") return "not_first_party";
96
99
  if (!claudeDesktopIntegrationEnabled(config)) return "integration_off";
97
100
  if (config.claudeCode?.intercept?.picker === false) return "disabled";
@@ -101,11 +104,11 @@ function statusReasonForConfig(config: OcxConfig, platform: NodeJS.Platform, pro
101
104
 
102
105
  function emptyStatus(config: OcxConfig, platform: NodeJS.Platform, reason: DesktopPickerReason): DesktopPickerStatus {
103
106
  return {
104
- desired: platform === "darwin"
107
+ desired: supportsDesktopPicker(platform)
105
108
  && claudeDesktopIntegrationEnabled(config)
106
109
  && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) === "first-party"
107
110
  && config.claudeCode?.intercept?.picker !== false,
108
- supported: platform === "darwin",
111
+ supported: supportsDesktopPicker(platform),
109
112
  trust: "unknown",
110
113
  profile: "absent",
111
114
  listenerReady: false,
@@ -142,7 +145,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
142
145
  const trust = trustOverride ?? runtime.trust;
143
146
  const effective = runtime.effective && profileCurrent && trust === "trusted";
144
147
  let reason: DesktopPickerReason;
145
- if (platform !== "darwin") reason = "unsupported_platform";
148
+ if (!supportsDesktopPicker(platform)) reason = "unsupported_platform";
146
149
  else if (!runtime.desired) reason = statusReasonForConfig(deps.readConfig(), platform, deps.proxyPort() !== null);
147
150
  else if (deps.proxyPort() === null) reason = "proxy_unavailable";
148
151
  else if (trust !== "trusted") reason = "trust_pending";
@@ -178,7 +181,7 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
178
181
 
179
182
  async function untrustCurrentCa(): Promise<boolean> {
180
183
  const caPath = pickerCaCertPath(deps.configDir);
181
- if (platform !== "darwin" || !existsSync(caPath)) return true;
184
+ if (!supportsDesktopPicker(platform) || !existsSync(caPath)) return true;
182
185
  try {
183
186
  const sha1 = pickerCaFingerprints(readFileSync(caPath, "utf8")).sha1;
184
187
  return (await untrustPickerCa(caPath, sha1, deps.security, platform)).ok;
@@ -207,11 +210,11 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
207
210
  return withTrustFailure({ ...runtimeStatus(observedTrust), reason }, trustFailed);
208
211
  };
209
212
  const check = (config: OcxConfig, includePreference: boolean): DesktopPickerReason | null => {
210
- if (platform !== "darwin") return "unsupported_platform";
213
+ if (!supportsDesktopPicker(platform)) return "unsupported_platform";
211
214
  if (resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") return "mode_not_committed";
212
215
  if (!claudeDesktopIntegrationEnabled(config)) return "integration_off";
213
216
  if (includePreference && config.claudeCode?.intercept?.picker === false) return "disabled";
214
- if (deps.proxyPort() === null) return "proxy_unavailable";
217
+ if (deps.proxyPort() === null && !deps.ensureProxy) return "proxy_unavailable";
215
218
  return null;
216
219
  };
217
220
 
@@ -224,6 +227,10 @@ export function createDesktopPickerController(deps: DesktopPickerControllerDeps)
224
227
  }
225
228
  const second = check(deps.readConfig(), true);
226
229
  if (second) return refuse(second);
230
+ if (deps.proxyPort() === null) {
231
+ try { await deps.ensureProxy?.(); } catch { return refuse("proxy_unavailable"); }
232
+ }
233
+ if (deps.proxyPort() === null) return refuse("proxy_unavailable");
227
234
 
228
235
  let caPath: string;
229
236
  let caSha1: string;
@@ -336,7 +343,7 @@ export async function removeDesktopPickerArtifacts(options: { configDir?: string
336
343
  return { ok: false, residual };
337
344
  }
338
345
  const caPath = pickerCaCertPath(configDir);
339
- if (platform === "darwin" && existsSync(caPath)) {
346
+ if (supportsDesktopPicker(platform) && existsSync(caPath)) {
340
347
  try {
341
348
  const sha1 = pickerCaFingerprints(readFileSync(caPath, "utf8")).sha1;
342
349
  if (!(await untrustPickerCa(caPath, sha1, options.security, platform)).ok) residual.push("trust");
@@ -1,4 +1,6 @@
1
1
  import type { OcxConfig } from "../types";
2
+ import { connect } from "node:net";
3
+ import { directLocalHttpFetch } from "../server/direct-local-http";
2
4
  import {
3
5
  readWindowsProxyBypassRegistry,
4
6
  readWindowsSystemProxy,
@@ -8,6 +10,7 @@ import {
8
10
  } from "../lib/windows-system-proxy";
9
11
  import { inspectDesktopFirstParty, observeClaudeDesktopMode } from "./desktop-first-party";
10
12
  import { desktopFirstPartyDesired } from "./first-party-settings";
13
+ import { inspectDesktopPickerProfile, type DesktopPickerProfileInspection } from "./desktop-picker-profile";
11
14
 
12
15
  /**
13
16
  * Whether the Windows system proxy silently bypasses Desktop first-party mode.
@@ -17,7 +20,8 @@ import { desktopFirstPartyDesired } from "./first-party-settings";
17
20
  * as `HTTPS_PROXY`/`HTTP_PROXY`. That inherited value takes precedence over the `env` block
18
21
  * OpenCodex writes into the user's `~/.claude/settings.json` (only Claude Code managed settings
19
22
  * override it), so Code-tab traffic goes to the system proxy and never reaches the intercept.
20
- * Routed subagent models then fail, and nothing else reports why. A system proxy such as Clash or
23
+ * A selected picker profile with an available fixed egress relay takes precedence instead.
24
+ * Otherwise routed subagent models fail. A system proxy such as Clash or
21
25
  * v2rayN, with no bypass for the API host, is the common case.
22
26
  *
23
27
  * Observe-only: this reads the registry and never changes it. A PAC script and WPAD automatic
@@ -123,6 +127,39 @@ export interface DesktopSystemProxyDeps {
123
127
  firstPartyState?: () => DesktopFirstPartySettingsState;
124
128
  readSystemProxy?: () => WindowsSystemProxyResult;
125
129
  readBypass?: () => WindowsProxyBypassValues | null;
130
+ inspectPickerProfile?: () => DesktopPickerProfileInspection;
131
+ pickerRelayAvailable?: (proxyUrl: string) => boolean | Promise<boolean>;
132
+ }
133
+
134
+ /** Observe the selected endpoint even in a separate doctor process or a blind-only recovery relay. */
135
+ async function livePickerRelayAvailable(proxyUrl: string): Promise<boolean> {
136
+ const port = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/.exec(proxyUrl)?.[1];
137
+ if (!port || Number(port) > 65535) return false;
138
+ try {
139
+ // A listening TCP socket alone is not evidence of an OpenCodex CONNECT relay.
140
+ const response = await directLocalHttpFetch(proxyUrl, { signal: AbortSignal.timeout(1_000) });
141
+ if (response.status !== 405 || response.headers.get("allow") !== "CONNECT"
142
+ || !(await response.text()).startsWith("OpenCodex Claude intercept proxy accepts only CONNECT tunnels.")) return false;
143
+ // The main intercept proxy has the same HTTP response but requires credentials the Desktop
144
+ // profile cannot send. A loopback CONNECT is denied before dialing or choosing any tunnel;
145
+ // 403 proves unauthenticated admission, whereas the credential-bearing proxy returns 407.
146
+ return await new Promise<boolean>(resolve => {
147
+ const socket = connect({ host: "127.0.0.1", port: Number(port) });
148
+ const finish = (available: boolean) => { clearTimeout(timer); socket.destroy(); resolve(available); };
149
+ const timer = setTimeout(() => finish(false), 1_000);
150
+ let head = "";
151
+ socket.once("error", () => finish(false));
152
+ socket.once("close", () => finish(false));
153
+ socket.once("connect", () => socket.write("CONNECT 127.0.0.1:1 HTTP/1.1\r\nHost: 127.0.0.1:1\r\n\r\n"));
154
+ socket.on("data", bytes => {
155
+ head += bytes.toString("latin1");
156
+ if (head.includes("\r\n\r\n")) finish(/^HTTP\/1\.[01] 403 Forbidden\r\n/.test(head));
157
+ else if (head.length > 8 * 1024) finish(false);
158
+ });
159
+ });
160
+ } catch { // no-excuse-ok: catch -- failed relay probes retain the registry diagnostic.
161
+ return false;
162
+ }
126
163
  }
127
164
 
128
165
  function liveFirstPartyState(
@@ -137,15 +174,24 @@ function liveFirstPartyState(
137
174
  }
138
175
  }
139
176
 
140
- /** Reads the live state. Registry reads run only on Windows with a Desktop first-party env. */
141
- export function collectDesktopSystemProxy(
177
+ /** Registry reads run only on Windows with a Desktop first-party env and no verified picker relay. */
178
+ export async function collectDesktopSystemProxy(
142
179
  config: Pick<OcxConfig, "claudeCode" | "clientIntegrations" | "port" | "runtimeRole">,
143
180
  deps: DesktopSystemProxyDeps = {},
144
- ): DesktopSystemProxyAssessment {
181
+ ): Promise<DesktopSystemProxyAssessment> {
145
182
  const platform = deps.platform ?? process.platform;
146
183
  if (platform !== "win32") return { kind: "not-applicable" };
147
184
  const firstParty = (deps.firstPartyState ?? (() => liveFirstPartyState(config)))();
148
185
  if (firstParty === "off") return { kind: "not-applicable" };
186
+ if (firstParty === "applied" && config.claudeCode?.intercept?.picker !== false) {
187
+ try {
188
+ const profile = (deps.inspectPickerProfile ?? (() => inspectDesktopPickerProfile({ platform })))();
189
+ if (profile.kind === "applied" && await (deps.pickerRelayAvailable ?? livePickerRelayAvailable)(profile.proxyUrl)) {
190
+ return { kind: "not-applicable" };
191
+ }
192
+ } catch { // no-excuse-ok: catch -- missing profile/relay evidence must not hide a registry conflict.
193
+ }
194
+ }
149
195
  return assessDesktopSystemProxy({
150
196
  platform,
151
197
  firstParty,