From 4b385bfe50ca0d33f3d6035036fc74b3a205f1b3 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:26:19 -0700 Subject: [PATCH] fix(codex): require monotonic legacy refreshes --- .../runtime-home-service.test.ts | 63 +++++++++++++++++-- .../codex-accounts/runtime-home-service.ts | 47 +++++++++++++- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 222e0d176e2..59e25269d23 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -257,7 +257,8 @@ function createCodexAuthJson( email: string, accountId: string, refreshToken: string, - expiresAt?: number + expiresAt?: number, + lastRefresh?: string ): string { const idToken = [ encodeJwtPart({ alg: 'none', typ: 'JWT' }), @@ -274,6 +275,7 @@ function createCodexAuthJson( return `${JSON.stringify({ auth_mode: 'chatgpt', + ...(lastRefresh === undefined ? {} : { last_refresh: lastRefresh }), tokens: { access_token: `access-${accountId}`, id_token: idToken, @@ -3767,11 +3769,19 @@ describe('CodexRuntimeHomeService', () => { it('reads back system-default token refreshes after a pre-provenance restart', async () => { const runtimeAuthPath = getRuntimeCodexAuthPath() - const systemAuth = createCodexAuthJson('system@example.com', 'acct-system', 'system-old') + const systemAuth = createCodexAuthJson( + 'system@example.com', + 'acct-system', + 'system-old', + undefined, + '2026-07-30T12:00:00.000Z' + ) const refreshedAuth = createCodexAuthJson( 'system@example.com', 'acct-system', - 'system-refreshed' + 'system-refreshed', + undefined, + '2026-07-31T12:00:00.000Z' ) writeFileSync(getSystemCodexAuthPath(), systemAuth, 'utf-8') const store = createStore(createSettings()) @@ -3788,6 +3798,37 @@ describe('CodexRuntimeHomeService', () => { expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(refreshedAuth) }) + it('does not read back older same-identity auth without provenance', async () => { + const runtimeAuthPath = getRuntimeCodexAuthPath() + const systemAuth = createCodexAuthJson( + 'system@example.com', + 'acct-system', + 'system-newer', + undefined, + '2026-07-31T12:00:00.000Z' + ) + const staleRuntimeAuth = createCodexAuthJson( + 'system@example.com', + 'acct-system', + 'runtime-older', + undefined, + '2026-07-30T12:00:00.000Z' + ) + writeFileSync(getSystemCodexAuthPath(), systemAuth, 'utf-8') + const store = createStore(createSettings()) + + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + new CodexRuntimeHomeService(store as never) + + writeFileSync(runtimeAuthPath, staleRuntimeAuth, 'utf-8') + rmSync(getSharedRuntimeAuthProvenancePath()) + const restartedService = new CodexRuntimeHomeService(store as never) + restartedService.syncForCurrentSelection() + + expect(readFileSync(getSystemCodexAuthPath(), 'utf-8')).toBe(systemAuth) + expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(systemAuth) + }) + it('keeps a local runtime logout when the system-default auth still exists', async () => { const runtimeAuthPath = getRuntimeCodexAuthPath() writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8') @@ -3920,8 +3961,20 @@ describe('CodexRuntimeHomeService', () => { it('clears refreshed runtime auth after a pre-provenance external logout', async () => { const runtimeAuthPath = getRuntimeCodexAuthPath() - const systemAuth = createCodexAuthJson('system@example.com', 'acct-system', 'system') - const refreshedAuth = createCodexAuthJson('system@example.com', 'acct-system', 'refreshed') + const systemAuth = createCodexAuthJson( + 'system@example.com', + 'acct-system', + 'system', + undefined, + '2026-07-30T12:00:00.000Z' + ) + const refreshedAuth = createCodexAuthJson( + 'system@example.com', + 'acct-system', + 'refreshed', + undefined, + '2026-07-31T12:00:00.000Z' + ) writeFileSync(getSystemCodexAuthPath(), systemAuth, 'utf-8') const store = createStore(createSettings()) diff --git a/src/main/codex-accounts/runtime-home-service.ts b/src/main/codex-accounts/runtime-home-service.ts index 2e7df680619..36bcbd570de 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -126,6 +126,42 @@ type CodexReadBackMatch = } | { kind: 'none' | 'ambiguous' } +function readCodexLastRefresh(authJson: string): number | null { + try { + const parsed = JSON.parse(authJson) as unknown + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return null + } + const value = (parsed as Record).last_refresh + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null + } + if (typeof value !== 'string' || !value.trim()) { + return null + } + const timestamp = Date.parse(value) + return Number.isFinite(timestamp) ? timestamp : null + } catch { + return null + } +} + +function codexAuthIsMonotonicallyFresher( + candidateAuthJson: string, + baselineAuthJson: string +): boolean { + const candidateLastRefresh = readCodexLastRefresh(candidateAuthJson) + const baselineLastRefresh = readCodexLastRefresh(baselineAuthJson) + if (candidateLastRefresh !== null || baselineLastRefresh !== null) { + return ( + candidateLastRefresh !== null && + baselineLastRefresh !== null && + candidateLastRefresh > baselineLastRefresh + ) + } + return codexAuthIsFresher(candidateAuthJson, baselineAuthJson) +} + export class CodexRuntimeHomeService { // Which managed account runtime auth.json mirrors; null means it follows system-default ~/.codex instead of a managed account. private lastSyncedAccountId: string | null = null @@ -1593,14 +1629,19 @@ export class CodexRuntimeHomeService { return } const snapshot = this.readSystemDefaultSnapshot(this.getSystemDefaultSnapshotPath()) + const snapshotAuth = snapshot?.authJson ?? null + const preProvenanceRuntimeRefreshProven = + provenanceStatus.kind === 'missing' && + snapshotAuth !== null && + this.runtimeAuthMatchesSystemDefaultIdentity(runtimeAuth, snapshotAuth) && + codexAuthIsMonotonicallyFresher(runtimeAuth, snapshotAuth) const systemDefaultOwnershipProven = - provenance?.owner === 'system-default' || - (provenanceStatus.kind === 'missing' && snapshot !== null) + provenance?.owner === 'system-default' || preProvenanceRuntimeRefreshProven const mirroredSystemDefaultAuth = provenance?.owner === 'system-default' ? provenance.authJson : provenanceStatus.kind === 'missing' - ? (this.lastWrittenAuthJson ?? snapshot?.authJson ?? null) + ? (this.lastWrittenAuthJson ?? snapshotAuth) : null if (!existsSync(systemDefaultAuthPath)) { if (mirroredSystemDefaultAuth !== null && runtimeAuth === mirroredSystemDefaultAuth) {