fix(agents): route WSL OMP hooks to the Windows listener (#7565) (#7641)

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
Rod Boev 2026-07-08 20:05:21 -04:00 committed by GitHub
parent 95f3933ea2
commit 01d21cdce1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 398 additions and 0 deletions

View File

@ -0,0 +1,316 @@
import { runInNewContext } from 'node:vm'
import ts from 'typescript'
import { describe, expect, it, vi } from 'vitest'
import { getPiAgentStatusExtensionSource } from './agent-status-extension-source'
type HookHandler = (event?: unknown) => Promise<void> | void
type FakeCurlChild = {
on: ReturnType<typeof vi.fn>
stdin: {
on: ReturnType<typeof vi.fn>
end: ReturnType<typeof vi.fn>
}
}
type Harness = {
fetchMock: ReturnType<typeof vi.fn>
spawnMock: ReturnType<typeof vi.fn>
spawnedChildren: FakeCurlChild[]
fsMock: {
existsSync: ReturnType<typeof vi.fn>
readFileSync: ReturnType<typeof vi.fn>
}
handlers: Record<string, HookHandler>
callHook: (name: string, event?: unknown) => Promise<void>
}
const BASE_ENV = {
ORCA_PANE_KEY: 'pane-1',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-1',
ORCA_TAB_ID: 'tab-1',
ORCA_WORKTREE_ID: 'tree-1',
ORCA_AGENT_HOOK_PORT: '4321',
ORCA_AGENT_HOOK_TOKEN: 'token-1',
ORCA_AGENT_HOOK_ENV: 'env-1',
ORCA_AGENT_HOOK_VERSION: '1.2.3'
} satisfies Record<string, string>
function createHarness(args: {
kind: 'pi' | 'omp'
env?: Record<string, string | undefined>
title?: string
argv?: string[]
existsSync?: (path: string) => boolean
readFileSync?: (path: string, encoding: string) => string
fetchImpl?: (...params: Parameters<typeof fetch>) => Promise<unknown>
}): Harness {
const fetchMock = vi.fn(
args.fetchImpl ??
(async () => ({
ok: true
}))
)
const spawnedChildren: FakeCurlChild[] = []
const spawnMock = vi.fn(() => {
const child: FakeCurlChild = {
on: vi.fn(),
stdin: {
on: vi.fn(),
end: vi.fn()
}
}
spawnedChildren.push(child)
return child
})
const fsMock = {
existsSync: vi.fn(args.existsSync ?? (() => false)),
readFileSync: vi.fn(
args.readFileSync ??
((path: string) => {
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
})
)
}
const module = {
exports: {} as { default?: (pi: { on: (name: string, handler: HookHandler) => void }) => void }
}
const requireMock = vi.fn((specifier: string) => {
if (specifier === 'fs') {
return fsMock
}
if (specifier === 'child_process') {
return { spawn: spawnMock }
}
throw new Error(`unexpected require(${specifier})`)
})
const processMock = {
env: {
...BASE_ENV,
...args.env
},
title: args.title ?? 'node',
argv: args.argv ?? ['node', '/usr/bin/orca']
}
const context = {
module,
exports: module.exports,
require: requireMock,
process: processMock,
fetch: fetchMock,
console: {
warn: vi.fn(),
error: vi.fn(),
log: vi.fn()
},
Promise,
Buffer,
URL,
setTimeout,
clearTimeout
} as Record<string, unknown>
context.globalThis = context
const source = getPiAgentStatusExtensionSource(args.kind)
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020
}
}).outputText
runInNewContext(output, context)
const register = module.exports.default
if (!register) {
throw new Error('expected default export from generated source')
}
const handlers: Record<string, HookHandler> = {}
register({
on(name: string, handler: HookHandler) {
handlers[name] = handler
}
})
return {
fetchMock,
spawnMock,
spawnedChildren,
fsMock,
handlers,
callHook: async (name, event) => {
await handlers[name]?.(event)
}
}
}
describe('getPiAgentStatusExtensionSource', () => {
it('routes an OMP executable through /hook/omp', async () => {
const harness = createHarness({
kind: 'pi',
title: 'omp',
existsSync: () => false
})
await harness.callHook('agent_start')
expect(harness.fetchMock).toHaveBeenCalledTimes(1)
expect(harness.fetchMock.mock.calls[0]?.[0]).toBe('http://127.0.0.1:4321/hook/omp')
expect(harness.spawnMock).not.toHaveBeenCalled()
})
it('keeps native fetch as the only path even when the runtime looks like WSL', async () => {
const harness = createHarness({
kind: 'omp',
env: { WSL_DISTRO_NAME: 'Ubuntu' },
existsSync: () => true
})
await harness.callHook('agent_start')
expect(harness.fetchMock).toHaveBeenCalledTimes(1)
expect(harness.spawnMock).not.toHaveBeenCalled()
})
it('falls back to Windows curl from WSL when fetch fails', async () => {
const harness = createHarness({
kind: 'omp',
env: { WSL_DISTRO_NAME: 'Ubuntu' },
existsSync: (path) => path === '/mnt/c/Windows/System32/curl.exe',
fetchImpl: vi.fn(async () => {
throw new Error('loopback unreachable')
})
})
await harness.callHook('agent_start')
expect(harness.fetchMock).toHaveBeenCalledTimes(1)
expect(harness.spawnMock).toHaveBeenCalledTimes(1)
const [command, args, options] = harness.spawnMock.mock.calls[0] ?? []
expect(command).toBe('/mnt/c/Windows/System32/curl.exe')
expect(args).toEqual([
'-sS',
'--connect-timeout',
'3',
'--max-time',
'10',
'--noproxy',
'127.0.0.1',
'-o',
'NUL',
'-X',
'POST',
'-H',
'Content-Type: application/json',
'-H',
'X-Orca-Agent-Hook-Token: token-1',
'--data-binary',
'@-',
'http://127.0.0.1:4321/hook/omp'
])
// Why: delivery must be fire-and-forget off the pi event loop — no
// blocking wait — with the payload fed via stdin, never argv.
expect(options).toEqual({ stdio: ['pipe', 'ignore', 'ignore'] })
const child = harness.spawnedChildren[0]
expect(child?.on).toHaveBeenCalledWith('error', expect.any(Function))
expect(child?.stdin.on).toHaveBeenCalledWith('error', expect.any(Function))
expect(child?.stdin.end).toHaveBeenCalledWith(
JSON.stringify({
paneKey: 'pane-1',
launchToken: 'launch-1',
tabId: 'tab-1',
worktreeId: 'tree-1',
env: 'env-1',
version: '1.2.3',
payload: { hook_event_name: 'agent_start' }
})
)
})
it('probes WSL evidence and the curl path once per process', async () => {
const harness = createHarness({
kind: 'omp',
existsSync: (path) => path === '/mnt/c/Windows/System32/curl.exe',
readFileSync: (path) => {
if (path === '/proc/sys/kernel/osrelease') {
return 'microsoft-standard-WSL2'
}
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
},
fetchImpl: vi.fn(async () => {
throw new Error('loopback unreachable')
})
})
await harness.callHook('agent_start')
await harness.callHook('agent_end')
expect(harness.spawnMock).toHaveBeenCalledTimes(2)
// Why: WSL-ness and curl.exe presence are process-lifetime constants;
// the per-event failure path must not re-probe /proc or /mnt/c.
const procReads = harness.fsMock.readFileSync.mock.calls.filter(([path]) =>
String(path).startsWith('/proc/')
)
expect(procReads).toHaveLength(1)
expect(harness.fsMock.existsSync).toHaveBeenCalledTimes(1)
})
it('stays fail-open on ordinary Linux', async () => {
const harness = createHarness({
kind: 'omp',
existsSync: () => true,
fetchImpl: vi.fn(async () => {
throw new Error('loopback unreachable')
})
})
await harness.callHook('agent_start')
expect(harness.fetchMock).toHaveBeenCalledTimes(1)
expect(harness.spawnMock).not.toHaveBeenCalled()
})
it('does not treat WSLENV alone as WSL evidence', async () => {
const harness = createHarness({
kind: 'omp',
env: { WSLENV: 'FOO/u' },
existsSync: () => true,
readFileSync: (path) => {
if (path === '/proc/sys/kernel/osrelease' || path === '/proc/version') {
return 'Linux 6.8.0 generic'
}
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
},
fetchImpl: vi.fn(async () => {
throw new Error('loopback unreachable')
})
})
await harness.callHook('agent_start')
expect(harness.fetchMock).toHaveBeenCalledTimes(1)
expect(harness.spawnMock).not.toHaveBeenCalled()
})
it('remains fail-open when the Windows curl bridge is missing', async () => {
const harness = createHarness({
kind: 'omp',
env: { WSL_DISTRO_NAME: 'Ubuntu' },
existsSync: () => false,
fetchImpl: vi.fn(async () => {
throw new Error('loopback unreachable')
})
})
await expect(harness.callHook('agent_start')).resolves.toBeUndefined()
expect(harness.fetchMock).toHaveBeenCalledTimes(1)
expect(harness.spawnMock).not.toHaveBeenCalled()
})
})

