@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -69,11 +69,6 @@ import { runDevinProviderMergeStartupMigration } from "../providers/devin-provid
69
69
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
70
70
  import { providerCodexAccountMode } from "../providers/registry";
71
71
  import type { StorageCleanupPolicy } from "../types";
72
- import {
73
- MAX_CONFIGURABLE_INBOUND_BODY_BYTES,
74
- MIN_CONFIGURABLE_INBOUND_BODY_BYTES,
75
- resolveInboundBodyLimitBytes,
76
- } from "./request-decompress";
77
72
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
78
73
  export {
79
74
  clearThreadAccountMap,
@@ -116,7 +111,7 @@ import {
116
111
  type RequestLogEntry,
117
112
  } from "./request-log";
118
113
  import { sessionLaneIdFromRequest } from "./request-log-conversation";
119
- import { configureSharedSpendLedger, spendPolicyFromConfig } from "../lib/spend-reservation-ledger";
114
+ import { setUsageLedgerRetention } from "./usage-ledger-retention";
120
115
  import { admitHttpWorkflowTurn, workflowDecisionRefusalResponse, type WorkflowRefusalLog } from "./workflow-refusal";
121
116
  export {
122
117
  addFinalRequestLog,
@@ -204,9 +199,18 @@ import {
204
199
  } from "../lib/package-tree-integrity";
205
200
  import { detectInstall } from "../update/index";
206
201
  import { createServeOptions, type ServerIngress } from "./index/serve-options";
207
- import { inspectStartupOwnership, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
202
+ import { createClaudeInterceptLifecycle } from "./index/claude-intercept-lifecycle";
203
+ import { inspectStartupOwnership, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
204
+ import { acquireSpendLedgerServerLifecycle, recordFailedStartRollback, type SpendLedgerServerLifecycle } from "./index/spend-ledger-lifecycle";
205
+ export { waitForFailedStartRollback } from "./index/spend-ledger-lifecycle";
208
206
 
209
207
  export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
208
+ const spendLedgerLifecycle = acquireSpendLedgerServerLifecycle(getConfigDir());
209
+ try { return startServerWithSpendLedgerOwner(port, deps, spendLedgerLifecycle); }
210
+ catch (error) { recordFailedStartRollback(error, spendLedgerLifecycle.releaseAfterFailedStart()); throw error; }
211
+ }
212
+
213
+ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartServerDeps, spendLedgerLifecycle: SpendLedgerServerLifecycle): Server<WsData> {
210
214
  const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
211
215
  // Captured before loadConfig() starts the optional ACL flight so stop() drains the same dir
212
216
  // even if OPENCODEX_HOME changes underneath a long-lived process.
@@ -299,12 +303,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
299
303
  registerAppOwnedMemorySweepFallback();
300
304
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
301
305
  enforceAppOwnedMemoryBudget();
302
- // Operator token ceilings (#4546). Applied before the listener binds: the shared ledger is
303
- // built on the first reservation, and a request arriving before this ran would build it with
304
- // no policy and enforce nothing. An absent `spend` section resolves to the unconfigured
305
- // default, which refuses nothing and opens no journal -- so on an install that never wrote
306
- // the key this line changes no behaviour at all.
307
- configureSharedSpendLedger(spendPolicyFromConfig(config.spend));
306
+ // Observe-only mode still journals physical sends, so every server owns before configuring.
307
+ spendLedgerLifecycle.configure(config.spend);
308
+ // After ownership: a second server on the same home is refused above, so the process running
309
+ // this line is the only one appending to usage.jsonl and the only one that may compact it.
310
+ setUsageLedgerRetention(config.usageLedgerMaxBytes);
308
311
  registerCodexCooldownRecoveryProbeWorker(config);
309
312
  // Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
310
313
  // tick for daily/weekly; startup evaluation is fire-and-forget after listen.
@@ -625,26 +628,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
625
628
  let server: Server<WsData>;
626
629
  let loopbackServer: Server<WsData> | null = null;
627
630
  let managementIngressServer: Server<WsData> | null = null;
628
-
629
- // Resolved once, before any listener binds. The clamp is silent inside the resolver so it
630
- // stays pure and per-request cheap; the operator is told here instead, once, because a
631
- // config value that was quietly reduced is exactly the thing they would otherwise debug
632
- // against the wrong limit.
633
- const inboundBodyLimitBytes = resolveInboundBodyLimitBytes(config.maxInboundBodyBytes);
634
- const requestedInboundBodyLimit = config.maxInboundBodyBytes;
635
- if (requestedInboundBodyLimit !== undefined
636
- && requestedInboundBodyLimit > 0
637
- && requestedInboundBodyLimit !== inboundBodyLimitBytes) {
638
- console.warn(
639
- `[server] maxInboundBodyBytes=${requestedInboundBodyLimit} is outside the supported range `
640
- + `[${MIN_CONFIGURABLE_INBOUND_BODY_BYTES}, ${MAX_CONFIGURABLE_INBOUND_BODY_BYTES}]; `
641
- + `using ${inboundBodyLimitBytes} bytes.`,
642
- );
643
- }
631
+ const claudeIntercept = createClaudeInterceptLifecycle<WsData>();
632
+ const inboundBodyLimitBytes = resolveInboundBodyLimitWithWarning(config);
644
633
 
645
634
  function ingressForServer(requestServer: Server<WsData>): ServerIngress {
646
635
  if (requestServer === loopbackServer) return "unauthenticated-loopback";
647
636
  if (requestServer === managementIngressServer) return "hub-management";
637
+ if (claudeIntercept.ownsListener(requestServer)) return "claude-intercept";
648
638
  return "public";
649
639
  }
650
640
  let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
@@ -703,7 +693,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
703
693
  get remoteWorkspaceStopping() { return remoteWorkspaceStopping; },
704
694
  });
705
695
 
706
- server = Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost });
696
+ server = spendLedgerLifecycle.track(Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost }));
707
697
 
