- MCP Server Tools
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type Generation
- MCP Client Input
- MCP Server Tools: Advanced
- MCP Events
- MCP AppsUpdated
- Serve Tools over HTTP
- Serve Resources and Prompts
- Ask for Input
- Draft Text from a Tool
- MCP Server Tasks
- MCP Server Auth
- MCP Server Sessions
- MCP Server on stdio
Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- Providers
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Checkpoint at Any TimeNew
- Store CheckpointsNew
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Your Own Cloudflare Container
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:
- Start, stop, and destroy. The provider never calls them.
- Egress. The provider sets no enableInternet, no outbound intercept, and no outbound handler.
- Secrets and idle timeouts. The provider sets none of them.
- The image. It needs no special server, only sandbox-shim (step 2).
Run commands in your container#
- Install the packages. @cloudflare/sandbox must be version 1 or later.
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- Declare the container and your Durable Object in wrangler.jsonc. The image must have sandbox-shim at /usr/local/bin/sandbox-shim, because Files runs it. The cloudflare/sandbox image has it. Match the image tag to your @cloudflare/sandbox version. See the Cloudflare requirements.
{
"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"] }]
}- Start the container in your Durable Object, then make the handle. The container must run before you use the handle.
// 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
}
}- Export the Durable Object from your Worker, deploy, and call hello(). It returns the kernel line of your container.
// 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>Give the container to a coding agent#
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.
What the provider does not do#
- Ports. handle.ports.connect throws UnsupportedCapabilityError. Send requests to the container from your own Worker.
- Container variables. handle.env.set keeps the variables in the handle. Each command of that handle gets them. The container and a new handle do not.
- Destroy. destroy only calls your onDestroy. The container keeps running.
- A different workspace folder. The default is /workspace. Pass workspaceRoot to use another folder.
Save the workspace to R2#
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.
- Bind an R2 bucket in wrangler.jsonc:
{
"r2_buckets": [{ "binding": "BACKUPS", "bucket_name": "agent-backups" }]
}- Export DirectoryBackupGateway from your Worker entry. The container sends its backup traffic to this gateway.
// 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'- Make the provider with backup. If your Durable Object calls interceptAllOutboundHttp(), call backup.intercept() first. Do it right after the start, before the catch-all.
// 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.