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:
@@ -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 keychain;detached 后新会话读不到 → Not logged in)。
|
||||
* 存活性不依赖 detached:unref 后 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user