@bitkyc08/opencodex 2.80.0 → 2.81.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 (227) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -0,0 +1,41 @@
1
+ /** Ownership captured before a physical send, independent of subsequent cooldown observations. */
2
+ import type { OAuthAccessSnapshot } from "./index";
3
+ import { credentialGeneration, getAccountSet } from "./store";
4
+ import { isAnthropicInstanceId, type AnthropicInstanceId } from "../providers/anthropic-instance-id";
5
+ import { anthropicCooldownRecoveryFor } from "../providers/quota/anthropic-cooldown-recovery";
6
+ import { captureProviderAccountQuotaEpoch } from "../providers/quota/account-cache";
7
+
8
+ export type AnthropicPhysicalSendOwnership = Readonly<{
9
+ provider: AnthropicInstanceId;
10
+ accountId: string;
11
+ accessToken: string;
12
+ generation: string;
13
+ accountIncarnation: number;
14
+ loginId?: string;
15
+ addedAt?: number;
16
+ quotaEpoch: number;
17
+ }>;
18
+
19
+ /** Namespace API: config/target admission remains with the config-aware physical caller. */
20
+ export function captureAnthropicPhysicalSendOwnership(snapshot: OAuthAccessSnapshot): AnthropicPhysicalSendOwnership | null {
21
+ if (!isAnthropicInstanceId(snapshot.provider)) return null;
22
+ const row = getAccountSet(snapshot.provider)?.accounts.find(account => account.id === snapshot.accountId);
23
+ if (!row || row.paused || row.needsReauth || row.credential.access !== snapshot.accessToken
24
+ || credentialGeneration(row.credential) !== snapshot.generation) return null;
25
+ return Object.freeze({
26
+ provider: snapshot.provider, accountId: snapshot.accountId,
27
+ accessToken: snapshot.accessToken, generation: snapshot.generation,
28
+ accountIncarnation: anthropicCooldownRecoveryFor(snapshot.provider).reserveAnthropicAccountIncarnation(snapshot.accountId),
29
+ loginId: row.loginId, addedAt: row.addedAt,
30
+ quotaEpoch: captureProviderAccountQuotaEpoch(snapshot.provider),
31
+ });
32
+ }
33
+
34
+ /** Pure ownership read: never adopt or reserve the replacement account's current incarnation. */
35
+ export function anthropicPhysicalSendOwnershipIsCurrent(owner: AnthropicPhysicalSendOwnership): boolean {
36
+ const row = getAccountSet(owner.provider)?.accounts.find(account => account.id === owner.accountId);
37
+ return !!row && row.loginId === owner.loginId && row.addedAt === owner.addedAt
38
+ && row.credential.access === owner.accessToken && credentialGeneration(row.credential) === owner.generation
39
+ && anthropicCooldownRecoveryFor(owner.provider).anthropicAccountIncarnation(owner.accountId) === owner.accountIncarnation
40
+ && captureProviderAccountQuotaEpoch(owner.provider) === owner.quotaEpoch;
41
+ }
@@ -4,6 +4,8 @@ import { bindAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthro
4
4
  import { generatePKCE } from "./pkce";
5
5
  import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
6
6
  import { outboundProxyConfigured, startupOutboundProxyConfigured } from "../lib/proxy-env";
7
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
8
+ import { AnthropicLocalCliImportError } from "./store-anthropic-instance";
7
9
 
8
10
  const CLIENT_ID = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
9
11
  const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
@@ -178,9 +180,10 @@ export class AnthropicOAuthFlow extends OAuthCallbackFlow {
178
180
 
179
181
  export async function loginAnthropic(
180
182
  ctrl: OAuthController,
181
- opts?: { importLocal?: LocalTokenImportMode },
183
+ opts?: { importLocal?: LocalTokenImportMode; instance?: AnthropicInstanceId },
182
184
  ): Promise<OAuthCredentials> {
183
185
  const importLocal = opts?.importLocal ?? "off";
186
+ if (opts?.instance === "anthropic2" && importLocal !== "off") throw new AnthropicLocalCliImportError();
184
187
  if (importLocal !== "off") {
185
188
  const { detectClaudeCodeToken } = await import("./local-token-detect");
186
189
  const local = detectClaudeCodeToken();
@@ -1,3 +1,4 @@
1
+ import { isBuiltinAnthropicInstanceRow } from "../providers/anthropic-instance";
1
2
  /**
2
3
  * Generic OAuth multi-account 429 failover (#2568).
3
4
  *
@@ -157,7 +158,8 @@ export function kiroAutoSelection(
157
158
 
158
159
  /** True when this provider participates in generic rotation at all. */
159
160
  export function isGenericFailoverProvider(providerName: string, provider: OcxProviderConfig): boolean {
160
- return provider.authMode === "oauth" && !EXCLUDED_PROVIDERS.has(providerName);
161
+ return provider.authMode === "oauth" && !EXCLUDED_PROVIDERS.has(providerName)
162
+ && !isBuiltinAnthropicInstanceRow(providerName, provider);
161
163
  }
162
164
 
163
165
  /**
@@ -1,7 +1,8 @@
1
+ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
1
2
  import type { MainAccountHardLockStatus } from "../codex/main-account-hard-lock";
2
3
  import type { MainAccountExternalUsageWarning } from "../codex/main-account-external-usage";
3
4
  import { getCodexAccountHealthSnapshot, type CodexCooldownSource } from "../codex/routing";
4
- import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
5
+ import { anthropicRoutingFor } from "./anthropic-routing";
5
6
  import { isAccountNeedsReauth } from "../codex/account-runtime-state";
6
7
  import { getCodexAccountCredential, listCodexAccountIds, readCodexAccountRecord } from "../codex/account-store";
7
8
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
@@ -10,7 +11,7 @@ import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability"
10
11
  import { maskAccountId } from "../lib/privacy";
11
12
  import { findLiveProxy } from "../server/proxy-liveness";
12
13
  import { fetchBoundLocalManagementRead } from "../server/local-management-read-client";
13
- import { loadAuthStore, peekAuthStore, peekOAuthRefreshIntent, readOAuthRefreshIntent } from "./store";
14
+ import { accountNeedsReauthForStatus, loadAuthStore, peekAuthStore, peekOAuthRefreshIntent, readOAuthRefreshIntent } from "./store";
14
15
  import type { ProviderAccount } from "./types";
15
16
 
16
17
  export type OAuthAccountHealth =
@@ -207,12 +208,13 @@ export function projectStoredOAuthAccountHealth(
207
208
  now = Date.now(),
208
209
  opts: { observeOnly?: boolean } = {},
209
210
  ): OAuthAccountHealth {
210
- const anthropicSnap = provider === "anthropic"
211
- ? getAnthropicAccountHealthSnapshot(account.id, now)
211
+ const anthropicSnap = isAnthropicInstanceId(provider)
212
+ ? anthropicRoutingFor(provider).getAnthropicAccountHealthSnapshot(account.id, now)
212
213
  : null;
214
+ const needsReauth = accountNeedsReauthForStatus(provider, account, now);
213
215
  return projectOAuthAccountHealth({
214
- needsReauth: account.needsReauth === true,
215
- reauthReason: account.needsReauth === true
216
+ needsReauth,
217
+ reauthReason: needsReauth
216
218
  ? (account.needsReauthReason ?? "refresh_failed")
217
219
  : undefined,
218
220
  cooldownUntilMs: anthropicSnap?.cooldownUntil,
@@ -1,7 +1,7 @@
1
1
  import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
2
2
  import { initializeProviderModelSelection } from "../providers/initial-model-selection";
3
3
  import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
4
- import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfig } from "../config";
4
+ import { getDefaultConfig, initializePersistedConfigIfMissing, loadConfig, mutatePersistedConfig, observeInitialConfigState, saveConfig } from "../config";
5
5
  import { resolveProviderApiKey } from "../providers/key-store";
6
6
  import { projectEmail } from "../lib/privacy";
7
7
  import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
@@ -9,15 +9,13 @@ import {
9
9
  OAuthMutationBusyError,
10
10
  OAuthRefreshIntentIOError,
11
11
  clearOAuthRefreshIntent,
12
- clearOAuthRefreshIntentIfMatch,
13
12
  createOAuthRefreshIntentLock,
14
13
  credentialGeneration,
15
14
  getAccountCredential,
16
15
  getAccountCredentialWithStatus,
17
16
  getAccountSet,
18
17
  getCredential,
19
- markAccountNeedsReauthIfGeneration,
20
- markOAuthRefreshIntentCleanupPending,
18
+ markAccountNeedsReauthIfGeneration, accountNeedsReauthForStatus, markAccountRefreshAttentionIfGeneration,
21
19
  markOAuthRefreshIntentStaleOwner,
22
20
  mergeAccountCredential,
23
21
  normalizeAuthStoreBuffer,
@@ -29,11 +27,15 @@ import {
29
27
  writeOAuthRefreshIntent,
30
28
  type OAuthCredentialWriteReceipt,
31
29
  type OAuthRefreshIntent,
32
- type OAuthRefreshIntentCleanupPending,
33
30
  type AuthStore,
34
31
  } from "./store";
35
32
  import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
36
- import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
33
+ import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError } from "./anthropic";
34
+ import { isAnthropicOAuthInstance, anthropicInstanceRowShapeMatches } from "../providers/anthropic-instance";
35
+ import { captureModelsOAuthTarget, guardModelsOAuthRequest, mayResolveModelsOAuth } from "./model-discovery-auth";
36
+ import { anthropicOAuthDefinition, assertAnthropicInstanceLoginConfig, AnthropicInstanceCollisionError,
37
+ clearAnthropicRefreshIntentBestEffort, clearAnthropicRefreshIntentForKnownFailure,
38
+ resumeAnthropicRefreshIntentCleanup, clearObservedAnthropicRefreshIntent } from "./anthropic-oauth-definitions";
37
39
  import { loginKimi, refreshKimiToken } from "./kimi";
38
40
  import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, RefreshIntentIOError } from "./nous";
39
41
  import { loginChatGPT, refreshChatGPTToken, type ChatGPTLoginFlow } from "./chatgpt";
@@ -83,8 +85,9 @@ export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } fro
83
85
  import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
84
86
 
85
87
  import { REFRESH_SKEW_MS } from "./refresh-policy";
86
- import { newerClaudeCredential, captureAnthropicCredentialOwner } from "./anthropic-continuity";
88
+ import { newerClaudeCredentialForInstance, captureAnthropicCredentialOwnerForInstance } from "./anthropic-continuity";
87
89
  import { mergeAnthropicIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
90
+ import { AnthropicCrossInstanceDuplicateError, AnthropicLocalCliImportError, assertAnthropicCredentialSource } from "./store-anthropic-instance";
88
91
  export interface OAuthAccessSnapshot {
89
92
  provider: string;
90
93
  accountId: string;
@@ -195,7 +198,7 @@ export interface LoginFlowLifecycle {
195
198
  onSettled?: () => void | Promise<void>;
196
199
  }
197
200
 
198
- interface OAuthProviderDef {
201
+ export interface OAuthProviderDef {
199
202
  login(ctrl: OAuthController, opts?: LoginOpts, providerConfig?: OcxProviderConfig): Promise<OAuthCredentials>;
200
203
  refresh(
201
204
  refreshToken: string,
@@ -266,15 +269,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
266
269
  providerConfig: oauthConfig("xai"),
267
270
  defaultModel: oauthDefaultModel("xai"),
268
271
  },
269
- anthropic: {
270
- login: (ctrl, opts) => loginAnthropic(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
271
- refresh: refreshAnthropicToken,
272
- providerConfig: oauthConfig("anthropic"),
273
- defaultModel: oauthDefaultModel("anthropic"),
274
- // Anthropic actively server-side-blocks subscription OAuth outside its own clients (Feb 2026).
275
- // Never generate background refresh traffic for it — grade 20, highest ToS risk.
276
- defaultRefreshPolicy: "disabled",
277
- },
272
+ anthropic: anthropicOAuthDefinition("anthropic"),
273
+ anthropic2: anthropicOAuthDefinition("anthropic2"),
278
274
  kimi: {
279
275
  login: (ctrl) => loginKimi(ctrl),
280
276
  refresh: refreshKimiToken,
@@ -446,8 +442,10 @@ export class OAuthAccountPausedError extends Error {
446
442
  }
447
443
 
448
444
  export class OAuthProviderPublicationError extends Error {
449
- constructor() {
450
- super("OAuth credential was saved, but the provider entry was not written. Resolve the account namespace collision, then retry login.");
445
+ constructor(provider?: string, credentialWritten = true) {
446
+ super(provider !== "anthropic2" ? "OAuth credential was saved, but the provider entry was not written. Resolve the account namespace collision, then retry login."
447
+ : credentialWritten ? "Anthropic Pool 2 credential was saved as an orphan auth row, but the provider entry was not written. Repair the persisted config or resolve the provider/account namespace collision, then retry login."
448
+ : "Anthropic Pool 2 login did not start because the config file is missing or could not be written; no credential was saved. Repair the persisted config, then retry login.");
451
449
  this.name = "OAuthProviderPublicationError";
452
450
  }
453
451
  }
@@ -490,6 +488,9 @@ export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
490
488
  || error instanceof OAuthReauthIdentityUnverifiedError
491
489
  || error instanceof OAuthTokenRefreshBusyError
492
490
  || error instanceof OAuthTokenRefreshStaleError
491
+ || error instanceof AnthropicInstanceCollisionError
492
+ || error instanceof AnthropicCrossInstanceDuplicateError
493
+ || error instanceof AnthropicLocalCliImportError
493
494
  ) return error.message;
494
495
  return "OAuth authentication failed. Check the OpenCodex account status and retry.";
495
496
  }
@@ -739,131 +740,6 @@ function definitivelyAnswered(error: unknown): boolean {
739
740
  return false;
740
741
  }
741
742
 
742
- /**
743
- * Intent cleanup is secondary to the refresh outcome it protects.
744
- *
745
- * Once a credential is already durable, cleanup remains secondary and best-effort. A known
746
- * failed attempt takes the stricter path below: its retry-safe marker must become durable before
747
- * the original provider error can be returned.
748
- */
749
- function clearAnthropicRefreshIntentBestEffort(
750
- provider: string,
751
- accountId: string,
752
- expected: OAuthRefreshIntent,
753
- ): boolean {
754
- try {
755
- return expected.attemptId
756
- ? clearOAuthRefreshIntentIfMatch(provider, accountId, expected)
757
- : clearOAuthRefreshIntent(provider, accountId, expected.generation);
758
- } catch {
759
- console.warn(
760
- "[opencodex] Anthropic refresh intent cleanup failed; preserving the durable replay guard.",
761
- );
762
- return false;
763
- }
764
- }
765
-
766
- const ANTHROPIC_INTENT_MARK_RETRY_DELAYS_MS = [10, 25, 50] as const;
767
-
768
- function isConfigMutationLockContention(error: unknown): boolean {
769
- if (!(error instanceof ConfigMutationLockError)) return false;
770
- const cause = error.cause;
771
- const code = cause && typeof cause === "object" && "code" in cause
772
- ? String((cause as { code?: unknown }).code)
773
- : "";
774
- return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED";
775
- }
776
-
777
- async function clearAnthropicRefreshIntentForKnownFailure(
778
- provider: string,
779
- accountId: string,
780
- expected: OAuthRefreshIntent,
781
- cleanupPending: OAuthRefreshIntentCleanupPending,
782
- refreshError: unknown,
783
- ): Promise<boolean> {
784
- let marked: OAuthRefreshIntent | undefined;
785
- for (let attempt = 0; attempt <= ANTHROPIC_INTENT_MARK_RETRY_DELAYS_MS.length; attempt += 1) {
786
- try {
787
- marked = markOAuthRefreshIntentCleanupPending(
788
- provider,
789
- accountId,
790
- expected,
791
- cleanupPending,
792
- );
793
- break;
794
- } catch (cause) {
795
- const retryDelay = ANTHROPIC_INTENT_MARK_RETRY_DELAYS_MS[attempt];
796
- if (!isConfigMutationLockContention(cause) || retryDelay === undefined) {
797
- throw new OAuthRefreshIntentIOError(
798
- "mark-cleanup-pending",
799
- cause,
800
- refreshError,
801
- );
802
- }
803
- // The provider has definitively answered, so caller cancellation no longer changes the
804
- // settlement obligation. Yield briefly while retaining the per-account refresh lock, then
805
- // rerun the existing compare-and-swap marker against current disk state.
806
- await Bun.sleep(retryDelay);
807
- }
808
- }
809
- if (!marked) {
810
- throw new OAuthRefreshIntentIOError(
811
- "mark-cleanup-pending",
812
- new Error("Anthropic refresh intent changed before safe cleanup"),
813
- refreshError,
814
- );
815
- }
816
-
817
- let cleared: boolean;
818
- try {
819
- cleared = clearOAuthRefreshIntentIfMatch(provider, accountId, marked);
820
- } catch {
821
- console.warn(
822
- "[opencodex] Anthropic refresh intent cleanup failed; retry-safe cleanup remains pending.",
823
- );
824
- return false;
825
- }
826
- if (!cleared) {
827
- throw new OAuthRefreshIntentIOError(
828
- "clear-cleanup-pending",
829
- new Error("Anthropic refresh intent changed during safe cleanup"),
830
- refreshError,
831
- );
832
- }
833
- return true;
834
- }
835
-
836
- function resumeAnthropicRefreshIntentCleanup(
837
- provider: string,
838
- accountId: string,
839
- pendingIntent: OAuthRefreshIntent,
840
- ): void {
841
- let cleared: boolean;
842
- try {
843
- cleared = clearOAuthRefreshIntentIfMatch(provider, accountId, pendingIntent);
844
- } catch (cause) {
845
- throw new OAuthRefreshIntentIOError(
846
- "resume-cleanup",
847
- cause,
848
- );
849
- }
850
- if (!cleared) {
851
- throw new OAuthRefreshIntentIOError(
852
- "resume-cleanup",
853
- new Error("Pending Anthropic refresh intent changed before cleanup"),
854
- );
855
- }
856
- }
857
-
858
- function clearObservedAnthropicRefreshIntent(
859
- provider: string,
860
- accountId: string,
861
- pendingIntent: OAuthRefreshIntent,
862
- ): boolean {
863
- return pendingIntent.attemptId
864
- ? clearOAuthRefreshIntentIfMatch(provider, accountId, pendingIntent)
865
- : clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
866
- }
867
743
  function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OAuthCredentials{if(stored.source!=="local-cli")return stored;const disk=detectGrokCliToken();if(!disk)return stored;const allowed=isSameGrokIdentity(stored,disk)||(active&&!hasComparableGrokIdentity(stored,disk));return allowed&&shouldAdoptGrokGeneration(stored,disk,now(),REFRESH_SKEW_MS)?disk:stored;}
868
744
  function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCredentials {
869
745
  return {
@@ -932,6 +808,7 @@ export async function refreshAnthropicAccountWithLock(
932
808
  callerCredential: OAuthCredentials,
933
809
  deps: AnthropicRefreshDeps = {},
934
810
  ): Promise<string> {
811
+ if (!isAnthropicOAuthInstance(provider)) throw new UnsupportedOAuthProviderError(provider);
935
812
  const writerGeneration = captureConfigGeneration();
936
813
  const now = deps.now ?? Date.now;
937
814
  const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
@@ -941,10 +818,10 @@ export async function refreshAnthropicAccountWithLock(
941
818
  if (!accountSet || !account) throw new OAuthLoginRequiredError(provider);
942
819
  if (account.paused) throw new OAuthAccountPausedError();
943
820
  const stored = account.credential;
944
- const owns = captureAnthropicCredentialOwner(accountSet, accountId);
821
+ const owns = captureAnthropicCredentialOwnerForInstance(provider, accountSet, accountId);
945
822
  const generation = credentialGeneration(stored);
946
823
  let pendingIntent = readOAuthRefreshIntent(provider, accountId);
947
- const observed = await newerClaudeCredential(stored, now(), deps.signal, deps.resolveIdentity);
824
+ const observed = await newerClaudeCredentialForInstance(provider, stored, now(), deps.signal, deps.resolveIdentity);
948
825
  const assertOwner = (store: AuthStore, checkDisk = true) => {
949
826
  if (observed.kind !== "absent" && deps.signal?.aborted) throw new OAuthTokenRefreshStaleError();
950
827
  if (!owns(store, checkDisk && observed.kind !== "absent" ? observed.diskGeneration : undefined)) {
@@ -1116,6 +993,10 @@ export async function refreshGenericAccountWithLock(
1116
993
  return fresh.access;
1117
994
  } catch (error) {
1118
995
  if (error instanceof OAuthMutationBusyError) throw error;
996
+ if (provider === "kiro" && error instanceof KiroTokenRefreshError && error.refreshAttention) {
997
+ await markAccountRefreshAttentionIfGeneration(provider, accountId, generation, writerGeneration);
998
+ throw error;
999
+ }
1119
1000
  if (!terminal(error)) throw error;
1120
1001
  // Nous-specific failure-atomicity: a terminal refresh error that carries
1121
1002
  // an already-issued rotated refresh token (e.g. the access JWT lacked the
@@ -1177,20 +1058,22 @@ async function refreshAndPersistAccessToken(
1177
1058
  replacedStaleFlight?: OAuthRefreshFlightEvidence,
1178
1059
  ): Promise<string> {
1179
1060
  if (provider === "xai") return refreshXaiAccountWithLock(provider, accountId, def, cred, { signal });
1180
- if (provider === "anthropic") return refreshAnthropicAccountWithLock(provider, accountId, def, cred, { signal, flight, replacedStaleFlight });
1061
+ if (isAnthropicOAuthInstance(provider)) return refreshAnthropicAccountWithLock(provider, accountId, def, cred, { signal, flight, replacedStaleFlight });
1181
1062
  return refreshGenericAccountWithLock(provider, accountId, def, cred, { signal });
1182
1063
  }
1183
1064
 
1184
- /**
1185
- * Shared bearer-token resolver for /models listing — used by BOTH server.ts:fetchAllModels and
1186
- * codex-catalog.ts:fetchProviderModels so OAuth providers' models are listed once logged in.
1187
- * Returns undefined for forward-mode or oauth-not-logged-in (caller skips).
1188
- */
1065
+ /** Resolve discovery credentials only for the caller-owned configured OAuth row. */
1066
+ export async function getModelsOAuthAccessSnapshot(name: string, prov: OcxProviderConfig | undefined): Promise<OAuthAccessSnapshot> {
1067
+ if (!mayResolveModelsOAuth(name, prov)) throw new OAuthLoginRequiredError(name);
1068
+ return getValidAccessTokenSnapshot(name);
1069
+ }
1070
+
1071
+ /** Shared /models auth resolver; custom key rows keep their own key, including omitted authMode. */
1189
1072
  export async function resolveModelsAuthToken(name: string, prov: OcxProviderConfig): Promise<string | undefined> {
1190
1073
  if (prov.authMode === "forward") return undefined;
1191
1074
  if (prov.authMode === "oauth") {
1192
1075
  try {
1193
- return await getValidAccessToken(name);
1076
+ return (await getModelsOAuthAccessSnapshot(name, prov)).accessToken;
1194
1077
  } catch {
1195
1078
  return undefined;
1196
1079
  }
@@ -1235,6 +1118,8 @@ export function buildModelsRequest(
1235
1118
  providerName = "",
1236
1119
  observedAuth?: ModelsRequestObservedAuth,
1237
1120
  ): { method?: "POST"; url: string; headers: Record<string, string> } {
1121
+ const authorizedTarget = captureModelsOAuthTarget(providerName, prov);
1122
+ if (prov.authMode === "oauth" && !mayResolveModelsOAuth(providerName, prov)) apiKey = undefined;
1238
1123
  const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
1239
1124
  const copilotApiBaseUrl = observedAuth === undefined
1240
1125
  ? (providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined)
@@ -1261,16 +1146,18 @@ export function buildModelsRequest(
1261
1146
  effectiveProvider.baseUrl,
1262
1147
  defaultUrl,
1263
1148
  );
1149
+ const guarded = <T extends { url: string; headers: Record<string, string> }>(request: T): T =>
1150
+ guardModelsOAuthRequest(providerName, prov, request, authorizedTarget);
1264
1151
  if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") {
1265
1152
  headers.Accept = "application/json";
1266
1153
  headers["Content-Type"] = "application/json";
1267
1154
  headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
1268
1155
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
1269
- return {
1270
- method: "POST",
1156
+ return guarded({
1157
+ method: "POST" as const,
1271
1158
  url: discoveryUrl(`${effectiveProvider.baseUrl.replace(/\/+$/, "")}/v1internal:fetchAvailableModels`),
1272
1159
  headers,
1273
- };
1160
+ });
1274
1161
  }
1275
1162
  if (effectiveGoogleMode(providerName, effectiveProvider) === "ai-studio") {
1276
1163
  // Generative Language API: API key goes in x-goog-api-key (never Authorization: Bearer),
@@ -1278,7 +1165,7 @@ export function buildModelsRequest(
1278
1165
  // enough to list everything without a pageToken loop. Vertex/antigravity keep the
1279
1166
  // generic branch (they fall back to their static model lists).
1280
1167
  if (apiKey) headers["x-goog-api-key"] = apiKey;
1281
- return { url: discoveryUrl(`${effectiveProvider.baseUrl}/v1beta/models?pageSize=1000`), headers };
1168
+ return guarded({ url: discoveryUrl(`${effectiveProvider.baseUrl}/v1beta/models?pageSize=1000`), headers });
1282
1169
  }
1283
1170
  if (effectiveProvider.adapter === "anthropic") {
1284
1171
  const base = effectiveProvider.baseUrl.replace(/\/v1\/?$/, "");
@@ -1290,10 +1177,10 @@ export function buildModelsRequest(
1290
1177
  if (effectiveProvider.apiKeyTransport === "bearer") headers["Authorization"] = `Bearer ${apiKey}`;
1291
1178
  else headers["x-api-key"] = apiKey;
1292
1179
  }
1293
- return { url: discoveryUrl(`${base}/v1/models?limit=1000`), headers };
1180
+ return guarded({ url: discoveryUrl(`${base}/v1/models?limit=1000`), headers });
1294
1181
  }
1295
1182
  if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
1296
- return { url: discoveryUrl(providerModelsUrl(effectiveProvider.baseUrl)), headers };
1183
+ return guarded({ url: discoveryUrl(providerModelsUrl(effectiveProvider.baseUrl)), headers });
1297
1184
  }
1298
1185
 
1299
1186
  /**
@@ -1452,6 +1339,7 @@ function projectOAuthProviderReconciliation(config: OcxConfig): OAuthReconcilePr
1452
1339
  const touchedAntigravityVersion = projected.googleAntigravityStaticCatalogVersion !== beforeAntigravityVersion;
1453
1340
 
1454
1341
  for (const [name, prov] of Object.entries(projected.providers)) {
1342
+ if (name === "anthropic2" && !anthropicInstanceRowShapeMatches(name, prov)) continue;
1455
1343
  const beforeProvider = JSON.stringify(prov);
1456
1344
  const def = OAUTH_PROVIDERS[name];
1457
1345
  if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
@@ -1588,6 +1476,7 @@ const OAUTH_LOGIN_OWNED_PROVIDER_FIELDS = [
1588
1476
 
1589
1477
  /** Add/refresh only an OAuth provider's login-owned config fields (does not persist). */
1590
1478
  export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
1479
+ assertAnthropicInstanceLoginConfig(config, provider);
1591
1480
  if (provider === "chatgpt") return;
1592
1481
  const def = OAUTH_PROVIDERS[provider];
1593
1482
  if (!def) return;
@@ -1656,6 +1545,7 @@ interface RunLoginDeps {
1656
1545
  saveAccountCredential?: typeof saveAccountCredential;
1657
1546
  loadConfig?: typeof loadConfig;
1658
1547
  saveConfig?: typeof saveConfig;
1548
+ mutatePersistedConfig?: typeof mutatePersistedConfig;
1659
1549
  settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
1660
1550
  rollbackCredentialWrite?: typeof rollbackCredentialWriteIfMatch;
1661
1551
  assertCurrentOwner?: () => void;
@@ -1672,14 +1562,28 @@ export async function runLogin(
1672
1562
  if (!def) throw new UnsupportedOAuthProviderError(provider);
1673
1563
  const loadLatestConfig = deps.loadConfig ?? loadConfig;
1674
1564
  const saveLatestConfig = deps.saveConfig ?? saveConfig;
1675
- const preflightConfig = provider !== "chatgpt" ? loadLatestConfig() : undefined;
1565
+ const initialBConfigState = provider === "anthropic2" ? observeInitialConfigState() : undefined;
1566
+ let preflightConfig = provider !== "chatgpt" ? loadLatestConfig() : undefined;
1567
+ if (provider === "anthropic2" && preflightConfig && initialBConfigState === "missing") {
1568
+ // Explicit first-login onboarding may create the ordinary defaults, without a B row.
1569
+ // The create-only owner never replaces a concurrent file. Reload its winner before login;
1570
+ // once the browser starts, later deletion remains a publication refusal, not recreation.
1571
+ if (initializePersistedConfigIfMissing(getDefaultConfig()) === "invalid") {
1572
+ throw new OAuthProviderPublicationError(provider, false);
1573
+ }
1574
+ preflightConfig = loadLatestConfig();
1575
+ }
1676
1576
  if (preflightConfig) {
1577
+ assertAnthropicInstanceLoginConfig(preflightConfig, provider);
1677
1578
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(
1678
1579
  preflightConfig.codexAccountNamespaces,
1679
1580
  provider,
1680
1581
  );
1681
1582
  if (namespaceCollision) throw new Error(namespaceCollision);
1682
1583
  }
1584
+ if (provider === "anthropic2" && observeInitialConfigState() !== "exists") {
1585
+ throw new OAuthProviderPublicationError(provider, false);
1586
+ }
1683
1587
  // loginKiro keys its pending CLI-session transaction by object identity. Keep this exact object
1684
1588
  // for settlement even when source normalization below creates a derived credential object.
1685
1589
  const shouldRollbackKiroAccounts = provider === "kiro" && opts?.forceLogin === true;
@@ -1693,6 +1597,7 @@ export async function runLogin(
1693
1597
  }
1694
1598
  const rawCred = await def.login(ctrl, opts, loginProviderConfig);
1695
1599
  const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
1600
+ if (isAnthropicOAuthInstance(provider)) assertAnthropicCredentialSource(provider, cred);
1696
1601
  const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
1697
1602
  try {
1698
1603
  deps.assertCurrentOwner?.();
@@ -1738,16 +1643,28 @@ export async function runLogin(
1738
1643
  if (provider !== "chatgpt") {
1739
1644
  // Re-run against post-credential state so same-provider API-key additions, removals,
1740
1645
  // and active-key switches survive. A late namespace claim wins over provider creation.
1741
- const latestConfig = loadLatestConfig();
1742
- const lateCollision = codexAccountNamespaceProviderCollisionError(
1743
- latestConfig.codexAccountNamespaces,
1744
- provider,
1745
- );
1746
- if (lateCollision) {
1747
- throw new OAuthProviderPublicationError();
1646
+ if (provider === "anthropic2") {
1647
+ const publication = (deps.mutatePersistedConfig ?? mutatePersistedConfig)(latestConfig => {
1648
+ assertAnthropicInstanceLoginConfig(latestConfig, provider, true);
1649
+ if (codexAccountNamespaceProviderCollisionError(latestConfig.codexAccountNamespaces, provider)) {
1650
+ throw new OAuthProviderPublicationError(provider);
1651
+ }
1652
+ upsertOAuthProvider(latestConfig, provider);
1653
+ return { changed: true, value: undefined };
1654
+ });
1655
+ if (publication.status === "unavailable") throw new OAuthProviderPublicationError(provider);
1656
+ } else {
1657
+ const latestConfig = loadLatestConfig();
1658
+ const lateCollision = codexAccountNamespaceProviderCollisionError(
1659
+ latestConfig.codexAccountNamespaces,
1660
+ provider,
1661
+ );
1662
+ if (lateCollision) {
1663
+ throw new OAuthProviderPublicationError();
1664
+ }
1665
+ upsertOAuthProvider(latestConfig, provider);
1666
+ saveLatestConfig(latestConfig);
1748
1667
  }
1749
- upsertOAuthProvider(latestConfig, provider);
1750
- saveLatestConfig(latestConfig);
1751
1668
  }
1752
1669
  } catch (error) {
1753
1670
  const errors: unknown[] = [error];
@@ -1840,7 +1757,7 @@ export function getLoginStatus(provider: string, maskEmails = true): { loggedIn:
1840
1757
  ...(a.alias ? { alias: a.alias } : {}),
1841
1758
  email: projectEmail(a.credential.email, maskEmails) ?? undefined,
1842
1759
  active: a.id === set.activeAccountId,
1843
- ...(a.needsReauth ? { needsReauth: true } : {}),
1760
+ ...(accountNeedsReauthForStatus(provider, a) ? { needsReauth: true } : {}),
1844
1761
  ...(a.needsReauth && a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
1845
1762
  expiresAt: a.credential.expires,
1846
1763
  // Explicitly null rather than omitted — see OAuthAccountSummary.plan. No OAuth provider
@@ -1852,11 +1769,11 @@ export function getLoginStatus(provider: string, maskEmails = true): { loggedIn:
1852
1769
 
1853
1770
  // A stored credential counts as "logged in" when it exists and is not marked for
1854
1771
  // re-authentication. An expired access token with a valid refresh token is still
1855
- // logged in: request resolution refreshes expired/near-expiry credentials lazily.
1772
+ // logged in unless refresh-attention evidence applies; refresh eligibility stays internal.
1856
1773
  // Invalid/unknown local-import expiries are handled at parse/adoption time
1857
1774
  // (local-token-detect.ts), never by over-reporting login state here.
1858
- const activeNeedsReauth = set?.accounts
1859
- .find(a => a.id === set.activeAccountId)?.needsReauth === true;
1775
+ const activeAccount = set?.accounts.find(a => a.id === set.activeAccountId);
1776
+ const activeNeedsReauth = activeAccount && accountNeedsReauthForStatus(provider, activeAccount);
1860
1777
  return {
1861
1778
  loggedIn: !!cred && !activeNeedsReauth,
1862
1779
  email: projectEmail(cred?.email, maskEmails) ?? undefined,
package/src/oauth/kiro.ts CHANGED
@@ -60,6 +60,7 @@ export class KiroTokenRefreshError extends Error {
60
60
  constructor(
61
61
  readonly httpStatus: number,
62
62
  readonly oauthError?: string,
63
+ readonly refreshAttention = false,
63
64
  ) {
64
65
  super(`Kiro token refresh failed: ${httpStatus}${oauthError ? ` (${oauthError})` : ""}`);
65
66
  this.name = "KiroTokenRefreshError";
@@ -530,17 +531,21 @@ export function resolveKiroRequestProfile(
530
531
  : { profileArn: undefined, builderIdFallback: false };
531
532
  }
532
533
 
533
- async function kiroTokenRefreshError(response: Response): Promise<KiroTokenRefreshError> {
534
+ async function kiroTokenRefreshError(response: Response, awsSsoRefresh = false): Promise<KiroTokenRefreshError> {
534
535
  let oauthError: string | undefined;
536
+ let refreshAttention = false;
535
537
  try {
536
538
  const payload = await response.json() as { error?: unknown };
539
+ // This input-error code is attention evidence only for our AWS SSO refresh request,
540
+ // never a terminal grant verdict. Do not surface arbitrary upstream descriptions.
541
+ refreshAttention = awsSsoRefresh && response.status === 400 && payload.error === "invalid_request";
537
542
  if (typeof payload.error === "string" && KIRO_TERMINAL_REFRESH_ERRORS.has(payload.error)) {
538
543
  oauthError = payload.error;
539
544
  }
540
545
  } catch {
541
546
  // Error bodies are untrusted and intentionally excluded from the surfaced message.
542
547
  }
543
- return new KiroTokenRefreshError(response.status, oauthError);
548
+ return new KiroTokenRefreshError(response.status, oauthError, refreshAttention);
544
549
  }
545
550
 
546
551
  async function readTokenResponse(res: Response, oldRefresh: string): Promise<OAuthCredentials> {
@@ -608,7 +613,7 @@ async function refreshAwsSsoOidcToken(
608
613
  signal: kiroRefreshSignal(signal),
609
614
  });
610
615
  const res = await run(refresh);
611
- if (!res.ok) throw await kiroTokenRefreshError(res);
616
+ if (!res.ok) throw await kiroTokenRefreshError(res, true);
612
617
  return readTokenResponse(res, refresh);
613
618
  }
614
619
 
@@ -668,11 +673,19 @@ export async function refreshKiroToken(
668
673
  const local = matchingRotatedKiroCliCredential(refresh, credential);
669
674
  if (!local) throw error;
670
675
  const retryMetadata = metadataForRotatedKiroCliCredential(credential, local);
671
- const fresh = await refreshAwsSsoOidcToken(local.refresh, signal, {
672
- ...credential,
673
- refresh: local.refresh,
674
- kiro: retryMetadata,
675
- });
676
- return { ...fresh, kiro: retryMetadata };
676
+ try {
677
+ const fresh = await refreshAwsSsoOidcToken(local.refresh, signal, {
678
+ ...credential,
679
+ refresh: local.refresh,
680
+ kiro: retryMetadata,
681
+ });
682
+ return { ...fresh, kiro: retryMetadata };
683
+ } catch (recoveryError) {
684
+ // A failed CLI fallback may use the desktop endpoint. Retain the original AWS SSO
685
+ // attention evidence on another input rejection; transient recovery stays retryable.
686
+ if (error.refreshAttention && recoveryError instanceof KiroTokenRefreshError
687
+ && recoveryError.httpStatus === 400 && recoveryError.oauthError === undefined) throw error;
688
+ throw recoveryError;
689
+ }
677
690
  }
678
691
  }