Pi 与沙箱

为什么只靠正则拦截命令不够

在之前介绍 pi 的扩展系统时,我展示了一个 permission-gate.ts 的例子,用正则表达式拦截危险命令:

// permission-gate 的思路:正则匹配,匹配到就弹窗确认
const rules = [
  { pattern: /\bfind\s+\/(?![a-zA-Z0-9_./])/i, label: "find-root" },
];

这种基于模式匹配的"权限门"看似有用,但在现代大模型面前形同虚设。一个足够聪明的 LLM 可以轻松绕过

# 绕过正则的方式:
find . -path '*'              # 等价 find /
x="f"; y="ind"; $x$y /        # 变量拼接绕过
exec 3<>/dev/tcp; ...          # 使用完全不同的原语
python -c "import os; ..."     # 用解释器写文件

Claude Code 和 Codex 等主流 agent 的做法是:在权限规则之外,再加一层操作系统层面的沙箱隔离。权限规则控制"能不能执行",沙箱控制"执行后能碰什么"两者互补。一旦沙箱生效,不管 LLM 想出多诡异的命令,它也破坏不了沙箱允许范围之外的东西。

沙箱做什么

讲沙箱之前,先从最熟悉的 Docker 开始。

从 Docker 到沙箱

Docker 大家应该都用过docker run 起一个容器,里面有独立的文件系统,端口要显式映射才能访问。这其实就是一个比较重的沙箱实现。

但当前主流的 agent 的沙箱一般不用 Docker。原因是 agent 要把你的项目目录挂进去让 LLM 改代码,还要实时拿到 stdout/stderr 反馈给 LLM,每次跑一条命令就起一个容器实在太慢了。

所以 agent 沙箱要解决的问题是能不能在不到 Docker 的复杂度下,获得接近 Docker 的隔离效果?

主流方案沿着两条路线演进:

路线一:容器化内核特性(轻量但需要 root 或特权)

Docker 底层用的是 Linux namespace + cgroup。其中 namespace 可以把一个进程关进独立的"视窗",它看到自己的 /、自己的网络栈、自己的 PID 树,但其实和宿主共享同一个内核。

bubblewrap(简称 bwrap)就是 namespace 的精简版。它不需要 dockerd,不需要镜像,不需要 cgroup,只做一件事,给一个进程创建一个新的 mount namespace 和 network namespace,然后 exec 它。

# 手动用 bubblewrap 跑一个隔离的 bash
bwrap \
  --ro-bind /usr /usr \          # 只读挂载 /usr
  --bind /tmp /tmp \             # 读写挂载 /tmp
  --dev /dev \                   # 挂载设备
  --proc /proc \                 # 挂载 /proc
  /bin/bash
# 这个 bash 只能访问 /usr、/tmp 和 /dev ,
# 宿主上的 ~/、/etc/passwd 等全部不可见

agent 沙箱就是围绕 bubblewrap(或 macOS 的 Seatbelt)这种级别的能力构建的。挂载你要改的项目目录为可写,其余全部隔离。

路线二:微型虚拟机(隔离最强但更重)

如果连内核共享都觉得不安全,那就上真正的虚拟机,也就是在宿主上起一个独立的 Linux 内核,硬件级的隔离。

传统虚拟机软件(VirtualBox、VMware)当然不行,光是启动就几十秒。所以有了微型 VM(microVM)这个方向:把传统 VM 里用不到的东西全砍掉,只留一个极简内核 + 极少的虚拟设备,启动时间压到毫秒级。

这个方向最出名的产品是 Firecracker,AWS 为 Lambda 写的,Rust 实现,启动 <100ms,每个 Lambda 函数调用都在独立的 microVM 里跑。但它只能在 Linux 上跑,而且场景非常专一(serverless 函数执行)。

另一个更通用的 microVM 方案是 QEMU。

QEMU 是 2003 年 Fabrice Bellard(也是 FFmpeg 的作者)写的一个开源虚拟机。你可以把它当成一个纯命令行的、跨平台的、可编程的 VirtualBox。它有两套工作模式:

agent 沙箱显然用虚拟化模式,我们不关心跨架构模拟,要的是尽量快的执行速度。

