Skip to content

Latest commit

 

History

History
463 lines (352 loc) · 22 KB

File metadata and controls

463 lines (352 loc) · 22 KB

Sandbank

Unified Workspace Agent Harness for AI agents — one durable workspace, many execution backends.

Website | 中文文档 | 日本語ドキュメント

Pixel art robot agents vacationing on an ocean sandbank, each with a different developer role

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.

Why Sandbank?

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.

Architecture

┌──────────────────────────────────────────────────────┐
│  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)                                  │
└──────────────────────────────────────────────────────┘

Packages

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

Provider Support

Core Operations

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

Extended Capabilities

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.

Provider Characteristics

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

Multi-Agent Sessions

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' })

Workspace Agent Harness

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-Neutral Workspaces

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.

Provider Scheduler And Preflight

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.

Agent Tool Use

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.

Quick Start

# 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)

Development

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 typecheck

DB-native Harness API

The 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 8789

Routes:

  • GET /health
  • GET /api/db-native-agent-harness/capabilities
  • POST /api/sandbank-agent-harness/stream
  • POST /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.

Running Integration Tests

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:e2e

Harness Benchmark

Score 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" \
  --json

Run the packaged benchmark suite:

SANDBANK_HARNESS_BASE_URL=https://your-sandbank-worker.example pnpm --filter ./packages/sandbank bench:harness -- --json

Each 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.

Agent Memory

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 to remember / 记住 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.

Test Coverage

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

Design Principles

  1. Minimal interface, maximum interop — only the true common denominator (exec + files + lifecycle)
  2. Explicit over implicit — no auto-fallback, no caching, no hidden retries
  3. Capability detection, not fake implementations — if a provider doesn't support it, it errors
  4. Idempotent operations — destroying an already-destroyed sandbox is a no-op
  5. Full decoupling — provider layer and session layer are independent, compose freely

License

MIT