From 49b484a8f0bcb9609c60aaac76db221f0736fa29 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 31 Aug 2026 18:29:06 -0700 Subject: [PATCH] fix(claude-auth): preserve legacy shared runtime compatibility --- .../claude-accounts/runtime-auth-service.ts | 32 +++++++++++++-- .../runtime-auth/runtime-auth-preparation.ts | 8 ++-- .../runtime-auth/runtime-auth-readback.ts | 7 +++- .../runtime-auth/runtime-auth-sync.ts | 41 ++++++++++++++++--- 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/main/claude-accounts/runtime-auth-service.ts b/src/main/claude-accounts/runtime-auth-service.ts index f304093d550..79b4fbcfe43 100644 --- a/src/main/claude-accounts/runtime-auth-service.ts +++ b/src/main/claude-accounts/runtime-auth-service.ts @@ -14,8 +14,9 @@ export class ClaudeRuntimeAuthService extends ClaudeRuntimeAuthSync { constructor(store: Store) { super(store) this.initializeLastSyncedState() - void this.safeSyncForCurrentSelection() - void this.migrateLegacySharedAuth() + // Sync the selected runtime first; migration must not race a cleanup and + // repopulate a managed account from a stale shared Keychain entry. + void this.safeSyncForCurrentSelection().finally(() => this.migrateLegacySharedAuth()) } async prepareForClaudeLaunch( @@ -28,7 +29,32 @@ export class ClaudeRuntimeAuthService extends ClaudeRuntimeAuthSync { ? settings.claudeManagedAccounts.find((account) => account.id === selectedId) : null // Isolated accounts are already Claude's runtime store; never copy them into ~/.claude. - if (!selected || selected.managedAuthRuntime === 'wsl') { + // Legacy accounts with valid credentials still use the shared runtime and + // must be synchronized before launch; missing legacy credentials are left + // for the background cleanup path to handle without a second restore. + const legacyCredentials = + selected && selected.managedAuthRuntime === undefined + ? await this.readManagedCredentials(selected) + : null + let cleanupMissingLegacy = false + if (selected && selected.managedAuthRuntime === undefined && legacyCredentials === null) { + const runtimeCredentials = this.readRuntimeCredentialsFile() + const managedOauth = await this.readManagedOauthAccount(selected) + const runtimeMatches = this.runtimeCredentialsBelongToAccount( + runtimeCredentials, + selected, + managedOauth + ) + cleanupMissingLegacy = runtimeMatches + } + if ( + !selected || + selected.managedAuthRuntime === 'wsl' || + (selected.managedAuthRuntime === undefined && + legacyCredentials !== null && + this.isValidCredentialsJsonObject(legacyCredentials)) || + cleanupMissingLegacy + ) { await this.syncForCurrentSelection(effectiveTarget) } return this.getPreparation(effectiveTarget) diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts index 2b80c4a8126..392727695bd 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-preparation.ts @@ -77,7 +77,7 @@ export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapsh provenance: `wsl:${normalizeClaudeAccountSelectionTarget(normalizedTarget).wslDistro ?? '__default__'}:system` } } - if (activeAccount?.managedAuthRuntime !== 'wsl' && activeAccount) { + if (activeAccount?.managedAuthRuntime === 'host') { const managedPath = resolveOwnedClaudeManagedAuthPath( activeAccount.id, activeAccount.managedAuthPath, @@ -101,14 +101,14 @@ export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapsh wslDistro: null, wslLinuxConfigDir: null, envPatch: paths.envPatch, - stripAuthEnv: Boolean(activeAccountId && activeAccount?.managedAuthRuntime !== 'wsl'), + stripAuthEnv: Boolean(activeAccountId && activeAccount?.managedAuthRuntime === 'host'), managedRefreshDeferredByLivePty: Boolean( activeAccountId && - activeAccount?.managedAuthRuntime !== 'wsl' && + activeAccount?.managedAuthRuntime === 'host' && this.managedRefreshDeferredByLivePtyAccountId === activeAccountId ), provenance: - activeAccountId && activeAccount?.managedAuthRuntime !== 'wsl' + activeAccountId && activeAccount?.managedAuthRuntime === 'host' ? `managed:${activeAccountId}` : 'system' } diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-readback.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-readback.ts index bdbb9035e2a..82ba9263823 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-readback.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-readback.ts @@ -1,5 +1,6 @@ import { existsSync, readFileSync } from 'node:fs' import { startSpan } from '../../observability/tracer' +import { writeActiveClaudeKeychainCredentialsForRuntime } from '../keychain' import { ClaudeRuntimeAuthCredentialMatching } from './runtime-auth-credential-matching' import type { ClaudeReadBackMatch, @@ -113,8 +114,10 @@ export class ClaudeRuntimeAuthReadback extends ClaudeRuntimeAuthCredentialMatchi if (options.updateLastWrittenCredentialsJson) { this.writeRuntimeCredentials(runtimeContents) this.lastWrittenCredentialsJson = runtimeContents - // The managed account remains the source of truth; do not write the - // shared active Keychain service for an isolated account. + if (process.platform === 'darwin' && match.account.managedAuthRuntime === undefined) { + const paths = this.pathResolver.getRuntimePaths() + await writeActiveClaudeKeychainCredentialsForRuntime(runtimeContents, paths.configDir) + } } decisionSpan.end() return { status: 'persisted' } diff --git a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts index e987cfe9a84..21553ce0e05 100644 --- a/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts +++ b/src/main/claude-accounts/runtime-auth/runtime-auth-sync.ts @@ -8,6 +8,7 @@ import { } from '../runtime-selection' import { hasLiveClaudePtys } from '../live-pty-gate' import { isOauthTokenExpiring } from '../oauth-refresh' +import { writeActiveClaudeKeychainCredentialsForRuntime } from '../keychain' import { ClaudeRuntimeAuthPreparationService } from './runtime-auth-preparation' export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService { @@ -142,7 +143,17 @@ export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService { console.warn( '[claude-runtime-auth] Active managed account is not owned by Orca, restoring system default' ) - if (this.lastSyncedAccountId !== null) { + if (activeAccount.managedAuthRuntime === undefined) { + // Legacy accounts cannot safely restore a missing managed credential; + // retain the shared runtime while ensuring a valid snapshot marker. + if (!this.readSystemDefaultSnapshot(this.getSystemDefaultSnapshotPath())) { + await this.captureSystemDefaultSnapshot({ force: false }) + } + } + if ( + this.lastSyncedAccountId !== null && + (activeAccount.managedAuthRuntime !== undefined || previousAccount?.id !== activeAccount.id) + ) { if ( previousAccount && (previousAccount.id !== activeAccount.id || @@ -167,7 +178,15 @@ export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService { console.warn( '[claude-runtime-auth] Active managed account is missing or has invalid credentials, restoring system default' ) - if (this.lastSyncedAccountId !== null) { + if (activeAccount.managedAuthRuntime === undefined) { + if (!this.readSystemDefaultSnapshot(this.getSystemDefaultSnapshotPath())) { + await this.captureSystemDefaultSnapshot({ force: false }) + } + } + if ( + this.lastSyncedAccountId !== null && + (activeAccount.managedAuthRuntime !== undefined || previousAccount?.id !== activeAccount.id) + ) { if ( previousAccount && (previousAccount.id !== activeAccount.id || @@ -263,9 +282,21 @@ export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService { } this.writeRuntimeCredentials(credentialsJson) - // Isolated accounts are self-contained config roots. Never mirror their - // credentials into either active Keychain service (the legacy service is - // shared by all accounts and creates stale siblings). + // Legacy accounts predate per-account runtime isolation and still need both + // Keychain services for old and new Claude Code builds. Isolated accounts + // must never touch the shared active services. + if (process.platform === 'darwin' && activeAccount.managedAuthRuntime === undefined) { + const paths = this.pathResolver.getRuntimePaths() + try { + await writeActiveClaudeKeychainCredentialsForRuntime(credentialsJson, paths.configDir) + } catch (error) { + await this.restoreSystemDefaultSnapshot( + credentialsJson, + await this.readManagedOauthAccount(activeAccount) + ) + throw error + } + } const managedOauthAccount = await this.readManagedOauthAccount(activeAccount) if (this.writeRuntimeOauthAccount(managedOauthAccount)) { this.lastWrittenOauthAccount = managedOauthAccount