为什么 QEMU 在沙箱场景下比 Firecracker 更合适?Firecracker 快是快,但它被 AWS 锁定在 serverless 场景,它只支持 Linux KVM,只暴露极少的虚拟设备,你没法在 macOS 上用,也没法灵活定制网络行为。QEMU 则相反:跨平台是它的核心能力之一,macOS 用户装个 brew install qemu 就能跑,而且设备模型丰富(virtio-net 可以让 Gondolin 在宿主侧拦截和重写所有网络流量)。虽然比 Firecracker 重一些(冷启动约 1 秒),但对 agent 这种"跑一条命令等结果"的场景,1 秒启动完全在可接受范围内。

两条路线的对比

路线一:容器化 路线二:microVM
代表技术 bubblewrap、Seatbelt Firecracker、QEMU
隔离原理 共享宿主内核,靠 namespace/profile 限制 独立内核,硬件级隔离
启动速度 毫秒级 百毫秒 ~ 1 秒
平台限制 每平台不同 API QEMU 跨平台,Firecracker 仅 Linux
配置复杂度 低 需要准备内核镜像
适合场景 日常开发,快速隔离 执行不可信代码,需要强隔离

三层隔离

不管是 bubblewrap、Seatbelt 还是 Firecracker 风格的微 VM,沙箱的目标都是控制三个层面:

层面 要控制什么 怎么控制
文件系统 能读哪些路径、能写哪些路径 mount namespace / Seatbelt profile / chroot
网络 可以连哪些域名和 IP 代理(SOCKS5)或用户态网络栈
系统调用 进程能调用哪些内核函数 seccomp-bpf 过滤器 / no_new_privs

文件系统决定"能碰什么文件",网络决定"能连什么地址",系统调用决定"能做什么操作"。

不同平台的实现

同一套思路,三个平台走了不同的底层 API:

两种主流 agent 的做法

了解了这些底层机制,再看 Claude Code 和 Codex 各自的选型,它们都走路线一(容器化 OS 沙箱),但实现深度不同。

Claude Code 用的是 npm 包 @anthropic-ai/sandbox-runtime:macOS 调 Seatbelt,Linux 调 bubblewrap + socat。特点是"够用就好",只隔离 bash 命令,网络通过 SOCKS5 代理做域名白名单,文件系统默认只能写工作目录。额外支持 credential masking(把真实密钥替换为 sentinel,代理层在出站时动态注入,沙箱内始终看不到真值)。

Codex(OpenAI)则激进得多。全 Rust 重写,把 bubblewrap 的 C 源码直接编译进自己的二进制里(不依赖系统安装),Linux 上 bubblewrap + seccomp-bpf 双保险,macOS 走 Seatbelt,Windows 走 Restricted Token + WFP。还多了一层进程加固pre_main() 阶段就禁止 ptrace attach、禁用 core dump、清理 LD_PRELOAD 等危险环境变量。可以说 Codex 目前是 agent 沙箱实现得最深的一个。

两者都还没走路线二(microVM),因为对大部分日常开发场景,OS 级沙箱的速度和隔离度已经够用了。路线二的 microVM 更多是给高安全场景(执行完全不受信任的第三方代码)准备的。

Pi 示例中的两个沙箱方案

Pi 在 examples/extensions/ 下提供了两个沙箱扩展,代表了两种不同的技术路线:

Sandbox(进程级 OS 沙箱)

基于 @anthropic-ai/sandbox-runtime,这也是 Claude Code 的沙箱内核。在 macOS 上调 Seatbelt,在 Linux 上调 bubblewrap + socat。

┌─────────────── Pi 主进程 ───────────────┐
│                                           │
│  ┌─────────────────────────────────────┐  │
│  │  bash 命令                           │  │
│  │  ┌─────────────────────────────┐    │  │
│  │  │ OS 沙箱 (Seatbelt/bubblewrap)│    │  │
│  │  │ • 只写 当前目录 + /tmp        │    │  │
│  │  │ • 只连 白名单域名             │    │  │
│  │  │ • 不能读 ~/.ssh ~/.aws       │    │  │
│  │  └─────────────────────────────┘    │  │
│  └─────────────────────────────────────┘  │
│                                           │
│  read / write / edit → 不受沙箱限制        │
└───────────────────────────────────────────┘

特点:只隔离 bash 命令,read/write/edit 等其他工具不受影响。配置简单,性能开销几乎为零。

