十六、AgentSandbox——把所有模块串起来的编排类

本篇讲解 src/agentSandbox.ts------整个包的核心类。它把配置、策略、路径预检、环境隔离、执行器、审计、文件 Patch、Worktree 等所有模块编排在一起。

1. AgentSandbox 的角色

如果说前面每一篇讲的都是"零件",那 AgentSandbox 就是"整车"。它提供三个核心 API:

API 作用
executeCommand(input) 执行一条命令
proposePatch(input) 生成文件修改提案
applyPatch(proposal) 应用已审批的文件修改

以及三个生命周期方法:

方法 作用
initialize() 初始化(创建 worktree 等)
dispose() 清理(删除 worktree 等)
promoteWorktree() 将 worktree 合并回主仓库

2. 构造函数

typescript 复制代码
export class AgentSandbox {
  private readonly config: ReturnType<typeof resolveSandboxConfig>;
  private readonly profilesDir: string;
  private readonly auditRecorder: SandboxAuditRecorder;
  private readonly session: SessionApprovalState = createSessionApprovalState();

  private initialized = false;
  private executionRoot: string;
  private worktreeHandle: GitWorktreeHandle | null = null;

  constructor(config: SandboxConfig) {
    this.config = resolveSandboxConfig(config);         // 配置解析
    this.profilesDir = resolveProfilesDir(config.profilesDir); // 找 profiles
    this.executionRoot = this.config.projectRoot;        // 初始执行根 = 项目根
    this.auditRecorder = createSandboxAuditRecorder(this.config.auditDir); // 创建审计记录器
  }
}

四个核心私有状态

  • config:解析后的完整配置
  • session:会话级审批状态
  • executionRoot:当前执行根(direct 模式下等于 projectRoot,worktree 模式下等于 worktree 路径)
  • worktreeHandle:如果使用了 worktree,保存其句柄

3. initialize------初始化

typescript 复制代码
async initialize(): Promise<void> {
  if (this.initialized) return;

  fs.mkdirSync(this.config.auditDir, { recursive: true });

  if (this.config.executionSurface === 'worktree') {
    if (!isGitRepository(this.projectRoot)) {
      throw new Error('executionSurface=worktree requires a git repository at projectRoot.');
    }
    this.worktreeHandle = await createGitWorktree({
      repoRoot: this.projectRoot,
      worktreesDir: this.config.worktreesDir,
      branchPrefix: this.config.worktreeBranchPrefix,
    });
    this.executionRoot = this.worktreeHandle.worktreePath;
  }

  this.initialized = true;
}

懒初始化 :第一次 executeCommandapplyPatch 时自动调用。如果 executionSurface === 'worktree',会创建 git worktree 并把 executionRoot 指向它。

4. executeCommand------核心 API

这是整个包最复杂的方法,我们分步看:

4.1 初始化 + 标准化输入

