From 553cbffb5ada2f6a3a9c2e5880134fe0889c0e84 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:09:45 -0700 Subject: [PATCH] fix(antigravity): keep the selected account after agy refreshes its token (#33656) After agy silently refreshes its Google token (the new credential has no id_token), Orca still matches the saved Antigravity account by auth method + refresh token instead of refusing the launch with "native Antigravity account changed"; the vault accepts such refreshed snapshots while still rejecting one whose id_token names a different account. Fixes #31579 Supersedes #33204 Co-authored-by: biojuho <258838639+biojuho@users.noreply.github.com> Co-authored-by: Manan <100475418+thefrikidude@users.noreply.github.com> --- .../antigravity/native-account-service.ts | 30 ++++++++++++------- .../antigravity/native-account-store.test.ts | 24 ++++++++++++++- src/main/antigravity/native-account-store.ts | 3 +- .../native-account-test-fixtures.ts | 13 ++++++++ .../native-credential-codec.test.ts | 6 ++-- .../antigravity/native-credential-codec.ts | 7 ++++- 6 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/main/antigravity/native-account-service.ts b/src/main/antigravity/native-account-service.ts index b615b074b01..732ad2c8a6c 100644 --- a/src/main/antigravity/native-account-service.ts +++ b/src/main/antigravity/native-account-service.ts @@ -19,6 +19,14 @@ function matches(account: StoredAntigravityAccount, current: AntigravityNativeCr if (current.identity && account.subject) { return account.subject === current.identity.subject && account.authMethod === current.authMethod } + // agy's token refresh stores no id_token but keeps the grant's refresh token. + if ( + current.refreshToken && + account.authMethod === current.authMethod && + parseAntigravityNativeCredential(account.credentials).refreshToken === current.refreshToken + ) { + return true + } return account.credentials === current.contents } @@ -41,13 +49,13 @@ export class AntigravityAccountService { if (!current) { throw new Error('Sign in with agy on this execution host, then save the current account.') } - if (!current.identity) { - throw new Error( - 'The current Antigravity credential has no stable Google identity; it cannot be saved for switching.' - ) - } let account = vault.accounts.find((entry) => matches(entry, current)) if (!account) { + if (!current.identity) { + throw new Error( + 'The current Antigravity credential has no stable Google identity; it cannot be saved for switching.' + ) + } const timestamp = this.now() account = { id: randomUUID(), @@ -167,18 +175,18 @@ export class AntigravityAccountService { vault: AntigravityAccountVault current: AntigravityNativeCredential | null }): AntigravityAccountState { + const active = current ? vault.accounts.find((entry) => matches(entry, current)) : undefined + const subject = current?.identity?.subject ?? active?.subject ?? null return { accounts: vault.accounts.map(({ credentials: _credentials, ...account }) => account), - activeAccountId: current - ? (vault.accounts.find((entry) => matches(entry, current))?.id ?? null) - : null, + activeAccountId: active?.id ?? null, selectedAccountId: vault.selectedAccountId, currentAccount: current ? { - email: current.identity?.email ?? null, - subject: current.identity?.subject ?? null, + email: current.identity ? current.identity.email : (active?.email ?? null), + subject, authMethod: current.authMethod, - identityKnown: current.identity !== null + identityKnown: subject !== null } : null } diff --git a/src/main/antigravity/native-account-store.test.ts b/src/main/antigravity/native-account-store.test.ts index 9fbd8ab183f..22208aafc8f 100644 --- a/src/main/antigravity/native-account-store.test.ts +++ b/src/main/antigravity/native-account-store.test.ts @@ -9,7 +9,7 @@ import { } from '../../shared/secret-store' import { createEncryptedAntigravityAccountStore } from './native-account-store' import { AntigravityAccountService } from './native-account-service' -import { credential, harness } from './native-account-test-fixtures' +import { credential, harness, refreshedWithoutIdToken } from './native-account-test-fixtures' let dir: string beforeEach(() => { @@ -119,6 +119,28 @@ describe('protected Antigravity account snapshots', () => { } ) + it('keeps the selected account launchable after agy refreshes its token without an id_token', async () => { + const h = harness() + const store = createEncryptedAntigravityAccountStore(join(dir, 'vault')) + const service = new AntigravityAccountService(store, h.backend) + const id = (await service.addCurrentAccount()).activeAccountId! + await service.selectAccount(id) + h.setNative(refreshedWithoutIdToken(2)) + await service.prepareForLaunch() + const restarted = new AntigravityAccountService(store, h.backend) + h.setNative(refreshedWithoutIdToken(3)) + expect(await restarted.listAccounts()).toMatchObject({ + activeAccountId: id, + selectedAccountId: id, + currentAccount: { subject: 'a', identityKnown: true } + }) + await restarted.prepareForLaunch() + expect(store.read().accounts[0].credentials).toBe(refreshedWithoutIdToken(3)) + h.setNative(refreshedWithoutIdToken(4, 'refresh-other-grant')) + await expect(restarted.prepareForLaunch()).rejects.toThrow('native Antigravity account changed') + expect(h.backend.write).not.toHaveBeenCalled() + }) + it('keeps an unreadable vault intact rather than treating it as empty', () => { const path = join(dir, 'vault') writeFileSync(path, 'broken ciphertext', { mode: 0o600 }) diff --git a/src/main/antigravity/native-account-store.ts b/src/main/antigravity/native-account-store.ts index 15c8c0007a0..2fe9e96fabd 100644 --- a/src/main/antigravity/native-account-store.ts +++ b/src/main/antigravity/native-account-store.ts @@ -78,9 +78,10 @@ export function createEncryptedAntigravityAccountStore(path: string): Antigravit } for (const account of value.accounts) { const credential = parseAntigravityNativeCredential(account.credentials) + // agy refreshes drop the id_token, so a saved account may hold a credential without one. if ( credential.authMethod !== account.authMethod || - credential.identity?.subject !== account.subject + (credential.identity !== null && credential.identity.subject !== account.subject) ) { throw new Error('inconsistent identity') } diff --git a/src/main/antigravity/native-account-test-fixtures.ts b/src/main/antigravity/native-account-test-fixtures.ts index d8bc7f4694d..b25ff57e2ea 100644 --- a/src/main/antigravity/native-account-test-fixtures.ts +++ b/src/main/antigravity/native-account-test-fixtures.ts @@ -26,6 +26,19 @@ export function credential(subject: string, generation = 1, email = `${subject}@ }) } +// agy's refresh keeps the grant's refresh_token but stores no id_token (agy 1.4.7). +export function refreshedWithoutIdToken(generation: number, refreshToken = 'refresh-1') { + return JSON.stringify({ + auth_method: 'consumer', + id_token_email: 'a@example.invalid', + token: { + access_token: `synthetic-${generation}`, + refresh_token: refreshToken, + expiry: generation + } + }) +} + export function harness(initial: string | null = credential('a')) { let native = initial let vault: AntigravityAccountVault = { accounts: [], selectedAccountId: null } diff --git a/src/main/antigravity/native-credential-codec.test.ts b/src/main/antigravity/native-credential-codec.test.ts index 30ce78230a5..216f64b0d21 100644 --- a/src/main/antigravity/native-credential-codec.test.ts +++ b/src/main/antigravity/native-credential-codec.test.ts @@ -32,7 +32,8 @@ describe('native Antigravity credential format', () => { issuer: 'https://accounts.google.com', subject: 'synthetic-subject', email: 'test@example.invalid' - } + }, + refreshToken: 'synthetic-refresh' }) }) @@ -88,7 +89,8 @@ describe('native Antigravity credential format', () => { expect(parseAntigravityNativeCredential(contents)).toEqual({ contents, authMethod: 'future', - identity: null + identity: null, + refreshToken: null }) }) }) diff --git a/src/main/antigravity/native-credential-codec.ts b/src/main/antigravity/native-credential-codec.ts index ca0960db22f..3b048cfe29b 100644 --- a/src/main/antigravity/native-credential-codec.ts +++ b/src/main/antigravity/native-credential-codec.ts @@ -88,6 +88,7 @@ export type AntigravityNativeCredential = { contents: string authMethod: string identity: AntigravityCredentialIdentity | null + refreshToken: string | null } export function parseAntigravityNativeCredential(contents: string): AntigravityNativeCredential { @@ -114,6 +115,10 @@ export function parseAntigravityNativeCredential(contents: string): AntigravityN // Preserve unknown fields for CLI compatibility. contents, authMethod: value.auth_method, - identity: readIdentity(value.id_token ?? value.token.id_token) + identity: readIdentity(value.id_token ?? value.token.id_token), + refreshToken: + typeof value.token.refresh_token === 'string' && value.token.refresh_token + ? value.token.refresh_token + : null } }