Skip to content

Sandbox

Run commands, language runtimes, tests, and background processes in a Linux container.

  • Libraries Runtime.Sandbox Runtime.SandboxBridge
  • npm @cloudflare/sandbox 0.12.9
  • Upstream/API Beta; requires Workers Paid

When to use a sandbox

Choose Sandbox when a task needs installed packages, native executables, or an existing project's build tools. The agent sends work to a sandbox identified by ID and consumes the results. Its own conversation and decision state can stay in a Durable Object.

Configure the Sandbox Durable Object binding and container image before running these examples. They target the repository's pinned SDK version; newer SDK previews can have different session and execution APIs. Cloudflare's Sandbox guide explains the execution model.

Code Runner

Visitors POST Python source. runCode executes it in the sandbox's code interpreter, and the Worker sends back stdout and stderr as JSON.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Sandbox = FSharp.CloudEdge.Runtime.Sandbox

type Env =
    abstract Sandbox: Workers.DurableObjectNamespace<obj>

[<ExportDefault>]
let worker: Workers.ExportedHandler<Env, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request env _ ->
            async {
                let sandbox: Sandbox.ISandbox = Sandbox.Exports.getSandbox<Sandbox.Sandbox<obj>>(env.Sandbox, "playground")
                let! code = request.text () |> Async.AwaitPromise
                let! run = sandbox.runCode code |> Async.AwaitPromise
                return Workers.Exports.Response.json {| output = run.logs.stdout; errors = run.logs.stderr |}
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )

Needs a Durable Object binding named Sandbox for the Sandbox class of @cloudflare/sandbox, and a container image built on cloudflare/sandbox. For Python, use the image tag that ends in -python. Worker Upload shows how to declare the binding.

Emitted JavaScript
import { awaitPromise, startAsPromise } from "./fable_modules/fable-library-js.5.13.0/Async.js";
import { singleton } from "./fable_modules/fable-library-js.5.13.0/AsyncBuilder.js";
import { getSandbox } from "@cloudflare/sandbox";

export const worker = {
    fetch: (request, env, _arg) => startAsPromise(singleton.Delay(() => {
        const sandbox = getSandbox(env.Sandbox, "playground");
        return singleton.Bind(awaitPromise(request.text()), (_arg_1) => singleton.Bind(awaitPromise(sandbox.runCode(_arg_1)), (_arg_2) => {
            let output;
            const run = _arg_2;
            return singleton.Return(globalThis.Response.json((output = run.logs.stdout, {
                errors: run.logs.stderr,
                output: output,
            })));
        }));
    })),
};

export default worker;

Workspace Files

The Worker saves a PUT body to a file in the sandbox and serves the file on GET. resolveWorkspacePath from Runtime.SandboxBridge returns None for any path outside /workspace. The Worker responds to ?path=../etc/passwd with a 400.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Sandbox = FSharp.CloudEdge.Runtime.Sandbox
module Bridge = FSharp.CloudEdge.Runtime.SandboxBridge.Bridge

type Env =
    abstract Sandbox: Workers.DurableObjectNamespace<obj>

[<ExportDefault>]
let worker: Workers.ExportedHandler<Env, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request env _ ->
            async {
                let url = Workers.Exports.URL(U2.Case1 request.url)
                let sandbox: Sandbox.ISandbox = Sandbox.Exports.getSandbox<Sandbox.Sandbox<obj>>(env.Sandbox, "files")
                match url.searchParams.get "path" |> Option.bind Bridge.Exports.resolveWorkspacePath with
                | None ->
                    return Workers.Exports.Response.Create("Paths must stay inside /workspace", Workers.ResponseInit.Create(status = 400.))
                | Some path when request.``method`` = "PUT" ->
                    let! body = request.text () |> Async.AwaitPromise
                    let! saved = sandbox.writeFile(path, body) |> Async.AwaitPromise
                    return Workers.Exports.Response.json {| saved = saved.path |}
                | Some path ->
                    let! file = sandbox.readFile path |> Async.AwaitPromise
                    return Workers.Exports.Response.Create(file.content)
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )

Notebook

Each notebook is a sandbox, and the Worker runs one Python cell per request. It reuses the notebook's code context, so a variable set in one cell is still defined in the next.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Sandbox = FSharp.CloudEdge.Runtime.Sandbox

type Env =
    abstract Sandbox: Workers.DurableObjectNamespace<obj>

[<ExportDefault>]
let worker: Workers.ExportedHandler<Env, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request env _ ->
            async {
                let url = Workers.Exports.URL(U2.Case1 request.url)
                let notebook = url.searchParams.get "notebook" |> Option.defaultValue "scratch"
                let sandbox: Sandbox.ISandbox = Sandbox.Exports.getSandbox<Sandbox.Sandbox<obj>>(env.Sandbox, notebook)
                let! contexts = sandbox.listCodeContexts () |> Async.AwaitPromise
                let! context =
                    match Array.tryHead contexts with
                    | Some context -> async { return context }
                    | None -> sandbox.createCodeContext () |> Async.AwaitPromise
                let! cell = request.text () |> Async.AwaitPromise
                let! result = sandbox.runCode(cell, Sandbox.RunCodeOptions.Create(context = context)) |> Async.AwaitPromise
                let values = result.results |> Array.choose (fun item -> item.text)
                return Workers.Exports.Response.json {| output = result.logs.stdout; values = values |}
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )

Test Runner

POST a repository URL, and the Worker clones it with gitCheckout into a new sandbox for that run. execStream returns the output of npm ci and npm test as Server-Sent Events. The Worker sends that stream as the response body, so the browser receives output while the tests run.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Sandbox = FSharp.CloudEdge.Runtime.Sandbox

