十一、审计与 Run Session——每一步操作都被记录

本篇讲解 src/audit.tssrc/runSession.ts------沙箱包的"黑匣子"。每条命令的执行过程(开始、完成、失败、拒绝等)都会被记录到审计日志中。

1. 为什么需要审计?

如果 AI 助手偷偷执行了 rm -rf /,你事后怎么知道?

审计日志就是干这个的。每一条命令执行,都会在 audit.jsonl 里留下记录:

jsonl 复制代码
{"runId":"abc-123","time":"2024-01-15T10:30:00Z","event":"sandbox.command.started","command":"ls","mode":"workspace-write","approved":true}
{"runId":"abc-123","time":"2024-01-15T10:30:00Z","event":"sandbox.command.finished","command":"ls","exitCode":0,"durationMs":50,"stdoutBytes":1024}

2. 审计目录结构

c 复制代码
${元数据目录}/audit/
├── audit.jsonl                    ← 所有事件的 JSONL 日志
├── abc-123/                       ← 一次 run 的目录,以 runId 为目录名
│   ├── stdout.txt                 ← 命令的 stdout 输出
│   ├── stderr.txt                 ← 命令的 stderr 输出
│   └── metadata.json              ← run 摘要(风险等级、耗时等)
├── def-456/
│   ├── stdout.txt
│   ├── stderr.txt
│   └── metadata.json
└── ...

3. SandboxRunSession------单次 Run 的会话

typescript 复制代码
export class SandboxRunSession {
  readonly ctx: AuditContext;
  readonly paths: RunFilePaths;

  private constructor(
    private readonly auditRecorder: SandboxAuditRecorder,
    ctx: AuditContext,
    paths: RunFilePaths,
  ) {
    this.ctx = ctx;
    this.paths = paths;
  }

  get runId(): string {
    return this.ctx.runId;
  }

  static open(options: OpenSandboxRunOptions): SandboxRunSession {
    const runId = createUUID();
    fs.mkdirSync(options.auditDir, { recursive: true });
    fs.mkdirSync(path.join(options.auditDir, runId), { recursive: true });

    const ctx: AuditContext = {
      auditDir: options.auditDir,
      runId,
      projectRoot: options.projectRoot,
      executionRoot: options.executionRoot,
    };

    return new SandboxRunSession(options.auditRecorder, ctx, getRunFilePaths(ctx));
  }

  async record(partial: SandboxRunRecordInput): Promise<string> {
    return this.auditRecorder.record({ ...partial, runId: this.ctx.runId });
  }

