Sandbox SDK:命令、文件、进程与会话 API 实战

This article is extracted from the chat log with AI. Please identify it with caution.

说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。

Sandbox SDK 的价值不是“替你执行一个命令”,而是把一套隔离 Linux 环境的生命周期、文件系统、进程、解释器与服务暴露能力,收敛为 Worker 中可调用的 TypeScript API。它建立在 Containers 与 Durable Objects 之上,适合 AI Agent、代码解释器、在线 IDE 和不可信代码执行。

本文使用当前 RPC 模式写示例。Sandbox SDK 的接口在持续演进,旧项目如使用 HTTP/WebSocket transport、默认 session 或 exposePort(),应先阅读文末的 2026 迁移指南。

先区分四个层次#

flowchart LR
  R["HTTP 请求"] --> W["你的 Worker:认证与授权"]
  W --> ID["sandboxId:用户或租户边界"]
  ID --> SDK["getSandbox()"]
  SDK --> DO["Sandbox Durable Object"]
  DO --> C["独立 Linux Container"]
  C --> F["文件系统 /workspace"]
  C --> P["进程与 shell"]
  C --> I["代码解释器 context"]
  C --> T["Tunnel 或端口服务"]

sandboxId 是稳定的对象身份,不是永久磁盘。第一次真正操作才会启动容器;容器休眠、重启或销毁后,文件、后台进程、shell session 与解释器变量都会消失。要跨生命周期保存数据,使用 R2/S3/GCS 挂载或备份,或把控制面状态放在 Durable Object SQLite。

1. 最小配置与安全的取得方式#

官方当前最小配置包含 Container、Sandbox Durable Object binding 与 SQLite migration:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "code-runner",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-07",
  "compatibility_flags": ["nodejs_compat"],
  "containers": [
    {
      "class_name": "Sandbox",
      "image": "./Dockerfile"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "Sandbox",
        "class_name": "Sandbox"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["Sandbox"]
    }
  ]
}

Sandbox SDK 当前配置文档仍以 migration 方式初始化该 class。一般 Durable Object 的声明式 exports 与旧 migrations 不能在同一 Worker 中混用;以所用 SDK 模板及当前官方配置为准,不要自行同时配置两种 lifecycle 流程。

Worker 入口必须重新导出 SDK 的 Sandbox class:

