mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix: refuse Antigravity readiness while host is unverifiable
This commit is contained in:
@@ -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) &&
|
||||
|
||||
Reference in New Issue
Block a user