Skip to content

5. Cloudflare Sandbox

Checkpoint 5 of 68 minutes

Cloudflare Sandbox

Attach one Linux container per conversation, write itinerary.md, and read it again on the next turn.

useSandbox(cloudflareSandbox(getSandbox(env.Sandbox, id))) gives the model read, write, edit, bash, grep, and glob. The conversation ID keys one Sandbox Durable Object and container per conversation. Conversation history and usePersistentState remain durable in the agent object. Container disk is an ephemeral workspace: it can remain available across turns and deployments, but your application must not depend on files surviving container replacement.

The npm package and Docker image tags must match exactly:

If a dev server is running, press Ctrl+C in its terminal before changing dependencies. Installing while Vite watches node_modules can leave a stale optimized-dependency cache.

Terminal window
npm install --save-exact @cloudflare/sandbox@0.12.10

This adds the following dependency to package.json and regenerates package-lock.json automatically:

"@cloudflare/sandbox": "0.12.10"

Keep the other dependencies unchanged. If you use Local dev, leave Vite stopped until all checkpoint files are in place, then restart it from that verification tab.

Create Dockerfile in the project root:

Dockerfile+3−0

New file from cp/4-subagent → cp/5-sandbox

Dockerfile
===================================================================
--- /dev/null cp/4-subagent
+++ b/Dockerfile cp/5-sandbox
@@ -0,0 +1,3 @@
+# Container image for the agent sandbox (cp5).
+# The tag MUST match the @cloudflare/sandbox version in package.json.
+FROM docker.io/cloudflare/sandbox:0.12.10

Create src/cloudflare.ts:

src/cloudflare.ts+3−0

New file from cp/4-subagent → cp/5-sandbox

src/cloudflare.ts
===================================================================
--- /dev/null cp/4-subagent
+++ b/src/cloudflare.ts cp/5-sandbox
@@ -0,0 +1,3 @@
+// Extra Worker-entry exports. Flue re-exports everything from this file.
+// The Sandbox Durable Object class backs the agent's container sandbox (cp5).
+export { Sandbox } from '@cloudflare/sandbox';

Update src/env.d.ts with the typed Sandbox binding:

src/env.d.ts+5−0

Changes from cp/4-subagent → cp/5-sandbox

src/env.d.ts
===================================================================
--- a/src/env.d.ts cp/4-subagent
+++ b/src/env.d.ts cp/5-sandbox
@@ -3,3 +3,8 @@
const content: string;
export default content;
}
+
+// cp5: the Sandbox binding from wrangler.jsonc, typed for `import { env } from 'cloudflare:workers'`.
+declare module 'cloudflare:workers' {
+ export const env: { Sandbox: Parameters<typeof import('@cloudflare/sandbox').getSandbox>[0] };
+}

3. Configure the Sandbox object and container

Section titled “3. Configure the Sandbox object and container”

Update wrangler.jsonc to append the Sandbox migration and configure its Durable Object binding and container image:

wrangler.jsonc+7−2

Changes from cp/4-subagent → cp/5-sandbox

wrangler.jsonc
===================================================================
--- a/wrangler.jsonc cp/4-subagent
+++ b/wrangler.jsonc cp/5-sandbox
@@ -20,6 +20,11 @@
{
"tag": "v1",
"new_sqlite_classes": ["FlueFieldTripAgent"]
- }
- ]
+ },
+ { "tag": "v2", "new_sqlite_classes": ["Sandbox"] }
+ ],
+
+ // cp5: container sandbox (class exported from src/cloudflare.ts, image from ./Dockerfile).
+ "durable_objects": { "bindings": [{ "name": "Sandbox", "class_name": "Sandbox" }] },
+ "containers": [{ "class_name": "Sandbox", "image": "./Dockerfile", "max_instances": 10 }]
}

The v2 migration must be appended; never rewrite the deployed v1 migration.

Update src/agents/field-trip.ts to accept the conversation ID, attach its Sandbox, and add the itinerary file instructions:

src/agents/field-trip.ts+10−3

Changes from cp/4-subagent → cp/5-sandbox

