@tanstack/ai-sandbox
Version:
Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.
221 lines (170 loc) • 9.78 kB
Markdown
# @tanstack/ai-sandbox
Provider-agnostic sandbox layer for [TanStack AI](https://tanstack.com/ai). Run coding-agent harness adapters (Grok Build, Claude Code, Codex, OpenCode, Gemini CLI) **inside** an isolated environment with a real filesystem, shell, and cloned repo — and stream their work back through `chat()`.
```typescript
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
const sandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/repo', ref: 'main' }),
packageManager: 'pnpm',
setup: ['corepack enable', 'pnpm install'],
scripts: { test: 'pnpm test', build: 'pnpm build' },
secrets: createSecrets({
XAI_API_KEY: process.env.XAI_API_KEY ?? '',
}),
}),
lifecycle: { reuse: 'thread', snapshot: 'after-setup' },
})
const stream = chat({
threadId: 'my-thread',
adapter: grokBuildText('grok-build'),
messages: [{ role: 'user', content: 'Fix the failing test.' }],
middleware: [withSandbox(sandbox)],
})
```
## Installation
```bash
npm install @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-grok-build
```
Pick a **provider** package for where the sandbox runs:
| Package | Use when |
| ------------------------------------ | -------------------------------------- |
| `@tanstack/ai-sandbox-local-process` | Dev loop on your host (no isolation) |
| `@tanstack/ai-sandbox-docker` | Isolated containers, snapshots, resume |
| `@tanstack/ai-sandbox-cloudflare` | Cloudflare Workers + Containers |
| `@tanstack/ai-sandbox-vercel` | Vercel Sandbox |
| `@tanstack/ai-sandbox-daytona` | Daytona cloud sandboxes, snapshots |
| `@tanstack/ai-sandbox-upstash-box` | Upstash Box cloud sandboxes, snapshots |
| `@tanstack/ai-sandbox-sprites` | Sprites stateful sandboxes |
**Harness adapters** are separate packages. The default path is **Grok Build** (`@tanstack/ai-grok-build`); others include `@tanstack/ai-claude-code`, `@tanstack/ai-codex`, and `@tanstack/ai-opencode`. All require `withSandbox(...)` middleware — `chat()` fails fast without it.
## Three moving parts
| Part | What it is | How you configure it |
| ------------------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
| **Provider** | Isolation primitive — host, container, cloud VM | `dockerSandbox()`, `localProcessSandbox()`, … |
| **Workspace** | What the agent boots into — repo, setup, secrets, skills | `defineWorkspace({ … })` |
| **Harness adapter** | Which agent CLI runs and how output is translated | `grokBuildText()` (default), `claudeCodeText()`, `codexText()`, … |
`defineSandbox()` binds provider + workspace (+ optional policy, lifecycle, hooks). `withSandbox(definition)` is the `chat()` middleware that creates or resumes the sandbox for each run.
## Core APIs
### Workspace
Describe the working tree once, portably:
```typescript
defineWorkspace({
source: githubRepo({ repo: 'owner/app' }), // shallow clone by default
setup: ({ serial, parallel }) => {
serial('pnpm install')
parallel(['pnpm build', 'pnpm typecheck'])
},
scripts: { test: 'pnpm test' }, // surfaced in AGENTS.md; policy aliases
instructions: 'Run tests before proposing changes.',
skills: [
gitSkill({ repo: 'owner/skills', secret: secrets.GH }),
mcpSkill('api', {
url: 'https://mcp.example.com',
headers: { Authorization: bearer(secrets.TOKEN) },
}),
fileSkill({ path: '.hints.md', content: '# Hints\nPrefer pnpm.' }),
],
plugins: ['@anthropic/plugin-foo'], // Claude Code only; other harnesses warn+skip
secrets: createSecrets({ GH: process.env.GH_TOKEN ?? '' }),
})
```
Skills and plugins are **projected** into each harness's native format at run time (`.grok/config.toml`, `.mcp.json`, `.codex/config.toml`, `opencode.json`, …). Bootstrap writes `AGENTS.md` and clones `gitSkill` repos; harness adapters handle the rest.
### Policy
Guard what the agent may run:
```typescript
const policy = defineSandboxPolicy({
default: 'allow',
})
defineSandbox({ id: 'agent', provider, workspace, policy })
```
Headless Grok Build and Codex stay on auto-approve when `default` is `'allow'` and there is no `ask` list. Isolation is the outer sandbox (Docker, Daytona, and so on). Use Claude Code when you need command-level deny.
Claude Code can use an interactive policy:
```typescript
defineSandboxPolicy({
commands: {
allow: ['pnpm test', 'git diff'],
ask: ['pnpm install'],
deny: ['sudo *', 'rm -rf *'],
},
capabilities: { fileWrite: 'allow', network: 'ask' },
default: 'ask',
})
```
Precedence is `deny` > `ask` > `allow`. Each harness adapter maps policy onto its native permission system (coarse flags for Grok Build/Codex; full interactive `approval-requested` on Claude Code). Provider-specific privilege and network rules live in the [providers](../../docs/sandbox/providers.md) guide.
### Lifecycle
```typescript
lifecycle: {
reuse: 'thread', // resume one sandbox per threadId
snapshot: 'after-setup', // skip bootstrap on subsequent runs (when provider supports it)
keepAlive: '30m',
destroyOnComplete: false,
}
```
### Portable snapshots
Use portable snapshots when a later run must rebuild completed files after the
provider sandbox is gone. Create one snapshots object, pass it to
`withPersistence` and `withSandbox`, and put `withPersistence` first.
```typescript
import { withPersistence } from '@tanstack/ai-persistence'
import { memorySandboxSnapshots, withSandbox } from '@tanstack/ai-sandbox'
const snapshots = await memorySandboxSnapshots({ sandbox, instances })
const middleware = [
withPersistence(snapshots.persistence),
withSandbox(sandbox, { instances, snapshots }),
]
```
A successful terminal run saves regular files, empty directories, saved
conversation data, and thread artifacts. Restore runs only in a new private
sandbox. A live resumed sandbox keeps its current files. Read
[Keep Files After Reload](https://tanstack.com/ai/latest/docs/sandbox/portable-snapshots-configure)
for the full server setup.
### Secrets
Use `createSecrets()` so values stay behind opaque `SecretRef` tokens. They are never written to snapshots, the sandbox store, or event logs. The sandbox layer resolves them onto the live handle at create, resume, and snapshot restore:
```typescript
const secrets = createSecrets({ XAI_API_KEY: process.env.XAI_API_KEY ?? '' })
// secrets.XAI_API_KEY is a ref, not the string
```
### Host tool bridge
`chat()` server tools can be bridged into the in-sandbox agent over MCP. The agent calls `mcp__tanstack__<tool>`; execution runs back on the host where your closures, DB, and secrets live.
## Run flow
```text
chat({ adapter: grokBuildText(), middleware: [withSandbox(sandbox)] })
│
├─ withSandbox.setup → resume → restore snapshot → create + bootstrap
├─ adapter.chatStream → spawn `grok` (or other harness CLI) inside sandbox; stream AG-UI chunks
└─ withSandbox.onFinish → snapshot / destroy per lifecycle
```
## Subpath exports
| Import | Purpose |
| ---------------------------- | ----------------------------------------------------- |
| `@tanstack/ai-sandbox` | Core sandbox APIs |
| `@tanstack/ai-sandbox/ngrok` | Optional ngrok tunnel helper for remote tool bridging |
## Documentation
Full guides on [tanstack.com/ai](https://tanstack.com/ai/latest/docs/sandbox/overview):
- [Quick Start](https://tanstack.com/ai/latest/docs/sandbox/quick-start) — Grok Build in Docker
- [Providers](https://tanstack.com/ai/latest/docs/sandbox/providers)
- [Workspace](https://tanstack.com/ai/latest/docs/sandbox/workspace)
- [Provisioning](https://tanstack.com/ai/latest/docs/sandbox/provisioning) (skills, MCP, plugins)
- [Policy](https://tanstack.com/ai/latest/docs/sandbox/policy)
- [Tools](https://tanstack.com/ai/latest/docs/sandbox/tools) (host tool bridge)
- [Lifecycle & snapshots](https://tanstack.com/ai/latest/docs/sandbox/lifecycle)
- [Portable sandbox snapshots](https://tanstack.com/ai/latest/docs/sandbox/portable-snapshots)
- [Pick which files to keep](https://tanstack.com/ai/latest/docs/sandbox/portable-snapshots-files)
## Examples
| Example | What it demonstrates |
| ----------------------------- | --------------------------------------------------------------------- |
| `examples/sandbox-web` | Build-and-preview with harness × provider matrix (Grok default in UI) |
| `examples/sandbox-cloudflare` | Edge deploy with live preview URL |
## When to use a sandbox
Use a sandbox when the agent needs to **act on a real codebase** — run commands, edit files, clone repos, start dev servers. For read-only Q&A over code you already have in context, a normal `chat()` with server tools is enough.
Persistence (durable `SandboxStore` / `LockStore`, event-log replay) is out of scope for v1 but every seam is persistence-ready via optional capabilities.