二、类型系统——给所有概念起名字

1. 为什么先讲类型?

你有没有遇到过这种情况:三个人讨论一个功能,说着说着发现每个人嘴里的"命令"指的不是同一回事?

类型系统就是来统一语言的。在这个包里:

  • SandboxMode 只能是三种字符串之一,不会出现拼写错误
  • CommandRiskLevel 只能是 L0~L4,不会冒出 L5
  • SandboxCommandInput 明确区分了"shell 命令"和"结构化 exec"两种输入方式

先理解类型,后面看代码就不会迷路。

2. 类型文件结构

所有类型定义都在 src/types/ 目录下,由 index.ts 统一导出:

arduino 复制代码
src/types/
├── index.ts       ← 统一导出入口
├── sandbox.ts     ← 沙箱模式 & 执行面
├── policy.ts      ← 风险等级 & 操作意图
├── approval.ts    ← 审批策略 & 审批决策
├── command.ts     ← 命令输入/输出
├── config.ts      ← 配置对象
├── audit.ts       ← 审计事件 & Run 上下文
├── proxy.ts       ← 代理相关
├── executor.ts    ← 执行器 IO 回调
├── runner.ts      ← Runner 相关
├── seatbelt.ts    ← macOS Seatbelt
├── file-patch.ts  ← 文件 Patch
└── worktree.ts    ← Git Worktree

3. sandbox.ts------沙箱模式 & 执行面

3.1 SandboxMode(沙箱模式)

typescript 复制代码
export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';

这就是三种沙箱模式:

白话解释
read-only 只能看,不能改项目文件
workspace-write 可以在项目里改东西,但不能改项目外面的
danger-full-access 啥都能干,完全信任

3.2 ExecutionSurface(执行面)

typescript 复制代码
export type ExecutionSurface = 'direct' | 'worktree';

"执行面"说的是命令在哪儿跑:

白话解释
direct 直接在用户项目根目录执行
worktree 在 git worktree 副本里执行(改了不会影响原项目,除非你主动 promote)

4. policy.ts------风险等级 & 操作意图

4.1 CommandRiskLevel(风险等级)

typescript 复制代码
export type CommandRiskLevel = 'L0' | 'L1' | 'L2' | 'L3' | 'L4';

从安全到危险,5 个等级:

等级 含义 典型命令
L0 只读,无害 lscatgit status
L1 低风险本地执行 node script.jsjest
L2 会写工作区 mkdircpgit add
L3 网络/依赖/外部交互 curlnpm installgit push
L4 高危破坏性 rm -rfsudogit push --force

4.2 SandboxPermissionIntent(操作意图)

typescript 复制代码
export interface SandboxPermissionIntent {
  kind: 'shell' | 'exec';       // 命令类型
  riskLevel: CommandRiskLevel;   // 风险等级
  usesShell: boolean;            // 是否用 shell 执行
  usesInterpreter: boolean;      // 是否用了解释器(node/python/bash 等)
  interpreter?: string;          // 解释器名
  executableBase?: string;       // 可执行文件名
  usesNetwork: boolean;          // 是否用网络
  writesWorkspace: boolean;     // 是否写工作区
  touchesGit: boolean;           // 是否操作 git
  highImpact: boolean;           // 是否高危
}

这是对命令"想干什么"的结构化摘要。比如 node -e "process.exit(1)" 会被分析为:

json 复制代码
{
  "kind": "shell",
  "riskLevel": "L3",
  "usesShell": true,
  "usesInterpreter": true,
  "interpreter": "node",
  "executableBase": "node",
  "usesNetwork": false,
  "writesWorkspace": true,
  "touchesGit": false,
  "highImpact": false
}

为什么 node -e 是 L3 而不是 L2?因为 -e 参数意味着执行内联代码,风险比执行一个固定脚本更高。

5. approval.ts------审批策略 & 审批决策

5.1 ApprovalPolicy(审批策略)

typescript 复制代码
export type ApprovalPolicy = 'untrusted' | 'on-request' | 'autonomous';
策略 L0 L1 L2 L3 L4
untrusted 自动 要审 要审 要审 拦截+审
on-request 自动 自动 要审 要审 拦截+审
autonomous 自动 自动 自动 自动 拦截(除非 skipApproval)

5.2 ApprovalDecision(审批决策)

typescript 复制代码
export type ApprovalDecision = 'allow_once' | 'allow_session' | 'deny';

这是用户在终端里的三选一:

  • allow_once:这次让你跑,下次还得问我
  • allow_session :整个会话都让你跑,不用再问了
  • deny:不许跑