Gondolin(VM 级微虚拟机)

基于 @earendil-works/gondolin,在本地启动一个精简 Linux VM(QEMU),把所有工具,不只是 bash,都路由进 VM。

┌─────── Pi 主进程(宿主)───────┐
│  Node.js / 扩展                 │
└────────────┬───────────────────┘
             │ virtio-serial / virtio-net
┌────────────▼─── VM 边界 ───────┐
│  Linux VM                       │
│  read / write / edit / bash     │  全部在 VM 里执行
│  ls / find / grep               │
│  /workspace ↔ 宿主 cwd         │  文件变动穿透回宿主
│                                 │
│  网络:通过 virtio-net 出站      │  协议分类后由宿主代发
└─────────────────────────────────┘

特点:整体隔离更强,LLM 的每一行代码都在独立 Linux VM 里跑。需要装 QEMU,启动有轻微延迟。

注:Gondolin 本身支持密钥注入(通过 createHttpHooks({ secrets }) 在宿主侧注入 HTTP header),但 pi 的 Gondolin 示例扩展没有配置密钥注入,仅做了工具路由和文件系统穿透。

对比总结

Sandbox 扩展 Gondolin
隔离级别 进程级(OS 沙箱) VM 级(完整虚拟机)
覆盖范围 只隔离 bash 隔离 read/write/edit/bash 全部
密钥保护 denyRead 禁止读密钥文件 独立内核,逻辑隔离
macOS 依赖 无(Seatbelt 内建) 需要 QEMU
Linux 依赖 bubblewrap + socat QEMU
启动开销 毫秒级 秒级
适用场景 日常开发防护 高安全需求

实践中大多数用户用 Sandbox 扩展就足够,它在 Claude Code 同款沙箱内核上覆盖了最危险的攻击面(bash 命令执行),配置直观,开箱即用。

Sandbox 扩展:安装、配置与使用

安装

# 找到 pi 安装路径下的示例
PI_EXAMPLES=$(dirname $(dirname $(which pi)))/lib/node_modules/@earendil-works/pi-coding-agent/examples/extensions

# 拷贝到扩展目录
cp -r "$PI_EXAMPLES/sandbox" ~/.pi/agent/extensions/

# 安装依赖
cd ~/.pi/agent/extensions/sandbox && npm install --ignore-scripts

重启 pi 即生效。

默认配置

沙箱默认开启,配置内嵌在 index.ts 中:

DEFAULT_CONFIG = {
  enabled: true,
  network: {
    allowedDomains: [
      "npmjs.org", "*.npmjs.org",
      "registry.npmjs.org", "registry.yarnpkg.com",
      "pypi.org", "*.pypi.org",
      "github.com", "*.github.com",
      "api.github.com", "raw.githubusercontent.com",
    ],
    deniedDomains: [],
  },
  filesystem: {
    denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
    allowWrite: [".", "/tmp"],
    denyWrite: [".env", ".env.*", "*.pem", "*.key"],
  },
};

解读:

自定义配置

三个优先级,浅合并:

  1. 代码默认值
  2. ~/.pi/agent/extensions/sandbox.json(全局覆盖)
  3. .pi/sandbox.json(项目级覆盖,优先级最高)

例如项目需要访问 OpenAI API:

// .pi/sandbox.json
{
  "network": {
    "allowedDomains": ["api.openai.com"]
  }
}

⚠️ 注意:配置合并是浅合并,allowedDomains 数组会被整个替换。如果项目配置只写 ["api.openai.com"],默认的 github.com 等全部会丢失。要把需要的域名全部写上:

{
  "network": {
    "allowedDomains": ["api.openai.com", "github.com", "*.github.com", "npmjs.org", "*.npmjs.org", ...]
  }
}

如果要关掉某个默认域名,可以用 deniedDomains:

{
  "network": {
    "deniedDomains": ["raw.githubusercontent.com"]
  }
}

使用

启动 pi 后,沙箱自动生效。状态栏会显示:

🔒 Sandbox: 10 domains, 2 write paths

三个常用操作:

# 查看当前配置
/sandbox

# 临时关闭(仅本次启动)
pi --no-sandbox

# 完全禁用
# 在 ~/.pi/agent/extensions/sandbox.json 中设置 { "enabled": false }

注意事项