本篇讲解
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;
}
懒初始化 :第一次 executeCommand 或 applyPatch 时自动调用。如果 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 -rf、sudo)默认拦截,需要额外审批。
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;
}
三个条件同时满足才需要审批:
- 没有跳过审批
- 风险等级在当前策略下需要审批
- 会话没有全部放行
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 → 返回结果
核心思想:安全检查在前,执行在后,审计贯穿始终。