mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(antigravity): supervise POSIX stdin reader directly
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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})`)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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('')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user