@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -17,7 +17,7 @@ import {
17
17
  type OcxErrorPayload,
18
18
  } from "../lib/errors";
19
19
  import { mayBecomePatchEnvelope, repairFreeformToolInput } from "../responses/apply-patch-envelope";
20
- import { encodeCompactionSummary } from "../responses/compaction";
20
+ import { encodeCompactionSummary, releaseCompactionCiphertextLease } from "../responses/compaction";
21
21
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
22
22
  import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
23
23
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "../responses/reasoning-envelope";
@@ -59,21 +59,21 @@ export function buildResponseJSON(
59
59
  ): Record<string, unknown> {
60
60
  // Default-budget safety net: a caller that omits the budget gets a bounded
61
61
  // default (disposed with the call), never the unbounded append path.
62
- if (options?.translatorBudget) {
63
- const body = buildResponseJSONWithBudget(events, modelId, options);
64
- // A buffered turn delivers its whole answer as one body, so nothing calls the per-frame
65
- // recorder on the SSE bridge. Without this the attempt would persist adapter events with
66
- // zero relayed ones, which is the loss signal -- raised on every non-streaming request.
67
- if (options.recordBufferedDelivery !== false) {
68
- attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
62
+ const ownsBudget = !options?.translatorBudget;
63
+ const budget = options?.translatorBudget ?? createTranslatorBudget();
64
+ try {
65
+ const body = buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
66
+ // Buffered delivery has no per-frame recorder; retain its existing one-shot attribution.
67
+ if (options?.translatorBudget && options.recordBufferedDelivery !== false) {
68
+ attemptDeliveryRecorder(budget)?.noteBufferedDelivery(body);
69
69
  }
70
70
  return body;
71
- }
72
- const budget = createTranslatorBudget();
73
- try {
74
- return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
75
71
  } finally {
76
- budget.dispose();
72
+ for (const event of events) {
73
+ releaseTranslatedEvent(event, budget);
74
+ releaseCompactionCiphertextLease(event, budget);
75
+ }
76
+ if (ownsBudget) budget.dispose();
77
77
  }
78
78
  }
79
79
 
@@ -618,7 +618,7 @@ function buildResponseJSONWithBudget(
618
618
  type: "compaction", id: `cmp_${uuid()}`,
619
619
  encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(batchCompaction)),
620
620
  };
621
- pushOutput(item, compactionEncryptedContent ? bytesOf(compactionEncryptedContent) : batchCompaction.bytes);
621
+ pushOutput(item, compactionEncryptedContent ? 0 : batchCompaction.bytes);
622
622
  }
623
623
 
624
624
  const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
package/src/bridge/sse.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  repairFreeformToolInput,
23
23
  } from "../responses/apply-patch-envelope";
24
24
  import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
25
- import { encodeCompactionSummary } from "../responses/compaction";
25
+ import { encodeCompactionSummary, releaseCompactionCiphertextLease } from "../responses/compaction";
26
26
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
27
27
  import { mayBecomeCodeModeShellInput } from "../responses/code-mode-shell-input";
28
28
  import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
