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

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 所有可调参数

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

相关推荐
卡梅德生物科技小能手1 小时前
卡梅德生物科普:MAPT(微管相关蛋白Tau)
人工智能·经验分享·机器学习
战族狼魂2 小时前
基于 CNN 的ConvS2S(Convolutional Sequence-to-Sequence)架构英德机器翻译模型
人工智能·cnn·机器翻译
me8322 小时前
【AI面试】小白理解大模型:仅编码器(BERT类)、仅解码器(GPT类)和完整的编码器-解码器架构各有什么优缺点?
人工智能·gpt·ai·bert
醒醒该学习了!2 小时前
大语言模型(理论篇)
人工智能·语言模型·自然语言处理
小二·2 小时前
AI 代码审查 VSCode 插件实战
ide·人工智能·vscode
未来之窗软件服务2 小时前
精选之变,顺势而生(2026 年高考语文作文)
大数据·人工智能·高考·仙盟创梦ide·东方仙盟
意图共鸣2 小时前
意图共鸣科技发布《AI记忆链商业化白皮书3.0》:从存算解耦到“第二大脑”的技术演进
人工智能·科技·架构
仰望星空的代码2 小时前
科技是市场的唯一
大数据·人工智能·科技·财经·股市行情
芯盾时代2 小时前
企业建立安全防线治理失控的Agent
大数据·人工智能·安全