Skip to content

dot-do/sandbox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@dotdo/sandbox

E2E Tests

Cloudflare Container sandboxes for coding agents and databases.

Installation

npm install @dotdo/sandbox
# or
pnpm add @dotdo/sandbox

Features

  • Coding Agent Sandboxes: Run Claude Code, OpenCode, and Codex in isolated containers
  • Database Sandboxes: PostgreSQL, SQLite, and DuckDB containers
  • RPC Client: WebSocket-based client for browser and Node.js
  • Durable Objects: State management via Cloudflare Durable Objects

Quick Start

Coding Agent in a Worker

import { getSandbox } from '@cloudflare/sandbox'
import { createClaudeCodeSandbox } from '@dotdo/sandbox/agents/claude-code'

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Create sandbox from container binding
    const sandbox = getSandbox(env.CLAUDE_CODE_SANDBOX, 'my-session')

    const claude = createClaudeCodeSandbox({
      sandbox,
      apiKey: env.ANTHROPIC_API_KEY,
      permissionMode: 'acceptEdits',
    })

    await claude.init()

    const result = await claude.run('Create a REST API with Express')

    await claude.close()

    return Response.json({ output: result.result })
  },
}

Database Container in a Worker

import { createPostgresContainer } from '@dotdo/sandbox/databases/postgres'

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const pg = createPostgresContainer(env.POSTGRES_CONTAINER)

    await pg.connect()

    const users = await pg.query<{ id: number; name: string }>(
      'SELECT * FROM users WHERE active = $1',
      [true]
    )

    await pg.close()

    return Response.json({ users })
  },
}

RPC Client (Browser/Node.js)

import { createSandboxClient } from '@dotdo/sandbox/client'

const client = createSandboxClient({
  auth: 'your-api-key',
})

// Create and use a sandbox
const instance = await client.agents.create({ type: 'claude-code' })
const result = await client.agents.run(instance.id, 'Hello world')

console.log(result.stdout)

Examples

See the examples/ directory for complete Worker examples:

API Reference

Coding Agent Sandboxes

ClaudeCodeSandbox

Run Claude Code in a container for autonomous coding tasks.

import { getSandbox } from '@cloudflare/sandbox'
import { ClaudeCodeSandbox, createClaudeCodeSandbox } from '@dotdo/sandbox/agents/claude-code'

// Using factory function
const claude = createClaudeCodeSandbox({
  sandbox: getSandbox(env.CLAUDE_CODE_SANDBOX, sessionId),
  apiKey: env.ANTHROPIC_API_KEY,
  permissionMode: 'acceptEdits',
  model: 'claude-sonnet-4-20250514',
  workingDirectory: '/workspace',
})

// Or using class directly
const claude = new ClaudeCodeSandbox({
  sandbox,
  apiKey: env.ANTHROPIC_API_KEY,
  permissionMode: 'bypassPermissions',
  maxThinkingTokens: 10000,
})

// Initialize (starts container, verifies claude CLI)
await claude.init()

// Run a prompt
const result = await claude.run('Build a todo app with React', {
  timeout: 600000, // 10 minutes
})

// Access results
console.log(result.success)      // boolean
console.log(result.result)       // parsed JSON output
console.log(result.stdout)       // raw stdout
console.log(result.sessionId)    // for conversation continuity

// Execute shell commands directly
const execResult = await claude.exec('npm install')

// Clone and work on a repository
const repoResult = await claude.runOnRepo(
  'https://github.com/user/repo',
  'Add unit tests for the auth module',
  { branch: 'main', shallow: true }
)

// Multi-turn conversation
const results = await claude.runConversation([
  'Create a new Express server',
  'Add authentication middleware',
  'Write tests for the auth routes',
])

// File operations
await claude.writeFile('/workspace/config.json', JSON.stringify({ port: 3000 }))
const content = await claude.readFile('/workspace/package.json')
const files = await claude.listFiles('/workspace')

