Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions apps/cli/__tests__/host-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4331,13 +4331,13 @@ describe('HostAgentSupervisor — re-link of an env-delivered credential', () =>
fs.rmSync(tmpRoot, { recursive: true, force: true });
});

function boot(auth: AgentAuth) {
function boot(auth: AgentAuth, agent = 'claude_code') {
const a = path.join(tmpRoot, 'dep-a');
const b = path.join(tmpRoot, 'dep-b');
fs.mkdirSync(a, { recursive: true });
fs.mkdirSync(b, { recursive: true });
let list: Rec[] = [
{ deployId: 'dep-a', cwd: a, agent: 'claude_code', startedAt: 1 },
{ deployId: 'dep-a', cwd: a, agent, startedAt: 1 },
{ deployId: 'dep-b', cwd: b, agent: 'codex', startedAt: 2 },
];
const spawned: Array<{ env: Record<string, string>; proc: ReturnType<typeof fakeProc> }> = [];
Expand All @@ -4360,7 +4360,7 @@ describe('HostAgentSupervisor — re-link of an env-delivered credential', () =>
id: 'cmd-refresh',
sessionId: 'sh-plugin-1',
type: 'self_hosted_refresh_credentials',
payload: { agentId: 'claude_code', sealedAgentAuth: 'sealed-fresh' },
payload: { agentId: agent, sealedAgentAuth: 'sealed-fresh' },
});
return { sup, spawned, refresh };
}
Expand Down Expand Up @@ -4417,6 +4417,30 @@ describe('HostAgentSupervisor — re-link of an env-delivered credential', () =>
sup.stop();
}
});

// agy holds its token in memory (verified 2026-11-06: a rewritten token file is never read
// again by the running process), so a re-link must restart its sessions or they keep the
// expired login.
it('oauth_token for Antigravity: rewrites its token file AND restarts only its children', async () => {
const blob =
'{"token":{"access_token":"ya29.fresh","token_type":"Bearer","refresh_token":"1//fresh","expiry":"2099-01-01T00:00:00Z"},"auth_method":"consumer"}';
const { sup, spawned, refresh } = boot({ kind: 'oauth_token', value: blob }, 'antigravity');
try {
const oldA = bySession(spawned, 'sess-a')[0].proc;
const oldB = bySession(spawned, 'sess-b')[0].proc;

await refresh();

const file = path.join(tmpRoot, '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
expect(JSON.parse(fs.readFileSync(file, 'utf8')).token.access_token).toBe('ya29.fresh');
expect(oldA.kill).toHaveBeenCalledWith('SIGTERM');
expect(bySession(spawned, 'sess-a')).toHaveLength(2);
expect(oldB.kill).not.toHaveBeenCalled();
expect(bySession(spawned, 'sess-b')).toHaveLength(1);
} finally {
sup.stop();
}
});
});

/**
Expand Down
21 changes: 21 additions & 0 deletions apps/cli/src/commands/host-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ const HEARTBEAT_INTERVAL_MS = 20_000;
*/
export const RESUME_RETRY_BACKOFF_MS = [15_000, 30_000, 60_000, 120_000] as const;

/**
* Agents whose login FILE is read once at start and then held in memory, so rewriting it on a
* re-link changes nothing for a running session: those children are restarted (resume path, same
* conversation) like an env-delivered key. Antigravity CLI: verified 2026-11-06 on agy 1.5.2 — after
* a rewrite of `~/.gemini/antigravity-cli/antigravity-oauth-token`, the next prompt never read the
* file again (its atime stayed behind the rewrite under relatime). Claude Code and Codex re-read
* theirs on a 401 and stay in place.
*/
export const AUTH_FILE_CACHED_IN_MEMORY: ReadonlySet<string> = new Set(['antigravity']);

/** A child must have been up this long before its death counts as a crash to respawn
* (shorter = it never came up → report `failed` instead of looping). */
export const CRASH_RESPAWN_MIN_UPTIME_MS = 60_000;
Expand Down Expand Up @@ -2984,6 +2994,17 @@ export class HostAgentSupervisor {
const auth = await this.resolveAgentAuth(this.identity, payload.sealedAgentAuth);
const credEnv = provisionAgentCredentials(payload.agentId, auth, undefined);
const affected = [...this.children.values()].filter((c) => c.agent === payload.agentId);
if (Object.keys(credEnv).length === 0 && AUTH_FILE_CACHED_IN_MEMORY.has(payload.agentId)) {
for (const child of affected) {
this.credentialEnvByDeploy.delete(child.deployId);
this.restartWithFreshCredentials(child);
}
log.info(
'host-agent',
`refreshed login file for agent=${payload.agentId}; restarted ${affected.length} child(ren)`,
);
return;
}
if (Object.keys(credEnv).length === 0) {
for (const c of affected) this.credentialEnvByDeploy.delete(c.deployId);
log.info(
Expand Down
Loading