containerSandbox makes a SandboxHandle over ctx.container of your own Durable Object. Your Durable Object starts the container and keeps egress, secrets, and instance limits. The provider only runs commands and moves files.
To let the package make the Worker, the Durable Objects, and the container for you, use Cloudflare (Edge) instead.
Your Durable Object keeps these controls:
npm i @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-harness @tanstack/ai-openai @cloudflare/sandboxpnpm add @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-harness @tanstack/ai-openai @cloudflare/sandboxyarn add @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-harness @tanstack/ai-openai @cloudflare/sandboxbun add @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-harness @tanstack/ai-openai @cloudflare/sandbox{
"name": "agent-worker",
"main": "src/worker.ts",
"compatibility_date": "2026-05-01",
"compatibility_flags": ["nodejs_compat"],
"containers": [{ "class_name": "AgentContainer", "image": "./Dockerfile", "max_instances": 5 }],
"durable_objects": {
"bindings": [{ "name": "AGENT_CONTAINER", "class_name": "AgentContainer" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["AgentContainer"] }]
}// ignore: needs the Cloudflare Workers runtime types (cloudflare:workers, Env, R2Bucket), which the docs check does not load.
// src/agent-container.ts
import { Files } from '@cloudflare/sandbox'
import { containerSandbox } from '@tanstack/ai-sandbox-cloudflare/container'
import { DurableObject } from 'cloudflare:workers'
export class AgentContainer extends DurableObject<Env> {
async sandbox() {
const container = this.ctx.container
if (!container) throw new Error('The container binding is not configured')
// Your rules. This container has no internet.
if (!container.running) container.start({ enableInternet: false })
const provider = containerSandbox({
container,
files: new Files(container),
})
return provider.create({ id: this.ctx.id.toString() })
}
async hello() {
const handle = await this.sandbox()
const result = await handle.process.exec('uname -a')
return result.stdout
}
}// ignore: needs the Cloudflare Workers runtime types (cloudflare:workers, Env, R2Bucket), which the docs check does not load.
// src/worker.ts
export { AgentContainer } from './agent-container'
export default {
async fetch(_request, env) {
const agent = env.AGENT_CONTAINER.getByName('user-123')
return new Response(await agent.hello())
},
} satisfies ExportedHandler<Env>Pass the handle to sandboxWorkspaceBackend. The coding tools of the harness then run in your container, behind your egress rules.
// src/coder.ts
import { defineHarness } from '@tanstack/ai-harness'
import { workspaceTools } from '@tanstack/ai-harness/plugins/coding'
import { openaiText } from '@tanstack/ai-openai'
import { sandboxWorkspaceBackend } from '@tanstack/ai-sandbox/harness'
import type { SandboxHandle } from '@tanstack/ai-sandbox'
export function coder(handle: SandboxHandle) {
return defineHarness({
name: 'acme/coder',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [
workspaceTools({
workspace: {
bind: async () => ({
...sandboxWorkspaceBackend(handle),
root: '/workspace',
}),
},
}),
],
})
}Leave owned off. Then a closed session does not destroy the sandbox, and your Durable Object decides when the container stops. To run the harness, see Run the coding tools for many threads.
Pass a DirectoryBackup as backup. Then handle.snapshot(label) saves the workspace to an R2 bucket. provider.restoreSnapshot({ snapshotId }) writes it back into the current container.
{
"r2_buckets": [{ "binding": "BACKUPS", "bucket_name": "agent-backups" }]
}// ignore: needs the Cloudflare Workers runtime types (cloudflare:workers, Env, R2Bucket), which the docs check does not load.
// src/worker.ts
export { DirectoryBackupGateway } from '@cloudflare/sandbox'
export { AgentContainer } from './agent-container'// ignore: needs the Cloudflare Workers runtime types (cloudflare:workers, Env, R2Bucket), which the docs check does not load.
// src/agent-container.ts
import { DirectoryBackup, Files } from '@cloudflare/sandbox'
import { containerSandbox } from '@tanstack/ai-sandbox-cloudflare/container'
import { DurableObject } from 'cloudflare:workers'
export class AgentContainer extends DurableObject<Env> {
provider() {
const container = this.ctx.container
if (!container) throw new Error('The container binding is not configured')
if (!container.running) container.start({ enableInternet: false })
const backup = new DirectoryBackup(
container,
this.ctx.exports.DirectoryBackupGateway,
{ binding: 'BACKUPS' },
)
return containerSandbox({ container, files: new Files(container), backup })
}
async save(label: string) {
const handle = await this.provider().create({ id: this.ctx.id.toString() })
if (!handle.snapshot) throw new Error('The provider has no backup')
const ref = await handle.snapshot(label)
await this.ctx.storage.put('latestSnapshotId', ref.id)
return ref.id
}
async restore() {
const snapshotId = await this.ctx.storage.get('latestSnapshotId')
if (typeof snapshotId !== 'string') return null
const provider = this.provider()
if (!provider.restoreSnapshot) throw new Error('The provider has no backup')
return provider.restoreSnapshot({ snapshotId })
}
}The snapshot id is the backup record as a JSON string. Store it where you keep state, for example in Durable Object storage or in latestSnapshotId of your SandboxInstanceStore. restoreSnapshot throws when the id is not a backup record.
Take the snapshot at a time with no commands that write to the workspace. A file that changes during the backup is saved as it is at that moment.
A DirectoryBackup works only on Cloudflare. To keep checkpoints in your own store, and restore them on any provider, see Checkpoint at Any Time.
Call save('after-tests'), stop the container, then call restore(). The files of the workspace are back.