mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
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:
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user