Skip to content

Sandbox

Last updated View as MarkdownAgent setup

Give an agent a Linux sandbox for shell commands, language runtimes, package installation, and project files that last across turns. The sandbox is a Container attached to the agent. The agent gives the model a tool that runs commands in the sandbox.

Use a sandbox when an agent needs to:

  • Run shell commands or scripts.
  • Build or test code with compilers and other Linux tools.
  • Work on the same files across several turns of a conversation.
  • Run untrusted or model-generated code away from the storage and credentials of the agent.

How it works

Attaching a container to the Durable Object of an agent gives the agent a sandbox through this.ctx.container. Agents with different names get separate sandboxes, and every turn sent to the same agent reaches the same sandbox.

The sandbox runs in its own virtual machine. It cannot read the storage, environment variables, or bindings of the agent. It reaches the Internet only when you allow it.

Basic pattern

The following Think agent gives the model one tool, run_command. The tool starts the sandbox on first use and runs each command with Bash.

import { Think } from "@cloudflare/think";
import { routeAgentRequest } from "agents";
import { tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";

const MAX_OUTPUT_CHARS = 10_000;
// Keep the container for 10 minutes after the agent becomes inactive.
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;

export class CodeAgent extends Think {
	constructor(ctx, env) {
		super(ctx, env);
		const container = ctx.container;
		// A restarted agent sets the timeout again.
		if (container?.running) {
			void ctx.blockConcurrencyWhile(() =>
				container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
			);
		}
	}

	getModel() {
		return createWorkersAI({ binding: this.env.AI })(
			"@cf/moonshotai/kimi-k2.6",
		);
	}

	getSystemPrompt() {
		return "You are a coding assistant. Use run_command to work in your Debian Linux sandbox.";
	}

	// Offer only the sandbox tool, so the model does not use the workspace file tools.
	beforeTurn() {
		return { activeTools: ["run_command"] };
	}

	getTools() {
		return {
			run_command: tool({
				description:
					"Run a shell command in this agent's Debian Linux sandbox. The sandbox cannot reach the Internet.",
				inputSchema: z.object({
					command: z.string().describe("The command to run with bash -lc"),
				}),
				execute: ({ command }) => this.runCommand(command),
			}),
		};
	}

	async runCommand(command) {
		const container = this.ctx.container;
		if (!container) {
			throw new Error("The container binding is not configured");
		}

		if (!container.running) {
			container.start({
				image: "cloudflare/debian-trixie",
				entrypoint: ["sleep", "infinity"],
				enableInternet: false,
			});
			await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
		}

		// `timeout` stops the command and every process it started.
		const process = await container.exec([
			"timeout",
			"--kill-after=5",
			"60",
			"bash",
			"-lc",
			command,
		]);
		const output = await process.output();
		const decoder = new TextDecoder();

		return {
			exitCode: output.exitCode,
			stdout: decoder.decode(output.stdout).slice(-MAX_OUTPUT_CHARS),
			stderr: decoder.decode(output.stderr).slice(-MAX_OUTPUT_CHARS),
		};
	}
}

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
};
import { Think } from "@cloudflare/think";
import { routeAgentRequest } from "agents";
import { tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";

const MAX_OUTPUT_CHARS = 10_000;
// Keep the container for 10 minutes after the agent becomes inactive.
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;

export class CodeAgent extends Think<Env> {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		const container = ctx.container;
		// A restarted agent sets the timeout again.
		if (container?.running) {
			void ctx.blockConcurrencyWhile(() =>
				container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
			);
		}
	}

	getModel() {
		return createWorkersAI({ binding: this.env.AI })(
			"@cf/moonshotai/kimi-k2.6",
		);
	}

	getSystemPrompt() {
		return "You are a coding assistant. Use run_command to work in your Debian Linux sandbox.";
	}

	// Offer only the sandbox tool, so the model does not use the workspace file tools.
	beforeTurn() {
		return { activeTools: ["run_command"] };
	}

	getTools() {
		return {
			run_command: tool({
				description:
					"Run a shell command in this agent's Debian Linux sandbox. The sandbox cannot reach the Internet.",
				inputSchema: z.object({
					command: z.string().describe("The command to run with bash -lc"),
				}),
				execute: ({ command }) => this.runCommand(command),
			}),
		};
	}

	private async runCommand(command: string) {
		const container = this.ctx.container;
		if (!container) {
			throw new Error("The container binding is not configured");
		}

		if (!container.running) {
			container.start({
				image: "cloudflare/debian-trixie",
				entrypoint: ["sleep", "infinity"],
				enableInternet: false,
			});
			await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
		}

		// `timeout` stops the command and every process it started.
		const process = await container.exec([
			"timeout",
			"--kill-after=5",
			"60",
			"bash",
			"-lc",
			command,
		]);
		const output = await process.output();
		const decoder = new TextDecoder();

		return {
			exitCode: output.exitCode,
			stdout: decoder.decode(output.stdout).slice(-MAX_OUTPUT_CHARS),
			stderr: decoder.decode(output.stderr).slice(-MAX_OUTPUT_CHARS),
		};
	}
}