src/agents/field-trip.ts
===================================================================
--- a/src/agents/field-trip.ts cp/4-subagent
+++ b/src/agents/field-trip.ts cp/5-sandbox
@@ -1,6 +1,9 @@
'use agent';
-import { useModel, usePersistentState, useSubagent, useTool } from '@flue/runtime';
+import { type AgentProps, useModel, usePersistentState, useSandbox, useSubagent, useTool } from '@flue/runtime';
+import { cloudflareSandbox } from '@flue/runtime/cloudflare';
+import { getSandbox } from '@cloudflare/sandbox';
+import { env } from 'cloudflare:workers';
import * as v from 'valibot';
import { geocodeCity, getForecast } from '../tools/weather.ts';
import { findNearbyPlaces } from '../tools/wikipedia.ts';
@@ -16,7 +19,7 @@
interests?: string[];
};
-export function FieldTrip() {
+export function FieldTrip({ id }: AgentProps) {
useModel('cloudflare/@cf/google/gemma-4-26b-a4b-it');
// Durable, per-conversation state (stored in this conversation's Durable Object).
@@ -56,6 +59,9 @@
// fresh context with its own tools, and only its final answer comes back.
useSubagent(venueScout);
+ // A Linux container per conversation (adds read/write/edit/bash/grep/glob tools).
+ useSandbox(cloudflareSandbox(getSandbox(env.Sandbox, id)));
+
// The agent re-renders before every model call, so these instructions
// always reflect the latest saved brief.
const hasBrief = Object.keys(brief).length > 0;
@@ -71,7 +77,8 @@
b. Pick the 3 places that best fit the brief (skip stations, offices, hospitals, embassies, companies, events).
c. Call \`task\` ONCE with agent \`venue-scout\` for all 3 places. The scout cannot see this conversation, so the prompt must be a complete briefing: the exact place titles, the city, the headcount, and the interests.
d. Combine the results into a short plan, keeping the links. If you know the forecast, suggest outdoor places for dry days and indoor ones for rainy days.
-5. Keep replies short: at most 120 words unless the user asks for more detail.
+5. For an itinerary: \`write\` it to itinerary.md (one section per day: places, timing, weather), then \`read\` it to check. Do not repeat the file in your reply (the user sees the read result); reply in one sentence.
+6. Keep replies short: at most 120 words unless the user asks for more detail.
Today is ${today}.

Everything under src/tools/, src/subagents/venue-scout.ts, and src/app.ts is unchanged from cp/4-subagent.

Reuse one conversation ID so all turns reach the same container. Trip dates are filled automatically in the commands and their copy buttons.

Keep Docker running: deployment builds and uploads the image from Dockerfile.

Terminal window
npm run deploy
sleep 5

Wait for deployment to finish. The first container deployment can need several minutes to provision even after the Worker URL is printed. Then run:

Terminal window
npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp5-live "Offsite in Lisbon from <START> to <END> for 14 people, budget 400 EUR each. We like food, history and the outdoors."
TIMEOUT_S=200 npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp5-live "Write the itinerary to itinerary.md and show it to me."
npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp5-live "Show me itinerary.md again."

The itinerary turn must show write, then read. The last turn must read the same file again. Judge the tool calls rather than the exact model wording. This tests image upload and Sandbox execution on Cloudflare Containers.

Optional depth check: repeat delegation, then prove isolation:

Terminal window
TIMEOUT_S=200 npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp5-live "Suggest 3 places for our offsite."
TIMEOUT_S=200 npm run smoke -- https://field-trip-agent.<subdomain>.workers.dev cp5-live-other "Use bash to run: ls -la /workspace. Then tell me whether a trip brief is saved."

cp5-live-other must not inherit cp5-live’s itinerary or trip brief. A redeploy may reuse an existing container, so it does not deterministically prove ephemeral disk.

Save your workers.dev subdomain to open this test ↗https://field-trip-agent.<subdomain>.workers.dev/?id=cp5-live

This opens the same conversation as the smoke test. If you repeat this checkpoint, before copying its commands.

Verification gate

Prove it works

  • The agent uses write and read for itinerary.md.
  • A later message in the same conversation reads the file again.
  • Optional depth: a different conversation ID has a separate workspace and persistent state.
  • You can explain why container files and persistent state have different lifetimes.
Optional mini quizCheck the Sandbox boundary3 questions instant feedback
1What selects the conversation container?
2What must match @cloudflare/sandbox@0.12.10?
3What survives a container replacement?
Recovery lane

Get back on track in under a minute

If a dev server is running, stop it first. Uncommitted work is stashed and the current commit gets a backup branch before anything moves.

Option 01

Restart this exercise

Return to cp/4-subagent, rebuild this exercise, then use its Live deploy verification tab. Stop any running dev server with Ctrl+C before switching.

Terminal
git stash push --include-untracked -m "workshop recovery"
git fetch origin
git branch -f workshop-backup HEAD
git switch --no-track -C workshop origin/cp/4-subagent
npm ci
Option 02

Catch up with the room

Jump to cp/5-sandbox, then use this checkpoint's Live deploy verification tab. Stop any running dev server with Ctrl+C before switching.

Terminal
git stash push --include-untracked -m "workshop recovery"
git fetch origin
git branch -f workshop-backup HEAD
git switch --no-track -C workshop origin/cp/5-sandbox
npm ci

Next: Trace the agent →