- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Workspace
The workspace clones your repo and installs dependencies before the agent starts. The same setup runs on each provider and each agent.
defineWorkspace() describes the working tree once. Each harness adapter converts it to its own format, and it runs in any provider.
Define a workspace#
Pass a workspace to defineSandbox:
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
export const sandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
// Where the code comes from.
source: githubRepo({ repo: 'owner/repo', ref: 'main' }),
// Detected from the lockfile when you omit it.
packageManager: 'pnpm',
// Runs once, during bootstrap.
setup: ['corepack enable', 'pnpm install'],
// Named commands the agent can run.
scripts: { test: 'pnpm test', build: 'pnpm build' },
// Put into the sandbox env at create and resume. Never saved.
secrets: createSecrets({ XAI_API_KEY: process.env.XAI_API_KEY ?? '' }),
}),
})| Field | What it sets |
|---|---|
| source | Where the code comes from: a git repo, a local path, or nothing. |
| packageManager | npm, pnpm, yarn, bun, or auto. The default is auto. |
| setup | Commands that run once during bootstrap. |
| scripts | Named commands that the agent and the user can run by name. |
| secrets | Typed secret refs injected into the env. See Provisioning. |
defineWorkspace() also takes skills, plugins, and instructions. Those are on Provisioning.
Source#
source has five shapes:
import { defineWorkspace, githubRepo, gitSource } from '@tanstack/ai-sandbox'
// A GitHub repo (owner/repo or a full URL).
defineWorkspace({ source: githubRepo({ repo: 'owner/repo', ref: 'main' }) })
// Any git URL.
defineWorkspace({ source: gitSource({ url: 'https://git.example.com/owner/repo.git' }) })
// The same as gitSource, as a plain object.
defineWorkspace({ source: { type: 'git', url: 'https://github.com/owner/repo', ref: 'main' } })
// A directory on the host (for example, the local-process dev loop).
defineWorkspace({ source: { type: 'local', path: '/abs/path/to/repo' } })
// No code: the agent starts in an empty workspace.
defineWorkspace({ source: { type: 'none' } })githubRepo is a short form of gitSource:
- A short owner/repo becomes https://github.com/owner/repo.git.
- A full URL stays as it is.
Both make a { type: 'git' } source, so you can also write that object yourself.
Shallow clone by default#
githubRepo and gitSource make a shallow single-branch clone (--depth 1 --single-branch), so cold starts stay fast. Pass depth for more history:
import { defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'
// Shallow clone (depth 1). This is the default.
defineWorkspace({ source: githubRepo({ repo: 'owner/app' }) })
// The last 10 commits.
defineWorkspace({ source: githubRepo({ repo: 'owner/app', depth: 10 }) })
// Full history. No depth flag.
defineWorkspace({ source: githubRepo({ repo: 'owner/app', depth: 'full' }) })Package manager#
packageManager is 'npm', 'pnpm', 'yarn', 'bun', or 'auto'. The default 'auto' reads the lockfile after the source lands. To pin the choice, set a value.
Setup#
setup turns a fresh clone into an installed repo. It runs once, during bootstrap. The simplest form is an array. Each step runs after the one before it:
import { defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'
defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: ['corepack enable', 'pnpm install'],
})To run some steps at the same time, pass a callback:
import { defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'
defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: ({ serial, parallel }) => {
// These run in order. cwd and env carry over.
serial('corepack enable')
serial('pnpm install')
// These two start at the same time, with the same cwd and env.
parallel(['pnpm build', 'pnpm typecheck'])
// This waits for both parallel steps.
serial('echo bootstrap done')
},
})The callback runs on one persistent shell. A cd or an export in a serial step is visible to the next step.
Note
When the provider supports snapshots, bootstrap saves the result after setup, so later runs skip it. See Lifecycle & Snapshots.
Scripts#
scripts maps names to commands. The agent and the user can run a command by its name:
import { defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'
defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
scripts: {
test: 'pnpm test',
build: 'pnpm build',
typecheck: 'pnpm test:types',
},
})Named scripts also give Policy stable names to allow, ask about, or deny.
Advanced#
Installing an agent CLI#
Every setup step runs through sh -c. A step with a non-zero exit code fails the bootstrap.
When a step installs a harness CLI globally, run the CLI at the end of the step. The agent CLIs (@openai/codex, @anthropic-ai/claude-code, and more) ship their native binary as a platform-specific optional dependency. npm treats optional dependencies as best effort:
- A download of the binary fails.
- npm install -g still exits 0.
- The CLI fails later, during the run, with Missing optional dependency.
If the step runs the CLI, that failure happens at bootstrap, where you see it:
import { defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'
const install = 'npm install -g @openai/codex --include=optional && codex --version'
defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
// Retry once: this failure is usually a short download problem.
setup: [`${install} || { ${install} ; }`],
})Keep each command self-contained. Do not build a step by putting one command inside another (`${cmd} || sudo ${cmd}`). A command that starts with a subshell, such as the Grok CLI installer (curl ... || curl ...) | bash, is not a valid argument to another command. sh then fails to parse it with syntax error: unexpected "(" (exit 2) and runs nothing.
Non-root providers#
Some providers run as a non-root user:
- Installs inside the workspace, such as pnpm install, do not need sudo.
- System or global package installs on those providers need sudo -n in setup.
See Providers (Advanced) for which providers do this, and Policy so that you do not deny sudo there.