跳到主要内容

Cloudflare Sandbox SDK — 架构与原理

30 秒导读: 这是一个 npm 包(@cloudflare/sandbox),让你在 Cloudflare Worker 里 用几行 await 就能拿到一台隔离的 Linux 容器,在里面跑任意命令、读写文件、起后台 服务、把端口暴露成公网 URL。专为「AI 帮你跑它自己生成的代码」这类需要安全执行不可信代码 的场景设计。

1. 这是什么(零基础也能懂)

一句话定义: Sandbox SDK 把「一台可编程的 Linux 计算机」做成 Worker 里的一个对象—— 你调它的方法,它就在一个隔离容器里替你执行。

仓库 package.json:4 给自己的描述就一句话:"an api for computers"(给计算机的 API)。

解决什么问题 / 给谁用:

假设你在写一个 AI agent,模型生成了一段 Python,你必须把它跑起来才知道对不对。 但你绝不敢在自己的服务器进程里 eval 它——那是把钥匙交给陌生人。你需要一个用完即弃、 炸了也炸不到你的盒子。Sandbox SDK 就是那个盒子,而且开在离用户最近的 Cloudflare 边缘。

典型用户:

场景为什么需要沙箱
AI 代码执行跑模型生成的不可信代码
在线 IDE / Notebook给每个用户一个隔离的执行环境
数据分析平台跑用户上传的脚本
CI / 构建临时、干净的构建环境

它能做什么(功能):

  • 执行 shell 命令(exec),拿到 stdout / stderr / exitCode
  • 流式执行(execStream)与后台进程(startProcess)
  • 读写 / 列目录 / 删除 / 监听文件变化
  • git checkout 克隆仓库
  • 代码解释器:跑 Python / JS 并拿到结构化结果(图表、表格、富输出)
  • 把容器内端口暴露成公网预览 URL,或开 Cloudflare 隧道
  • 把容器目录备份到 R2、之后恢复

用起来什么样: 一段最小的真实 Worker(摘自 packages/sandbox/README.md):

import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';
export { Sandbox } from '@cloudflare/sandbox';

export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 预览 URL 路由必需:先让 SDK 看这是不是发给某个暴露端口的请求
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;

const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); // 按名字拿到沙箱
const result = await sandbox.exec('python3 -c "print(2 + 2)"');
return Response.json({ output: result.stdout }); // → "4"
}
};

你写的是「调一个本地对象的方法」,底层是跨进程、跨容器的远程调用——但 SDK 把它藏起来了。

一句话直觉 / 类比:getSandbox(ns, 'my-sandbox') 想成 「按名字打开一台云端电脑的远程桌面」:同一个名字永远连到同一台机器(状态保留), exec 就是在它的终端里敲命令。这台「电脑」其实是 「一个 Durable Object(有状态的 Worker)+ 它管的一个容器」——DO 是大脑(记住身份、端口、配置), 容器是手脚(真正跑命令)。

2. 顶层全景(它大概怎么转)

整个系统是三层,数据从你的 Worker 一路流到容器里的 bash:

你的 Worker 代码
│ getSandbox(ns, id).exec("...")

┌──────────────────────────────────────────────┐
│ Sandbox(Durable Object) —— @cloudflare/sandbox │ ← 发布到 npm 的公开包
│ · 记住身份/端口/配置(DO 存储) │
│ · 选一种传输,把调用转发给容器 │
└───────────────┬──────────────────────────────┘
│ 控制通道(默认 capnweb RPC over /rpc WebSocket)

┌──────────────────────────────────────────────┐
│ 容器运行时 —— @repo/sandbox-container │ ← 打进 Docker 镜像,不发布
│ · Bun HTTP 服务器(端口 3000) │
│ · DI 容器 → 路由 → 服务(命令/文件/进程…) │
│ · 一个常驻 bash 真正跑命令 │
└──────────────────────────────────────────────┘

三层各自干什么:

职责发布?
SDK@cloudflare/sandbox(packages/sandbox/)Sandbox DO 类 + 各能力客户端 + 预览路由
共享@repo/shared(packages/shared/)双方共用的类型、错误类、日志
容器运行时@repo/sandbox-container(packages/sandbox-container/)容器内的 Bun HTTP 服务器 + 服务否(打进镜像)

来源:架构 skill /.agents/skills/architecture/SKILL.md,与三个包的 package.json 一致。