type Env =
    abstract Sandbox: Workers.DurableObjectNamespace<obj>

[<ExportDefault>]
let worker: Workers.ExportedHandler<Env, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request env _ ->
            async {
                let! repository = request.text () |> Async.AwaitPromise
                let runId = string (System.Guid.NewGuid())
                let sandbox: Sandbox.ISandbox = Sandbox.Exports.getSandbox<Sandbox.Sandbox<obj>>(env.Sandbox, runId)
                let checkout = Sandbox.ISandbox.GitCheckout.Options.Create(targetDir = "/workspace/app", depth = 1.)
                let! _ = sandbox.gitCheckout(repository, checkout) |> Async.AwaitPromise
                let! log = sandbox.execStream "cd /workspace/app && npm ci && npm test" |> Async.AwaitPromise
                let headers = {| ``Content-Type`` = "text/event-stream"; ``Cache-Control`` = "no-cache" |}
                return Workers.Exports.Response.Create(log, Workers.ResponseInit.Create(headers = headers))
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )
Emitted JavaScript
import { awaitPromise, startAsPromise } from "./fable_modules/fable-library-js.5.13.0/Async.js";
import { singleton } from "./fable_modules/fable-library-js.5.13.0/AsyncBuilder.js";
import { newGuid } from "./fable_modules/fable-library-js.5.13.0/Guid.js";
import { getSandbox } from "@cloudflare/sandbox";

export const worker = {
    fetch: (request, env, _arg) => startAsPromise(singleton.Delay(() => singleton.Bind(awaitPromise(request.text()), (_arg_1) => {
        const runId = newGuid();
        const sandbox = getSandbox(env.Sandbox, runId);
        const checkout = {
            targetDir: "/workspace/app",
            depth: 1,
        };
        return singleton.Bind(awaitPromise(sandbox.gitCheckout(_arg_1, checkout)), (_arg_2) => singleton.Bind(awaitPromise(sandbox.execStream("cd /workspace/app && npm ci && npm test")), (_arg_3) => {
            const headers = {
                "Cache-Control": "no-cache",
                "Content-Type": "text/event-stream",
            };
            return singleton.Return(new globalThis.Response(_arg_3, ({
                headers: headers,
            })));
        }));
    }))),
};

export default worker;

This Worker searches the files sandbox that Workspace Files uses. shellQuote from Runtime.SandboxBridge quotes the search term. Whatever the visitor typed, the shell passes it to grep as one argument.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Sandbox = FSharp.CloudEdge.Runtime.Sandbox
module Bridge = FSharp.CloudEdge.Runtime.SandboxBridge.Bridge

type Env =
    abstract Sandbox: Workers.DurableObjectNamespace<obj>

[<ExportDefault>]
let worker: Workers.ExportedHandler<Env, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request env _ ->
            async {
                let url = Workers.Exports.URL(U2.Case1 request.url)
                let term = url.searchParams.get "q" |> Option.defaultValue ""
                let sandbox: Sandbox.ISandbox = Sandbox.Exports.getSandbox<Sandbox.Sandbox<obj>>(env.Sandbox, "files")
                let! found = sandbox.exec $"grep -rn -- {Bridge.Exports.shellQuote term} /workspace" |> Async.AwaitPromise
                return Workers.Exports.Response.Create(found.stdout)
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )
Emitted JavaScript
import { awaitPromise, startAsPromise } from "./fable_modules/fable-library-js.5.13.0/Async.js";
import { singleton } from "./fable_modules/fable-library-js.5.13.0/AsyncBuilder.js";
import { defaultArg } from "./fable_modules/fable-library-js.5.13.0/Option.js";
import { getSandbox } from "@cloudflare/sandbox";
import { concat } from "./fable_modules/fable-library-js.5.13.0/String.js";
import { shellQuote } from "@cloudflare/sandbox/bridge";

export const worker = {
    fetch: (request, env, _arg) => startAsPromise(singleton.Delay(() => {
        const url = new URL(request.url);
        const term = defaultArg(url.searchParams.get("q"), "");
        const sandbox = getSandbox(env.Sandbox, "files");
        return singleton.Bind(awaitPromise(sandbox.exec(concat("grep -rn -- ", shellQuote(term), " /workspace"))), (_arg_1) => singleton.Return(new globalThis.Response(_arg_1.stdout)));
    })),
};

export default worker;

Backup, restore, and version history

A backup lets you restore a prepared directory into a later sandbox session, including work that would otherwise require another checkout and dependency installation. The SDK's backup and restore API stores directory backups in R2. Keep the returned handle in durable application state and define when backups expire.

Git commits answer a different question: which source files changed, and which changes should be kept or merged? Use Artifacts for that repository history. A directory backup does not capture the agent's Durable Object state or turn running processes into resumable Git commits.

Session ownership

Derive sandbox IDs from your application's task or user identity. Reusing an ID intentionally reuses that environment; unrelated jobs should not accidentally share it. Decide which files to publish or back up before destroying an environment, and whether a background process should outlive the HTTP request that started it.

Testing and feedback

Useful test cases include command results and errors, stream handling, session ownership, file operations, and backup/restore behavior.

Verify bindings explains how to run checks and report an issue. Include a small reproduction and the package versions used; working examples are welcome too.

NuGet packages

Runtime.Sandbox 0.1.0, Runtime.SandboxBridge 0.1.0, Runtime.Workers 0.1.0.

See installation and release availability.

Edit this page