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>
This commit is contained in:
Neil
2026-10-25 16:09:45 -07:00
committed by GitHub
co-authored by biojuho Manan
parent 12cc6c5fff
commit 553cbffb5a
6 changed files with 67 additions and 16 deletions
+19 -11
View File
@@ -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
}
@@ -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 })
+2 -1
View File
@@ -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')
}
@@ -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 }
@@ -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
})
})
})
@@ -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
}
}