十一、审计与 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. 前缀

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

相关推荐
m0_462605222 分钟前
week05
人工智能
johnsong6 分钟前
AI 前沿日报:2026年9月4日
人工智能
lee_curry30 分钟前
AI Agent 工程师完整学习路线(面向生产级项目与面试)
人工智能·学习·ai·面试·agent
大模型真好玩35 分钟前
DeepSeek Harness 入门很简单(二)——DeepSeek Harness通用设置及Agent预设详解
人工智能·agent·deepseek
前端世界38 分钟前
基于 Linux 的本地 AI 智能问答平台搭建实践 —— Ollama + FastAPI + Nginx 实现本地大模型 Web 服务
linux·运维·人工智能
小小小小钰儿40 分钟前
1.2-AI核心理论与术语
人工智能·计算机·网络安全·信息安全·程序员·编程
桃西西呀43 分钟前
"循环深度"让小模型免费变大?GPT-6 Astra 背后的潜空间推理原理,8 个误读我替你拆了
人工智能·gpt·llm
八角丶1 小时前
Node 网络编程 —— TLS 模块
javascript·后端·node.js
数智工坊1 小时前
NTIRE2024×4图像超分竞赛综述|Mamba崛起、数据缩放、频域增强成为新趋势
网络·人工智能·深度学习·cnn·机器人
Raas1001 小时前
AI网关和大模型网关区别?MAI Gateway(魔芋企业级AI网关)如何实现AI流量统一治理
大数据·人工智能·gateway·ai网关·mai gateway·企业级产品