5.3 SandboxApprovalDecision(沙箱完整审批决策)

typescript 复制代码
export type SandboxApprovalDecision = ApprovalDecision | 'allow';

在 ApprovalDecision 基础上多了一个 allow,表示系统自动放行(比如 L0 命令不需要问人)。

5.4 SandboxApprovalContext

typescript 复制代码
export interface SandboxApprovalContext {
  command: string;                      // 命令内容
  riskLevel: CommandRiskLevel;         // 风险等级
  reason: string;                      // 为什么需要审批
  executionRoot: string;               // 执行根
  requestedMode: SandboxMode;          // 请求的模式
  permissionIntent: SandboxPermissionIntent; // 权限意图
}

想象你是公司的门卫,有个人要进大楼。你总得知道他是谁、要干什么,才能决定放不放吧?

SandboxApprovalContext 就是"来者的介绍信"------当系统需要问用户"要不要批准这条命令"时,会把所有相关信息打包成这个对象,交给审批回调函数。回调函数拿到这些信息后,才能做出知情决定。

打个具体的比方:AI 助手想执行 rm -rf /tmp/old-builds,沙箱判定它是 L3 中高风险命令,需要人工审批。于是沙箱构造这样一份上下文:

arduino 复制代码
SandboxApprovalContext {
  command:      "rm -rf /tmp/old-builds"      // 想执行的命令
  riskLevel:    "L3"                          // 危险等级:中高
  reason:       "命令包含 rm -rf,可能删除大量文件"  // 为什么觉得危险
  executionRoot: "/Users/you/project/my-app"   // 在哪个项目目录下执行
  requestedMode: "workspace-write"             // 当前沙箱模式
  permissionIntent: "modify"                  // 这条命令的意图是修改文件
}

用户看到这些信息,就能判断:"哦,是在 /tmp 下删东西,不是删项目源码,可以放行。"

6 个字段分别是什么意思:

字段 含义 例子
command AI 想执行的那条命令 "rm -rf /tmp/old-builds"
riskLevel 沙箱判定的风险等级 "L3"
reason 为什么被判为这个等级(人话解释) "命令包含 rm -rf,可能删除大量文件"
executionRoot 命令会在哪个目录下跑 "/Users/you/project/my-app"
requestedMode 当前沙箱处于什么模式 "workspace-write"
permissionIntent 这条命令想干什么(读/写/删/...) "modify"

5.5 SandboxApprovalHandler

typescript 复制代码
export type SandboxApprovalHandler = (
  ctx: SandboxApprovalContext,
) => Promise<SandboxApprovalDecision>;

审批回调函数的类型签名。调用方可以自己实现审批逻辑(比如弹个对话框、调个 API),只要返回 SandboxApprovalDecision 就行。

5.6 SessionApprovalState

typescript 复制代码
export interface SessionApprovalState {
  sessionAllowsAll: boolean;
}

会话级审批状态。一旦用户选了 allow_sessionsessionAllowsAll 就变成 true,后续命令就不用再审批了。

还是用门卫的比方:上一节的 SandboxApprovalContext 是"来者的介绍信",那 SandboxApprovalHandler 就是"门卫做决定的规则"。

门卫拿到介绍信后怎么处理?沙箱不管------你可以让门卫弹个对话框问用户,也可以让门卫自动查个审批 API,甚至让门卫掷骰子(不推荐)。沙箱只关心一件事:门卫最终得给个结论------放行、拒绝、还是本次会话全部放行。

所以它的输入是介绍信(SandboxApprovalContext),输出是结论(SandboxApprovalDecision)。中间怎么决定的,你自己写。

typescript 复制代码
// 举个最简单的实现:什么都放行(危险!仅用于演示)
const alwaysAllow: SandboxApprovalHandler = async (ctx) => 'allow';

// 再举个实际点的:弹出终端提示,让用户手动选
const askUser: SandboxApprovalHandler = async (ctx) => {
  console.log(`AI 想执行: ${ctx.command}`);
  console.log(`风险等级: ${ctx.riskLevel},原因: ${ctx.reason}`);
  const answer = await askYesNo("允许执行吗?");
  return answer ? 'allow_session' : 'deny';
};

5.6 SessionApprovalState

typescript 复制代码
export interface SessionApprovalState {
  sessionAllowsAll: boolean;
}

会话级审批状态。一旦用户选了 allow_sessionsessionAllowsAll 就变成 true,后续命令就不用再审批了。

6. command.ts------命令的输入输出

6.1 SandboxCommandInput

这是一个联合类型(Union Type),支持两种命令格式:

