Move other 0.x features·续
Give OpenAI agents a shell and an editor
In 0.12, Shell and Editor from @cloudflare/sandbox/openai ran the commands and file edits of an OpenAI agent in a sandbox, while the agent ran in your Worker. In 1.0, you write both classes over container.exec() and Files, and run the agent in your Durable Object, where the container is.
- Add a
Shellthat runs each command withbashin/workspace:
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` [stops the command and the processes it started](https://developers.cloudflare.com/containers/guides/execute-commands/#stop-the-processes-a-command-starts) that stay in its process group. The command exits with code `124`, or `137` if `timeout` also sends `SIGKILL`. 0.12 set no time limit when the action had no `timeoutMs`, and this class sets 60 seconds.
2. Add an `Editor` that applies the patches from the agent with `Files`:
*src/index.tsts*
```ts
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()` reads `/etc/passwd` as `/workspace/etc/passwd`, and throws for `../etc/passwd`, as 0.12 did. It checks the path only. A symbolic link in `/workspace` can still point outside it. For more information, refer to [Accept paths from callers](https://developers.cloudflare.com/sandbox/files/manage-files/#accept-paths-from-callers).
A failed `Files` call throws `SandboxFileError`. Both 0.12 classes kept a `results` list, and the 0.12 `Editor` added a `failed` result to it before it threw the error again. If your code read the list, record results in each method.
3. Run the agent in a method of your class:
*src/index.ts (0.12)ts*
```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*
```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;
}
}
```
Your Worker calls `runAgent()` on the stub that `getByName()` returns. The Agents SDK reads `OPENAI_API_KEY` from the environment, as it did in your Worker.
If your class already has the `files` field from [Change file calls](https://developers.cloudflare.com/sandbox/sdk/migrate/files/), keep one of the two.
4. Check both classes without a model by calling them directly. `shell.run({ commands: ["pwd", "sleep 10"], timeoutMs: 2000 })` returns the working directory, then a timeout:
```json
{
"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" })` returns `{ "status": "completed", "output": "Created notes/todo.md" }`. `/workspace/notes/todo.md` then contains the lines `first` and `second`.
## Replace the bridge
The 0.12 bridge, `bridge()` from `@cloudflare/sandbox/bridge`, served an HTTP API over `Sandbox` calls for clients outside Workers, and checked a `SANDBOX_API_KEY` bearer token. 1.0 has no bridge. Add a Worker route for each call that your client makes, and have the route call a method of your Durable Object. Check the token in your Worker, as the bridge did.
Clients written for the bridge API, such as `CloudflareSandboxClient` in the OpenAI Agents SDK for Python, work only with a 0.x bridge Worker. Keep that Worker on 0.x until your client calls your own routes.
## Remove WarmPool
Start each sandbox when a request needs it, and remove `WarmPool`. The pool kept 0.x sandboxes started ahead of time, because a 0.x start took seconds. With the Durable Object scheduling policy, the median time from creating a sandbox to running a command in it was 648 milliseconds in an independent benchmark, so a pool saves little time.
1. Remove the `WarmPool` export from the main module of your Worker, and its binding from `durable_objects` in the Wrangler configuration.
2. Delete the class in the same way your Worker added it. If your Worker uses `migrations`, add a migration:
```jsonc
{
"migrations": [
{ "new_sqlite_classes": ["Sandbox"], "tag": "v1" },
{ "new_sqlite_classes": ["WarmPool"], "tag": "v2" },
{ "deleted_classes": ["WarmPool"], "tag": "v3" },
],
}
```
```toml
[[migrations]]
new_sqlite_classes = [ "Sandbox" ]
tag = "v1"
[[migrations]]
new_sqlite_classes = [ "WarmPool" ]
tag = "v2"
[[migrations]]
deleted_classes = [ "WarmPool" ]
tag = "v3"
```
If your Worker uses `exports`, mark the class as deleted instead:
```jsonc
{
"exports": {
"WarmPool": {
"type": "durable-object",
"state": "deleted",
},
},
}
```
```toml
[exports.WarmPool]
type = "durable-object"
state = "deleted"
```
3. Remove the `WARM_POOL_TARGET`, `WARM_POOL_REFRESH_INTERVAL`, `WARM_POOL_MAX_INSTANCES`, and `WARM_POOL_SCALE_BATCH_SIZE` variables. `bridge()` filled the pool from its `scheduled()` handler, so remove the cron trigger too if nothing else uses it.
The sandboxes that the pool started are 0.12 sandboxes, so [Plan the move](https://developers.cloudflare.com/sandbox/sdk/migrate/plan-the-move/) covers them like any other.
If your application still needs those last milliseconds, call `start()` for the sandbox that a user is about to use, for example when they open a session. Do not keep a pool of started sandboxes. Each one is billed while it waits, and your code must assign it to a session.
## Related resources
- [Docker-in-Docker in the Containers FAQ](https://developers.cloudflare.com/containers/faq/#can-i-run-docker-inside-a-container-docker-in-docker)
- [Durable Object Container API](https://developers.cloudflare.com/containers/api/durable-object-container/)
- [Move outbound rules](https://developers.cloudflare.com/sandbox/sdk/migrate/outbound-traffic/)
- [Run OpenCode in a sandbox](https://developers.cloudflare.com/sandbox/coding-agents/opencode/)
- [`Files` API](https://developers.cloudflare.com/sandbox/reference/files/)
Was this helpful?
YesNo
## On this page
[Docs](https://developers.cloudflare.com/)
```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/sdk/migrate/other-features/#page","headline":"Move other 0.x features","description":"Move @cloudflare/sandbox 0.12 Docker-in-Docker images, binding handlers, and the opencode, openai, and bridge subpaths to 1.0, and remove WarmPool.","url":"https://developers.cloudflare.com/sandbox/sdk/migrate/other-features/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/sdk/migrate/other-features/og.png?v=f9210298c4783c03","dateModified":"2026-10-22","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Replace @cloudflare/sandbox 0.12 outbound rules with a WorkerEntrypoint that your Durable Object registers, and replace gitCheckout() with git clone. title: Move outbound rules image: https://developers.cloudflare.com/sandbox/sdk/migrate/outbound-traffic/og.png?v=a5df19060d257d2f
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt
Use this file to discover all available pages before exploring further.