// Clean up
await claude.close()

Configuration Options:

Option Type Default Description
sandbox Sandbox required Sandbox instance from @cloudflare/sandbox
apiKey string - Anthropic API key
permissionMode 'default' | 'acceptEdits' | 'bypassPermissions' 'acceptEdits' Permission handling mode
model string 'claude-sonnet-4-20250514' Model to use
maxThinkingTokens number 0 Max tokens for extended thinking
workingDirectory string '/workspace' Working directory in container
commandTimeout number 300000 Command timeout in ms
systemPrompt string - Custom system prompt
allowedTools string[] - Tools to allow
disallowedTools string[] - Tools to disallow

OpenCodeSandbox

Run OpenCode with multiple provider support.

import { createOpenCodeSandbox } from '@dotdo/sandbox/agents/opencode'

const opencode = createOpenCodeSandbox({
  sandbox: getSandbox(env.OPENCODE_SANDBOX, sessionId),
  provider: 'anthropic', // or 'openai', 'ollama'
  apiKey: env.ANTHROPIC_API_KEY,
})

await opencode.init()
const result = await opencode.run('Refactor this codebase to use TypeScript')
await opencode.close()

CodexSandbox

Run OpenAI Codex CLI for code generation.

import { createCodexSandbox } from '@dotdo/sandbox/agents/codex'

const codex = createCodexSandbox({
  sandbox: getSandbox(env.CODEX_SANDBOX, sessionId),
  apiKey: env.OPENAI_API_KEY,
  approvalMode: 'full-auto',
})

await codex.init()
const result = await codex.run('Generate a Python script to process CSV files')
await codex.close()

Factory Function

Create any agent sandbox by type:

import { createAgentSandbox, isAgentType } from '@dotdo/sandbox/agents'

const type = 'claude-code' // or 'opencode', 'codex'

if (isAgentType(type)) {
  const agent = createAgentSandbox(type, {
    sandbox: getSandbox(env.SANDBOX, sessionId),
    apiKey: env.API_KEY,
  })
}

Database Containers

PostgresContainer

Full-featured PostgreSQL in a container.

import { PostgresContainer, createPostgresContainer } from '@dotdo/sandbox/databases/postgres'

const pg = createPostgresContainer(env.POSTGRES_CONTAINER, {
  database: 'myapp',
  username: 'postgres',
  password: 'secret',
})

// Connect (waits for container startup)
await pg.connect()

// Check startup time
console.log(`Cold start: ${pg.getStartupTime()}ms`)

// Execute DDL
await pg.execute(`
  CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    name TEXT
  )
`)

// Insert with parameters (SQL injection safe)
await pg.execute(
  'INSERT INTO users (email, name) VALUES ($1, $2)',
  ['alice@example.com', 'Alice']
)

// Query with typed results
interface User {
  id: number
  email: string
  name: string
}
const users = await pg.query<User>('SELECT * FROM users WHERE id = $1', [1])

// Query with full metadata
const result = await pg.queryWithMetadata<User>('SELECT * FROM users')
console.log(result.rows)     // User[]
console.log(result.rowCount) // number
console.log(result.fields)   // column metadata
console.log(result.command)  // 'SELECT'

