mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix(codex-accounts): stop overwriting auth.json when no managed account is active (#847)
This commit is contained in:
parent
9d56108e89
commit
c294a4d49e
2 changed files with 113 additions and 6 deletions
|
|
@ -242,10 +242,7 @@ describe('CodexRuntimeHomeService', () => {
|
|||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
writeFileSync(runtimeAuthPath, '{"account":"managed"}\n', 'utf-8')
|
||||
|
||||
service.syncForCurrentSelection()
|
||||
new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({ activeCodexManagedAccountId: null })
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"system"}\n')
|
||||
|
|
@ -262,6 +259,92 @@ describe('CodexRuntimeHomeService', () => {
|
|||
expect(existsSync(join(testState.fakeHomeDir, '.codex'))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not overwrite auth.json when no managed account was ever active', async () => {
|
||||
const runtimeAuthPath = join(testState.fakeHomeDir, '.codex', 'auth.json')
|
||||
writeFileSync(runtimeAuthPath, '{"account":"original"}\n', 'utf-8')
|
||||
const store = createStore(createSettings())
|
||||
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
writeFileSync(runtimeAuthPath, '{"account":"external-switch"}\n', 'utf-8')
|
||||
service.syncForCurrentSelection()
|
||||
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"external-switch"}\n')
|
||||
})
|
||||
|
||||
it('does not overwrite auth.json after deselection + external change', async () => {
|
||||
const runtimeAuthPath = join(testState.fakeHomeDir, '.codex', 'auth.json')
|
||||
writeFileSync(runtimeAuthPath, '{"account":"system"}\n', 'utf-8')
|
||||
const managedHomePath = createManagedAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
'{"account":"managed"}\n'
|
||||
)
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'user@example.com',
|
||||
managedHomePath,
|
||||
providerAccountId: null,
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1'
|
||||
})
|
||||
const store = createStore(settings)
|
||||
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
// Deselect managed account — should restore system default once
|
||||
settings.activeCodexManagedAccountId = null
|
||||
service.syncForCurrentSelection()
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"system"}\n')
|
||||
|
||||
// External tool changes auth — subsequent syncs must not overwrite
|
||||
writeFileSync(runtimeAuthPath, '{"account":"cc-switch"}\n', 'utf-8')
|
||||
service.syncForCurrentSelection()
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"cc-switch"}\n')
|
||||
})
|
||||
|
||||
it('restores system default on restart when persisted active account is invalid', async () => {
|
||||
const runtimeAuthPath = join(testState.fakeHomeDir, '.codex', 'auth.json')
|
||||
writeFileSync(runtimeAuthPath, '{"account":"system"}\n', 'utf-8')
|
||||
const settings = createSettings({
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'user@example.com',
|
||||
managedHomePath: join(testState.userDataDir, 'codex-accounts', 'account-1', 'home'),
|
||||
providerAccountId: null,
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1'
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
new CodexRuntimeHomeService(store as never)
|
||||
|
||||
// Constructor initializes lastSyncedAccountId='account-1' from settings,
|
||||
// then syncForCurrentSelection finds missing auth.json and restores snapshot
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({ activeCodexManagedAccountId: null })
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe('{"account":"system"}\n')
|
||||
expect(warnSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('imports legacy managed-home history into the shared runtime history', async () => {
|
||||
const runtimeHomePath = join(testState.fakeHomeDir, '.codex')
|
||||
const runtimeHistoryPath = join(runtimeHomePath, 'history.jsonl')
|
||||
|
|
|
|||
|
|
@ -19,11 +19,23 @@ import type { Store } from '../persistence'
|
|||
import { writeFileAtomically } from './fs-utils'
|
||||
|
||||
export class CodexRuntimeHomeService {
|
||||
// Why: tracks whether auth.json is currently managed by Orca. When null,
|
||||
// Orca does NOT own auth.json and must not overwrite external changes
|
||||
// (e.g. user running `codex login` or using cc-switch). The snapshot
|
||||
// restore only fires on the managed→system-default transition.
|
||||
private lastSyncedAccountId: string | null = null
|
||||
|
||||
constructor(private readonly store: Store) {
|
||||
this.safeMigrateLegacyManagedState()
|
||||
this.initializeLastSyncedState()
|
||||
this.safeSyncForCurrentSelection()
|
||||
}
|
||||
|
||||
private initializeLastSyncedState(): void {
|
||||
const settings = this.store.getSettings()
|
||||
this.lastSyncedAccountId = settings.activeCodexManagedAccountId
|
||||
}
|
||||
|
||||
prepareForCodexLaunch(): string {
|
||||
this.safeSyncForCurrentSelection()
|
||||
return this.getRuntimeHomePath()
|
||||
|
|
@ -43,7 +55,15 @@ export class CodexRuntimeHomeService {
|
|||
settings.activeCodexManagedAccountId
|
||||
)
|
||||
if (!activeAccount) {
|
||||
this.restoreSystemDefaultSnapshot()
|
||||
// Why: only restore the snapshot when transitioning FROM a managed
|
||||
// account back to system default. When no managed account was ever
|
||||
// active, auth.json belongs to the user and Orca must not touch it.
|
||||
// This prevents overwriting external auth changes (codex login,
|
||||
// cc-switch, or other tools) on every PTY launch / rate-limit fetch.
|
||||
if (this.lastSyncedAccountId !== null) {
|
||||
this.restoreSystemDefaultSnapshot()
|
||||
this.lastSyncedAccountId = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -53,10 +73,14 @@ export class CodexRuntimeHomeService {
|
|||
'[codex-runtime-home] Active managed account is missing auth.json, restoring system default'
|
||||
)
|
||||
this.store.updateSettings({ activeCodexManagedAccountId: null })
|
||||
this.restoreSystemDefaultSnapshot()
|
||||
if (this.lastSyncedAccountId !== null) {
|
||||
this.restoreSystemDefaultSnapshot()
|
||||
this.lastSyncedAccountId = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.lastSyncedAccountId = activeAccount.id
|
||||
this.writeRuntimeAuth(readFileSync(activeAuthPath, 'utf-8'))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue