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 | 只读,无害 | ls、cat、git status |
| L1 | 低风险本地执行 | node script.js、jest |
| L2 | 会写工作区 | mkdir、cp、git add |
| L3 | 网络/依赖/外部交互 | curl、npm install、git push |
| L4 | 高危破坏性 | rm -rf、sudo、git 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_session,sessionAllowsAll 就变成 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_session,sessionAllowsAll 就变成 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 |
所有可调参数 |
后续的篇目里,你会反复看到这些类型。现在你已经认识它们了!