export default {
	async fetch(request: Request, env: Env) {
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

start() boots the cloudflare/debian-trixie image, a Debian system with Node.js and npm. The default command of the image exits immediately, so the sleep infinity entrypoint keeps the container running between commands. enableInternet: false blocks outbound requests.

setInactivityTimeout() keeps the container running for 10 minutes after the agent becomes inactive, so the files remain for the next turn. The tool sets it after start(). A Durable Object that restarts starts without the timeout, so the constructor sets it again when the container is already running. For more information, refer to setInactivityTimeout.

timeout stops a command, and every process it started, after 60 seconds. For more information, refer to Stop the processes a command starts.

The tool returns only the last 10,000 characters of each output stream, so a noisy command does not fill the model context.

Think gives every agent built-in workspace tools, which store files in the agent. Commands in the sandbox cannot see those files. activeTools in beforeTurn() limits the model to run_command, so all of its file work happens in the sandbox.

To add Python, Git, compilers, or other tools, build your own image and pass it to start(). For more information, refer to images.

Configuration

Attach a container to the agent class in wrangler.jsonc. The class also needs a Durable Object binding and a SQLite migration, like any agent. Containers require the Workers Paid plan.

{
	"compatibility_flags": ["nodejs_compat"],
	"ai": {
		"binding": "AI"
	},
	"containers": [
		{
			"class_name": "CodeAgent",
			"scheduling_policy": "durable_object"
		}
	],
	"durable_objects": {
		"bindings": [
			{
				"class_name": "CodeAgent",
				"name": "CodeAgent"
			}
		]
	},
	"migrations": [
		{
			"tag": "v1",
			"new_sqlite_classes": ["CodeAgent"]
		}
	]
}
compatibility_flags = [ "nodejs_compat" ]

[ai]
binding = "AI"

[[containers]]
class_name = "CodeAgent"
scheduling_policy = "durable_object"

[[durable_objects.bindings]]
class_name = "CodeAgent"
name = "CodeAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = [ "CodeAgent" ]

scheduling_policy: "durable_object" sets the Durable Object scheduling policy, which lets the agent choose the image when it calls start().

Network access

The sandbox starts without Internet access, so commands such as npm install cannot download packages. Setting enableInternet: true allows every destination. Commands that the model writes can then send anything they can read to any server on the Internet.

To allow specific hostnames, or to add a credential to a request after it leaves the sandbox, route requests from the sandbox through an outbound handler in your Worker. For an example, refer to Call an authenticated API from a sandbox.

Sandbox and agent state

Files in the sandbox last while the container runs. When the container stops, the next command starts a new container from the image, and earlier files are gone. To keep files after the container stops, save a snapshot and start from it later. For more information, refer to Save and restore a sandbox with snapshots.

Use agent state for user-visible progress and small metadata, such as the ID of the latest snapshot.

For long-running sandbox work, pair the sandbox with durable execution with fibers or Workflows so the agent can recover or report progress if work outlives a single request.

Security considerations

The model reads everything that run_command returns. A file, package, or web page in the sandbox can print text written to steer the model, such as an instruction to run another command. Check tool results before the agent acts on them outside the sandbox, and require approval for tools with side effects.

Every user who talks to the same agent instance shares its sandbox. Give each user their own agent instance when their files must stay apart.

For more information, refer to Sandbox security.

Sandboxes

Run untrusted or generated code in Containers or Dynamic Workers.

Container API

Start, run commands in, and snapshot a container from a Durable Object.

Was this helpful?