View File

@ -130,6 +130,88 @@ export function getPiAgentStatusExtensionSource(kind: PiAgentKind = 'pi'): strin
' } catch {',
' // Why: status reporting must never fail the pi run just because Orca',
' // is unavailable or the loopback request failed (e.g. Orca restart).',
' if (!isWslRuntime()) return',
' postViaWindowsCurl(url, coords, body)',
' }',
'}',
'',
'// Why: WSL-ness and curl.exe presence cannot change within a process',
'// lifetime; re-probing /proc and /mnt/c on every failed event would add',
'// filesystem work to the per-event path.',
'let cachedIsWslRuntime: boolean | null = null',
'let cachedWindowsCurlPath: string | null | undefined',
'',
'function isWslRuntime(): boolean {',
' if (cachedIsWslRuntime !== null) return cachedIsWslRuntime',
' cachedIsWslRuntime = detectWslRuntime()',
' return cachedIsWslRuntime',
'}',
'',
'function detectWslRuntime(): boolean {',
' if (process.env.WSL_DISTRO_NAME) return true',
' try {',
" const fs = require('fs')",
" for (const path of ['/proc/sys/kernel/osrelease', '/proc/version']) {",
' try {',
" const contents = String(fs.readFileSync(path, 'utf8'))",
' if (/microsoft|wsl/i.test(contents)) return true',
' } catch {',
' // Why: probe the next runtime hint when a proc file is absent or unreadable.',
' }',
' }',
' } catch {',
' return false',
' }',
' return false',
'}',
'',
'function resolveWindowsCurlPath(): string | null {',
' if (cachedWindowsCurlPath !== undefined) return cachedWindowsCurlPath',
' try {',
" const fs = require('fs')",
" const curlPath = '/mnt/c/Windows/System32/curl.exe'",
' cachedWindowsCurlPath = fs.existsSync(curlPath) ? curlPath : null',
' } catch {',
' cachedWindowsCurlPath = null',
' }',
' return cachedWindowsCurlPath',
'}',
'',
'// Why: WSL loopback is not the Windows loopback, so a WSL-side POST cannot',
'// reach Orca. curl.exe runs on the Windows side, where 127.0.0.1 IS the',
'// listener Orca binds. Fire-and-forget: blocking on the spawn would stall',
'// the pi event loop (and the TUI) on every hook event.',
'function postViaWindowsCurl(url: string, coords: { token: string }, body: string): void {',
' const curlPath = resolveWindowsCurlPath()',
' if (!curlPath) return',
' try {',
" const { spawn } = require('child_process')",
' const child = spawn(',
' curlPath,',
' [',
" '-sS',",
' // Why: the spawn is detached from the event loop, so these bounds',
' // size a background process, not TUI latency. WSL->Win32 interop',
' // connects can exceed 0.5s on loaded machines (observed 3/3 drops',
' // to a healthy listener); size for delivery, not snappiness.',
" '--connect-timeout', '3',",
" '--max-time', '10',",
" '--noproxy', '127.0.0.1',",
" '-o', 'NUL',",
" '-X', 'POST',",
" '-H', 'Content-Type: application/json',",
" '-H', `X-Orca-Agent-Hook-Token: ${coords.token}`,",
" '--data-binary', '@-',",
' url',
' ],',
" { stdio: ['pipe', 'ignore', 'ignore'] }",
' )',
" child.on('error', () => {})",
" child.stdin.on('error', () => {})",
' child.stdin.end(body)',
' } catch {',
' // Why: the bridge is best-effort; a failed spawn must not surface',
' // inside the pi TUI.',
' }',
'}',
'',