主线走一遍(高层,不进代码):

  1. 你调 getSandbox(env.Sandbox, 'my-sandbox')(packages/sandbox/src/sandbox.ts:696 getSandbox)。 它通过 @cloudflare/containersgetContainer 按 id 拿到对应的 Sandbox Durable Object 桩, 再包一层 Proxy 增强若干方法。
  2. 你调 sandbox.exec('...')。请求进入 Sandbox DO(Sandbox 类,sandbox.ts:947)。
  3. DO 通过它选定的 client(默认 RPC,见 §2 文档)把调用送到容器
  4. 容器里的 Bun 服务器(packages/sandbox-container/src/server.ts:172 startServer)收下, 交给 SandboxControlAPI(control-plane/api.ts:79)→ 对应服务 → 常驻 bash 真正执行。
  5. stdout/stderr/exitCode 原路返回,封成 ExecResult 给你。

这条主线在 §1 文档(架构)里逐帧拆开。

3. 阅读地图(按这个顺序读)

这个项目子系统很多(命令、文件、进程、端口、隧道、备份、解释器…),但主干就一条: DO ↔ 容器的受控通道。建议顺序:

  1. 01-architecture.md —— 先把三层、getSandbox 的 Proxy 增强、一次请求的端到端流程吃透。 读完你能讲清「我的 await 到底经过了谁」。
  2. 02-control-plane.md —— DO 怎么跟容器说话:三种传输、capnweb RPC 主通道,以及 最妙的一招:用 capnweb 的 getStats() 判忙闲来决定沙箱何时休眠。
  3. 03-sessions-and-exec.md —— 会话隔离 + 容器里那个常驻 bash 怎么把 stdout/stderr 用二进制前缀分流。这是「为什么 cd 在下一条命令还在」的答案。
  4. 04-preview-and-ports.md —— exposePort{port}-{id}-{token}.domain 预览 URL、 proxyToSandbox 路由、运行时身份 + token 双重鉴权。
  5. 05-interpreter-tunnels-backup.md —— 上层能力:代码解释器(结构化输出)、快速隧道、R2 备份。

每章末尾都有代码地图(主题 | 文件 | 符号名),可直接 grep 跳进源码。

4. 一眼记住的几个「精华」

下面这些是读完全套最该带走的设计决策(各章详述):

  • DO 当大脑、容器当手脚。 身份、端口 token、配置都存在 Durable Object 里(跨容器重启存活); 容器是可丢弃的执行体。预览 URL 的鉴权由 DO 拥有,转发只在端口被当前运行时激活后才生效 (sandbox.ts:exposePort + currentRuntime)。
  • 休眠靠「会话忙闲」而非「请求计数」。 RPC 传输把所有调用复用在一条 WebSocket 上, 一个返回 ReadableStream 的调用其 Promise 早就 resolve 了、但流还在写——所以 SDK 改去轮询 capnweb 的 getStats(),只要 imports/exports 超过基线就算「忙」,把休眠闹钟往后推 (container-control/client.ts 文件头注释)。
  • 会话 = 一个常驻 bash。 cdexport、shell 函数能跨命令存活,是因为容器维护一个长寿命 bash 进程;stdout/stderr 用二进制前缀写进同一份日志再解析回两路 (sandbox-container/src/session.ts 文件头)。
  • 预览 URL 把路由信息编进子域名: {port}-{sandboxId}-{token}.yourdomain.com, proxyToSandbox 解析子域名 → 找到沙箱 → 校验 token → 转发(request-handler.ts + preview-url.ts)。

5. 边界与局限(先知道它不做什么)

  • 隔离不是 SDK 做的。 容器级隔离(VM 边界)由 Cloudflare 平台负责,SDK 代码不负责沙箱逃逸防护 (架构 skill「Container isolation — handled at the Cloudflare platform level」)。
  • 生产预览 URL 需要自定义域名 + 通配 DNS(*.yourdomain.com)。 .workers.dev 不支持所需的 子域名形态,exposePort 会直接抛 CustomDomainRequiredError(sandbox.ts:4994)。
  • 隧道 URL 不跨容器重启存活。 quick tunnel 的主机名是 cloudflared 启动握手时分配的, 每次重启都换;SDK 在容器启动时清缓存(README「Quick tunnels」节)。
  • 端口范围受限: 只允许 1024–65535,且 3000 被控制面占用(security.ts:validatePort)。
  • tunnels 需要 RPC 传输。 路由式传输的 tunnels 桩会抛 "RPC transport required"(README)。