-
Notifications
You must be signed in to change notification settings - Fork 19
fix(custody): credential-presence gates and reported-failed version refusal #232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8cb9a24
e6bb27d
a0b6d47
964973f
29f25b9
210fd6b
b843e6e
0f87166
e2fedbd
8772d86
2df9d0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -510,6 +510,8 @@ export type AccountRefreshError = { | |
| const DEFAULT_FALLBACK_ON = [401, 403, 429] | ||
| const MIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240 | ||
| const DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_REFRESH_BEFORE_EXPIRY_MINUTES | ||
| // Claustrum requests extra headroom beyond the local refresh threshold. | ||
| export const VAULT_REFRESH_HEADROOM_MINUTES = 30 | ||
| const DEFAULT_REFRESH_INTERVAL_MINUTES = 10 | ||
| const MIN_REFRESH_RETRY_DELAY_MS = 5 * 60_000 | ||
| const MAX_REFRESH_RETRY_DELAY_MS = 60 * 60_000 | ||
|
|
@@ -3353,6 +3355,12 @@ export function getRefreshBeforeExpiryMs(storage: AccountStorage | null) { | |
| return refreshBeforeExpiryMs(storage) | ||
| } | ||
|
|
||
| export function getVaultRefreshMinTtlMs(storage: AccountStorage | null) { | ||
| return ( | ||
| getRefreshBeforeExpiryMs(storage) + VAULT_REFRESH_HEADROOM_MINUTES * 60_000 | ||
| ) | ||
| } | ||
|
|
||
| export function getRefreshIntervalMs(storage: AccountStorage | null) { | ||
| const minutes = | ||
| storage?.refresh?.intervalMinutes ?? DEFAULT_REFRESH_INTERVAL_MINUTES | ||
|
|
@@ -4119,9 +4127,15 @@ function canUseCachedQuotaAfterRefreshError( | |
| storage: AccountStorage | null, | ||
| error: unknown, | ||
| now: number, | ||
| vaultServed: boolean, | ||
| ) { | ||
| return ( | ||
| Boolean(account.access && account.expires && account.expires > now) && | ||
| // Cached quota remains attributable after a transient failure when either | ||
| // the local credential is live or a live Claustrum binding serves it. | ||
| Boolean( | ||
| (account.access && account.expires && account.expires > now) || | ||
| vaultServed, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a non-scoped vault binding starts serving a different Anthropic identity, the account UUID is updated without clearing the existing quota snapshot. If the next quota refresh fails transiently, this new vault-presence branch accepts that old snapshot without checking its identity or authentication lineage. The replacement account can therefore inherit the previous account’s quota decision, either admitting an exhausted account or incorrectly blocking a healthy one. Clear or identity-fence the persisted snapshot before using vault presence to justify cached-quota recovery. Knowledge Base Used: |
||
| ) && | ||
| isTransientQuotaError(error) && | ||
| quotaSnapshotPassesPolicy(account.quota, storage) && | ||
| cachedQuotaSnapshotStillRelevant(account.quota, now) | ||
|
|
@@ -4642,8 +4656,9 @@ export class FallbackAccountManager { | |
|
|
||
| for (const account of storage.accounts) { | ||
| if (account.enabled === false || !isOAuthAccount(account)) continue | ||
| const vaultServed = this.isFallbackAccountVaultServed(account.id, storage) | ||
| if (this.isFallbackAccountVaultEnabled(account.id, storage)) { | ||
| if (!this.isFallbackAccountVaultServed(account.id, storage)) continue | ||
| if (!vaultServed) continue | ||
| if ( | ||
| hasNoLocalCredential(account) && | ||
| !storage.quota?.minimumRemaining && | ||
|
|
@@ -4658,7 +4673,7 @@ export class FallbackAccountManager { | |
| if ( | ||
| tokenNeedsRefresh(next, storage, this.now()) && | ||
| !this.isFallbackAccountVaultEnabled(next.id, storage) && | ||
| !this.isFallbackAccountVaultServed(next.id, storage) | ||
| !vaultServed | ||
| ) { | ||
| const refreshError = next.lastRefreshError | ||
| if ( | ||
|
|
@@ -4711,7 +4726,13 @@ export class FallbackAccountManager { | |
| usable.push(next) | ||
| } catch (error) { | ||
| if ( | ||
| canUseCachedQuotaAfterRefreshError(next, storage, error, this.now()) | ||
| canUseCachedQuotaAfterRefreshError( | ||
| next, | ||
| storage, | ||
| error, | ||
| this.now(), | ||
| this.isFallbackAccountVaultServed(next.id, storage), | ||
| ) | ||
| ) { | ||
| log( | ||
| '[refresh] fallback quota using cached quota after refresh error', | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.