import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { readSandboxConfig, buildSandboxProfile, ulimitLines, wrapCommand, prepareSandboxedSpawn, describeSandbox, DEFAULT_NOFILE, type SandboxProfileInput, } from '../src/executor/sandbox.js'; import { writeJobSpec, runDir } from '../src/executor/protocol.js'; import type { JobSpec } from '../src/executor/protocol.js'; import type { Project, Task } from '../src/model/types.js'; // ───────────────────────── readSandboxConfig ───────────────────────── test('readSandboxConfig:默认关闭,spawn 行为零变化', () => { const c = readSandboxConfig({}, 'darwin'); assert.equal(c.enabled, false); assert.equal(c.profile, false); }); test('readSandboxConfig:MAESTRO_SANDBOX 开启 → enabled;macOS 默认套 profile', () => { const c = readSandboxConfig({ MAESTRO_SANDBOX: 'on' }, 'darwin'); assert.equal(c.enabled, true); assert.equal(c.profile, true); assert.equal(c.allowNetwork, true, '默认放行网络(SDK 连 Anthropic)'); assert.equal(c.limits.nofile, DEFAULT_NOFILE); assert.equal(c.limits.cpuSec, 0, '默认不限 CPU'); }); test('readSandboxConfig:非 macOS 不套 profile(仅 ulimit)', () => { const c = readSandboxConfig({ MAESTRO_SANDBOX: '1' }, 'linux'); assert.equal(c.enabled, true); assert.equal(c.profile, false); }); test('readSandboxConfig:MAESTRO_SANDBOX_PROFILE=off 单独关 profile,保留 ulimit', () => { const c = readSandboxConfig({ MAESTRO_SANDBOX: '1', MAESTRO_SANDBOX_PROFILE: 'off' }, 'darwin'); assert.equal(c.enabled, true); assert.equal(c.profile, false); }); test('readSandboxConfig:资源上限从 env 解析(MB→KB),网络可关', () => { const c = readSandboxConfig({ MAESTRO_SANDBOX: 'true', MAESTRO_SANDBOX_CPU: '1800', MAESTRO_SANDBOX_MEM: '2048', MAESTRO_SANDBOX_NOFILE: '1024', MAESTRO_SANDBOX_FSIZE: '512', MAESTRO_SANDBOX_NET: 'off', }, 'darwin'); assert.equal(c.limits.cpuSec, 1800); assert.equal(c.limits.asKB, 2048 * 1024); assert.equal(c.limits.nofile, 1024); assert.equal(c.limits.fsizeKB, 512 * 1024); assert.equal(c.allowNetwork, false); }); test('readSandboxConfig:非法/负数限额回退默认', () => { const c = readSandboxConfig({ MAESTRO_SANDBOX: 'on', MAESTRO_SANDBOX_CPU: 'abc', MAESTRO_SANDBOX_NOFILE: '-5', }, 'darwin'); assert.equal(c.limits.cpuSec, 0); assert.equal(c.limits.nofile, DEFAULT_NOFILE); }); test('readSandboxConfig:额外写路径按冒号分隔', () => { const c = readSandboxConfig({ MAESTRO_SANDBOX: 'on', MAESTRO_SANDBOX_WRITE_EXTRA: '/a/b: /c/d :' }, 'darwin'); assert.deepEqual(c.extraWritePaths, ['/a/b', '/c/d']); }); // ───────────────────────── buildSandboxProfile ───────────────────────── function profileInput(over: Partial = {}): SandboxProfileInput { return { worktreeParent: '/data/worktrees/repo', repoGitDir: '/proj/repo/.git', repoNodeModules: '/proj/repo/node_modules', runDir: '/data/runs/run1', transcriptsDir: '/data/transcripts', tmpDir: '/tmp/x', homeDir: '/Users/u', allowNetwork: true, extraWritePaths: [], ...over, }; } test('buildSandboxProfile:allow default + 写收口 + 逐条放行', () => { const p = buildSandboxProfile(profileInput()); assert.match(p, /^\(version 1\)/m); assert.match(p, /\(allow default\)/); assert.match(p, /\(deny file-write\*\)/); // 关键写目标都在 assert.match(p, /\(subpath "\/data\/worktrees\/repo"\)/); assert.match(p, /\(subpath "\/proj\/repo\/\.git"\)/); assert.match(p, /\(subpath "\/proj\/repo\/node_modules"\)/); assert.match(p, /\(subpath "\/data\/runs\/run1"\)/); assert.match(p, /\(subpath "\/data\/transcripts"\)/); assert.match(p, /\(subpath "\/tmp\/x"\)/); // home 缓存 assert.match(p, /\(subpath "\/Users\/u\/\.npm"\)/); assert.match(p, /\(subpath "\/Users\/u\/Library\/Caches"\)/); }); test('buildSandboxProfile:deny file-write* 出现在第一条 allow file-write* 之前(后匹配优先才正确)', () => { const p = buildSandboxProfile(profileInput()); assert.ok(p.indexOf('(deny file-write*)') < p.indexOf('(allow file-write*'), 'deny 须在放行之前'); }); test('buildSandboxProfile:默认放行网络(无 deny network*);allowNetwork=false 时隔离', () => { assert.doesNotMatch(buildSandboxProfile(profileInput({ allowNetwork: true })), /deny network/); assert.match(buildSandboxProfile(profileInput({ allowNetwork: false })), /\(deny network\*\)/); }); test('buildSandboxProfile:额外写路径被纳入;含特殊字符的路径被转义', () => { const p = buildSandboxProfile(profileInput({ extraWritePaths: ['/extra/one'], homeDir: '/U/a"b\\c' })); assert.match(p, /\(subpath "\/extra\/one"\)/); assert.match(p, /\\"b\\\\c/); // " → \" 和 \ → \\ }); // ───────────────────────── ulimitLines ───────────────────────── test('ulimitLines:0 项跳过,非 0 项下发且容错(|| true)', () => { const lines = ulimitLines({ cpuSec: 1800, asKB: 0, nofile: 4096, fsizeKB: 0 }); assert.equal(lines.length, 2); assert.match(lines[0], /^ulimit -t 1800 2>\/dev\/null \|\| true$/); assert.match(lines[1], /^ulimit -n 4096 2>\/dev\/null \|\| true$/); }); test('ulimitLines:全 0 → 空', () => { assert.deepEqual(ulimitLines({ cpuSec: 0, asKB: 0, nofile: 0, fsizeKB: 0 }), []); }); // ───────────────────────── wrapCommand ───────────────────────── test('wrapCommand:套 profile → sh -c,含 sandbox-exec 与 ulimit 与 exec "$@"', () => { const { cmd, args } = wrapCommand(['node', '/w/worker.js', 'run1'], { profilePath: '/data/runs/run1/sandbox.sb', limits: { cpuSec: 600, asKB: 0, nofile: 4096, fsizeKB: 0 }, }); assert.equal(cmd, '/bin/sh'); assert.equal(args[0], '-c'); const script = args[1]; assert.match(script, /ulimit -t 600/); assert.match(script, /ulimit -n 4096/); assert.match(script, /exec "\$@"\n$/); // 位置参数:$0 标签 + 目标命令 assert.deepEqual(args.slice(2), [ 'maestro-worker', 'sandbox-exec', '-f', '/data/runs/run1/sandbox.sb', 'node', '/w/worker.js', 'run1', ]); }); test('wrapCommand:无 profile(非 macOS)→ 仅 ulimit + 原命令', () => { const { cmd, args } = wrapCommand(['node', '/w/worker.js', 'run1'], { profilePath: undefined, limits: { cpuSec: 0, asKB: 0, nofile: 4096, fsizeKB: 0 }, }); assert.equal(cmd, '/bin/sh'); assert.deepEqual(args.slice(2), ['maestro-worker', 'node', '/w/worker.js', 'run1']); assert.doesNotMatch(args[1], /sandbox-exec/); }); // ───────────────────────── prepareSandboxedSpawn ───────────────────────── function fakeJob(runId: string, dataDir: string, repoPath: string): JobSpec { const task = { id: 'tsk_x', title: 't', status: 'executing' } as unknown as Task; const project = { id: 'prj_x', repoPath, defaultBranch: 'main' } as unknown as Project; return { runId, task, project, worktreeDir: join(dataDir, 'worktrees', 'repo', 'tsk_x'), branch: 'maestro/tsk_x', }; } test('prepareSandboxedSpawn:关闭 → null(原命令直接 spawn)', () => { const out = prepareSandboxedSpawn(['node', 'w.js', 'r'], 'r', {}); assert.equal(out, null); }); test('prepareSandboxedSpawn:开启 → 读 job、预建父目录、(macOS)写 profile 并包命令', () => { const dataDir = mkdtempSync(join(tmpdir(), 'maestro-sbx-')); try { const env: NodeJS.ProcessEnv = { MAESTRO_SANDBOX: 'on', MAESTRO_DATA_DIR: dataDir, HOME: dataDir, TMPDIR: join(dataDir, 'tmp'), }; const runId = 'run_sbx'; const job = fakeJob(runId, dataDir, join(dataDir, 'repo')); // writeJobSpec / runDir 走 MAESTRO_DATA_DIR const prev = process.env.MAESTRO_DATA_DIR; process.env.MAESTRO_DATA_DIR = dataDir; try { writeJobSpec(job); const out = prepareSandboxedSpawn(['node', 'w.js', runId], runId, env); assert.ok(out); assert.equal(out!.cmd, '/bin/sh'); // 预建的父目录 assert.ok(existsSync(join(dataDir, 'worktrees', 'repo')), '预建 worktree 父目录'); assert.ok(existsSync(join(dataDir, 'transcripts')), '预建 transcripts 目录'); const onDarwin = process.platform === 'darwin'; const profilePath = join(runDir(runId), 'sandbox.sb'); if (onDarwin) { assert.ok(existsSync(profilePath), 'macOS 应落 sandbox.sb'); const prof = readFileSync(profilePath, 'utf8'); assert.match(prof, /\(deny file-write\*\)/); // 放行的是 worktree 父目录(含本任务 worktree),以及 runs/ assert.ok(prof.includes(`(subpath "${join(dataDir, 'worktrees', 'repo')}")`), 'profile 含 worktree 父目录'); assert.ok(prof.includes(`(subpath "${runDir(runId)}")`), 'profile 含 runs/'); assert.ok(out!.args.includes('sandbox-exec')); } else { assert.ok(!out!.args.includes('sandbox-exec'), '非 macOS 不套 profile'); } } finally { if (prev === undefined) delete process.env.MAESTRO_DATA_DIR; else process.env.MAESTRO_DATA_DIR = prev; } } finally { rmSync(dataDir, { recursive: true, force: true }); } }); // ───────────────────────── describeSandbox ───────────────────────── test('describeSandbox:关闭/开启的人读摘要', () => { assert.match(describeSandbox({}, 'darwin'), /关闭/); const s = describeSandbox({ MAESTRO_SANDBOX: 'on', MAESTRO_SANDBOX_CPU: '1800' }, 'darwin'); assert.match(s, /启用/); assert.match(s, /sandbox-exec/); assert.match(s, /cpu=1800s/); assert.match(s, /nofile=4096/); });