typescript 复制代码
async executeCommand(input: SandboxCommandInput): Promise<SandboxCommandResult> {
  await this.initialize();

  const commandSpec = normalizeCommandInput(input);
  const displayCommand = commandSpec.displayCommand;
  const startedAt = Date.now();
  const mode = input.mode ?? this.config.mode;
  const realHome = process.env.HOME ?? os.homedir();

4.2 Protected Paths 预检

typescript 复制代码
  if (this.config.enforceProtectedPaths && mode !== 'danger-full-access') {
    const protectedHomeRelative = [
      ...DEFAULT_PROTECTED_HOME_RELATIVE,
      ...this.config.protectedHomePaths,
    ];
    const violations = findProtectedPathViolations({
      commandSpec,
      executionRoot: this.executionRoot,
      realHome,
      protectedHomeRelative,
    });

    if (violations.length > 0) {
      return await this.buildBlockedProtectedPathResult({ ... });
    }
  }

注意danger-full-access 模式下跳过路径预检。

4.3 风险分级 + L4 默认拦截

typescript 复制代码
  const permissionIntent = analyzeCommandSpec(commandSpec);
  const riskLevel = permissionIntent.riskLevel;
  const reason = describeCommandRisk(riskLevel, displayCommand);

  if (isBlockedByDefault(riskLevel) && !input.skipApproval) {
    const decision = await this.requestApproval({ ... });
    if (!isPositiveApproval(decision)) {
      return await this.buildDeniedCommandResult({ ... });
    }
  }

L4 命令(如 rm -rfsudo)默认拦截,需要额外审批。

4.4 审批策略判断

typescript 复制代码
  const needsApproval =
    !input.skipApproval &&
    riskLevelNeedsApproval(riskLevel, this.config.approvalPolicy) &&
    !isApprovedForSession(this.session);

  let approved = !needsApproval;

  if (needsApproval) {
    const decision = await this.requestApproval({ ... });
    if (decision === 'deny') {
      return await this.buildDeniedCommandResult({ ... });
    }
    applyApprovalDecision(this.session, decision);
    approved = true;
  }

三个条件同时满足才需要审批:

  1. 没有跳过审批
  2. 风险等级在当前策略下需要审批
  3. 会话没有全部放行

4.5 执行命令

typescript 复制代码
  const run = this.openRun();
  const ioPaths = run.paths;
  const stdoutCapture = createOutputCapture(ioPaths.stdoutPath, this.config.maxStdoutBytes);
  const stderrCapture = createOutputCapture(ioPaths.stderrPath, this.config.maxStderrBytes);

  // 记录开始事件
  await run.record({ event: 'sandbox.command.started', ... });

  // 构建沙箱环境
  const env = buildSandboxEnv({ executionRoot: this.executionRoot, realHome, ... });

  // 执行
  try {
    const result = await runSandboxedCommand({
      commandSpec,
      mode,
      timeoutMs: this.config.timeoutMs,
      executionRoot: this.executionRoot,
      profilesDir: this.profilesDir,
      allowDangerousFallback: this.config.allowDangerousFallback,
      spawnEnv: env,
      onOsSandboxUnavailable: ...,
      io: {
        onStdout: (chunk) => stdoutCapture.appendBuffer(chunk),
        onStderr: (chunk) => stderrCapture.appendBuffer(chunk),
      },
    });
    exitCode = result.exitCode;
    effectiveMode = result.effectiveMode;
  } catch (error) {
    // 处理异常,记录错误事件
    return ...;
  }

  // 关闭输出捕获
  stdoutCapture.close();
  stderrCapture.close();

4.6 审计记录 + 返回结果

typescript 复制代码
  const dangerousFallbackUsed =
    mode !== 'danger-full-access' && effectiveMode === 'danger-full-access';

  run.writeMetadata(buildCommandAuditMetadata({ ... }));

  if (dangerousFallbackUsed) {
    await run.record({ event: 'sandbox.command.fallback', ... });
  }

  await run.record({ event: 'sandbox.command.finished', ... });

  return {
    runId: run.runId,
    exitCode,
    riskLevel,
    approved,
    effectiveMode,
    requestedMode: mode,
    dangerousFallbackUsed,
    executionRoot: this.executionRoot,
    durationMs,
    stdoutBytes: stdoutCapture.bytesWritten,
    stderrBytes: stderrCapture.bytesWritten,
    stdoutTruncated: stdoutCapture.truncated,
    stderrTruncated: stderrCapture.truncated,
    stdoutPath: ioPaths.stdoutPath,
    stderrPath: ioPaths.stderrPath,
  };

5. proposePatch / applyPatch

5.1 proposePatch

typescript 复制代码
proposePatch(input: FilePatchInput): FilePatchProposal {
  if (this.config.executionSurface === 'worktree' && !this.initialized) {
    throw new Error('proposePatch requires initialize() first when executionSurface=worktree.');
  }
  return proposeFilePatch(this.executionRoot, input);
}

生成只读提案,不写盘。

5.2 applyPatch

typescript 复制代码
async applyPatch(proposal: FilePatchProposal, options?: { skipApproval?: boolean }): Promise<FilePatchResult> {
  await this.initialize();

  // 1. 只读模式拒绝
  if (this.config.mode === 'read-only') {
    return await this.buildDeniedPatchResult(proposal, 'read-only mode blocks patch application.');
  }

  // 2. 审批
  let approved = options?.skipApproval ?? false;
  if (!approved) {
    const decision = await this.requestApproval({ ... });
    if (decision === 'deny') {
      return { applied: false, proposal, approved: false };
    }
    applyApprovalDecision(this.session, decision);
    approved = true;
  }

  // 3. 落盘
  applyFilePatchToDisk(this.executionRoot, proposal);

  // 4. 审计
  const run = this.openRun();
  run.writeMetadata({ kind: 'sandbox.file.patch', ... });
  await run.record({ event: 'sandbox.file.patch.applied', ... });

  return { applied: true, proposal, approved };
}

6. promoteWorktree

typescript 复制代码
async promoteWorktree(): Promise<WorktreePromoteResult> {
  // 不是 worktree 模式 → 不适用
  if (this.config.executionSurface !== 'worktree' || !this.worktreeHandle) { ... }

  // 只读模式 → 拒绝
  if (this.config.mode === 'read-only') { ... }

  // 审批(L4 级别)
  const decision = await this.requestApproval({ riskLevel: 'L4', ... });
  if (decision === 'deny') { ... }

  // 检查主仓库干净
  if (!(await isGitTreeClean(this.projectRoot))) { ... }

  // 执行合并
  const result = await promoteGitWorktree(this.projectRoot, this.worktreeHandle);
  // 审计记录
  return result;
}

7. dispose------清理

typescript 复制代码
async dispose(options?: { removeWorktree?: boolean }): Promise<void> {
  const shouldRemove = options?.removeWorktree ?? true;

  if (shouldRemove && this.worktreeHandle) {
    await removeGitWorktree(this.projectRoot, this.worktreeHandle);
    this.worktreeHandle = null;
  }

  this.initialized = false;
  this.executionRoot = this.projectRoot;
}

默认删除 worktree。传 { removeWorktree: false } 可以保留。

8. requestApproval------审批回调

typescript 复制代码
private async requestApproval(ctx: SandboxApprovalContext): Promise<SandboxApprovalDecision> {
  if (this.config.onApproval) {
    return this.config.onApproval(ctx);
  }

  // 没有审批回调 → fail-closed
  if (ctx.riskLevel === 'L0') return 'allow';
  return 'deny';
}

默认行为 :没有 onApproval 回调时,只有 L0 自动放行,其他一律拒绝。

9. normalizeCommandInput------输入标准化

typescript 复制代码
function normalizeCommandInput(input: SandboxCommandInput): SandboxCommandSpec {
  if (input.kind === 'exec') {
    const executable = input.executable.trim();
    if (!executable) throw new Error('SandboxCommandInput.executable must be non-empty.');
    const args = [...(input.args ?? [])];
    return {
      kind: 'exec',
      executable,
      args,
      displayCommand: [executable, ...args].join(' '),
    };
  }

  const command = input.command.trim();
  if (!command) throw new Error('SandboxCommandInput.command must be non-empty.');
  return {
    kind: 'shell',
    shellCommand: command,
    displayCommand: command,
  };
}

10. 小结

AgentSandbox 是一个"编排器",它本身不实现任何底层逻辑,而是把各模块的函数按正确顺序调用:

markdown 复制代码
1. initialize()          → config + worktree
2. normalizeCommandInput → 类型标准化
3. findProtectedPathViolations → 路径预检
4. analyzeCommandSpec    → 风险分级
5. isBlockedByDefault    → L4 拦截
6. riskLevelNeedsApproval → 审批判断
7. requestApproval       → 人工审批
8. buildSandboxEnv       → 环境隔离
9. runSandboxedCommand   → 执行命令
10. createOutputCapture  → 输出捕获
11. audit record         → 审计记录
12. return result        → 返回结果

核心思想:安全检查在前,执行在后,审计贯穿始终。

相关推荐
没事别瞎琢磨1 小时前
十一、审计与 Run Session——每一步操作都被记录
人工智能·node.js
George3751 小时前
当 Loop Engineering 成为行业共识,我发现自己的开源项目已经实践了 3 个月
人工智能
没事别瞎琢磨1 小时前
十二、网络代理与白名单规则引擎
人工智能·node.js
马士兵教育1 小时前
Java还有前景吗?Java+AI大模型学习路线及项目?
java·人工智能·python·学习·机器学习
没事别瞎琢磨1 小时前
十四、Git Worktree 隔离执行
人工智能·node.js
安全指北针1 小时前
大模型时代,谁在领跑中国AI安全赛道?中国AI安全产品市场分析
人工智能
KaMeidebaby2 小时前
卡梅德生物技术快报|纯化重组蛋白实操详解
人工智能·python·tcp/ip·算法·机器学习
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第五章 Item 30 - 32)
开发语言·人工智能·笔记·python·学习方法
YueTann2 小时前
OpenRLHF设计
人工智能