diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9196e2f0e0a6f0..c8d78282060a5e9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -138,6 +138,7 @@ /src/content/docs/workers/observability/ @irvinebroque @mikenomitch @rohinlohe @kodster28 @cloudflare/pcx-technical-writing /src/content/docs/workers/static-assets @irvinebroque @GregBrimble @WalshyDev @kodster28 @cloudflare/deploy-config @cloudflare/pcx-technical-writing /src/content/docs/workflows/ @elithrar @celso @cloudflare/pcx-technical-writing +/src/content/docs/sandbox/ @whoiskatrin @ghostwriternr @cloudflare/pcx-technical-writing @ai-agents # DDoS Protection diff --git a/src/content/docs/sandbox/api/commands.mdx b/src/content/docs/sandbox/api/commands.mdx new file mode 100644 index 000000000000000..d96ed92db4505a1 --- /dev/null +++ b/src/content/docs/sandbox/api/commands.mdx @@ -0,0 +1,219 @@ +--- +title: Commands +pcx_content_type: concept +sidebar: + order: 1 +--- + +import { Details, TypeScriptExample } from "~/components"; + +Execute commands and manage background processes in the sandbox's isolated container environment. + +## Methods + +### `exec()` + +Execute a command and return the complete result. + +```ts +const result = await sandbox.exec(command: string, options?: ExecOptions): Promise +``` + +**Parameters**: +- `command` - The command to execute (can include arguments) +- `options` (optional): + - `stream` - Enable streaming callbacks (default: `false`) + - `onOutput` - Callback for real-time output: `(stream: 'stdout' | 'stderr', data: string) => void` + - `timeout` - Maximum execution time in milliseconds + +**Returns**: `Promise` with `success`, `stdout`, `stderr`, `exitCode` + + +``` +const result = await sandbox.exec('npm run build'); + +if (result.success) { + console.log('Build output:', result.stdout); +} else { + console.error('Build failed:', result.stderr); +} + +// With streaming +await sandbox.exec('npm install', { + stream: true, + onOutput: (stream, data) => console.log(`[${stream}] ${data}`) +}); +``` + + +### `execStream()` + +Execute a command and return a Server-Sent Events stream for real-time processing. + +```ts +const stream = await sandbox.execStream(command: string, options?: ExecOptions): Promise +``` + +**Parameters**: +- `command` - The command to execute +- `options` - Same as `exec()` + +**Returns**: `Promise` emitting `ExecEvent` objects (`start`, `stdout`, `stderr`, `complete`, `error`) + + +``` +import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; + +const stream = await sandbox.execStream('npm run build'); + +for await (const event of parseSSEStream(stream)) { + switch (event.type) { + case 'stdout': + console.log('Output:', event.data); + break; + case 'complete': + console.log('Exit code:', event.exitCode); + break; + case 'error': + console.error('Failed:', event.error); + break; + } +} +``` + + +### `startProcess()` + +Start a long-running background process. + +```ts +const process = await sandbox.startProcess(command: string, options?: ProcessOptions): Promise +``` + +**Parameters**: +- `command` - The command to start as a background process +- `options` (optional): + - `cwd` - Working directory + - `env` - Environment variables + +**Returns**: `Promise` with `id`, `pid`, `command`, `status` + + +``` +const server = await sandbox.startProcess('python -m http.server 8000'); +console.log('Started with PID:', server.pid); + +// With custom environment +const app = await sandbox.startProcess('node app.js', { + cwd: '/workspace/my-app', + env: { NODE_ENV: 'production', PORT: '3000' } +}); +``` + + +### `listProcesses()` + +List all running processes. + +```ts +const processes = await sandbox.listProcesses(): Promise +``` + + +``` +const processes = await sandbox.listProcesses(); + +for (const proc of processes) { + console.log(`${proc.id}: ${proc.command} (PID ${proc.pid})`); +} +``` + + +### `killProcess()` + +Terminate a specific process. + +```ts +await sandbox.killProcess(processId: string, signal?: string): Promise +``` + +**Parameters**: +- `processId` - The process ID (from `startProcess()` or `listProcesses()`) +- `signal` - Signal to send (default: `"SIGTERM"`) + + +``` +const server = await sandbox.startProcess('python -m http.server 8000'); +await sandbox.killProcess(server.id); +``` + + +### `killAllProcesses()` + +Terminate all running processes. + +```ts +await sandbox.killAllProcesses(): Promise +``` + + +``` +await sandbox.killAllProcesses(); +``` + + +### `streamProcessLogs()` + +Stream logs from a running process in real-time. + +```ts +const stream = await sandbox.streamProcessLogs(processId: string): Promise +``` + +**Parameters**: +- `processId` - The process ID + +**Returns**: `Promise` emitting `LogEvent` objects + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +const server = await sandbox.startProcess('node server.js'); +const logStream = await sandbox.streamProcessLogs(server.id); + +for await (const log of parseSSEStream(logStream)) { + console.log(`[${log.timestamp}] ${log.data}`); + + if (log.data.includes('Server started')) break; +} +``` + + +### `getProcessLogs()` + +Get accumulated logs from a process. + +```ts +const logs = await sandbox.getProcessLogs(processId: string): Promise +``` + +**Parameters**: +- `processId` - The process ID + +**Returns**: `Promise` with all accumulated output + + +``` +const server = await sandbox.startProcess('node server.js'); +await new Promise(resolve => setTimeout(resolve, 5000)); + +const logs = await sandbox.getProcessLogs(server.id); +console.log('Server logs:', logs); +``` + + +## Related resources + +- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running processes +- [Files API](/sandbox/api/files/) - File operations diff --git a/src/content/docs/sandbox/api/files.mdx b/src/content/docs/sandbox/api/files.mdx new file mode 100644 index 000000000000000..adfd7c8149c8150 --- /dev/null +++ b/src/content/docs/sandbox/api/files.mdx @@ -0,0 +1,167 @@ +--- +title: Files +pcx_content_type: concept +sidebar: + order: 2 +--- + +import { TypeScriptExample } from "~/components"; + +Read, write, and manage files in the sandbox filesystem. All paths are absolute (e.g., `/workspace/app.js`). + +## Methods + +### `writeFile()` + +Write content to a file. + +```ts +await sandbox.writeFile(path: string, content: string, options?: WriteFileOptions): Promise +``` + +**Parameters**: +- `path` - Absolute path to the file +- `content` - Content to write +- `options` (optional): + - `encoding` - File encoding (default: `"utf-8"`) + + +``` +await sandbox.writeFile('/workspace/app.js', `console.log('Hello!');`); + +// Binary data +await sandbox.writeFile('/tmp/image.png', base64Data, { encoding: 'base64' }); +``` + + +### `readFile()` + +Read a file from the sandbox. + +```ts +const file = await sandbox.readFile(path: string, options?: ReadFileOptions): Promise +``` + +**Parameters**: +- `path` - Absolute path to the file +- `options` (optional): + - `encoding` - File encoding (default: `"utf-8"`) + +**Returns**: `Promise` with `content` and `encoding` + + +``` +const file = await sandbox.readFile('/workspace/package.json'); +const pkg = JSON.parse(file.content); + +// Binary data +const image = await sandbox.readFile('/tmp/image.png', { encoding: 'base64' }); +``` + + +### `mkdir()` + +Create a directory. + +```ts +await sandbox.mkdir(path: string, options?: MkdirOptions): Promise +``` + +**Parameters**: +- `path` - Absolute path to the directory +- `options` (optional): + - `recursive` - Create parent directories if needed (default: `false`) + + +``` +await sandbox.mkdir('/workspace/src'); + +// Nested directories +await sandbox.mkdir('/workspace/src/components/ui', { recursive: true }); +``` + + +### `deleteFile()` + +Delete a file. + +```ts +await sandbox.deleteFile(path: string): Promise +``` + +**Parameters**: +- `path` - Absolute path to the file + + +``` +await sandbox.deleteFile('/workspace/temp.txt'); +``` + + +### `renameFile()` + +Rename a file. + +```ts +await sandbox.renameFile(oldPath: string, newPath: string): Promise +``` + +**Parameters**: +- `oldPath` - Current file path +- `newPath` - New file path + + +``` +await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt'); +``` + + +### `moveFile()` + +Move a file to a different directory. + +```ts +await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise +``` + +**Parameters**: +- `sourcePath` - Current file path +- `destinationPath` - Destination path + + +``` +await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt'); +``` + + +### `gitCheckout()` + +Clone a git repository. + +```ts +await sandbox.gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promise +``` + +**Parameters**: +- `repoUrl` - Git repository URL +- `options` (optional): + - `branch` - Branch to checkout (default: main branch) + - `targetDir` - Directory to clone into (default: repo name) + - `depth` - Clone depth for shallow clone + + +``` +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Specific branch +await sandbox.gitCheckout('https://github.com/user/repo', { + branch: 'develop', + targetDir: 'my-project' +}); +``` + + +## Related resources + +- [Manage files guide](/sandbox/guides/manage-files/) - Detailed guide with best practices +- [Commands API](/sandbox/api/commands/) - Execute commands diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx new file mode 100644 index 000000000000000..6799a789554b406 --- /dev/null +++ b/src/content/docs/sandbox/api/index.mdx @@ -0,0 +1,68 @@ +--- +title: API Reference +pcx_content_type: navigation +sidebar: + order: 4 +--- + +import { LinkTitleCard, CardGrid } from "~/components"; + +The Sandbox SDK provides a comprehensive API for executing code, managing files, running processes, and exposing services in isolated sandboxes. All operations are performed through the `Sandbox` instance you obtain via `getSandbox()`. + +## Getting a sandbox instance + +```typescript +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'user-123'); +``` + +The sandbox ID should be unique per user or session. The same ID will always return the same sandbox instance with persistent state. + +## API organization + +The Sandbox SDK is organized into focused APIs: + + + + + Execute commands and stream output. Includes `exec()`, `execStream()`, and background process management. + + + + Read, write, and manage files in the sandbox filesystem. Includes directory operations and file metadata. + + + + Execute Python and JavaScript code with rich outputs including charts, tables, and formatted data. + + + + Expose services running in the sandbox via preview URLs. Access web servers and APIs from the internet. + + + + Advanced: Create isolated execution contexts with persistent shell state. Configure environment variables and manage container lifecycle. + + + diff --git a/src/content/docs/sandbox/api/interpreter.mdx b/src/content/docs/sandbox/api/interpreter.mdx new file mode 100644 index 000000000000000..2e61518d314d3f7 --- /dev/null +++ b/src/content/docs/sandbox/api/interpreter.mdx @@ -0,0 +1,186 @@ +--- +title: Code Interpreter +pcx_content_type: concept +sidebar: + order: 3 +--- + +import { TypeScriptExample } from "~/components"; + +Execute Python, JavaScript, and TypeScript code with support for data visualizations, tables, and rich output formats. Contexts maintain state (variables, imports, functions) across executions. + +## Methods + +### `createCodeContext()` + +Create a persistent execution context for running code. + +```ts +const context = await sandbox.createCodeContext(options?: CreateContextOptions): Promise +``` + +**Parameters**: +- `options` (optional): + - `language` - `"python" | "javascript" | "typescript"` (default: `"python"`) + - `cwd` - Working directory (default: `"/workspace"`) + - `envVars` - Environment variables + - `timeout` - Request timeout in milliseconds (default: 30000) + +**Returns**: `Promise` with `id`, `language`, `cwd`, `createdAt`, `lastUsed` + + +``` +const ctx = await sandbox.createCodeContext({ + language: 'python', + envVars: { API_KEY: env.API_KEY } +}); +``` + + +### `runCode()` + +Execute code in a context and return the complete result. + +```ts +const result = await sandbox.runCode(code: string, options?: RunCodeOptions): Promise +``` + +**Parameters**: +- `code` - The code to execute (required) +- `options` (optional): + - `context` - Context to run in (recommended - see below) + - `language` - `"python" | "javascript" | "typescript"` (default: `"python"`) + - `timeout` - Execution timeout in milliseconds (default: 60000) + - `onStdout`, `onStderr`, `onResult`, `onError` - Streaming callbacks + +**Returns**: `Promise` with: +- `code` - The executed code +- `logs` - `stdout` and `stderr` arrays +- `results` - Array of rich outputs (see [Rich Output Formats](#rich-output-formats)) +- `error` - Execution error if any +- `executionCount` - Execution counter + +**Recommended usage - create explicit context**: + + +``` +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +await sandbox.runCode('import math; radius = 5', { context: ctx }); +const result = await sandbox.runCode('math.pi * radius ** 2', { context: ctx }); + +console.log(result.results[0].text); // "78.53981633974483" +``` + + +:::note[Default context behavior] +If no `context` is provided, a default context is automatically created/reused for the specified `language`. While convenient for quick tests, **explicitly creating contexts is recommended** for production use to maintain predictable state. + + +``` +const result = await sandbox.runCode(` +data = [1, 2, 3, 4, 5] +print(f"Sum: {sum(data)}") +sum(data) +`, { language: 'python' }); + +console.log(result.logs.stdout); // ["Sum: 15"] +console.log(result.results[0].text); // "15" +``` + +::: + +**Error handling**: + + +``` +const result = await sandbox.runCode('x = 1 / 0', { language: 'python' }); + +if (result.error) { + console.error(result.error.name); // "ZeroDivisionError" + console.error(result.error.value); // "division by zero" + console.error(result.error.traceback); // Stack trace array +} +``` + + +### `listCodeContexts()` + +List all active code execution contexts. + +```ts +const contexts = await sandbox.listCodeContexts(): Promise +``` + + +``` +const contexts = await sandbox.listCodeContexts(); +console.log(`Found ${contexts.length} contexts`); +``` + + +### `deleteCodeContext()` + +Delete a code execution context and free its resources. + +```ts +await sandbox.deleteCodeContext(contextId: string): Promise +``` + + +``` +const ctx = await sandbox.createCodeContext({ language: 'python' }); +await sandbox.runCode('print("Hello")', { context: ctx }); +await sandbox.deleteCodeContext(ctx.id); +``` + + +## Rich Output Formats + +Results include: `text`, `html`, `png`, `jpeg`, `svg`, `latex`, `markdown`, `json`, `chart`, `data` + +**Charts (matplotlib)**: + + +``` +const result = await sandbox.runCode(` +import matplotlib.pyplot as plt +import numpy as np + +x = np.linspace(0, 10, 100) +plt.plot(x, np.sin(x)) +plt.show() +`, { language: 'python' }); + +if (result.results[0]?.png) { + const imageBuffer = Buffer.from(result.results[0].png, 'base64'); + return new Response(imageBuffer, { + headers: { 'Content-Type': 'image/png' } + }); +} +``` + + +**Tables (pandas)**: + + +``` +const result = await sandbox.runCode(` +import pandas as pd +df = pd.DataFrame({'Name': ['Alice', 'Bob'], 'Age': [25, 30]}) +df +`, { language: 'python' }); + +if (result.results[0]?.html) { + return new Response(result.results[0].html, { + headers: { 'Content-Type': 'text/html' } + }); +} +``` + + +## Related resources + +- [Build an AI Code Executor](/sandbox/tutorials/ai-code-executor/) - Complete tutorial +- [Commands API](/sandbox/api/commands/) - Lower-level command execution +- [Files API](/sandbox/api/files/) - File operations diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx new file mode 100644 index 000000000000000..ffd6fb522674680 --- /dev/null +++ b/src/content/docs/sandbox/api/ports.mdx @@ -0,0 +1,86 @@ +--- +title: Ports +pcx_content_type: concept +sidebar: + order: 4 +--- + +import { TypeScriptExample } from "~/components"; + +Expose services running in your sandbox via public preview URLs. See [Preview URLs concept](/sandbox/concepts/preview-urls/) for details. + +## Methods + +### `exposePort()` + +Expose a port and get a preview URL. + +```ts +const response = await sandbox.exposePort(port: number, options?: ExposePortOptions): Promise +``` + +**Parameters**: +- `port` - Port number to expose (1024-65535) +- `options` (optional): + - `name` - Friendly name for the port + +**Returns**: `Promise` with `port`, `exposedAt` (preview URL), `name` + + +``` +await sandbox.startProcess('python -m http.server 8000'); +const exposed = await sandbox.exposePort(8000); + +console.log('Available at:', exposed.exposedAt); +// https://abc123-8000.sandbox.workers.dev + +// Multiple services with names +await sandbox.startProcess('node api.js'); +const api = await sandbox.exposePort(3000, { name: 'api' }); + +await sandbox.startProcess('npm run dev'); +const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); +``` + + +### `unexposePort()` + +Remove an exposed port and close its preview URL. + +```ts +await sandbox.unexposePort(port: number): Promise +``` + +**Parameters**: +- `port` - Port number to unexpose + + +``` +await sandbox.unexposePort(8000); +``` + + +### `getExposedPorts()` + +Get information about all currently exposed ports. + +```ts +const response = await sandbox.getExposedPorts(): Promise +``` + +**Returns**: `Promise` with `ports` array (containing `port`, `exposedAt`, `name`) + + +``` +const { ports } = await sandbox.getExposedPorts(); + +for (const port of ports) { + console.log(`${port.name || port.port}: ${port.exposedAt}`); +} +``` + + +## Related resources + +- [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work +- [Commands API](/sandbox/api/commands/) - Start background processes diff --git a/src/content/docs/sandbox/api/sessions.mdx b/src/content/docs/sandbox/api/sessions.mdx new file mode 100644 index 000000000000000..90ff309d9693a6a --- /dev/null +++ b/src/content/docs/sandbox/api/sessions.mdx @@ -0,0 +1,155 @@ +--- +title: Sessions +pcx_content_type: configuration +sidebar: + order: 5 +--- + +import { TypeScriptExample } from "~/components"; + +Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory. See [Session management concept](/sandbox/concepts/sessions/) for details. + +:::note +Every sandbox has a default session that automatically maintains shell state. Create additional sessions when you need isolated shell contexts for different environments or parallel workflows. +::: + +## Methods + +### `createSession()` + +Create a new isolated execution session. + +```ts +const session = await sandbox.createSession(options?: SessionOptions): Promise +``` + +**Parameters**: +- `options` (optional): + - `id` - Custom session ID (auto-generated if not provided) + - `env` - Environment variables for this session + - `cwd` - Working directory (default: `"/workspace"`) + +**Returns**: `Promise` with all sandbox methods bound to this session + + +``` +// Multiple isolated environments +const prodSession = await sandbox.createSession({ + id: 'prod', + env: { NODE_ENV: 'production', API_URL: 'https://api.example.com' }, + cwd: '/workspace/prod' +}); + +const testSession = await sandbox.createSession({ + id: 'test', + env: { NODE_ENV: 'test', API_URL: 'http://localhost:3000' }, + cwd: '/workspace/test' +}); + +// Run in parallel +const [prodResult, testResult] = await Promise.all([ + prodSession.exec('npm run build'), + testSession.exec('npm run build') +]); +``` + + +### `getSession()` + +Retrieve an existing session by ID. + +```ts +const session = await sandbox.getSession(sessionId: string): Promise +``` + +**Parameters**: +- `sessionId` - ID of an existing session + +**Returns**: `Promise` bound to the specified session + + +``` +// First request - create session +const session = await sandbox.createSession({ id: 'user-123' }); +await session.exec('git clone https://github.com/user/repo.git'); +await session.exec('cd repo && npm install'); + +// Second request - resume session (environment and cwd preserved) +const session = await sandbox.getSession('user-123'); +const result = await session.exec('cd repo && npm run build'); +``` + + +### `setEnvVars()` + +Set environment variables in the sandbox. + +```ts +await sandbox.setEnvVars(envVars: Record): Promise +``` + +**Parameters**: +- `envVars` - Key-value pairs of environment variables to set + +:::caution +Call `setEnvVars()` **before** any other sandbox operations to ensure environment variables are available from the start. +::: + + +``` +const sandbox = getSandbox(env.Sandbox, 'user-123'); + +// Set environment variables first +await sandbox.setEnvVars({ + API_KEY: env.OPENAI_API_KEY, + DATABASE_URL: env.DATABASE_URL, + NODE_ENV: 'production' +}); + +// Now commands can access these variables +await sandbox.exec('python script.py'); +``` + + +### `destroy()` + +Destroy the sandbox container and free up resources. + +```ts +await sandbox.destroy(): Promise +``` + +:::note +Containers automatically sleep after 3 minutes of inactivity, but still count toward account limits. Use `destroy()` to immediately free up resources. +::: + + +``` +async function executeCode(code: string): Promise { + const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); + + try { + await sandbox.writeFile('/tmp/code.py', code); + const result = await sandbox.exec('python /tmp/code.py'); + return result.stdout; + } finally { + await sandbox.destroy(); + } +} +``` + + +## ExecutionSession methods + +The `ExecutionSession` object has all sandbox methods bound to the specific session: + +**Commands**: [`exec()`](/sandbox/api/commands/#exec), [`execStream()`](/sandbox/api/commands/#execstream) +**Processes**: [`startProcess()`](/sandbox/api/commands/#startprocess), [`listProcesses()`](/sandbox/api/commands/#listprocesses), [`killProcess()`](/sandbox/api/commands/#killprocess), [`killAllProcesses()`](/sandbox/api/commands/#killallprocesses), [`getProcessLogs()`](/sandbox/api/commands/#getprocesslogs), [`streamProcessLogs()`](/sandbox/api/commands/#streamprocesslogs) +**Files**: [`writeFile()`](/sandbox/api/files/#writefile), [`readFile()`](/sandbox/api/files/#readfile), [`mkdir()`](/sandbox/api/files/#mkdir), [`deleteFile()`](/sandbox/api/files/#deletefile), [`renameFile()`](/sandbox/api/files/#renamefile), [`moveFile()`](/sandbox/api/files/#movefile), [`gitCheckout()`](/sandbox/api/files/#gitcheckout) +**Environment**: [`setEnvVars()`](/sandbox/api/sessions/#setenvvars) +**Code Interpreter**: [`createCodeContext()`](/sandbox/api/interpreter/#createcodecontext), [`runCode()`](/sandbox/api/interpreter/#runcode), [`listCodeContexts()`](/sandbox/api/interpreter/#listcodecontexts), [`deleteCodeContext()`](/sandbox/api/interpreter/#deletecodecontext) + +## Related resources + +- [Session management concept](/sandbox/concepts/sessions/) - How sessions work +- [Commands API](/sandbox/api/commands/) - Execute commands diff --git a/src/content/docs/sandbox/concepts/architecture.mdx b/src/content/docs/sandbox/concepts/architecture.mdx new file mode 100644 index 000000000000000..c904a01d100292c --- /dev/null +++ b/src/content/docs/sandbox/concepts/architecture.mdx @@ -0,0 +1,139 @@ +--- +title: Architecture +pcx_content_type: concept +sidebar: + order: 1 +--- + +The Sandbox SDK provides isolated code execution environments on Cloudflare's edge network. It combines three Cloudflare technologies: + +- **Workers** - JavaScript runtime at the edge +- **Durable Objects** - Stateful compute with persistent storage +- **Containers** - Isolated execution environments with full Linux capabilities + +## Three-layer architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Your Application │ +│ (Cloudflare Worker) │ +└───────────────────────────┬─────────────────────────────┘ + ├─ getSandbox() + ├─ exec() + ├─ writeFile() + │ + ┌────────────────▼──────────────────┐ + │ Container-enabled Durable Object │ + │ (SDK methods via RPC from Worker) │ + └───────────────────────────────────┘ + │ HTTP/JSON + │ + ┌───────▼───────┐ + │ Durable Object │ Layer 2: State Management + │ (Persistent) │ + └───────┬───────┘ + │ Container Protocol + │ + ┌───────▼───────┐ + │ Container │ Layer 3: Isolated Execution + │ (Linux + Bun) │ + └───────────────┘ +``` + +### Layer 1: Client SDK + +The developer-facing API you use in your Workers: + +```typescript +import { getSandbox } from "@cloudflare/sandbox"; + +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); +const result = await sandbox.exec("python script.py"); +``` + +**Purpose**: Provide a clean, type-safe TypeScript interface for all sandbox operations. + +### Layer 2: Durable Object + +Manages sandbox lifecycle and routing: + +```typescript +export class Sandbox extends DurableObject { + // Extends Cloudflare Container for isolation + // Routes requests between client and container + // Manages preview URLs and state +} +``` + +**Purpose**: Provide persistent, stateful sandbox instances with unique identities. + +**Why Durable Objects**: + +- **Persistent identity** - Same sandbox ID always routes to same instance +- **State management** - Filesystem and processes persist between requests +- **Geographic distribution** - Sandboxes run close to users +- **Automatic scaling** - Cloudflare manages provisioning + +### Layer 3: Container Runtime + +Executes code in isolation with full Linux capabilities. + +**Purpose**: Safely execute untrusted code. + +**Why containers**: + +- **True isolation** - Process-level isolation with namespaces +- **Full environment** - Real Linux with Python, Node.js, Git, etc. +- **Resource limits** - CPU, memory, disk constraints + +## Request flow + +When you execute a command: + +```typescript +await sandbox.exec("python script.py"); +``` + +1. **Client SDK** validates parameters and sends HTTP request to Durable Object +2. **Durable Object** authenticates and routes to container runtime +3. **Container Runtime** validates inputs, executes command, captures output +4. **Response flows back** through all layers with proper error transformation + +## State persistence + +Sandboxes maintain state across requests: + +**Filesystem**: + +```typescript +// Request 1 +await sandbox.writeFile("/workspace/data.txt", "hello"); + +// Request 2 (minutes later) +const file = await sandbox.readFile("/workspace/data.txt"); +// Returns 'hello' - file persisted +``` + +**Processes**: + +```typescript +// Request 1 +await sandbox.startProcess("node server.js"); + +// Request 2 (minutes later) +const processes = await sandbox.listProcesses(); +// Server still running +``` + +## Performance + +**Cold start**: 100-300ms (container initialization) +**Warm start**: \<10ms (reuse existing container) +**Network latency**: 10-50ms (edge-to-edge) + +## Related resources + +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - How sandboxes are created and managed +- [Container runtime](/sandbox/concepts/containers/) - Inside the execution environment +- [Security model](/sandbox/concepts/security/) - How isolation and validation work +- [Session management](/sandbox/concepts/sessions/) - Advanced state management diff --git a/src/content/docs/sandbox/concepts/containers.mdx b/src/content/docs/sandbox/concepts/containers.mdx new file mode 100644 index 000000000000000..d80298576c0e1c7 --- /dev/null +++ b/src/content/docs/sandbox/concepts/containers.mdx @@ -0,0 +1,131 @@ +--- +title: Container runtime +pcx_content_type: concept +sidebar: + order: 3 +--- + +Each sandbox runs in an isolated Linux container based on Ubuntu 22.04. + +## Pre-installed software + +The base container comes pre-packaged with a full development environment: + +**Languages and runtimes**: +- Python 3.11 (with pip) +- Node.js 20 LTS (with npm) +- Bun (JavaScript/TypeScript runtime) + +**Python packages**: +- NumPy - Numerical computing +- pandas - Data analysis +- Matplotlib - Plotting and visualization +- IPython - Interactive Python + +**Development tools**: +- Git - Version control +- Build tools (gcc, make, pkg-config) +- Text editors (vim, nano) +- Process monitoring (htop, procps) + +**Utilities**: +- curl, wget - HTTP clients +- jq - JSON processor +- Network tools (ping, dig, netstat) +- Compression (zip, unzip) + +Install additional software at runtime or [customize the base image](/sandbox/configuration/dockerfile/): + +```bash +# Python packages +pip install scikit-learn tensorflow + +# Node.js packages +npm install express + +# System packages +apt-get install redis-server +``` + +## Filesystem + +The container provides a standard Linux filesystem. You can read and write anywhere you have permissions. + +**Standard directories**: +- `/workspace` - Default working directory for user code +- `/tmp` - Temporary files +- `/home` - User home directory +- `/usr/bin`, `/usr/local/bin` - Executable binaries + +**Example**: +```typescript +await sandbox.writeFile('/workspace/app.py', 'print("Hello")'); +await sandbox.writeFile('/tmp/cache.json', '{}'); +await sandbox.exec('ls -la /workspace'); +``` + +## Process management + +Processes run as you'd expect in a regular Linux environment. + +**Foreground processes** (`exec()`): +```typescript +const result = await sandbox.exec('npm test'); +// Waits for completion, returns output +``` + +**Background processes** (`startProcess()`): +```typescript +const process = await sandbox.startProcess('node server.js'); +// Returns immediately, process runs in background +``` + +## Network capabilities + +**Outbound connections** work: +```bash +curl https://api.example.com/data +pip install requests +npm install express +``` + +**Inbound connections** require port exposure: +```typescript +await sandbox.startProcess('python -m http.server 8000'); +const exposed = await sandbox.exposePort(8000); +console.log(exposed.exposedAt); // Public URL +``` + +**Localhost** works within sandbox: +```bash +redis-server & # Start server +redis-cli ping # Connect locally +``` + +## Security + +**Between sandboxes** (isolated): +- Each sandbox is a separate container +- Filesystem, memory and network are all isolated + +**Within sandbox** (shared): +- All processes see the same files +- Processes can communicate with each other +- Environment variables are session-scoped + +To run untrusted code, use separate sandboxes per user: +```typescript +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); +``` + +## Limitations + +**Cannot**: +- Load kernel modules or access host hardware +- Run nested containers (no Docker-in-Docker) + +## Related resources + +- [Architecture](/sandbox/concepts/architecture/) - How containers fit in the system +- [Security model](/sandbox/concepts/security/) - Container isolation details +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Container lifecycle management diff --git a/src/content/docs/sandbox/concepts/index.mdx b/src/content/docs/sandbox/concepts/index.mdx new file mode 100644 index 000000000000000..422eb9220fb23c9 --- /dev/null +++ b/src/content/docs/sandbox/concepts/index.mdx @@ -0,0 +1,23 @@ +--- +title: Concepts +pcx_content_type: navigation +sidebar: + order: 5 +--- + +These pages explain how the Sandbox SDK works, why it's designed the way it is, and the concepts you need to understand to use it effectively. + +## Available concepts + +- [Architecture](/sandbox/concepts/architecture/) - How the SDK is structured and why +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox states and behavior +- [Container runtime](/sandbox/concepts/containers/) - How code executes in isolated containers +- [Session management](/sandbox/concepts/sessions/) - When and how to use sessions +- [Preview URLs](/sandbox/concepts/preview-urls/) - How service exposure works +- [Security model](/sandbox/concepts/security/) - Isolation, validation, and safety mechanisms + +## Related resources + +- [Tutorials](/sandbox/tutorials/) - Learn by building complete applications +- [How-to guides](/sandbox/guides/) - Solve specific problems +- [API reference](/sandbox/api/) - Technical details and method signatures diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx new file mode 100644 index 000000000000000..9dbcc26226895b0 --- /dev/null +++ b/src/content/docs/sandbox/concepts/preview-urls.mdx @@ -0,0 +1,173 @@ +--- +title: Preview URLs +pcx_content_type: concept +sidebar: + order: 5 +--- + +Preview URLs provide public access to services running inside sandboxes. When you expose a port, you get a unique HTTPS URL that proxies requests to your service. + +```typescript +await sandbox.startProcess('python -m http.server 8000'); +const exposed = await sandbox.exposePort(8000); + +console.log(exposed.exposedAt); +// https://abc123-8000.sandbox.workers.dev +``` + +## URL format + +Preview URLs follow this pattern: + +``` +https://{sandbox-id}-{port}.sandbox.workers.dev +``` + +**Examples**: +- Port 3000: `https://abc123-3000.sandbox.workers.dev` +- Port 8080: `https://abc123-8080.sandbox.workers.dev` + +**URL stability**: URLs remain the same for a given sandbox ID and port. You can share, bookmark, or use them in webhooks. + +## Request routing + +``` +User's Browser + ↓ HTTPS +Your Worker + ↓ +Durable Object (sandbox) + ↓ HTTP +Your Service (on exposed port) +``` + +**Important**: You must handle preview URL routing in your Worker using `proxyToSandbox()`: + +```typescript +import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox"; + +export default { + async fetch(request, env) { + // Route preview URL requests to sandboxes + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; + + // Your custom routes here + // ... + } +}; +``` + +Without this, preview URLs won't work. + +## Multiple ports + +Expose multiple services simultaneously: + +```typescript +await sandbox.startProcess('node api.js'); // Port 3000 +await sandbox.startProcess('node admin.js'); // Port 3001 + +const api = await sandbox.exposePort(3000, { name: 'api' }); +const admin = await sandbox.exposePort(3001, { name: 'admin' }); + +// Each gets its own URL: +// https://abc123-3000.sandbox.workers.dev +// https://abc123-3001.sandbox.workers.dev +``` + +## What works + +- HTTP/HTTPS requests +- WebSocket (WSS) via HTTP upgrade +- Server-Sent Events +- All HTTP methods (GET, POST, PUT, DELETE, etc.) +- Request and response headers + +## What doesn't work + +- Raw TCP/UDP connections +- Custom protocols (must wrap in HTTP) +- Ports 80/443 (use 1024+) + +## Security + +:::caution +Preview URLs are publicly accessible. Anyone with the URL can access your service. +::: + +**Add authentication in your service**: + +```python +from flask import Flask, request, abort + +app = Flask(__name__) + +@app.route('/data') +def get_data(): + token = request.headers.get('Authorization') + if token != 'Bearer secret-token': + abort(401) + return {'data': 'protected'} +``` + +**Security features**: +- All traffic is HTTPS (automatic TLS) +- URLs use random sandbox IDs (hard to guess) +- You control authentication in your service + +## Troubleshooting + +### URL not accessible + +Check if service is running and listening: + +```typescript +// 1. Is service running? +const processes = await sandbox.listProcesses(); + +// 2. Is port exposed? +const ports = await sandbox.getExposedPorts(); + +// 3. Is service binding to 0.0.0.0 (not 127.0.0.1)? +// Good: +app.run(host='0.0.0.0', port=3000) + +// Bad (localhost only): +app.run(host='127.0.0.1', port=3000) +``` + +## Best practices + +**Service design**: +- Bind to `0.0.0.0` to make accessible +- Add authentication (don't rely on URL secrecy) +- Include health check endpoints +- Handle CORS if accessed from browsers + +**Cleanup**: +- Unexpose ports when done: `await sandbox.unexposePort(port)` +- Stop processes: `await sandbox.killAllProcesses()` + +## Local development + +:::caution[Local development only] +When using `wrangler dev`, you must expose ports in your Dockerfile: + +```dockerfile +FROM docker.io/cloudflare/sandbox:0.3.3 + +# Required for local development +EXPOSE 3000 +EXPOSE 8080 +``` + +Without `EXPOSE`, you'll see: `connect(): Connection refused: container port not found` + +This is **only required for local development**. In production, all container ports are automatically accessible. +::: + +## Related resources + +- [Ports API reference](/sandbox/api/ports/) - Complete port exposure API +- [Expose services guide](/sandbox/guides/expose-services/) - Practical patterns diff --git a/src/content/docs/sandbox/concepts/sandboxes.mdx b/src/content/docs/sandbox/concepts/sandboxes.mdx new file mode 100644 index 000000000000000..335d52f0139f472 --- /dev/null +++ b/src/content/docs/sandbox/concepts/sandboxes.mdx @@ -0,0 +1,141 @@ +--- +title: Sandbox lifecycle +pcx_content_type: concept +sidebar: + order: 2 +--- + +A sandbox is an isolated execution environment where your code runs. Each sandbox: + +- Has a unique identifier (sandbox ID) +- Contains an isolated filesystem +- Runs in a dedicated Linux container +- Persists state between requests +- Exists as a Cloudflare Durable Object + +## Lifecycle states + +### Creation + +A sandbox is created the first time you reference its ID: + +```typescript +const sandbox = getSandbox(env.Sandbox, 'user-123'); +await sandbox.exec('echo "Hello"'); // First request creates sandbox +``` + +**Duration**: 100-300ms (cold start) or \<10ms (if warm) + +### Active + +The sandbox is running and processing requests. Filesystem, processes, and environment variables persist across requests. + +### Idle + +After inactivity, the sandbox may enter idle state. Filesystem state is preserved, but the container may be paused. Next request triggers a warm start. + +### Destruction + +Sandboxes are explicitly destroyed or automatically cleaned up: + +```typescript +await sandbox.destroy(); +// All files, processes, and state deleted permanently +``` + +## Persistence + +Between requests to the same sandbox: + +**What persists**: +- Files in `/workspace`, `/tmp`, `/home` +- Background processes (started with `startProcess()`) +- Code interpreter contexts and variables +- Environment variables and port exposures + +**What doesn't persist**: +- Nothing survives `destroy()` +- Background processes may stop after container restarts (rare) + +## Naming strategies + +### Per-user sandboxes + +```typescript +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); +``` + +User's work persists across sessions. Good for interactive environments, playgrounds, and notebooks. + +### Per-session sandboxes + +```typescript +const sessionId = `session-${Date.now()}-${Math.random()}`; +const sandbox = getSandbox(env.Sandbox, sessionId); +// Later: +await sandbox.destroy(); +``` + +Fresh environment each time. Good for one-time execution, CI/CD, and isolated tests. + +### Per-task sandboxes + +```typescript +const sandbox = getSandbox(env.Sandbox, `build-${repoName}-${commit}`); +``` + +Idempotent operations with clear task-to-sandbox mapping. Good for builds, pipelines, and background jobs. + +## Request routing + +The first request to a sandbox determines its geographic location. Subsequent requests route to the same location. + +**For global apps**: +- Option 1: Multiple sandboxes per user with region suffix (`user-123-us`, `user-123-eu`) +- Option 2: Single sandbox per user (simpler, but some users see higher latency) + +## Lifecycle management + +### When to destroy + +```typescript +try { + const sandbox = getSandbox(env.Sandbox, sessionId); + await sandbox.exec('npm run build'); +} finally { + await sandbox.destroy(); // Clean up temporary sandboxes +} +``` + +**Destroy when**: Session ends, task completes, resources no longer needed + +**Don't destroy**: Personal environments, long-running services + +### Failure recovery + +If container crashes or Durable Object is evicted (rare): + +```typescript +try { + await sandbox.exec('command'); +} catch (error) { + if (error.message.includes('container') || error.message.includes('connection')) { + await sandbox.exec('command'); // Retry - container recreates + } +} +``` + +## Best practices + +- **Name consistently** - Use clear, predictable naming schemes +- **Clean up temporary sandboxes** - Always destroy when done +- **Reuse long-lived sandboxes** - One per user is often sufficient +- **Batch operations** - Combine commands: `npm install && npm test && npm build` +- **Handle failures** - Design for container restarts + +## Related resources + +- [Architecture](/sandbox/concepts/architecture/) - How sandboxes fit in the system +- [Container runtime](/sandbox/concepts/containers/) - What runs inside sandboxes +- [Session management](/sandbox/concepts/sessions/) - Advanced state isolation +- [Sessions API](/sandbox/api/sessions/) - Programmatic lifecycle control diff --git a/src/content/docs/sandbox/concepts/security.mdx b/src/content/docs/sandbox/concepts/security.mdx new file mode 100644 index 000000000000000..0c8717ab42f9a39 --- /dev/null +++ b/src/content/docs/sandbox/concepts/security.mdx @@ -0,0 +1,178 @@ +--- +title: Security model +pcx_content_type: concept +sidebar: + order: 6 +--- + +The Sandbox SDK is built on [Containers](/containers/), which run each sandbox in its own VM for strong isolation. + +## Container isolation + +Each sandbox runs in a separate VM, providing complete isolation: + +- **Filesystem isolation** - Sandboxes cannot access other sandboxes' files +- **Process isolation** - Processes in one sandbox cannot see or affect others +- **Network isolation** - Sandboxes have separate network stacks +- **Resource limits** - CPU, memory, and disk quotas are enforced per sandbox + +For complete security details about the underlying container platform, see [Containers architecture](/containers/platform-details/architecture/). + +## Within a sandbox + +All code within a single sandbox shares resources: + +- **Filesystem** - All processes see the same files +- **Processes** - All sessions can see all processes +- **Network** - Processes can communicate via localhost + +For complete isolation, use separate sandboxes per user: + +```typescript +// Good - Each user in separate sandbox +const userSandbox = getSandbox(env.Sandbox, `user-${userId}`); + +// Bad - Users sharing one sandbox +const shared = getSandbox(env.Sandbox, 'shared'); +// Users can read each other's files! +``` + +## Input validation + +### Command injection + +Always validate user input before using it in commands: + +```typescript +// Dangerous - user input directly in command +const filename = userInput; +await sandbox.exec(`cat ${filename}`); +// User could input: "file.txt; rm -rf /" + +// Safe - validate input +const filename = userInput.replace(/[^a-zA-Z0-9._-]/g, ''); +await sandbox.exec(`cat ${filename}`); + +// Better - use file API +await sandbox.writeFile('/tmp/input', userInput); +await sandbox.exec('cat /tmp/input'); +``` + +## Authentication + +### Sandbox access + +Sandbox IDs provide basic access control but aren't cryptographically secure. Add application-level authentication: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const userId = await authenticate(request); + if (!userId) { + return new Response('Unauthorized', { status: 401 }); + } + + // User can only access their sandbox + const sandbox = getSandbox(env.Sandbox, userId); + return Response.json({ authorized: true }); + } +}; +``` + +### Preview URLs + +Preview URLs are public. Add authentication in your service: + +```python +from flask import Flask, request, abort +import os + +app = Flask(__name__) + +def check_auth(): + token = request.headers.get('Authorization') + if token != f"Bearer {os.environ['AUTH_TOKEN']}": + abort(401) + +@app.route('/api/data') +def get_data(): + check_auth() + return {'data': 'protected'} +``` + +## Secrets management + +Use environment variables, not hardcoded secrets: + +```typescript +// Bad - hardcoded in file +await sandbox.writeFile('/workspace/config.js', ` + const API_KEY = 'sk_live_abc123'; +`); + +// Good - use environment variables +await sandbox.startProcess('node app.js', { + env: { + API_KEY: env.API_KEY, // From Worker environment binding + } +}); +``` + +Clean up temporary sensitive data: + +```typescript +try { + await sandbox.writeFile('/tmp/sensitive.txt', secretData); + await sandbox.exec('python process.py /tmp/sensitive.txt'); +} finally { + await sandbox.deleteFile('/tmp/sensitive.txt'); +} +``` + +## What the SDK protects against + +- Sandbox-to-sandbox access (VM isolation) +- Resource exhaustion (enforced quotas) +- Container escapes (VM-based isolation) + +## What you must implement + +- Authentication and authorization +- Input validation and sanitization +- Rate limiting +- Application-level security (SQL injection, XSS, etc.) + +## Best practices + +**Use separate sandboxes for isolation**: +```typescript +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); +``` + +**Validate all inputs**: +```typescript +const safe = input.replace(/[^a-zA-Z0-9._-]/g, ''); +await sandbox.exec(`command ${safe}`); +``` + +**Use environment variables for secrets**: +```typescript +await sandbox.startProcess('node app.js', { + env: { API_KEY: env.API_KEY } +}); +``` + +**Clean up temporary resources**: +```typescript +try { + const sandbox = getSandbox(env.Sandbox, sessionId); + await sandbox.exec('npm test'); +} finally { + await sandbox.destroy(); +} +``` + +## Related resources + +- [Containers architecture](/containers/platform-details/architecture/) - Underlying platform security +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Resource management diff --git a/src/content/docs/sandbox/concepts/sessions.mdx b/src/content/docs/sandbox/concepts/sessions.mdx new file mode 100644 index 000000000000000..035617fb8090a18 --- /dev/null +++ b/src/content/docs/sandbox/concepts/sessions.mdx @@ -0,0 +1,153 @@ +--- +title: Session management +pcx_content_type: concept +sidebar: + order: 4 +--- + +Sessions are bash shell execution contexts within a sandbox. Think of them like terminal tabs or panes in the same container. + +- **Sandbox** = A computer (container) +- **Session** = A terminal shell session in that computer + +## Default session + +Every sandbox has a default session that maintains shell state across commands: + +```typescript +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// These commands run in the default session +await sandbox.exec("cd /app"); +await sandbox.exec("pwd"); // Output: /app + +await sandbox.exec("export MY_VAR=hello"); +await sandbox.exec("echo $MY_VAR"); // Output: hello +``` + +Shell state persists: working directory, environment variables, exported variables all carry over between commands. + +## Creating sessions + +Create additional sessions for isolated shell contexts: + +```typescript +const buildSession = await sandbox.createSession({ + name: "build", + env: { NODE_ENV: "production" }, + cwd: "/build" +}); + +const testSession = await sandbox.createSession({ + name: "test", + env: { NODE_ENV: "test" }, + cwd: "/test" +}); + +// Different shell contexts +await buildSession.exec("npm run build"); +await testSession.exec("npm test"); +``` + +## What's isolated per session + +Each session has its own: + +**Shell environment**: +```typescript +await session1.exec("export MY_VAR=hello"); +await session2.exec("echo $MY_VAR"); // Empty - different shell +``` + +**Working directory**: +```typescript +await session1.exec("cd /workspace/project1"); +await session2.exec("pwd"); // Different working directory +``` + +**Environment variables** (set via `createSession` options): +```typescript +const session1 = await sandbox.createSession({ + env: { API_KEY: 'key-1' } +}); +const session2 = await sandbox.createSession({ + env: { API_KEY: 'key-2' } +}); +``` + +## What's shared + +All sessions in a sandbox share: + +**Filesystem**: +```typescript +await session1.writeFile('/workspace/file.txt', 'data'); +await session2.readFile('/workspace/file.txt'); // Can read it +``` + +**Processes**: +```typescript +await session1.startProcess('node server.js'); +await session2.listProcesses(); // Sees the server +``` + +**Ports**: +```typescript +await session1.exposePort(3000); +await session2.getExposedPorts(); // Sees port 3000 +``` + +## When to use sessions + +**Use sessions when**: +- You need isolated shell state for different tasks +- Running parallel operations with different environments +- Keeping AI agent credentials separate from app runtime + +**Example - separate dev and runtime environments**: +```typescript +// Phase 1: AI agent writes code (with API keys) +const devSession = await sandbox.createSession({ + name: "dev", + env: { ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY } +}); +await devSession.exec('ai-tool "build a web server"'); + +// Phase 2: Run the code (without API keys) +const appSession = await sandbox.createSession({ + name: "app", + env: { PORT: "3000" } +}); +await appSession.exec("node server.js"); +``` + +**Use separate sandboxes when**: +- You need complete isolation (untrusted code) +- Different users require fully separated environments +- Independent resource allocation is needed + +## Best practices + +**Clean up temporary sessions**: +```typescript +try { + const session = await sandbox.createSession({ name: 'temp' }); + await session.exec('command'); +} finally { + await sandbox.deleteSession('temp'); +} +``` + +**Sessions share filesystem**: +```typescript +// Bad - affects all sessions +await session.exec('rm -rf /workspace/*'); + +// Use separate sandboxes for untrusted isolation +const userSandbox = getSandbox(env.Sandbox, userId); +``` + +## Related resources + +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox management +- [Sessions API](/sandbox/api/sessions/) - Complete session API reference diff --git a/src/content/docs/sandbox/configuration/dockerfile.mdx b/src/content/docs/sandbox/configuration/dockerfile.mdx new file mode 100644 index 000000000000000..8bc255fa301b03e --- /dev/null +++ b/src/content/docs/sandbox/configuration/dockerfile.mdx @@ -0,0 +1,73 @@ +--- +title: Dockerfile reference +pcx_content_type: configuration +sidebar: + order: 2 +--- + +Customize the sandbox container image with your own packages, tools, and configurations by extending the base runtime image. + +## Base image + +The Sandbox SDK uses a Ubuntu-based Linux container with Python, Node.js (via Bun), and common development tools pre-installed: + +```dockerfile +FROM docker.io/cloudflare/sandbox:0.3.3 +``` + +:::note +Always match the Docker image version to your npm package version. If you're using `@cloudflare/sandbox@0.3.3`, use `docker.io/cloudflare/sandbox:0.3.3` as your base image. +::: + +**What's included:** + +- Ubuntu 22.04 LTS base +- Python 3.11+ with pip +- Bun (JavaScript/TypeScript runtime) +- Git, curl, wget, and common CLI tools +- Pre-installed Python packages: pandas, numpy, matplotlib +- System libraries for most common use cases + +## Creating a custom image + +Create a `Dockerfile` in your project root: + +```dockerfile title="Dockerfile" +FROM docker.io/cloudflare/sandbox:0.3.3 + +# Install additional Python packages +RUN pip install --no-cache-dir \ + scikit-learn==1.3.0 \ + tensorflow==2.13.0 \ + transformers==4.30.0 + +# Install Node.js packages globally +RUN npm install -g typescript ts-node prettier + +# Install system packages +RUN apt-get update && apt-get install -y \ + postgresql-client \ + redis-tools \ + && rm -rf /var/lib/apt/lists/* +``` + +Update `wrangler.jsonc` to reference your Dockerfile: + +```jsonc title="wrangler.jsonc" +{ + "containers": [ + { + "binding": "CONTAINER", + "dockerfile": "./Dockerfile", + }, + ], +} +``` + +When you run `wrangler dev` or `wrangler deploy`, Wrangler automatically builds your Docker image and pushes it to Cloudflare's container registry. You don't need to manually build or publish images. + +## Related resources + +- [Wrangler configuration](/sandbox/configuration/wrangler/) - Using custom images in wrangler.jsonc +- [Docker documentation](https://docs.docker.com/reference/dockerfile/) - Complete Dockerfile syntax +- [Container concepts](/sandbox/concepts/containers/) - Understanding the runtime environment diff --git a/src/content/docs/sandbox/configuration/environment-variables.mdx b/src/content/docs/sandbox/configuration/environment-variables.mdx new file mode 100644 index 000000000000000..a70cfca35e774e2 --- /dev/null +++ b/src/content/docs/sandbox/configuration/environment-variables.mdx @@ -0,0 +1,197 @@ +--- +title: Environment variables +pcx_content_type: configuration +sidebar: + order: 3 +--- + +Pass configuration, secrets, and runtime settings to your sandboxes using environment variables. + +### Command and process variables + +Pass environment variables when executing commands or starting processes: + +```typescript +// Commands +await sandbox.exec("node app.js", { + env: { + NODE_ENV: "production", + API_KEY: env.API_KEY, // Pass from Worker env + PORT: "3000", + }, +}); + +// Background processes (same syntax) +await sandbox.startProcess("python server.py", { + env: { + DATABASE_URL: env.DATABASE_URL, + SECRET_KEY: env.SECRET_KEY, + }, +}); +``` + +### Session-level variables + +Set environment variables for all commands in a session: + +```typescript +const session = await sandbox.createSession(); + +await session.setEnvVars({ + DATABASE_URL: env.DATABASE_URL, + SECRET_KEY: env.SECRET_KEY, +}); + +// All commands in this session have these vars +await session.exec("python migrate.py"); +await session.exec("python seed.py"); +``` + +## Common patterns + +### Pass Worker secrets to sandbox + +Securely pass secrets from Worker environment: + +```typescript +interface Env { + Sandbox: DurableObjectNamespace; + OPENAI_API_KEY: string; // Set with `wrangler secret put` + DATABASE_URL: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, "user-sandbox"); + + const result = await sandbox.exec("python analyze.py", { + env: { + OPENAI_API_KEY: env.OPENAI_API_KEY, + DATABASE_URL: env.DATABASE_URL, + }, + }); + + return Response.json({ result }); + }, +}; +``` + +### Default values with spreading + +Combine default and command-specific variables: + +```typescript +const defaults = { + NODE_ENV: env.ENVIRONMENT || "production", + LOG_LEVEL: "info", + TZ: "UTC", +}; + +await sandbox.exec("npm start", { + env: { + ...defaults, + PORT: "3000", // Command-specific override + API_KEY: env.API_KEY, + }, +}); +``` + +## Environment variable precedence + +When the same variable is set at multiple levels: + +1. **Command-level** (highest) - Passed to `exec()` or `startProcess()` +2. **Session-level** - Set with `setEnvVars()` +3. **Container default** - Built into the Docker image +4. **System default** (lowest) - Operating system defaults + +Example: + +```typescript +// In Dockerfile: ENV NODE_ENV=development +// In session: await sandbox.setEnvVars({ NODE_ENV: 'staging' }); + +// In command (overrides all): +await sandbox.exec("node app.js", { + env: { NODE_ENV: "production" }, // This wins +}); +``` + +## Security best practices + +### Never hardcode secrets + +**Bad** - Secrets in code: + +```typescript +await sandbox.exec("python app.py", { + env: { + API_KEY: "sk-1234567890abcdef", // NEVER DO THIS + }, +}); +``` + +**Good** - Secrets from Worker environment: + +```typescript +await sandbox.exec("python app.py", { + env: { + API_KEY: env.API_KEY, // From Wrangler secret + }, +}); +``` + +Set secrets with Wrangler: + +```bash +wrangler secret put API_KEY +``` + +## Debugging + +List all environment variables: + +```typescript +const result = await sandbox.exec("env"); +console.log(result.stdout); +``` + +Check specific variable: + +```typescript +const result = await sandbox.exec("echo $NODE_ENV"); +console.log("NODE_ENV:", result.stdout.trim()); +``` + +## Troubleshooting + +### Variable not set + +Verify the variable is being passed: + +```typescript +console.log("Worker env:", env.API_KEY ? "Set" : "Missing"); + +const result = await sandbox.exec("env | grep API_KEY", { + env: { API_KEY: env.API_KEY }, +}); +console.log("Sandbox:", result.stdout); +``` + +### Shell expansion issues + +Use runtime-specific access instead of shell variables: + +```typescript +// Instead of: await sandbox.exec('echo $NODE_ENV') +await sandbox.exec('node -e "console.log(process.env.NODE_ENV)"', { + env: { NODE_ENV: "production" }, +}); +``` + +## Related resources + +- [Wrangler configuration](/sandbox/configuration/wrangler/) - Setting Worker-level environment +- [Secrets](/workers/configuration/secrets/) - Managing sensitive data +- [Sessions API](/sandbox/api/sessions/) - Session-level environment variables +- [Security model](/sandbox/concepts/security/) - Understanding data isolation diff --git a/src/content/docs/sandbox/configuration/index.mdx b/src/content/docs/sandbox/configuration/index.mdx new file mode 100644 index 000000000000000..fc9a582aa34fe37 --- /dev/null +++ b/src/content/docs/sandbox/configuration/index.mdx @@ -0,0 +1,107 @@ +--- +title: Configuration +pcx_content_type: navigation +sidebar: + order: 7 +--- + +import { LinkTitleCard, CardGrid } from "~/components"; + +Configure your Sandbox SDK deployment with Wrangler, customize container images, and manage environment variables. + + + + + Configure Durable Objects bindings, container images, and Worker settings in wrangler.jsonc. + + + + Customize the sandbox container image with your own packages, tools, and configurations. + + + + Pass configuration and secrets to your sandboxes using environment variables. + + + + +## Quick reference + +### Essential wrangler.jsonc settings + +```jsonc +{ + "name": "my-worker", + "main": "src/index.ts", + "compatibility_date": "2024-09-02", + "compatibility_flags": ["nodejs_compat"], + "durable_objects": { + "bindings": [ + { + "name": "Sandbox", + "class_name": "Sandbox", + "script_name": "@cloudflare/sandbox" + } + ] + }, + "containers": [ + { + "binding": "CONTAINER", + "image": "ghcr.io/cloudflare/sandbox-runtime:latest" + } + ] +} +``` + +### Common Dockerfile customizations + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Install additional Python packages +RUN pip install scikit-learn tensorflow pandas + +# Install Node.js packages globally +RUN npm install -g typescript ts-node + +# Install system packages +RUN apt-get update && apt-get install -y postgresql-client + +# Add custom scripts +COPY ./scripts /usr/local/bin/ +``` + +### Environment variables + +```typescript +// Pass to sandbox at creation +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Configure environment for commands +await sandbox.exec('node app.js', { + env: { + NODE_ENV: 'production', + API_KEY: env.API_KEY, + DATABASE_URL: env.DATABASE_URL + } +}); +``` + +## Related resources + +- [Get Started guide](/sandbox/get-started/) - Initial setup walkthrough +- [Wrangler documentation](/workers/wrangler/) - Complete Wrangler reference +- [Docker documentation](https://docs.docker.com/engine/reference/builder/) - Dockerfile syntax +- [Security model](/sandbox/concepts/security/) - Understanding environment isolation diff --git a/src/content/docs/sandbox/configuration/wrangler.mdx b/src/content/docs/sandbox/configuration/wrangler.mdx new file mode 100644 index 000000000000000..91b409f6279181d --- /dev/null +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -0,0 +1,249 @@ +--- +title: Wrangler configuration +pcx_content_type: configuration +sidebar: + order: 1 +--- + +## Minimal configuration + +The minimum required configuration for using Sandbox SDK: + +```jsonc title="wrangler.jsonc" +{ + "name": "my-sandbox-worker", + "main": "src/index.ts", + "compatibility_date": "2025-10-13", + "compatibility_flags": ["nodejs_compat"], + "containers": [ + { + "class_name": "Sandbox", + "image": "docker.io/cloudflare/sandbox:0.3.3", + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox", + }, + ], + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1", + }, + ], +} +``` + +## Required settings + +### containers + +Each container is backed by its own Durable Object. The container image contains your runtime environment. + +```jsonc +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "docker.io/cloudflare/sandbox:0.3.3", + }, + ], +} +``` + +**Parameters**: + +- **class_name** (string, required) - Must match the `class_name` of the Durable Object. +- **image** (string, required) - The Docker image to use. Must match your npm package version. + +For custom images, use a Dockerfile: + +```jsonc +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + }, + ], +} +``` + +See [Dockerfile reference](/sandbox/configuration/dockerfile/) for customization. + +### durable_objects.bindings + +Bind the Sandbox Durable Object to your Worker: + +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox", + }, + ], + }, +} +``` + +**Parameters**: + +- **class_name** (string, required) - Must match the `class_name` of the container configuration. +- **name** (string, required) - The binding name you'll use in your code. Conventionally `"Sandbox"`. + +### migrations + +Required for Durable Object initialization: + +```jsonc +{ + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1", + }, + ], +} +``` + +This tells Cloudflare to initialize the Sandbox Durable Object with SQLite storage. + +## Optional settings + +These settings are illustrative and not required for basic usage. + +### Environment variables + +Pass configuration to your Worker: + +```jsonc +{ + "vars": { + "ENVIRONMENT": "production", + "LOG_LEVEL": "info", + }, +} +``` + +Access in your Worker: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + console.log(`Running in ${env.ENVIRONMENT} mode`); + // ... + }, +}; +``` + +### Secrets + +Store sensitive values securely: + +```bash +# Set secrets via CLI (never commit these) +wrangler secret put ANTHROPIC_API_KEY +wrangler secret put GITHUB_TOKEN +wrangler secret put DATABASE_URL +``` + +Access like environment variables: + +```typescript +interface Env { + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; + GITHUB_TOKEN: string; +} +``` + +### Cron triggers + +Run sandboxes on a schedule: + +```jsonc +{ + "triggers": { + "crons": ["0 0 * * *"], // Daily at midnight + }, +} +``` + +```typescript +export default { + async scheduled(event: ScheduledEvent, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, "scheduled-task"); + await sandbox.exec("python3 /workspace/daily-report.py"); + await sandbox.destroy(); + }, +}; +``` + +## Troubleshooting + +### Binding not found + +**Error**: `TypeError: env.Sandbox is undefined` + +**Solution**: Ensure your `wrangler.jsonc` includes the Durable Objects binding: + +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox", + }, + ], + }, +} +``` + +### Container image pull fails + +**Error**: `Failed to pull container image` + +**Solution**: Ensure you're using the correct image version (must match npm package): + +```jsonc +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "docker.io/cloudflare/sandbox:0.3.3", + }, + ], +} +``` + +### Missing migrations + +**Error**: Durable Object not initialized + +**Solution**: Add migrations for the Sandbox class: + +```jsonc +{ + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1", + }, + ], +} +``` + +## Related resources + +- [Wrangler documentation](/workers/wrangler/) - Complete Wrangler reference +- [Durable Objects setup](/durable-objects/get-started/) - DO-specific configuration +- [Dockerfile reference](/sandbox/configuration/dockerfile/) - Custom container images +- [Environment variables](/sandbox/configuration/environment-variables/) - Passing configuration to sandboxes +- [Get Started guide](/sandbox/get-started/) - Initial setup walkthrough diff --git a/src/content/docs/sandbox/get-started.mdx b/src/content/docs/sandbox/get-started.mdx new file mode 100644 index 000000000000000..f1d496f64d1399d --- /dev/null +++ b/src/content/docs/sandbox/get-started.mdx @@ -0,0 +1,212 @@ +--- +title: Getting started +pcx_content_type: get-started +sidebar: + order: 2 +--- + +import { Render, PackageManagers, Steps, WranglerConfig } from "~/components"; + +Build your first application with Sandbox SDK - a secure code execution environment. In this guide, you'll create a Worker that can execute Python code and work with files in isolated containers. + +:::note[What you're building] +A simple API that can safely execute Python code and perform file operations in isolated sandbox environments. +::: + +## Prerequisites + + + +### Ensure Docker is running locally + +Sandbox SDK uses [Docker](https://www.docker.com/) to build container images alongside your Worker. Docker must be running when you deploy or run locally. + +Install Docker by following the [Docker Desktop installation guide](https://docs.docker.com/desktop/). + +Verify Docker is running: + +```sh +docker info +``` + +If Docker is not running, this command will hang or return "Cannot connect to the Docker daemon". + +## 1. Create a new project + +Create a new Sandbox SDK project: + + + +This creates a `my-sandbox` directory with everything you need: + +- `src/index.ts` - Worker with sandbox integration +- `wrangler.jsonc` - Configuration for Workers and Containers +- `Dockerfile` - Container environment definition + +```sh +cd my-sandbox +``` + +## 2. Explore the template + +The template provides a minimal Worker that demonstrates core sandbox capabilities: + +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; + +export { Sandbox } from '@cloudflare/sandbox'; + +type Env = { + Sandbox: DurableObjectNamespace; +}; + +export default { + async fetch(request: Request, env: Env): Promise { + // Required for preview URLs (if you expose ports later) + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; + + const url = new URL(request.url); + + // Get or create a sandbox instance + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + + // Execute Python code + if (url.pathname === '/run') { + const result = await sandbox.exec('python -c "print(2 + 2)"'); + return Response.json({ + output: result.stdout, + success: result.success + }); + } + + // Work with files + if (url.pathname === '/file') { + await sandbox.writeFile('/workspace/hello.txt', 'Hello, Sandbox!'); + const file = await sandbox.readFile('/workspace/hello.txt'); + return Response.json({ + content: file.content + }); + } + + return new Response('Try /run or /file'); + }, +}; +``` + +**Key concepts**: + +- `getSandbox()` - Gets or creates a sandbox instance by ID +- `proxyToSandbox()` - Required at the top for preview URLs to work +- `sandbox.exec()` - Execute commands and capture output +- `sandbox.writeFile()` / `readFile()` - File operations + +## 3. Test locally + +Start the development server: + +```sh +npm run dev +``` + +:::note +First run builds the Docker container (2-3 minutes). Subsequent runs are much faster due to caching. +::: + +Test the endpoints: + +```sh +# Execute Python code +curl http://localhost:8787/run + +# File operations +curl http://localhost:8787/file +``` + +You should see JSON responses with the command output and file contents. + +## 4. Deploy to production + +Deploy your Worker and container: + +```sh +npx wrangler deploy +``` + +This will: +1. Build your container image using Docker +2. Push it to Cloudflare's Container Registry +3. Deploy your Worker globally + +:::caution[Wait for provisioning] +After first deployment, wait 2-3 minutes before making requests. The Worker deploys immediately, but the container needs time to provision. +::: + +Check deployment status: + +```sh +npx wrangler containers list +``` + +## 5. Test your deployment + +Visit your Worker URL (shown in deploy output): + +```sh +# Replace with your actual URL +curl https://my-sandbox.YOUR_SUBDOMAIN.workers.dev/run +``` + +Your sandbox is now deployed. + +## Understanding the configuration + +Your `wrangler.jsonc` connects three pieces together: + + + +```jsonc +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile" + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1" + } + ] +} +``` + + + +- **containers** - Your Dockerfile defines the execution environment +- **durable_objects** - Makes the `Sandbox` binding available in your Worker +- **migrations** - Initializes Durable Object storage (required once) + +For detailed configuration options including environment variables, secrets, and custom images, see the [Wrangler configuration reference](/sandbox/configuration/wrangler/). + +## Next steps + +Now that you have a working sandbox, explore more capabilities: + +- [Execute commands](/sandbox/guides/execute-commands/) - Run shell commands and stream output +- [Manage files](/sandbox/guides/manage-files/) - Work with files and directories +- [Expose services](/sandbox/guides/expose-services/) - Get public URLs for services running in your sandbox +- [API reference](/sandbox/api/) - Complete API documentation diff --git a/src/content/docs/sandbox/guides/background-processes.mdx b/src/content/docs/sandbox/guides/background-processes.mdx new file mode 100644 index 000000000000000..eabf973ebbbf997 --- /dev/null +++ b/src/content/docs/sandbox/guides/background-processes.mdx @@ -0,0 +1,204 @@ +--- +title: Run background processes +pcx_content_type: how-to +sidebar: + order: 3 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to start, monitor, and manage long-running background processes in the sandbox. + +## When to use background processes + +Use `startProcess()` instead of `exec()` when: + +- **Running web servers** - HTTP servers, APIs, WebSocket servers +- **Long-running services** - Database servers, caches, message queues +- **Development servers** - Hot-reloading dev servers, watch modes +- **Continuous monitoring** - Log watchers, health checkers +- **Parallel execution** - Multiple services running simultaneously + +Use `exec()` for: + +- **One-time commands** - Installations, builds, data processing +- **Quick scripts** - Simple operations that complete and exit + +## Start a background process + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Start a web server +const server = await sandbox.startProcess('python -m http.server 8000'); + +console.log('Server started'); +console.log('Process ID:', server.id); +console.log('PID:', server.pid); +console.log('Status:', server.status); // 'running' + +// Process runs in background - your code continues +``` + + +## Configure process environment + +Set working directory and environment variables: + + +``` +const process = await sandbox.startProcess('node server.js', { + cwd: '/workspace/api', + env: { + NODE_ENV: 'production', + PORT: '8080', + API_KEY: env.API_KEY, + DATABASE_URL: env.DATABASE_URL + } +}); + +console.log('API server started'); +``` + + +## Monitor process status + +List and check running processes: + + +``` +const processes = await sandbox.listProcesses(); + +console.log(`Running ${processes.length} processes:`); + +for (const proc of processes) { + console.log(`${proc.id}: ${proc.command} (${proc.status})`); +} + +// Check if specific process is running +const isRunning = processes.some(p => p.id === processId && p.status === 'running'); +``` + + +## Monitor process logs + +Stream logs in real-time to detect when a service is ready: + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +const server = await sandbox.startProcess('node server.js'); + +// Stream logs +const logStream = await sandbox.streamProcessLogs(server.id); + +for await (const log of parseSSEStream(logStream)) { + console.log(log.data); + + if (log.data.includes('Server listening')) { + console.log('Server is ready!'); + break; + } +} +``` + + +Or get accumulated logs: + + +``` +const logs = await sandbox.getProcessLogs(server.id); +console.log('Logs:', logs); +``` + + +## Stop processes + + +``` +// Stop specific process +await sandbox.killProcess(server.id); + +// Force kill if needed +await sandbox.killProcess(server.id, 'SIGKILL'); + +// Stop all processes +await sandbox.killAllProcesses(); +``` + + +## Run multiple processes + +Start services in sequence, waiting for dependencies: + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +// Start database first +const db = await sandbox.startProcess('redis-server'); + +// Wait for database to be ready +const dbLogs = await sandbox.streamProcessLogs(db.id); +for await (const log of parseSSEStream(dbLogs)) { + if (log.data.includes('Ready to accept connections')) { + break; + } +} + +// Now start API server (depends on database) +const api = await sandbox.startProcess('node api-server.js', { + env: { DATABASE_URL: 'redis://localhost:6379' } +}); + +console.log('All services running'); +``` + + +## Best practices + +- **Wait for readiness** - Stream logs to detect when services are ready +- **Clean up** - Always stop processes when done +- **Handle failures** - Monitor logs for errors and restart if needed +- **Use try/finally** - Ensure cleanup happens even on errors + +## Troubleshooting + +### Process exits immediately + +Check logs to see why: + + +``` +const process = await sandbox.startProcess('node server.js'); +await new Promise(resolve => setTimeout(resolve, 1000)); + +const processes = await sandbox.listProcesses(); +if (!processes.find(p => p.id === process.id)) { + const logs = await sandbox.getProcessLogs(process.id); + console.error('Process exited:', logs); +} +``` + + +### Port already in use + +Kill existing processes before starting: + + +``` +await sandbox.killAllProcesses(); +const server = await sandbox.startProcess('node server.js'); +``` + + +## Related resources + +- [Commands API reference](/sandbox/api/commands/) - Complete process management API +- [Execute commands guide](/sandbox/guides/execute-commands/) - One-time command execution +- [Expose services guide](/sandbox/guides/expose-services/) - Make processes accessible +- [Streaming output guide](/sandbox/guides/streaming-output/) - Monitor process output diff --git a/src/content/docs/sandbox/guides/code-execution.mdx b/src/content/docs/sandbox/guides/code-execution.mdx new file mode 100644 index 000000000000000..85945f6c037d6e8 --- /dev/null +++ b/src/content/docs/sandbox/guides/code-execution.mdx @@ -0,0 +1,262 @@ +--- +title: Use code interpreter +pcx_content_type: how-to +sidebar: + order: 5 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to execute Python and JavaScript code with rich outputs using the Code Interpreter API. + +## When to use code interpreter + +Use the Code Interpreter API for **simple, direct code execution** with minimal setup: + +- **Quick code execution** - Run Python/JS code without environment setup +- **Rich outputs** - Get charts, tables, images, HTML automatically +- **AI-generated code** - Execute LLM-generated code with structured results +- **Persistent state** - Variables preserved between executions in the same context + +Use `exec()` for **advanced or custom workflows**: + +- **System operations** - Install packages, manage files, run builds +- **Custom environments** - Configure specific versions, dependencies +- **Shell commands** - Git operations, system utilities, complex pipelines +- **Long-running processes** - Background services, servers + +## Create an execution context + +Code contexts maintain state between executions: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Create a Python context +const pythonContext = await sandbox.createCodeContext({ + language: 'python' +}); + +console.log('Context ID:', pythonContext.id); +console.log('Language:', pythonContext.language); + +// Create a JavaScript context +const jsContext = await sandbox.createCodeContext({ + language: 'javascript' +}); +``` + + +## Execute code + +### Simple execution + + +``` +// Create context +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +// Execute code +const result = await sandbox.runCode(context.id, ` +print("Hello from Code Interpreter!") +result = 2 + 2 +print(f"2 + 2 = {result}") +`); + +console.log('Output:', result.output); +console.log('Success:', result.success); +``` + + +### Persistent state + +Variables and imports persist between executions in the same context: + + +``` +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +// First execution - import and define variables +await sandbox.runCode(context.id, ` +import pandas as pd +import numpy as np + +data = [1, 2, 3, 4, 5] +print("Data initialized") +`); + +// Second execution - use previously defined variables +const result = await sandbox.runCode(context.id, ` +mean = np.mean(data) +print(f"Mean: {mean}") +`); + +console.log(result.output); // "Mean: 3.0" +``` + + +## Handle rich outputs + +The code interpreter returns multiple output formats: + + +``` +const result = await sandbox.runCode(context.id, ` +import matplotlib.pyplot as plt + +plt.plot([1, 2, 3], [1, 4, 9]) +plt.title('Simple Chart') +plt.show() +`); + +// Check available formats +console.log('Formats:', result.formats); // ['text', 'png'] + +// Access outputs +if (result.outputs.png) { + // Return as image + return new Response(atob(result.outputs.png), { + headers: { 'Content-Type': 'image/png' } + }); +} + +if (result.outputs.html) { + // Return as HTML (pandas DataFrames) + return new Response(result.outputs.html, { + headers: { 'Content-Type': 'text/html' } + }); +} + +if (result.outputs.json) { + // Return as JSON + return Response.json(result.outputs.json); +} +``` + + +## Stream execution output + +For long-running code, stream output in real-time: + + +``` +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +const result = await sandbox.runCode( + context.id, + ` +import time + +for i in range(10): + print(f"Processing item {i+1}/10...") + time.sleep(0.5) + +print("Done!") +`, + { + stream: true, + onOutput: (data) => { + console.log('Output:', data); + }, + onResult: (result) => { + console.log('Result:', result); + }, + onError: (error) => { + console.error('Error:', error); + } + } +); +``` + + +## Execute AI-generated code + +Run LLM-generated code safely in a sandbox: + + +``` +// 1. Generate code with Claude +const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 1024, + messages: [{ + role: 'user', + content: 'Write Python code to calculate fibonacci sequence up to 100' + }] + }) +}); + +const { content } = await response.json(); +const code = content[0].text; + +// 2. Execute in sandbox +const context = await sandbox.createCodeContext({ language: 'python' }); +const result = await sandbox.runCode(context.id, code); + +console.log('Generated code:', code); +console.log('Output:', result.output); +console.log('Success:', result.success); +``` + + +## Manage contexts + +### List all contexts + + +``` +const contexts = await sandbox.listCodeContexts(); + +console.log(`${contexts.length} active contexts:`); + +for (const ctx of contexts) { + console.log(` ${ctx.id} (${ctx.language})`); +} +``` + + +### Delete contexts + + +``` +// Delete specific context +await sandbox.deleteCodeContext(context.id); +console.log('Context deleted'); + +// Clean up all contexts +const contexts = await sandbox.listCodeContexts(); +for (const ctx of contexts) { + await sandbox.deleteCodeContext(ctx.id); +} +console.log('All contexts deleted'); +``` + + +## Best practices + +- **Clean up contexts** - Delete contexts when done to free resources +- **Handle errors** - Always check `result.success` and `result.error` +- **Stream long operations** - Use streaming for code that takes >2 seconds +- **Validate AI code** - Review generated code before execution + +## Related resources + +- [Code Interpreter API reference](/sandbox/api/interpreter/) - Complete API documentation +- [AI code executor tutorial](/sandbox/tutorials/ai-code-executor/) - Build complete AI executor +- [Execute commands guide](/sandbox/guides/execute-commands/) - Lower-level command execution diff --git a/src/content/docs/sandbox/guides/execute-commands.mdx b/src/content/docs/sandbox/guides/execute-commands.mdx new file mode 100644 index 000000000000000..8564ae320474638 --- /dev/null +++ b/src/content/docs/sandbox/guides/execute-commands.mdx @@ -0,0 +1,171 @@ +--- +title: Execute commands +pcx_content_type: how-to +sidebar: + order: 1 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to execute commands in the sandbox, handle output, and manage errors effectively. + +## Choose the right method + +The SDK provides two methods for command execution: + +- **`exec()`** - Run a command and wait for complete result. Best for most use cases. +- **`execStream()`** - Stream output in real-time. Best for long-running commands where you need immediate feedback. + +## Execute basic commands + +Use `exec()` for simple commands that complete quickly: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Execute a single command +const result = await sandbox.exec('python --version'); + +console.log(result.stdout); // "Python 3.11.0" +console.log(result.exitCode); // 0 +console.log(result.success); // true +``` + + +## Pass arguments safely + +When passing user input or dynamic values, avoid string interpolation to prevent injection attacks: + + +``` +// Unsafe - vulnerable to injection +const filename = userInput; +await sandbox.exec(`cat ${filename}`); + +// Safe - use proper escaping or validation +const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, ''); +await sandbox.exec(`cat ${safeFilename}`); + +// Better - write to file and execute +await sandbox.writeFile('/tmp/input.txt', userInput); +await sandbox.exec('python process.py /tmp/input.txt'); +``` + + +## Handle errors + +Commands can fail in two ways: + +1. **Non-zero exit code** - Command ran but failed (result.success === false) +2. **Execution error** - Command couldn't start (throws exception) + + +``` +try { + const result = await sandbox.exec('python analyze.py'); + + if (!result.success) { + // Command failed (non-zero exit code) + console.error('Analysis failed:', result.stderr); + console.log('Exit code:', result.exitCode); + + // Handle specific exit codes + if (result.exitCode === 1) { + throw new Error('Invalid input data'); + } else if (result.exitCode === 2) { + throw new Error('Missing dependencies'); + } + } + + // Success - process output + return JSON.parse(result.stdout); + +} catch (error) { + // Execution error (couldn't start command) + console.error('Execution failed:', error.message); + throw error; +} +``` + + +## Execute shell commands + +The sandbox supports shell features like pipes, redirects, and chaining: + + +``` +// Pipes and filters +const result = await sandbox.exec('ls -la | grep ".py" | wc -l'); +console.log('Python files:', result.stdout.trim()); + +// Output redirection +await sandbox.exec('python generate.py > output.txt 2> errors.txt'); + +// Multiple commands +await sandbox.exec('cd /workspace && npm install && npm test'); +``` + + +## Execute Python scripts + + +``` +// Run inline Python +const result = await sandbox.exec('python -c "print(sum([1, 2, 3, 4, 5]))"'); +console.log('Sum:', result.stdout.trim()); // "15" + +// Run a script file +await sandbox.writeFile('/workspace/analyze.py', ` +import sys +print(f"Argument: {sys.argv[1]}") +`); + +await sandbox.exec('python /workspace/analyze.py data.csv'); +``` + + +## Best practices + +- **Check exit codes** - Always verify `result.success` and `result.exitCode` +- **Validate inputs** - Escape or validate user input to prevent injection +- **Use streaming** - For long operations, use `execStream()` for real-time feedback +- **Handle errors** - Check stderr for error details + +## Troubleshooting + +### Command not found + +Verify the command exists in the container: + + +``` +const check = await sandbox.exec('which python3'); +if (!check.success) { + console.error('python3 not found'); +} +``` + + +### Working directory issues + +Use absolute paths or change directory: + + +``` +// Use absolute path +await sandbox.exec('python /workspace/my-app/script.py'); + +// Or change directory +await sandbox.exec('cd /workspace/my-app && python script.py'); +``` + + +## Related resources + +- [Commands API reference](/sandbox/api/commands/) - Complete method documentation +- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running processes +- [Streaming output guide](/sandbox/guides/streaming-output/) - Advanced streaming patterns +- [Code Interpreter guide](/sandbox/guides/code-execution/) - Higher-level code execution diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx new file mode 100644 index 000000000000000..713ba3ff6c3d2e6 --- /dev/null +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -0,0 +1,271 @@ +--- +title: Expose services +pcx_content_type: how-to +sidebar: + order: 4 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to expose services running in your sandbox to the internet via preview URLs. + +## When to expose ports + +Expose ports when you need to: + +- **Test web applications** - Preview frontend or backend apps +- **Share demos** - Give others access to running applications +- **Develop APIs** - Test endpoints from external tools +- **Debug services** - Access internal services for troubleshooting +- **Build dev environments** - Create shareable development workspaces + +## Basic port exposure + +The typical workflow is: start service → wait for ready → expose port → handle requests with `proxyToSandbox`. + + +``` +import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// 1. Start a web server +await sandbox.startProcess('python -m http.server 8000'); + +// 2. Wait for service to start +await new Promise(resolve => setTimeout(resolve, 2000)); + +// 3. Expose the port +const exposed = await sandbox.exposePort(8000); + +// 4. Preview URL is now available (public by default) +console.log('Server accessible at:', exposed.exposedAt); +// Returns: https://abc123-8000.sandbox.workers.dev + +// 5. Handle preview URL requests in your Worker +export default { + async fetch(request: Request, env: Env): Promise { + // Proxy requests to the exposed port + return proxyToSandbox(request, env.Sandbox, 'my-sandbox'); + } +}; +``` + + +:::caution +**Preview URLs are public by default.** Anyone with the URL can access your service. Add authentication if needed. +::: + +## Name your exposed ports + +When exposing multiple ports, use names to stay organized: + + +``` +// Start and expose API server +await sandbox.startProcess('node api.js', { env: { PORT: '8080' } }); +await new Promise(resolve => setTimeout(resolve, 2000)); +const api = await sandbox.exposePort(8080, { name: 'api' }); + +// Start and expose frontend +await sandbox.startProcess('npm run dev', { env: { PORT: '5173' } }); +await new Promise(resolve => setTimeout(resolve, 2000)); +const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); + +console.log('Services:'); +console.log('- API:', api.exposedAt); +console.log('- Frontend:', frontend.exposedAt); +``` + + +## Wait for service readiness + +Always verify a service is ready before exposing. Use a simple delay for most cases: + + +``` +// Start service +await sandbox.startProcess('npm run dev', { env: { PORT: '8080' } }); + +// Wait 2-3 seconds +await new Promise(resolve => setTimeout(resolve, 2000)); + +// Now expose +await sandbox.exposePort(8080); +``` + + +For critical services, poll the health endpoint: + + +``` +await sandbox.startProcess('node api-server.js', { env: { PORT: '8080' } }); + +// Wait for health check +for (let i = 0; i < 10; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + + const check = await sandbox.exec('curl -f http://localhost:8080/health || echo "not ready"'); + if (check.stdout.includes('ok')) { + break; + } +} + +await sandbox.exposePort(8080); +``` + + +## Multiple services + +Expose multiple ports for full-stack applications: + + +``` +// Start backend +await sandbox.startProcess('node api/server.js', { + env: { PORT: '8080' } +}); +await new Promise(resolve => setTimeout(resolve, 2000)); + +// Start frontend +await sandbox.startProcess('npm run dev', { + cwd: '/workspace/frontend', + env: { PORT: '5173', API_URL: 'http://localhost:8080' } +}); +await new Promise(resolve => setTimeout(resolve, 3000)); + +// Expose both +const api = await sandbox.exposePort(8080, { name: 'api' }); +const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); + +return Response.json({ + api: api.exposedAt, + frontend: frontend.exposedAt +}); +``` + + +## Manage exposed ports + +### List currently exposed ports + + +``` +const { ports, count } = await sandbox.getExposedPorts(); + +console.log(`${count} ports currently exposed:`); + +for (const port of ports) { + console.log(` Port ${port.port}: ${port.exposedAt}`); + if (port.name) { + console.log(` Name: ${port.name}`); + } +} +``` + + +### Unexpose ports + + +``` +// Unexpose a single port +await sandbox.unexposePort(8000); + +// Unexpose multiple ports +for (const port of [3000, 5173, 8080]) { + await sandbox.unexposePort(port); +} +``` + + +## Best practices + +- **Wait for readiness** - Don't expose ports immediately after starting processes +- **Use named ports** - Easier to track when exposing multiple ports +- **Clean up** - Unexpose ports when done to prevent abandoned URLs +- **Add authentication** - Preview URLs are public; protect sensitive services + +## Local development + +When developing locally with `wrangler dev`, you must expose ports in your Dockerfile: + +```dockerfile title="Dockerfile" +FROM docker.io/cloudflare/sandbox:0.3.3 + +# Expose ports you plan to use +EXPOSE 8000 +EXPOSE 8080 +EXPOSE 5173 +``` + +Update `wrangler.jsonc` to use your Dockerfile: + +```jsonc title="wrangler.jsonc" +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile" + } + ] +} +``` + +In production, all ports are available and controlled programmatically via `exposePort()` / `unexposePort()`. + +## Troubleshooting + +### Port 3000 is reserved + +Port 3000 is used by the internal Bun server and cannot be exposed: + + +``` +// ❌ This will fail +await sandbox.exposePort(3000); // Error: Port 3000 is reserved + +// ✅ Use a different port +await sandbox.startProcess('node server.js', { env: { PORT: '8080' } }); +await sandbox.exposePort(8080); +``` + + +### Port not ready + +Wait for the service to start before exposing: + + +``` +await sandbox.startProcess('npm run dev'); +await new Promise(resolve => setTimeout(resolve, 3000)); +await sandbox.exposePort(8080); +``` + + +### Port already exposed + +Check before exposing to avoid errors: + + +``` +const { ports } = await sandbox.getExposedPorts(); +if (!ports.some(p => p.port === 8080)) { + await sandbox.exposePort(8080); +} +``` + + +## Preview URL format + +Preview URLs follow the pattern `https://{sandbox-id}-{port}.sandbox.workers.dev`: + +- Port 8080: `https://abc123-8080.sandbox.workers.dev` +- Port 5173: `https://abc123-5173.sandbox.workers.dev` + +**Note**: Port 3000 is reserved for the internal Bun server and cannot be exposed. + +## Related resources + +- [Ports API reference](/sandbox/api/ports/) - Complete port exposure API +- [Background processes guide](/sandbox/guides/background-processes/) - Managing services +- [Execute commands guide](/sandbox/guides/execute-commands/) - Starting services diff --git a/src/content/docs/sandbox/guides/git-workflows.mdx b/src/content/docs/sandbox/guides/git-workflows.mdx new file mode 100644 index 000000000000000..87656296362a795 --- /dev/null +++ b/src/content/docs/sandbox/guides/git-workflows.mdx @@ -0,0 +1,142 @@ +--- +title: Work with Git +pcx_content_type: how-to +sidebar: + order: 6 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to clone repositories, manage branches, and automate Git operations in the sandbox. + +## Clone repositories + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Basic clone +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Clone specific branch +await sandbox.gitCheckout('https://github.com/user/repo', { + branch: 'develop' +}); + +// Shallow clone (faster for large repos) +await sandbox.gitCheckout('https://github.com/user/large-repo', { + depth: 1 +}); + +// Clone to specific directory +await sandbox.gitCheckout('https://github.com/user/my-app', { + targetDir: '/workspace/project' +}); +``` + + +## Clone private repositories + +Use a personal access token in the URL: + + +``` +const token = env.GITHUB_TOKEN; +const repoUrl = `https://${token}@github.com/user/private-repo.git`; + +await sandbox.gitCheckout(repoUrl); +``` + + +## Clone and build + +Clone a repository and run build steps: + + +``` +await sandbox.gitCheckout('https://github.com/user/my-app'); + +const repoName = 'my-app'; + +// Install and build +await sandbox.exec(`cd ${repoName} && npm install`); +await sandbox.exec(`cd ${repoName} && npm run build`); + +console.log('Build complete'); +``` + + +## Work with branches + + +``` +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Switch branches +await sandbox.exec('cd repo && git checkout feature-branch'); + +// Create new branch +await sandbox.exec('cd repo && git checkout -b new-feature'); +``` + + +## Make changes and commit + + +``` +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Modify a file +const readme = await sandbox.readFile('/workspace/repo/README.md'); +await sandbox.writeFile('/workspace/repo/README.md', readme.content + '\n\n## New Section'); + +// Commit changes +await sandbox.exec('cd repo && git config user.name "Sandbox Bot"'); +await sandbox.exec('cd repo && git config user.email "bot@example.com"'); +await sandbox.exec('cd repo && git add README.md'); +await sandbox.exec('cd repo && git commit -m "Update README"'); +``` + + +## Best practices + +- **Use shallow clones** - Faster for large repos with `depth: 1` +- **Store credentials securely** - Use environment variables for tokens +- **Clean up** - Delete unused repositories to save space + +## Troubleshooting + +### Authentication fails + +Verify your token is set: + + +``` +if (!env.GITHUB_TOKEN) { + throw new Error('GITHUB_TOKEN not configured'); +} + +const repoUrl = `https://${env.GITHUB_TOKEN}@github.com/user/private-repo.git`; +await sandbox.gitCheckout(repoUrl); +``` + + +### Large repository timeout + +Use shallow clone: + + +``` +await sandbox.gitCheckout('https://github.com/user/large-repo', { + depth: 1 +}); +``` + + +## Related resources + +- [Files API reference](/sandbox/api/files/) - File operations after cloning +- [Execute commands guide](/sandbox/guides/execute-commands/) - Run git commands +- [Manage files guide](/sandbox/guides/manage-files/) - Work with cloned files diff --git a/src/content/docs/sandbox/guides/index.mdx b/src/content/docs/sandbox/guides/index.mdx new file mode 100644 index 000000000000000..81c700f7ebb6d12 --- /dev/null +++ b/src/content/docs/sandbox/guides/index.mdx @@ -0,0 +1,23 @@ +--- +title: How-to guides +pcx_content_type: navigation +sidebar: + order: 4 +--- + +These guides show you how to solve specific problems and implement features with the Sandbox SDK. Each guide focuses on a particular task and provides practical, production-ready solutions. + +## Available guides + +- [Execute commands](/sandbox/guides/execute-commands/) - Run commands with streaming output, error handling, and shell access +- [Manage files](/sandbox/guides/manage-files/) - Read, write, organize, and synchronize files in the sandbox +- [Run background processes](/sandbox/guides/background-processes/) - Start and manage long-running services and applications +- [Expose services](/sandbox/guides/expose-services/) - Create preview URLs and expose ports for web services +- [Use code interpreter](/sandbox/guides/code-execution/) - Execute Python and JavaScript code with rich outputs +- [Work with Git](/sandbox/guides/git-workflows/) - Clone repositories, manage branches, and automate Git operations +- [Stream output](/sandbox/guides/streaming-output/) - Handle real-time output from commands and processes + +## Related resources + +- [Tutorials](/sandbox/tutorials/) - Step-by-step learning paths +- [API reference](/sandbox/api/) - Complete method documentation diff --git a/src/content/docs/sandbox/guides/manage-files.mdx b/src/content/docs/sandbox/guides/manage-files.mdx new file mode 100644 index 000000000000000..69b1bd40d972860 --- /dev/null +++ b/src/content/docs/sandbox/guides/manage-files.mdx @@ -0,0 +1,175 @@ +--- +title: Manage files +pcx_content_type: how-to +sidebar: + order: 2 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to read, write, organize, and synchronize files in the sandbox filesystem. + +## Path conventions + +File operations support both absolute and relative paths: + +- `/workspace` - Default working directory for application files +- `/tmp` - Temporary files (may be cleared) +- `/home` - User home directory + + +``` +// Absolute paths +await sandbox.writeFile('/workspace/app.js', code); + +// Relative paths (session-aware) +const session = await sandbox.createSession(); +await session.exec('cd /workspace/my-project'); +await session.writeFile('app.js', code); // Writes to /workspace/my-project/app.js +await session.writeFile('src/index.js', code); // Writes to /workspace/my-project/src/index.js +``` + + +## Write files + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Write text file +await sandbox.writeFile('/workspace/app.js', `console.log('Hello from sandbox!');`); + +// Write JSON +const config = { name: 'my-app', version: '1.0.0' }; +await sandbox.writeFile('/workspace/config.json', JSON.stringify(config, null, 2)); + +// Write binary file (base64) +const buffer = await fetch(imageUrl).then(r => r.arrayBuffer()); +const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer))); +await sandbox.writeFile('/workspace/image.png', base64, { encoding: 'base64' }); +``` + + +## Read files + + +``` +// Read text file +const file = await sandbox.readFile('/workspace/app.js'); +console.log(file.content); + +// Read and parse JSON +const configFile = await sandbox.readFile('/workspace/config.json'); +const config = JSON.parse(configFile.content); + +// Read binary file +const imageFile = await sandbox.readFile('/workspace/image.png', { encoding: 'base64' }); +return new Response(atob(imageFile.content), { + headers: { 'Content-Type': 'image/png' } +}); +``` + + +## Organize files + + +``` +// Create directories +await sandbox.mkdir('/workspace/src', { recursive: true }); +await sandbox.mkdir('/workspace/tests', { recursive: true }); + +// Rename file +await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt'); + +// Move file +await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt'); + +// Delete file +await sandbox.deleteFile('/workspace/temp.txt'); +``` + + +## Batch operations + +Write multiple files in parallel: + + +``` +const files = { + '/workspace/src/app.js': 'console.log("app");', + '/workspace/src/utils.js': 'console.log("utils");', + '/workspace/README.md': '# My Project' +}; + +await Promise.all( + Object.entries(files).map(([path, content]) => + sandbox.writeFile(path, content) + ) +); +``` + + +## Check if file exists + + +``` +try { + await sandbox.readFile('/workspace/config.json'); + console.log('File exists'); +} catch (error) { + if (error.code === 'FILE_NOT_FOUND') { + // Create default config + await sandbox.writeFile('/workspace/config.json', '{}'); + } +} +``` + + +## Best practices + +- **Use `/workspace`** - Default working directory for app files +- **Use absolute paths** - Always use full paths like `/workspace/file.txt` +- **Batch operations** - Use `Promise.all()` for multiple independent file writes +- **Create parent directories** - Use `recursive: true` when creating nested paths +- **Handle errors** - Check for `FILE_NOT_FOUND` errors gracefully + +## Troubleshooting + +### Directory doesn't exist + +Create parent directories first: + + +``` +// Create directory, then write file +await sandbox.mkdir('/workspace/data', { recursive: true }); +await sandbox.writeFile('/workspace/data/file.txt', content); +``` + + +### Binary file encoding + +Use base64 for binary files: + + +``` +// Write binary +await sandbox.writeFile('/workspace/image.png', base64Data, { + encoding: 'base64' +}); + +// Read binary +const file = await sandbox.readFile('/workspace/image.png', { + encoding: 'base64' +}); +``` + + +## Related resources + +- [Files API reference](/sandbox/api/files/) - Complete method documentation +- [Execute commands guide](/sandbox/guides/execute-commands/) - Run file operations with commands +- [Git workflows guide](/sandbox/guides/git-workflows/) - Clone and manage repositories +- [Code Interpreter guide](/sandbox/guides/code-execution/) - Generate and execute code files diff --git a/src/content/docs/sandbox/guides/streaming-output.mdx b/src/content/docs/sandbox/guides/streaming-output.mdx new file mode 100644 index 000000000000000..d1ac81f09e6b380 --- /dev/null +++ b/src/content/docs/sandbox/guides/streaming-output.mdx @@ -0,0 +1,167 @@ +--- +title: Stream output +pcx_content_type: how-to +sidebar: + order: 7 +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to handle real-time output from commands, processes, and code execution. + +## When to use streaming + +Use streaming when you need: + +- **Real-time feedback** - Show progress as it happens +- **Long-running operations** - Builds, tests, installations that take time +- **Interactive applications** - Chat bots, code execution, live demos +- **Large output** - Process output incrementally instead of all at once +- **User experience** - Prevent users from waiting with no feedback + +Use non-streaming (`exec()`) for: + +- **Quick operations** - Commands that complete in seconds +- **Small output** - When output fits easily in memory +- **Post-processing** - When you need complete output before processing + +## Stream command execution + +Use `execStream()` to get real-time output: + + +``` +import { getSandbox, parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +const stream = await sandbox.execStream('npm run build'); + +for await (const event of parseSSEStream(stream)) { + switch (event.type) { + case 'stdout': + console.log(event.data); + break; + + case 'stderr': + console.error(event.data); + break; + + case 'complete': + console.log('Exit code:', event.exitCode); + break; + + case 'error': + console.error('Failed:', event.error); + break; + } +} +``` + + +## Stream to client + +Return streaming output to users via Server-Sent Events: + + +``` +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'builder'); + + const stream = await sandbox.execStream('npm run build'); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache' + } + }); + } +}; +``` + + +Client-side consumption: + + +``` +// Browser JavaScript +const eventSource = new EventSource('/build'); + +eventSource.addEventListener('stdout', (event) => { + const data = JSON.parse(event.data); + console.log(data.data); +}); + +eventSource.addEventListener('complete', (event) => { + const data = JSON.parse(event.data); + console.log('Exit code:', data.exitCode); + eventSource.close(); +}); +``` + + +## Stream process logs + +Monitor background process output: + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +const process = await sandbox.startProcess('node server.js'); + +const logStream = await sandbox.streamProcessLogs(process.id); + +for await (const log of parseSSEStream(logStream)) { + console.log(log.data); + + if (log.data.includes('Server listening')) { + console.log('Server is ready'); + break; + } +} +``` + + +## Handle errors + +Check exit codes and handle stream errors: + + +``` +const stream = await sandbox.execStream('npm run build'); + +for await (const event of parseSSEStream(stream)) { + switch (event.type) { + case 'stdout': + console.log(event.data); + break; + + case 'error': + throw new Error(`Build failed: ${event.error}`); + + case 'complete': + if (event.exitCode !== 0) { + throw new Error(`Build failed with exit code ${event.exitCode}`); + } + break; + } +} +``` + + +## Best practices + +- **Always consume streams** - Don't let streams hang unconsumed +- **Handle all event types** - Process stdout, stderr, complete, and error events +- **Check exit codes** - Non-zero exit codes indicate failure +- **Provide feedback** - Show progress to users for long operations + +## Related resources + +- [Commands API reference](/sandbox/api/commands/) - Complete streaming API +- [Execute commands guide](/sandbox/guides/execute-commands/) - Command execution patterns +- [Background processes guide](/sandbox/guides/background-processes/) - Process log streaming +- [Code Interpreter guide](/sandbox/guides/code-execution/) - Stream code execution output diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx new file mode 100644 index 000000000000000..b7223eb27544b60 --- /dev/null +++ b/src/content/docs/sandbox/index.mdx @@ -0,0 +1,241 @@ +--- +title: Sandbox SDK (Beta) +order: 0 + +pcx_content_type: overview +sidebar: + order: 1 + badge: + text: Beta + group: + label: sandbox +head: + - tag: title + content: Overview +--- + +import { + CardGrid, + Description, + Feature, + LinkTitleCard, + Plan, + RelatedProduct, + LinkButton, + Tabs, + TabItem, +} from "~/components"; + + + +Build secure, isolated code execution environments + + + + + +The Sandbox SDK enables you to run untrusted code securely in isolated environments. Built on [Containers](/containers/), Sandbox SDK provides a simple API for executing commands, managing files, running background processes, and exposing services — all from your [Workers](/workers/) applications. + +Sandboxes are ideal for building AI agents that need to execute code, interactive development environments, data analysis platforms, CI/CD systems, and any application that needs secure code execution at the edge. Each sandbox runs in its own isolated container with a full Linux environment, providing strong security boundaries while maintaining performance. + +With Sandbox, you can execute Python scripts, run Node.js applications, analyze data, compile code, and perform complex computations — all with a simple TypeScript API and no infrastructure to manage. + + + + ```typescript + import { getSandbox } from '@cloudflare/sandbox'; + + export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // Execute a command and get the result + const result = await sandbox.exec('python --version'); + + return Response.json({ + output: result.stdout, + exitCode: result.exitCode, + success: result.success + }); + } + }; + ``` + + + ```typescript + import { getSandbox } from '@cloudflare/sandbox'; + + export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // Create a Python execution context + const ctx = await sandbox.createCodeContext({ language: 'python' }); + + // Execute Python code with automatic result capture + const result = await sandbox.runCode(` +import pandas as pd +data = {'product': ['A', 'B', 'C'], 'sales': [100, 200, 150]} +df = pd.DataFrame(data) +df['sales'].sum() # Last expression is automatically returned + `, { context: ctx }); + + return Response.json({ + result: result.results?.[0]?.text, + logs: result.logs + }); + } + }; + ``` + + + ```typescript + import { getSandbox } from '@cloudflare/sandbox'; + + export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // Create a project structure + await sandbox.mkdir('/workspace/project/src', { recursive: true }); + + // Write files + await sandbox.writeFile( + '/workspace/project/package.json', + JSON.stringify({ name: 'my-app', version: '1.0.0' }) + ); + + // Read a file back + const content = await sandbox.readFile('/workspace/project/package.json'); + + return Response.json({ content }); + } + }; + ``` + + + + + Get started + + + API Reference + + +--- + +## Features + + + +Run shell commands, Python scripts, Node.js applications, and more in isolated containers with streaming output support and automatic timeout handling. + + + + + +Read, write, and manipulate files in the sandbox filesystem. Run background processes, monitor output, and manage long-running operations. + + + + + +Expose HTTP services running in your sandbox with automatically generated preview URLs, perfect for interactive development environments and application hosting. + + + +--- + +## Use Cases + +Build powerful applications with Sandbox: + +### AI Code Execution + +Execute code generated by Large Language Models safely and reliably. Perfect for AI agents, code assistants, and autonomous systems that need to run untrusted code. + +### Data Analysis & Notebooks + +Create interactive data analysis environments with Python, pandas, and visualization libraries. Build notebook-like experiences at the edge. + +### Interactive Development Environments + +Build cloud IDEs, coding playgrounds, and collaborative development tools with full Linux environments and preview URLs. + +### CI/CD & Build Systems + +Run tests, compile code, and execute build pipelines in isolated environments with parallel execution and streaming logs. + +--- + +## Related products + + + +Serverless container runtime that powers Sandbox, enabling you to run any containerized workload on the edge. + + + + + +Run machine learning models and LLMs on the network. Combine with Sandbox for secure AI code execution workflows. + + + + + +Stateful coordination layer that enables Sandbox to maintain persistent environments with strong consistency. + + + +--- + +## More resources + + + + + Explore complete examples including AI code execution, data analysis, and + interactive environments. + + + + Learn about the Sandbox Beta, current status, and upcoming features. + + + + Understand Sandbox resource limits, quotas, and best practices for working + within them. + + + + Learn security best practices, performance optimization, and production + deployment patterns. + + + + View the SDK source code, report issues, and contribute to the project. + + + + Connect with the Workers community on Discord. Ask questions, share what + you're building, and get help from other developers. + + + diff --git a/src/content/docs/sandbox/tutorials/ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx new file mode 100644 index 000000000000000..33351b1d8db3531 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx @@ -0,0 +1,223 @@ +--- +title: Build an AI code executor +pcx_content_type: tutorial +sidebar: + order: 1 +--- + +import { Render, PackageManagers } from "~/components"; + +Build an AI-powered code execution system using Sandbox SDK and Claude. Turn natural language questions into Python code, execute it securely, and return results. + +**Time to complete:** 20 minutes + +## What you'll build + +An API that accepts questions like "What's the 100th Fibonacci number?", uses Claude to generate Python code, executes it in an isolated sandbox, and returns the results. + +## Prerequisites + + + +You'll also need: +- An [Anthropic API key](https://console.anthropic.com/) for Claude +- [Docker](https://www.docker.com/) running locally + +## 1. Create your project + +Create a new Sandbox SDK project: + + + +```sh +cd ai-code-executor +``` + +## 2. Install dependencies + +Install the Anthropic SDK: + + + +## 3. Build your code executor + +Replace the contents of `src/index.ts`: + +```typescript +import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; +import Anthropic from '@anthropic-ai/sdk'; + +export { Sandbox } from '@cloudflare/sandbox'; + +interface Env { + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.method !== 'POST' || new URL(request.url).pathname !== '/execute') { + return new Response('POST /execute with { "question": "your question" }'); + } + + try { + const { question } = await request.json(); + + if (!question) { + return Response.json({ error: 'Question is required' }, { status: 400 }); + } + + // Use Claude to generate Python code + const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); + const codeGeneration = await anthropic.messages.create({ + model: 'claude-sonnet-4-5', + max_tokens: 1024, + messages: [{ + role: 'user', + content: `Generate Python code to answer: "${question}" + +Requirements: +- Use only Python standard library +- Print the result using print() +- Keep code simple and safe + +Return ONLY the code, no explanations.` + }], + }); + + const generatedCode = codeGeneration.content[0]?.type === 'text' + ? codeGeneration.content[0].text + : ''; + + if (!generatedCode) { + return Response.json({ error: 'Failed to generate code' }, { status: 500 }); + } + + // Execute the code in a sandbox + const sandbox = getSandbox(env.Sandbox, 'demo-user'); + await sandbox.writeFile('/tmp/code.py', generatedCode); + const result = await sandbox.exec('python /tmp/code.py'); + + return Response.json({ + success: result.success, + question, + code: generatedCode, + output: result.stdout, + error: result.stderr + }); + + } catch (error: any) { + return Response.json( + { error: 'Internal server error', message: error.message }, + { status: 500 } + ); + } + }, +}; +``` + +**How it works:** +1. Receives a question via POST to `/execute` +2. Uses Claude to generate Python code +3. Writes code to `/tmp/code.py` in the sandbox +4. Executes with `sandbox.exec('python /tmp/code.py')` +5. Returns both the code and execution results + +## 4. Set your Anthropic API key + +Store your Anthropic API key as a secret: + +```sh +npx wrangler secret put ANTHROPIC_API_KEY +``` + +Paste your API key from the [Anthropic Console](https://console.anthropic.com/) when prompted. + +## 5. Test locally + +Start the development server: + +```sh +npm run dev +``` + +:::note +First run builds the Docker container (2-3 minutes). Subsequent runs are much faster. +::: + +Test with curl: + +```sh +curl -X POST http://localhost:8787/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "What is the 10th Fibonacci number?"}' +``` + +Response: + +```json +{ + "success": true, + "question": "What is the 10th Fibonacci number?", + "code": "def fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)\n\nprint(fibonacci(10))", + "output": "55\n", + "error": "" +} +``` + +## 6. Deploy + +Deploy your Worker: + +```sh +npx wrangler deploy +``` + +:::caution +After first deployment, wait 2-3 minutes for container provisioning. Check status with `npx wrangler containers list`. +::: + +## 7. Test your deployment + +Try different questions: + +```sh +# Factorial +curl -X POST https://ai-code-executor.YOUR_SUBDOMAIN.workers.dev/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "Calculate the factorial of 5"}' + +# Statistics +curl -X POST https://ai-code-executor.YOUR_SUBDOMAIN.workers.dev/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "What is the mean of [10, 20, 30, 40, 50]?"}' + +# String manipulation +curl -X POST https://ai-code-executor.YOUR_SUBDOMAIN.workers.dev/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "Reverse the string \"Hello World\""}' +``` + +## What you built + +You created an AI code execution system that: +- Accepts natural language questions +- Generates Python code with Claude +- Executes code securely in isolated sandboxes +- Returns results with error handling + +## Next steps + +- [Analyze data with AI](/sandbox/tutorials/analyze-data-with-ai/) - Add pandas and matplotlib for data analysis +- [Code Interpreter API](/sandbox/api/interpreter/) - Use the built-in code interpreter instead of exec +- [Streaming output](/sandbox/guides/streaming-output/) - Show real-time execution progress +- [API reference](/sandbox/api/) - Explore all available methods + +## Related resources + +- [Anthropic Claude documentation](https://docs.anthropic.com/) +- [Workers AI](/workers-ai/) - Use Cloudflare's built-in models diff --git a/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx new file mode 100644 index 000000000000000..227a4b45faa3de7 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx @@ -0,0 +1,237 @@ +--- +title: Analyze data with AI +pcx_content_type: tutorial +sidebar: + order: 2 +--- + +import { Render, PackageManagers } from "~/components"; + +Build an AI-powered data analysis system that accepts CSV uploads, uses Claude to generate Python analysis code, executes it in sandboxes, and returns visualizations. + +**Time to complete**: 25 minutes + +## Prerequisites + + + +You'll also need: +- An [Anthropic API key](https://console.anthropic.com/) for Claude +- [Docker](https://www.docker.com/) running locally + +## 1. Create your project + +Create a new Sandbox SDK project: + + + +```sh +cd analyze-data +``` + +## 2. Install dependencies + + + +## 3. Build the analysis handler + +Replace `src/index.ts`: + +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; +import Anthropic from '@anthropic-ai/sdk'; + +export { Sandbox } from '@cloudflare/sandbox'; + +interface Env { + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; + + if (request.method !== 'POST') { + return Response.json({ error: 'POST CSV file and question' }, { status: 405 }); + } + + try { + const formData = await request.formData(); + const csvFile = formData.get('file') as File; + const question = formData.get('question') as string; + + if (!csvFile || !question) { + return Response.json({ error: 'Missing file or question' }, { status: 400 }); + } + + // Upload CSV to sandbox + const sandbox = getSandbox(env.Sandbox, `analysis-${Date.now()}`); + const csvPath = '/workspace/data.csv'; + await sandbox.writeFile(csvPath, new Uint8Array(await csvFile.arrayBuffer())); + + // Analyze CSV structure + const structure = await sandbox.exec( + `python -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print(f'Rows: {len(df)}'); print(f'Columns: {list(df.columns)[:5]}')"` + ); + + if (!structure.success) { + return Response.json({ error: 'Failed to read CSV', details: structure.stderr }, { status: 400 }); + } + + // Generate analysis code with Claude + const code = await generateAnalysisCode(env.ANTHROPIC_API_KEY, csvPath, question, structure.stdout); + + // Write and execute the analysis code + await sandbox.writeFile('/workspace/analyze.py', code); + const result = await sandbox.exec('python /workspace/analyze.py'); + + if (!result.success) { + return Response.json({ error: 'Analysis failed', details: result.stderr }, { status: 500 }); + } + + // Check for generated chart + let chart = null; + try { + const chartFile = await sandbox.readFile('/workspace/chart.png'); + const buffer = new Uint8Array(chartFile.content); + chart = `data:image/png;base64,${btoa(String.fromCharCode(...buffer))}`; + } catch { + // No chart generated + } + + await sandbox.destroy(); + + return Response.json({ + success: true, + output: result.stdout, + chart, + code + }); + + } catch (error: any) { + return Response.json({ error: error.message }, { status: 500 }); + } + }, +}; + +async function generateAnalysisCode( + apiKey: string, + csvPath: string, + question: string, + csvStructure: string +): Promise { + const anthropic = new Anthropic({ apiKey }); + + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5', + max_tokens: 2048, + messages: [{ + role: 'user', + content: `CSV at ${csvPath}: +${csvStructure} + +Question: "${question}" + +Generate Python code that: +- Reads CSV with pandas +- Answers the question +- Saves charts to /workspace/chart.png if helpful +- Prints findings to stdout + +Use pandas, numpy, matplotlib.` + }], + tools: [{ + name: 'generate_python_code', + description: 'Generate Python code for data analysis', + input_schema: { + type: 'object', + properties: { + code: { type: 'string', description: 'Complete Python code' } + }, + required: ['code'] + } + }] + }); + + for (const block of response.content) { + if (block.type === 'tool_use' && block.name === 'generate_python_code') { + return (block.input as { code: string }).code; + } + } + + throw new Error('Failed to generate code'); +} +``` + +## 4. Set your API key + +```sh +npx wrangler secret put ANTHROPIC_API_KEY +``` + +## 5. Test locally + +Download a sample CSV: + +```sh +# Create a test CSV +echo "year,rating,title +2020,8.5,Movie A +2021,7.2,Movie B +2022,9.1,Movie C" > test.csv +``` + +Start the dev server: + +```sh +npm run dev +``` + +Test with curl: + +```sh +curl -X POST http://localhost:8787 \ + -F "file=@test.csv" \ + -F "question=What is the average rating by year?" +``` + +Response: + +```json +{ + "success": true, + "output": "Average ratings by year:\n2020: 8.5\n2021: 7.2\n2022: 9.1", + "chart": "data:image/png;base64,...", + "code": "import pandas as pd\nimport matplotlib.pyplot as plt\n..." +} +``` + +## 6. Deploy + +```sh +npx wrangler deploy +``` + +:::caution +Wait 2-3 minutes after first deployment for container provisioning. +::: + +## What you built + +An AI data analysis system that: +- Uploads CSV files to sandboxes +- Uses Claude's tool calling to generate analysis code +- Executes Python with pandas and matplotlib +- Returns text output and visualizations + +## Next steps + +- [Code Interpreter API](/sandbox/api/interpreter/) - Use the built-in code interpreter +- [File operations](/sandbox/guides/manage-files/) - Advanced file handling +- [Streaming output](/sandbox/guides/streaming-output/) - Real-time progress updates diff --git a/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx b/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx new file mode 100644 index 000000000000000..7324de9daea228a --- /dev/null +++ b/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx @@ -0,0 +1,203 @@ +--- +title: Automated testing pipeline +pcx_content_type: tutorial +sidebar: + order: 4 +--- + +import { Render, PackageManagers } from "~/components"; + +Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results. + +**Time to complete**: 25 minutes + +## Prerequisites + + + +You'll also need a GitHub repository with tests (public or private with access token). + +## 1. Create your project + + + +```sh +cd test-pipeline +``` + +## 2. Build the pipeline + +Replace `src/index.ts`: + +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; + +export { Sandbox } from '@cloudflare/sandbox'; + +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN?: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; + + if (request.method !== 'POST') { + return new Response('POST { "repoUrl": "https://github.com/owner/repo", "branch": "main" }'); + } + + try { + const { repoUrl, branch = 'main' } = await request.json(); + + if (!repoUrl) { + return Response.json({ error: 'repoUrl required' }, { status: 400 }); + } + + const sandbox = getSandbox(env.Sandbox, `test-${Date.now()}`); + + try { + // Clone repository + let cloneUrl = repoUrl; + if (env.GITHUB_TOKEN && repoUrl.includes('github.com')) { + cloneUrl = repoUrl.replace('https://', `https://${env.GITHUB_TOKEN}@`); + } + + await sandbox.exec(`git clone --depth=1 --branch=${branch} ${cloneUrl} /workspace/repo`); + + // Detect project type + const projectType = await detectProjectType(sandbox); + + // Install dependencies + const installCmd = getInstallCommand(projectType); + if (installCmd) { + const installResult = await sandbox.exec(`cd /workspace/repo && ${installCmd}`); + if (!installResult.success) { + return Response.json({ + success: false, + error: 'Install failed', + output: installResult.stderr + }); + } + } + + // Run tests + const testCmd = getTestCommand(projectType); + const testResult = await sandbox.exec(`cd /workspace/repo && ${testCmd}`); + + return Response.json({ + success: testResult.exitCode === 0, + exitCode: testResult.exitCode, + output: testResult.stdout, + errors: testResult.stderr, + projectType + }); + + } finally { + await sandbox.destroy(); + } + + } catch (error: any) { + return Response.json({ error: error.message }, { status: 500 }); + } + }, +}; + +async function detectProjectType(sandbox: any): Promise { + try { + await sandbox.readFile('/workspace/repo/package.json'); + return 'nodejs'; + } catch {} + + try { + await sandbox.readFile('/workspace/repo/requirements.txt'); + return 'python'; + } catch {} + + try { + await sandbox.readFile('/workspace/repo/go.mod'); + return 'go'; + } catch {} + + return 'unknown'; +} + +function getInstallCommand(projectType: string): string { + switch (projectType) { + case 'nodejs': return 'npm install'; + case 'python': return 'pip install -r requirements.txt || pip install -e .'; + case 'go': return 'go mod download'; + default: return ''; + } +} + +function getTestCommand(projectType: string): string { + switch (projectType) { + case 'nodejs': return 'npm test'; + case 'python': return 'python -m pytest || python -m unittest discover'; + case 'go': return 'go test ./...'; + default: return 'echo "Unknown project type"'; + } +} +``` + +## 3. Test locally + +Start the dev server: + +```sh +npm run dev +``` + +Test with a repository: + +```sh +curl -X POST http://localhost:8787 \ + -H "Content-Type: application/json" \ + -d '{ + "repoUrl": "https://github.com/sindresorhus/is-promise", + "branch": "main" + }' +``` + +Response: + +```json +{ + "success": true, + "exitCode": 0, + "output": "...test output...", + "projectType": "nodejs" +} +``` + +## 4. Deploy + +```sh +npx wrangler deploy +``` + +For private repositories, set your GitHub token: + +```sh +npx wrangler secret put GITHUB_TOKEN +``` + +## What you built + +An automated testing pipeline that: +- Clones Git repositories +- Detects project type (Node.js, Python, Go) +- Installs dependencies automatically +- Runs tests and reports results + +## Next steps + +- [Streaming output](/sandbox/guides/streaming-output/) - Add real-time test output +- [Background processes](/sandbox/guides/background-processes/) - Handle long-running tests +- [Sessions API](/sandbox/api/sessions/) - Cache dependencies between runs diff --git a/src/content/docs/sandbox/tutorials/code-review-bot.mdx b/src/content/docs/sandbox/tutorials/code-review-bot.mdx new file mode 100644 index 000000000000000..87df1c451defdc4 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/code-review-bot.mdx @@ -0,0 +1,242 @@ +--- +title: Build a code review bot +pcx_content_type: tutorial +sidebar: + order: 3 +--- + +import { Render, PackageManagers } from "~/components"; + +Build a GitHub bot that responds to pull requests, clones the repository in a sandbox, uses Claude to analyze code changes, and posts review comments. + +**Time to complete**: 30 minutes + +## Prerequisites + + + +You'll also need: +- A [GitHub account](https://github.com/) and personal access token with repo permissions +- An [Anthropic API key](https://console.anthropic.com/) for Claude +- A GitHub repository for testing + +## 1. Create your project + + + +```sh +cd code-review-bot +``` + +## 2. Install dependencies + + + +## 3. Build the webhook handler + +Replace `src/index.ts`: + +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; +import { Octokit } from '@octokit/rest'; +import Anthropic from '@anthropic-ai/sdk'; + +export { Sandbox } from '@cloudflare/sandbox'; + +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN: string; + ANTHROPIC_API_KEY: string; + WEBHOOK_SECRET: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; + + const url = new URL(request.url); + + if (url.pathname === '/webhook' && request.method === 'POST') { + const signature = request.headers.get('x-hub-signature-256'); + const body = await request.text(); + + // Verify webhook signature + if (!signature || !(await verifySignature(body, signature, env.WEBHOOK_SECRET))) { + return Response.json({ error: 'Invalid signature' }, { status: 401 }); + } + + const event = request.headers.get('x-github-event'); + const payload = JSON.parse(body); + + // Only handle opened PRs + if (event === 'pull_request' && payload.action === 'opened') { + reviewPullRequest(payload, env).catch(console.error); + return Response.json({ message: 'Review started' }); + } + + return Response.json({ message: 'Event ignored' }); + } + + return new Response('Code Review Bot\n\nConfigure GitHub webhook to POST /webhook'); + }, +}; + +async function verifySignature(payload: string, signature: string, secret: string): Promise { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(payload)); + const expected = 'sha256=' + Array.from(new Uint8Array(signatureBytes)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + return signature === expected; +} + +async function reviewPullRequest(payload: any, env: Env): Promise { + const pr = payload.pull_request; + const repo = payload.repository; + const octokit = new Octokit({ auth: env.GITHUB_TOKEN }); + + // Post initial comment + await octokit.issues.createComment({ + owner: repo.owner.login, + repo: repo.name, + issue_number: pr.number, + body: 'Code review in progress...' + }); + + const sandbox = getSandbox(env.Sandbox, `review-${pr.number}`); + + try { + // Clone repository + const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${repo.owner.login}/${repo.name}.git`; + await sandbox.exec(`git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`); + + // Get changed files + const comparison = await octokit.repos.compareCommits({ + owner: repo.owner.login, + repo: repo.name, + base: pr.base.sha, + head: pr.head.sha + }); + + const files = []; + for (const file of (comparison.data.files || []).slice(0, 5)) { + if (file.status !== 'removed') { + const content = await sandbox.readFile(`/workspace/repo/${file.filename}`); + files.push({ + path: file.filename, + patch: file.patch || '', + content: content.content + }); + } + } + + // Generate review with Claude + const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); + const response = await anthropic.messages.create({ + model: 'claude-sonnet-4-5', + max_tokens: 2048, + messages: [{ + role: 'user', + content: `Review this PR: + +Title: ${pr.title} + +Changed files: +${files.map(f => `File: ${f.path}\nDiff:\n${f.patch}\n\nContent:\n${f.content.substring(0, 1000)}`).join('\n\n')} + +Provide a brief code review focusing on bugs, security, and best practices.` + }] + }); + + const review = response.content[0]?.type === 'text' ? response.content[0].text : 'No review generated'; + + // Post review comment + await octokit.issues.createComment({ + owner: repo.owner.login, + repo: repo.name, + issue_number: pr.number, + body: `## Code Review\n\n${review}\n\n---\n*Generated by Claude*` + }); + + } catch (error: any) { + await octokit.issues.createComment({ + owner: repo.owner.login, + repo: repo.name, + issue_number: pr.number, + body: `Review failed: ${error.message}` + }); + } finally { + await sandbox.destroy(); + } +} +``` + +## 4. Set your secrets + +```sh +# GitHub token (needs repo permissions) +npx wrangler secret put GITHUB_TOKEN + +# Anthropic API key +npx wrangler secret put ANTHROPIC_API_KEY + +# Webhook secret (generate a random string) +npx wrangler secret put WEBHOOK_SECRET +``` + +## 5. Deploy + +```sh +npx wrangler deploy +``` + +## 6. Configure GitHub webhook + +1. Go to your repository **Settings** > **Webhooks** > **Add webhook** +2. Set **Payload URL**: `https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook` +3. Set **Content type**: `application/json` +4. Set **Secret**: Same value you used for `WEBHOOK_SECRET` +5. Select **Let me select individual events** → Check **Pull requests** +6. Click **Add webhook** + +## 7. Test with a pull request + +Create a test PR: + +```sh +git checkout -b test-review +echo "console.log('test');" > test.js +git add test.js +git commit -m "Add test file" +git push origin test-review +``` + +Open the PR on GitHub and watch for the bot's review comment! + +## What you built + +A GitHub code review bot that: +- Receives webhook events from GitHub +- Clones repositories in isolated sandboxes +- Uses Claude to analyze code changes +- Posts review comments automatically + +## Next steps + +- [Git operations](/sandbox/api/files/#gitcheckout) - Advanced repository handling +- [Sessions API](/sandbox/api/sessions/) - Manage long-running sandbox operations +- [GitHub Apps](https://docs.github.com/en/apps) - Build a proper GitHub App diff --git a/src/content/docs/sandbox/tutorials/index.mdx b/src/content/docs/sandbox/tutorials/index.mdx new file mode 100644 index 000000000000000..7975ec6472c76c3 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/index.mdx @@ -0,0 +1,71 @@ +--- +title: Tutorials +pcx_content_type: navigation +sidebar: + order: 3 +--- + +import { LinkTitleCard, CardGrid } from "~/components"; + +Learn how to build applications with Sandbox SDK through step-by-step tutorials. Each tutorial takes 20-30 minutes. + + + + + Use Claude to generate Python code from natural language and execute it securely in sandboxes. + + + + Upload CSV files, generate analysis code with Claude, and return visualizations. + + + + Clone repositories, analyze code with Claude, and post review comments to GitHub PRs. + + + + Clone repositories, install dependencies, run tests, and report results. + + + + +## What you'll learn + +These tutorials cover real-world applications: + +- **AI Code Execution** - Integrate Claude with secure code execution +- **Data Analysis** - Generate and run analysis code on uploaded datasets +- **Code Review Automation** - Clone repositories and analyze code changes +- **CI/CD Pipelines** - Automated testing workflows +- **File Operations** - Work with files and directories +- **Error Handling** - Validation and error management +- **Deployment** - Deploy Workers with containers + +## Before you start + +All tutorials assume you have: + +- Completed the [Get Started guide](/sandbox/get-started/) +- Basic familiarity with [Workers](/workers/) +- [Docker](https://www.docker.com/) installed and running + +## Related resources + +- [How-to guides](/sandbox/guides/) - Solve specific problems +- [API reference](/sandbox/api/) - Complete SDK reference diff --git a/src/content/products/sandbox.yaml b/src/content/products/sandbox.yaml new file mode 100644 index 000000000000000..194bb04c05a126a --- /dev/null +++ b/src/content/products/sandbox.yaml @@ -0,0 +1,13 @@ +name: Sandbox SDK + +product: + title: Sandbox SDK + url: /sandbox/ + group: Developer platform + additional_groups: [AI] + tags: [AI] + +meta: + title: Cloudflare Sandbox SDK docs + description: Build secure, isolated code execution environments + author: "@cloudflare" diff --git a/src/icons/sandbox.svg b/src/icons/sandbox.svg new file mode 100644 index 000000000000000..cf8f7779bc25819 --- /dev/null +++ b/src/icons/sandbox.svg @@ -0,0 +1 @@ + \ No newline at end of file