本页替换或移除 Sandbox SDK 0.12 中在 1.0 没有对应类的功能。每个章节独立。仅遵循应用使用的功能对应的章节。
你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter、ensureRunning() 和 startContainer() 方法,以及 ENV 常量。通过 this.ctx.exports 访问的类需要兼容日期 2025-11-17 或更晚,或 enable_ctx_exports 标志。
每个 0.12 功能对应一节:
| 0.12 功能 | 章节 |
|---|---|
docker:dind-rootless 镜像中的 Docker |
在 sandbox 内运行 Docker |
读取绑定的 outboundByHost handler |
从容器访问 Workers 绑定 |
@cloudflare/sandbox/opencode |
启动 OpenCode 服务器 |
@cloudflare/sandbox/openai |
为 OpenAI 智能体提供 shell 和编辑器 |
@cloudflare/sandbox/bridge |
替换 bridge |
WarmPool |
移除 WarmPool |
0.x Docker 设置从 docker:dind-rootless 开始,切换到 USER root,并复制 0.x 服务器的 musl 构建。在 1.0 容器上,其 Docker 守护进程会以 failed to set IP forwarding '/proc/sys/net/ipv4/ip_forward' = '1': open /proc/sys/net/ipv4/ip_forward: read-only file system 退出。
-
替换你的
Dockerfile:Dockerfiledockerfile FROM docker:dind COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim ENTRYPOINT ["sh", "-c", "dockerd-entrypoint.sh dockerd --iptables=false --ip6tables=false --ip-forward=false > /var/log/dockerd.log 2>&1 & exec sleep infinity"]Docker 守护进程以
root身份运行,并关闭 iptables 和 IP 转发。它会记录到/var/log,因为docker:dind入口点会挂载一个空的/tmp。sleep infinity可保持容器运行。仅当你的类使用Files、S3Mount或DirectoryBackup时,才保留COPY行。sandbox-shim是静态链接的,因此可在基于 Alpine 的docker:dind镜像上运行。 -
在
start()中保持enableInternet: true,以便 Docker 可以拉取镜像。0.12 默认允许 Internet 访问。 -
等待 Docker,并用
--network=host运行容器:src/index.ts (0.12)ts const sandbox = getSandbox(env.Sandbox, "docker-sandbox"); const run = await sandbox.exec( "docker run --rm --network=host alpine:3.22 " + "echo Hello from Docker", );src/index.ts (1.0)ts export class MySandbox extends DurableObject<Env> { // ... private async waitForDocker(): Promise<void> { const wait = await this.container.exec([ "timeout", "30", "sh", "-c", "until docker version > /dev/null 2>&1; do sleep 0.2; done", ]); if ((await wait.exitCode) !== 0) { throw new Error("Docker did not start within 30 seconds"); } } async runImage(): Promise<string> { await this.ensureRunning(); // The Docker daemon starts after the container, so wait for it await this.waitForDocker(); const run = await this.container.exec( [ "docker", "run", "--rm", "--network=host", "alpine:3.22", "echo", "Hello from Docker", ], { env: ENV }, ); const result = await run.output(); return new TextDecoder().decode(result.stdout); } }没有
--network=host时,docker run会以failed to disable IPv6 on container's interface eth0失败。使用网络的docker build步骤(例如安装包)也需要docker build --network=host。以这种方式启动的内部容器共享 sandbox 的网络访问。 -
从 Worker 调用
runImage()。它返回Hello from Docker。第一次调用还会拉取alpine:3.22,Docker 将其进度写入stderr。
更多信息请参阅 Containers FAQ 中的 Docker-in-Docker。
在 0.12 中,outboundByHost handler 响应例如 my.kv 的主机名请求,并从 env 参数读取 Workers 绑定。在 1.0 中,WorkerEntrypoint 响应它们,你的 Durable Object 为该主机名注册它。
-
将每个 handler 替换为
WorkerEntrypoint,并从 Worker 的主模块导出它:src/index.ts (0.12)ts MySandbox.outboundByHost = { "my.kv": async (request: Request, env: Env) => { const key = new URL(request.url).pathname.slice(1); const value = await env.KV.get(key); return new Response(value ?? "", { status: value ? 200 : 404, }); }, };src/index.ts (1.0)ts import { WorkerEntrypoint } from "cloudflare:workers"; type KvProps = { containerId: string }; export class KvGateway extends WorkerEntrypoint<Env, KvProps> { async fetch(request: Request): Promise<Response> { const key = new URL(request.url).pathname.slice(1); const value = await this.env.KV.get(key); return new Response(value ?? "", { status: value === null ? 404 : 200, }); } }this.env具有与env参数相同的绑定。原先 0.12 handler 读取ctx.containerId的地方,改为读取this.ctx.props.containerId。 -
在
startContainer()的try块中注册入口点:src/index.tsts export class MySandbox extends DurableObject<Env> { // ... private async startContainer(): Promise<void> { const newContainer = !this.container.running; if (newContainer) { this.container.start({ image: this.container.images.sandbox, instance: "standard-1", env: ENV, // 0.12 allowed Internet access by default. enableInternet: true, }); } try { if (newContainer) { // Replaces onStart(). await this.ctx.storage.put("startedAt", Date.now()); } await this.container.interceptOutboundHttp( "my.kv", this.ctx.exports.KvGateway({ props: { containerId: this.ctx.id.toString() }, }), ); await this.container.setInactivityTimeout( INACTIVITY_TIMEOUT_MS, ); } catch (error) { // The next request starts a new container. await this.container.destroy(); throw error; } } }this.ctx.id.toString()是 0.12 作为containerId传递的值,因此从它构建的键和路径保持不变。该拦截也适用于enableInternet: false。为每个主机名注册一个入口点。如果你的类还使用 迁移出站规则 中的
Outbound入口点,改为将 handler 添加到其HANDLERS映射。在interceptAllOutboundHttp()之后注册的主机名拦截收不到请求。 -
从容器请求该主机名。在将
hello from KV存储到键greeting下之后,此命令会打印该值:node -e 'fetch("http://my.kv/greeting").then(async (r) => console.log(r.status, await r.text()))'200 hello from KV
在 0.12 中,createOpencode() 在 sandbox 中启动 opencode serve,并返回通过 sandbox 到达它的 OpenCode SDK 客户端。在 1.0 中,你的 Durable Object 启动服务器并创建客户端,使用连接到服务器端口的 fetch 函数。
-
在
Dockerfile中安装 OpenCode,如 在 sandbox 中运行 OpenCode 所述:Dockerfiledockerfile RUN npm install --global --ignore-scripts [email protected] \ && ln -s /usr/local/lib/node_modules/opencode-linux-x64-baseline/bin/opencode /usr/local/bin/opencode0.12
opencode镜像包含 OpenCode。用standard-1或更大的实例类型启动容器,如startContainer()所做。在lite上,服务器在 30 秒内未响应请求。 -
在 Worker 项目中安装 OpenCode SDK:
npm i @opencode-ai/[email protected]yarn add @opencode-ai/[email protected]pnpm add @opencode-ai/[email protected]bun add @opencode-ai/[email protected] -
用为每个容器启动一次服务器的方法替换
createOpencode()。它使用 在后台运行服务器 第 1 步中的脚本和函数:src/index.ts (0.12)ts const sandbox = getSandbox(env.Sandbox, "my-agent"); const { client } = await createOpencode(sandbox, { directory: "/workspace", config: OPENCODE_CONFIG, }); const session = await client.session.create({ title: "Fix tests" });src/index.ts (1.0)ts import { createOpencodeClient, type OpencodeClient, } from "@opencode-ai/sdk/v2/client"; const OPENCODE_PORT = 4096; const OPENCODE_DIR = `${SERVERS}/opencode`; // Your OpenCode configuration, without apiKey values. const OPENCODE_CONFIG = {}; const OPENCODE = [ "opencode", "serve", "--port", String(OPENCODE_PORT), "--hostname", "0.0.0.0", ];src/index.ts (1.0)ts export class MySandbox extends DurableObject<Env> { // ... private opencodeReady = false; private async openCode(): Promise<OpencodeClient> { await this.ensureRunning(); if (!this.opencodeReady) { await startServer(this.container, OPENCODE_DIR, OPENCODE, { cwd: "/workspace", env: { ...ENV, OPENCODE_CONFIG_CONTENT: JSON.stringify(OPENCODE_CONFIG), ANTHROPIC_API_KEY: this.env.ANTHROPIC_API_KEY, }, }); await waitForServer( this.container, OPENCODE_DIR, OPENCODE_PORT, 30_000, ); this.opencodeReady = true; } const port = this.container.getTcpPort(OPENCODE_PORT); return createOpencodeClient({ baseUrl: "http://opencode", fetch: (request) => port.fetch(request), }); } async createSession(title: string): Promise<string> { const client = await this.openCode(); const session = await client.session.create({ title }); if (!session.data) { throw new Error("OpenCode did not create the session"); } return session.data.id; } }从 OpenCode 配置中移除
apiKey,并将每个密钥存储为 secret。与 0.12 一样,该方法将配置作为OPENCODE_CONFIG_CONTENT传递,并将密钥放在例如ANTHROPIC_API_KEY的提供商变量中。容器可以读取该密钥。要将模型凭据保留在 Worker 中,请参阅 在 sandbox 中运行 OpenCode。在
startContainer()中、if (newContainer)内部设置this.opencodeReady = false,因为新容器没有服务器。如果 Durable Object 在容器运行时重启,startServer()会找到已经在运行的服务器。在类的方法中使用客户端,例如createSession()。 -
从 Durable Object 的
fetch()处理程序提供 OpenCode Web UI:src/index.tsts export class MySandbox extends DurableObject<Env> { // ... async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const accept = request.headers.get("Accept") ?? ""; const page = accept.includes("text/html") || url.pathname === "/"; // The UI calls the server at the origin in ?url=. if ( request.method === "GET" && page && !url.searchParams.has("url") ) { url.searchParams.set("url", url.origin); return Response.redirect(url.toString(), 302); } await this.openCode(); // The container port accepts only http: URLs. url.protocol = "http:"; return this.container .getTcpPort(OPENCODE_PORT) .fetch(new Request(url, request)); } }这替换了
proxyToOpencode(),后者做相同的重定向。UI 从/加载其文件,因此在主机名的根路径提供它,并将每个路径转发到 Durable Object,例如用env.Sandbox.getByName("my-agent").fetch(request)。UI 可以在 sandbox 中运行命令,因此请在 Worker 中认证这些请求。 -
从 Worker 调用
createSession("Fix tests")。它返回新会话 ID,例如ses_f1822cedcffe1CpcMlwM9eQSGZ。在浏览器中打开 Worker URL。它会重定向到/?url=并加载 OpenCode。
在 0.12 中,@cloudflare/sandbox/openai 中的 Shell 和 Editor 在 sandbox 中运行 OpenAI 智能体的命令和文件编辑,而智能体在 Worker 中运行。在 1.0 中,你在 container.exec() 和 Files 上编写这两个类,并在 Durable Object 中运行智能体(容器所在之处)。
-
添加用
bash在/workspace中运行每条命令的Shell:src/index.tsts import { Files } from "@cloudflare/sandbox"; import { Agent, applyDiff, applyPatchTool, run, shellTool, type ApplyPatchOperation, type ApplyPatchResult, type Editor, type Shell, type ShellAction, type ShellResult, } from "@openai/agents"; class SandboxShell implements Shell { constructor(private readonly container: Container) {} async run(action: ShellAction): Promise<ShellResult> { const timeoutMs = action.timeoutMs ?? 60_000; const seconds = String(Math.ceil(timeoutMs / 1000)); const limit = ["timeout", "--kill-after=5", seconds]; const output: ShellResult["output"] = []; const decoder = new TextDecoder(); for (const command of action.commands) { const process = await this.container.exec( [...limit, "bash", "-c", command], { cwd: "/workspace", env: ENV }, ); const result = await process.output(); const timedOut = result.exitCode === 124 || result.exitCode === 137; output.push({ stdout: decoder.decode(result.stdout), stderr: decoder.decode(result.stderr), outcome: timedOut ? { type: "timeout" } : { type: "exit", exitCode: result.exitCode }, }); // 0.12 also skipped the rest after a timeout. if (timedOut) { break; } } return { output, providerData: { working_directory: "/workspace" }, }; } }timeout会停止命令及其启动的进程,这些进程必须仍位于其进程组中。命令会以代码124退出;如果timeout还发送了SIGKILL,则以137退出。0.12 在操作未设置timeoutMs时不会限制时间,而此类将限制为 60 秒。 -
添加用
Files应用智能体补丁的Editor:src/index.tsts type Operation<T> = Extract<ApplyPatchOperation, { type: T }>; class SandboxEditor implements Editor { constructor(private readonly files: Files) {} // Reads each path as relative to /workspace, as 0.12 did, and throws // when `..` leaves /workspace. private path(path: string): string { const root = "/workspace"; const inRoot = path === root || path.startsWith(`${root}/`); const relative = inRoot ? path.slice(root.length) : path; const segments: string[] = []; for (const segment of relative.split("/")) { if (segment === "..") { if (segments.pop() === undefined) { throw new Error(`Operation outside workspace: ${path}`); } } else if (segment !== "" && segment !== ".") { segments.push(segment); } } return [root, ...segments].join("/"); } async createFile( operation: Operation<"create_file">, ): Promise<ApplyPatchResult> { const path = this.path(operation.path); const parent = path.slice(0, path.lastIndexOf("/")); await this.files.mkdir(parent, { recursive: true }); const content = applyDiff("", operation.diff, "create"); await this.files.writeFile(path, content); return { status: "completed", output: `Created ${operation.path}`, }; } async updateFile( operation: Operation<"update_file">, ): Promise<ApplyPatchResult> { const path = this.path(operation.path); const file = await this.files.readFile(path); const current = await file.text(); const content = applyDiff(current, operation.diff); await this.files.writeFile(path, content); return { status: "completed", output: `Updated ${operation.path}`, }; } async deleteFile( operation: Operation<"delete_file">, ): Promise<ApplyPatchResult> { await this.files.remove(this.path(operation.path)); return { status: "completed", output: `Deleted ${operation.path}`, }; } }path()将/etc/passwd读取为/workspace/etc/passwd,并对../etc/passwd抛出,与 0.12 相同。它仅检查路径。/workspace中的符号链接仍可能指向其外部。更多信息请参阅 接受调用方提供的路径。失败的
Files调用会抛出SandboxFileError。两个 0.12 类都保留results列表,0.12Editor在再次抛出错误之前会向其中添加failed结果。如果你的代码读取该列表,请在每个方法中记录结果。 -
在类的方法中运行智能体:
src/index.ts (0.12)ts const sandbox = getSandbox(env.Sandbox, `session-${sessionId}`); const agent = new Agent({ name: "Sandbox agent", instructions: INSTRUCTIONS, tools: [ shellTool({ shell: new Shell(sandbox) }), applyPatchTool({ editor: new Editor(sandbox, "/workspace"), }), ], }); const result = await run(agent, input);src/index.ts (1.0)ts export class MySandbox extends DurableObject<Env> { // ... private readonly files = new Files(this.container); async runAgent(input: string): Promise<string | undefined> { await this.ensureRunning(); const shell = new SandboxShell(this.container); const editor = new SandboxEditor(this.files); const agent = new Agent({ name: "Sandbox agent", instructions: INSTRUCTIONS, tools: [shellTool({ shell }), applyPatchTool({ editor })], }); const result = await run(agent, input); return result.finalOutput; } }Worker 在
getByName()返回的 stub 上调用runAgent()。Agents SDK 从环境读取OPENAI_API_KEY,与在 Worker 中一样。如果你的类已有 更改文件调用 中的
files字段,保留两者之一。 -
不使用模型,通过直接调用检查这两个类。
shell.run({ commands: ["pwd", "sleep 10"], timeoutMs: 2000 })返回工作目录,然后是超时:{ "output": [ { "stdout": "/workspace\n", "stderr": "", "outcome": { "type": "exit", "exitCode": 0 } }, { "stdout": "", "stderr": "", "outcome": { "type": "timeout" } } ], "providerData": { "working_directory": "/workspace" } }editor.createFile({ type: "create_file", path: "notes/todo.md", diff: "+first\n+second\n" })返回{ "status": "completed", "output": "Created notes/todo.md" }。/workspace/notes/todo.md随后包含行first和second。
0.12 bridge(@cloudflare/sandbox/bridge 中的 bridge())为 Workers 外部的客户端通过 Sandbox 调用提供 HTTP API,并检查 SANDBOX_API_KEY bearer 令牌。1.0 没有 bridge。为客户端发出的每次调用添加 Worker 路由,并让路由调用 Durable Object 的方法。像 bridge 那样在 Worker 中检查令牌。
为 bridge API 编写的客户端(例如 Python OpenAI Agents SDK 中的 CloudflareSandboxClient)仅适用于 0.x bridge Worker。在客户端调用你自己的路由之前,将该 Worker 保持在 0.x。
在请求需要时启动每个 sandbox,并移除 WarmPool。该池会提前启动 0.x sandbox,因为 0.x 启动需要数秒。采用 Durable Object 调度策略后,独立基准测试中从创建 sandbox 到运行命令的中位时间为 648 毫秒,因此连接池只能节省很少时间。
-
从 Worker 主模块移除
WarmPool导出,并从 Wrangler 配置的durable_objects中移除其绑定。 -
以 Worker 添加该类的相同方式删除该类。如果 Worker 使用
migrations,添加一次迁移:{ "migrations": [ { "new_sqlite_classes": ["Sandbox"], "tag": "v1" }, { "new_sqlite_classes": ["WarmPool"], "tag": "v2" }, { "deleted_classes": ["WarmPool"], "tag": "v3" }, ], }[[migrations]] new_sqlite_classes = [ "Sandbox" ] tag = "v1" [[migrations]] new_sqlite_classes = [ "WarmPool" ] tag = "v2" [[migrations]] deleted_classes = [ "WarmPool" ] tag = "v3"如果 Worker 使用
exports,改为将该类标记为已删除:{ "exports": { "WarmPool": { "type": "durable-object", "state": "deleted", }, }, }[exports.WarmPool] type = "durable-object" state = "deleted" -
移除
WARM_POOL_TARGET、WARM_POOL_REFRESH_INTERVAL、WARM_POOL_MAX_INSTANCES和WARM_POOL_SCALE_BATCH_SIZE变量。bridge()从其scheduled()处理程序填充该池,因此如果没有其他内容使用它,也请移除 cron 触发器。
该池启动的 sandbox 是 0.12 sandbox,因此 规划迁移 像对待其他 sandbox 一样覆盖它们。
如果应用仍需要那最后几毫秒,为用户即将使用的 sandbox 调用 start(),例如在他们打开会话时。不要保留已启动 sandbox 的池。每个在等待时都会计费,你的代码必须将其分配给会话。