fix(antigravity): supervise POSIX stdin reader directly

This commit is contained in:
Brennan Benson
2026-08-23 14:35:16 -07:00
parent 7e5c5d6086
commit d9179c8b7f
6 changed files with 77 additions and 29 deletions
@@ -147,12 +147,16 @@ function readGeneratedScripts(home, minMtime) {
guardIndex === -1 ||
captureIndex === -1 ||
captureIndex <= guardIndex ||
!body.includes('kill -9 "$_orca_cat"')
!body.includes('if command -p cat </dev/null >/dev/null 2>&1; then') ||
!body.includes('command -p cat <&3 2>/dev/null &') ||
!body.includes('command cat <&3 2>/dev/null &') ||
!body.includes('kill -9 "$_orca_reader"') ||
!body.includes('wait "$_orca_reader" 2>/dev/null || :')
) {
throw new Error([fileName, ' is missing the bounded stdin watchdog'].join(''))
}
} else {
const captureIndex = body.indexOf('payload=$(cat)')
const captureIndex = body.indexOf('payload=$({ command -p cat 2>/dev/null || cat; })')
const firstExitIndex = body.indexOf('exit 0')
if (captureIndex === -1 || firstExitIndex <= captureIndex) {
throw new Error([fileName, ' can exit before capturing stdin'].join(''))
@@ -216,7 +220,7 @@ async function verifyNoOpWrites(scripts, home, payload) {
: (process.env.PATH ?? '/usr/bin:/bin')
const result = await runShell(
['/bin/sh ', JSON.stringify(script.path)].join(''),
payload,
script.fileName === 'antigravity-hook.sh' ? '' : payload,
withoutOrcaEnvironment({
HOME: home,
PATH: path,
+2 -1
View File
@@ -4,7 +4,8 @@ export type PosixHookEmptyPayloadPolicy = 'exit' | 'empty-object'
// sees exit 127 and a broken pipe mid-write (#8110). `command -p` resolves from
// the shell's built-in default PATH, so it also survives hosts without /bin/cat
// (NixOS) and ignores a worktree-local `cat` that could capture the payload.
export const POSIX_HOOK_STDIN_READER = '{ command -p cat 2>/dev/null || cat; }'
export const POSIX_HOOK_STDIN_READER_COMMAND = 'command -p cat'
export const POSIX_HOOK_STDIN_READER = `{ ${POSIX_HOOK_STDIN_READER_COMMAND} 2>/dev/null || cat; }`
export const POSIX_HOOK_STDIN_DRAIN_COMMAND = `${POSIX_HOOK_STDIN_READER} >/dev/null 2>&1 || :`
// Why: every POSIX hook must own stdin before any no-op exit; sharing this
@@ -62,7 +62,7 @@ import { GrokHookService } from '../grok/hook-service'
import { KimiHookService } from '../kimi/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
import { wrapPosixHookCommand, wrapWindowsHookCommand } from './installer-utils'
import { POSIX_HOOK_STDIN_READER } from './hook-stdin-contract'
import { POSIX_HOOK_STDIN_READER, POSIX_HOOK_STDIN_READER_COMMAND } from './hook-stdin-contract'
import { wrapRuntimeHomeHookCommand } from './runtime-home-hook-command'
import { createAgentHookMemorySftp } from './agent-hook-memory-sftp.test-fixture'
import { findGitBash } from './windows-git-bash-path.test-fixture'
@@ -479,7 +479,16 @@ describe.skipIf(process.platform === 'win32')('managed hook stdin lifecycle', ()
const captureIndex = script.indexOf('exec 3<&0')
expect(guardIndex, `${agent} env guard`).toBeGreaterThanOrEqual(0)
expect(captureIndex, `${agent} bounded capture`).toBeGreaterThan(guardIndex)
expect(script, `${agent} watchdog`).toContain('kill -9 "$_orca_cat"')
expect(script, `${agent} reader probe`).toContain(
`if ${POSIX_HOOK_STDIN_READER_COMMAND} </dev/null >/dev/null 2>&1; then`
)
expect(script, `${agent} qualified reader`).toContain(
`${POSIX_HOOK_STDIN_READER_COMMAND} <&3 2>/dev/null &`
)
expect(script, `${agent} fallback reader`).toContain('command cat <&3 2>/dev/null &')
expect(script, `${agent} grouped reader`).not.toContain(`${POSIX_HOOK_STDIN_READER} <&3 &`)
expect(script, `${agent} watchdog`).toContain('kill -9 "$_orca_reader"')
expect(script, `${agent} reader wait`).toContain('wait "$_orca_reader" 2>/dev/null || :')
continue
}
const captureIndex = script.indexOf(`payload=$(${POSIX_HOOK_STDIN_READER})`)
+10 -6
View File
@@ -1,7 +1,7 @@
import {
buildWindowsHookEnvironmentGuardLines,
buildWindowsHookStdinDrainEpilogue,
POSIX_HOOK_STDIN_READER,
POSIX_HOOK_STDIN_READER_COMMAND,
WINDOWS_HOOK_STDIN_DRAIN_COMMAND
} from '../agent-hooks/hook-stdin-contract'
import { buildWindowsAgentHookPostCommand } from '../agent-hooks/installer-utils'
@@ -68,13 +68,17 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string {
// Detach the sleeper from this capture so closed-stdin events do not wait the bound.
'exec 3<&0',
'payload=$(',
` ${POSIX_HOOK_STDIN_READER} <&3 &`,
' _orca_cat=$!',
` ( command -p sleep ${ANTIGRAVITY_POSIX_STDIN_WATCHDOG_SECONDS} && kill -9 "$_orca_cat" 2>/dev/null ) >/dev/null 2>&1 &`,
` if ${POSIX_HOOK_STDIN_READER_COMMAND} </dev/null >/dev/null 2>&1; then`,
` ${POSIX_HOOK_STDIN_READER_COMMAND} <&3 2>/dev/null &`,
' else',
' command cat <&3 2>/dev/null &',
' fi',
' _orca_reader=$!',
` ( command -p sleep ${ANTIGRAVITY_POSIX_STDIN_WATCHDOG_SECONDS} && kill -9 "$_orca_reader" 2>/dev/null ) >/dev/null 2>&1 &`,
' _orca_watch=$!',
' wait "$_orca_cat"',
' wait "$_orca_reader" 2>/dev/null || :',
' kill -9 "$_orca_watch" 2>/dev/null',
' wait "$_orca_watch" 2>/dev/null',
' wait "$_orca_watch" 2>/dev/null || :',
')',
'exec 3<&-',
'if [ -z "$payload" ]; then',
+15 -4
View File
@@ -17,7 +17,10 @@ vi.mock('os', async () => {
})
import { AntigravityHookService } from './hook-service'
import { POSIX_HOOK_STDIN_READER } from '../agent-hooks/hook-stdin-contract'
import {
POSIX_HOOK_STDIN_READER,
POSIX_HOOK_STDIN_READER_COMMAND
} from '../agent-hooks/hook-stdin-contract'
import { createManagedCommandMatcher } from '../agent-hooks/installer-utils'
const ANTIGRAVITY_SCRIPT_FILE_NAME =
@@ -106,11 +109,19 @@ describe('AntigravityHookService', () => {
expect(script).toContain('setlocal DisableDelayedExpansion')
} else {
expect(script).toContain('hook_event_name=${ORCA_ANTIGRAVITY_EVENT}')
expect(script).toContain(`${POSIX_HOOK_STDIN_READER} <&3 &`)
expect(script).toContain(
`if ${POSIX_HOOK_STDIN_READER_COMMAND} </dev/null >/dev/null 2>&1; then`
)
expect(script).toContain(`${POSIX_HOOK_STDIN_READER_COMMAND} <&3 2>/dev/null &`)
expect(script).toContain('command cat <&3 2>/dev/null &')
expect(script).not.toContain(`${POSIX_HOOK_STDIN_READER} <&3 &`)
expect(script).toContain('command -p sleep')
expect(script).toContain('kill -9 "$_orca_cat"')
expect(script).toContain('kill -9 "$_orca_reader"')
expect(script).toContain('wait "$_orca_reader" 2>/dev/null || :')
expect(script).toContain('exec 3<&0')
expect(script.indexOf('[ -z "$ORCA_PANE_KEY" ]')).toBeLessThan(script.indexOf('_orca_cat=$!'))
expect(script.indexOf('[ -z "$ORCA_PANE_KEY" ]')).toBeLessThan(
script.indexOf('_orca_reader=$!')
)
expect(script).toContain("payload='{}'")
expect(script).not.toContain('if [ -z "$payload" ]; then\n exit 0\nfi')
// Why: payload is piped to curl via stdin (`payload@-`) so it never lands
@@ -3,7 +3,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { spawn } from 'node:child_process'
import { createServer, type Server } from 'node:http'
import { mkdtempSync, rmSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type * as osModule from 'node:os'
@@ -38,7 +38,7 @@ const CLOSED_PAYLOAD = `${JSON.stringify({
toolCall: { name: 'run_command', args: { CommandLine: 'pwd' } },
transcriptPath: '/tmp/antigravity-transcript.jsonl'
})}\n`
const ABANDONED_LINE_PAYLOAD = `${JSON.stringify({ fullyIdle: false })}\n`
const ABANDONED_LINE_PAYLOAD = `${JSON.stringify({ fullyIdle: false })} \n`
type HookPost = {
payload: string | null
@@ -81,11 +81,13 @@ async function startHookListener(): Promise<{
type HookRun = {
exitCode: number | null
stdout: string
stderr: string
timedOut: boolean
elapsedMs: number
}
const EXIT_BUDGET_MS = (ANTIGRAVITY_POSIX_STDIN_WATCHDOG_SECONDS + 2) * 1000
// Includes curl's 1.5s bound and the listener settle delay.
const EXIT_BUDGET_MS = (ANTIGRAVITY_POSIX_STDIN_WATCHDOG_SECONDS + 3) * 1000
function runScript(
scriptPath: string,
@@ -97,11 +99,12 @@ function runScript(
const startedAt = Date.now()
const child = spawn('/bin/sh', [scriptPath], { stdio: ['pipe', 'pipe', 'pipe'], env })
let stdout = ''
let stderr = ''
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
child.kill('SIGKILL')
}, EXIT_BUDGET_MS + 1000)
}, EXIT_BUDGET_MS)
child.on('error', (error) => {
clearTimeout(timer)
reject(error)
@@ -109,7 +112,9 @@ function runScript(
child.stdout.on('data', (chunk: Buffer) => {
stdout += chunk.toString()
})
child.stderr.on('data', () => {})
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString()
})
child.on('close', (exitCode) => {
clearTimeout(timer)
setTimeout(
@@ -117,6 +122,7 @@ function runScript(
resolve({
exitCode,
stdout,
stderr,
timedOut,
elapsedMs: Date.now() - startedAt
}),
@@ -206,17 +212,23 @@ describe.skipIf(process.platform === 'win32')('Antigravity POSIX hook payload de
expect(result.exitCode).toBe(0)
expect(result.elapsedMs).toBeLessThan(EXIT_BUDGET_MS)
expect(result.stdout.trim()).toBe(expectedStdout(eventName))
expect(
posts.some((post) => post.hookEventName === eventName && post.token === HOOK_TOKEN)
).toBe(true)
expect(result.stderr).toBe('')
expect(posts).toEqual([{ payload: '{}', hookEventName: eventName, token: HOOK_TOKEN }])
}
)
it('exits when a payload line is written but stdin is never closed', async () => {
const { scriptPath, port, posts } = await installAndListen()
const endpointPath = join(home!, 'empty-path-endpoint.sh')
writeFileSync(endpointPath, 'curl() { /usr/bin/curl "$@"; }\n')
const result = await runScript(
scriptPath,
{ ...orcaEnv(port), ORCA_ANTIGRAVITY_EVENT: 'Stop' },
{
...orcaEnv(port),
PATH: '',
ORCA_AGENT_HOOK_ENDPOINT: endpointPath,
ORCA_ANTIGRAVITY_EVENT: 'Stop'
},
ABANDONED_LINE_PAYLOAD,
false
)
@@ -225,9 +237,14 @@ describe.skipIf(process.platform === 'win32')('Antigravity POSIX hook payload de
expect(result.exitCode).toBe(0)
expect(result.elapsedMs).toBeLessThan(EXIT_BUDGET_MS)
expect(result.stdout.trim()).toBe('{"decision":""}')
expect(posts.some((post) => post.hookEventName === 'Stop' && post.token === HOOK_TOKEN)).toBe(
true
)
expect(result.stderr).toBe('')
expect(posts).toEqual([
{
payload: ABANDONED_LINE_PAYLOAD.slice(0, -1),
hookEventName: 'Stop',
token: HOOK_TOKEN
}
])
})
it('posts a closed-stdin Stop payload byte-exact', async () => {
@@ -242,9 +259,10 @@ describe.skipIf(process.platform === 'win32')('Antigravity POSIX hook payload de
expect(result.timedOut).toBe(false)
expect(result.exitCode).toBe(0)
expect(result.stdout.trim()).toBe('{"decision":""}')
expect(result.stderr).toBe('')
expect(posts).toHaveLength(1)
expect(posts[0]?.hookEventName).toBe('Stop')
expect(posts[0]?.payload).toBe(CLOSED_PAYLOAD.trimEnd())
expect(posts[0]?.payload).toBe(CLOSED_PAYLOAD.slice(0, -1))
})
it('exits without waiting the watchdog when Orca env is missing', async () => {
@@ -265,5 +283,6 @@ describe.skipIf(process.platform === 'win32')('Antigravity POSIX hook payload de
expect(result.exitCode).toBe(0)
expect(result.elapsedMs).toBeLessThan(500)
expect(result.stdout.trim()).toBe(ANTIGRAVITY_PRE_TOOL_USE_DECISION)
expect(result.stderr).toBe('')
})
})