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
28 changes: 28 additions & 0 deletions docs/reference/antigravity-readiness-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,31 @@ without transcript truncation. The final identity-checked implementation passed
another delayed launch with an exact 920-character first-turn match. Generation
still failed with the existing 401 authentication error; this verifies delivery,
not a successful continuation task. Real Windows/WSL/SSH runs remain unverified.

## Native Windows command approval — 1.2.7 (2026-09-20)

`antigravity-windows-command-approval.txt` records an actual authenticated tool
turn, stopped while the four-choice “Run this command?” dialog owns the screen.
`antigravity-windows-command-cancelled.txt` records another harmless command turn
and Escape dismissing that dialog back to the empty composer. Both used the
repository PTY recorder on native Windows at 120×40, with account information
hidden. The command executable's OS username is replaced with a same-length
placeholder in the transcript and sidecar. Both pass the transcript secret scan.

The approval screen ends with the navigation hint and `esc to cancel`; its four
choices distinguish it from ordinary working output. The runtime regression
previously timed out without a blocked reason. The current screen classifier
reports `agent-approval-prompt`; the cancelled capture must become ready even
though its retained output contains the earlier menu.

This establishes readiness detection only. Hook-owned permission publication,
Chat approval controls, selection changes, narrower widths and other platforms
still need verification. Do not infer permission from `PreToolUse`: agy can
execute an already-allowed tool without waiting for a user decision.

`antigravity-windows-command-allow-key.txt` additionally records the pending menu,
sending the single key `1` (no Enter), successful harmless command execution and
return to the empty composer. This verifies the existing Chat approval card's
one-time acceptance key on native Windows 1.2.7. The recorder metadata confirms
completion; the capture was transferred as raw UTF-8 bytes without newline
normalization and its username was replaced with a same-length placeholder.
14 changes: 14 additions & 0 deletions docs/site/content/docs/agents/antigravity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ Orca launches Google's **Antigravity CLI**, whose command is `agy`. Installing t
Antigravity desktop application alone does not establish that this command is available.
The separate `gemini` command is a different agent entry in Orca.

## Existing Gemini CLI sessions

Orca labels the Gemini CLI integration as **legacy** and recommends Antigravity for
new Google CLI sessions. If both CLIs are detected and no preferred agent is selected,
Antigravity comes before Gemini in automatic selection. An explicit Gemini preference
still launches Gemini; disabling or not installing Antigravity does not remove the
Gemini fallback.

Existing Gemini history, session resume, and account settings remain available.
Gemini account settings do not authenticate Antigravity, and a Gemini session ID
cannot be reused as an Antigravity conversation ID. Authenticate `agy` separately
on its execution host. Gemini model names returned by `agy models` are still valid
Antigravity model names; the legacy label refers to the CLI provider.

## Check the CLI first

Install the CLI using [Google's current instructions](https://antigravity.google/docs/cli-overview).
Expand Down
155 changes: 155 additions & 0 deletions src/main/agent-hooks/server-ingest-antigravity-screen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from './server'
import { PANE } from './server.test-fixtures'

vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn(() => ({})) }))

beforeEach(() => _internals.resetCachesForTests())

function workingPane(
agentType = 'antigravity',
connectionId: string | null = null
): AgentHookServer {
const server = new AgentHookServer()
server.ingestTerminalStatus({
paneKey: PANE,
terminalHandle: 'term-screen',
connectionId,
payload: { state: 'working', agentType, prompt: 'print marker', toolName: 'run_command' }
})
return server
}

function baseline(server: AgentHookServer) {
const row = server.getStatusSnapshot()[0]
if (!row) {
throw new Error('Missing test status row')
}
return row
}