import { getSandbox, type Sandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

interface Env {
  Sandbox: DurableObjectNamespace<Sandbox>;
}

function sandboxIdFor(tenantId: string, taskId: string) {
  return "tenant-" + tenantId + "-task-" + taskId;
}

export default {
  async fetch(request: Request, env: Env) {
    const { tenantId, taskId } = await authenticate(request);

    const sandbox = getSandbox(
      env.Sandbox,
      sandboxIdFor(tenantId, taskId),
      {
        transport: "rpc",
        enableDefaultSession: false,
        sleepAfter: "10m",
      },
    );

    return Response.json({ sandboxCreated: Boolean(sandbox) });
  },
} satisfies ExportedHandler<Env>;

把未经鉴权的 user ID 直接拼成 sandbox ID 是常见漏洞。应先完成认证与租户授权,再由可信 Worker 生成只属于一个不可信主体的 ID。不同用户必须使用不同 sandbox;同一 sandbox 中的 session 共享文件系统、进程空间与 localhost 网络,session 不是多租户隔离边界。

2. SDK 接口地图#

下面是面向日常应用最有用的接口分组。它不是要你一次学完所有方法;应先按任务选择一组 API。

任务常用接口什么时候用
生命周期getSandbox()destroy()取得按 ID 寻址的 sandbox;临时任务结束后主动销毁。
单次命令exec(command, options)执行固定命令,读取完整 stdout/stderr/exitCode。
后台服务startProcess()getProcess()listProcesses()killProcess()启动并管理 dev server、编译器或长期运行服务。
进程日志getProcessLogs()streamProcessLogs()诊断或把长任务输出转给浏览器。
文件writeFile()readFile()mkdir()listFiles()将输入写到绝对路径,并读取生成物。
会话createSession()getSession()deleteSession()只有确实需要保存 cwd/env/shell 上下文时才创建。
环境变量setEnvVars()在任何命令之前设置;避免把长期密钥放进不可信容器。
代码解释器createCodeContext()runCode()deleteCodeContext()需要 Python/JS/TS 的交互式状态与富输出时使用。
服务访问tunnels.get(port)tunnels.destroy(port)为 sandbox 内已监听的 HTTP 服务创建公网 URL。
持久数据Storage / Backups API挂载对象存储或备份目录,不把容器磁盘当数据库。

3. 一次性任务:文件 API + 固定命令 + stdin#

对于用户提交的源代码或数据,优先把内容写入文件或通过 stdin 传入;不要把用户文本插入 shell 命令字符串。

const sandbox = getSandbox(env.Sandbox, sandboxId, {
  transport: "rpc",
  enableDefaultSession: false,
});

await sandbox.writeFile(
  "/workspace/runner.py",
  [
    "import json",
    "import sys",
    "payload = json.load(sys.stdin)",
    "print(payload['name'].upper())",
  ].join("\n"),
);

const result = await sandbox.exec("python3 /workspace/runner.py", {
  stdin: JSON.stringify({ name: "cloudflare" }),
  timeout: 10_000,
});

if (!result.success) {
  throw new Error("runner failed: " + result.stderr);
}

return Response.json({
  output: result.stdout.trim(),
  exitCode: result.exitCode,
});

exec() 返回 successstdoutstderrexitCode。它的 cwdenvstdintimeout 与实时输出回调适合单次工作;命令超时会让调用端报错,但底层进程可能仍在容器内运行,因此需要在失败策略中考虑删除 session、杀死进程或销毁 sandbox。

RPC transport 下,writeFile()readFile() 还能处理 ReadableStream,适合大文件和二进制传输,而不必把数据转成巨大的 base64 字符串。

4. 有状态构建:显式 session,而不是默认 shell#

默认 session 是兼容行为。新应用推荐 enableDefaultSession: false:没有显式 session 的调用彼此不继承 shell state。需要连续执行构建步骤时,创建一个任务专属 session:

const session = await sandbox.createSession({
  id: "build",
  cwd: "/workspace/app",
  commandTimeoutMs: 60_000,
});

try {
  const install = await session.exec("npm ci");
  if (!install.success) throw new Error(install.stderr);

  const test = await session.exec("npm test");
  if (!test.success) throw new Error(test.stderr);
} finally {
  await sandbox.deleteSession("build");
}

session 只隔离 cwd、环境变量和命令执行状态。两个 session 仍可看见彼此写入的文件和启动的进程,所以不要因为“用了不同 session”就把不可信租户放进同一个 sandbox。

5. 交互式分析:Code Context 与 runCode()#

当任务是 LLM 驱动的数据分析或 notebook 式交互时,runCode() 比自己拼 shell 命令更自然。显式 context 让变量、imports 和函数在同一个活跃容器内持续存在:

const context = await sandbox.createCodeContext({
  language: "python",
  cwd: "/workspace",
});

await sandbox.runCode(
  [
    "values = [2, 3, 5, 7]",
    "total = sum(values)",
  ].join("\n"),
  { context },
);

const result = await sandbox.runCode(
  "print({'count': len(values), 'total': total})",
  { context },
);

if (result.error) {
  throw new Error(result.error.value);
}

return Response.json({
  logs: result.logs.stdout,
  richOutputs: result.results,
});

runCode() 支持 Python、JavaScript 与 TypeScript,并能返回文本、表格、图表等富输出。context 的状态只在容器仍活跃时存在;需要长期保存的计算结果应写入外部存储。任务结束时可以 deleteCodeContext(context.id) 回收资源。

6. 启动服务并暴露 URL:进程 + Tunnel#

下面是一个预览服务的典型流程:先按固定 process ID 复用或启动服务,等待端口就绪,再通过 tunnel 取得 URL。

let server = await sandbox.getProcess("preview");

if (!server) {
  server = await sandbox.startProcess(
    "python3 -m http.server 8080 --directory /workspace/app",
    { processId: "preview" },
  );
}

await server.waitForPort(8080);

const tunnel = await sandbox.tunnels.get(8080);
return Response.json({ url: tunnel.url });

tunnels.get(8080) 创建 quick tunnel,适合本地开发、演示或短期预览;容器重启后 URL 会变。指定 { name: “preview” } 可创建指向自有 zone 的 named tunnel,适合稳定 hostname、webhook 和 OAuth callback,但会创建 Cloudflare Tunnel/DNS 资源,必须在生命周期结束时清理。Tunnel API 需要 RPC transport。

7. 生命周期、持久化与安全决策#

问题正确理解
同一 ID 是否同一机器/磁盘?是同一 Sandbox DO 身份,不保证同一活跃 Container;重启后运行态会消失。
可否把每个用户放进 session?不行。隔离边界是 sandbox ID;session 只是一组 shell 上下文。
何时 destroy()临时 CI、一次性 Agent task、临时预览结束时;它会清理运行态和相关资源。
怎样保存成果?R2/S3/GCS 挂载、备份 API,或由 Worker 把结果写到外部存储。
是否能把 API key 放进容器?尽量不要。应让 Worker 的 outbound handler/Service Binding 在可信侧注入凭据。
是否默认允许网络?不要依赖默认值。按需求关闭互联网,再只开放 allowlist 或受控出口。

Sandbox 提供 VM 级隔离,但隔离不代替应用认证、授权、审计、资源配额和输入验证。尤其不要把未验证用户输入拼到命令、路径、tunnel name 或 sandbox ID 中。

8. 从旧示例迁移时检查#

  • 使用 RPC transport;tunnels 依赖 RPC。
  • exposePort() 的公网 URL 场景迁到 tunnels.get(),并根据稳定性选择 quick 或 named tunnel。
  • 设置 enableDefaultSession: false,依赖 cwd/env 的连续任务改用显式 session。
  • 把旧的 stream 专用 helper 迁到基础 exec()readFile()writeFile() 的当前流式能力。
  • 为长任务定义 timeout、进程清理和 destroy() 策略。

参考资料#

本文共 3068 字,创建于 Aug 7, 2026

相关标签: Cloud, DevOps, TypeScript, ByAI