shell 模式(传一个字符串命令):

typescript 复制代码
{
  kind?: 'shell';       // 默认就是 shell
  command: string;       // 比如 "ls -la"
  mode?: SandboxMode;   // 可以覆盖沙箱模式
  skipApproval?: boolean; // 是否跳过审批
  env?: Record<string, string | undefined>; // 额外环境变量
}

exec 模式(结构化的可执行文件 + 参数):

typescript 复制代码
{
  kind: 'exec';
  executable: string;    // 比如 "/usr/bin/git"
  args?: string[];       // 比如 ["status"]
  mode?: SandboxMode;
  skipApproval?: boolean;
  env?: Record<string, string | undefined>;
}

为什么需要两种?因为:

  • shell 模式 简单,直接传一个字符串,但可能包含管道 |、重定向 > 等 shell 特性
  • exec 模式更安全,参数是结构化的数组,不会被 shell 解释,避免注入风险

6.2 SandboxCommandSpec

这是 SandboxCommandInput 经过 normalizeCommandInput() 标准化后的内部格式:

typescript 复制代码
// shell 模式
{
  kind: 'shell';
  displayCommand: string;   // 给人看的命令文本
  shellCommand: string;    // 实际执行的命令文本
}

// exec 模式
{
  kind: 'exec';
  displayCommand: string;   // "git status" 这种展示文本
  executable: string;       // 可执行文件路径
  args: string[];           // 参数数组
}

6.3 SandboxCommandResult

命令执行完的返回值:

typescript 复制代码
export interface SandboxCommandResult {
  runId: string;                    // 本次执行的唯一 ID
  exitCode: number;                // 退出码(0=成功)
  riskLevel: CommandRiskLevel;    // 风险等级
  approved: boolean;               // 是否通过审批
  effectiveMode: SandboxMode;      // 实际执行模式(可能和请求的不同)
  requestedMode: SandboxMode;      // 用户请求的模式
  dangerousFallbackUsed: boolean;  // 是否从安全模式回退到了 danger-full-access
  executionRoot: string;           // 执行根目录
  durationMs: number;              // 耗时(毫秒)
  stdoutBytes: number;             // stdout 写了多少字节
  stderrBytes: number;             // stderr 写了多少字节
  stdoutTruncated: boolean;        // stdout 是否被截断
  stderrTruncated: boolean;        // stderr 是否被截断
  stdoutPath: string;              // stdout 文件路径
  stderrPath: string;              // stderr 文件路径
}

7. config.ts------配置对象

typescript 复制代码
export interface SandboxConfig {
  /** 用户项目根目录(git 仓库路径;worktree / 审计路径以此为基准) */
  projectRoot: string; // 项目根目录
  /** 命令在哪跑:`direct` 主仓,`worktree` 隔离副本 */
  executionSurface?: ExecutionSurface;
  mode?: SandboxMode; // 沙箱模式
  approvalPolicy?: ApprovalPolicy; // 审批策略
  timeoutMs?: number; // 命令超时时间
  maxStdoutBytes?: number; // stdout 字节上限
  maxStderrBytes?: number; // stderr 字节上限
  network?: boolean; // 是否允许联网
  proxyUrl?: string; // 代理地址
  /** 审计 JSONL 与每次 run 目录;默认 `${projectRoot}/${元数据目录,类似于 .cursor}/audit` */
  auditDir?: string;
  /** Seatbelt `.sb` profile 目录;不传则自动解析 */
  profilesDir?: string;
  /** worktree 分支名前缀,如 `${元数据目录}-agent` */
  worktreeBranchPrefix?: string;
  /** git worktree 存放目录;默认 `${projectRoot}/${元数据目录}/worktrees` */
  worktreesDir?: string;
  /** 传给子进程的环境变量白名单(不含密钥等) */
  envAllowlist?: string[];
  /** 在白名单基础上追加或覆盖的环境变量 */
  extraEnv?: Record<string, string | undefined>;
  /**
   * 是否在 spawn 前做 protected paths 预检(`~/.ssh`、`~/.npmrc`、工作区外读盘等)。
   * `danger-full-access` 下始终跳过。
   */
  enforceProtectedPaths?: boolean;
  /** 相对真实用户 HOME 的额外 protected 子路径 */
  protectedHomePaths?: string[];
  /** OS 沙箱不可用时,是否允许回退到 `danger-full-access` */
  allowDangerousFallback?: boolean;
  /** 需审批时回调;不传则除 L0 外默认拒绝 */
  onApproval?: SandboxApprovalHandler;
  /** 非 macOS / 无 sandbox-exec 时的回退决策回调 */
  onOsSandboxUnavailable?: (reason: string) => Promise<OsSandboxFallbackDecision>;
}