describe('host-owned Antigravity screen permission', () => {
it('publishes permission through the canonical store and clears only its own prompt', () => {
const server = workingPane()
const listener = vi.fn()
server.setListener(listener)
listener.mockClear()
expect(
server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: 'echo OK' })
).toBe(true)
expect(baseline(server)).toMatchObject({
state: 'waiting',
toolName: 'run_command',
observation: { origin: 'process' }
})
expect(JSON.parse(baseline(server).interactivePrompt ?? '')).toEqual({
approval: { source: 'antigravity-screen', tool: 'run_command', summary: 'echo OK' }
})
expect(listener).toHaveBeenCalledTimes(1)
expect(
server.ingestAntigravityScreenPermission({
baseline: baseline(server),
command: null,
clearedState: 'done'
})
).toBe(true)
expect(baseline(server)).toMatchObject({ state: 'done', prompt: 'print marker' })
expect(baseline(server).interactivePrompt).toBeUndefined()
expect(baseline(server).toolName).toBeUndefined()
expect(listener).toHaveBeenCalledTimes(2)
})

it('does not overwrite a newer provider state with a late snapshot', () => {
const server = workingPane()
const old = baseline(server)
server.ingestTerminalStatus({
paneKey: PANE,
terminalHandle: 'term-screen',
payload: { state: 'done', agentType: 'antigravity', prompt: 'print marker' }
})
expect(server.ingestAntigravityScreenPermission({ baseline: old, command: 'echo OK' })).toBe(
false
)
expect(baseline(server).state).toBe('done')
})

it('rejects another terminal incarnation and missing observation proof', () => {
const server = workingPane()
const row = baseline(server)
expect(
server.ingestAntigravityScreenPermission({
baseline: { ...row, terminalHandle: 'replaced' },
command: 'echo OK'
})
).toBe(false)
expect(
server.ingestAntigravityScreenPermission({
baseline: { ...row, observation: undefined },
command: 'echo OK'
})
).toBe(false)
})

it.each([
['claude', null],
['antigravity', 'ssh-connection']
] as const)('does not infer permission for %s on %s', (agent, connection) => {
const server = workingPane(agent, connection)
expect(
server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: 'echo OK' })
).toBe(false)
})

it('does not clear a permission prompt from another producer', () => {
const server = workingPane()
server.ingestTerminalStatus({
paneKey: PANE,
terminalHandle: 'term-screen',
payload: {
state: 'waiting',
agentType: 'antigravity',
prompt: 'print marker',
interactivePrompt: '{"questions":[]}'
}
})
expect(
server.ingestAntigravityScreenPermission({
baseline: baseline(server),
command: null,
clearedState: 'done'
})
).toBe(false)
expect(baseline(server).state).toBe('waiting')
})

it('coalesces an unchanged permission observation without refreshing its revision', () => {
const server = workingPane()
server.ingestAntigravityScreenPermission({ baseline: baseline(server), command: 'echo OK' })
const waiting = baseline(server)
expect(
server.ingestAntigravityScreenPermission({ baseline: waiting, command: 'echo OK' })
).toBe(true)
expect(baseline(server).observation).toEqual(waiting.observation)
})
})

it('accepts the runtime-attested binding absent from a native hook row', () => {
const server = new AgentHookServer()
server.ingestTerminalStatus({
paneKey: PANE,
payload: { state: 'working', agentType: 'antigravity', prompt: 'print marker' }
})
const row = baseline(server)
expect(row.terminalHandle).toBeUndefined()
expect(server.ingestAntigravityScreenPermission({ baseline: row, command: 'echo OK' })).toBe(
false
)
expect(
server.ingestAntigravityScreenPermission({
baseline: { ...row, terminalHandle: 'term-runtime' },
command: 'echo OK'
})
).toBe(true)
expect(baseline(server)).toMatchObject({ state: 'waiting', terminalHandle: 'term-runtime' })
})
79 changes: 79 additions & 0 deletions src/main/agent-hooks/server/server-ingest-antigravity-screen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { AgentStatusIpcPayload } from '../../../shared/agent-status-types'
import type { EnrichedAgentHookEventPayload } from './server-types'
import { AgentHookServerIngestNormalization } from './server-ingest-normalization'

export type AntigravityScreenPermissionObservation = {
baseline: Pick<AgentStatusIpcPayload, 'paneKey' | 'terminalHandle' | 'observation'>
} & ({ command: string } | { command: null; clearedState: 'working' | 'done' })