// Transactions
await pg.transaction(async (execute) => {
  await execute('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1])
  await execute('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2])
  // Auto-commits on success, auto-rollback on error
})

// Database administration
const version = await pg.version()
const tables = await pg.listTables()
const columns = await pg.describeTable('users')
const databases = await pg.listDatabases()
await pg.createDatabase('newdb')

// Health check
const isHealthy = await pg.ping()

// Clean up
await pg.close()

SQLiteContainer

Lightweight SQLite in a container.

import { createSQLiteContainer } from '@dotdo/sandbox/databases/sqlite'

const sqlite = createSQLiteContainer(env.SQLITE_CONTAINER, {
  database: '/data/app.db',
})

await sqlite.connect()

// SQLite operations
await sqlite.execute('CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)')
await sqlite.execute('INSERT INTO tasks (title) VALUES (?)', ['Learn containers'])

const tasks = await sqlite.query<{ id: number; title: string }>(
  'SELECT * FROM tasks'
)

// SQLite-specific features
const tableInfo = await sqlite.tableInfo('tasks')
const tables = await sqlite.listTables()

await sqlite.close()

DuckDBContainer

DuckDB for analytics and OLAP workloads.

import { createDuckDBContainer } from '@dotdo/sandbox/databases/duckdb'

const duckdb = createDuckDBContainer(env.DUCKDB_CONTAINER)

await duckdb.connect()

// Create and query tables
await duckdb.execute(`
  CREATE TABLE events (
    event_type VARCHAR,
    user_id VARCHAR,
    timestamp TIMESTAMP,
    properties JSON
  )
`)

// Analytics queries
const stats = await duckdb.query(`
  SELECT event_type, COUNT(*) as count
  FROM events
  GROUP BY event_type
  ORDER BY count DESC
`)

// Load from Parquet files (DuckDB specialty)
const parquetData = await duckdb.query(`
  SELECT * FROM read_parquet('https://example.com/data.parquet')
`)

// Export data
const csvExport = await duckdb.exportData('events', 'csv')

await duckdb.close()

RPC Client

WebSocket-based client for remote sandbox management.

import {
  createSandboxClient,
  connectSandbox,
  SANDBOX_DO_URL,
} from '@dotdo/sandbox/client'

// Create client for sandbox.do
const client = createSandboxClient({
  url: 'wss://sandbox.do/rpc',  // or custom endpoint
  auth: 'your-api-key',
  timeout: 30000,
  reconnect: true,
})

// Agent operations
const agent = await client.agents.create({
  type: 'claude-code',
  size: 'standard-2',
  env: { DEBUG: 'true' },
})

const status = await client.agents.get(agent.id)
const agents = await client.agents.list()

const result = await client.agents.run(agent.id, 'Create a hello world app', {
  model: 'claude-sonnet-4-20250514',
  permissionMode: 'acceptEdits',
  timeout: 300000,
})

const execResult = await client.agents.exec(agent.id, 'npm install', {
  cwd: '/workspace',
  timeout: 60000,
})

await client.agents.writeFile(agent.id, '/workspace/config.json', '{}')
const content = await client.agents.readFile(agent.id, '/workspace/package.json')

await client.agents.cloneRepo(agent.id, 'https://github.com/user/repo', {
  branch: 'main',
  shallow: true,
})

await client.agents.stop(agent.id)

// Database operations
const db = await client.databases.create({
  type: 'postgres',
  size: 'standard-1',
})

const rows = await client.databases.query<{ id: number }>(
  db.id,
  'SELECT * FROM users WHERE active = $1',
  [true]
)

const { rowsAffected } = await client.databases.execute(
  db.id,
  'UPDATE users SET active = $1 WHERE id = $2',
  [false, 123]
)

await client.databases.stop(db.id)

// Session management
const session = await client.sessions.current()
const quota = await client.sessions.quota()

// Connect directly to a specific sandbox
const sandbox = connectSandbox('sandbox-id', { auth: 'your-api-key' })
await sandbox.exec('ls -la')
const status = await sandbox.status()
await sandbox.stop()

REST API

The sandbox.do Worker also exposes a REST API:

# Health check
GET /health

# List agents
GET /api/agents
Authorization: Bearer your-api-key

# Create agent
POST /api/agents
Content-Type: application/json
Authorization: Bearer your-api-key

{"type": "claude-code", "size": "standard-2"}

# Get agent
GET /api/agents/{id}
Authorization: Bearer your-api-key

# Run prompt
POST /api/agents/{id}/run
Content-Type: application/json
Authorization: Bearer your-api-key

{"prompt": "Create a hello world app", "options": {"timeout": 300000}}

# Execute command
POST /api/agents/{id}/exec
Content-Type: application/json
Authorization: Bearer your-api-key

{"command": "npm install", "options": {"cwd": "/workspace"}}

# Stop agent
DELETE /api/agents/{id}
Authorization: Bearer your-api-key

# Database operations
GET /api/databases
POST /api/databases
GET /api/databases/{id}
POST /api/databases/{id}/query
POST /api/databases/{id}/execute
DELETE /api/databases/{id}

# Session info
GET /api/sessions/current
GET /api/sessions/quota

Wrangler Configuration

Example wrangler.toml for a sandbox worker:

name = "my-sandbox-worker"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]

# Claude Code container
[[containers]]
binding = "CLAUDE_CODE_SANDBOX"
class_name = "ClaudeCodeContainer"
image = "./dockerfiles/Dockerfile.claude-code"
max_instances = 5
default_port = 8080
sleep_after = "10m"

# PostgreSQL container
[[containers]]
binding = "POSTGRES_CONTAINER"
class_name = "PostgresContainer"
image = "./dockerfiles/Dockerfile.postgres"
max_instances = 10
default_port = 8080
sleep_after = "10m"

# Durable Object for state
[[durable_objects.bindings]]
name = "SANDBOX_DO"
class_name = "SandboxDO"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SandboxDO"]

[vars]
# Set API keys via wrangler secret or .dev.vars

Pre-built wrangler configs are available:

import {
  CLAUDE_CODE_WRANGLER_CONFIG,
  OPENCODE_WRANGLER_CONFIG,
  CODEX_WRANGLER_CONFIG,
  POSTGRES_WRANGLER_CONFIG,
  SQLITE_WRANGLER_CONFIG,
  DUCKDB_WRANGLER_CONFIG,
} from '@dotdo/sandbox'

console.log(POSTGRES_WRANGLER_CONFIG)
// {
//   binding: 'POSTGRES_CONTAINER',
//   className: 'PostgresContainer',
//   image: './dockerfiles/Dockerfile.postgres',
//   defaultPort: 8080,
//   maxInstances: 10,
//   sleepAfter: '10m',
//   size: 'standard-1',
// }

Development

Prerequisites

  • Node.js >= 18.0.0
  • pnpm
  • Cloudflare account with Containers enabled

Setup

# Install dependencies
pnpm install

# Build
pnpm build

# Run unit tests
pnpm test

# Run E2E tests (requires Cloudflare credentials)
pnpm test:e2e

# Run integration tests (requires SANDBOX_API_KEY)
pnpm test:integration

# Run all tests
pnpm test:all

Environment Variables

Variable Required Description
CLOUDFLARE_API_TOKEN For E2E tests Cloudflare API token with Workers permissions
CLOUDFLARE_ACCOUNT_ID For E2E tests Your Cloudflare account ID
SANDBOX_API_KEY For integration tests API key for sandbox.do

Test Configurations

  • Unit tests (pnpm test): Fast, isolated tests with mocks
  • E2E tests (pnpm test:e2e): Deploy to Cloudflare preview and test live infrastructure
  • Integration tests (pnpm test:integration): Test against live sandbox.do deployment
  • Workers tests (pnpm test:workers): Cloudflare Workers-specific tests using vitest-pool-workers

CI/CD

This project uses GitHub Actions for continuous integration:

  • Unit Tests: Run on every push and PR
  • E2E Tests: Deploy to Cloudflare preview and run end-to-end tests
  • Integration Tests: Test against live sandbox.do (requires secrets)
  • Node.js Matrix: Test across Node.js 18, 20, and 22

Required Secrets

Configure these secrets in your GitHub repository:

  • CLOUDFLARE_API_TOKEN: API token with Workers edit permissions
  • CLOUDFLARE_ACCOUNT_ID: Your Cloudflare account ID
  • SANDBOX_API_KEY: (Optional) API key for integration tests

License

MIT

About

Development sandbox for testing

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors