Merge branch 'main' into fix/gcloud-proxy-type

This commit is contained in:
Dylan Conway
2026-05-05 13:15:34 -07:00
committed by GitHub
5 changed files with 129 additions and 59 deletions

View File

@@ -40,7 +40,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.1
bun-version: 1.3.13
- name: Install system dependencies (Linux)
if: matrix.os == 'linux'

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, afterAll } from 'bun:test'
import { spawnSync } from 'node:child_process'
import * as http from 'node:http'
import { spawnAsync } from './helpers/spawn.js'
import * as net from 'node:net'
import { SandboxManager } from '../src/sandbox/sandbox-manager.js'
import type { SandboxRuntimeConfig } from '../src/sandbox/sandbox-config.js'
@@ -394,7 +394,7 @@ describe('Configurable Proxy Ports Integration Tests', () => {
'curl -s --max-time 5 http://example.com',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 10000,

66
test/helpers/spawn.ts Normal file
View File

@@ -0,0 +1,66 @@
import { spawn } from 'node:child_process'
type RunOpts = {
shell?: boolean
encoding?: 'utf8'
timeout?: number
cwd?: string
}
export type RunResult = {
stdout: string
stderr: string
status: number | null
signal: NodeJS.Signals | null
}
/**
* Async stand-in for spawnSync, for tests that drive a wrapped command
* which talks back to the in-process HTTP/SOCKS proxy. spawnSync would
* block this event loop while curl waits for that same proxy to respond,
* which is a self-deadlock — bun 1.3.2+ runs spawnSync on an isolated
* loop so the main loop's I/O never ticks during the wait.
*
* Mirrors enough of spawnSync's surface for the test call sites:
* - (cmd, opts) → shell:true by default
* - (cmd, args[], opts) → argv form, no shell
* - opts.timeout → SIGTERM after N ms (like spawnSync)
* - stdin → closed immediately (EOF), like spawnSync
* with no `input`
*/
export async function spawnAsync(
cmd: string,
argsOrOpts?: readonly string[] | RunOpts,
maybeOpts?: RunOpts,
): Promise<RunResult> {
const args = Array.isArray(argsOrOpts) ? argsOrOpts : undefined
const opts = (Array.isArray(argsOrOpts) ? maybeOpts : argsOrOpts) ?? {}
const child = args
? spawn(cmd, args, { cwd: opts.cwd })
: spawn(cmd, { shell: opts.shell ?? true, cwd: opts.cwd })
// Match spawnSync's default: when no `input` is given, the child sees
// EOF on stdin immediately. Without this, things like `su` wait for a
// password on the open pipe.
child.stdin?.end()
let stdout = ''
let stderr = ''
child.stdout?.setEncoding('utf8').on('data', d => (stdout += d))
child.stderr?.setEncoding('utf8').on('data', d => (stderr += d))
let timer: ReturnType<typeof setTimeout> | undefined
let signal: NodeJS.Signals | null = null
if (opts.timeout) {
timer = setTimeout(() => {
signal = 'SIGTERM'
child.kill('SIGTERM')
}, opts.timeout)
}
const status = await new Promise<number | null>(resolve =>
child.on('close', code => resolve(code)),
)
if (timer) clearTimeout(timer)
return { stdout, stderr, status, signal }
}

View File

@@ -1,5 +1,4 @@
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
import { spawnSync } from 'node:child_process'
import {
existsSync,
unlinkSync,
@@ -11,6 +10,7 @@ import type { Server } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { isLinux } from '../helpers/platform.js'
import { spawnAsync } from '../helpers/spawn.js'
import { SandboxManager } from '../../src/sandbox/sandbox-manager.js'
import type { SandboxRuntimeConfig } from '../../src/sandbox/sandbox-config.js'
import { getApplySeccompBinaryPath } from '../../src/sandbox/generate-seccomp-filter.js'
@@ -131,7 +131,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`echo "Test message" | nc -U ${TEST_SOCKET_PATH}`,
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -154,7 +154,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s http://blocked-domain.example',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -170,7 +170,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --show-error --max-time 2 https://www.anthropic.com',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -194,7 +194,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s http://example.com',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 10000,
@@ -220,7 +220,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`echo "should fail" > ${testFile}`,
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -251,7 +251,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`echo "${testContent}" > allowed-write.txt`,
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -288,7 +288,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'head -n 5 ~/.bashrc',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -324,7 +324,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
allowAllUnixSockets: false, // Enable seccomp
})
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -360,7 +360,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'echo "Hello from sandbox"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -375,7 +375,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'echo "line1\nline2\nline3" | grep line2',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -390,7 +390,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
describe('Shell Selection (binShell parameter)', () => {
it('should execute commands with zsh when binShell is specified', async () => {
// Check if zsh is available
const zshCheck = spawnSync('which zsh', {
const zshCheck = await spawnAsync('which zsh', {
shell: true,
encoding: 'utf8',
})
@@ -405,7 +405,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'zsh',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -418,7 +418,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
it('should use zsh syntax successfully with binShell=zsh', async () => {
// Check if zsh is available
const zshCheck = spawnSync('which zsh', {
const zshCheck = await spawnAsync('which zsh', {
shell: true,
encoding: 'utf8',
})
@@ -433,7 +433,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'zsh',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -449,7 +449,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'echo "Shell: $BASH_VERSION"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -469,7 +469,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'ls /proc | grep -E "^[0-9]+$" | wc -l',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -494,7 +494,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`ln -s ${targetOutside} ${linkInAllowed} 2>&1 && echo "escaped" > ${linkInAllowed} 2>&1`,
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -531,7 +531,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
)
const startTime = Date.now()
spawnSync(command, {
await spawnAsync(command, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -577,11 +577,15 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
// Use timeout to kill the sandbox after 1 second
// The inner command would take 10 seconds to complete
const result = spawnSync('timeout', ['1', 'bash', '-c', command], {
encoding: 'utf8',
cwd: TEST_DIR,
timeout: 5000,
})
const result = await spawnAsync(
'timeout',
['1', 'bash', '-c', command],
{
encoding: 'utf8',
cwd: TEST_DIR,
timeout: 5000,
},
)
// timeout returns 124 when it kills the process
expect(result.status).toBe(124)
@@ -619,7 +623,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
)
// Kill after 0.5 seconds
spawnSync('timeout', ['0.5', 'bash', '-c', command], {
await spawnAsync('timeout', ['0.5', 'bash', '-c', command], {
encoding: 'utf8',
cwd: TEST_DIR,
timeout: 3000,
@@ -629,7 +633,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
await new Promise(resolve => setTimeout(resolve, 1500))
// Check for orphan processes with our marker
const psResult = spawnSync(
const psResult = await spawnAsync(
'bash',
['-c', `ps aux | grep "${uniqueMarker}" | grep -v grep || true`],
{
@@ -657,7 +661,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`cp /bin/bash ${setuidTest} 2>&1 && chmod u+s ${setuidTest} 2>&1 && ${setuidTest} -c "id -u" 2>&1`,
)
const result1 = spawnSync(command1, {
const result1 = await spawnAsync(command1, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -673,7 +677,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'sudo -n echo "elevated" 2>&1 || su -c "echo elevated" 2>&1 || echo "commands blocked"',
)
const result2 = spawnSync(command2, {
const result2 = await spawnAsync(command2, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -703,7 +707,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --show-error --max-time 2 --connect-timeout 2 https://blocked-domain.example 2>&1 || echo "curl_failed"',
)
const result1 = spawnSync(command1, {
const result1 = await spawnAsync(command1, {
shell: true,
encoding: 'utf8',
timeout: 4000,
@@ -725,7 +729,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --show-error --max-time 2 http://blocked-domain.example:8080 2>&1',
)
const result2 = spawnSync(command2, {
const result2 = await spawnAsync(command2, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -741,7 +745,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --max-time 2 http://1.1.1.1 2>&1', // Cloudflare DNS
)
const result3 = spawnSync(command3, {
const result3 = await spawnAsync(command3, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -757,7 +761,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --max-time 5 https://example.com 2>&1',
)
const result4 = spawnSync(command4, {
const result4 = await spawnAsync(command4, {
shell: true,
encoding: 'utf8',
timeout: 10000,
@@ -793,7 +797,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --connect-timeout 2 --max-time 2 http://api.github.com 2>&1 | head -20',
)
const result1 = spawnSync(command1, {
const result1 = await spawnAsync(command1, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -808,7 +812,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --max-time 2 http://github.com 2>&1',
)
const result2 = spawnSync(command2, {
const result2 = await spawnAsync(command2, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -823,7 +827,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --max-time 2 http://malicious-github.com 2>&1',
)
const result3 = spawnSync(command3, {
const result3 = await spawnAsync(command3, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -838,7 +842,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
'curl -s --max-time 3 http://raw.githubusercontent.com 2>&1 | head -20',
)
const result4 = spawnSync(command4, {
const result4 = await spawnAsync(command4, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -871,7 +875,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`mkfifo ${fifoPath} && test -p ${fifoPath} && echo "FIFO created"`,
)
const result1 = spawnSync(command1, {
const result1 = await spawnAsync(command1, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -887,7 +891,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`echo "test content" > ${regularFile}`,
)
spawnSync(command2a, {
await spawnAsync(command2a, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -898,7 +902,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`ln /etc/passwd ${hardlinkPath} 2>&1`,
)
const result2b = spawnSync(command2b, {
const result2b = await spawnAsync(command2b, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -917,7 +921,7 @@ describe.if(isLinux)('Sandbox Integration Tests', () => {
`mknod ${devicePath} c 1 3 2>&1`,
)
const result3 = spawnSync(command3, {
const result3 = await spawnAsync(command3, {
shell: true,
encoding: 'utf8',
timeout: 3000,
@@ -996,7 +1000,7 @@ describe.if(isLinux)(
'curl -s --max-time 2 --connect-timeout 2 http://example.com 2>&1 || echo "network_failed"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1030,7 +1034,7 @@ describe.if(isLinux)(
'curl -s --max-time 2 --connect-timeout 2 https://example.com 2>&1 || echo "network_failed"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1058,7 +1062,7 @@ describe.if(isLinux)(
'host example.com 2>&1 || nslookup example.com 2>&1 || echo "dns_failed"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1084,7 +1088,7 @@ describe.if(isLinux)(
'wget -q --timeout=2 -O - http://example.com 2>&1 || echo "wget_failed"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1112,7 +1116,7 @@ describe.if(isLinux)(
`echo "${testContent}" > ${testFile} && cat ${testFile}`,
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
cwd: TEST_DIR,
@@ -1151,7 +1155,7 @@ describe.if(isLinux)(
'curl -s --max-time 5 http://example.com 2>&1',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 10000,
@@ -1167,7 +1171,7 @@ describe.if(isLinux)(
'curl -s --max-time 2 http://anthropic.com 2>&1',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1198,7 +1202,7 @@ describe.if(isLinux)(
'curl -s --max-time 2 http://example.com 2>&1 || echo "blocked"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1258,7 +1262,7 @@ describe.if(isLinux)('Git over SSH through sandbox proxy', () => {
'echo "$GIT_SSH_COMMAND"',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -1283,7 +1287,7 @@ describe.if(isLinux)('Git over SSH through sandbox proxy', () => {
'git ls-remote ssh://git@github.com/anthropic-experimental/sandbox-runtime.git HEAD 2>&1',
)
const result = spawnSync(command, {
const result = await spawnAsync(command, {
shell: true,
encoding: 'utf8',
timeout: 15000,

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, afterEach, beforeEach } from 'vitest'
import { SandboxManager } from '../../src/index.js'
import { connect } from 'net'
import { spawnSync } from 'child_process'
import { getPlatform } from '../../src/utils/platform.js'
import { spawnAsync } from '../helpers/spawn.js'
import { isLinux } from '../helpers/platform.js'
/**
@@ -304,7 +304,7 @@ describe('SandboxManager.updateConfig integration (wrapWithSandbox)', () => {
const cmd1 = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 3 http://example.com 2>&1',
)
const result1 = spawnSync(cmd1, {
const result1 = await spawnAsync(cmd1, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -324,7 +324,7 @@ describe('SandboxManager.updateConfig integration (wrapWithSandbox)', () => {
const cmd2 = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 5 http://example.com 2>&1',
)
const result2 = spawnSync(cmd2, {
const result2 = await spawnAsync(cmd2, {
shell: true,
encoding: 'utf8',
timeout: 10000,
@@ -349,7 +349,7 @@ describe('SandboxManager.updateConfig integration (wrapWithSandbox)', () => {
const cmd1 = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 5 http://example.com 2>&1',
)
const result1 = spawnSync(cmd1, {
const result1 = await spawnAsync(cmd1, {
shell: true,
encoding: 'utf8',
timeout: 10000,
@@ -367,7 +367,7 @@ describe('SandboxManager.updateConfig integration (wrapWithSandbox)', () => {
const cmd2 = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 3 http://example.com 2>&1',
)
const result2 = spawnSync(cmd2, {
const result2 = await spawnAsync(cmd2, {
shell: true,
encoding: 'utf8',
timeout: 5000,
@@ -397,7 +397,7 @@ describe('SandboxManager.updateConfig integration (wrapWithSandbox)', () => {
const cmd = await SandboxManager.wrapWithSandbox(
'curl -s --max-time 5 http://example.com 2>&1',
)
const result = spawnSync(cmd, {
const result = await spawnAsync(cmd, {
shell: true,
encoding: 'utf8',
timeout: 10000,