mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 08:02:28 +00:00
Isolate Claude auth ownership and migration
This commit is contained in:
@@ -229,7 +229,13 @@ export class ClaudeManagedAuthStorage {
|
||||
if (process.platform !== 'win32') {
|
||||
if (
|
||||
!existsSync(candidatePath) ||
|
||||
!existsSync(join(candidatePath, '.orca-managed-claude-auth'))
|
||||
!existsSync(join(candidatePath, '.orca-managed-claude-auth')) ||
|
||||
lstatSync(candidatePath).isSymbolicLink() ||
|
||||
lstatSync(join(candidatePath, '.orca-managed-claude-auth')).isSymbolicLink() ||
|
||||
!lstatSync(join(candidatePath, '.orca-managed-claude-auth')).isFile() ||
|
||||
(expectedAccountId !== undefined &&
|
||||
readFileSync(join(candidatePath, '.orca-managed-claude-auth'), 'utf-8').trim() !==
|
||||
expectedAccountId)
|
||||
) {
|
||||
throw new Error('Managed Claude auth storage is not owned by Orca.')
|
||||
}
|
||||
@@ -249,6 +255,8 @@ export class ClaudeManagedAuthStorage {
|
||||
'managed_root="${HOME%/}/.local/share/orca/claude-accounts"',
|
||||
'candidate_real=$(readlink -f -- "$candidate")',
|
||||
'managed_root_real=$(readlink -f -- "$managed_root")',
|
||||
'test ! -L "$candidate"',
|
||||
'test ! -L "$candidate_real/.orca-managed-claude-auth"',
|
||||
'test -f "$candidate_real/.orca-managed-claude-auth"',
|
||||
expected,
|
||||
'case "$candidate_real" in "$managed_root_real"/*/auth) printf "%s\\n" "$candidate_real" ;; *) exit 35 ;; esac'
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { mkdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type { ClaudeManagedAccount } from '../../shared/managed-account-types'
|
||||
import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence'
|
||||
import { writeFileAtomically } from '../codex-accounts/fs-utils'
|
||||
import { resolveOwnedClaudeManagedAuthPath } from './managed-auth-path'
|
||||
|
||||
export const LEGACY_SHARED_CLAUDE_AUTH_MIGRATION_MARKER =
|
||||
'per-account-claude-auth-migration-v1.json'
|
||||
|
||||
export type ClaudeAuthMigrationOutcome =
|
||||
| 'migrated'
|
||||
| 'already-present'
|
||||
| 'no-shared-auth'
|
||||
| 'ambiguous'
|
||||
| 'unavailable'
|
||||
|
||||
type MigrationOptions = {
|
||||
accounts: ClaudeManagedAccount[]
|
||||
sharedAuthPath: string
|
||||
metadataDir: string
|
||||
readLegacyKeychain?: () => Promise<string | null>
|
||||
readManagedCredentials: (account: ClaudeManagedAccount) => Promise<string | null>
|
||||
writeManagedCredentials: (account: ClaudeManagedAccount, contents: string) => Promise<void>
|
||||
}
|
||||
|
||||
/** One-way migration for pre-isolation shared Claude credentials. */
|
||||
export async function migrateLegacySharedClaudeAuth(
|
||||
options: MigrationOptions
|
||||
): Promise<ClaudeAuthMigrationOutcome> {
|
||||
const markerPath = join(options.metadataDir, LEGACY_SHARED_CLAUDE_AUTH_MIGRATION_MARKER)
|
||||
try {
|
||||
if (statSync(markerPath).isFile()) {
|
||||
return 'already-present'
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isDefinitiveAbsence(error)) {
|
||||
return 'unavailable'
|
||||
}
|
||||
}
|
||||
|
||||
let shared: string | null = null
|
||||
try {
|
||||
shared = options.readLegacyKeychain
|
||||
? await options.readLegacyKeychain()
|
||||
: readFileSync(options.sharedAuthPath, 'utf-8')
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return stamp(markerPath, 'no-shared-auth')
|
||||
}
|
||||
return 'unavailable'
|
||||
}
|
||||
if (!shared) {
|
||||
return stamp(markerPath, 'no-shared-auth')
|
||||
}
|
||||
|
||||
const identity = parseIdentity(shared)
|
||||
if (!identity) {
|
||||
return 'unavailable'
|
||||
}
|
||||
const candidates = options.accounts.filter((account) => {
|
||||
const emailMatch = identity.email && account.email.trim().toLowerCase() === identity.email
|
||||
const orgMatch = identity.organizationUuid
|
||||
? account.organizationUuid === identity.organizationUuid
|
||||
: true
|
||||
return Boolean(emailMatch && orgMatch)
|
||||
})
|
||||
if (candidates.length !== 1) {
|
||||
return 'ambiguous'
|
||||
}
|
||||
const account = candidates[0]
|
||||
if (!resolveOwnedClaudeManagedAuthPath(account.id, account.managedAuthPath)) {
|
||||
return 'unavailable'
|
||||
}
|
||||
const existing = await options.readManagedCredentials(account)
|
||||
if (!existing) {
|
||||
try {
|
||||
await options.writeManagedCredentials(account, shared)
|
||||
} catch {
|
||||
return 'unavailable'
|
||||
}
|
||||
}
|
||||
return stamp(markerPath, existing ? 'already-present' : 'migrated', account.id)
|
||||
}
|
||||
|
||||
function parseIdentity(
|
||||
contents: string
|
||||
): { email: string | null; organizationUuid: string | null } | null {
|
||||
try {
|
||||
const root = JSON.parse(contents) as Record<string, unknown>
|
||||
const oauth = root.claudeAiOauth
|
||||
if (!oauth || typeof oauth !== 'object' || Array.isArray(oauth)) {
|
||||
return null
|
||||
}
|
||||
const value = oauth as Record<string, unknown>
|
||||
const email = typeof value.email === 'string' ? value.email.trim().toLowerCase() : null
|
||||
const organizationUuid =
|
||||
typeof value.organizationUuid === 'string' ? value.organizationUuid.trim() : null
|
||||
return email ? { email, organizationUuid } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function stamp(path: string, outcome: string, accountId?: string): ClaudeAuthMigrationOutcome {
|
||||
try {
|
||||
mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 })
|
||||
writeFileAtomically(
|
||||
path,
|
||||
`${JSON.stringify({ version: 1, completedAt: Date.now(), outcome, accountId: accountId ?? null })}\n`,
|
||||
{ mode: 0o600 }
|
||||
)
|
||||
return outcome as ClaudeAuthMigrationOutcome
|
||||
} catch {
|
||||
return 'unavailable'
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,150 @@
|
||||
import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join, relative, resolve, sep } from 'node:path'
|
||||
import { app } from 'electron'
|
||||
import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence'
|
||||
import { writeFileAtomically } from '../codex-accounts/fs-utils'
|
||||
|
||||
const MANAGED_AUTH_MARKER = '.orca-managed-claude-auth'
|
||||
|
||||
export type ClaudeManagedAuthOwnershipVerdict =
|
||||
| { kind: 'owned'; authPath: string }
|
||||
| { kind: 'untrusted'; reason: string }
|
||||
| { kind: 'indeterminate'; error: unknown }
|
||||
|
||||
export class ClaudeManagedAuthTemporarilyUnavailableError extends Error {
|
||||
constructor(options?: { cause?: unknown }) {
|
||||
super('Claude managed auth storage is temporarily unavailable.', options)
|
||||
}
|
||||
}
|
||||
|
||||
export function getClaudeManagedAccountsRoot(): string {
|
||||
return join(app.getPath('userData'), 'claude-accounts')
|
||||
}
|
||||
|
||||
function pathIsInsideOrEqual(rootPath: string, candidatePath: string): boolean {
|
||||
const value = relative(rootPath, candidatePath)
|
||||
return value === '' || (!value.startsWith('..') && !value.includes(`..${sep}`))
|
||||
}
|
||||
|
||||
function canonicalizeIfPresent(path: string): string {
|
||||
try {
|
||||
return realpathSync(path)
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return resolve(path)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-throwing ownership check; indeterminate must never be treated as absent. */
|
||||
export function resolveClaudeManagedAuthOwnership(
|
||||
accountId: string,
|
||||
candidatePath: string
|
||||
): ClaudeManagedAuthOwnershipVerdict {
|
||||
const resolvedCandidate = resolve(candidatePath)
|
||||
const resolvedRoot = resolve(getClaudeManagedAccountsRoot())
|
||||
let canonicalCandidate: string
|
||||
let canonicalRoot: string
|
||||
let canonicalSystemHome: string
|
||||
try {
|
||||
if (lstatSync(resolvedRoot).isSymbolicLink()) {
|
||||
return { kind: 'untrusted', reason: 'Claude managed accounts root is a symlink.' }
|
||||
}
|
||||
statSync(resolvedCandidate)
|
||||
if (lstatSync(resolvedCandidate).isSymbolicLink()) {
|
||||
return { kind: 'untrusted', reason: 'Claude managed auth path is a symlink.' }
|
||||
}
|
||||
canonicalRoot = realpathSync(resolvedRoot)
|
||||
canonicalCandidate = realpathSync(resolvedCandidate)
|
||||
canonicalSystemHome = canonicalizeIfPresent(join(homedir(), '.claude'))
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return { kind: 'untrusted', reason: 'Managed Claude auth directory does not exist on disk.' }
|
||||
}
|
||||
return { kind: 'indeterminate', error }
|
||||
}
|
||||
|
||||
let canonicalExpected: string
|
||||
try {
|
||||
canonicalExpected = canonicalizeIfPresent(join(canonicalRoot, accountId, 'auth'))
|
||||
} catch (error) {
|
||||
return { kind: 'indeterminate', error }
|
||||
}
|
||||
if (
|
||||
!pathIsInsideOrEqual(canonicalRoot, canonicalCandidate) ||
|
||||
canonicalCandidate === canonicalRoot ||
|
||||
canonicalCandidate !== canonicalExpected
|
||||
) {
|
||||
return { kind: 'untrusted', reason: 'Managed Claude auth path is outside its account root.' }
|
||||
}
|
||||
if (pathIsInsideOrEqual(canonicalSystemHome, canonicalCandidate)) {
|
||||
return {
|
||||
kind: 'untrusted',
|
||||
reason: 'Managed Claude auth resolves inside the system Claude home.'
|
||||
}
|
||||
}
|
||||
|
||||
const markerPath = join(canonicalCandidate, MANAGED_AUTH_MARKER)
|
||||
let markerContents: string
|
||||
try {
|
||||
const markerStat = lstatSync(markerPath)
|
||||
if (!markerStat.isFile() || markerStat.isSymbolicLink()) {
|
||||
return { kind: 'untrusted', reason: 'Managed Claude auth marker is not a regular file.' }
|
||||
}
|
||||
markerContents = readFileSync(markerPath, 'utf-8')
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return { kind: 'untrusted', reason: 'Managed Claude auth marker is missing.' }
|
||||
}
|
||||
return { kind: 'indeterminate', error }
|
||||
}
|
||||
if (markerContents.trim() !== accountId) {
|
||||
return {
|
||||
kind: 'untrusted',
|
||||
reason: 'Managed Claude auth marker does not match its account ID.'
|
||||
}
|
||||
}
|
||||
return { kind: 'owned', authPath: canonicalCandidate }
|
||||
}
|
||||
|
||||
export function resolveOwnedClaudeManagedAuthPath(
|
||||
accountId: string,
|
||||
candidatePath: string,
|
||||
options: { adoptLegacyMarker?: boolean } = {}
|
||||
): string | null {
|
||||
const rootPath = getClaudeManagedAccountsRoot()
|
||||
const resolvedCandidate = resolve(candidatePath)
|
||||
if (!existsSync(resolvedCandidate) || !existsSync(rootPath)) {
|
||||
return null
|
||||
let verdict = resolveClaudeManagedAuthOwnership(accountId, candidatePath)
|
||||
if (verdict.kind === 'untrusted' && options.adoptLegacyMarker) {
|
||||
const root = resolve(getClaudeManagedAccountsRoot())
|
||||
const expected = join(root, accountId, 'auth')
|
||||
try {
|
||||
if (resolve(candidatePath) === expected && statSync(expected).isDirectory()) {
|
||||
writeFileSync(join(expected, MANAGED_AUTH_MARKER), `${accountId}\n`, {
|
||||
encoding: 'utf-8',
|
||||
mode: 0o600,
|
||||
flag: 'wx'
|
||||
})
|
||||
verdict = resolveClaudeManagedAuthOwnership(accountId, candidatePath)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isDefinitiveAbsence(error)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (lstatSync(resolvedCandidate).isSymbolicLink()) {
|
||||
return null
|
||||
}
|
||||
const canonicalCandidate = realpathSync(resolvedCandidate)
|
||||
const canonicalRoot = realpathSync(rootPath)
|
||||
if (
|
||||
canonicalCandidate === canonicalRoot ||
|
||||
!canonicalCandidate.startsWith(canonicalRoot + sep)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const relativePath = relative(canonicalRoot, canonicalCandidate)
|
||||
const relativeParts = relativePath.split(sep)
|
||||
const escaped = relativePath.startsWith('..') || relativePath.includes(`..${sep}`)
|
||||
if (
|
||||
escaped ||
|
||||
relativeParts.length !== 2 ||
|
||||
relativeParts[0] !== accountId ||
|
||||
relativeParts[1] !== 'auth'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const markerPath = join(canonicalCandidate, MANAGED_AUTH_MARKER)
|
||||
const markerValid = isManagedAuthMarkerValid(markerPath, accountId)
|
||||
if (!markerValid && options.adoptLegacyMarker) {
|
||||
writeFileSync(markerPath, `${accountId}\n`, { encoding: 'utf-8', mode: 0o600, flag: 'wx' })
|
||||
}
|
||||
if (!markerValid && !isManagedAuthMarkerValid(markerPath, accountId)) {
|
||||
return null
|
||||
}
|
||||
return canonicalCandidate
|
||||
} catch {
|
||||
return null
|
||||
return verdict.kind === 'owned' ? verdict.authPath : null
|
||||
}
|
||||
|
||||
export function assertOwnedClaudeManagedAuthPath(accountId: string, candidatePath: string): string {
|
||||
const verdict = resolveClaudeManagedAuthOwnership(accountId, candidatePath)
|
||||
if (verdict.kind === 'owned') {
|
||||
return verdict.authPath
|
||||
}
|
||||
if (verdict.kind === 'indeterminate') {
|
||||
throw new ClaudeManagedAuthTemporarilyUnavailableError({ cause: verdict.error })
|
||||
}
|
||||
throw new Error(verdict.reason)
|
||||
}
|
||||
|
||||
export function readClaudeManagedAuthFile(
|
||||
@@ -66,8 +157,11 @@ export function readClaudeManagedAuthFile(
|
||||
return null
|
||||
}
|
||||
return readFileSync(filePath, 'utf-8')
|
||||
} catch {
|
||||
return null
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,36 +171,36 @@ export function writeClaudeManagedAuthFile(
|
||||
contents: string
|
||||
): void {
|
||||
const filePath = resolve(managedAuthPath, filename)
|
||||
if (existsSync(filePath) && !isOwnedChildFile(managedAuthPath, filePath)) {
|
||||
if (!isOwnedChildFile(managedAuthPath, filePath, true)) {
|
||||
throw new Error('Managed Claude auth child file is not owned by Orca.')
|
||||
}
|
||||
writeFileAtomically(filePath, contents, { mode: 0o600 })
|
||||
}
|
||||
|
||||
function isManagedAuthMarkerValid(markerPath: string, accountId: string): boolean {
|
||||
function isOwnedChildFile(
|
||||
managedAuthPath: string,
|
||||
filePath: string,
|
||||
allowMissing = false
|
||||
): boolean {
|
||||
try {
|
||||
if (
|
||||
!existsSync(markerPath) ||
|
||||
lstatSync(markerPath).isSymbolicLink() ||
|
||||
!lstatSync(markerPath).isFile()
|
||||
) {
|
||||
const authStat = lstatSync(managedAuthPath)
|
||||
if (!authStat.isDirectory() || authStat.isSymbolicLink()) {
|
||||
return false
|
||||
}
|
||||
return readFileSync(markerPath, 'utf-8').trim() === accountId
|
||||
} catch {
|
||||
return false
|
||||
const fileStat = lstatSync(filePath)
|
||||
if (fileStat.isSymbolicLink() || (!fileStat.isFile() && !allowMissing)) {
|
||||
return false
|
||||
}
|
||||
const canonicalAuthPath = realpathSync(managedAuthPath)
|
||||
const canonicalFilePath = allowMissing ? resolve(filePath) : realpathSync(filePath)
|
||||
return (
|
||||
pathIsInsideOrEqual(canonicalAuthPath, canonicalFilePath) &&
|
||||
canonicalFilePath !== canonicalAuthPath
|
||||
)
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return allowMissing
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function isOwnedChildFile(managedAuthPath: string, filePath: string): boolean {
|
||||
if (
|
||||
!existsSync(filePath) ||
|
||||
lstatSync(filePath).isSymbolicLink() ||
|
||||
!lstatSync(filePath).isFile()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const canonicalAuthPath = realpathSync(managedAuthPath)
|
||||
const canonicalFilePath = realpathSync(filePath)
|
||||
return canonicalFilePath.startsWith(canonicalAuthPath + sep)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
} from './runtime-selection'
|
||||
import { ClaudeRuntimeAuthSync } from './runtime-auth/runtime-auth-sync'
|
||||
import type { ClaudeRuntimeAuthPreparation } from './runtime-auth/runtime-auth-types'
|
||||
import { migrateLegacySharedClaudeAuth } from './legacy-shared-claude-auth-migration'
|
||||
import { readActiveClaudeKeychainCredentialsStrict } from './keychain'
|
||||
|
||||
export type { ClaudeRuntimeAuthPreparation } from './runtime-auth/runtime-auth-types'
|
||||
|
||||
@@ -13,6 +15,7 @@ export class ClaudeRuntimeAuthService extends ClaudeRuntimeAuthSync {
|
||||
super(store)
|
||||
this.initializeLastSyncedState()
|
||||
void this.safeSyncForCurrentSelection()
|
||||
void this.migrateLegacySharedAuth()
|
||||
}
|
||||
|
||||
async prepareForClaudeLaunch(
|
||||
@@ -81,6 +84,28 @@ export class ClaudeRuntimeAuthService extends ClaudeRuntimeAuthSync {
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateLegacySharedAuth(): Promise<void> {
|
||||
const settings = this.store.getSettings()
|
||||
const paths = this.pathResolver.getRuntimePaths()
|
||||
const metadataDir = this.getRuntimeMetadataDir()
|
||||
try {
|
||||
await migrateLegacySharedClaudeAuth({
|
||||
accounts: settings.claudeManagedAccounts,
|
||||
sharedAuthPath: paths.credentialsPath,
|
||||
metadataDir,
|
||||
readLegacyKeychain:
|
||||
process.platform === 'darwin'
|
||||
? () => readActiveClaudeKeychainCredentialsStrict()
|
||||
: undefined,
|
||||
readManagedCredentials: (account) => this.readManagedCredentials(account),
|
||||
writeManagedCredentials: (account, contents) =>
|
||||
this.writeManagedCredentials(account, contents)
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('[claude-runtime-auth] Legacy auth migration deferred:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private serializeMutation<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const next = this.mutationQueue.then(fn, fn)
|
||||
this.mutationQueue = next.catch(() => {})
|
||||
|
||||
@@ -4,7 +4,9 @@ import { parseWslUncPath } from '../../../shared/wsl-paths'
|
||||
import { toWindowsWslPath } from '../../wsl'
|
||||
import { runWslProcess } from '../../wsl/wsl-runner'
|
||||
import {
|
||||
assertOwnedClaudeManagedAuthPath,
|
||||
readClaudeManagedAuthFile,
|
||||
resolveClaudeManagedAuthOwnership,
|
||||
resolveOwnedClaudeManagedAuthPath,
|
||||
writeClaudeManagedAuthFile
|
||||
} from '../managed-auth-path'
|
||||
@@ -137,8 +139,21 @@ export class ClaudeRuntimeAuthManagedCredentials extends ClaudeRuntimeAuthCreden
|
||||
}
|
||||
return existsSync(account.managedAuthPath) ? account.managedAuthPath : null
|
||||
}
|
||||
return resolveOwnedClaudeManagedAuthPath(account.id, account.managedAuthPath, {
|
||||
adoptLegacyMarker: true
|
||||
})
|
||||
try {
|
||||
const verdict = resolveClaudeManagedAuthOwnership(account.id, account.managedAuthPath)
|
||||
if (verdict.kind === 'indeterminate') {
|
||||
return assertOwnedClaudeManagedAuthPath(account.id, account.managedAuthPath)
|
||||
}
|
||||
return verdict.kind === 'owned'
|
||||
? verdict.authPath
|
||||
: resolveOwnedClaudeManagedAuthPath(account.id, account.managedAuthPath, {
|
||||
adoptLegacyMarker: true
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('temporarily unavailable')) {
|
||||
throw error
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,19 @@ export class ClaudeRuntimeAuthPreparationService extends ClaudeRuntimeAuthSnapsh
|
||||
)
|
||||
const activeAccountId = getSelectedClaudeAccountIdForTarget(settings, normalizedTarget)
|
||||
const activeAccount = this.getActiveAccount(settings.claudeManagedAccounts, activeAccountId)
|
||||
// An explicit user config root is authoritative; account selection must not
|
||||
// silently redirect a pane away from it.
|
||||
if (process.env.CLAUDE_CONFIG_DIR?.trim()) {
|
||||
return {
|
||||
configDir: paths.configDir,
|
||||
runtime: normalizedTarget.runtime,
|
||||
wslDistro: normalizedTarget.wslDistro,
|
||||
wslLinuxConfigDir: null,
|
||||
envPatch: paths.envPatch,
|
||||
stripAuthEnv: false,
|
||||
provenance: 'system:explicit-config-dir'
|
||||
}
|
||||
}
|
||||
if (
|
||||
normalizeClaudeAccountSelectionTarget(normalizedTarget).runtime === 'wsl' &&
|
||||
activeAccount?.managedAuthRuntime === 'wsl' &&
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { writeActiveClaudeKeychainCredentialsForRuntime } from '../keychain'
|
||||
import { startSpan } from '../../observability/tracer'
|
||||
import { ClaudeRuntimeAuthCredentialMatching } from './runtime-auth-credential-matching'
|
||||
import type {
|
||||
@@ -114,10 +113,8 @@ export class ClaudeRuntimeAuthReadback extends ClaudeRuntimeAuthCredentialMatchi
|
||||
if (options.updateLastWrittenCredentialsJson) {
|
||||
this.writeRuntimeCredentials(runtimeContents)
|
||||
this.lastWrittenCredentialsJson = runtimeContents
|
||||
if (process.platform === 'darwin') {
|
||||
const paths = this.pathResolver.getRuntimePaths()
|
||||
await writeActiveClaudeKeychainCredentialsForRuntime(runtimeContents, paths.configDir)
|
||||
}
|
||||
// The managed account remains the source of truth; do not write the
|
||||
// shared active Keychain service for an isolated account.
|
||||
}
|
||||
decisionSpan.end()
|
||||
return { status: 'persisted' }
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 {
|
||||
@@ -263,20 +262,10 @@ export class ClaudeRuntimeAuthSync extends ClaudeRuntimeAuthPreparationService {
|
||||
}
|
||||
}
|
||||
|
||||
const paths = this.pathResolver.getRuntimePaths()
|
||||
this.writeRuntimeCredentials(credentialsJson)
|
||||
if (process.platform === 'darwin') {
|
||||
// Why: Claude Code 2.1+ reads the scoped service, older builds the legacy unsuffixed one; runtime switching must satisfy both.
|
||||
try {
|
||||
await writeActiveClaudeKeychainCredentialsForRuntime(credentialsJson, paths.configDir)
|
||||
} catch (error) {
|
||||
await this.restoreSystemDefaultSnapshot(
|
||||
credentialsJson,
|
||||
await this.readManagedOauthAccount(activeAccount)
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
// 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).
|
||||
const managedOauthAccount = await this.readManagedOauthAccount(activeAccount)
|
||||
if (this.writeRuntimeOauthAccount(managedOauthAccount)) {
|
||||
this.lastWrittenOauthAccount = managedOauthAccount
|
||||
|
||||
Reference in New Issue
Block a user