@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
@@ -14,6 +14,22 @@ import { loadAuthStore, peekAuthStore, peekOAuthRefreshIntent, readOAuthRefreshI
14
14
  import type { ProviderAccount } from "./types";
15
15
 
16
16
  export type OAuthAccountHealth =
17
+ | { status: "healthy" }
18
+ | { status: "cooldown"; until: string; reason: "rate_limit" | "quota" }
19
+ | { status: "reauth_required"; reason: OAuthReauthReason }
20
+ | { status: "warning"; reason: "refresh_conflict" | "metadata_mismatch" | "stale_credentials" | "validation_pending" };
21
+
22
+ /** Why an OAuth account needs reauthentication. `verify_account` is distinct from a dead
23
+ * credential: the grant is alive but the provider blocks the account until a human
24
+ * verifies it, so a plain re-login without that verification will not help. */
25
+ export type OAuthReauthReason = "unauthorized" | "forbidden" | "refresh_failed" | "verify_account";
26
+
27
+ /**
28
+ * Codex-pool health: the same shape minus the provider-verification cause, which only
29
+ * the generic OAuth quarantine produces. Narrowing the return keeps Codex DTOs on
30
+ * `CodexAccountReauthReason` without a lossy re-narrow at every consumer.
31
+ */
32
+ export type CodexPoolAccountHealth =
17
33
  | { status: "healthy" }
18
34
  | { status: "cooldown"; until: string; reason: "rate_limit" | "quota" }
19
35
  | { status: "reauth_required"; reason: "unauthorized" | "forbidden" | "refresh_failed" }
@@ -24,6 +40,7 @@ export type OAuthHealthLabel =
24
40
  | "Rate limited"
25
41
  | "Quota limited"
26
42
  | "Reauthentication required"
43
+ | "Verification required"
27
44
  | "Refresh failed"
28
45
  | "Metadata mismatch"
29
46
  | "Credential conflict"
@@ -60,7 +77,7 @@ type OAuthWarningReason = "refresh_conflict" | "metadata_mismatch" | "stale_cred
60
77
 
61
78
  export function projectOAuthAccountHealth(input: {
62
79
  needsReauth?: boolean;
63
- reauthReason?: "unauthorized" | "forbidden" | "refresh_failed";
80
+ reauthReason?: OAuthReauthReason;
64
81
  cooldownUntilMs?: number;
65
82
  cooldownReason?: "rate_limit" | "quota";
66
83
  warningReason?: OAuthWarningReason;
@@ -96,6 +113,9 @@ function actionFor(provider: string, health: OAuthAccountHealth): string | undef
96
113
  }
97
114
  if (health.status === "reauth_required") {
98
115
  if (provider === "codex") return CODEX_REAUTH_ACTION;
116
+ if (health.reason === "verify_account") {
117
+ return `verify the account with the provider in a browser, then run \`ocx login ${provider}\``;
118
+ }
99
119
  return `run \`ocx login ${provider}\``;
100
120
  }
101
121
  if (health.status === "cooldown") {
@@ -115,7 +135,9 @@ export function oauthHealthLabel(health: OAuthAccountHealth): OAuthHealthLabel {
115
135
  case "cooldown":
116
136
  return health.reason === "rate_limit" ? "Rate limited" : "Quota limited";
117
137
  case "reauth_required":
118
- return health.reason === "refresh_failed" ? "Refresh failed" : "Reauthentication required";
138
+ if (health.reason === "refresh_failed") return "Refresh failed";
139
+ if (health.reason === "verify_account") return "Verification required";
140
+ return "Reauthentication required";
119
141
  case "warning":
120
142
  switch (health.reason) {
121
143
  case "validation_pending":
@@ -190,7 +212,9 @@ export function projectStoredOAuthAccountHealth(
190
212
  : null;
191
213
  return projectOAuthAccountHealth({
192
214
  needsReauth: account.needsReauth === true,
193
- reauthReason: account.needsReauth === true ? "refresh_failed" : undefined,
215
+ reauthReason: account.needsReauth === true
216
+ ? (account.needsReauthReason ?? "refresh_failed")
217
+ : undefined,
194
218
  cooldownUntilMs: anthropicSnap?.cooldownUntil,
195
219
  // Same mapping as the Codex pool's `cooldownReasonFromSource`: only a Retry-After is
196
220
  // request-rate throttling. A reset-derived cooldown means a usage window is spent, which
@@ -206,7 +230,7 @@ export function projectCodexAccountHealth(input: {
206
230
  needsReauth: boolean;
207
231
  reauthReason?: "unauthorized" | "forbidden" | "refresh_failed";
208
232
  now?: number;
209
- }): OAuthAccountHealth {
233
+ }): CodexPoolAccountHealth {
210
234
  // One read serves every verdict below. Each lookup re-reads and re-hardens the whole store
211
235
  // file, and the main account lives in the native Codex auth file rather than the pool store,
212
236
  // so a lookup for it could only ever miss.
@@ -250,13 +274,16 @@ export function projectCodexAccountHealth(input: {
250
274
  }
251
275
  const now = input.now ?? Date.now();
252
276
  const snap = getCodexAccountHealthSnapshot(input.accountId, now);
277
+ // Safe narrowing: every reason above is Codex-scoped (`verify_account` only enters
278
+ // through the generic OAuth quarantine path), so the shared projector cannot
279
+ // produce it here despite the wider return type.
253
280
  return projectOAuthAccountHealth({
254
281
  needsReauth,
255
282
  reauthReason: needsReauth ? (input.reauthReason ?? storedFailureReason ?? "refresh_failed") : undefined,
256
283
  cooldownUntilMs: snap?.cooldownUntil,
257
284
  cooldownReason: cooldownReasonFromSource(snap?.cooldownSource),
258
285
  now,
259
- });
286
+ }) as CodexPoolAccountHealth;
260
287
  }
261
288
 
262
289
  /**
@@ -52,7 +52,7 @@ import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
52
52
  import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
53
53
  import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
54
54
  import { resolveProviderTransport } from "../providers/xai-transport";
55
- import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
55
+ import { detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
56
56
  import { logOAuthEvent } from "./log";
57
57
  import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
58
58
  import { clearManualCodeSlot, ensureManualCodeSlot, kiroLoginSettling, loginAbort, loginState, waitForManualLoginCode, type OAuthLoginHint } from "./login-flow-state";
@@ -82,7 +82,9 @@ export {
82
82
  export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } from "./store";
83
83
  import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
84
84
 
85
- const REFRESH_SKEW_MS = 60_000;
85
+ import { REFRESH_SKEW_MS } from "./refresh-policy";
86
+ import { newerClaudeCredential, captureAnthropicCredentialOwner } from "./anthropic-continuity";
87
+ import { mergeAnthropicIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
86
88
  export interface OAuthAccessSnapshot {
87
89
  provider: string;
88
90
  accountId: string;
@@ -169,7 +171,7 @@ export function seedOAuthTokenRefreshFlightsForTests(rows: Array<{ key: string;
169
171
  const XAI_PERMANENT_FAILURE_TTL_MS=30_000;
170
172
  const permanentRefreshFailures=new Map<string,number>();
171
173
  interface XaiRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
172
- interface AnthropicRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal; flight?: OAuthRefreshFlightEvidence; replacedStaleFlight?: OAuthRefreshFlightEvidence }
174
+ interface AnthropicRefreshDeps { resolveIdentity?: AnthropicIdentityResolver; intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal; flight?: OAuthRefreshFlightEvidence; replacedStaleFlight?: OAuthRefreshFlightEvidence }
173
175
  interface GenericRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
174
176
  function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${credentialGeneration(c)}`;}
175
177
  function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
@@ -866,6 +868,7 @@ function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OA
866
868
  function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCredentials {
867
869
  return {
868
870
  ...fresh,
871
+ ...mergeAnthropicIdentity(fresh, previous),
869
872
  // Shared: a refresh function returns "local-cli" only when the credential it hands back
870
873
  // still is the local CLI's (Devin re-reading the CLI file, Meta Muse echoing its durable
871
874
  // CLI key). Relabelling that "oauth" would stop the next forced refresh from re-reading it.
@@ -880,12 +883,6 @@ function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCrede
880
883
  }
881
884
  export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const writerGeneration=captureConfigGeneration();const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const row=getAccountCredentialWithStatus(provider,accountId);if(!row)throw new OAuthLoginRequiredError(provider);if(row.paused)throw new OAuthAccountPausedError();const stored=row.credential;const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh,deps.signal),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(error instanceof OAuthMutationBusyError){permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));throw error;}if(!terminal(error))throw error;const failedAt=now();permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),failedAt+XAI_PERMANENT_FAILURE_TTL_MS);sweepExpiredOnWrite(failedAt);await markAccountNeedsReauthIfGeneration(provider,accountId,generation,writerGeneration);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
882
885
 
883
- function newerClaudeCredential(stored: OAuthCredentials, now: number): OAuthCredentials | undefined {
884
- if (stored.source !== "local-cli") return undefined;
885
- const disk = detectClaudeCodeToken();
886
- if (!disk || disk.expires <= now + REFRESH_SKEW_MS) return undefined;
887
- return credentialGeneration(disk) !== credentialGeneration(stored) ? disk : undefined;
888
- }
889
886
 
890
887
  /**
891
888
  * Preserve an already-rotated Nous refresh token (RT-B) after a terminal refresh
@@ -939,33 +936,42 @@ export async function refreshAnthropicAccountWithLock(
939
936
  const now = deps.now ?? Date.now;
940
937
  const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
941
938
  try {
942
- const row = getAccountCredentialWithStatus(provider, accountId);
943
- if (!row) throw new OAuthLoginRequiredError(provider);
944
- if (row.paused) throw new OAuthAccountPausedError();
945
- const stored = row.credential;
946
- const account = getAccountSet(provider)?.accounts.find(candidate => candidate.id === accountId);
939
+ const accountSet = getAccountSet(provider);
940
+ const account = accountSet?.accounts.find(candidate => candidate.id === accountId);
941
+ if (!accountSet || !account) throw new OAuthLoginRequiredError(provider);
942
+ if (account.paused) throw new OAuthAccountPausedError();
943
+ const stored = account.credential;
944
+ const owns = captureAnthropicCredentialOwner(accountSet, accountId);
947
945
  const generation = credentialGeneration(stored);
948
946
  let pendingIntent = readOAuthRefreshIntent(provider, accountId);
949
- const disk = newerClaudeCredential(stored, now());
950
- if (disk) {
951
- const outcome = await mergeAccountCredential(provider, accountId, disk, {
952
- expectedGeneration: credentialGeneration(stored),
947
+ const observed = await newerClaudeCredential(stored, now(), deps.signal, deps.resolveIdentity);
948
+ const assertOwner = (store: AuthStore, checkDisk = true) => {
949
+ if (observed.kind !== "absent" && deps.signal?.aborted) throw new OAuthTokenRefreshStaleError();
950
+ if (!owns(store, checkDisk && observed.kind !== "absent" ? observed.diskGeneration : undefined)) {
951
+ if (store[provider]?.accounts.find(a => a.id === accountId)?.paused) throw new OAuthAccountPausedError();
952
+ throw new OAuthTokenRefreshStaleError();
953
+ }
954
+ };
955
+ assertOwner({ [provider]: getAccountSet(provider)! }, observed.kind !== "adopt");
956
+ if (observed.kind === "adopt") {
957
+ const outcome = await mergeAccountCredential(provider, accountId, observed.credential, {
958
+ expectedGeneration: generation,
953
959
  afterPrePersistRead: deps.afterPrePersistRead,
960
+ assertOwnership: assertOwner,
954
961
  });
955
- if (outcome.superseded) {
956
- // The disk credential is already durable here, so cleanup is secondary: an unlink
957
- // failure must not mask a committed credential by throwing over the return below.
958
- if (pendingIntent) clearAnthropicRefreshIntentBestEffort(provider, accountId, pendingIntent);
959
- if (outcome.stored.expires > now() + REFRESH_SKEW_MS) return outcome.stored.access;
960
- throw new OAuthLoginRequiredError(provider);
961
- }
962
+ // A superseding write did not adopt our candidate. Its pending intent remains owned by it.
963
+ if (outcome.superseded) throw new OAuthTokenRefreshStaleError();
962
964
  if (pendingIntent) clearAnthropicRefreshIntentBestEffort(provider, accountId, pendingIntent);
963
- return disk.access;
965
+ return observed.credential.access;
964
966
  }
967
+ // Unknown rotation may have consumed this refresh token. Refuse without replay or changing health.
968
+ if (observed.kind === "unresolved") throw new OAuthLoginRequiredError(provider);
965
969
  if (pendingIntent?.cleanupPending && pendingIntent.generation === generation) {
966
970
  resumeAnthropicRefreshIntentCleanup(provider, accountId, pendingIntent);
967
971
  pendingIntent = undefined;
968
972
  }
973
+ if (observed.kind === "different"
974
+ && (pendingIntent?.uncertain || pendingIntent?.generation === generation)) throw new OAuthLoginRequiredError(provider);
969
975
  if (!pendingIntent?.uncertain && pendingIntent?.generation === generation) {
970
976
  if (pendingIntent.staleOwner) throw new OAuthTokenRefreshStaleError();
971
977
  if (deps.replacedStaleFlight && pendingIntent.flightId === deps.replacedStaleFlight.flightId) {
@@ -1007,6 +1013,7 @@ export async function refreshAnthropicAccountWithLock(
1007
1013
  const outcome = await mergeAccountCredential(provider, accountId, fresh, {
1008
1014
  expectedGeneration: generation,
1009
1015
  afterPrePersistRead: deps.afterPrePersistRead,
1016
+ assertOwnership: store => { if (!owns(store, undefined, true)) throw new OAuthTokenRefreshStaleError(); },
1010
1017
  });
1011
1018
  if (outcome.superseded) {
1012
1019
  if (attemptIntent) clearAnthropicRefreshIntentBestEffort(provider, accountId, attemptIntent);
@@ -1025,18 +1032,16 @@ export async function refreshAnthropicAccountWithLock(
1025
1032
  // OAuthLoginRequiredError. One 503 locked the account out of refresh until manual
1026
1033
  // re-auth even after upstream recovered.
1027
1034
  //
1028
- // Only clear the intent when the server DEFINITIVELY answered and rejected the
1029
- // request. The adapter attaches an HTTP status only to that explicit non-success
1030
- // response. A timeout, a dropped connection, or an unreadable/unparseable body
1031
- // carries no status: the server may already have
1032
- // rotated the token, and replaying it could trip refresh-token-reuse revocation.
1033
- // Those outcomes keep the intent so the guard still refuses a blind replay.
1034
- if ((!refreshMayHaveReachedProvider || definitivelyAnswered(error)) && attemptIntent) {
1035
+ // Clear only for proven pre-dispatch failure or an explicit HTTP rejection.
1036
+ // Other transport/body failures may follow rotation and keep the replay guard.
1037
+ const requestNotSent = !refreshMayHaveReachedProvider
1038
+ || (error instanceof AnthropicTokenError && error.requestNotSent);
1039
+ if ((requestNotSent || definitivelyAnswered(error)) && attemptIntent) {
1035
1040
  await clearAnthropicRefreshIntentForKnownFailure(
1036
1041
  provider,
1037
1042
  accountId,
1038
1043
  attemptIntent,
1039
- refreshMayHaveReachedProvider ? "definitive-rejection" : "pre-dispatch",
1044
+ requestNotSent ? "pre-dispatch" : "definitive-rejection",
1040
1045
  error,
1041
1046
  );
1042
1047
  }
@@ -1717,7 +1722,7 @@ export async function runLogin(
1717
1722
  });
1718
1723
  } else {
1719
1724
  const saveOptions = {
1720
- preserveIdentityless: opts?.forceLogin === true,
1725
+ preserveIdentityless: opts?.forceLogin === true || (provider === "anthropic" && cred.source === "local-cli"),
1721
1726
  assertBeforePersist: deps.assertCurrentOwner,
1722
1727
  };
1723
1728
  if (shouldRollbackKiroAccounts && !deps.saveCredential) {
@@ -1799,6 +1804,7 @@ export interface OAuthAccountSummary {
1799
1804
  email?: string;
1800
1805
  active: boolean;
1801
1806
  needsReauth?: boolean;
1807
+ needsReauthReason?: "verify_account";
1802
1808
  expiresAt?: number;
1803
1809
  /**
1804
1810
  * Subscription tier, mirroring the field the OpenAI/Codex provider reports, so a consumer
@@ -1835,6 +1841,7 @@ export function getLoginStatus(provider: string, maskEmails = true): { loggedIn:
1835
1841
  email: projectEmail(a.credential.email, maskEmails) ?? undefined,
1836
1842
  active: a.id === set.activeAccountId,
1837
1843
  ...(a.needsReauth ? { needsReauth: true } : {}),
1844
+ ...(a.needsReauth && a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
1838
1845
  expiresAt: a.credential.expires,
1839
1846
  // Explicitly null rather than omitted — see OAuthAccountSummary.plan. No OAuth provider
1840
1847
  // exposes a subscription tier today, so there is nothing truthful to put here; deriving one
@@ -128,3 +128,10 @@ export function detectClaudeCodeToken(): OAuthCredentials | null {
128
128
  if (!raw) return null;
129
129
  return parseClaudeOauthPayload(raw);
130
130
  }
131
+
132
+ /** Shared-token continuity for opaque Claude tokens; fully rotated pairs need authenticated account proof. */
133
+ export function hasClaudeCredentialContinuity(stored: OAuthCredentials, disk: OAuthCredentials): boolean {
134
+ const sameToken = (left: string, right: string): boolean =>
135
+ typeof left === "string" && left.trim().length > 0 && left === right;
136
+ return sameToken(stored.refresh, disk.refresh) || sameToken(stored.access, disk.access);
137
+ }
@@ -50,7 +50,7 @@ export function parseGenericStickyLimit(value: unknown): number | null {
50
50
 
51
51
  /** Fields the unified pool-settings contract can carry, per kind. */
52
52
  export const POOL_SETTINGS_FIELDS = [
53
- "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes",
53
+ "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes", "nativeMessages",
54
54
  ] as const;
55
55
  export type PoolSettingsField = typeof POOL_SETTINGS_FIELDS[number];
56
56
 
@@ -84,8 +84,12 @@ export interface PoolSettingsDto {
84
84
  quotaWindow: string | null;
85
85
  maxConcurrentPerAccount: number | null;
86
86
  routes: AnthropicModelRoute[] | null;
87
+ /** Anthropic preference, default true; null for unsupported pool kinds. */
88
+ nativeMessages: boolean | null;
87
89
  /** Present only when stored Anthropic routes fail validation on read. */
88
90
  routesError?: string;
91
+ /** Saved state confirmed despite a post-publication failure. */
92
+ warning?: "config_bookkeeping_failed";
89
93
  }
90
94
 
91
95
 
@@ -134,7 +138,7 @@ export function genericPoolSettingsDto(
134
138
  /** Which fields each kind actually honours. Declared, never silently omitted. */
135
139
  const SUPPORTED_BY_KIND: Record<PoolSettingsKind, PoolSettingsField[]> = {
136
140
  codex: ["strategy", "stickyLimit", "autoSwitchThreshold"],
137
- anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes"],
141
+ anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes", "nativeMessages"],
138
142
  generic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold"],
139
143
  };
140
144
 
@@ -165,6 +169,7 @@ export function unifiedPoolSettingsDto(
165
169
  quotaWindow: null,
166
170
  maxConcurrentPerAccount: null,
167
171
  routes: null,
172
+ nativeMessages: null,
168
173
  };
169
174
  }
170
175
  if (kind === "anthropic") {
@@ -174,6 +179,7 @@ export function unifiedPoolSettingsDto(
174
179
  ...base,
175
180
  enabled,
176
181
  enabledEffective: enabled === true,
182
+ nativeMessages: !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true,
177
183
  strategy: parseGenericPoolStrategy(pool.strategy) ?? "quota",
178
184
  stickyLimit: parseGenericStickyLimit(pool.stickyLimit) ?? 1,
179
185
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(pool.autoSwitchThreshold) ?? 80,
@@ -198,5 +204,6 @@ export function unifiedPoolSettingsDto(
198
204
  quotaWindow: null,
199
205
  maxConcurrentPerAccount: provider === "kiro" ? parseKiroAccountCap(failover.maxConcurrentPerAccount) : null,
200
206
  routes: null,
207
+ nativeMessages: null,
201
208
  };
202
209
  }
@@ -0,0 +1,2 @@
1
+ /** Refresh early enough to avoid dispatching a bearer that expires in flight. */
2
+ export const REFRESH_SKEW_MS = 60_000;
@@ -19,6 +19,7 @@
19
19
  * distinct one. Kimi extracts JWT `user_id`/`sub` as accountId; Cursor extracts JWT `sub` —
20
20
  * both append distinct identified accounts under multiauth.
21
21
  */
22
+ import { normalizeAnthropicIdentity, matchesAnthropicImport, mergeAnthropicIdentity } from "./anthropic-identity";
22
23
  import { createHash, randomUUID } from "node:crypto";
23
24
  import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
24
25
  import { join } from "node:path";
@@ -34,6 +35,7 @@ import {
34
35
  } from "../lib/state-store-sweeper";
35
36
  import { validateCopilotApiBaseUrl } from "./github-copilot";
36
37
  import { validateDevinApiBaseUrl } from "./devin/api-base";
38
+ import { REFRESH_SKEW_MS } from "./refresh-policy";
37
39
  import { parseAnthropicAccountThreshold } from "./anthropic-account-threshold";
38
40
  import type { OAuthAccountSelection, OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
39
41
 
@@ -531,6 +533,8 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
531
533
  };
532
534
  if (typeof candidate.email === "string" && candidate.email.length > 0) normalized.email = candidate.email;
533
535
  if (typeof candidate.accountId === "string" && candidate.accountId.length > 0) normalized.accountId = candidate.accountId;
536
+ const identity = normalizeAnthropicIdentity(candidate.anthropicIdentity, candidate.access);
537
+ if (identity) normalized.anthropicIdentity = identity;
534
538
  if (isCredentialSource(candidate.source)) normalized.source = candidate.source;
535
539
  if (typeof candidate.projectId === "string" && candidate.projectId.length > 0) normalized.projectId = candidate.projectId;
536
540
  if (typeof candidate.apiBaseUrl === "string" && candidate.apiBaseUrl.length > 0) {
@@ -629,6 +633,9 @@ function normalizeAccount(value: unknown): ProviderAccount | null {
629
633
  if (candidate.paused === true) account.paused = true;
630
634
  const threshold = parseAnthropicAccountThreshold(candidate.autoSwitchThresholdOverride);
631
635
  if (threshold !== null) account.autoSwitchThresholdOverride = threshold;
636
+ if (candidate.needsReauth === true && candidate.needsReauthReason === "verify_account") {
637
+ account.needsReauthReason = "verify_account";
638
+ }
632
639
  if (typeof candidate.addedAt === "number") account.addedAt = candidate.addedAt;
633
640
  if (typeof candidate.loginId === "string"
634
641
  && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate.loginId)) {
@@ -862,9 +869,10 @@ export async function saveCredentialWithReceipt(
862
869
  ): Promise<OAuthCredentialWriteReceipt | null> {
863
870
  const safe = normalizeCredential(cred);
864
871
  if (!safe) return null;
865
- return await mutateStore(store => {
872
+ const receipt = await mutateStore(store => {
866
873
  const set = store[provider];
867
874
  const previousActiveAccountId = set?.activeAccountId;
875
+ const previousSelectionRevision = set?.selectionRevision;
868
876
  const previousAccounts = new Map(
869
877
  set?.accounts.map(account => [account.id, structuredClone(account)]) ?? [],
870
878
  );
@@ -876,11 +884,27 @@ export async function saveCredentialWithReceipt(
876
884
  const id = newAccountId(safe);
877
885
  store[provider] = { activeAccountId: id, accounts: [{ id, credential: safe, addedAt: Date.now() }] };
878
886
  accountId = id;
887
+ } else if (provider === "anthropic" && safe.source === "local-cli") {
888
+ const existing = set.accounts.find(a => matchesAnthropicImport(a.credential, safe));
889
+ if (existing) {
890
+ existing.credential = { ...safe, ...mergeAnthropicIdentity(safe, existing.credential),
891
+ accountId: safe.accountId ?? existing.credential.accountId, email: safe.email ?? existing.credential.email };
892
+ set.selectionRevision = previousSelectionRevision;
893
+ delete existing.needsReauth;
894
+ delete existing.needsReauthReason;
895
+ accountId = existing.id;
896
+ } else {
897
+ const id = distinctAccountId(safe, set.accounts);
898
+ set.accounts.push({ id, credential: safe, addedAt: Date.now() });
899
+ set.activeAccountId = id;
900
+ accountId = id;
901
+ }
879
902
  } else if (identity) {
880
903
  const existing = set.accounts.find(a => (a.credential.accountId ?? a.credential.email) === identity);
881
904
  if (existing) {
882
905
  existing.credential = safe;
883
906
  delete existing.needsReauth;
907
+ delete existing.needsReauthReason;
884
908
  if (existing.paused !== true) set.activeAccountId = existing.id;
885
909
  accountId = existing.id;
886
910
  } else {
@@ -892,6 +916,7 @@ export async function saveCredentialWithReceipt(
892
916
  if (!opts.preserveIdentityless && active && active.credential.accountId === undefined && active.credential.email === undefined) {
893
917
  active.credential = safe;
894
918
  delete active.needsReauth;
919
+ delete active.needsReauthReason;
895
920
  accountId = active.id;
896
921
  } else {
897
922
  const id = distinctAccountId(safe, set.accounts);
@@ -911,6 +936,7 @@ export async function saveCredentialWithReceipt(
911
936
  if (active) {
912
937
  active.credential = safe;
913
938
  delete active.needsReauth;
939
+ delete active.needsReauthReason;
914
940
  accountId = active.id;
915
941
  } else {
916
942
  const id = distinctAccountId(safe, set.accounts);
@@ -944,6 +970,18 @@ export async function saveCredentialWithReceipt(
944
970
  receipt.selectionRevision = store[provider]?.selectionRevision;
945
971
  },
946
972
  });
973
+ // An explicit login retires that account's failover evidence: a cooldown recorded
974
+ // against the old grant must not hold out the fresh credential. Dynamic import
975
+ // keeps the store free of a static edge into the failover module, which reads the
976
+ // store itself.
977
+ try {
978
+ const { clearGenericFailoverHealthForAccount } = await import("./generic-account-failover");
979
+ clearGenericFailoverHealthForAccount(provider, receipt.accountId);
980
+ } catch {
981
+ // The credential write has already committed. A cleanup failure must not
982
+ // surface as a failed login, or the Kiro path rolls the new grant back.
983
+ }
984
+ return receipt;
947
985
  }
948
986
 
949
987
  /** Ordinary callers do not acquire rollback authority merely by saving a credential. */
@@ -1047,6 +1085,7 @@ export async function upsertCredentialByIdentity(
1047
1085
  if (existing && set) {
1048
1086
  existing.credential = safe;
1049
1087
  delete existing.needsReauth;
1088
+ delete existing.needsReauthReason;
1050
1089
  set.activeAccountId ??= existing.id;
1051
1090
  return "updated";
1052
1091
  }
@@ -1149,6 +1188,7 @@ export async function saveAccountCredential(
1149
1188
  account.credential = safe;
1150
1189
  if (opts.rotateLoginId) account.loginId = randomUUID();
1151
1190
  delete account.needsReauth;
1191
+ delete account.needsReauthReason;
1152
1192
  // A pause that found no usable fallback leaves the active id on a paused row. Once this
1153
1193
  // reauthentication makes an unpaused account usable, hand the selection to it.
1154
1194
  const active = set.accounts.find(a => a.id === set.activeAccountId);
@@ -1288,7 +1328,9 @@ export async function setAccountPaused(
1288
1328
  if (paused && set.activeAccountId === accountId) {
1289
1329
  const start = set.accounts.findIndex(candidate => candidate.id === accountId);
1290
1330
  const ring = [...set.accounts.slice(start + 1), ...set.accounts.slice(0, start)];
1291
- const fallback = ring.find(candidate => candidate.paused !== true && candidate.needsReauth !== true);
1331
+ const fallback = ring.find(candidate => candidate.paused !== true && candidate.needsReauth !== true
1332
+ && !(provider === "anthropic" && candidate.credential.source === "local-cli"
1333
+ && candidate.credential.expires <= Date.now() + REFRESH_SKEW_MS));
1292
1334
  if (fallback) {
1293
1335
  set.activeAccountId = fallback.id;
1294
1336
  set.selectionRevision = randomUUID();
@@ -1363,6 +1405,7 @@ export async function replaceProviderAccountSet(
1363
1405
  ...(account.needsReauth ? { needsReauth: true } : {}),
1364
1406
  ...(account.paused ? { paused: true } : {}),
1365
1407
  ...(account.autoSwitchThresholdOverride !== undefined ? { autoSwitchThresholdOverride: account.autoSwitchThresholdOverride } : {}),
1408
+ ...(account.needsReauthReason === "verify_account" ? { needsReauthReason: account.needsReauthReason } : {}),
1366
1409
  ...(account.addedAt !== undefined ? { addedAt: account.addedAt } : {}),
1367
1410
  ...(account.loginId ? { loginId: account.loginId } : {}),
1368
1411
  })),
@@ -1408,18 +1451,24 @@ export async function markAccountNeedsReauth(
1408
1451
  provider: string,
1409
1452
  accountId: string,
1410
1453
  needsReauth: boolean,
1454
+ reason?: "verify_account",
1411
1455
  writerGeneration = captureConfigGeneration(),
1412
1456
  ): Promise<void> {
1413
1457
  if (writerGeneration < lastReconciledGeneration && !liveOAuthAccountKeys.has(oauthAccountKey(provider, accountId))) return;
1414
1458
  await mutateStore(store => {
1415
1459
  const account = store[provider]?.accounts.find(a => a.id === accountId);
1416
1460
  if (!account) return;
1417
- if (needsReauth) account.needsReauth = true;
1418
- else delete account.needsReauth;
1461
+ if (needsReauth) {
1462
+ account.needsReauth = true;
1463
+ if (reason === "verify_account") account.needsReauthReason = reason;
1464
+ } else {
1465
+ delete account.needsReauth;
1466
+ delete account.needsReauthReason;
1467
+ }
1419
1468
  }, [provider, accountId]);
1420
1469
  }
1421
1470
 
1422
- export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;delete account.needsReauth;return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
1471
+ export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>;assertOwnership?: (store: AuthStore) => void}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};opts.assertOwnership?.(store);account.credential=safe;if(account.needsReauthReason!=="verify_account"){delete account.needsReauth;delete account.needsReauthReason;}return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
1423
1472
  // A late refresh failure must not change an operator-paused account's health. Check under
1424
1473
  // the mutation lock, not before awaiting it, so a concurrent pause cannot be overwritten.
1425
- export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration()):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;return true;},[provider,accountId,generation]);}
1474
+ export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration(),reason?:"verify_account"):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||account.paused||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;if(reason==="verify_account")account.needsReauthReason=reason;return true;},[provider,accountId,generation]);}
@@ -62,6 +62,8 @@ export type OAuthCredentials = {
62
62
  email?: string;
63
63
  accountId?: string;
64
64
  source?: OAuthCredentialSource;
65
+ /** Private authenticated account proof; never projected into account summaries. */
66
+ anthropicIdentity?: { v: 1; accountUuid: string; bearerSha256: string };
65
67
  /** Google Antigravity (Cloud Code Assist) discovered project id; injected into the CCA envelope. */
66
68
  projectId?: string;
67
69
  /**
@@ -92,6 +94,13 @@ export interface ProviderAccount {
92
94
  paused?: boolean;
93
95
  /** Anthropic-only usage-switch override; absent inherits its pool default, zero disables it. */
94
96
  autoSwitchThresholdOverride?: number;
97
+ /**
98
+ * Why the account needs reauthentication. `verify_account` means the grant is
99
+ * alive but the provider blocks the account until the human verifies it
100
+ * (Antigravity 403 PERMISSION_DENIED) — a plain re-login without that
101
+ * verification will not help. Absent means an ordinary credential failure.
102
+ */
103
+ needsReauthReason?: "verify_account";
95
104
  addedAt?: number;
96
105
  }
97
106
 
@@ -386,7 +386,8 @@ function createAnthropicMessageWriter(
386
386
  return;
387
387
  }
388
388
  const message = typeof error.message === "string" ? error.message : "upstream request failed";
389
- fail(anthropicFailedStatus(error, message), message, true);
389
+ fail(anthropicFailedStatus(error, message), message, true,
390
+ error.code === "context_length_exceeded" ? error.code : undefined);
390
391
  },
391
392
  overflow() {
392
393
  fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
@@ -413,7 +414,7 @@ export function encodeAnthropicMessageSse(
413
414
  /**
414
415
  * Fold an encoded Messages stream into the non-streaming client response, with the status
415
416
  * mapping the Messages ingress applied to its collected message: a translator overflow is 413,
416
- * a stream that ended in an error frame is 502, anything else unexpected a 502 api_error.
417
+ * a classified context rejection is 400; other error frames/unexpected failures stay 502.
417
418
  */
418
419
  export async function collectAnthropicMessageResponse(
419
420
  stream: ReadableStream<Uint8Array>,
@@ -444,7 +445,7 @@ export async function collectAnthropicMessageResponse(
444
445
  );
445
446
  }
446
447
  return new Response(JSON.stringify(message), {
447
- status: isError ? 502 : 200,
448
+ status: isError ? (translatedError?.code === "context_length_exceeded" ? 400 : 502) : 200,
448
449
  headers: { "Content-Type": "application/json" },
449
450
  });
450
451
  }
@@ -138,7 +138,7 @@ function candidateFor(
138
138
  const reason = rows.effortRow ? "effort-row" : nativeChatDeclineReason(settled, chatBodyForFeatures(features), config);
139
139
  nativeEligible = reason === undefined;
140
140
  if (reason) declineReasons = [reason];
141
- } else if (inbound === "messages" && resolveProtocolSettings(config).rollout.managedMessagesNative) {
141
+ } else if (inbound === "messages" && resolveProtocolSettings(config, settled.providerName).rollout.managedMessagesNative) {
142
142
  // The runtime rule itself. With the switch off nothing is judged, so the default preview
143
143
  // is exactly what it was before the managed native lane existed.
144
144
  // Pinned effort is judged from config and the route; blocked-skill elision and the web-search
@@ -71,19 +71,29 @@ export interface ProtocolSettings {
71
71
  }
72
72
 
73
73
  /** Resolve protocol policy with conservative defaults for every absent or malformed field. */
74
- export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols">): Readonly<ProtocolSettings> {
74
+ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "anthropicAccountPool">, providerName?: string): Readonly<ProtocolSettings> {
75
75
  const raw: unknown = config.protocols;
76
76
  const protocols = isRec(raw) ? raw : {};
77
77
  const rollout = isRec(protocols.rollout) ? protocols.rollout : {};
78
78
  const on = (key: keyof ProtocolRolloutSettings): boolean => rollout[key] === true;
79
- const managedMessagesNative = on("managedMessagesNative");
79
+ // A present malformed container cannot become an absent/default-on policy.
80
+ const validContainers = (raw === undefined || isRec(raw))
81
+ && (!Object.hasOwn(protocols, "rollout") || isRec(protocols.rollout));
82
+ const rawPool: unknown = config.anthropicAccountPool;
83
+ const pool = isRec(rawPool) ? rawPool : {};
84
+ const pooled = providerName === "anthropic" && pool.enabled === true;
85
+ const poolPreference = !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true;
86
+ const nativeOn = (key: "managedMessagesNative" | "managedMessagesNativeOAuth") => validContainers
87
+ && (!pooled || poolPreference)
88
+ && (Object.hasOwn(rollout, key) ? rollout[key] === true : pooled);
89
+ const managedMessagesNative = nativeOn("managedMessagesNative");
80
90
  return Object.freeze({
81
91
  unrepresentable: protocols.unrepresentable === "reject" ? "reject" : "legacy",
82
92
  rollout: Object.freeze({
83
93
  nativeChatCombos: on("nativeChatCombos"),
84
94
  managedMessagesNative,
85
95
  // OAuth extension is meaningless without the key-auth path it extends.
86
- managedMessagesNativeOAuth: managedMessagesNative && on("managedMessagesNativeOAuth"),
96
+ managedMessagesNativeOAuth: managedMessagesNative && nativeOn("managedMessagesNativeOAuth"),
87
97
  directEncoders: on("directEncoders"),
88
98
  shadowPlan: on("shadowPlan"),
89
99
  }),
@@ -95,16 +105,30 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols">): R
95
105
  * dashboard can tell a preview computed under an older policy from a current one. Not a
96
106
  * security boundary; FNV-1a over a canonical JSON projection.
97
107
  */
98
- export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols">): string {
108
+ export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols" | "anthropicAccountPool">): string {
99
109
  const surfaces = resolveApiSurfaceSettings(config);
100
110
  const settings = resolveProtocolSettings(config);
111
+ const anthropicSettings = resolveProtocolSettings(config, "anthropic");
112
+ const state = (record: unknown, key: string) => !isRec(record) || !Object.hasOwn(record, key)
113
+ ? "absent" : record[key] === true ? "true" : record[key] === false ? "false" : "invalid";
114
+ const container = (value: unknown) => value === undefined ? "absent" : isRec(value) ? "object" : "invalid";
115
+ const rawProtocols: unknown = config.protocols;
116
+ const rawRollout = isRec(rawProtocols) ? rawProtocols.rollout : undefined;
101
117
  const canonical = JSON.stringify({
102
118
  messages: [surfaces.messages.enabled, surfaces.messages.source],
103
119
  unrepresentable: settings.unrepresentable,
120
+ nativeInputs: [
121
+ container(rawProtocols), container(rawRollout),
122
+ state(rawRollout, "managedMessagesNative"), state(rawRollout, "managedMessagesNativeOAuth"),
123
+ container(config.anthropicAccountPool), state(config.anthropicAccountPool, "enabled"),
124
+ state(config.anthropicAccountPool, "nativeMessages"),
125
+ ],
104
126
  rollout: [
105
127
  settings.rollout.nativeChatCombos,
106
128
  settings.rollout.managedMessagesNative,
107
129
  settings.rollout.managedMessagesNativeOAuth,
130
+ anthropicSettings.rollout.managedMessagesNative,
131
+ anthropicSettings.rollout.managedMessagesNativeOAuth,
108
132
  settings.rollout.directEncoders,
109
133
  settings.rollout.shadowPlan,
110
134
  ],