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.

  1. Add a Shell that runs each command with bash in /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

[![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NiIgaGVpZ2h0PSIzMCIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDY2IDMwIj48ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNzQ2XzIpIj48cGF0aCBmaWxsPSIjZmY5OTEwIiBkPSJNNTMuMTIgMTMuMTI1YzYuOTk3IDAgMTIuNjY4IDUuNzA4IDEyLjY2OCAxMi43NSAwIDEuMjU1LS4xOCAyLjQ2OC0uNTE2IDMuNjE0YS43Mi43MiAwIDAgMS0uNjk0LjUxMUg0Ni45NTNhLjM3NS4zNzUgMCAwIDEtLjM0OC0uNTFsLjMzNy0uODgyYzEuMDc2LTIuODMzIDQuMTQ2LTUuMTU0IDYuOTg3LTUuMjlsNS4zODQtLjI3N2EuNTQzLjU0MyAwIDAgMCAuNTE0LS41NDQuNTQzLjU0MyAwIDAgMC0uNTEtLjU0NGwtNS4wNTYtLjI5OWMtMi43MTgtLjE0LTQuMDQ2LTIuNTYtMy4zMTUtNS4wODFsLjg0Mi0yLjkwMmEuNzIuNzIgMCAwIDEgLjY0Ni0uNTI3cS4zNC0uMDIuNjg2LS4wMTkiLz48cGF0aCBmaWxsPSIjZmY1ZjA4IiBkPSJNMzUuNjA5IDBjNy4yOTUgMCAxMy40MjQgNS4wMjUgMTUuMTU3IDExLjgyNC4wNzMuMjg1LjA1LjU4Ni0uMDU0Ljg2MmwtMS4zODggMy42NzdjLTEuMDc2IDIuODMzLTMuODQyIDUuMjktNi45NyA1LjI5bC0yNC4xMDUuMjc4YS41MzMuNTMzIDAgMCAwLS41MjUuNTM1YzAgLjI5My4yMzQuNTMxLjUyNC41MzVsMjMuNzY1LjMxNmMyLjcyNCAwIDQuMDQ2IDIuNTYgMy4zMTYgNS4wODFsLS4zMDcgMS4wNjFhLjc1Ljc1IDAgMCAxLS43MTYuNTQxSC45NjRhLjcxLjcxIDAgMCAxLS43MTgtLjY0NiAxMSAxMSAwIDAgMS0uMDM0LS44NTRjMC01LjQ3NiA0LjE2Ni05Ljk3MyA5LjQ4Mi0xMC40NTZhNy4yIDcuMiAwIDAgMS0uMTY3LTEuNTQ0YzAtMy45MzUgMy4xNy03LjEyNSA3LjA4LTcuMTI1IDEuNTU1IDAgMi45OTQuNTA2IDQuMTYyIDEuMzYyQzIyLjg0OSA0LjQ5NyAyOC43MDcgMCAzNS42MDkgMCIvPjxwYXRoIGZpbGw9IiNmZjk5MTAiIGQ9Ik01My4xMiAxMy4xMjVjNi45OTcgMCAxMi42NjggNS43MDggMTIuNjY4IDEyLjc1IDAgMS4yNTUtLjE4IDIuNDY4LS41MTYgMy42MTRhLjcyLjcyIDAgMCAxLS42OTQuNTExSDQ2Ljk1M2EuMzc1LjM3NSAwIDAgMS0uMzQ4LS41MWwuMzM3LS44ODJjMS4wNzYtMi44MzMgNC4xNDYtNS4xNTQgNi45ODctNS4yOWw1LjM4NC0uMjc3YS41NDMuNTQzIDAgMCAwIC41MTQtLjU0NC41NDMuNTQzIDAgMCAwLS41MS0uNTQ0bC01LjA1Ni0uMjk5Yy0yLjcxOC0uMTQtNC4wNDYtMi41Ni0zLjMxNS01LjA4MWwuODQyLTIuOTAyYS43Mi43MiAwIDAgMSAuNjQ2LS41MjdxLjM0LS4wMi42ODYtLjAxOSIvPjxwYXRoIGZpbGw9IiNmZjVmMDgiIGQ9Ik0zNS42MDkgMGM3LjI5NSAwIDEzLjQyNCA1LjAyNSAxNS4xNTcgMTEuODI0LjA3My4yODUuMDUuNTg2LS4wNTQuODYybC0xLjM4OCAzLjY3N2MtMS4wNzYgMi44MzMtMy44NDIgNS4yOS02Ljk3IDUuMjlsLTI0LjEwNS4yNzhhLjUzMy41MzMgMCAwIDAtLjUyNS41MzVjMCAuMjkzLjIzNC41MzEuNTI0LjUzNWwyMy43NjUuMzE2YzIuNzI0IDAgNC4wNDYgMi41NiAzLjMxNiA1LjA4MWwtLjMwNyAxLjA2MWEuNzUuNzUgMCAwIDEtLjcxNi41NDFILjk2NGEuNzEuNzEgMCAwIDEtLjcxOC0uNjQ2IDExIDExIDAgMCAxLS4wMzQtLjg1NGMwLTUuNDc2IDQuMTY2LTkuOTczIDkuNDgyLTEwLjQ1NmE3LjIgNy4yIDAgMCAxLS4xNjctMS41NDRjMC0zLjkzNSAzLjE3LTcuMTI1IDcuMDgtNy4xMjUgMS41NTUgMCAyLjk5NC41MDYgNC4xNjIgMS4zNjJDMjIuODQ5IDQuNDk3IDI4LjcwNyAwIDM1LjYwOSAwIi8+PC9nPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDBfNzQ2XzIiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDBoNjZ2MzBIMHoiLz48L2NsaXBQYXRoPjwvZGVmcz48L3N2Zz4=)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


Skip to content

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.