fix: refuse Antigravity readiness while host is unverifiable

This commit is contained in:
Neil
2026-09-19 01:04:19 -07:00
parent dfbb928816
commit 49a85bfad5
5 changed files with 88 additions and 1 deletions
@@ -1,5 +1,20 @@
# Antigravity readiness: what the transcripts show
## 2026-09-19: host contact and live prompt submission
A regression marked an SSH terminal `unverifiable` after caching a ready screen.
The wait incorrectly returned ready. Readiness now refuses that cached verdict
while host contact is unverifiable, and the adopted-screen fallback rechecks
liveness when an outstanding snapshot completes. Both cases pass runtime tests;
this is simulated host loss, not a claim of real SSH validation. Reconnection
snapshot freshness remains a separate validation task.
In the hidden app, a real `terminal.send` with text plus Enter submitted a harmless
prompt to installed agy. The rendered request failed with HTTP 401 and
`ACCESS_TOKEN_TYPE_UNSUPPORTED`; the subsequent empty composer satisfied the
readiness wait. The public RPC's guarded prompt route is limited to Claude/Codex,
so this agy check proves ordinary input delivery, not guarded worker submission.
## 2026-09-19: ordinary wait and delivery integration
The ordinary wait paths now consult the same current-screen classifier for
@@ -127,4 +127,23 @@ describe('Antigravity ordinary waits use the current screen', () => {
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-plan-127'), Date.now())
await vi.waitFor(() => expect(deliver).toHaveBeenCalled(), { timeout: 4500 })
}, 10000)
it('does not reuse a ready screen after the execution host becomes unverifiable', async () => {
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'agy',
foregroundProcess: 'agy',
data: '',
connectionId: 'ssh-host'
})
runtime.seedHeadlessTerminal(TRANSCRIPT_PANE_PTY_ID, '\x1b[0m', { cols: 120, rows: 40 })
runtime.onPtyData(TRANSCRIPT_PANE_PTY_ID, capture('antigravity-ready-default-127'), Date.now())
await expect(
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 3500 })
).resolves.toMatchObject({ satisfied: true })
runtime.markPtyLivenessUnverifiable(TRANSCRIPT_PANE_PTY_ID, 'SSH transport disconnected')
await expect(
runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 2500 })
).rejects.toThrow('timeout')
expect(runtime.getPtyLivenessVerdict(TRANSCRIPT_PANE_PTY_ID)?.status).toBe('unverifiable')
}, 10000)
})
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { HeadlessEmulator } from '../daemon/headless-emulator'
import { createTranscriptPane } from './agent-transcript-pane-test-harness'
import { createTranscriptPane, TRANSCRIPT_PANE_PTY_ID } from './agent-transcript-pane-test-harness'
import { projectTerminalVisibleLines } from './orca-runtime-terminal-projection'
vi.mock('electron', () => ({
@@ -58,4 +58,48 @@ describe('Antigravity adopted-terminal visible readiness probe', () => {
}
})
}
it('does not accept a snapshot that finishes after losing contact with its host', async () => {
const emulator = new HeadlessEmulator({ cols: 120, rows: 40, scrollback: 0 })
try {
await emulator.write(
readFileSync(join(__dirname, '__fixtures__', 'antigravity-ready-default-127.txt'), 'utf8')
)
const screen = projectTerminalVisibleLines(emulator)
const { runtime, handle } = await createTranscriptPane({
paneTitle: 'agy',
foregroundProcess: 'agy',
data: '',
connectionId: 'ssh-host'
})
let release = () => {}
const pending = new Promise<void>((resolve) => {
release = resolve
})
const read = vi.spyOn(runtime, 'readTerminal').mockImplementation(async () => {
await pending
return {
handle,
status: 'running',
tail: screen.lines,
draft: screen.draft,
source: 'screen',
truncated: false,
limited: false,
oldestCursor: '0',
nextCursor: '0',
latestCursor: '0',
returnedLineCount: screen.lines.length
}
})
const waiting = runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 600 })
const outcome = expect(waiting).rejects.toThrow('timeout')
expect(read).toHaveBeenCalled()
runtime.markPtyLivenessUnverifiable(TRANSCRIPT_PANE_PTY_ID, 'SSH transport disconnected')
release()
await outcome
} finally {
emulator.dispose()
}
})
})
@@ -64,6 +64,12 @@ export class OrcaRuntimeWithStartTuiIdleVisibleReadProbe extends OrcaRuntimeWith
) {
return
}
const ptyId =
this.getLivePtyForHandle(waiter.handle)?.pty.ptyId ??
this.getLiveLeafForHandle(waiter.handle).leaf.ptyId
if (ptyId && this.getPtyLivenessVerdict(ptyId)?.status === 'unverifiable') {
return
}
const snapshotText = projection.tail.join('\n')
const blockedReason = detectTerminalWaitBlockedReason(snapshotText)
if (!blockedReason && !isKnownReadyTerminalScreen(projection)) {
@@ -27,6 +27,9 @@ export class OrcaRuntimeWithVisibleSnapshotPreview extends OrcaRuntimeWithCaptur
) {
return null
}
if (this.getPtyLivenessVerdict(ptyId)?.status === 'unverifiable') {
return { ready: false, blockedReason: null }
}
const cached = this.providerVisibleStateByPtyId.get(ptyId)
if (
cached?.generation === this.getPtyLifecycleGeneration(ptyId) &&