除了 projectRoot 是必填的,其他都有默认值(下一篇会详细讲)。

8. audit.ts------审计相关

8.1 AuditEvent

每次执行命令会产生多条审计事件(started → finished/error 等):

typescript 复制代码
export type AuditEvent = {
  runId: string;
  time: string;             // ISO 8601
  event: string;            // 如 "sandbox.command.started"
  command?: string;
  mode?: SandboxMode;
  approvalPolicy?: ApprovalPolicy;
  approved?: boolean;
  exitCode?: number | null;
  durationMs?: number;
  // ... 还有很多字段,上面见过
};

8.2 AuditContext & RunFilePaths

typescript 复制代码
export interface AuditContext {
  auditDir: string;     // 审计根目录
  runId: string;        // 唯一 ID
  projectRoot: string;
  executionRoot: string;
}

export interface RunFilePaths {
  stdoutPath: string;     // <auditDir>/<runId>/stdout.txt
  stderrPath: string;     // <auditDir>/<runId>/stderr.txt
  metadataPath: string;   // <auditDir>/<runId>/metadata.json
}

AuditContext 是每次命令执行时的审计上下文------"这次跑在哪个项目、哪个目录、审计日志写到哪"。RunFilePaths 是本次执行产生的三个文件路径(标准输出、标准错误、元数据),方便后续查阅。

8.3 OutputCapture

子进程输出(stdout/stderr)的流式写入器------一边接收数据一边写文件,写超了就截断。你可以把它理解为一个"带容量限制的漏斗"。

typescript 复制代码
export interface OutputCapture {
  readonly filePath: string; // 当前捕获写入的目标文件路径
  readonly bytesWritten: number; // 已累计写入的字节数
  readonly truncated: boolean; // 是否已触发字节上限截断
  appendBuffer(chunk: Buffer): void; // 追加二进制 chunk(内部按上限截断)
  appendText(text: string): void; // 追加 UTF-8 文本
  close(): void; // 关闭底层写流
}

9. proxy.ts------网络代理相关

9.1 ProxyAccessDecision

代理对每次网络请求做出的"放行还是拦截"判定结果。比如子进程想连 registry.npmjs.org,代理查白名单后给出:允许、命中哪条规则、解析出的 IP 是什么。如果拒绝,还会告诉你为什么。

typescript 复制代码
export interface ProxyAccessDecision {
  allowed: boolean;          // 是否允许
  normalizedHost: string;    // 标准化后的主机名
  port: number;              // 端口
  resolvedAddress?: string;  // DNS 解析出的地址
  matchedRule?: string;      // 命中的白名单规则
  reason?: string;           // 拒绝原因
}

9.2 NetworkAllowlist

网络白名单------只有在这个列表里的域名才允许访问。就像公司 WiFi 的 MAC 地址白名单,不在列表里的设备连不上。

typescript 复制代码
export interface NetworkAllowlist {
  allow: string[];  // 如 ["registry.npmjs.org", "*.github.com"]
}

10. executor.ts------执行器回调

子进程执行时,stdout/stderr 的数据流、超时、启动失败等事件,都通过这个回调接口通知调用方。你可以把它理解为"子进程的传声筒"------子进程说了什么、出了什么问题,都从这里传出来。

typescript 复制代码
export interface SpawnIoHooks {
  onStdout?: (chunk: Buffer) => void;
  onStderr?: (chunk: Buffer) => void;
  onTimeout?: () => void;
  onSpawnError?: (message: string) => void;
}

11. runner.ts------Runner 相关

当操作系统的沙箱能力不可用(比如你不在 macOS 上、或者 sandbox-exec 被禁用了),Runner 需要问调用方:"怎么办?是降级到 danger-full-access 继续跑,还是直接中止?"下面三个类型就是处理这种场景的。

typescript 复制代码
/**
 * OS 沙箱不可用时的用户决策(§9)。
 *
 * - `danger-full-access`:显式允许「不走 OS sandbox 全开执行」。
 * - `abort`:完全拒绝执行,返回退出码 1。
 */
export type OsSandboxFallbackDecision = 'danger-full-access' | 'abort';

/** 传给 `onOsSandboxUnavailable` 回调的上下文 */
export interface OsSandboxUnavailableContext {
  reason: string; // 不可用原因(如无 `sandbox-exec`、profile 缺失)
  requestedMode: Exclude<SandboxMode, 'danger-full-access'>; // 调用方原本请求的沙箱模式(不含 `danger-full-access`)
  platform: NodeJS.Platform; // 当前运行平台(`process.platform`)
}

