From 44cceb12a6b35e07c7db97ea416b37c9845e843d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 12 Jun 2026 16:42:12 -0700 Subject: [PATCH] Use managed Claude credentials for selected usage --- src/main/rate-limits/claude-fetcher.test.ts | 44 +++++++++++++++++++ src/main/rate-limits/claude-fetcher.ts | 48 +++++++++++++++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/main/rate-limits/claude-fetcher.test.ts b/src/main/rate-limits/claude-fetcher.test.ts index e722455374a..0cb4ba665cd 100644 --- a/src/main/rate-limits/claude-fetcher.test.ts +++ b/src/main/rate-limits/claude-fetcher.test.ts @@ -167,6 +167,50 @@ describe('fetchClaudeRateLimits', () => { ) }) + it('uses selected managed account credentials before stale runtime Keychain credentials', async () => { + const configDir = '/Users/test/.claude' + const authPreparation: ClaudeRuntimeAuthPreparation = { + configDir, + envPatch: {}, + stripAuthEnv: true, + provenance: 'managed:account-1' + } + vi.mocked(readManagedClaudeKeychainCredentials).mockResolvedValue( + JSON.stringify({ + claudeAiOauth: { + accessToken: 'managed-oauth-token', + expiresAt: Date.now() + 60_000 + } + }) + ) + vi.mocked(readActiveClaudeKeychainCredentialsStrict).mockResolvedValue( + JSON.stringify({ + claudeAiOauth: { + accessToken: 'stale-runtime-oauth-token', + expiresAt: Date.now() + 60_000 + } + }) + ) + + await expect(fetchClaudeRateLimits({ authPreparation })).resolves.toMatchObject({ + provider: 'claude', + status: 'ok', + session: { usedPercent: 12 }, + weekly: { usedPercent: 34 } + }) + + expect(readManagedClaudeKeychainCredentials).toHaveBeenCalledWith('account-1') + expect(readActiveClaudeKeychainCredentialsStrict).not.toHaveBeenCalled() + expect(netFetchMock).toHaveBeenCalledWith( + 'https://api.anthropic.com/api/oauth/usage', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer managed-oauth-token' + }) + }) + ) + }) + it('falls back to the credentials file when Keychain access fails', async () => { const configDir = '/Users/test/.claude' const authPreparation: ClaudeRuntimeAuthPreparation = { diff --git a/src/main/rate-limits/claude-fetcher.ts b/src/main/rate-limits/claude-fetcher.ts index 8ec9b9642a4..cd5a0793bc6 100644 --- a/src/main/rate-limits/claude-fetcher.ts +++ b/src/main/rate-limits/claude-fetcher.ts @@ -140,6 +140,29 @@ async function readCredentialsFromStrictKeychain( } } +function getManagedAccountIdFromProvenance(provenance?: string): string | null { + const match = provenance?.match(/^managed:([^:]+)(?::|$)/) + return match?.[1] ?? null +} + +async function readManagedCredentialsFromProvenance( + provenance?: string +): Promise { + if (process.platform !== 'darwin') { + return emptyOAuthCredentialReadResult() + } + const accountId = getManagedAccountIdFromProvenance(provenance) + if (!accountId) { + return emptyOAuthCredentialReadResult() + } + try { + const raw = await readManagedClaudeKeychainCredentials(accountId) + return raw ? parseOAuthCredentialsJson(raw) : emptyOAuthCredentialReadResult() + } catch { + return emptyOAuthCredentialReadResult() + } +} + /** * Read OAuth token from ~/.claude/.credentials.json (legacy path). * Why: older Claude CLI versions store credentials in this plain JSON @@ -161,8 +184,22 @@ async function readFromCredentialsFile(configDir?: string): Promise { - // 1. macOS Keychain (Claude Max/Pro OAuth) +async function readOAuthCredentials( + configDir?: string, + provenance?: string +): Promise { + // 1. Orca managed-account storage. The shared Claude runtime Keychain can + // drift when live Claude sessions refresh or rewrite auth, but the status bar + // must report usage for the account Orca has selected. + const fromManagedAccount = await readManagedCredentialsFromProvenance(provenance) + if (fromManagedAccount.token) { + return fromManagedAccount + } + if (fromManagedAccount.hasRefreshableCredentials) { + return fromManagedAccount + } + + // 2. macOS Keychain (Claude Max/Pro OAuth) const fromKeychain = await readFromKeychain(configDir) if (fromKeychain.token) { return fromKeychain @@ -171,7 +208,7 @@ async function readOAuthCredentials(configDir?: string): Promise