const SCREEN_APPROVAL_PREFIX = '{"approval":{"source":"antigravity-screen",'

export abstract class AgentHookServerIngestAntigravityScreen extends AgentHookServerIngestNormalization {
ingestAntigravityScreenPermission(request: AntigravityScreenPermissionObservation): boolean {
const { baseline, command } = request
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This server's canonical row writer stores enriched events in the shared listener map.
const previous = this.state.lastStatusByPaneKey.get(baseline.paneKey) as
| EnrichedAgentHookEventPayload
| undefined
const expected = baseline.observation
const current = previous?.observation
if (
!previous ||
previous.restoredUnconfirmed ||
previous.connectionId ||
previous.payload.agentType !== 'antigravity' ||
!baseline.terminalHandle ||
(previous.terminalHandle !== undefined &&
previous.terminalHandle !== baseline.terminalHandle) ||
!expected ||
!current ||
current.authorityId !== expected.authorityId ||
current.incarnation !== expected.incarnation ||
current.revision !== expected.revision ||
this.getAgentStatusDisposition(baseline.paneKey) !== 'accept'
) {
return false
}
const ownsPrompt =
current.origin === 'process' &&
previous.payload.state === 'waiting' &&
previous.payload.interactivePrompt?.startsWith(SCREEN_APPROVAL_PREFIX) === true
if (command === null ? !ownsPrompt : previous.payload.state !== 'working' && !ownsPrompt) {
return false
}
const interactivePrompt =
command === null
? undefined
: JSON.stringify({
approval: {
source: 'antigravity-screen',
tool: previous.payload.toolName ?? 'run_command',
summary: command
}
})
if (interactivePrompt === previous.payload.interactivePrompt) {
return true
}
// Only a positively identified working or idle screen can clear our approval.
return (
this.applyNormalizedStatus(
{
paneKey: previous.paneKey,
tabId: previous.tabId,
worktreeId: previous.worktreeId,
connectionId: previous.connectionId,
terminalHandle: baseline.terminalHandle,
providerSession: previous.providerSession,
payload: {
...previous.payload,
state: request.command === null ? request.clearedState : 'waiting',
...(command === null ? { toolName: undefined, toolInput: undefined } : {}),
interactivePrompt
}
},
undefined,
'process'
) !== undefined
)
}
}
4 changes: 2 additions & 2 deletions src/main/agent-hooks/server/server-ingest-terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../shared/stable-
import { terminalStatusPayloadMatchesHook } from '../../../shared/agent-terminal-status-equivalence'
import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types'
import type { EnrichedAgentHookEventPayload } from './server-types'
import { AgentHookServerIngestNormalization } from './server-ingest-normalization'
import { AgentHookServerIngestAntigravityScreen } from './server-ingest-antigravity-screen'

export abstract class AgentHookServerIngestTerminal extends AgentHookServerIngestNormalization {
export abstract class AgentHookServerIngestTerminal extends AgentHookServerIngestAntigravityScreen {
ingestTerminalStatus(event: {
ptyId?: string
paneKey: string
Expand Down
1 change: 1 addition & 0 deletions src/main/orcad/orcad-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ async function startOrcadRuntime(
// PTY agent on this host, and the store is the only place `worktree.ps` and the mobile
// projection read from — unwired, orcad lists no PTY agents at all.
onTerminalAgentStatus: (event) => agentHookServer.ingestTerminalStatus(event),
onTerminalScreenPermission: (event) => agentHookServer.ingestAntigravityScreenPermission(event),
// Why here too and not only on the desktop: orcad serves `worktree.ps` and `agentSession.*`,
// so without these a headless host publishes its structured chats nowhere and lists no agents.
getAgentStatusSnapshot: () =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"capturedAt": "2026-09-20T10:03:18.668Z",
"platform": "win32",
"command": ["C:\\Users\\user\\AppData\\Local\\agy\\bin\\agy.exe"],
"cols": 120,
"rows": 40,
"note": "Native Windows agy 1.2.7; approved 20-second command still running; key 1 at 45 seconds; 120x40.",
"exitCode": 1
}
Loading
Loading