708
698
  // Both binds are one startup transaction (#1102). If the loopback bind fails after the
709
699
  // public one succeeded, leaving the public listener up would strand it: the CLI's port
@@ -711,11 +701,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
711
701
  // accumulating listeners. Roll back and rethrow the original error instead.
712
702
  if (loopbackListenerPort !== null) {
713
703
  try {
714
- loopbackServer = Bun.serve<WsData>({
704
+ loopbackServer = spendLedgerLifecycle.track(Bun.serve<WsData>({
715
705
  ...serveOptions,
716
706
  port: loopbackListenerPort,
717
707
  hostname: "127.0.0.1",
718
- });
708
+ }));
719
709
  } catch (error) {
720
710
  try {
721
711
  // startServer is synchronous, so this rollback cannot await. Bun begins closing the
@@ -731,11 +721,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
731
721
  }
732
722
  if (managementIngressPort !== null) {
733
723
  try {
734
- managementIngressServer = Bun.serve<WsData>({
724
+ managementIngressServer = spendLedgerLifecycle.track(Bun.serve<WsData>({
735
725
  ...serveOptions,
736
726
  port: managementIngressPort,
737
727
  hostname: "127.0.0.1",
738
- });
728
+ }));
739
729
  } catch (error) {
740
730
  // Preserve the management bind failure while synchronously initiating rollback of every
741
731
  // listener already opened in this startup transaction. startServer must not become async.
@@ -746,6 +736,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
746
736
  throw new AuxiliaryListenerBindError("hub.managementIngress", managementIngressPort, "127.0.0.1", error);
747
737
  }
748
738
  }
739
+ claudeIntercept.start({
740
+ config, publicPort: server.port ?? listenPort, requestedPort: listenPort, maxRequestBodySize: inboundBodyLimitBytes,
741
+ dispatch: (req, requestServer) => serveOptions.fetch(req, requestServer),
742
+ });
749
743
  } catch (error) {
750
744
  unregisterQuotaAutoRefresh?.();
751
745
  userCostOverlayReconciler?.stop();
@@ -774,6 +768,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
774
768
  ...(managementIngressRef
775
769
  ? [() => managementIngressRef.stop(closeActiveConnections)]
776
770
  : []),
771
+ () => claudeIntercept.stop(),
777
772
  async () => { await remoteWorkspaceShutdown?.(); },
778
773
  async () => {
779
774
  try {
@@ -783,16 +778,17 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
783
778
  }
784
779
  },
785
780
  ],
786
- async () => {
781
+ async listenersStopped => {
787
782
  try {
788
783
  await backgroundLifecycle.release();
789
784
  await releaseNativeMainStartupLifecycle(server);
790
785
  } finally {
791
786
  // icacls.exe from hardenConfigDir() holds the config dir open; a caller that
792
787
  // removes the dir right after stop() settles would hit EPERM/EBUSY on Windows
793
- // otherwise. Runs even when an earlier release rejected — that rejection still
794
- // propagates, but not before the child is drained.
795
- await flushConfigDirHardening(startupConfigDir);
788
+ // otherwise. Config hardening still flushes when an earlier release rejects. The
789
+ // spend owner is retained when a listener stop failed because the socket may live.
790
+ try { if (listenersStopped) spendLedgerLifecycle.release(); }
791
+ finally { await flushConfigDirHardening(startupConfigDir); }
796
792
  }
797
793
  },
798
794
  );
@@ -357,12 +357,12 @@ export function getServerListenPort(): number | undefined {
357
357
  * caller sees the same result before a replacement binds the port. Swallowing it would let
358
358
  * `drainAndShutdown` report success while a socket is still held.
359
359
  *
360
- * `always` runs after the listeners regardless of their outcome, and its own failure joins the
361
- * reported set rather than replacing it.
360
+ * `always` runs after the listeners regardless of their outcome and receives whether every
361
+ * listener stop succeeded. Its own failure joins the reported set rather than replacing it.
362
362
  */
363
363
  export async function runListenerShutdown(
364
364
  steps: Array<() => Promise<void>>,
365
- always: () => Promise<void>,
365
+ always: (listenersStopped: boolean) => Promise<void>,
366
366
  ): Promise<void> {
367
367
  const failures: unknown[] = [];
368
368
  // Close admission and start connection-owner cleanup before waiting for any drain.
@@ -372,7 +372,7 @@ export async function runListenerShutdown(
372
372
  if (result.status === "rejected") failures.push(result.reason);
373
373
  }
374
374
  try {
375
- await always();
375
+ await always(failures.length === 0);
376
376
  } catch (error) {
377
377
  failures.push(error);
378
378
  }
@@ -9,6 +9,12 @@ export interface LiveCallBinding {
9
9
  upstreamCallId: string;
10
10
  joinStyle: "frameless-path" | "realtime-query";
11
11
  providerName: string;
12
+ /**
13
+ * The model this call settled on when it was created. A join names no model
14
+ * of its own, so this is what a per-key scope is judged against on reconnect
15
+ * — without it, a call created for one model could be rejoined as another.
16
+ */
17
+ model?: string;
12
18
  accountId?: string;
13
19
  chatgptAccountId?: string;
14
20
  keyedCredentialDigest?: string;
@@ -44,7 +44,8 @@ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
44
44
  import { sidecarEnter } from "../lib/sidecar-tracker";
45
45
  import type { OcxConfig } from "../types";
46
46
  import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
47
- import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
47
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "./auth-cors";
48
+ import { admissionScopeDenial } from "./admission-model-scope";
48
49
  import type { RequestLogContext } from "./request-log";
49
50
  import { codexLogAccountId } from "./responses";
50
51
  import type { AdmissionLease } from "../lib/admission";
@@ -550,6 +551,62 @@ async function readRequestBodyCapped(req: Request, maxBytes: number): Promise<Ar
550
551
  }
551
552
  }
552
553
 
554
+ /**
555
+ * Who is asking and for which live model, as far as a per-key scope is concerned.
556
+ *
557
+ * This path has no router to resolve a destination, so the model is read where
558
+ * the client states it — the call-create session, or a standalone socket's own
559
+ * query — and the provider is whichever OpenAI upstream this relay settles on.
560
+ * `model` is undefined when nobody stated one: a call-create that sends no
561
+ * session model, or a join onto a call this compatibility path never recorded.
562
+ */
563
+ export interface LiveScopeDestination {
564
+ admission?: DataPlaneAdmission;
565
+ model: string | undefined;
566
+ }
567
+
568
+ /**
569
+ * The live model a call-create body names, or undefined when it names none.
570
+ *
571
+ * Both inbound shapes carry it at `session.model` — JSON directly, multipart in
572
+ * the `session` field — and this path relays the body upstream unchanged, so
573
+ * the string is the destination rather than a selector the proxy rewrites. A
574
+ * body that states nothing readable leaves the model to the upstream, which is
575
+ * a destination no model list can describe.
576
+ */
577
+ export async function liveCallCreateModel(body: ArrayBuffer, contentType: string): Promise<string | undefined> {
578
+ try {
579
+ let session: unknown;
580
+ if (contentType.toLowerCase().includes("multipart/form-data")) {
581
+ const form = await new Response(body, { headers: { "content-type": contentType } }).formData();
582
+ const raw = form.get("session");
583
+ session = typeof raw === "string" ? JSON.parse(raw) : undefined;
584
+ } else {
585
+ session = (JSON.parse(new TextDecoder().decode(body)) as { session?: unknown } | null)?.session;
586
+ }
587
+ const model = (session as { model?: unknown } | null | undefined)?.model;
588
+ return typeof model === "string" && model.trim() ? model.trim() : undefined;
589
+ } catch {
590
+ return undefined;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * The model a sideband upgrade is for, or undefined when the request names none.
596
+ *
597
+ * A standalone session states it in the query it forwards. A join names nothing
598
+ * of its own: the call it attaches to chose a model at create time, and only a
599
+ * recorded binding can say which. This compatibility path keeps no such record,
600
+ * so a native join is undefined here and the external path in `audio-live.ts`
601
+ * supplies the model its binding stored.
602
+ */
603
+ export function liveSidebandModel(target: LiveSidebandTarget): string | undefined {
604
+ if (target.style === "realtime-standalone" || target.style === "frameless-standalone") {
605
+ return new URLSearchParams(target.query).get("model")?.trim() || undefined;
606
+ }
607
+ return undefined;
608
+ }
609
+
553
610
  /**
554
611
  * Resolve OpenAI/ChatGPT auth + headers for live HTTP or sideband WebSocket relays.
555
612
  * Shared by call-create and sideband so pool token override stays consistent.
@@ -559,6 +616,7 @@ export async function resolveLiveRelay(
559
616
  config: OcxConfig,
560
617
  logCtx: RequestLogContext,
561
618
  turnAdmissionLease?: AdmissionLease,
619
+ destination?: LiveScopeDestination,
562
620
  ): Promise<LiveRelayTarget | Response> {
563
621
  try {
564
622
  validateForwardAdmissionCredential(req.headers, config);
@@ -624,8 +682,22 @@ export async function resolveLiveRelay(
624
682
 
625
683
  // Client protocol headers first so provider/auth headers below always win on conflict.
626
684
  const headers: Record<string, string> = clientProtocolHeaders(req.headers);
685
+ const scopedModel = destination?.model;
627
686
  if (forward) {
628
687
  const { provider } = forward;
688
+ // The upstream is settled here and voice bills it for whatever model this
689
+ // request carries. Refuse before any of it is sent, and give back the probe
690
+ // lease the resolution took. A join states no model and this path keeps no
691
+ // record of the call it attaches to, so a key with a model list is refused
692
+ // there rather than admitted against an assumed default.
693
+ const denial = admissionScopeDenial(config, destination?.admission, scopedModel, {
694
+ providerName: forward.providerName,
695
+ modelId: scopedModel,
696
+ });
697
+ if (denial) {
698
+ forward.releaseProbeLease?.();
699
+ return denial;
700
+ }
629
701
  if (provider.headers) Object.assign(headers, provider.headers);
630
702
  for (const [name, value] of forward.headers) headers[name] = value;
631
703
  logCtx.model = "gpt-live";
@@ -640,6 +712,11 @@ export async function resolveLiveRelay(
640
712
  if (forwardAuthError) return forwardAuthError;
641
713
  if (candidates.keyed) {
642
714
  const { provider, apiKey, providerName } = candidates.keyed;
715
+ const denial = admissionScopeDenial(config, destination?.admission, scopedModel, {
716
+ providerName,
717
+ modelId: scopedModel,
718
+ });
719
+ if (denial) return denial;
643
720
  if (provider.headers) Object.assign(headers, provider.headers);
644
721
  headers.authorization = `Bearer ${apiKey}`;
645
722
  logCtx.provider = providerName;
@@ -663,13 +740,17 @@ export async function handleLive(
663
740
  config: OcxConfig,
664
741
  logCtx: RequestLogContext,
665
742
  turnAdmissionLease?: AdmissionLease,
743
+ admission?: DataPlaneAdmission,
666
744
  ): Promise<Response> {
667
745
  const inboundContentType = req.headers.get("content-type") ?? "application/octet-stream";
668
746
  const inboundBodyOrError = await readRequestBodyCapped(req, LIVE_REQUEST_MAX_BYTES);
669
747
  if (inboundBodyOrError instanceof Response) return inboundBodyOrError;
670
748
  const inboundBody = inboundBodyOrError;
671
749
 
672
- const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease);
750
+ const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease, {
751
+ admission,
752
+ model: await liveCallCreateModel(inboundBody, inboundContentType),
753
+ });
673
754
  if (relay instanceof Response) return relay;
674
755
 
675
756
  const headers: Record<string, string> = { ...relay.headers };
@@ -766,8 +847,12 @@ export async function resolveLiveSidebandUpgrade(
766
847
  logCtx: RequestLogContext,
767
848
  target: LiveSidebandTarget,
768
849
  turnAdmissionLease?: AdmissionLease,
850
+ admission?: DataPlaneAdmission,
769
851
  ): Promise<{ headers: Record<string, string>; upstreamWsUrl: string; recordOutcome?: LiveRelayTarget["recordOutcome"] } | Response> {
770
- const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease);
852
+ const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease, {
853
+ admission,
854
+ model: liveSidebandModel(target),
855
+ });
771
856
  if (relay instanceof Response) return relay;
772
857
  return {
773
858
  headers: relay.headers,
@@ -1,3 +1,4 @@
1
+ import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { readFileSync } from "node:fs";
3
4
  import type { CatalogModel } from "../../codex/catalog";
@@ -131,6 +132,17 @@ function persistDesktopProfileField(
131
132
  return { ok: true };
132
133
  }
133
134
 
135
+ async function persistDesktopModeField(
136
+ config: OcxConfig,
137
+ desktopMode: "first-party" | "gateway",
138
+ ): Promise<{ ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" }> {
139
+ const { recordClaudeDesktopMode } = await import("../../claude/desktop-first-party");
140
+ const outcome = mutatePersistedConfig(persisted => recordClaudeDesktopMode(persisted, desktopMode));
141
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
142
+ recordClaudeDesktopMode(config, desktopMode);
143
+ return { ok: true };
144
+ }
145
+
134
146
  export function grokApplyFlightSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
135
147
  return {
136
148
  currentBytes: grokApplyFlight?.bytes ?? 0,
@@ -1002,7 +1014,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1002
1014
  if (url.pathname === "/api/claude-desktop/apply" && req.method === "POST") {
1003
1015
  try {
1004
1016
  // #859: the CLI delegates here so the registry is built in the serving
1005
- // process. Accept an optional mode; default stays static for back-compat.
1017
+ // process. `mode` selects the Desktop mode: `first-party` (default when the
1018
+ // install has no gateway profile) or `gateway`; the legacy gateway shapes
1019
+ // (static/hybrid/discovery) still select gateway with that library layout.
1006
1020
  let mode: "static" | "hybrid" | "discovery" = "static";
1007
1021
  let parsed: unknown;
1008
1022
  try {
@@ -1011,12 +1025,17 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1011
1025
  rethrowManagementBodyTooLarge(error);
1012
1026
  return jsonResponse({ error: "invalid JSON body" }, 400);
1013
1027
  }
1028
+ const { resolveClaudeDesktopApplyMode, applyDesktopFirstParty, captureDesktopFirstPartyRollback, removeDesktopFirstParty } = await import("../../claude/desktop-first-party");
1014
1029
  const requested = (parsed as { mode?: unknown } | null)?.mode;
1030
+ let desktopMode: "first-party" | "gateway" = resolveClaudeDesktopApplyMode(config);
1015
1031
  if (requested !== undefined) {
1016
1032
  if (requested === "static" || requested === "hybrid" || requested === "discovery") {
1017
1033
  mode = requested;
1034
+ desktopMode = "gateway";
1035
+ } else if (requested === "gateway" || requested === "first-party") {
1036
+ desktopMode = requested;
1018
1037
  } else {
1019
- return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
1038
+ return jsonResponse({ error: "mode must be first-party, gateway, static, hybrid, or discovery" }, 400);
1020
1039
  }
1021
1040
  }
1022
1041
  // #859: a delegated CLI apply carries the profile it just saved — the
@@ -1032,6 +1051,60 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1032
1051
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
1033
1052
  }
1034
1053
  }
1054
+ if (desktopMode === "first-party") {
1055
+ const { setIntegrationEnabled } = await import("../../codex/desired-state");
1056
+ const desired = setIntegrationEnabled("claude-desktop", true);
1057
+ if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
1058
+ mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
1059
+ const { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot } = await import("../../claude/desktop-3p");
1060
+ const rollback = captureDesktopFirstPartyRollback(config);
1061
+ const applied = applyDesktopFirstParty(config);
1062
+ if (!applied.ok) {
1063
+ const { firstPartyRefusalMessage } = await import("./native-integration-routes");
1064
+ return jsonResponse({
1065
+ error: firstPartyRefusalMessage(applied.reason, applied.path),
1066
+ code: "claude_desktop_first_party_refused",
1067
+ reason: applied.reason,
1068
+ path: applied.path,
1069
+ }, applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? 409 : 500);
1070
+ }
1071
+ // Remove the gateway only after the replacement env was written.
1072
+ const appliedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
1073
+ const library = inspectDesktop3pConfigLibrary({ appliedFingerprint });
1074
+ let gatewayRemoved = false;
1075
+ if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
1076
+ const removed = (deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint, replaceWhileEnabled: true });
1077
+ if (!removed.ok) {
1078
+ const restored = removed.changed || !applied.changed || rollback();
1079
+ const modeSaved = !removed.changed || (await persistDesktopModeField(config, "first-party")).ok;
1080
+ const warning = [restored ? "" : "first-party settings rollback did not complete",
1081
+ modeSaved ? "" : "first-party is active but its mode marker was not saved"].filter(Boolean).join("; ");
1082
+ return jsonResponse({
1083
+ error: removed.kind === "cleanup_incomplete"
1084
+ ? "Claude Desktop now points at standard mode, but gateway credential cleanup is incomplete; the first-party connection remains active."
1085
+ : "The gateway profile could not be removed safely, so first-party mode was not applied.",
1086
+ code: "claude_desktop_gateway_removal_failed",
1087
+ ...(warning ? { warning } : {}),
1088
+ reason: removed.reason ?? removed.kind,
1089
+ ...(removed.residualPaths ? { residualPaths: removed.residualPaths } : {}),
1090
+ }, removed.kind === "cleanup_incomplete" ? 500 : 409);
1091
+ }
1092
+ gatewayRemoved = removed.changed;
1093
+ }
1094
+ const modeSaved = await persistDesktopModeField(config, "first-party");
1095
+ return jsonResponse({
1096
+ ok: true,
1097
+ mode: "first-party",
1098
+ saved: modeSaved.ok,
1099
+ applied: true,
1100
+ changed: applied.changed || gatewayRemoved,
1101
+ gatewayRemoved,
1102
+ path: applied.path,
1103
+ proxyPort: applied.proxyPort,
1104
+ caCertPath: applied.env.NODE_EXTRA_CA_CERTS,
1105
+ ...(modeSaved.ok ? {} : { warning: `First-party env was applied, but the mode marker was not saved (${modeSaved.reason}).` }),
1106
+ });
1107
+ }
1035
1108
  const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
1036
1109
  const desired = setIntegrationEnabled("claude-desktop", true);
1037
1110
  if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
@@ -1083,42 +1156,31 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1083
1156
  nativeContextLimits(latest),
1084
1157
  );
1085
1158
  if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
1159
+ const committed = persistCommittedDesktopGateway(config, state.profile, result.fingerprint);
1160
+ const modeWarning = committed.ok ? undefined : `Gateway applied, but its mode/profile state was not saved (${committed.reason}).`;
1161
+ // The durable marker describes the committed gateway even if old-mode cleanup fails.
1162
+ const firstPartyRemoved = removeDesktopFirstParty();
1163
+ if (!firstPartyRemoved.ok) {
1164
+ return jsonResponse({
1165
+ error: `Claude Code settings could not be parsed (${firstPartyRemoved.path}); first-party cleanup remains incomplete after gateway apply.`,
1166
+ code: "claude_desktop_first_party_refused", reason: firstPartyRemoved.reason,
1167
+ applied: true, saved: committed.ok, mode: "gateway", path: result.path,
1168
+ ...(modeWarning ? { warning: modeWarning } : {}),
1169
+ }, 500);
1170
+ }
1086
1171
  const { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
1087
- const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
1088
- platform: deps.platform ?? process.platform,
1089
- });
1172
+ const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({ platform: deps.platform ?? process.platform });
1090
1173
  const policyWarning = claudeDesktopPolicyWarning(policyState);
1091
- // Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
1092
- if (result.fingerprint) {
1093
- // The Desktop write already landed, so a failed bookkeeping save is not
1094
- // an apply failure: report the miss instead of claiming a clean apply.
1095
- const marked = persistDesktopProfileField(config, {
1096
- ...state.profile,
1097
- appliedFingerprint: result.fingerprint,
1098
- appliedAt: new Date().toISOString(),
1099
- });
1100
- if (!marked.ok) {
1101
- return jsonResponse({
1102
- ok: true,
1103
- applied: true,
1104
- saved: false,
1105
- path: result.path,
1106
- fingerprint: result.fingerprint,
1107
- warning: [
1108
- `Claude Desktop was applied, but the applied marker was not saved (${marked.reason}).`,
1109
- policyWarning,
1110
- ].filter(Boolean).join(" "),
1111
- });
1112
- }
1113
- }
1174
+ const warning = [modeWarning, policyWarning].filter(Boolean).join(" ");
1114
1175
  return jsonResponse({
1115
1176
  ok: true,
1116
- saved: true,
1177
+ mode: "gateway",
1178
+ saved: committed.ok,
1117
1179
  applied: true,
1118
1180
  path: result.path,
1119
1181
  fingerprint: result.fingerprint,
1120
1182
  policyState,
1121
- ...(policyWarning ? { warning: policyWarning } : {}),
1183
+ ...(warning ? { warning } : {}),
1122
1184
  });
1123
1185
  } catch (error) {
1124
1186
  rethrowManagementBodyTooLarge(error);
@@ -1131,21 +1193,39 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1131
1193
  try {
1132
1194
  const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
1133
1195
  const { inspectDesktop3pConfigLibrary } = await import("../../claude/desktop-3p");
1196
+ const { resolveClaudeDesktopApplyMode, inspectDesktopFirstParty } = await import("../../claude/desktop-first-party");
1197
+ const { getClaudeInterceptState } = await import("../../claude/intercept/runtime");
1134
1198
  const persisted = loadConfig();
1135
1199
  const savedFingerprint = persisted.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
1136
1200
  const observed = inspectDesktop3pConfigLibrary({ appliedFingerprint: savedFingerprint });
1137
1201
  const desiredEnabled = claudeDesktopIntegrationEnabled(persisted);
1138
- const applied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
1202
+ const gatewayApplied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
1203
+ // A gateway profile on disk is what Desktop actually runs, whatever the saved mode says.
1204
+ const mode = gatewayApplied ? "gateway" : resolveClaudeDesktopApplyMode(persisted);
1205
+ const firstPartySeen = inspectDesktopFirstParty(persisted);
1206
+ const intercept = getClaudeInterceptState();
1207
+ const firstParty = {
1208
+ settingsState: firstPartySeen.settings.kind,
1209
+ applied: firstPartySeen.applied,
1210
+ stale: firstPartySeen.stale,
1211
+ interceptEnabled: firstPartySeen.interceptEnabled,
1212
+ interceptRunning: intercept !== null,
1213
+ proxyPort: intercept?.proxyPort ?? firstPartySeen.proxyPort,
1214
+ caCertPath: firstPartySeen.caCertPath,
1215
+ };
1216
+ const applied = mode === "first-party" ? firstPartySeen.applied : gatewayApplied;
1139
1217
  // "Needs update" is only meaningful while the integration is wanted. When the
1140
1218
  // durable switch is OFF, a leftover drifted profile is residue to clear — not
1141
1219
  // a stale apply the operator should refresh.
1142
- const stale = desiredEnabled && observed.kind === "gateway_drifted";
1220
+ const stale = desiredEnabled && (mode === "first-party" ? firstPartySeen.stale : observed.kind === "gateway_drifted");
1143
1221
  const { getDesktopHealth } = await import("../../claude/desktop-health");
1144
1222
  const { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
1145
1223
  const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)({
1146
1224
  platform: deps.platform ?? process.platform,
1147
1225
  });
1148
- const policy = claudeDesktopPolicyHealth(policyState);
1226
+ // Managed-policy conflicts only matter for the gateway profile; first-party mode never
1227
+ // touches Desktop's own configuration.
1228
+ const policy = claudeDesktopPolicyHealth(mode === "first-party" ? "absent" : policyState);
1149
1229
  const health = {
1150
1230
  ...getDesktopHealth(),
1151
1231
  ok: policy.ok,
@@ -1153,14 +1233,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1153
1233
  policy,
1154
1234
  };
1155
1235
  const policyConflict = desiredEnabled && !policy.ok;
1156
- const baseDrift = desiredEnabled ? !applied || stale : applied || observed.kind === "unsafe";
1236
+ const firstPartyResidue = mode === "gateway" && (firstPartySeen.applied || firstPartySeen.stale);
1237
+ const baseDrift = desiredEnabled
1238
+ ? !applied || stale || firstPartyResidue
1239
+ : applied || gatewayApplied || firstPartySeen.applied || firstPartySeen.stale || observed.kind === "unsafe";
1157
1240
  const driftReason = policyConflict
1158
1241
  ? policy.state === "present" ? "managed_policy_present" : "managed_policy_unknown"
1159
1242
  : desiredEnabled
1160
- ? !applied ? "desired_on_not_current" : stale ? "profile_drift" : null
1161
- : applied ? "desired_off_gateway_selected" : null;
1243
+ ? !applied ? "desired_on_not_current" : stale ? "profile_drift" : firstPartyResidue ? "first_party_residue" : null
1244
+ : gatewayApplied ? "desired_off_gateway_selected" : firstPartySeen.applied || firstPartySeen.stale ? "desired_off_first_party_env" : null;
1162
1245
  return jsonResponse({
1163
1246
  desiredEnabled,
1247
+ mode,
1248
+ firstParty,
1164
1249
  installed: observed.kind !== "not_installed",
1165
1250
  observedKind: observed.kind,
1166
1251
  applied,