/** `checkOsSandboxCapability` 对指定 mode 的探测结果 */
export interface OsSandboxCapability {
  available: boolean; // 当前平台是否可用 OS 沙箱执行该 mode
  reason: string; // 可用时为说明文案;不可用时为失败原因
  platform: NodeJS.Platform; // 探测时的运行平台
}

12. seatbelt.ts、file-patch.ts、worktree.ts

12.1 Seatbelt

SeatbeltCapability 告诉你当前平台能不能用 macOS 的 sandbox-exec;SeatbeltSpawnSpec 是调用 sandbox-exec 时需要的命令和参数------相当于"启动 OS 沙箱的配方"。

typescript 复制代码
export interface SeatbeltCapability {
  available: boolean;
  reason: string;
}

export interface SeatbeltSpawnSpec {
  command: 'sandbox-exec';
  args: string[];
}

12.2 FilePatch

文件修改三件套:FilePatchInput 是调用方提交的"我想改这个文件"的请求;FilePatchProposal 是沙箱生成的正式提案(包含 diff),等待审批;FilePatchResult 是最终结果------改了没有、批了没有、原因是什么。

简单说:Input 是草稿 → Proposal 是正式提案 → Result 是审批结果。

typescript 复制代码
export interface FilePatchInput {
  relativePath: string;       // 相对路径,不能含 .. 或绝对路径
  nextContent: string | null; // null 表示删除文件
  reason?: string;
}

export interface FilePatchProposal {
  relativePath: string;
  absolutePath: string;
  isNewFile: boolean;
  isDeletion: boolean;
  previousContent: string | null;
  nextContent: string | null;
  diff: string;              // unified diff 文本
  reason?: string;
}

export interface FilePatchResult {
  applied: boolean;
  proposal: FilePatchProposal;
  approved: boolean;
  reason?: string;
}

12.3 Worktree

GitWorktreeHandle 是 worktree 的"门票"------创建成功后给你一个路径和分支名,后续操作靠它找回来;WorktreePromoteResult 是把 worktree 的修改合并回原项目后的结果------成功了还是失败了、从哪合并到哪。

typescript 复制代码
export interface GitWorktreeHandle {
  worktreePath: string;   // worktree 目录路径
  branchName: string;     // 创建的分支名
}

export interface WorktreePromoteResult {
  promoted: boolean;       // 是否成功合并
  fromPath: string;
  toPath: string;
  branchName?: string;
  message: string;
}

13. 小结

类型系统就是这个包的"词汇表"。回顾一下最核心的几个概念:

概念 类型 一句话
沙箱模式 SandboxMode 能干多少事
风险等级 CommandRiskLevel 命令有多危险
审批策略 ApprovalPolicy 什么时候要问人
审批决策 SandboxApprovalDecision 问了人之后的回答
命令输入 SandboxCommandInput 用户给我们的命令
命令结果 SandboxCommandResult 执行完返回的东西
配置 SandboxConfig 所有可调参数

后续的篇目里,你会反复看到这些类型。现在你已经认识它们了!

相关推荐
犀利豆3 分钟前
为什么全世界的 AI 都画不好一只骑自行车的鹈鹕
人工智能·llm·aigc
天天被压力12 分钟前
【跨市场数据实战 #08】可转债折价机会怎么筛:3个接口抓比价、列表和实时盘口
java·人工智能·python
baopixiaoz28 分钟前
BeeQuant × BeeAgent:用AI加速策略验证
大数据·人工智能·python·区块链
咸鱼老弟38 分钟前
AI Agent 的"自主性悖论"——为什么给它的自由度越大,越要配一套更硬的护栏
前端·人工智能
tq108643 分钟前
对字节AI战略的评价
人工智能
专职八阿哥1 小时前
GPT-6 Astar 是什么?从模型名称、A*算法到实际接入讲清楚
人工智能
龙腾AI白云1 小时前
大语言模型:从语言理解到通用智能的跃迁
数据库·人工智能·机器学习·知识图谱
EatFans2 小时前
AI Agent 为什么总是失忆?一篇讲透 Agent Memory
人工智能
sijiaoh2 小时前
用 Jev 写了个按意思搜的 grep
人工智能
智商网输送线配件2 小时前
2026 自动化流水线配件采购难题破解:小批量混批与非标定制的供应链实测
大数据·人工智能·自动化·智商网·流水线设备配件