@bitkyc08/opencodex 2.86.0 → 2.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/lib/errors.ts CHANGED
@@ -650,10 +650,22 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
650
650
  return { httpStatus, error };
651
651
  }
652
652
 
653
+ /** A declared client refusal: retrying the identical request cannot change the verdict. */
654
+ const CLIENT_ERROR_STATUS: Record<string, 400 | 401 | 403 | 404> = {
655
+ invalid_request_error: 400,
656
+ authentication_error: 401,
657
+ permission_error: 403,
658
+ not_found_error: 404,
659
+ };
660
+
653
661
  /** Narrow structured classes carried through a synthesized bare-error terminal. */
654
662
  export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
655
- { type: string; code: string; httpStatus: 429 | 503 } | undefined {
663
+ { type: string; code: string; httpStatus: 400 | 401 | 403 | 404 | 429 | 503 } | undefined {
656
664
  const code = typeof error.code === "string" ? error.code : undefined;
665
+ const clientClass = code ?? (typeof error.type === "string" ? error.type : undefined);
666
+ if (clientClass !== undefined && Object.hasOwn(CLIENT_ERROR_STATUS, clientClass)) {
667
+ return { type: clientClass, code: clientClass, httpStatus: CLIENT_ERROR_STATUS[clientClass] };
668
+ }
657
669
  // An explicit unknown code must not gain a recognized class from its type or copy.
658
670
  if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
659
671
  if (code === "rate_limit_exceeded" || code === "rate_limit_error"
@@ -680,8 +692,13 @@ export function httpStatusFromTerminalError(error: {
680
692
  if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
681
693
  return 400;
682
694
  }
695
+ const message = error.message ?? "";
683
696
  const recognized = recognizedUpstreamError(error);
684
- if (recognized) return recognized.httpStatus;
697
+ // Client-closed messages often arrive as invalid_request_error after classifyError, so a
698
+ // recognized 400 yields to the client-closed check below; every other class keeps its order.
699
+ if (recognized && !(recognized.httpStatus === 400 && message && isClientClosedMessage(message))) {
700
+ return recognized.httpStatus;
701
+ }
685
702
  if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
686
703
  if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
687
704
  if (
@@ -693,7 +710,6 @@ export function httpStatusFromTerminalError(error: {
693
710
  if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
694
711
  // Client-closed messages often arrive as invalid_request_error after classifyError; check message
695
712
  // before treating every invalid_request_error as HTTP 400.
696
- const message = error.message ?? "";
697
713
  if (message && isClientClosedMessage(message)) return 499;
698
714
  if (error.type === "invalid_request_error") return 400;
699
715
  if (error.type === "proxy_error") return 500;
@@ -2,6 +2,10 @@
2
2
  * Application Default Credentials (ADC) resolution for Vertex AI.
3
3
  *
4
4
  * Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
5
+ * 0. A `gcp-sa:<account>` reference (the provider's active key resolving to one) → the Service
6
+ * Account / authorized_user JSON stored in the OS credential store under that account
7
+ * (`keychain:<account>` secret, written by the key-store save path when the user pastes a
8
+ * credential JSON directly into the API-key slot). See resolveGcpCredentialMarker.
5
9
  * 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
6
10
  * exchange) or `type: "authorized_user"` (refresh-token exchange).
7
11
  * 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
@@ -18,6 +22,8 @@ import { Buffer } from "node:buffer";
18
22
  import * as os from "node:os";
19
23
  import * as path from "node:path";
20
24
  import { readFileSync, existsSync, statSync } from "node:fs";
25
+ import { GCP_CREDENTIAL_MARKER_PREFIX, providerKeychainEntry } from "../providers/api-key-resolve";
26
+ export { isGcpCredentialMaterial } from "./gcp-credential-input";
21
27
  import {
22
28
  captureConfigGeneration,
23
29
  sweepExpiredOnWrite,
@@ -36,6 +42,50 @@ const TOKEN_TIMEOUT_MS = 15_000;
36
42
  const TOKEN_ATTEMPTS = 3;
37
43
  const TOKEN_RETRY_BASE_MS = 300;
38
44
 
45
+ /**
46
+ * Reference prefix stored in `provider.apiKey` (and `apiKeyPool` entries) when the user pastes a
47
+ * GCP credential JSON directly into an API-key slot. The secret JSON lives in the OS credential
48
+ * store under the same service the `keychain:` references use; only the marker is persisted to
49
+ * `config.json`. The leaf reference prefix also identifies already-stored material for migration.
50
+ */
51
+ export { GCP_CREDENTIAL_MARKER_PREFIX } from "../providers/api-key-resolve";
52
+
53
+ /** Extract a marker account; malformed explicit markers never opt into host ADC. */
54
+ export function gcpCredentialMarkerAccount(value: string | undefined): string | undefined {
55
+ if (typeof value !== "string" || !value.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
56
+ const account = value.slice(GCP_CREDENTIAL_MARKER_PREFIX.length);
57
+ if (!account.trim()) throw new Error("GCP credential marker requires a non-empty account");
58
+ return account;
59
+ }
60
+
61
+ /**
62
+ * Whether a pasted API-key value is in fact a GCP credential JSON. Used by the save path to
63
+ * divert JSON material out of the literal-key flow before it reaches `config.json`. Deliberately
64
+ * strict: only a `type` field with a known ADC credential type counts, so a user's literal API
65
+ * key that happens to start with `{` is never eaten.
66
+ */
67
+ export function parseGcpCredentialJson(value: string): AdcFileCredentials | undefined {
68
+ const trimmed = (value ?? "").trim();
69
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return undefined;
70
+ try {
71
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
72
+ if (parsed?.type === "service_account") {
73
+ if (typeof parsed.client_email !== "string" || !parsed.client_email.trim()) return undefined;
74
+ if (typeof parsed.private_key !== "string" || !parsed.private_key.trim()) return undefined;
75
+ return parsed as unknown as AdcFileCredentials;
76
+ }
77
+ if (parsed?.type === "authorized_user") {
78
+ if (typeof parsed.client_id !== "string" || !parsed.client_id.trim()) return undefined;
79
+ if (typeof parsed.client_secret !== "string" || !parsed.client_secret.trim()) return undefined;
80
+ if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token.trim()) return undefined;
81
+ return parsed as unknown as AdcFileCredentials;
82
+ }
83
+ return undefined;
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ }
88
+
39
89
  interface CachedToken {
40
90
  token: string;
41
91
  expiresAtMs: number;
@@ -81,7 +131,10 @@ export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
81
131
 
82
132
  export function reconcileGcpAdcTokens(context: GenerationContext): number {
83
133
  if (context.generation <= lastReconciledGeneration) return 0;
84
- const currentSource = currentAdcSourceKey();
134
+ // The periodic sweep registration has no request context, so no marker is active here: markers
135
+ // travel per-request via getVertexAccessToken(options.markerAccount), and a marker-token cache
136
+ // entry whose account no longer resolves is simply pruned by this sweep when it expires.
137
+ const currentSource = currentAdcSourceKey(undefined);
85
138
  let removed = 0;
86
139
  for (const source of tokenCache.keys()) {
87
140
  if (source === currentSource) continue;
@@ -131,7 +184,29 @@ function readJsonFile<T>(filePath: string): T | undefined {
131
184
  return JSON.parse(readFileSync(filePath, "utf8")) as T;
132
185
  }
133
186
 
134
- function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | undefined {
187
+ /**
188
+ * Read the credential JSON for a `gcp-sa:<account>` marker from the OS credential store. Uses the
189
+ * same service name and the same injectable entry factory as the `keychain:` API-key references
190
+ * (api-key-resolve.providerKeychainEntry), so one probe surface and one test seam cover both.
191
+ * Mirrors `readKeychain`'s fail-closed policy: an unreadable entry yields no credential (the
192
+ * caller surfaces the failure instead of silently falling through to the next ADC source).
193
+ */
194
+ function readMarkerCredential(account: string): AdcFileCredentials | undefined {
195
+ try {
196
+ const raw = providerKeychainEntry(account).getPassword();
197
+ if (typeof raw !== "string" || !raw.trim()) return undefined;
198
+ return parseGcpCredentialJson(raw);
199
+ } catch {
200
+ return undefined;
201
+ }
202
+ }
203
+
204
+ function loadAdcCredentials(markerAccount: string | undefined): { source: string; creds: AdcFileCredentials } | undefined {
205
+ if (markerAccount) {
206
+ const creds = readMarkerCredential(markerAccount);
207
+ if (!creds) throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
208
+ return { source: `marker:${markerAccount}`, creds };
209
+ }
135
210
  const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
136
211
  if (gacPath) {
137
212
  const creds = readJsonFile<AdcFileCredentials>(gacPath);
@@ -148,9 +223,10 @@ function loadAdcCredentials(): { source: string; creds: AdcFileCredentials } | u
148
223
  * The cache key for the source the NEXT resolve would use, computed cheaply (no network). Lets the
149
224
  * cache return a token only when it still matches the active credential source, so an in-process
150
225
  * change to GOOGLE_APPLICATION_CREDENTIALS (or the user ADC file) does not keep serving a stale
151
- * token from a different source. Falls back to "metadata" when no file/env ADC is present.
226
+ * token from a different source. Falls back to "metadata" when no marker/file/env ADC is present.
152
227
  */
153
- function currentAdcSourceKey(): string {
228
+ function currentAdcSourceKey(markerAccount: string | undefined): string {
229
+ if (markerAccount) return `marker:${markerAccount}`;
154
230
  const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
155
231
  if (gacPath) return fileSourceTag("gac", gacPath);
156
232
  const userPath = userAdcPath();
@@ -279,8 +355,8 @@ async function fetchMetadataToken(signal: AbortSignal | undefined, fetchImpl: Fe
279
355
  }
280
356
  }
281
357
 
282
- async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
283
- const adc = loadAdcCredentials();
358
+ async function resolveAccessTokenUncached(markerAccount: string | undefined, signal: AbortSignal | undefined, fetchImpl: FetchImpl): Promise<{ source: string; token: TokenResponse }> {
359
+ const adc = loadAdcCredentials(markerAccount);
284
360
  if (adc) {
285
361
  const token = adc.creds.type === "service_account"
286
362
  ? await exchangeJwtForToken(adc.creds, signal, fetchImpl)
@@ -295,14 +371,25 @@ async function resolveAccessTokenUncached(signal: AbortSignal | undefined, fetch
295
371
  }
296
372
 
297
373
  /** Returns a Bearer access token for the `Authorization` header on Vertex AI calls (cached + refreshed). */
298
- export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl }): Promise<string> {
374
+ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fetch?: FetchImpl; markerAccount?: string }): Promise<string> {
375
+ // The marker travels as an argument, never as process-global state: a module-level "active
376
+ // marker" written by the router would race across concurrent requests for different providers
377
+ // and could resolve one provider's Vertex call with another provider's service account. The
378
+ // adapter passes `gcpCredentialMarkerAccount(provider.apiKey)` per request instead.
379
+ const markerAccount = options?.markerAccount;
299
380
  const fetchImpl = options?.fetch ?? globalThis.fetch.bind(globalThis);
300
381
  const skew = getRefreshSkewMs();
301
382
  const now = Date.now();
302
383
 
303
384
  // Only serve a cached token that matches the source the next resolve would actually use; prune
304
385
  // expired or now-stale (different-source) entries so a credential-source change is honored.
305
- const expectedSource = currentAdcSourceKey();
386
+ const expectedSource = currentAdcSourceKey(markerAccount);
387
+ // A cached bearer token is usable only while the explicitly selected store entry remains
388
+ // readable. Removal or a locked store must fail closed before the cache can return it.
389
+ if (markerAccount && !readMarkerCredential(markerAccount)) {
390
+ tokenCache.delete(expectedSource);
391
+ throw new Error("GCP credential marker is set but its credential could not be read from the OS credential store");
392
+ }
306
393
  const writerGeneration = captureConfigGeneration();
307
394
  for (const [source, cached] of tokenCache) {
308
395
  if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
@@ -317,7 +404,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
317
404
 
318
405
  const promise = (async () => {
319
406
  try {
320
- const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
407
+ const { source, token } = await resolveAccessTokenUncached(markerAccount, options?.signal, fetchImpl);
321
408
  const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
322
409
  if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
323
410
  tokenCache.set(source, { token: token.access_token, expiresAtMs });
@@ -0,0 +1,12 @@
1
+ /** Structured JSON is never literal key material; malformed GCP text is also refused. */
2
+ export function isGcpCredentialMaterial(value: string): boolean {
3
+ const credentialField = /\b(?:service_account|authorized_user|external_account|private_key|refresh_token|client_secret)\b|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
4
+ if (/[\[\{"']/.test(value) && credentialField.test(value)) return true;
5
+ // Reject structured carriers outright rather than recursively guessing their credential shape.
6
+ // This includes quoted/array/object wrappers with arbitrarily escaped or nested JSON inside.
7
+ try {
8
+ const parsed: unknown = JSON.parse(value);
9
+ return typeof parsed === "string" || (parsed !== null && typeof parsed === "object");
10
+ }
11
+ catch { return false; }
12
+ }
@@ -72,6 +72,11 @@ export async function buildLabProviderAuthHeaders(
72
72
  && configuredAnthropicInstance(config, routeContext.providerId) !== routeContext.providerId) {
73
73
  throw new TransportError("auth_blocked", "anthropic instance unavailable");
74
74
  }
75
+ // The DSH account token is sent only by its own adapter to its pinned endpoint; the generic
76
+ // probe transport would use another endpoint and auth scheme.
77
+ if (routeContext.providerId === "dsh-account") {
78
+ throw new TransportError("auth_blocked", "dsh-account probes are not supported");
79
+ }
75
80
  try {
76
81
  const snapshot = await getValidAccessTokenSnapshot(routeContext.providerId);
77
82
  headers.Authorization = `Bearer ${snapshot.accessToken}`;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Single-route opaque proxy tunnel. The desktop upstream dialer retains its
3
+ * own fixed-target TLS contract.
4
+ * Shares OpenCodex's MIT SOCKS handshake; the caller owns all post-establishment
5
+ * cancellation and socket cleanup. Never replay or implicitly retry directly.
6
+ */
7
+ import net, { type Socket } from 'node:net';
8
+ import tls from 'node:tls';
9
+ import { socks5Handshake, socks5Credentials, Socks5HandshakeError } from './socks5-handshake';
10
+
11
+ interface RouteEndpoint { host: string; port: number }
12
+ type RouteCredentials = { username?: undefined; password?: undefined } | { username: string; password: string };
13
+ type PlainProxy = { kind?: 'http'; proxyCa?: never };
14
+ type SecureProxy = { kind: 'https'; proxyCa?: string };
15
+ type SocksProxy = { kind: 'socks5' | 'socks5h'; proxyCa?: never };
16
+ export type OpaqueProxyRoute = RouteEndpoint & RouteCredentials & (PlainProxy | SecureProxy | SocksProxy);
17
+ export interface OpaqueTunnelOptions {
18
+ signal?: AbortSignal;
19
+ timeoutMs?: number;
20
+ maxHeaderBytes?: number;
21
+ }
22
+ interface AttemptOptions {
23
+ signal?: AbortSignal;
24
+ timeoutMs: number;
25
+ maxHeaderBytes: number;
26
+ }
27
+ export class TunnelError extends Error {
28
+ override readonly name = 'TunnelError';
29
+ readonly code: string;
30
+ readonly status?: number;
31
+ constructor(code: string, message: string, { status }: { status?: number } = {}) {
32
+ super(message); this.code = code; this.status = status;
33
+ }
34
+ }
35
+ const aborted = () => new TunnelError('ABORTED', 'Tunnel establishment cancelled');
36
+
37
+ function validateAuthority(authority: string): string {
38
+ if (typeof authority !== 'string' || !/^(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):[0-9]+$/.test(authority)) {
39
+ throw new TunnelError('TARGET', 'An explicit host:port authority is required');
40
+ }
41
+ const port = Number(authority.slice(authority.lastIndexOf(':') + 1));
42
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new TunnelError('TARGET', 'Invalid target port');
43
+ return authority;
44
+ }
45
+
46
+ function validateOptions(p: OpaqueProxyRoute, timeoutMs: number, maxHeaderBytes: number): void {
47
+ if (!p || Array.isArray(p) || typeof p.host !== 'string' || !p.host || /[\s/@]/.test(p.host)
48
+ || !Number.isInteger(p.port) || p.port < 1 || p.port > 65535
49
+ || !['http', 'https', 'socks5', 'socks5h'].includes(p.kind ?? 'http')) throw new TunnelError('CONFIG', 'Invalid proxy route');
50
+ if ((p.username === undefined) !== (p.password === undefined)
51
+ || (p.username !== undefined && (typeof p.username !== 'string' || typeof p.password !== 'string'
52
+ || (!p.kind?.startsWith('socks') && p.username.includes(':')) || Buffer.byteLength(p.username + p.password) > 4096))) throw new TunnelError('CONFIG', 'Invalid proxy credentials');
53
+ if ((p.kind ?? 'http').startsWith('socks') && p.username !== undefined
54
+ && (Buffer.byteLength(p.username) > 255 || Buffer.byteLength(p.password) > 255)) throw new TunnelError('CONFIG', 'SOCKS credentials must each fit in 255 UTF-8 bytes');
55
+ if (p.proxyCa !== undefined && p.kind !== 'https') throw new TunnelError('CONFIG', 'Proxy TLS settings require HTTPS proxy');
56
+ if (p.proxyCa !== undefined && typeof p.proxyCa !== 'string') throw new TunnelError('CONFIG', 'Invalid proxy CA');
57
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000
58
+ || !Number.isSafeInteger(maxHeaderBytes) || maxHeaderBytes < 64 || maxHeaderBytes > 65536) {
59
+ throw new TunnelError('CONFIG', 'Invalid deadline or header limit');
60
+ }
61
+ }
62
+
63
+ function connectOne(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs, maxHeaderBytes }: AttemptOptions): Promise<Socket> {
64
+ if (proxy.kind === 'socks5' || proxy.kind === 'socks5h') return connectSocks(authority, proxy, { signal, timeoutMs, maxHeaderBytes });
65
+ return new Promise<Socket>((resolve, reject) => {
66
+ if (signal?.aborted) return reject(aborted());
67
+ const secure = proxy.kind === 'https';
68
+ const identity = proxy.host;
69
+ const tlsOptions: tls.ConnectionOptions & { allowHalfOpen: boolean } = {
70
+ host: proxy.host, port: proxy.port, allowHalfOpen: true,
71
+ servername: net.isIP(identity) ? undefined : identity, ca: proxy.proxyCa,
72
+ checkServerIdentity: (_name, cert) => tls.checkServerIdentity(identity, cert),
73
+ rejectUnauthorized: true, ALPNProtocols: ['http/1.1'],
74
+ };
75
+ const socket = secure
76
+ ? tls.connect(tlsOptions)
77
+ : net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
78
+ // Keep a defensive observer across the promise-to-owner handoff. Errors still
79
+ // settle the establishment promise or are handled by the relay after handoff.
80
+ socket.on('error', () => {});
81
+ let pending: Buffer = Buffer.alloc(0); let settled = false, tlsReady = !secure;
82
+ const connectedEvent = secure ? 'secureConnect' : 'connect';
83
+ const cleanup = () => {
84
+ clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
85
+ socket.removeListener(connectedEvent, onConnect); socket.removeListener('data', onData);
86
+ socket.removeListener('error', onError); socket.removeListener('end', onEnd); socket.removeListener('close', onClose);
87
+ };
88
+ const fail = (error: Error) => {
89
+ if (settled) return; settled = true; cleanup(); socket.destroy(); reject(error);
90
+ };
91
+ const onAbort = () => fail(aborted());
92
+ const establishmentFailure = (code: string, message: string) => fail(new TunnelError(code, message));
93
+ const onError = () => {
94
+ if (secure && !tlsReady) {
95
+ fail(new TunnelError('PROXY_TLS', 'HTTPS proxy establishment failed'));
96
+ } else establishmentFailure('TRANSPORT', 'HTTP proxy connection failed');
97
+ };
98
+ const onEnd = () => establishmentFailure('EARLY_CLOSE', 'HTTP proxy closed before establishing the tunnel');
99
+ const onClose = onEnd;
100
+ const onConnect = () => {
101
+ tlsReady = true;
102
+ const auth = proxy.username === undefined ? '' : `Proxy-Authorization: Basic ${Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}\r\n`;
103
+ socket.write(`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${auth}\r\n`);
104
+ };
105
+ const onData = (chunk: Buffer) => {
106
+ pending = Buffer.concat([pending, chunk]);
107
+ const lineEnd = pending.indexOf('\r\n');
108
+ if (lineEnd >= 0) {
109
+ const firstLine = pending.subarray(0, lineEnd).toString('latin1');
110
+ const firstStatus = /^HTTP\/1\.[01] ([0-9]{3})(?: [\t\x20-\x7e\x80-\xff]*)?$/.exec(firstLine);
111
+ if (!firstStatus) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
112
+ const status = Number(firstStatus[1]);
113
+ if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
114
+ }
115
+ const end = pending.indexOf('\r\n\r\n');
116
+ if (end < 0) {
117
+ if (pending.length > maxHeaderBytes) fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
118
+ return;
119
+ }
120
+ if (end + 4 > maxHeaderBytes) return fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
121
+ const lines = pending.subarray(0, end).toString('latin1').split('\r\n');
122
+ const statusLine = lines[0];
123
+ const match = /^HTTP\/1\.[01] ([0-9]{3})(?: [^\r\n]*)?$/.exec(statusLine);
124
+ if (!match) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
125
+ const status = Number(match[1]);
126
+ if (lines.slice(1).some(line => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+:[\t\x20-\x7e\x80-\xff]*$/.test(line))) {
127
+ return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy response header'));
128
+ }
129
+ if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
130
+ settled = true; socket.pause(); cleanup();
131
+ const head = pending.subarray(end + 4);
132
+ if (head.length) socket.unshift(head);
133
+ resolve(socket);
134
+ };
135
+ const timer = setTimeout(() => establishmentFailure('TIMEOUT', 'HTTP proxy establishment deadline exceeded'), timeoutMs);
136
+ socket.once(connectedEvent, onConnect); socket.on('data', onData);
137
+ socket.once('error', onError); socket.once('end', onEnd); socket.once('close', onClose);
138
+ signal?.addEventListener('abort', onAbort, { once: true });
139
+ if (signal?.aborted) onAbort();
140
+ });
141
+ }
142
+
143
+ // The bounded socket reader owns setup cancellation and preserves queued bytes.
144
+ // Framing uses the existing
145
+ // MIT socks5-handshake module; neither shared framing nor its DNS policy changes.
146
+ async function connectSocks(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs }: AttemptOptions): Promise<Socket> {
147
+ if (signal?.aborted) throw aborted();
148
+ const socket = net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
149
+ socket.on('error', () => {});
150
+ let buffer: Buffer = Buffer.alloc(0), failure: Error | undefined;
151
+ let waiting: { length: number; resolve: () => void; reject: (error: Error) => void } | undefined;
152
+ const fail = (error: Error) => { if (failure) return; failure = error; const current = waiting; waiting = undefined; current?.reject(error); socket.destroy(); };
153
+ const transportError = (code: string, message: string) => fail(new TunnelError(code, message));
154
+ const onData = (chunk: Buffer) => {
155
+ // Consume at most the bytes needed by a bounded handshake read, then pause.
156
+ // A valid CONNECT reply can share a packet with arbitrary early target data;
157
+ // that remainder is not a malformed/oversized handshake and must survive.
158
+ socket.pause();
159
+ buffer = Buffer.concat([buffer, chunk]);
160
+ if (waiting && buffer.length >= waiting.length) { const current = waiting; waiting = undefined; current.resolve(); }
161
+ else if (waiting) socket.resume();
162
+ };
163
+ const onError = () => transportError('TRANSPORT', 'SOCKS proxy connection failed');
164
+ const onClose = () => transportError('EARLY_CLOSE', 'SOCKS proxy closed before tunnel establishment');
165
+ const onAbort = () => fail(aborted());
166
+ const timer = setTimeout(() => transportError('TIMEOUT', 'SOCKS establishment deadline exceeded'), timeoutMs);
167
+ socket.on('data', onData); socket.once('error', onError); socket.once('end', onClose); socket.once('close', onClose);
168
+ socket.pause();
169
+ signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) onAbort();
170
+ const reader = {
171
+ write(bytes: Uint8Array): void { if (failure) throw failure; socket.write(bytes); },
172
+ async readExact(length: number): Promise<Buffer> {
173
+ if (failure) throw failure;
174
+ if (!Number.isSafeInteger(length) || length < 0 || length > 1024) throw new TunnelError('PROTOCOL', 'Invalid handshake read size');
175
+ if (buffer.length < length) await new Promise<void>((resolve, reject) => {
176
+ if (waiting) { reject(new TunnelError('PROTOCOL', 'Concurrent handshake read')); return; }
177
+ waiting = { length, resolve, reject };
178
+ socket.resume();
179
+ });
180
+ if (failure) throw failure;
181
+ const result = buffer.subarray(0, length); buffer = buffer.subarray(length); return result;
182
+ },
183
+ };
184
+ try {
185
+ const credentialsUrl = new URL('socks5://fixture.invalid');
186
+ if (proxy.username !== undefined) { credentialsUrl.username = encodeURIComponent(proxy.username); credentialsUrl.password = encodeURIComponent(proxy.password); }
187
+ const colon = authority.lastIndexOf(':');
188
+ const target = { host: authority.slice(0, colon).replace(/^\[|\]$/g, ''), port: Number(authority.slice(colon + 1)) };
189
+ await socks5Handshake(reader, target, socks5Credentials(credentialsUrl), signal);
190
+ if (failure) throw failure;
191
+ if (signal?.aborted || socket.destroyed) throw aborted();
192
+ socket.pause(); if (buffer.length) socket.unshift(buffer); buffer = Buffer.alloc(0);
193
+ return socket;
194
+ } catch (error) {
195
+ socket.destroy();
196
+ if (error instanceof Socks5HandshakeError) throw new TunnelError('SOCKS_REFUSED', 'SOCKS authentication, policy, or protocol refusal');
197
+ throw error;
198
+ } finally {
199
+ clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
200
+ socket.removeListener('data', onData); socket.removeListener('error', onError);
201
+ socket.removeListener('end', onClose); socket.removeListener('close', onClose);
202
+ }
203
+ }
204
+
205
+ /** Establish only. There is no application data, implicit DIRECT, or replay here.
206
+ * HTTP/HTTPS CONNECT and SOCKS5/SOCKS5h yield an opaque raw target tunnel.
207
+ */
208
+ export async function dialTunnel(authority: string, proxy: OpaqueProxyRoute, options: OpaqueTunnelOptions = {}): Promise<Socket> {
209
+ const { signal, timeoutMs = 10000, maxHeaderBytes = 65536 } = options;
210
+ validateAuthority(authority);
211
+ validateOptions(proxy, timeoutMs, maxHeaderBytes);
212
+ if (signal?.aborted) throw aborted();
213
+ // Snapshot the selected route. No alternate proxy or direct fallback exists.
214
+ const socket = await connectOne(authority, { ...proxy }, { signal, timeoutMs, maxHeaderBytes });
215
+ if (signal?.aborted || socket.destroyed) { socket.destroy(); throw aborted(); }
216
+ return socket;
217
+ }
package/src/lib/redact.ts CHANGED
@@ -7,7 +7,7 @@ export const REDACTED_SECRET = "[REDACTED]";
7
7
  * credentials over an unsafe channel (e.g. plaintext non-loopback HTTP) rather than
8
8
  * re-deriving a narrower local list.
9
9
  */
10
- export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
10
+ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
11
11
 
12
12
  /**
13
13
  * Colon-labelled credential headers echoed back inside an error body
@@ -40,7 +40,7 @@ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cook
40
40
  // Every letter position also accepts \u0001, the placeholder the fold emits for
41
41
  // an unresolved HTML named reference: `author&ii;zation` is the label with one
42
42
  // character we cannot name, and that is still the label.
43
- const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
43
+ const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
44
44
 
45
45
  const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
46
46
  .replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
@@ -223,6 +223,8 @@ const RECOVERY_KIND_CAUSE = {
223
223
  "rate-limit-429": "rate-limit",
224
224
  "anthropic-oauth-429": "rate-limit",
225
225
  "oauth-account-429": "rate-limit",
226
+ // A 402 is the account's quota, not a transient limit; only another account can serve it.
227
+ "oauth-account-402": "quota-exhausted",
226
228
  "image-413": "payload-too-large",
227
229
  // The gateway rejects a body it accepts seconds later and the replay is byte-identical, so
228
230
  // nothing about the payload was wrong; the origin declined to take it at that moment.
@@ -96,7 +96,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
96
96
  const family = classifyModelFamilyForQuota(provider, requestedModelId);
97
97
  if (family) {
98
98
  const percents = (quota.customWindows ?? [])
99
- .filter(window => windowMatchesFamily(window.label, family))
99
+ .filter(window => window.modelId === undefined && windowMatchesFamily(window.label, family))
100
100
  .map(window => window.percent)
101
101
  .filter((value): value is number => typeof value === "number");
102
102
  if (percents.length === 0) return null;
@@ -106,7 +106,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
106
106
  quota.fiveHourPercent,
107
107
  quota.weeklyPercent,
108
108
  quota.monthlyPercent,
109
- ...(quota.customWindows ?? []).map(window => window.percent),
109
+ ...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent),
110
110
  ].filter((value): value is number => typeof value === "number");
111
111
  if (percents.length === 0) return null;
112
112
  return 100 - Math.max(...percents);
@@ -140,6 +140,73 @@ export function isAccountQuotaExhausted(
140
140
  return headroom !== null && headroom <= 0;
141
141
  }
142
142
 
143
+ /**
144
+ * Earliest future reset timestamp (in ms) for the relevant quota window, or null if unknown.
145
+ * Prefers weekly reset when multiple windows exist so expiring allowance is consumed first.
146
+ */
147
+ export function accountResetTimestamp(
148
+ provider: string,
149
+ accountId: string,
150
+ requestedModelId?: string | null,
151
+ now = Date.now(),
152
+ ): number | null {
153
+ const quota = getCachedProviderAccountQuota(provider, accountId);
154
+ if (!quota) return null;
155
+ const family = classifyModelFamilyForQuota(provider, requestedModelId);
156
+ const windows = (quota.customWindows ?? []).filter(w => !family || windowMatchesFamily(w.label, family));
157
+ const weekly = windows.find(w => /week/i.test(w.label));
158
+ let rawReset = weekly?.resetAt ?? quota.weeklyResetAt;
159
+ if (rawReset === undefined && provider !== "google-antigravity" && provider !== "anthropic") {
160
+ // Providers without a weekly cadence: prefer an explicitly 5h-labeled window before
161
+ // falling back to first-window order.
162
+ const fiveHour = windows.find(w => /5h|five/i.test(w.label)) ?? windows[0];
163
+ rawReset = fiveHour?.resetAt ?? quota.fiveHourResetAt;
164
+ } else if (rawReset === undefined) {
165
+ rawReset = windows[0]?.resetAt ?? quota.fiveHourResetAt;
166
+ }
167
+ if (typeof rawReset !== "number" || !Number.isFinite(rawReset)) return null;
168
+ const ms = rawReset < 1e11 ? rawReset * 1000 : rawReset;
169
+ return ms > now ? ms : null;
170
+ }
171
+
172
+ /**
173
+ * Rank candidates by soonest reset timestamp first among healthy accounts.
174
+ * Accounts whose quota will reset earliest get priority so allowances do not expire unused.
175
+ *
176
+ * When a passive provider has incomplete quota evidence (some ring members with no
177
+ * cached record), reset timestamps are unreliable: an unknown account converts to
178
+ * Infinity and always loses to a known near-exhausted one. Fall back to headroom
179
+ * order instead of inventing a reset ordering.
180
+ */
181
+ export function rankAccountsByResetFirst(
182
+ provider: string,
183
+ ring: readonly string[],
184
+ requestedModelId?: string | null,
185
+ now = Date.now(),
186
+ accounts?: ReadonlyMap<string, ProviderAccount>,
187
+ ): string[] {
188
+ if (ring.length < 2) return [...ring];
189
+ const ranked = rankAccountsByHeadroom(provider, ring, requestedModelId, accounts);
190
+ const healthy = ranked.filter(id => !isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
191
+ if (healthy.length < 2) return ranked;
192
+ if (
193
+ hasPassiveAccountQuota(provider)
194
+ && !ring.every(id => headroomOf(provider, id, requestedModelId, accounts?.get(id)) !== null)
195
+ ) return ranked;
196
+
197
+ const withReset = healthy.map((id, index) => ({
198
+ id,
199
+ resetAt: accountResetTimestamp(provider, id, requestedModelId, now) ?? Number.POSITIVE_INFINITY,
200
+ headroom: headroomOf(provider, id, requestedModelId, accounts?.get(id)) ?? 0,
201
+ index,
202
+ }));
203
+
204
+ withReset.sort((a, b) => (a.resetAt - b.resetAt) || (b.headroom - a.headroom) || (a.index - b.index));
205
+ const sortedHealthy = withReset.map(r => r.id);
206
+ const exhausted = ranked.filter(id => isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
207
+ return [...sortedHealthy, ...exhausted];
208
+ }
209
+
143
210
  /**
144
211
  * Order candidates best-first.
145
212
  *
@@ -31,6 +31,22 @@ export function bindAnthropicRefusalCredentialForSend(response: Response, owner:
31
31
  responseCredentials.set(response, Object.freeze({ ...owner, providerAccountUuid, checkProviderUuid: arguments.length >= 3 }));
32
32
  }
33
33
 
34
+ /** Preserve the original send proof across internal error normalization, without renewing it. */
35
+ export function carryAnthropicRefusalCredential(source: Response, target: Response): void {
36
+ const sent = responseCredentials.get(source);
37
+ if (sent) responseCredentials.set(target, sent);
38
+ }
39
+
40
+ /** Recheck after body consumption: a late refusal cannot acquire a replacement login's authority. */
41
+ export function anthropicRefusalCredentialIsCurrent(response: Response, instance: AnthropicInstanceId, accountId: string): boolean {
42
+ const sent = responseCredentials.get(response);
43
+ if (!sent || sent.provider !== instance || sent.accountId !== accountId
44
+ || !anthropicPhysicalSendOwnershipIsCurrent(sent)) return false;
45
+ const row = getAccountCredentialWithStatus(instance, accountId);
46
+ return !!row && !row.paused && !row.needsReauth
47
+ && (!sent.checkProviderUuid || row.credential.accountId === sent.providerAccountUuid);
48
+ }
49
+
34
50
  async function isRevokedOAuthToken(response: Response, signal?: AbortSignal): Promise<boolean> {
35
51
  if (response.status !== 401) return false;
36
52
  try {
@@ -59,9 +75,11 @@ async function isAccountRefusal(response: Response, signal?: AbortSignal): Promi
59
75
  // Whole-message patterns: request-policy/model/resource refusals and quoted diagnostics
60
76
  // must not acquire account authority merely by containing entitlement keywords.
61
77
  if ("code" in error && error.code != null
62
- && (typeof error.code !== "string" || !["subscription_required", "insufficient_quota", "permission_denied"].includes(error.code))) return false;
78
+ && (typeof error.code !== "string" || !["subscription_required", "insufficient_quota", "permission_denied", "oauth_org_not_allowed"].includes(error.code))) return false;
79
+ // The organization disabled subscription access: an account-scoped verdict, cooled like its siblings.
80
+ if (error.type === "oauth_org_not_allowed" || ("code" in error && error.code === "oauth_org_not_allowed")) return true;
63
81
  const message = error.message.trim();
64
- if (error.type === "permission_error" && /^(?:Your account does not have access to Claude Code|Your (?:Claude )?subscription (?:has expired|is (?:expired|inactive))|Your account does not have an active subscription)[.!]?$/i.test(message)) return true;
82
+ if (error.type === "permission_error" && /^(?:Your account does not have access to Claude Code|Your (?:Claude )?subscription (?:has expired|is (?:expired|inactive))|Your account does not have an active subscription|(?:Your|This) organization has disabled Claude subscription access for Claude Code(?:\. Use an Anthropic API key instead, or ask your admin to enable access)?)[.!]?$/i.test(message)) return true;
65
83
  return (error.type === "billing_error" || error.type === "permission_error")
66
84
  && /^Your credit balance is too low to access the Anthropic API\.(?: Please go to Plans & Billing to upgrade or purchase credits\.)?$/i.test(message);
67
85
  } catch {
@@ -1,7 +1,7 @@
1
1
  import { ConfigMutationLockError } from "../config";
2
2
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
3
3
  import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
4
- import { loginAnthropic, refreshAnthropicToken } from "./anthropic";
4
+ import { ANTHROPIC_CODE_REDIRECT_URI, loginAnthropic, refreshAnthropicToken } from "./anthropic";
5
5
  import type { OAuthProviderDef } from "./index";
6
6
  import {
7
7
  clearOAuthRefreshIntent, clearOAuthRefreshIntentIfMatch, markOAuthRefreshIntentCleanupPending,
@@ -16,8 +16,10 @@ export function anthropicOAuthDefinition(instance: AnthropicInstanceId): OAuthPr
16
16
  return {
17
17
  login: (ctrl, opts) => loginAnthropic(ctrl, {
18
18
  instance, importLocal: instance === "anthropic2" || opts?.forceLogin ? "off" : "fallback",
19
+ codeMode: opts?.codeMode === true,
19
20
  }),
20
21
  refresh: refreshAnthropicToken,
22
+ codeRedirectUri: ANTHROPIC_CODE_REDIRECT_URI,
21
23
  providerConfig,
22
24
  defaultModel,
23
25
  // Subscription OAuth must never generate unattended traffic by default.