@@ -714,6 +714,10 @@ export function bridgeToResponsesSSE(
714
714
  firstOutputReported = true;
715
715
  try { options?.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
716
716
  };
717
+ const releaseEvent = (event: AdapterEvent) => {
718
+ releaseTranslatedEvent(event, budget);
719
+ releaseCompactionCiphertextLease(event, budget);
720
+ };
717
721
  const it = events[Symbol.asyncIterator]();
718
722
  let iteratorStarted = false;
719
723
  let iteratorReturned = false;
@@ -734,8 +738,11 @@ export function bridgeToResponsesSSE(
734
738
  if (!iteratorStarted) {
735
739
  iteratorStarted = true;
736
740
  try {
737
- void it.next().then(finishReturn, () => {}).catch(() => {});
741
+ void it.next().then(next => {
742
+ try { if (!next.done) releaseEvent(next.value); } finally { finishReturn(); }
743
+ }, finishReturn).catch(() => {});
738
744
  } catch {
745
+ finishReturn();
739
746
  /* synchronous iterator start failure is also best-effort */
740
747
  }
741
748
  return;
@@ -809,6 +816,7 @@ export function bridgeToResponsesSSE(
809
816
  while (!terminated && !closed && emittedFrames === emittedAtStart) {
810
817
  iteratorStarted = true;
811
818
  const next = await it.next();
819
+ try {
812
820
  // A cancel during this await disposes the owned budget; a late event
813
821
  // must never be processed or charged against it. Exit step() outright:
814
822
  // falling into EOF synthesis would let closeCurrentMessage() charge
@@ -1228,9 +1236,8 @@ export function bridgeToResponsesSSE(
1228
1236
  encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
1229
1237
  };
1230
1238
  emit("response.output_item.done", { output_index: outputIndex, item });
1231
- retainFinishedItem(item as OutputItem, event.compactionEncryptedContent
1232
- ? bytesOf(event.compactionEncryptedContent)
1233
- : compaction.bytes);
1239
+ retainFinishedItem(item as OutputItem, event.compactionEncryptedContent ? 0 : compaction.bytes);
1240
+ releaseCompactionCiphertextLease(event, budget);
1234
1241
  outputIndex++;
1235
1242
  }
1236
1243
  // Recognize every adapter's truncation vocabulary, not just the canonical pair.
@@ -1333,6 +1340,9 @@ export function bridgeToResponsesSSE(
1333
1340
  terminated = true;
1334
1341
  break;
1335
1342
  }
1343
+ } finally {
1344
+ if (!next.done) releaseEvent(next.value);
1345
+ }
1336
1346
  }
1337
1347
  } catch (err) {
1338
1348
  if (isTranslatorBudgetExceededError(err)) {
@@ -1,10 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { lstatSync } from "node:fs";
3
- import { dirname, relative, sep } from "node:path";
3
+ import { dirname, join, relative, sep } from "node:path";
4
4
 
5
5
  /**
6
- * The launcher persistently execs a binary taken from a discovered bundle, so the bundle must be
7
- * one this user can trust: OpenAI-signed, and not replaceable by another local account. Bundle
6
+ * Launch and restore execute code from a discovered bundle, so the bundle must be
7
+ * OpenAI-signed and pass ownership/permission checks along its filesystem path. Bundle
8
8
  * identifier alone (what discovery checks) is plain text in Info.plist that anyone can copy.
9
9
  */
10
10
  export const OPENAI_TEAM_ID = "2DC432GLL2";
@@ -14,17 +14,23 @@ const CODESIGN_TIMEOUT_MS = 30_000;
14
14
 
15
15
  export interface BundleTrustDeps {
16
16
  readonly uid: number;
17
- stat(path: string): { uid: number; mode: number; isFile: boolean; isDirectory: boolean; isSymbolicLink: boolean } | null;
17
+ readonly adminGroupId?: number;
18
+ stat(path: string): { uid: number; gid?: number; mode: number; isFile: boolean; isDirectory: boolean; isSymbolicLink: boolean } | null;
18
19
  codesign(args: readonly string[]): { status: number | null; output: string };
19
20
  }
20
21
 
21
22
  export function defaultBundleTrustDeps(): BundleTrustDeps {
23
+ const admin = spawnSync("/usr/bin/dscl", [".", "-read", "/Groups/admin", "PrimaryGroupID"], {
24
+ encoding: "utf8", timeout: 5000,
25
+ });
26
+ const adminId = admin.status === 0 ? /^PrimaryGroupID: ([0-9]+)\s*$/.exec(admin.stdout)?.[1] : undefined;
22
27
  return {
28
+ adminGroupId: adminId === undefined ? undefined : Number(adminId),
23
29
  uid: typeof process.getuid === "function" ? process.getuid() : -1,
24
30
  stat(path) {
25
31
  try {
26
32
  const s = lstatSync(path);
27
- return { uid: s.uid, mode: s.mode, isFile: s.isFile(), isDirectory: s.isDirectory(), isSymbolicLink: s.isSymbolicLink() };
33
+ return { uid: s.uid, gid: s.gid, mode: s.mode, isFile: s.isFile(), isDirectory: s.isDirectory(), isSymbolicLink: s.isSymbolicLink() };
28
34
  } catch {
29
35
  return null;
30
36
  }
@@ -46,26 +52,50 @@ function ownedSafely(deps: BundleTrustDeps, path: string, kind: "file" | "direct
46
52
  return null;
47
53
  }
48
54
 
55
+ /** Protect the bundle's directory entry all the way to the filesystem root. */
56
+ function untrustedAncestorReason(bundleRoot: string, deps: BundleTrustDeps): string | null {
57
+ for (let dir = dirname(bundleRoot); ; dir = dirname(dir)) {
58
+ const s = deps.stat(dir);
59
+ if (!s || !s.isDirectory || s.isSymbolicLink) return `${dir} is not a regular ancestor directory`;
60
+ if (s.uid !== 0 && s.uid !== deps.uid) return `${dir} is owned by another user`;
61
+ // A trusted sticky parent protects the already-checked child from replacement.
62
+ // Root-owned admin-group install containers are privileged OS-controlled paths;
63
+ // resolve that group from the local directory service rather than assuming a GID.
64
+ const sticky = (s.mode & 0o1000) !== 0;
65
+ const privileged = s.uid === 0 && (s.mode & 0o002) === 0
66
+ && deps.adminGroupId !== undefined && s.gid === deps.adminGroupId;
67
+ if ((s.mode & 0o022) !== 0 && !sticky && !privileged) return `${dir} is writable by group or others`;
68
+ if (dirname(dir) === dir) return null;
69
+ }
70
+ }
71
+
49
72
  /**
50
- * Null when the bundle root and the app-server binary inside it may be exec'd by this user;
51
- * otherwise a short reason. Every directory from the binary up to the bundle root, and the
73
+ * Null when the bundle root and the selected executable may be run by this user.
74
+ * Restore omits the app-server binary and validates the app shell instead, so a missing
75
+ * app-server does not prevent returning to a normal launch. Otherwise returns a short reason.
76
+ * Every directory from the binary up to the bundle root, and the
52
77
  * binary itself, must be owned by root or this user and not group/other-writable, and both the
53
78
  * bundle and the binary must pass strict code-signature verification under OpenAI's team ID.
79
+ * Ancestors above the bundle are also checked for POSIX directory-entry replacement rights.
54
80
  */
55
81
  export function untrustedChatgptBundleReason(
56
82
  bundleRoot: string,
57
- binary: string,
83
+ binary: string | undefined = undefined,
58
84
  deps: BundleTrustDeps = defaultBundleTrustDeps(),
59
85
  ): string | null {
86
+ const label = binary === undefined ? "app executable" : "app-server binary";
87
+ binary ??= join(bundleRoot, "Contents", "MacOS", "ChatGPT");
60
88
  const inside = relative(bundleRoot, binary);
61
- if (!inside || inside.startsWith("..") || inside.startsWith(sep)) return "the app-server binary is outside the bundle";
89
+ if (!inside || inside.startsWith("..") || inside.startsWith(sep)) return `the ${label} is outside the bundle`;
62
90
  const fileProblem = ownedSafely(deps, binary, "file");
63
- if (fileProblem) return `the app-server binary ${fileProblem}`;
91
+ if (fileProblem) return `the ${label} ${fileProblem}`;
64
92
  for (let dir = dirname(binary); ; dir = dirname(dir)) {
65
93
  const dirProblem = ownedSafely(deps, dir, "directory");
66
94
  if (dirProblem) return `${dir} ${dirProblem}`;
67
95
  if (dir === bundleRoot || dirname(dir) === dir) break;
68
96
  }
97
+ const ancestorProblem = untrustedAncestorReason(bundleRoot, deps);
98
+ if (ancestorProblem) return ancestorProblem;
69
99
  for (const target of [bundleRoot, binary]) {
70
100
  if (deps.codesign(["--verify", "--strict", target]).status !== 0) return `${target} failed code-signature verification`;
71
101
  const details = deps.codesign(["-dv", "--verbose=2", target]);
@@ -38,7 +38,10 @@ export function unlockRateLimitGate(value: unknown): boolean {
38
38
  return { cleared, exhausted, blocked };
39
39
  }
40
40
  if (!isRecord(node)) return { cleared, exhausted, blocked };
41
+ // A usage window at 100%, in either spelling the gate fields come in: `usedPercent` in the
42
+ // app-server's JSON-RPC, `used_percent` in the web usage snapshot.
41
43
  if (typeof node.usedPercent === "number" && node.usedPercent >= 100) exhausted = true;
44
+ if (typeof node.used_percent === "number" && node.used_percent >= 100) exhausted = true;
42
45
  if (node.spendControlReached !== undefined && node.spendControlReached !== null && node.spendControlReached !== false) blocked = true;
43
46
  if (isRecord(node.spend_control) && node.spend_control.reached === true) blocked = true;
44
47
  const reachedType = node.rate_limit_reached_type;
@@ -15,14 +15,12 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileS
15
15
  import { join } from "node:path";
16
16
  import type { OcxConfig } from "../types";
17
17
  import { renameAtomicFile } from "../lib/windows-atomic-replace";
18
- import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
19
- import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
18
+ import { entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
19
+ import { stripOneMillionMarker } from "./context-windows";
20
20
  import { claudeConfigDir } from "./gateway-cache";
21
- import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config";
21
+ import { DEFAULT_SUBAGENT_MODELS } from "../config";
22
22
  import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
23
23
  import { AnthropicRequestError } from "./inbound-records";
24
- import { knownModelIdsForProvider } from "../router";
25
- import { decodeRoutedModelIdOrThrow } from "../providers/slug-codec";
26
24
  import { siblingOfLivePort } from "../codex/sibling-start";
27
25
 
28
26
  export interface ClaudeAgentDef {
@@ -37,7 +35,7 @@ export interface ClaudeAgentDef {
37
35
  const OWNED_PREFIX = "ocx-";
38
36
  /** Ownership proof (audit 071 #2): a file without this marker is NEVER touched. */
39
37
  const GENERATED_MARKER = "generated-by: opencodex";
40
- const SAFE_AGENT_MODEL_ID = /^[a-z0-9][a-z0-9._:/@+\[\]~-]*$/i;
38
+
41
39
 
42
40
  function sanitizeName(value: string): string {
43
41
  const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -59,43 +57,6 @@ function pickerDefaultModel(configDir: string): string | null {
59
57
  }
60
58
  }
61
59
 
62
- /** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
63
- * Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
64
- * so the alias joins the raw-native context-window map (context-windows.ts). */
65
-
66
- /**
67
- * Generated subagent defs cannot rely on the parent's auto-context compaction
68
- * pairing, so their [1m] marker follows the AUTHORITATIVE window only: mark when
69
- * the effective window (exact selector, then the canonical [1m] form, then bare)
70
- * is genuinely >= 1M; strip an inherited unsafe marker back to the bare selector;
71
- * with no window information, keep the selector as it was. Genuine routed [1m]
72
- * ids are preserved through the canonical-exact lookup. (#854)
73
- */
74
- function withSubagentContextMarker(selector: string, windows: Record<string, number>): string {
75
- const bare = stripOneMillionMarker(selector);
76
- const wasMarked = selector !== bare;
77
- const canonicalExact = wasMarked ? `${bare}[1m]` : selector;
78
- const authoritativeWindow = windows[selector] ?? windows[canonicalExact] ?? windows[bare];
79
- if (typeof authoritativeWindow === "number" && authoritativeWindow > 0) {
80
- return shouldMarkOneMillion(authoritativeWindow, AUTO_CONTEXT_OFF)
81
- ? (withOneMillionMarker(selector, windows) ?? selector)
82
- : bare;
83
- }
84
- return wasMarked ? selector : bare;
85
- }
86
- function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
87
- const slash = entry.indexOf("/");
88
- if (slash > 0) {
89
- const provider = entry.slice(0, slash);
90
- const prov = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
91
- const id = prov
92
- ? decodeRoutedModelIdOrThrow(entry.slice(slash + 1), knownModelIdsForProvider(provider, prov, config))
93
- : entry.slice(slash + 1);
94
- return { alias: claudeCodeAlias(provider, id), id, provider };
95
- }
96
- return { alias: claudeCodeNativeAlias(entry), id: entry, provider: "native" };
97
- }
98
-
99
60
  export function buildClaudeAgentDefs(
100
61
  config: OcxConfig,
101
62
  windows: Record<string, number>,
@@ -73,19 +73,13 @@ export function writeGatewayModelCache(baseUrl: string, models: readonly Gateway
73
73
  }
74
74
  }
75
75
 
76
- /** Fetch the anthropic-flavor /v1/models from the local proxy and write the cache. */
77
- export async function refreshGatewayModelCacheFromProxy(
78
- port: number,
79
- options?: GatewayModelCacheRefreshOptions,
80
- ): Promise<string | null>;
81
- export async function refreshGatewayModelCacheFromProxy(
82
- target: GatewayModelTarget,
83
- options?: GatewayModelCacheRefreshOptions,
84
- ): Promise<string | null>;
85
- export async function refreshGatewayModelCacheFromProxy(
76
+ export interface GatewayModelSnapshot { baseUrl: string; models: GatewayModelRow[] }
77
+
78
+ /** Acquire fresh exposure independently of whether the local cache is writable. */
79
+ export async function fetchGatewayModels(
86
80
  portOrTarget: number | GatewayModelTarget,
87
81
  options: GatewayModelCacheRefreshOptions = {},
88
- ): Promise<string | null> {
82
+ ): Promise<GatewayModelSnapshot | null> {
89
83
  try {
90
84
  const headers = new Headers({ "anthropic-version": "2023-06-01" });
91
85
  // A wildcard/non-loopback listener requires data-plane admission even for a
@@ -114,14 +108,23 @@ export async function refreshGatewayModelCacheFromProxy(
114
108
  const body = await res.json() as { data?: unknown };
115
109
  if (!Array.isArray(body.data)) return null;
116
110
  const models: GatewayModelRow[] = body.data
117
- .filter(m => typeof m.id === "string" && (m.id as string).length > 0)
111
+ .filter(m => m && typeof m === "object" && typeof m.id === "string" && m.id.length > 0)
118
112
  .map(m => ({
119
113
  id: m.id as string,
120
114
  display_name: typeof m.display_name === "string" ? m.display_name : undefined,
121
115
  description: typeof m.description === "string" ? m.description : undefined,
122
116
  }));
123
- return writeGatewayModelCache(baseUrl, models, options.configDir);
117
+ return { baseUrl, models };
124
118
  } catch {
125
119
  return null;
126
120
  }
127
121
  }
122
+
123
+ /** Preserve the cache-refresh API used by launchers and profile application. */
124
+ export async function refreshGatewayModelCacheFromProxy(
125
+ portOrTarget: number | GatewayModelTarget,
126
+ options: GatewayModelCacheRefreshOptions = {},
127
+ ): Promise<string | null> {
128
+ const snapshot = await fetchGatewayModels(portOrTarget, options);
129
+ return snapshot ? writeGatewayModelCache(snapshot.baseUrl, snapshot.models, options.configDir) : null;
130
+ }
@@ -1,4 +1,4 @@
1
- import type { OcxClaudeCodeConfig } from "../types";
1
+ import type { OcxConfig, OcxClaudeCodeConfig } from "../types";
2
2
  import { isAnthropicOutputSchema, satisfiesOpenAiStrictSchema } from "../adapters/anthropic-output-schema";
3
3
  import { resolveAlias } from "./alias";
4
4
  import { stripOneMillionMarker } from "./context-windows";
@@ -153,7 +153,14 @@ export function extractOcxRouteDirective(body: unknown): string | null {
153
153
  const text = systemText(body);
154
154
  if (!text) return null;
155
155
  const match = OCX_ROUTE_RE.exec(text);
156
- return match ? match[1]! : null;
156
+ if (!match) return null;
157
+ // Explicit gateway selection belongs to this invocation, not the mutable saved
158
+ // force setting. Bare Claude ids still need the legacy frontmatter fallback.
159
+ if (isRec(body) && typeof body.model === "string") {
160
+ const selector = stripOneMillionMarker(body.model);
161
+ if (resolveAlias(selector) || resolveDesktop3pAlias(selector)) return body.model;
162
+ }
163
+ return match[1]!;
157
164
  }
158
165
 
159
166
  /**
@@ -17,6 +17,7 @@ import { join } from "node:path";
17
17
  import type { ClaudeFirstPartyDesired } from "../first-party-settings";
18
18
  import { classifyInterceptClient } from "./client-class";
19
19
  import { BOOTSTRAP_MAX_DECODED_BYTES, injectPickerModels, type PickerModelEntry } from "./picker-bootstrap";
20
+ import { PickerRewriteBudget } from "./picker-budget";
20
21
 
21
22
  export type CliCatalogKind = "model_selector" | "bootstrap";
22
23
 
@@ -60,26 +61,36 @@ export function injectCliBootstrapOptions(bootstrap: unknown, models: readonly P
60
61
  if (current !== undefined && current !== null && !Array.isArray(current)) return 0;
61
62
  const options = Array.isArray(current) ? current : [];
62
63
  const existing = new Set(options.map(option => record(option)?.model));
63
- let added = 0;
64
- for (const model of models) {
65
- if (existing.has(model.id)) continue;
66
- options.push({ model: model.id, name: model.name, description: model.description ?? "" });
67
- existing.add(model.id);
68
- added++;
69
- }
70
- if (added > 0) body.additional_model_options = options;
71
- return added;
64
+ const additions: Record<string, unknown>[] = [];
65
+ try {
66
+ const budget = new PickerRewriteBudget(body);
67
+ // A missing/null property needs a key and array, plus a possible separator (conservative).
68
+ if (!Array.isArray(current)) budget.reserveBytes(32);
69
+ for (const model of models) {
70
+ if (existing.has(model.id)) continue;
71
+ const row = { model: model.id, name: model.name, description: model.description ?? "" };
72
+ budget.reserveRow(row);
73
+ additions.push(row);
74
+ existing.add(model.id);
75
+ }
76
+ } catch { return 0; } // Fail open without mutating the original options.
77
+ for (const row of additions) options.push(row);
78
+ if (additions.length > 0) body.additional_model_options = options;
79
+ return additions.length;
72
80
  }
73
81
 
74
82
  /** Rewrite a decoded catalog body; `null` when nothing was added or the body is not the expected shape. */
75
83
  export function rewriteCliCatalogBody(kind: CliCatalogKind, text: string, models: readonly PickerModelEntry[]): string | null {
76
- if (models.length === 0) return null;
77
- let parsed: unknown;
78
- try { parsed = JSON.parse(text); } catch { return null; }
79
- const added = kind === "bootstrap"
80
- ? injectCliBootstrapOptions(parsed, models)
81
- : injectPickerModels(parsed, models, undefined, { surfaces: CLI_PICKER_SURFACE_IDS, extraStrippedKeys: CLI_EXTRA_STRIPPED_KEYS });
82
- return added > 0 ? JSON.stringify(parsed) : null;
84
+ if (models.length === 0 || Buffer.byteLength(text) > BOOTSTRAP_MAX_DECODED_BYTES) return null;
85
+ try {
86
+ const parsed: unknown = JSON.parse(text);
87
+ const added = kind === "bootstrap"
88
+ ? injectCliBootstrapOptions(parsed, models)
89
+ : injectPickerModels(parsed, models, undefined, { surfaces: CLI_PICKER_SURFACE_IDS, extraStrippedKeys: CLI_EXTRA_STRIPPED_KEYS });
90
+ if (added === 0) return null;
91
+ const rewritten = JSON.stringify(parsed);
92
+ return Buffer.byteLength(rewritten) <= BOOTSTRAP_MAX_DECODED_BYTES ? rewritten : null;
93
+ } catch { return null; }
83
94
  }
84
95
 
85
96
  /** Read at most `cap` bytes; null (and the stream cancelled) once the body would exceed it. */
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Reads the ALPN offer from a TLS ClientHello without terminating TLS.
3
+ *
4
+ * The picker listener uses it to send a client that offers `h2` to its HTTP/2 server and every
5
+ * other client to its HTTP/1.1 server. The ClientHello is reassembled from as many plaintext
6
+ * handshake records as it spans (RFC 8446 5.1 allows any split), within fixed byte and record
7
+ * limits. This is a routing hint, not validation: parsing stops at the ALPN decision, and the TLS
8
+ * server that receives the replayed bytes validates the handshake. Anything the parser cannot read
9
+ * up to that decision returns false, which keeps that client on HTTP/1.1, the behaviour before
10
+ * HTTP/2 existed here.
11
+ */
12
+
13
+ /** Wire bytes the front reads before deciding; a real ClientHello is far smaller. */
14
+ export const CLIENT_HELLO_MAX_BYTES = 64 * 1024;
15
+ /** Handshake records one ClientHello may span. */
16
+ export const CLIENT_HELLO_MAX_RECORDS = 16;
17
+
18
+ const RECORD_HEADER_BYTES = 5;
19
+ const RECORD_HANDSHAKE = 0x16;
20
+ const MAX_RECORD_PAYLOAD = 16_384;
21
+ const HANDSHAKE_CLIENT_HELLO = 0x01;
22
+ const EXTENSION_ALPN = 0x0010;
23
+
24
+ /**
25
+ * Whether the ClientHello at the start of `data` offers `h2` in ALPN. Returns null while more
26
+ * bytes are needed to decide, and false for anything else (not TLS, malformed, over a limit).
27
+ */
28
+ export function clientHelloOffersH2(data: Uint8Array): boolean | null {
29
+ const handshake: Uint8Array[] = [];
30
+ let handshakeBytes = 0;
31
+ let at = 0;
32
+ for (let records = 0; ; records++) {
33
+ if (handshakeBytes >= 4) {
34
+ const first = concat(handshake, 4);
35
+ if (first[0] !== HANDSHAKE_CLIENT_HELLO) return false;
36
+ const needed = 4 + ((first[1]! << 16) | (first[2]! << 8) | first[3]!);
37
+ if (needed > CLIENT_HELLO_MAX_BYTES) return false;
38
+ if (handshakeBytes >= needed) return helloOffersH2(concat(handshake, needed).subarray(4));
39
+ }
40
+ if (records >= CLIENT_HELLO_MAX_RECORDS || at >= CLIENT_HELLO_MAX_BYTES) return false;
41
+ if (data.length < at + RECORD_HEADER_BYTES) return null;
42
+ if (data[at] !== RECORD_HANDSHAKE || data[at + 1] !== 0x03) return false;
43
+ const length = (data[at + 3]! << 8) | data[at + 4]!;
44
+ if (length === 0 || length > MAX_RECORD_PAYLOAD) return false;
45
+ const end = at + RECORD_HEADER_BYTES + length;
46
+ if (end > CLIENT_HELLO_MAX_BYTES) return false;
47
+ if (data.length < end) return null;
48
+ handshake.push(data.subarray(at + RECORD_HEADER_BYTES, end));
49
+ handshakeBytes += length;
50
+ at = end;
51
+ }
52
+ }
53
+
54
+ function concat(parts: readonly Uint8Array[], bytes: number): Uint8Array {
55
+ const out = new Uint8Array(bytes);
56
+ let offset = 0;
57
+ for (const part of parts) {
58
+ if (offset >= bytes) break;
59
+ const take = part.subarray(0, bytes - offset);
60
+ out.set(take, offset);
61
+ offset += take.length;
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function helloOffersH2(hello: Uint8Array): boolean {
67
+ let at = 2 + 32; // legacy_version, random
68
+ const skip = (lengthBytes: 1 | 2): boolean => {
69
+ if (at + lengthBytes > hello.length) return false;
70
+ const length = lengthBytes === 1 ? hello[at]! : (hello[at]! << 8) | hello[at + 1]!;
71
+ at += lengthBytes + length;
72
+ return at <= hello.length;
73
+ };
74
+ if (!skip(1) || !skip(2) || !skip(1)) return false; // session id, cipher suites, compression
75
+ if (at + 2 > hello.length) return false; // no extensions block: no ALPN
76
+ const extensionsEnd = at + 2 + ((hello[at]! << 8) | hello[at + 1]!);
77
+ if (extensionsEnd > hello.length) return false;
78
+ at += 2;
79
+ // Unknown and GREASE extensions are skipped by their declared length, in any order.
80
+ while (at + 4 <= extensionsEnd) {
81
+ const type = (hello[at]! << 8) | hello[at + 1]!;
82
+ const length = (hello[at + 2]! << 8) | hello[at + 3]!;
83
+ const body = at + 4;
84
+ if (body + length > extensionsEnd) return false;
85
+ if (type === EXTENSION_ALPN) return alpnListOffersH2(hello.subarray(body, body + length));
86
+ at = body + length;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ function alpnListOffersH2(extension: Uint8Array): boolean {
92
+ if (extension.length < 2) return false;
93
+ const end = 2 + ((extension[0]! << 8) | extension[1]!);
94
+ if (end > extension.length) return false;
95
+ for (let at = 2; at < end;) {
96
+ const length = extension[at]!;
97
+ const name = at + 1;
98
+ if (length === 0 || name + length > end) return false;
99
+ if (length === 2 && extension[name] === 0x68 && extension[name + 1] === 0x32) return true; // "h2"
100
+ at = name + length;
101
+ }
102
+ return false;
103
+ }
@@ -113,7 +113,7 @@ export function parseInterceptBindingPatch(body: unknown): InterceptBindingPatch
113
113
  if (!set || typeof set !== "object" || Array.isArray(set)) return { error: "set must be an object of picker id to route" };
114
114
  const entries: Record<string, string> = {};
115
115
  for (const [id, route] of Object.entries(set as Record<string, unknown>)) {
116
- if (!isInterceptBindingId(id)) return { error: `invalid picker id: ${id} (expected a claude- model id)` };
116
+ if (!isInterceptBindingId(id)) return { error: "invalid picker id (expected a claude- model id)" };
117
117
  if (!isInterceptBindingRoute(route)) return { error: `invalid route for ${id}` };
118
118
  entries[id] = route;
119
119
  }
@@ -137,7 +137,8 @@ export function applyInterceptBindingPatch(
137
137
  ): InterceptBindingPatchResult {
138
138
  const next: Record<string, string> = { ...current };
139
139
  for (const [id, route] of Object.entries(patch.set ?? {})) {
140
- if (!availableRoutes.has(route)) return { ok: false, error: `route is not available: ${route}` };
140
+ // A rejected route is not echoed: whatever sits in that position may be a misplaced credential.
141
+ if (!availableRoutes.has(route)) return { ok: false, error: "route is not available (see ocx claude desktop show)" };
141
142
  next[id] = route;
142
143
  }
143
144
  for (const id of patch.remove ?? []) delete next[id];