feat(executor): worker OS 级沙箱 + 资源限额(sandbox-exec / ulimit)[tsk_oTFzOMeAU20D]

进程隔离之上再加一层 OS 级硬隔离,默认关闭,便于先在一个项目灰度:

- 新增 src/executor/sandbox.ts:
  - macOS sandbox-exec 写围栏 profile(allow default 基线 + deny file-write* 收口
    + 逐条放行 worktree/主仓.git/node_modules/runs/transcripts/tmp/工具缓存)。
    不 deny default 全围栏——合法 git/npm/go/node 需读海量系统路径,写才是破坏向量。
  - ulimit 资源上限(CPU 时间/虚拟内存/打开文件数/单文件大小),经 sh -c 下发,
    exec 链保证 pid 不变(daemon 记录的 workerPid 仍是真 worker)。
  - 全部由 env 解析(MAESTRO_SANDBOX*),网络默认放行(SDK 连 Anthropic)。
- orchestrator.defaultSpawnWorker 接入:开启时把命令包成
  sh -c 'ulimit…; exec sandbox-exec -f profile <原命令>';关闭时行为零变化。
  startOrchestrator 启动时打印生效策略,每个 run 的 profile 落 runs/<runId>/sandbox.sb。
- README 增补环境变量与「worker 沙箱」说明。
- 新增 test/sandbox.test.ts(18 例:config 解析 / profile 生成 / ulimit / 包装 / 预建目录)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 15:08:18 +08:00
parent f020e15137
commit d0b807460e
4 changed files with 546 additions and 2 deletions
+10 -2
View File
@@ -7,6 +7,7 @@ import { rankByScore } from '../model/scoring.js';
import { branchFor, worktreeDirFor } from '../executor/worktree.js';
import { writeJobSpec, isWorkerAlive, heartbeatAgeMs, type JobSpec } from '../executor/protocol.js';
import { pickModel } from '../executor/models.js';
import { prepareSandboxedSpawn, describeSandbox } from '../executor/sandbox.js';
import { ingestAll, type IngestLogger } from './ingest.js';
/** 失败后默认最多自动重试次数(项目级 maxRetries 未设置时回退此值)。失败/退避策略本体在 store.failTaskAttempt。 */
@@ -56,10 +57,16 @@ export function workerEnv(src: NodeJS.ProcessEnv = process.env): NodeJS.ProcessE
* daemon 的鉴权上下文(本机无 API_KEY/凭证文件,鉴权走 macOS keychaindetached 后新会话读不到 → Not logged in)。
* 存活性不依赖 detachedunref 后 daemon 退出,worker 作为孤儿被 init/launchd 收养、继续运行(daemon 无控制终端,
* 不会有进程组 SIGHUP;重启用单 pid kill 不波及 worker)。
*
* OS 级沙箱(可选,默认关):MAESTRO_SANDBOX 开启时,prepareSandboxedSpawn 把命令包成
* `sh -c 'ulimit…; exec sandbox-exec -f profile <原命令>'`——文件写围栏 + 资源上限。
* 末尾 exec 链保证 pid 不变(仍是真 worker 的 pid),unref/同会话/keychain 等约束不受影响。
*/
function defaultSpawnWorker(runId: string): number {
const [cmd, ...args] = workerEntry();
const child = spawn(cmd, [...args, runId], {
const base = [...workerEntry(), runId];
const wrapped = prepareSandboxedSpawn(base, runId); // 沙箱关闭 → null,按原命令直接 spawn
const [cmd, ...args] = wrapped ? [wrapped.cmd, ...wrapped.args] : base;
const child = spawn(cmd, args, {
detached: false,
stdio: 'ignore',
env: workerEnv(),
@@ -242,5 +249,6 @@ export function startOrchestrator(
const timer = setInterval(() => orch.tick(), intervalSec * 1000);
timer.unref();
app.log.info(`编排器已启用:每 ${intervalSec}s 一轮(ingest→回收→领取,autonomy≠manual 的 active 项目)`);
app.log.info(describeSandbox()); // 记录当前生效的沙箱/资源上限策略
return timer;
}