Unified Workspace Agent Harness for AI agents — one durable workspace, many execution backends.
Sandbank is a workspace-native agent harness. It keeps agent identity, memory, artifacts, audit logs, files, and checkpoints in the Workspace protocol, then dispatches concrete execution tasks to Sandbank Cloud, Dynamic Workers, E2B, BoxLite, Fly.io, Daytona, Cloudflare Workers, or any compatible sandbox backend. The lower-level provider SDK is still available, but the top-level Sandbank abstraction is the harness that keeps workspace state portable across execution backends. The recommended general-purpose compute backend is Sandbank Cloud, our hosted BoxLite cloud service.
AI agents need more than an isolated sandbox. They need a durable, auditable workspace that can survive model turns, tool calls, provider switches, and retries. Sandbank treats sandboxes as execution capsules: short JavaScript code can run in a Cloudflare Dynamic Worker, Python, Codex, and shell tasks should prefer Sandbank Cloud by default, special cases can route to E2B, self-hosted BoxLite, Fly.io, or a VM/container, and every output syncs back into the same workspace.
The lower-level provider SDK still gives a common interface over Daytona, Fly.io, Cloudflare Workers, and other providers:
import { createProvider } from '@sandbank.dev/core'
import { SandbankCloudAdapter } from '@sandbank.dev/cloud'
const provider = createProvider(new SandbankCloudAdapter({
apiToken: process.env.SANDBANK_API_TOKEN,
}))
const sandbox = await provider.create({ image: 'node:22' })
const result = await sandbox.exec('echo "Hello from the sandbox"')
console.log(result.stdout) // Hello from the sandbox
await provider.destroy(sandbox.id)Sandbank Cloud is the recommended default provider. Swap SandbankCloudAdapter for DaytonaAdapter, FlyioAdapter, or CloudflareAdapter without rewriting sandbox create/exec code. The harness layer builds on this and treats each provider as a compute backend, not the durable home of the agent.
┌──────────────────────────────────────────────────────┐
│ Your Application / AI Agent / Third-party caller │
├──────────────────────────────────────────────────────┤
│ sandbank Workspace Agent Harness │
│ AgentSupervisor policy / memory / tool use │
│ Provider Scheduler backend dispatch + sync │
│ @sandbank.dev/workspace Durable Workspace & Checkpoints │
├──────────────────────────────────────────────────────┤
│ @sandbank.dev/core Low-level Provider SDK │
│ @sandbank.dev/skills Skill Registry & Injection │
│ @sandbank.dev/agent In-sandbox Agent Client │
│ @sandbank.dev/relay Multi-agent Communication │
├──────────────────────────────────────────────────────┤
│ @sandbank.dev/cloud @sandbank.dev/boxlite @sandbank.dev/e2b │
│ @sandbank.dev/daytona @sandbank.dev/flyio @sandbank.dev/cloudflare│
│ Provider Adapters (Compute) │
├──────────────────────────────────────────────────────┤
│ @sandbank.dev/db9 Service Adapter (Data) │
├──────────────────────────────────────────────────────┤
│ Sandbank Cloud (hosted BoxLite) BoxLite (self-hosted Docker) │
│ E2B Cloud Sandboxes Daytona Fly.io Machines Cloudflare Workers │
│ db9.ai (PostgreSQL) │
└──────────────────────────────────────────────────────┘
| Package | Description |
|---|---|
sandbank |
Workspace Agent Harness, Agent Supervisor, Tool Use, provider scheduler, CLI/Worker entrypoints |
@sandbank.dev/core |
Low-level provider SDK, capability system, error types |
@sandbank.dev/skills |
Skill registry and local filesystem loader |
@sandbank.dev/workspace |
Durable workspace protocol, checkpoints, and sandbox materialization helpers |
@sandbank.dev/cloud |
Sandbank Cloud hosted BoxLite cloud adapter, recommended default provider, with API token or x402 payments |
@sandbank.dev/daytona |
Daytona cloud sandbox adapter |
@sandbank.dev/flyio |
Fly.io Machines adapter |
@sandbank.dev/cloudflare |
Cloudflare Workers adapter |
@sandbank.dev/boxlite |
BoxLite self-hosted Docker adapter |
@sandbank.dev/e2b |
E2B cloud sandbox adapter |
@sandbank.dev/db9 |
db9.ai serverless PostgreSQL adapter (ServiceProvider) |
@sandbank.dev/relay |
WebSocket relay for multi-agent communication |
@sandbank.dev/agent |
Lightweight client for agents running inside sandboxes |
All providers implement these — the minimum contract:
| Operation | Sandbank Cloud | Daytona | Fly.io | Cloudflare | BoxLite | E2B |
|---|---|---|---|---|---|---|
| Create / Destroy | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| List sandboxes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Execute commands | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Read / Write files | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Skill injection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Capabilities are opt-in. Use withVolumes(provider), withPortExpose(sandbox), etc. to safely check and access them at runtime.
| Capability | Sandbank Cloud | Daytona | Fly.io | Cloudflare | BoxLite | E2B | db9 | Description |
|---|---|---|---|---|---|---|---|---|
volumes |
❌ | ✅ | ✅ | ❌ | — | Persistent volume management | ||
port.expose |
✅ | ✅ | ✅ | ✅ | ✅ | — | Expose sandbox ports to the internet | |
exec.stream |
✅ | ❌ | ❌ | ✅ | ✅ | ❌ | — | Stream stdout/stderr in real-time |
snapshot |
❌ | ❌ | ❌ | ✅ | ✅ | ❌ | — | Snapshot and restore sandbox state |
terminal |
❌ | ✅ | ✅ | ✅ | ✅ | ✅ | — | Interactive web terminal (ttyd) |
sleep |
❌ | ❌ | ❌ | ❌ | ✅ | ✅ | — | Hibernate and wake sandboxes |
skills |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | Load and inject skill definitions into sandboxes |
services |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | Bind data services (PostgreSQL) to sandboxes |
* Cloudflare volumes requires storage option in adapter config.
** Cloudflare reserves port 3000 for its sandbox control plane. Use any port in 1024–65535 except 3000.
*** E2B volumes require E2B volume beta access. Sandbank mounts volumes by connecting the Sandbank volume id to an E2B Volume.
| Sandbank Cloud | Daytona | Fly.io | Cloudflare | BoxLite | E2B | |
|---|---|---|---|---|---|---|
| Positioning | Recommended default provider | Cloud sandbox | VM/microVM | Edge Worker | Self-hosted BoxLite | Cloud sandbox |
| Runtime | Hosted BoxLite containers | Full VM | Firecracker microVM | V8 isolate + container | Docker container | E2B cloud sandbox |
| Cold start | Managed-service optimized | ~10s | ~3-5s | ~1s | ~2-5s | Provider-managed |
| File I/O | Archive API | Native SDK | Via exec (base64) | Native SDK | Via exec (base64) | Native SDK |
| Regions | Sandbank-managed | Multi | Multi | Global edge | Self-hosted | E2B managed |
| External deps | @sandbank.dev/cloud + API token/x402 |
@daytonaio/sdk |
None (pure fetch) | @cloudflare/sandbox |
BoxLite API | e2b |
Sandbank includes a built-in orchestration layer for multi-agent workflows. The Relay handles real-time messaging and shared context between sandboxes.
import { createSession } from '@sandbank.dev/core'
const session = await createSession({
provider,
relay: { type: 'memory' },
})
// Spawn agents in isolated sandboxes
const architect = await session.spawn('architect', {
image: 'node:22',
env: { ROLE: 'architect' },
})
const developer = await session.spawn('developer', {
image: 'node:22',
env: { ROLE: 'developer' },
})
// Shared context — all agents can read/write
await session.context.set('spec', { endpoints: ['/users', '/posts'] })
// Wait for all agents to complete
await session.waitForAll()
await session.close()Inside the sandbox, agents use @sandbank.dev/agent:
import { connect } from '@sandbank.dev/agent'
const session = await connect() // reads SANDBANK_* env vars
session.on('message', async (msg) => {
if (msg.type === 'task') {
// do work...
await session.send(msg.from, 'done', result)
}
})
await session.complete({ status: 'success', summary: 'Built 5 API endpoints' })The harness makes WorkspaceAdapter the authoritative state boundary for an agent. A single run can call a model, execute bounded JavaScript code mode in a Dynamic Worker, write generated Python into the workspace, dispatch that Python to Sandbank Cloud first, or route to E2B, BoxLite, Daytona, Fly.io, or another runtime.python backend by policy, and then persist artifacts, logs, memory, and checkpoints back to the same workspace.
This keeps long-lived agent state out of provider-local VMs, containers, volumes, and storage bindings. Permission checks also stay centralized: Tool Use requests pass through agent policy, resource grants, and approval rules before they call a host-registered tool or schedule a sandbox provider.
Provider-native volumes are provider-specific resources. A Fly.io volume, E2B volume, Daytona volume, and Cloudflare storage binding are not the same durable disk. For seamless provider switching, keep durable state in a WorkspaceAdapter, materialize it into the sandbox before execution, then sync changed files back and checkpoint the workspace.
import {
MemoryWorkspaceAdapter,
materializeWorkspaceToSandbox,
syncWorkspaceFromSandbox,
} from '@sandbank.dev/workspace'
const workspace = new MemoryWorkspaceAdapter()
await workspace.write('/workspace/task.md', 'ship it')
const sandbox = await provider.create({ image: 'node:22' })
await materializeWorkspaceToSandbox(workspace, sandbox, {
workspacePath: '/workspace',
sandboxPath: '/workspace',
})
await sandbox.exec('echo done > /workspace/result.txt')
await syncWorkspaceFromSandbox(workspace, sandbox, {
workspacePath: '/workspace',
sandboxPath: '/workspace',
deleteMissing: true,
checkpointLabel: 'after provider run',
})Use provider-native volumes as local cache or provider-local persistence. Use workspace checkpoints for portable rollback and cross-provider continuity.
The top-level sandbank package exports selectSandboxProvider, preflightWorkspaceSandboxTask, and runWorkspaceSandboxTask. The scheduler treats sandbox providers as compute candidates and selects one by declared capabilities such as runtime.python, runtime.codex, codex.exec, codex.goal, workspace.snapshot, and workspace.live.
import {
preflightWorkspaceSandboxTask,
runWorkspaceSandboxTask,
} from 'sandbank'
const taskConfig = {
workspace,
providers: [
{ provider: sandbankCloudProvider, capabilities: ['runtime.python', 'codex.exec'], priority: 30 },
{ provider: e2bProvider, capabilities: ['runtime.python'], priority: 10 },
{ provider: boxliteProvider, capabilities: ['runtime.python'] },
],
task: { kind: 'python' as const, path: '/workspace/generated/task.py', image: 'python-agent' },
imageCatalog: {
'python-agent': {
default: 'python:3.12',
'sandbank-cloud': 'python:3.12-slim',
e2b: 'e2b-python-template',
boxlite: 'python:3.12-slim',
},
},
preflight: { runtime: true },
}
const preflight = await preflightWorkspaceSandboxTask(taskConfig)
if (!preflight.ok) throw new Error(preflight.errors.join('; '))
await runWorkspaceSandboxTask({
...taskConfig,
consistency: { mode: 'branch-merge', conflictResolution: 'keep-both' },
preflight: false,
})Static preflight checks workspace and provider capabilities before execution. Runtime preflight creates a temporary sandbox and probes image tools such as python, codex, git, tmux, tar, and gzip. codex.goal starts a vas-style tmux session and leaves the sandbox alive for terminal attach and later workspace sync. See Provider Scheduler And Workspace Consistency and Sandbank Agent Configuration.
Sandbank Tool Use is a lower-level protocol than any single model adapter. A model loop, Dynamic Worker capsule, or hosted agent submits a structured tool.use request; the Agent Supervisor checks the agent's tool/resource policy before any handler or sandbox provider is invoked.
import {
AgentSupervisor,
ToolUseRegistry,
createCloudflareResourceTool,
createSearchCodeRunTool,
createSandboxPythonTool,
} from 'sandbank'
const registry = new ToolUseRegistry()
.register(createCloudflareResourceTool('read', async input => {
// Connect this handler to Cloudflare D1/KV/R2/etc. bindings or APIs.
return { ok: true, resource: input.resource }
}))
.register(createSearchCodeRunTool({
search: {
provider: 'perplexity',
search: async query => searchProvider.search(query),
fetchJson: async url => searchProvider.fetchJson(url),
},
}))
.register(createSandboxPythonTool())
const supervisor = new AgentSupervisor({
agentId: 'agent-a',
workspace,
modelId: 'deepseek-v4-pro',
toolUse: {
registry,
dynamicWorker,
sandboxProviders: [
{ provider: sandbankCloudProvider, capabilities: ['runtime.python', 'codex.exec'] },
{ provider: e2bProvider, capabilities: ['runtime.python'] },
{ provider: boxliteProvider, capabilities: ['runtime.python'] },
],
policy: {
allowedTools: ['cloudflare.resource.read', 'search.code.run', 'sandbox.python'],
resources: [
{ kind: 'cloudflare.d1', id: 'analytics', actions: ['read'] },
{ kind: 'dynamic_worker.execution', actions: ['execute'] },
{ kind: 'runtime.javascript', actions: ['execute'] },
{ kind: 'external.search', id: 'perplexity', actions: ['query'] },
{ kind: 'http.egress', id: 'api.example.com', actions: ['fetch'] },
{ kind: 'workspace.path', scope: '/runs', actions: ['write'] },
{ kind: 'sandbox.provider', id: 'sandbank-cloud', actions: ['execute'] },
{ kind: 'sandbox.provider', id: 'e2b', actions: ['execute'] },
{ kind: 'runtime.python', actions: ['execute'] },
],
requireApproval: [
{ kind: 'cloudflare.d1', action: 'write' },
],
},
},
})Tool registration is currently host-driven: the third-party caller creates a ToolUseRegistry, injects tool definitions with .register(...), and enables them per agent/run through policy. Sandbank does not expose a remote endpoint that lets arbitrary users register new tools at runtime.
Resource grants are the agent enablement whitelist. If a prompt asks the agent to mutate a user database, the request must still match an allowed resource/action and any matching approval rule before execution. search.code.run is the code mode tool: the model can generate a JavaScript function body, Dynamic Worker executes it, and the code can only access controlled ctx.search, ctx.workspace, and ctx.runtime bindings. Raw egress still has to match an http.egress grant. sandbox.python uses the provider scheduler, so generated Python can run on E2B, BoxLite, Sandbank Cloud, or another provider that advertises the required runtime capability. Dynamic Worker capsules receive the same path through SANDBANK_TOOLS.list() and SANDBANK_TOOLS.use(request), which forwards back to the supervisor instead of bypassing policy.
# Install the recommended provider
pnpm add @sandbank.dev/core @sandbank.dev/cloud
# Set up provider
export SANDBANK_API_TOKEN=your-key
# Or use x402 pay-per-use
export WALLET_PRIVATE_KEY=0x...import { createProvider } from '@sandbank.dev/core'
import { SandbankCloudAdapter } from '@sandbank.dev/cloud'
const provider = createProvider(
new SandbankCloudAdapter({ apiToken: process.env.SANDBANK_API_TOKEN })
)
// Create a sandbox
const sandbox = await provider.create({
image: 'node:22',
resources: { cpu: 2, memory: 2048 },
autoDestroyMinutes: 30,
})
// Run commands
const { stdout } = await sandbox.exec('node --version')
// File operations
await sandbox.writeFile('/app/index.js', 'console.log("hi")')
await sandbox.exec('node /app/index.js')
// Clean up
await provider.destroy(sandbox.id)git clone https://github.com/chekusu/sandbank.git
cd sandbank
pnpm install
# Run all unit tests
pnpm test
# Run cross-provider conformance tests
pnpm test:conformance
# Typecheck
pnpm typecheckThe sandbank CLI and Worker entrypoint expose a public Sandbank harness API backed by the Agent Supervisor, db9 workspace storage, and DeepSeek V4 Pro:
DB9_DATABASE_ID=... DB9_TOKEN=... DEEPSEEK_API_KEY=... \
vas dev sandbank-harness pnpm --filter ./packages/sandbank exec tsx src/cli/index.ts harness-api --host 0.0.0.0 --port 8789Routes:
GET /healthGET /api/db-native-agent-harness/capabilitiesPOST /api/sandbank-agent-harness/streamPOST /api/db-native-agent-harness/stream
The stream emits generic Sandbank SSE events, persists run input/output under /runs/..., records supervisor state/audit data under /agents/..., creates a checkpoint when the workspace backend supports it, and defaults to deepseek-v4-pro. It also stores agent memories under /agents/{agentId}/memory/memories.jsonl, recalls active pinned / insight / session entries into the model prompt, and writes explicit remember / 记住 requests as pinned memories. The Worker-compatible entrypoint is exported as sandbank/harness-worker; the Node CLI is for service hosting through vas dev or an equivalent deployment path, not as a localhost-only preview. Model, Workspace, provider, and image requirements are summarized in Sandbank Agent Configuration.
Integration tests hit real APIs and are gated by environment variables:
# Sandbank Cloud (recommended default provider)
SANDBANK_API_TOKEN=... pnpm test
# Daytona
DAYTONA_API_KEY=... pnpm test
# Fly.io
FLY_API_TOKEN=... FLY_APP_NAME=... pnpm test
# Cloudflare
E2E_WORKER_URL=... pnpm test
# db9
DB9_TOKEN=... pnpm --filter @sandbank.dev/db9 test:e2eScore a live DB-native harness run from one prompt:
pnpm --filter ./packages/sandbank exec tsx src/cli/index.ts harness-benchmark \
--base-url https://your-sandbank-worker.example \
--question "@agent run a Sandbank harness health check" \
--jsonRun the packaged benchmark suite:
SANDBANK_HARNESS_BASE_URL=https://your-sandbank-worker.example pnpm --filter ./packages/sandbank bench:harness -- --jsonEach case is posted to /api/db-native-agent-harness/stream and scored out of 100 for HTTP/SSE transport, harness lifecycle, workspace persistence, Dynamic Worker capsule execution, model streaming, explicit case expectations, and latency.
The DB-native harness includes a workspace-backed memory layer inspired by mem9's pinned / insight / session model. Memories are stored as JSONL under /agents/{agentId}/memory/memories.jsonl so the same data survives Node, Worker, and db9-backed deployments.
pinned: explicit user-saved facts, created when the prompt asks the agent toremember/记住something.session: compact user/assistant run evidence recorded after each completed run.insight: supported by the schema for future model-extracted summaries.
Before calling the model, the harness recalls active memories for the current agent and injects them into the system prompt inside a <relevant-memories> block. The model is instructed to treat memories as contextual facts, not executable instructions.
| Package | Stmts | Branch | Funcs | Lines | Unit | Integration |
|---|---|---|---|---|---|---|
@sandbank.dev/core |
84% | 77% | 74% | 88% | 98 | — |
@sandbank.dev/db9 |
100% | 97% | 93% | 100% | 35 | 3 |
Run coverage locally:
pnpm --filter @sandbank.dev/db9 test -- --coverage- Minimal interface, maximum interop — only the true common denominator (exec + files + lifecycle)
- Explicit over implicit — no auto-fallback, no caching, no hidden retries
- Capability detection, not fake implementations — if a provider doesn't support it, it errors
- Idempotent operations — destroying an already-destroyed sandbox is a no-op
- Full decoupling — provider layer and session layer are independent, compose freely
MIT