  writeMetadata(metadata: Record<string, unknown>): void {
    fs.writeFileSync(this.paths.metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
  }
}

3.1 open------创建 Run 会话

typescript 复制代码
static open(options: OpenSandboxRunOptions): SandboxRunSession {
  const runId = createUUID();  // 生成唯一 ID
  // 创建 run 目录
  fs.mkdirSync(options.auditDir, { recursive: true });
  fs.mkdirSync(path.join(options.auditDir, runId), { recursive: true });

  const ctx = { auditDir, runId, projectRoot, executionRoot };
  return new SandboxRunSession(auditRecorder, ctx, getRunFilePaths(ctx));
}

3.2 record------写审计事件

typescript 复制代码
async record(partial: SandboxRunRecordInput): Promise<string> {
  return this.auditRecorder.record({ ...partial, runId: this.ctx.runId });
}

自动注入 runId,调用方不需要手动传。

3.3 writeMetadata------写 run 摘要

typescript 复制代码
writeMetadata(metadata: Record<string, unknown>): void {
  fs.writeFileSync(this.paths.metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
}

每次覆盖写入。metadata.json 的内容类似:

json 复制代码
{
  "kind": "sandbox.command",
  "command": "ls -la",
  "riskLevel": "L0",
  "approved": true,
  "requestedMode": "workspace-write",
  "effectiveMode": "workspace-write",
  "exitCode": 0,
  "durationMs": 50,
  "stdoutBytes": 1024,
  "stderrBytes": 0
}

4. getRunFilePaths------计算 run 目录内路径

typescript 复制代码
export function getRunFilePaths(ctx: AuditContext): RunFilePaths {
  const runDir = path.join(ctx.auditDir, ctx.runId);
  return {
    stdoutPath: path.join(runDir, 'stdout.txt'),
    stderrPath: path.join(runDir, 'stderr.txt'),
    metadataPath: path.join(runDir, 'metadata.json'),
  };
}

5. audit.ts------审计辅助函数

5.1 createRunId

typescript 复制代码
export function createRunId(): string {
  return createUUID();
}

5.2 createAuditContext(已弃用)

typescript 复制代码
export function createAuditContext(auditDir, projectRoot, executionRoot): AuditContext {
  return SandboxRunSession.open({
    auditRecorder: getSharedAuditRecorder(auditDir),
    auditDir, projectRoot, executionRoot,
  }).ctx;
}

5.3 createSandboxAuditRecorder

typescript 复制代码
export function createSandboxAuditRecorder(auditDir: string): SandboxAuditRecorder {
  return getSharedAuditRecorder(auditDir);
}

内部用 Map 按 auditDir 缓存 recorder,同一个 auditDir 共享同一个 recorder。

5.4 buildCommandAuditMetadata

typescript 复制代码
export function buildCommandAuditMetadata(input: { ... }): Record<string, unknown> {
  return { kind: 'sandbox.command', ...input };
}

把命令执行摘要封装成 metadata.json 的内容。

5.5 normalizeSandboxEvent

typescript 复制代码
export function normalizeSandboxEvent(event: string): `sandbox.${string}` | string {
  return event.startsWith('sandbox.') ? event : `sandbox.${event}`;
}

确保事件名有 sandbox. 前缀。比如 command.started 会变成 sandbox.command.started

6. 审计事件类型

在一次命令执行中,会产生以下事件:

事件名 时机
sandbox.command.started 命令开始执行
sandbox.command.finished 命令正常结束
sandbox.command.failed 命令执行出错
sandbox.command.denied 审批拒绝
sandbox.command.blocked 路径预检拦截
sandbox.command.fallback 从安全模式回退到 danger-full-access
sandbox.file.patch.applied 文件 Patch 已应用
sandbox.file.patch.denied 文件 Patch 被拒绝
sandbox.worktree.promote.started Worktree 合并开始
sandbox.worktree.promote.finished Worktree 合并完成
sandbox.worktree.promote.denied Worktree 合并被拒绝

7. 小结

概念 作用
SandboxRunSession 一次命令执行的会话(runId + 目录 + IO 路径)
audit.jsonl 所有事件的 JSONL 日志
metadata.json 每次 run 的摘要(风险等级、退出码、耗时等)
stdout.txt / stderr.txt 命令输出落盘
normalizeSandboxEvent 确保事件名有 sandbox. 前缀

核心思想:所有操作都有迹可查。 审批通过的、拒绝的、回退的------全记下来。

相关推荐
秋921 小时前
从 Python 后端工程师转型 AI Engineer(AI 工程化)的完整补课清单(2026实战版)
开发语言·人工智能·python
啦啦啦_999921 小时前
5. 迁移学习
人工智能·机器学习·迁移学习
A.说学逗唱的Coke21 小时前
【AI·Coding】TDD × SDD × AI Coding:从“测试驱动“到“规范驱动“的智能协作实践
人工智能·驱动开发·tdd
云烟成雨TD21 小时前
Spring AI Alibaba 1.x 系列【78】沙箱(Sandbox)
java·人工智能·spring
tq10861 天前
基于SLIP的防幻觉的指南
人工智能
甲维斯1 天前
Kimi版超级玛丽效果“惊人”,配额不足5厘米!
前端·人工智能
console.log('npc')1 天前
AI前端工程与生成式UI学习路线
前端·人工智能·ui
秋91 天前
3年经验Python后端转AI Engineer:3个月实战转型计划(2026版)
开发语言·人工智能·python
圣殿骑士-Khtangc1 天前
GPT-5.5 技术深度解析与企业级生产落地实战:从幻觉率下降到百万Token工程化
人工智能·gpt
2601_961963381 天前
技术解剖:哈希值、区块链与CA认证如何守护电子合同安全?
网络·人工智能·安全·区块链·智能合约·政务