mirror of
https://github.com/anthropic-experimental/sandbox-runtime.git
synced 2026-05-07 06:01:25 +08:00
* Run full test suite in CI and migrate platform skips to describe.if
CI was running test:unit + test:integration, a curated subset of 5 files.
Most test files were never run in CI. Switch to `npm test` which runs
everything. Drop the test:unit/test:integration scripts.
Migrate the inline `if (skipIfNotLinux()) return` pattern to bun's native
`describe.if()`/`it.if()`. The old pattern made wrong-platform tests show
as pass (zero assertions, green checkmark) instead of skip — CI's test
count looked the same regardless of what actually ran. New
test/helpers/platform.ts exports isLinux/isMacOS/isSupportedPlatform.
Delete ~310 lines of unreachable tests from seccomp-filter.test.ts:
- skipIfNotAnt() gate checked USER_TYPE env var that nothing sets
- Two tests called wrapCommandWithSandboxLinux() with no restrictions,
which returns the command unwrapped at the early-return check —
expect("echo test").not.toContain("apply-seccomp") was vacuously true
Pin allow-read root-deny tests to /bin/bash — EXEC_DEPS doesn't list
/opt/homebrew, so execvp failed on Macs with homebrew bash as SHELL.
Add docker-tests CI job: unprivileged container on both arches,
exercises enableWeakerNestedSandbox end-to-end.
Drop push trigger from '**' to 'main' — PRs were running the full
matrix twice (once for branch push, once for the PR event).
* Replace mock.module with spyOn in linux-dependency-error tests
mock.module patches bun's module cache globally and never unmocks.
With npm test running all files in one process (instead of the old
test:unit + test:integration split), the mock leaked: every file that
imported getApplySeccompBinaryPath after this one got () => null, so
pid-namespace-isolation.test.ts and integration.test.ts failed in
beforeAll.
spyOn swaps one export binding; mockRestore in afterEach puts it back.
The callee's own import binding routes through the same slot in bun, so
checkLinuxDependencies sees the spy without any module-level surgery.
Also spies on whichSync directly rather than overwriting Bun.which on
globalThis — same fix, closer to what's actually being tested.
Drop stale README reference to the deleted test:integration script.
* Replace docker test-suite job with srt end-to-end test
The full suite assumes bwrap --proc /proc works; an unprivileged
container doesn't have CAP_SYS_ADMIN for that. Only tests that set
enableWeakerNestedSandbox can pass there.
Instead of filtering which unit tests to run, test the thing the job
is for: build srt, run it with enableWeakerNestedSandbox, check that
allowed writes land, denied writes don't, and the seccomp filter blocks
AF_UNIX. Gated on SRT_E2E_DOCKER so host jobs skip it.
* Rename docker job to match other Tests jobs
* Add required network key to docker test config
SandboxRuntimeConfigSchema requires network (no .optional()). Without it
loadConfig returns null, srt falls through to getDefaultConfig, and the
sandbox enforces a different allowWrite than the test expects.
* Add explicit timeouts to update-config sandboxed-curl tests
The three it.if(isLinux) tests each run two spawnSync calls with curl
--max-time 3 then --max-time 5. When example.com responds slowly both
curls run to their limits and the body takes ~8s, but bun's default
test timeout is 5000ms. bun aborts mid-body; afterEach runs reset()
against an in-flight spawn and the next test sees stale state.
These were never in test:integration so they never ran on CI before
this branch. On fast responses they complete in under 200ms.
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
/**
|
|
* End-to-end: run srt in an unprivileged container with
|
|
* enableWeakerNestedSandbox and verify the sandbox enforces.
|
|
*
|
|
* Gated on SRT_E2E_DOCKER so `npm test` on the host jobs skips it —
|
|
* it's designed for a container that lacks CAP_SYS_ADMIN.
|
|
*
|
|
* Invoked by CI via:
|
|
* docker run --rm \
|
|
* --security-opt seccomp=unconfined --security-opt apparmor=unconfined \
|
|
* -v "$PWD:/work" -w /work -e SRT_E2E_DOCKER=1 \
|
|
* ubuntu:24.04 bash -c '<setup> && bun test test/e2e/docker.test.ts'
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
|
import { spawnSync } from 'node:child_process'
|
|
import {
|
|
mkdirSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
existsSync,
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
const inDocker = process.env.SRT_E2E_DOCKER === '1'
|
|
|
|
describe.if(inDocker)('srt end-to-end in unprivileged container', () => {
|
|
const WORK = join(tmpdir(), `srt-e2e-${Date.now()}`)
|
|
const ALLOWED = join(WORK, 'allowed')
|
|
const DENIED = join(WORK, 'denied')
|
|
const CONFIG = join(WORK, 'srt.json')
|
|
|
|
const srt = (cmd: string) =>
|
|
spawnSync('node', ['dist/cli.js', '-s', CONFIG, '-c', cmd], {
|
|
encoding: 'utf8',
|
|
timeout: 15000,
|
|
})
|
|
|
|
beforeAll(() => {
|
|
mkdirSync(ALLOWED, { recursive: true })
|
|
mkdirSync(DENIED, { recursive: true })
|
|
writeFileSync(
|
|
CONFIG,
|
|
JSON.stringify({
|
|
network: { allowedDomains: [], deniedDomains: [] },
|
|
filesystem: {
|
|
denyRead: [],
|
|
allowWrite: [ALLOWED],
|
|
denyWrite: [],
|
|
},
|
|
enableWeakerNestedSandbox: true,
|
|
}),
|
|
)
|
|
})
|
|
|
|
afterAll(() => {
|
|
rmSync(WORK, { recursive: true, force: true })
|
|
})
|
|
|
|
it('writes to allowWrite dir', () => {
|
|
const out = join(ALLOWED, 'out')
|
|
const r = srt(`echo ok > ${out}`)
|
|
expect(r.status).toBe(0)
|
|
expect(readFileSync(out, 'utf8').trim()).toBe('ok')
|
|
})
|
|
|
|
it('blocks write outside allowWrite', () => {
|
|
const out = join(DENIED, 'out')
|
|
const r = srt(`echo bad > ${out}`)
|
|
expect(r.status).not.toBe(0)
|
|
expect(existsSync(out)).toBe(false)
|
|
})
|
|
|
|
it('seccomp blocks AF_UNIX socket creation', () => {
|
|
const r = srt('python3 -c "import socket; socket.socket(socket.AF_UNIX)"')
|
|
expect(r.status).not.toBe(0)
|
|
expect(r.stderr.toLowerCase()).toMatch(
|
|
/permission denied|operation not permitted/,
|
|
)
|
|
})
|
|
|
|
it('seccomp allows AF_INET socket creation', () => {
|
|
const r = srt('python3 -c "import socket; socket.socket(socket.AF_INET)"')
|
|
expect(r.status).toBe(0)
|
|
})
|
|
})
|