From f78aa80daa91f34ab43996d7ad431661fe042193 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 11:56:17 +0100 Subject: [PATCH 01/22] Add landing page for sandbox SDK --- src/content/docs/sandbox/index.mdx | 241 +++++++++++++++++++++++++++++ src/content/products/sandbox.yaml | 13 ++ src/icons/sandbox.svg | 1 + 3 files changed, 255 insertions(+) create mode 100644 src/content/docs/sandbox/index.mdx create mode 100644 src/content/products/sandbox.yaml create mode 100644 src/icons/sandbox.svg 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/products/sandbox.yaml b/src/content/products/sandbox.yaml new file mode 100644 index 000000000000000..6066c2ff51b6262 --- /dev/null +++ b/src/content/products/sandbox.yaml @@ -0,0 +1,13 @@ +name: Sandbox + +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 From 077e702cccd32a00c5c3cace1e457e41981ef0e8 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 12:07:37 +0100 Subject: [PATCH 02/22] Add first turorial --- src/content/docs/sandbox/get-started.mdx | 226 ++++++++ .../tutorials/build-an-ai-code-executor.mdx | 489 ++++++++++++++++++ src/content/docs/sandbox/tutorials/index.mdx | 46 ++ 3 files changed, 761 insertions(+) create mode 100644 src/content/docs/sandbox/get-started.mdx create mode 100644 src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx create mode 100644 src/content/docs/sandbox/tutorials/index.mdx diff --git a/src/content/docs/sandbox/get-started.mdx b/src/content/docs/sandbox/get-started.mdx new file mode 100644 index 000000000000000..c44a9be5808166b --- /dev/null +++ b/src/content/docs/sandbox/get-started.mdx @@ -0,0 +1,226 @@ +--- +title: Getting started +pcx_content_type: get-started +sidebar: + order: 2 +--- + +import { Render, PackageManagers, Steps, WranglerConfig } from "~/components"; + +This guide will walk you through creating your first secure code execution environment using Sandbox SDK. You'll learn how to: + +- Create a Worker that can execute code in isolated sandboxes +- Run commands and capture output +- Work with files in the sandbox +- Deploy your application globally + +By the end of this guide, you'll have a working sandbox that can safely execute Python code. + +## Prerequisites + + + +### Ensure Docker is running locally + +Sandbox SDK uses [Docker](https://www.docker.com/) to build and push container images alongside your Worker. Docker must be running locally when you run `wrangler deploy`. The easiest way to install Docker is to follow the [Docker Desktop installation guide](https://docs.docker.com/desktop/). + +You can verify Docker is running by executing: + +```sh +docker info +``` + +If Docker is running, the command will succeed. If not, it will hang or return "Cannot connect to the Docker daemon". + +## 1. Create your first sandbox project + +Create a new project using the Sandbox SDK template: + + + +This creates a new `my-sandbox` directory with: + +- A Worker at `src/index.ts` configured to handle sandbox requests +- A `wrangler.jsonc` configuration file with container and Durable Objects setup +- A `Dockerfile` that defines the sandbox container environment + +Change into your new project directory: + +```sh +cd my-sandbox +``` + +## 2. Understanding the configuration + +Your `wrangler.jsonc` file contains the configuration for both your Worker and the sandbox container: + + + +```jsonc +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + "name": "sandbox", + "max_instances": 1 + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1" + } + ] +} +``` + + + +Key points: + +- `containers` - Defines the container image and configuration +- `durable_objects` - Each sandbox runs in its own Durable Object for isolation +- `migrations` - Required for Durable Objects using SQLite storage + +## 3. Create your first sandbox Worker + +Replace the contents of `src/index.ts` with this simple example: + +```typescript +import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; + +export { Sandbox } from '@cloudflare/sandbox'; + +type Env = { + Sandbox: DurableObjectNamespace; +}; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Get a sandbox instance with a unique ID + const sandbox = getSandbox(env.Sandbox, 'my-first-sandbox'); + + // Execute Python code in the sandbox + if (url.pathname === '/run') { + const result = await sandbox.exec('python', [ + '-c', + 'print("Hello from Sandbox SDK!")' + ]); + + return Response.json({ + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + success: result.success + }); + } + + // Write and read a file + if (url.pathname === '/file') { + await sandbox.writeFile('/tmp/message.txt', 'Hello, World!'); + const content = await sandbox.readFile('/tmp/message.txt'); + + return Response.json({ + content: content, + message: 'File written and read successfully' + }); + } + + return new Response('Try /run or /file endpoints', { status: 200 }); + }, +}; +``` + +This Worker demonstrates two key Sandbox SDK features: + +1. **Command Execution** - Run Python code and capture output +2. **File Operations** - Write and read files in the sandbox + +## 4. Test locally + +Before deploying, test your sandbox locally: + +```sh +npm run dev +``` + +:::note +The first time you run `wrangler dev`, it will build the Docker container image. This may take a few minutes. Subsequent runs will be much faster due to Docker's layer caching. +::: + +Once the dev server is running, open your browser and visit: + +- `http://localhost:8787/run` - Execute Python code +- `http://localhost:8787/file` - Test file operations + +You should see JSON responses with the output from each operation. + +## 5. Deploy your sandbox + +Deploy your Worker and sandbox container to Cloudflare's global network: + +```sh +npx wrangler deploy +``` + +When you run `wrangler deploy`: + +1. Wrangler builds your container image using Docker +2. The image is pushed to Cloudflare's Container Registry +3. Your Worker is deployed with the sandbox configuration + +:::note +After your first deployment, wait 2-3 minutes for the container to be fully provisioned before making requests. During this time, the Worker is deployed but sandbox operations may error. +::: + +### Check deployment status + +Verify your sandbox is ready: + +```sh +npx wrangler containers list +``` + +This shows all containers in your account and their deployment status. + +## 6. Test your deployed sandbox + +Visit your deployed Worker URL (shown in the deploy output): + +``` +https://my-sandbox..workers.dev/run +``` + +You should see the same JSON response as when testing locally, but now running on Cloudflare's global network. + +## Summary + +In this guide, you: + +- Created a Worker with Sandbox SDK integration +- Executed Python code in an isolated sandbox +- Worked with files in the sandbox environment +- Deployed your sandbox globally + +## Next steps + +- Learn about [command execution](/sandbox/guides/execute-commands/) with streaming output +- Explore [file operations](/sandbox/guides/manage-files/) and working with projects +- Set up [preview URLs](/sandbox/guides/expose-services/) to expose services running in your sandbox +- Review the [API reference](/sandbox/api/) for all available methods +- Check out [examples](/sandbox/examples/) including AI code execution and data analysis diff --git a/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx new file mode 100644 index 000000000000000..edeadf3b41c4786 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx @@ -0,0 +1,489 @@ +--- +pcx_content_type: tutorial +title: Build an AI code executor +difficulty: Beginner +products: + - Workers + - Sandbox +tags: + - AI + - Anthropic + - Code Execution +sidebar: + order: 1 +description: Build a production-ready AI code execution system that safely runs Python code generated by Claude. +--- + +import { Render, PackageManagers, WranglerConfig } from "~/components"; + +In this tutorial, you will build a production-ready AI code execution system using Sandbox SDK and Anthropic's Claude. The system will accept natural language questions, generate Python code to answer them, execute the code securely, and return results with proper error handling. + +By the end of this tutorial, you will have: +- A Worker that integrates Claude with Sandbox SDK +- Secure code execution with proper isolation +- Streaming output support +- Error handling and validation +- A deployed application on Cloudflare's global network + +**Time to complete:** 20 minutes +**Level:** Beginner (no prior Cloudflare experience needed) + +## What you will build + +A production-ready code executor that: +- Accepts natural language questions (e.g., "What's the 100th Fibonacci number?") +- Uses Claude to generate Python code +- Executes the code securely in a sandbox +- Returns results with proper error handling +- Supports streaming output for long-running operations + +## Prerequisites + + + +You will also need: +- An [Anthropic API key](https://console.anthropic.com/) to use Claude +- [Docker](https://www.docker.com/) running locally for container builds + +## 1. Create your project + +Create a new Sandbox SDK project using the template: + + + + + +Change into your new project directory: + +```sh +cd ai-code-executor +``` + +## 2. Install dependencies + +Install the Anthropic SDK to interact with Claude: + + + +## 3. Configure your Worker + +Your `wrangler.jsonc` file already contains the required configuration for Sandbox SDK (containers and Durable Objects). Verify it looks like this: + + + +```jsonc +{ + "name": "ai-code-executor", + "main": "src/index.ts", + "compatibility_date": "2025-05-06", + "compatibility_flags": ["nodejs_compat"], + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + "name": "sandbox", + "max_instances": 10 + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1" + } + ] +} +``` + + + +## 4. Build your AI code executor + +Replace the contents of `src/index.ts` with the following code: + +```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; +} + +interface CodeExecutionRequest { + question: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + // Handle CORS preflight + if (request.method === 'OPTIONS') { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + } + }); + } + + const url = new URL(request.url); + + // Health check endpoint + if (url.pathname === '/health') { + return Response.json({ + status: 'healthy', + timestamp: new Date().toISOString() + }); + } + + // Main code execution endpoint + if (url.pathname === '/execute' && request.method === 'POST') { + try { + const { question }: CodeExecutionRequest = await request.json(); + + if (!question) { + return Response.json( + { error: 'Question is required' }, + { status: 400 } + ); + } + + // Initialize Claude client + const anthropic = new Anthropic({ + apiKey: env.ANTHROPIC_API_KEY, + }); + + // Get sandbox instance - use user ID for multi-tenancy in production + const sandboxId = 'demo-user'; + const sandbox = getSandbox(env.Sandbox, sandboxId); + + // Step 1: Ask Claude to generate Python code + const codeGeneration = await anthropic.messages.create({ + model: 'claude-3-5-sonnet-latest', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: `You are a Python code generator. Generate Python code to answer this question: "${question}" + +Requirements: +- Write complete, executable Python code +- Print the final result using print() +- Keep code simple and safe +- Only use Python standard library +- Do not use any external packages +- The code should be self-contained + +Return ONLY the Python code, no explanations or markdown formatting.` + } + ], + }); + + // Extract the generated code + const generatedCode = codeGeneration.content[0]?.type === 'text' + ? codeGeneration.content[0].text + : ''; + + if (!generatedCode) { + return Response.json( + { error: 'Failed to generate code' }, + { status: 500 } + ); + } + + // Step 2: Write the code to a file in the sandbox + const codeFilePath = '/tmp/generated_code.py'; + await sandbox.writeFile(codeFilePath, generatedCode); + + // Step 3: Execute the code in the sandbox + const result = await sandbox.exec('python', [codeFilePath]); + + // Step 4: Return the results + return Response.json({ + success: result.success, + question, + code: generatedCode, + output: result.stdout, + error: result.stderr, + exitCode: result.exitCode, + }, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + } + }); + + } catch (error: any) { + console.error('Execution error:', error); + return Response.json( + { + error: 'Internal server error', + message: error.message + }, + { status: 500 } + ); + } + } + + // Default response + return new Response( + 'AI Code Executor\n\nPOST /execute with { "question": "your question" }', + { + status: 200, + headers: { 'Content-Type': 'text/plain' } + } + ); + }, +}; +``` + +This Worker: +1. **Receives questions** via POST requests to `/execute` +2. **Uses Claude** to generate Python code that answers the question +3. **Writes the code** to a file in the sandbox +4. **Executes** the code securely +5. **Returns** both the generated code and execution results + +## 5. Set your Anthropic API key + +Before deploying, store your Anthropic API key as a secret: + +```sh +npx wrangler secret put ANTHROPIC_API_KEY +``` + +When prompted, paste your API key from the [Anthropic Console](https://console.anthropic.com/). + +## 6. Test locally + +Start the local development server: + +```sh +npm run dev +``` + +:::note +The first run will build the Docker container image. This takes 2-3 minutes. Subsequent runs are much faster due to Docker's layer caching. +::: + +Once running, test your code executor with curl: + +```sh +curl -X POST http://localhost:8787/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "What is the 10th Fibonacci number?"}' +``` + +You should see a response like: + +```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\nresult = fibonacci(10)\nprint(result)", + "output": "55\n", + "error": "", + "exitCode": 0 +} +``` + +## 7. Deploy to production + +Deploy your Worker to Cloudflare's global network: + +```sh +npx wrangler deploy +``` + +When deployment completes, Wrangler will output your Worker's URL: + +```txt output +Published ai-code-executor (1.23 sec) + https://ai-code-executor..workers.dev +``` + +:::note +After your first deployment, wait 2-3 minutes for the container to be fully provisioned. During this time, requests may fail. Check the status with: + +```sh +npx wrangler containers list +``` +::: + +## 8. Test your deployed Worker + +Test your production deployment: + +```sh +curl -X POST https://ai-code-executor..workers.dev/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "Calculate the factorial of 5"}' +``` + +Try different questions: + +```sh +# Statistical calculation +curl -X POST https://ai-code-executor..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..workers.dev/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "Reverse the string \"Hello World\""}' + +# Mathematical computation +curl -X POST https://ai-code-executor..workers.dev/execute \ + -H "Content-Type: application/json" \ + -d '{"question": "What is 2 to the power of 10?"}' +``` + +## 9. Add error handling (optional) + +For production use, enhance error handling by creating a validation function. Add this to your Worker: + +```typescript +function validateCode(code: string): { safe: boolean; reason?: string } { + // Basic safety checks + const dangerousPatterns = [ + /import\s+os/i, + /import\s+sys/i, + /import\s+subprocess/i, + /exec\(/i, + /eval\(/i, + /__import__/i, + /open\(/i, + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(code)) { + return { + safe: false, + reason: `Code contains potentially dangerous pattern: ${pattern}` + }; + } + } + + return { safe: true }; +} +``` + +Then update your code execution logic: + +```typescript +// After generating code, before executing +const validation = validateCode(generatedCode); +if (!validation.safe) { + return Response.json( + { + error: 'Generated code failed safety validation', + reason: validation.reason + }, + { status: 400 } + ); +} +``` + +## 10. Add streaming support (optional) + +For long-running code, add streaming support to show real-time output: + +```typescript +// Streaming execution endpoint +if (url.pathname === '/execute/stream' && request.method === 'POST') { + const { question }: CodeExecutionRequest = await request.json(); + + // ... Claude code generation ... + + const sandbox = getSandbox(env.Sandbox, 'demo-user'); + await sandbox.writeFile('/tmp/generated_code.py', generatedCode); + + // Create a streaming response + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + + // Execute with streaming + (async () => { + try { + const result = await sandbox.exec('python', ['/tmp/generated_code.py']); + + // Stream the output + await writer.write(encoder.encode(JSON.stringify({ + type: 'output', + data: result.stdout + }) + '\n')); + + await writer.write(encoder.encode(JSON.stringify({ + type: 'complete', + exitCode: result.exitCode + }) + '\n')); + } catch (error: any) { + await writer.write(encoder.encode(JSON.stringify({ + type: 'error', + message: error.message + }) + '\n')); + } finally { + await writer.close(); + } + })(); + + return new Response(readable, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Access-Control-Allow-Origin': '*', + } + }); +} +``` + +## Summary + +In this tutorial, you built a production-ready AI code execution system that: + +- ✅ Integrates Claude API with Sandbox SDK +- ✅ Generates Python code from natural language +- ✅ Executes code securely in isolated sandboxes +- ✅ Returns results with proper error handling +- ✅ Runs on Cloudflare's global network + +## Next steps + +Explore more ways to use Sandbox SDK: + +- [Build a data analysis platform](/sandbox/tutorials/data-analysis/) with pandas and matplotlib +- [Create an interactive development environment](/sandbox/tutorials/dev-environment/) with preview URLs +- Learn about [file operations](/sandbox/guides/manage-files/) for working with larger projects +- Explore [background processes](/sandbox/guides/background-processes/) for long-running tasks +- Review the [complete API reference](/sandbox/api/) for all available methods + +## Related resources + +- [Anthropic Claude documentation](https://docs.anthropic.com/) +- [Sandbox SDK API reference](/sandbox/api/) +- [Workers AI integration](/workers-ai/) for using Cloudflare's built-in models +- [Best practices for production deployments](/sandbox/best-practices/) diff --git a/src/content/docs/sandbox/tutorials/index.mdx b/src/content/docs/sandbox/tutorials/index.mdx new file mode 100644 index 000000000000000..dd458e2c8e799ab --- /dev/null +++ b/src/content/docs/sandbox/tutorials/index.mdx @@ -0,0 +1,46 @@ +--- +title: Tutorials +pcx_content_type: navigation +sidebar: + order: 3 +--- + +import { LinkTitleCard, CardGrid } from "~/components"; + +Learn how to build complete applications with Sandbox SDK through step-by-step tutorials. Each tutorial is production-ready and designed to be completed in 20-30 minutes. + + + + + Build a production-ready AI code execution system using Claude and Sandbox SDK. Generate Python code from natural language and execute it securely. + + + + +## What you'll learn + +These tutorials cover complete, real-world applications: + +- **AI Code Execution** - Integrate LLMs with secure code execution +- **File Operations** - Work with files and directories in sandboxes +- **Error Handling** - Production-ready validation and error management +- **Deployment** - Deploy to Cloudflare's global network + +## 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 +- [Examples](/sandbox/examples/) - Code snippets and patterns +- [Best practices](/sandbox/best-practices/) - Production deployment guidance From 508adb5060978c1d5b9375e55081ef1e9f90c5c5 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 12:19:27 +0100 Subject: [PATCH 03/22] Start with API reference --- src/content/docs/sandbox/api/commands.mdx | 471 +++++++++++++++++ src/content/docs/sandbox/api/files.mdx | 499 ++++++++++++++++++ src/content/docs/sandbox/api/index.mdx | 212 ++++++++ .../tutorials/build-an-ai-code-executor.mdx | 4 +- 4 files changed, 1184 insertions(+), 2 deletions(-) create mode 100644 src/content/docs/sandbox/api/commands.mdx create mode 100644 src/content/docs/sandbox/api/files.mdx create mode 100644 src/content/docs/sandbox/api/index.mdx diff --git a/src/content/docs/sandbox/api/commands.mdx b/src/content/docs/sandbox/api/commands.mdx new file mode 100644 index 000000000000000..d1449e31a3e2288 --- /dev/null +++ b/src/content/docs/sandbox/api/commands.mdx @@ -0,0 +1,471 @@ +--- +title: Commands API +pcx_content_type: concept +sidebar: + order: 1 +--- + +import { Details } from "~/components"; + +The Commands API lets you execute commands in the sandbox and manage background processes. All command execution happens in the sandbox's isolated container environment. + +## Methods + +### `exec()` + +Execute a command and return the complete result. + +```typescript +const result = await sandbox.exec(command: string, options?: ExecOptions): Promise +``` + +#### Parameters + +- **command** (`string`, required) - The command to execute. Can include arguments. +- **options** (`ExecOptions`, optional) - Execution options: + - `stream` (`boolean`) - Enable streaming callbacks (default: false) + - `onOutput` (`(stream: 'stdout' | 'stderr', data: string) => void`) - Callback for real-time output + - `timeout` (`number`) - Maximum execution time in milliseconds + +#### Return value + +Returns a `Promise` with: + +- `success` (`boolean`) - Whether the command succeeded (exitCode === 0) +- `stdout` (`string`) - Standard output from the command +- `stderr` (`string`) - Standard error from the command +- `exitCode` (`number`) - Process exit code + +#### Examples + +**Basic command execution:** + +```typescript +const result = await sandbox.exec('python --version'); + +console.log(result.stdout); // "Python 3.11.0" +console.log(result.success); // true +console.log(result.exitCode); // 0 +``` + +**Command with arguments:** + +```typescript +const result = await sandbox.exec('python -c "print(2 + 2)"'); + +if (result.success) { + console.log('Result:', result.stdout); // "4" +} else { + console.error('Error:', result.stderr); +} +``` + +**Streaming output:** + +```typescript +const result = await sandbox.exec('npm install express', { + stream: true, + onOutput: (stream, data) => { + console.log(`[${stream}] ${data}`); + } +}); + +// Logs installation progress in real-time +// Still returns complete result when done +console.log('Final exit code:', result.exitCode); +``` + +**Error handling:** + +```typescript +try { + const result = await sandbox.exec('nonexistent-command'); + + if (!result.success) { + console.log('Command failed with exit code:', result.exitCode); + console.log('Error output:', result.stderr); + } +} catch (error) { + console.error('Execution error:', error.message); +} +``` + +**Complex shell command:** + +```typescript +// Use shell features like pipes and redirects +const result = await sandbox.exec('ls -la | grep ".py" | wc -l'); +console.log('Number of Python files:', result.stdout.trim()); +``` + +--- + +### `execStream()` + +Execute a command and return a Server-Sent Events stream for real-time processing. + +```typescript +const stream = await sandbox.execStream(command: string, options?: ExecOptions): Promise +``` + +#### Parameters + +- **command** (`string`, required) - The command to execute +- **options** (`ExecOptions`, optional) - Same as `exec()` + +#### Return value + +Returns a `Promise` that emits `ExecEvent` objects via Server-Sent Events. + +Event types: +- `start` - Command execution started +- `stdout` - Standard output data +- `stderr` - Standard error data +- `complete` - Command finished +- `error` - Execution error occurred + +#### Examples + +**Basic streaming:** + +```typescript +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 'start': + console.log('Build started:', event.command); + break; + case 'stdout': + console.log('Output:', event.data); + break; + case 'stderr': + console.error('Error:', event.data); + break; + case 'complete': + console.log('Build finished with exit code:', event.exitCode); + break; + case 'error': + console.error('Execution failed:', event.error); + break; + } +} +``` + +**Streaming to client:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + const stream = await sandbox.execStream('python long-running-script.py'); + + // Return stream directly to client + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + } + }); + } +}; +``` + +**Progress tracking:** + +```typescript +let progress = 0; + +const stream = await sandbox.execStream('npm test'); + +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout') { + if (event.data.includes('PASS')) { + progress++; + console.log(`Tests passed: ${progress}`); + } + } + + if (event.type === 'complete') { + console.log(`All tests complete. ${progress} passed.`); + } +} +``` + +--- + +### `startProcess()` + +Start a long-running background process. + +```typescript +const process = await sandbox.startProcess(command: string, options?: ProcessOptions): Promise +``` + +#### Parameters + +- **command** (`string`, required) - The command to start as a background process +- **options** (`ProcessOptions`, optional): + - `cwd` (`string`) - Working directory + - `env` (`Record`) - Environment variables + +#### Return value + +Returns a `Promise` with: + +- `id` (`string`) - Unique process identifier +- `pid` (`number`) - Process ID +- `command` (`string`) - Command that was executed +- `status` (`string`) - Process status (running, exited, etc.) + +#### Examples + +**Start a web server:** + +```typescript +const server = await sandbox.startProcess('python -m http.server 8000'); + +console.log('Server started with PID:', server.pid); +console.log('Process ID:', server.id); + +// Process runs in the background +// You can expose the port +await sandbox.exposePort(8000); +``` + +**Start multiple processes:** + +```typescript +// Start a backend server +const api = await sandbox.startProcess('node api-server.js'); + +// Start a frontend dev server +const frontend = await sandbox.startProcess('npm run dev'); + +console.log('Started API server:', api.pid); +console.log('Started frontend:', frontend.pid); +``` + +**Process with custom environment:** + +```typescript +const process = await sandbox.startProcess('node app.js', { + cwd: '/workspace/my-app', + env: { + NODE_ENV: 'production', + PORT: '3000', + API_KEY: 'secret-key' + } +}); +``` + +--- + +### `listProcesses()` + +List all running processes in the sandbox. + +```typescript +const processes = await sandbox.listProcesses(): Promise +``` + +#### Return value + +Returns a `Promise` with information about each running process. + +#### Example + +```typescript +const processes = await sandbox.listProcesses(); + +for (const proc of processes) { + console.log(`Process ${proc.id}:`); + console.log(` PID: ${proc.pid}`); + console.log(` Command: ${proc.command}`); + console.log(` Status: ${proc.status}`); +} +``` + +--- + +### `killProcess()` + +Terminate a specific process. + +```typescript +await sandbox.killProcess(processId: string, signal?: string): Promise +``` + +#### Parameters + +- **processId** (`string`, required) - The process ID (from `startProcess()` or `listProcesses()`) +- **signal** (`string`, optional) - Signal to send (default: 'SIGTERM') + +#### Example + +```typescript +const server = await sandbox.startProcess('python -m http.server 8000'); + +// Stop the server after some time +setTimeout(async () => { + await sandbox.killProcess(server.id); + console.log('Server stopped'); +}, 60000); +``` + +--- + +### `killAllProcesses()` + +Terminate all running processes in the sandbox. + +```typescript +await sandbox.killAllProcesses(): Promise +``` + +#### Example + +```typescript +// Clean up all processes +await sandbox.killAllProcesses(); +console.log('All processes terminated'); +``` + +--- + +### `streamProcessLogs()` + +Stream logs from a running process in real-time. + +```typescript +const stream = await sandbox.streamProcessLogs(processId: string): Promise +``` + +#### Parameters + +- **processId** (`string`, required) - The process ID + +#### Return value + +Returns a `Promise` that emits `LogEvent` objects. + +#### Example + +```typescript +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')) { + console.log('Server is ready!'); + break; // Stop watching logs + } +} +``` + +--- + +### `getProcessLogs()` + +Get accumulated logs from a process. + +```typescript +const logs = await sandbox.getProcessLogs(processId: string): Promise +``` + +#### Parameters + +- **processId** (`string`, required) - The process ID + +#### Return value + +Returns a `Promise` with all accumulated output from the process. + +#### Example + +```typescript +const server = await sandbox.startProcess('node server.js'); + +// Wait a bit +await new Promise(resolve => setTimeout(resolve, 5000)); + +// Get all logs so far +const logs = await sandbox.getProcessLogs(server.id); +console.log('Server logs:', logs); +``` + +## Common patterns + +### Run and wait + +Execute a command and wait for it to complete: + +```typescript +const result = await sandbox.exec('npm run build'); + +if (result.success) { + console.log('Build succeeded'); +} else { + throw new Error(`Build failed: ${result.stderr}`); +} +``` + +### Run in background + +Start a process and let it run: + +```typescript +const process = await sandbox.startProcess('node server.js'); +await sandbox.exposePort(3000); + +// Process continues running +// Client can access it via the exposed URL +``` + +### Monitor progress + +Stream output and track progress: + +```typescript +const stream = await sandbox.execStream('npm test'); +let passed = 0; +let failed = 0; + +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout') { + if (event.data.includes('✓')) passed++; + if (event.data.includes('✗')) failed++; + } + + if (event.type === 'complete') { + console.log(`Tests: ${passed} passed, ${failed} failed`); + } +} +``` + +### Cleanup processes + +Stop all processes when done: + +```typescript +try { + // Run your code + await sandbox.exec('npm run build'); +} finally { + // Always clean up + await sandbox.killAllProcesses(); +} +``` + +## Related resources + +- [Execute commands guide](/sandbox/guides/execute-commands/) - Detailed guide with best practices +- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running processes +- [Streaming output guide](/sandbox/guides/streaming-output/) - Working with real-time output +- [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..23c1d2f5b015719 --- /dev/null +++ b/src/content/docs/sandbox/api/files.mdx @@ -0,0 +1,499 @@ +--- +title: Files API +pcx_content_type: concept +sidebar: + order: 2 +--- + +The Files API provides methods for reading, writing, and managing files in the sandbox filesystem. All file operations use absolute paths within the container. + +## Methods + +### `writeFile()` + +Write content to a file in the sandbox. + +```typescript +await sandbox.writeFile(path: string, content: string, options?: WriteFileOptions): Promise +``` + +#### Parameters + +- **path** (`string`, required) - Absolute path to the file +- **content** (`string`, required) - Content to write to the file +- **options** (`WriteFileOptions`, optional): + - `encoding` (`string`) - File encoding (default: 'utf-8') + - `mode` (`number`) - File permissions (default: 0o644) + +#### Examples + +**Write a text file:** + +```typescript +await sandbox.writeFile('/workspace/app.js', ` +console.log('Hello from sandbox!'); +`); +``` + +**Write with specific encoding:** + +```typescript +// Write binary data as base64 +await sandbox.writeFile('/tmp/image.png', base64ImageData, { + encoding: 'base64' +}); +``` + +**Write configuration file:** + +```typescript +const config = { + port: 3000, + environment: 'production', + apiKey: process.env.API_KEY +}; + +await sandbox.writeFile( + '/workspace/config.json', + JSON.stringify(config, null, 2) +); +``` + +**Create and write multiple files:** + +```typescript +// Package.json +await sandbox.writeFile('/workspace/package.json', JSON.stringify({ + name: 'my-app', + version: '1.0.0', + dependencies: { + express: '^4.18.0' + } +})); + +// Application code +await sandbox.writeFile('/workspace/server.js', ` +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { + res.json({ status: 'running' }); +}); + +app.listen(3000); +`); + +// Install and run +await sandbox.exec('npm install'); +await sandbox.startProcess('node server.js'); +``` + +--- + +### `readFile()` + +Read a file from the sandbox. + +```typescript +const file = await sandbox.readFile(path: string, options?: ReadFileOptions): Promise +``` + +#### Parameters + +- **path** (`string`, required) - Absolute path to the file +- **options** (`ReadFileOptions`, optional): + - `encoding` (`string`) - File encoding (default: 'utf-8') + +#### Return value + +Returns a `Promise` with: + +- `content` (`string`) - File contents +- `encoding` (`string`) - Encoding used + +#### Examples + +**Read a text file:** + +```typescript +const file = await sandbox.readFile('/workspace/app.js'); +console.log(file.content); +``` + +**Read and parse JSON:** + +```typescript +const file = await sandbox.readFile('/workspace/package.json'); +const packageJson = JSON.parse(file.content); + +console.log('Project name:', packageJson.name); +console.log('Version:', packageJson.version); +``` + +**Read binary file:** + +```typescript +const file = await sandbox.readFile('/tmp/image.png', { + encoding: 'base64' +}); + +// file.content is base64-encoded +const imageData = file.content; +``` + +**Check file contents:** + +```typescript +const file = await sandbox.readFile('/workspace/.env'); + +if (file.content.includes('API_KEY=')) { + console.log('API key is configured'); +} +``` + +--- + +### `mkdir()` + +Create a directory in the sandbox. + +```typescript +await sandbox.mkdir(path: string, options?: MkdirOptions): Promise +``` + +#### Parameters + +- **path** (`string`, required) - Absolute path to the directory +- **options** (`MkdirOptions`, optional): + - `recursive` (`boolean`) - Create parent directories if needed (default: false) + - `mode` (`number`) - Directory permissions (default: 0o755) + +#### Examples + +**Create a single directory:** + +```typescript +await sandbox.mkdir('/workspace/src'); +``` + +**Create nested directories:** + +```typescript +// Create entire path +await sandbox.mkdir('/workspace/src/components/ui', { + recursive: true +}); +``` + +**Create project structure:** + +```typescript +// Create multiple directories +await sandbox.mkdir('/workspace/src', { recursive: true }); +await sandbox.mkdir('/workspace/tests', { recursive: true }); +await sandbox.mkdir('/workspace/public', { recursive: true }); + +// Then create files +await sandbox.writeFile('/workspace/src/index.js', '// Main app'); +await sandbox.writeFile('/workspace/tests/app.test.js', '// Tests'); +``` + +--- + +### `deleteFile()` + +Delete a file from the sandbox. + +```typescript +await sandbox.deleteFile(path: string): Promise +``` + +#### Parameters + +- **path** (`string`, required) - Absolute path to the file + +#### Examples + +**Delete a file:** + +```typescript +await sandbox.deleteFile('/workspace/temp.txt'); +``` + +**Clean up temporary files:** + +```typescript +const tempFiles = [ + '/tmp/build-output.log', + '/tmp/cache.json', + '/workspace/.env.local' +]; + +for (const file of tempFiles) { + try { + await sandbox.deleteFile(file); + } catch (error) { + console.log(`Could not delete ${file}`); + } +} +``` + +--- + +### `renameFile()` + +Rename a file in the sandbox. + +```typescript +await sandbox.renameFile(oldPath: string, newPath: string): Promise +``` + +#### Parameters + +- **oldPath** (`string`, required) - Current file path +- **newPath** (`string`, required) - New file path + +#### Examples + +**Rename a file:** + +```typescript +await sandbox.renameFile( + '/workspace/draft.txt', + '/workspace/final.txt' +); +``` + +**Rename with backup:** + +```typescript +// Create backup before overwriting +await sandbox.renameFile( + '/workspace/config.json', + '/workspace/config.json.backup' +); + +// Write new config +await sandbox.writeFile('/workspace/config.json', newConfig); +``` + +--- + +### `moveFile()` + +Move a file to a different directory. + +```typescript +await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise +``` + +#### Parameters + +- **sourcePath** (`string`, required) - Current file path +- **destinationPath** (`string`, required) - Destination path + +#### Examples + +**Move a file:** + +```typescript +await sandbox.moveFile( + '/tmp/download.txt', + '/workspace/data.txt' +); +``` + +**Organize files:** + +```typescript +// Move source files to src directory +await sandbox.mkdir('/workspace/src', { recursive: true }); + +const files = ['app.js', 'server.js', 'utils.js']; +for (const file of files) { + await sandbox.moveFile( + `/workspace/${file}`, + `/workspace/src/${file}` + ); +} +``` + +--- + +### `gitCheckout()` + +Clone a git repository into the sandbox. + +```typescript +await sandbox.gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promise +``` + +#### Parameters + +- **repoUrl** (`string`, required) - Git repository URL +- **options** (`GitCheckoutOptions`, optional): + - `branch` (`string`) - Branch to checkout (default: main branch) + - `targetDir` (`string`) - Directory to clone into (default: repo name) + - `depth` (`number`) - Clone depth for shallow clone + +#### Examples + +**Clone a repository:** + +```typescript +await sandbox.gitCheckout('https://github.com/user/repo'); +``` + +**Clone specific branch:** + +```typescript +await sandbox.gitCheckout('https://github.com/user/repo', { + branch: 'develop', + targetDir: 'my-project' +}); +``` + +**Clone and build:** + +```typescript +// Clone the repository +await sandbox.gitCheckout('https://github.com/user/app'); + +// Navigate and build +await sandbox.exec('cd app && npm install'); +await sandbox.exec('cd app && npm run build'); + +// Read build output +const buildOutput = await sandbox.readFile('/workspace/app/dist/index.html'); +``` + +**Shallow clone for faster cloning:** + +```typescript +await sandbox.gitCheckout('https://github.com/user/large-repo', { + depth: 1 // Only fetch latest commit +}); +``` + +## Common patterns + +### Create a project structure + +```typescript +// Create directories +await sandbox.mkdir('/workspace/my-app/src', { recursive: true }); +await sandbox.mkdir('/workspace/my-app/tests', { recursive: true }); + +// Create package.json +await sandbox.writeFile('/workspace/my-app/package.json', JSON.stringify({ + name: 'my-app', + version: '1.0.0', + scripts: { + start: 'node src/index.js', + test: 'jest' + } +})); + +// Create main file +await sandbox.writeFile('/workspace/my-app/src/index.js', ` +console.log('App started!'); +`); +``` + +### Read and modify files + +```typescript +// Read existing file +const file = await sandbox.readFile('/workspace/config.json'); +const config = JSON.parse(file.content); + +// Modify +config.lastUpdated = new Date().toISOString(); +config.version = '2.0.0'; + +// Write back +await sandbox.writeFile( + '/workspace/config.json', + JSON.stringify(config, null, 2) +); +``` + +### Copy files + +```typescript +// Read source file +const source = await sandbox.readFile('/workspace/template.html'); + +// Write to new location +await sandbox.writeFile('/workspace/index.html', source.content); +``` + +### Batch operations + +```typescript +const files = { + '/workspace/src/app.js': 'console.log("app");', + '/workspace/src/utils.js': 'console.log("utils");', + '/workspace/README.md': '# My Project' +}; + +// Write all files +await Promise.all( + Object.entries(files).map(([path, content]) => + sandbox.writeFile(path, content) + ) +); +``` + +### Work with binary files + +```typescript +// Read binary file (e.g., from an upload) +const imageBase64 = request.headers.get('X-Image-Data'); + +// Write to sandbox +await sandbox.writeFile('/workspace/upload.png', imageBase64, { + encoding: 'base64' +}); + +// Process with Python +await sandbox.exec('python process-image.py /workspace/upload.png'); + +// Read processed result +const result = await sandbox.readFile('/workspace/output.png', { + encoding: 'base64' +}); +``` + +## Error handling + +File operations may fail if files don't exist or permissions are insufficient: + +```typescript +try { + const file = await sandbox.readFile('/workspace/missing.txt'); +} catch (error) { + if (error.code === 'FILE_NOT_FOUND') { + console.log('File does not exist'); + // Create it + await sandbox.writeFile('/workspace/missing.txt', 'default content'); + } else { + throw error; + } +} +``` + +## Path conventions + +All paths in the sandbox are absolute. Common directories: + +- `/workspace` - Default working directory, recommended for application files +- `/tmp` - Temporary files (may be cleared) +- `/home` - User home directory + +## Related resources + +- [Manage files guide](/sandbox/guides/manage-files/) - Detailed guide with best practices +- [Commands API](/sandbox/api/commands/) - Execute commands +- [Code Interpreter API](/sandbox/api/interpreter/) - Execute code with rich outputs +- [Examples](/sandbox/examples/) - Complete code examples diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx new file mode 100644 index 000000000000000..880151bae9e108d --- /dev/null +++ b/src/content/docs/sandbox/api/index.mdx @@ -0,0 +1,212 @@ +--- +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. + + + + +## Quick reference + +### Command execution + +| Method | Description | +|--------|-------------| +| `exec()` | Execute a command and return results | +| `execStream()` | Execute with streaming Server-Sent Events | +| `startProcess()` | Start a background process | +| `listProcesses()` | List all running processes | +| `killProcess()` | Terminate a specific process | + +### File operations + +| Method | Description | +|--------|-------------| +| `writeFile()` | Write content to a file | +| `readFile()` | Read a file's contents | +| `mkdir()` | Create a directory | +| `deleteFile()` | Delete a file | +| `renameFile()` | Rename a file | +| `moveFile()` | Move a file to a new location | + +### Code interpreter + +| Method | Description | +|--------|-------------| +| `createCodeContext()` | Create a persistent execution context | +| `runCode()` | Execute code with optional streaming | +| `runCodeStream()` | Execute code with SSE streaming | +| `listCodeContexts()` | List all active contexts | +| `deleteCodeContext()` | Delete a specific context | + +### Port management + +| Method | Description | +|--------|-------------| +| `exposePort()` | Expose a port and get a preview URL | +| `unexposePort()` | Remove port exposure | +| `getExposedPorts()` | List all exposed ports with URLs | + +### Git operations + +| Method | Description | +|--------|-------------| +| `gitCheckout()` | Clone a repository into the sandbox | + +### Environment and sessions + +| Method | Description | +|--------|-------------| +| `setEnvVars()` | Set environment variables (must be called first) | +| `createSession()` | Create an isolated execution session | + +## Common patterns + +### Error handling + +All methods return results or throw errors. Use try-catch for error handling: + +```typescript +try { + const result = await sandbox.exec('python script.py'); + if (!result.success) { + console.error('Command failed:', result.stderr); + } +} catch (error) { + console.error('Sandbox error:', error.message); +} +``` + +### Streaming operations + +Many methods support streaming for real-time output: + +```typescript +import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; + +const stream = await sandbox.execStream('npm install'); +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout') { + console.log(event.data); + } +} +``` + +### Working with files + +File operations use absolute paths within the sandbox: + +```typescript +// Write a file +await sandbox.writeFile('/workspace/app.js', code); + +// Read it back +const file = await sandbox.readFile('/workspace/app.js'); +console.log(file.content); + +// Create directories +await sandbox.mkdir('/workspace/src', { recursive: true }); +``` + +## Response types + +### ExecuteResponse + +Returned by `exec()` and similar methods: + +```typescript +interface ExecuteResponse { + success: boolean; // true if exitCode === 0 + stdout: string; // Standard output + stderr: string; // Standard error + exitCode: number; // Process exit code +} +``` + +### ProcessInfo + +Returned by `startProcess()` and `listProcesses()`: + +```typescript +interface ProcessInfo { + id: string; // Unique process identifier + pid: number; // Process ID + command: string; // Command that was executed + status: string; // Process status (running, exited, etc.) + exitCode?: number; // Exit code if process has terminated +} +``` + +### FileInfo + +Returned by `readFile()`: + +```typescript +interface FileInfo { + content: string; // File contents + encoding: string; // File encoding (utf-8, base64, etc.) +} +``` + +## TypeScript support + +The SDK is fully typed. Install the package to get automatic type checking and IntelliSense in your editor: + +```typescript +import type { Sandbox, ExecuteResponse, ProcessInfo } from '@cloudflare/sandbox'; + +const sandbox: Sandbox = getSandbox(env.Sandbox, 'user-123'); +const result: ExecuteResponse = await sandbox.exec('echo test'); +``` + +## Related resources + +- [Get Started](/sandbox/get-started/) - Quick introduction to Sandbox SDK +- [Execute Commands](/sandbox/guides/execute-commands/) - Guide to command execution +- [Manage Files](/sandbox/guides/manage-files/) - Guide to file operations +- [Examples](/sandbox/examples/) - Complete code examples diff --git a/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx index edeadf3b41c4786..2a9eae4a8cf3d67 100644 --- a/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx +++ b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx @@ -7,8 +7,8 @@ products: - Sandbox tags: - AI - - Anthropic - - Code Execution + - Python + - Security sidebar: order: 1 description: Build a production-ready AI code execution system that safely runs Python code generated by Claude. From 084eb5b8e92516aa6716b22c0658832a533edcd6 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 12:48:41 +0100 Subject: [PATCH 04/22] Complete v1 of API reference --- src/content/docs/sandbox/api/commands.mdx | 2 +- src/content/docs/sandbox/api/files.mdx | 2 +- src/content/docs/sandbox/api/index.mdx | 23 +- src/content/docs/sandbox/api/interpreter.mdx | 814 +++++++++++++++++++ src/content/docs/sandbox/api/ports.mdx | 721 ++++++++++++++++ src/content/docs/sandbox/api/sessions.mdx | 551 +++++++++++++ 6 files changed, 2104 insertions(+), 9 deletions(-) create mode 100644 src/content/docs/sandbox/api/interpreter.mdx create mode 100644 src/content/docs/sandbox/api/ports.mdx create mode 100644 src/content/docs/sandbox/api/sessions.mdx diff --git a/src/content/docs/sandbox/api/commands.mdx b/src/content/docs/sandbox/api/commands.mdx index d1449e31a3e2288..9cbbad1a953d6b1 100644 --- a/src/content/docs/sandbox/api/commands.mdx +++ b/src/content/docs/sandbox/api/commands.mdx @@ -1,5 +1,5 @@ --- -title: Commands API +title: Commands pcx_content_type: concept sidebar: order: 1 diff --git a/src/content/docs/sandbox/api/files.mdx b/src/content/docs/sandbox/api/files.mdx index 23c1d2f5b015719..993cfcc0f6dc515 100644 --- a/src/content/docs/sandbox/api/files.mdx +++ b/src/content/docs/sandbox/api/files.mdx @@ -1,5 +1,5 @@ --- -title: Files API +title: Files pcx_content_type: concept sidebar: order: 2 diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 880151bae9e108d..34523af99fd7797 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -49,6 +49,22 @@ The Sandbox SDK is organized into focused APIs: 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. + + ## Quick reference @@ -98,13 +114,6 @@ The Sandbox SDK is organized into focused APIs: |--------|-------------| | `gitCheckout()` | Clone a repository into the sandbox | -### Environment and sessions - -| Method | Description | -|--------|-------------| -| `setEnvVars()` | Set environment variables (must be called first) | -| `createSession()` | Create an isolated execution session | - ## Common patterns ### Error handling diff --git a/src/content/docs/sandbox/api/interpreter.mdx b/src/content/docs/sandbox/api/interpreter.mdx new file mode 100644 index 000000000000000..dca6bf6bae5a78f --- /dev/null +++ b/src/content/docs/sandbox/api/interpreter.mdx @@ -0,0 +1,814 @@ +--- +title: Code Interpreter +pcx_content_type: concept +sidebar: + order: 3 +--- + +The Code Interpreter API provides a high-level interface for executing Python, JavaScript, and TypeScript code with rich output support. Unlike the Commands API which executes arbitrary shell commands, the Code Interpreter is designed specifically for interactive code execution with support for data visualizations, tables, charts, and formatted output. + +## Overview + +The Code Interpreter API is ideal for: +- **Data analysis workflows** - Execute pandas, numpy, matplotlib code +- **Interactive notebooks** - Build Jupyter-like experiences +- **AI code execution** - Run LLM-generated code safely +- **Multi-language execution** - Python, JavaScript, TypeScript in isolated contexts + +Key features: +- **Persistent contexts** - Maintain state across multiple executions +- **Rich outputs** - Charts, tables, images, HTML, LaTeX, JSON +- **Streaming support** - Real-time output with callbacks +- **Multi-language** - Python 3.11, Node.js with TypeScript support +- **Automatic retries** - Built-in retry logic for transient errors + +## Methods + +### `createCodeContext()` + +Create a persistent execution context for running code. Contexts maintain state (variables, imports, functions) across multiple code executions. + +```typescript +const context = await sandbox.createCodeContext(options?: CreateContextOptions): Promise +``` + +#### Parameters + +- **options** (`CreateContextOptions`, optional): + - `language` (`"python" | "javascript" | "typescript"`) - Programming language (default: `"python"`) + - `cwd` (`string`) - Working directory (default: `"/workspace"`) + - `envVars` (`Record`) - Environment variables + - `timeout` (`number`) - Request timeout in milliseconds (default: 30000) + +#### Return value + +Returns a `Promise` with: + +- `id` (`string`) - Unique context identifier +- `language` (`string`) - Programming language +- `cwd` (`string`) - Working directory +- `createdAt` (`Date`) - When context was created +- `lastUsed` (`Date`) - When context was last used + +#### Examples + +**Create Python context:** + +```typescript +const pythonCtx = await sandbox.createCodeContext({ + language: 'python', + cwd: '/workspace' +}); + +console.log('Context ID:', pythonCtx.id); +``` + +**Create JavaScript context with environment variables:** + +```typescript +const jsCtx = await sandbox.createCodeContext({ + language: 'javascript', + envVars: { + NODE_ENV: 'production', + API_KEY: process.env.API_KEY + } +}); +``` + +**Create TypeScript context:** + +```typescript +const tsCtx = await sandbox.createCodeContext({ + language: 'typescript', + cwd: '/workspace/app' +}); +``` + +--- + +### `runCode()` + +Execute code in a context and return the complete result with all outputs. + +```typescript +const result = await sandbox.runCode(code: string, options?: RunCodeOptions): Promise +``` + +#### Parameters + +- **code** (`string`, required) - The code to execute +- **options** (`RunCodeOptions`, optional): + - `context` (`CodeContext`) - Context to run in (creates default if not provided) + - `language` (`"python" | "javascript" | "typescript"`) - Language if no context provided + - `envVars` (`Record`) - Environment variables for this execution + - `timeout` (`number`) - Execution timeout in milliseconds (default: 60000) + - `signal` (`AbortSignal`) - For cancelling execution + - `onStdout` (`(output: OutputMessage) => void`) - Callback for stdout + - `onStderr` (`(output: OutputMessage) => void`) - Callback for stderr + - `onResult` (`(result: Result) => void`) - Callback for results + - `onError` (`(error: ExecutionError) => void`) - Callback for errors + +#### Return value + +Returns a `Promise` with: + +- `code` (`string`) - Code that was executed +- `logs` (`object`) - Output logs + - `stdout` (`string[]`) - Standard output lines + - `stderr` (`string[]`) - Standard error lines +- `results` (`Result[]`) - Rich execution results (charts, tables, etc.) +- `error` (`ExecutionError`) - Execution error if any +- `executionCount` (`number`) - Execution counter + +#### Examples + +**Basic Python execution:** + +```typescript +const result = await sandbox.runCode(` +print("Hello from Python!") +x = 42 +x * 2 +`); + +console.log('Stdout:', result.logs.stdout); // ["Hello from Python!"] +console.log('Result:', result.results[0].text); // "84" +``` + +**Python with data analysis:** + +```typescript +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +const result = await sandbox.runCode(` +import statistics + +data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +mean = statistics.mean(data) +median = statistics.median(data) +stdev = statistics.stdev(data) + +print(f"Mean: {mean}") +print(f"Median: {median}") +print(f"Std Dev: {stdev:.2f}") + +mean # Last expression is returned +`, { context: ctx }); + +console.log('Logs:', result.logs.stdout); +// ["Mean: 5.5", "Median: 5.5", "Std Dev: 3.03"] + +console.log('Result:', result.results[0].text); // "5.5" +``` + +**JavaScript execution:** + +```typescript +const result = await sandbox.runCode(` +const data = [1, 2, 3, 4, 5]; +const sum = data.reduce((a, b) => a + b, 0); +const avg = sum / data.length; + +console.log('Average:', avg); +avg; +`, { language: 'javascript' }); + +console.log(result.logs.stdout); // ["Average: 3"] +console.log(result.results[0].text); // "3" +``` + +**Execution with streaming callbacks:** + +```typescript +const result = await sandbox.runCode(` +for i in range(5): + print(f"Processing item {i}...") +`, { + language: 'python', + onStdout: (output) => { + console.log('[STDOUT]', output.text); + }, + onStderr: (output) => { + console.error('[STDERR]', output.text); + }, + onResult: (result) => { + console.log('[RESULT]', result.text); + }, + onError: (error) => { + console.error('[ERROR]', error.name, error.value); + } +}); + +// Logs appear in real-time: +// [STDOUT] Processing item 0... +// [STDOUT] Processing item 1... +// ... +``` + +**Error handling:** + +```typescript +try { + const result = await sandbox.runCode(` +x = 1 / 0 # Division by zero +`, { language: 'python' }); + + if (result.error) { + console.error('Execution error:'); + console.error('Name:', result.error.name); // "ZeroDivisionError" + console.error('Message:', result.error.value); // "division by zero" + console.error('Traceback:', result.error.traceback); + } +} catch (error) { + console.error('Failed to execute:', error.message); +} +``` + +**Working with persistent context:** + +```typescript +// Create context +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +// First execution - define variables +await sandbox.runCode(` +import math +radius = 5 +`, { context: ctx }); + +// Second execution - use variables from previous execution +const result = await sandbox.runCode(` +area = math.pi * radius ** 2 +print(f"Area: {area:.2f}") +area +`, { context: ctx }); + +console.log('Result:', result.results[0].text); // "78.53981633974483" +``` + +**AI code execution example:** + +```typescript +import Anthropic from '@anthropic-ai/sdk'; + +// Generate code with Claude +const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); +const response = await anthropic.messages.create({ + model: 'claude-3-5-sonnet-latest', + max_tokens: 1024, + messages: [{ + role: 'user', + content: 'Write Python code to calculate the factorial of 10' + }] +}); + +const generatedCode = response.content[0].text; + +// Execute safely in sandbox +const result = await sandbox.runCode(generatedCode, { + language: 'python', + timeout: 5000 // 5 second timeout +}); + +console.log('Result:', result.results[0].text); +``` + +--- + +### `listCodeContexts()` + +List all active code execution contexts for the sandbox. + +```typescript +const contexts = await sandbox.listCodeContexts(): Promise +``` + +#### Return value + +Returns a `Promise` with all active contexts. + +#### Example + +```typescript +const contexts = await sandbox.listCodeContexts(); + +for (const ctx of contexts) { + console.log(`Context: ${ctx.id}`); + console.log(` Language: ${ctx.language}`); + console.log(` Created: ${ctx.createdAt.toISOString()}`); + console.log(` Last used: ${ctx.lastUsed.toISOString()}`); +} +``` + +--- + +### `deleteCodeContext()` + +Delete a code execution context and free its resources. + +```typescript +await sandbox.deleteCodeContext(contextId: string): Promise +``` + +#### Parameters + +- **contextId** (`string`, required) - The context ID to delete + +#### Example + +```typescript +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +// Use the context... +await sandbox.runCode('print("Hello")', { context: ctx }); + +// Clean up when done +await sandbox.deleteCodeContext(ctx.id); +console.log('Context deleted'); +``` + +**Cleanup pattern:** + +```typescript +// List and clean up old contexts +const contexts = await sandbox.listCodeContexts(); + +for (const ctx of contexts) { + const ageMinutes = (Date.now() - ctx.lastUsed.getTime()) / 1000 / 60; + + // Delete contexts older than 30 minutes + if (ageMinutes > 30) { + await sandbox.deleteCodeContext(ctx.id); + console.log(`Deleted old context: ${ctx.id}`); + } +} +``` + +## Rich Output Formats + +The Code Interpreter supports multiple output formats for data visualization and presentation: + +### Result Object + +Each execution returns `Result` objects with these properties: + +```typescript +interface Result { + text?: string; // Plain text representation + html?: string; // HTML tables, formatted output + png?: string; // PNG image (base64) + jpeg?: string; // JPEG image (base64) + svg?: string; // SVG vector graphics + latex?: string; // LaTeX mathematical notation + markdown?: string; // Markdown formatted text + javascript?: string; // JavaScript code + json?: any; // JSON data + chart?: ChartData; // Chart/visualization data + data?: any; // Raw data object + + formats(): string[]; // List available formats +} +``` + +### Working with Charts + +**Python with matplotlib:** + +```typescript +const result = await sandbox.runCode(` +import matplotlib.pyplot as plt +import numpy as np + +x = np.linspace(0, 10, 100) +y = np.sin(x) + +plt.figure(figsize=(10, 6)) +plt.plot(x, y) +plt.title('Sine Wave') +plt.xlabel('x') +plt.ylabel('sin(x)') +plt.grid(True) +plt.show() +`, { language: 'python' }); + +// Access the chart image +if (result.results[0]?.png) { + const imageData = result.results[0].png; + // imageData is base64-encoded PNG + console.log('Chart generated'); +} +``` + +**Return image to client:** + +```typescript +const result = await sandbox.runCode(visualizationCode, { + language: 'python' +}); + +// Return chart as image response +if (result.results[0]?.png) { + const imageBuffer = Buffer.from(result.results[0].png, 'base64'); + return new Response(imageBuffer, { + headers: { 'Content-Type': 'image/png' } + }); +} +``` + +### Working with Tables + +**Python with pandas:** + +```typescript +const result = await sandbox.runCode(` +import pandas as pd + +data = { + 'Name': ['Alice', 'Bob', 'Charlie'], + 'Age': [25, 30, 35], + 'City': ['NYC', 'LA', 'Chicago'] +} + +df = pd.DataFrame(data) +df # Returns HTML table representation +`, { language: 'python' }); + +// Access HTML table +if (result.results[0]?.html) { + const tableHtml = result.results[0].html; + return new Response(tableHtml, { + headers: { 'Content-Type': 'text/html' } + }); +} +``` + +### Working with JSON Data + +```typescript +const result = await sandbox.runCode(` +import json + +data = { + 'status': 'success', + 'items': [1, 2, 3, 4, 5], + 'summary': { + 'total': 15, + 'average': 3.0 + } +} + +json.dumps(data) +`, { language: 'python' }); + +// Access JSON result +if (result.results[0]?.json) { + const jsonData = result.results[0].json; + return Response.json(jsonData); +} +``` + +## Common Patterns + +### Interactive data analysis + +```typescript +// Create persistent context for analysis session +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +// Load data +await sandbox.runCode(` +import pandas as pd +import numpy as np + +# Load dataset +df = pd.read_csv('/workspace/data.csv') +print(f"Loaded {len(df)} rows") +`, { context: ctx }); + +// Analyze data +const analysis = await sandbox.runCode(` +# Calculate statistics +summary = df.describe() +summary +`, { context: ctx }); + +console.log('Stats:', analysis.results[0].html); + +// Visualize data +const viz = await sandbox.runCode(` +import matplotlib.pyplot as plt + +df['value'].hist(bins=20) +plt.title('Distribution') +plt.show() +`, { context: ctx }); + +// Return chart +if (viz.results[0]?.png) { + // Use chart image... +} +``` + +### Multi-language workflow + +```typescript +// Python for data processing +const pythonCtx = await sandbox.createCodeContext({ language: 'python' }); + +const processed = await sandbox.runCode(` +import json + +data = [1, 2, 3, 4, 5] +result = { + 'sum': sum(data), + 'avg': sum(data) / len(data) +} + +# Write results to file +with open('/workspace/results.json', 'w') as f: + json.dump(result, f) + +result +`, { context: pythonCtx }); + +// JavaScript for presentation +const jsResult = await sandbox.runCode(` +const fs = require('fs'); +const data = JSON.parse(fs.readFileSync('/workspace/results.json', 'utf8')); + +console.log('Sum:', data.sum); +console.log('Average:', data.avg); + +data; +`, { language: 'javascript' }); + +console.log(jsResult.logs.stdout); +``` + +### AI-powered data analysis + +```typescript +import OpenAI from 'openai'; + +async function analyzeData(question: string, dataPath: string) { + const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY }); + + // Ask AI to generate analysis code + const completion = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ + role: 'user', + content: `Generate Python pandas code to answer: "${question}". +Data is in: ${dataPath}. Return only code, no markdown.` + }] + }); + + const code = completion.choices[0].message.content; + + // Execute in sandbox + const result = await sandbox.runCode(code, { + language: 'python', + timeout: 30000, + onStdout: (output) => console.log(output.text) + }); + + return result; +} + +// Use it +const analysis = await analyzeData( + 'What is the average value by category?', + '/workspace/sales.csv' +); + +console.log(analysis.logs.stdout); +if (analysis.results[0]) { + console.log('Result:', analysis.results[0].text); +} +``` + +### Real-time execution monitoring + +```typescript +let progress = 0; + +const result = await sandbox.runCode(` +import time + +for i in range(10): + print(f"Step {i+1}/10") + time.sleep(0.5) + +print("Complete!") +`, { + language: 'python', + onStdout: (output) => { + if (output.text.startsWith('Step')) { + progress++; + console.log(`Progress: ${progress * 10}%`); + } + } +}); +``` + +### Error recovery + +```typescript +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +async function executeWithRetry(code: string, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + const result = await sandbox.runCode(code, { + context: ctx, + timeout: 10000 + }); + + if (!result.error) { + return result; + } + + console.log(`Attempt ${attempt + 1} failed:`, result.error.value); + + // Don't retry on syntax errors + if (result.error.name === 'SyntaxError') { + throw new Error('Syntax error in code'); + } + + // Wait before retry + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + + throw new Error('Max retries exceeded'); +} + +try { + const result = await executeWithRetry('import requests; ...'); + console.log('Success:', result.results[0].text); +} catch (error) { + console.error('Failed:', error.message); +} +``` + +## Error Handling + +### Execution Errors + +The Code Interpreter returns structured error information: + +```typescript +interface ExecutionError { + name: string; // Error type (e.g., 'NameError', 'SyntaxError') + value: string; // Error message + traceback: string[]; // Full stack trace + lineNumber?: number; // Line number where error occurred +} +``` + +**Example error handling:** + +```typescript +const result = await sandbox.runCode(` +undefined_variable +`, { language: 'python' }); + +if (result.error) { + console.error('Error Type:', result.error.name); + console.error('Error Message:', result.error.value); + console.error('Stack Trace:'); + result.error.traceback.forEach(line => console.error(line)); + + if (result.error.lineNumber) { + console.error('Error at line:', result.error.lineNumber); + } +} + +// Output: +// Error Type: NameError +// Error Message: name 'undefined_variable' is not defined +// Stack Trace: [traceback lines...] +``` + +### Timeout Handling + +```typescript +try { + const result = await sandbox.runCode(` +import time +time.sleep(120) # Sleep for 2 minutes +`, { + language: 'python', + timeout: 5000 // 5 second timeout +}); +} catch (error) { + if (error.message.includes('timeout')) { + console.error('Code execution timed out'); + } else { + throw error; + } +} +``` + +### Context Not Ready + +The Code Interpreter includes automatic retry logic for initialization: + +```typescript +// First execution might need time to initialize +const result = await sandbox.runCode(`print("Hello")`, { + language: 'python' +}); + +// The SDK automatically retries if the interpreter is not ready +// Default: 3 retries with exponential backoff +``` + +## Best Practices + +### 1. Use persistent contexts for related executions + +```typescript +// ✅ Good - reuse context +const ctx = await sandbox.createCodeContext({ language: 'python' }); +await sandbox.runCode('import pandas as pd', { context: ctx }); +await sandbox.runCode('df = pd.read_csv("data.csv")', { context: ctx }); +await sandbox.runCode('df.describe()', { context: ctx }); + +// ❌ Bad - creates new context each time +await sandbox.runCode('import pandas as pd', { language: 'python' }); +await sandbox.runCode('df = pd.read_csv("data.csv")', { language: 'python' }); // Import lost! +``` + +### 2. Set appropriate timeouts + +```typescript +// Short timeout for quick operations +await sandbox.runCode('2 + 2', { + language: 'python', + timeout: 1000 // 1 second +}); + +// Longer timeout for data processing +await sandbox.runCode(dataAnalysisCode, { + language: 'python', + timeout: 60000 // 1 minute +}); +``` + +### 3. Clean up contexts + +```typescript +const ctx = await sandbox.createCodeContext({ language: 'python' }); + +try { + // Use context... + await sandbox.runCode(code, { context: ctx }); +} finally { + // Always clean up + await sandbox.deleteCodeContext(ctx.id); +} +``` + +### 4. Handle errors gracefully + +```typescript +const result = await sandbox.runCode(userCode, { + language: 'python', + onError: (error) => { + // Log error but don't crash + console.error('Execution error:', error.name, error.value); + } +}); + +if (result.error) { + return Response.json({ + success: false, + error: result.error.value, + traceback: result.error.traceback + }, { status: 400 }); +} +``` + +### 5. Use callbacks for long-running operations + +```typescript +let output = ''; + +await sandbox.runCode(longRunningCode, { + language: 'python', + onStdout: (msg) => { + output += msg.text + '\n'; + // Stream to client, log, update UI, etc. + }, + onResult: (result) => { + // Process result as it becomes available + console.log('Got result:', result.text); + } +}); +``` + +## Related Resources + +- [Build an AI Code Executor tutorial](/sandbox/tutorials/build-an-ai-code-executor/) - Complete AI integration example +- [Commands API](/sandbox/api/commands/) - Lower-level command execution +- [Files API](/sandbox/api/files/) - File operations +- [API Overview](/sandbox/api/) - Complete API reference diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx new file mode 100644 index 000000000000000..ea14a83acd67b44 --- /dev/null +++ b/src/content/docs/sandbox/api/ports.mdx @@ -0,0 +1,721 @@ +--- +title: Ports +pcx_content_type: concept +sidebar: + order: 4 +--- + +The Ports API enables you to expose services running in your sandbox to the internet via preview URLs. This allows you to run web servers, APIs, or any network service inside the sandbox and access them from outside. + +## Overview + +When you start a service in the sandbox (like a web server), it runs on localhost inside the container. The Ports API creates a publicly accessible URL that proxies requests to your service, enabling: + +- **Web development** - Run development servers and preview changes +- **API testing** - Test backend services in isolation +- **Interactive demos** - Share running applications with others +- **Multi-service apps** - Expose multiple ports for microservices + +Key features: +- **Automatic URL generation** - Get a unique preview URL for each exposed port +- **Secure proxying** - All traffic is proxied through Cloudflare's network +- **Multiple ports** - Expose several services simultaneously +- **Named ports** - Assign friendly names to organize multiple exposed ports + +## Methods + +### `exposePort()` + +Expose a port running in the sandbox and get a preview URL. + +```typescript +const response = await sandbox.exposePort(port: number, options?: ExposePortOptions): Promise +``` + +#### Parameters + +- **port** (`number`, required) - Port number to expose (1-65535) +- **options** (`ExposePortOptions`, optional): + - `name` (`string`) - Friendly name for the port (useful when exposing multiple ports) + +#### Return value + +Returns a `Promise` with: + +- `port` (`number`) - The exposed port number +- `exposedAt` (`string`) - The public preview URL +- `name` (`string`, optional) - The name if provided +- `success` (`boolean`) - Whether the operation succeeded +- `timestamp` (`string`) - ISO timestamp of the operation + +#### Examples + +**Expose a web server:** + +```typescript +import { getSandbox } from '@cloudflare/sandbox'; + +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'demo-user'); + + // Start a Python web server + await sandbox.startProcess('python -m http.server 8000'); + + // Expose port 8000 + const exposed = await sandbox.exposePort(8000); + + return Response.json({ + message: 'Server running', + url: exposed.exposedAt, + port: exposed.port + }); + } +}; + +// Response: +// { +// "message": "Server running", +// "url": "https://abc123-8000.sandbox.workers.dev", +// "port": 8000 +// } +``` + +**Expose with a friendly name:** + +```typescript +// Start a Node.js API server +await sandbox.startProcess('node api-server.js'); + +// Expose with a descriptive name +const api = await sandbox.exposePort(3000, { + name: 'api-server' +}); + +console.log('API available at:', api.exposedAt); +console.log('Port name:', api.name); // "api-server" +``` + +**Expose multiple services:** + +```typescript +// Start backend API +await sandbox.startProcess('node api.js'); +const api = await sandbox.exposePort(3000, { name: 'api' }); + +// Start frontend dev server +await sandbox.startProcess('npm run dev'); +const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); + +// Start database admin panel +await sandbox.startProcess('python db-admin.py'); +const admin = await sandbox.exposePort(8080, { name: 'admin' }); + +return Response.json({ + services: { + api: api.exposedAt, + frontend: frontend.exposedAt, + admin: admin.exposedAt + } +}); +``` + +**Full development environment:** + +```typescript +// Clone a repository +await sandbox.gitCheckout('https://github.com/user/my-app'); + +// Install dependencies +await sandbox.exec('cd my-app && npm install'); + +// Start the development server +await sandbox.startProcess('cd my-app && npm run dev'); + +// Wait a moment for server to start +await new Promise(resolve => setTimeout(resolve, 2000)); + +// Expose the dev server +const dev = await sandbox.exposePort(3000, { name: 'dev-server' }); + +return Response.json({ + message: 'Development environment ready', + url: dev.exposedAt +}); +``` + +--- + +### `unexposePort()` + +Remove a previously exposed port and close its preview URL. + +```typescript +await sandbox.unexposePort(port: number): Promise +``` + +#### Parameters + +- **port** (`number`, required) - Port number to unexpose + +#### Return value + +Returns a `Promise` with: + +- `port` (`number`) - The port that was unexposed +- `success` (`boolean`) - Whether the operation succeeded +- `timestamp` (`string`) - ISO timestamp of the operation + +#### Examples + +**Unexpose a port:** + +```typescript +// Expose a port +const exposed = await sandbox.exposePort(8000); +console.log('Exposed at:', exposed.exposedAt); + +// Later, clean up +await sandbox.unexposePort(8000); +console.log('Port 8000 is no longer accessible'); +``` + +**Cleanup multiple ports:** + +```typescript +const ports = [3000, 5173, 8080]; + +// Expose all ports +for (const port of ports) { + await sandbox.exposePort(port); +} + +// Later, clean up all ports +for (const port of ports) { + await sandbox.unexposePort(port); +} + +console.log('All ports unexposed'); +``` + +**Conditional unexpose:** + +```typescript +// Get currently exposed ports +const { ports } = await sandbox.getExposedPorts(); + +// Unexpose only specific ports +for (const portInfo of ports) { + if (portInfo.name === 'temporary-service') { + await sandbox.unexposePort(portInfo.port); + console.log(`Cleaned up ${portInfo.name}`); + } +} +``` + +--- + +### `getExposedPorts()` + +Get information about all currently exposed ports. + +```typescript +const response = await sandbox.getExposedPorts(): Promise +``` + +#### Return value + +Returns a `Promise` with: + +- `ports` (`ExposedPortInfo[]`) - Array of exposed port information + - `port` (`number`) - Port number + - `exposedAt` (`string`) - Preview URL + - `name` (`string`, optional) - Port name if provided +- `count` (`number`) - Total number of exposed ports +- `success` (`boolean`) - Whether the operation succeeded +- `timestamp` (`string`) - ISO timestamp of the operation + +#### Examples + +**List all exposed ports:** + +```typescript +const { ports, count } = await sandbox.getExposedPorts(); + +console.log(`${count} ports exposed:`); + +for (const port of ports) { + console.log(` Port ${port.port}: ${port.exposedAt}`); + if (port.name) { + console.log(` Name: ${port.name}`); + } +} +``` + +**Check if a specific port is exposed:** + +```typescript +const { ports } = await sandbox.getExposedPorts(); + +const isExposed = ports.some(p => p.port === 3000); + +if (!isExposed) { + console.log('Port 3000 is not exposed, exposing now...'); + await sandbox.exposePort(3000); +} +``` + +**Return exposed services to client:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + if (url.pathname === '/services') { + const { ports } = await sandbox.getExposedPorts(); + + const services = ports.map(p => ({ + name: p.name || `Port ${p.port}`, + port: p.port, + url: p.exposedAt + })); + + return Response.json({ services }); + } + + return new Response('Not found', { status: 404 }); + } +}; +``` + +**Monitor exposed ports:** + +```typescript +// Check which ports are exposed +const before = await sandbox.getExposedPorts(); +console.log('Ports before:', before.count); + +// Start some services +await sandbox.startProcess('node api.js'); +await sandbox.exposePort(3000, { name: 'api' }); + +await sandbox.startProcess('npm run dev'); +await sandbox.exposePort(5173, { name: 'frontend' }); + +// Check again +const after = await sandbox.getExposedPorts(); +console.log('Ports after:', after.count); +console.log('New ports:', after.ports.map(p => p.name)); +``` + +## Common Patterns + +### Start and expose a service + +The typical workflow for running a web service: + +```typescript +// 1. Start the service as a background process +const process = await sandbox.startProcess('node server.js'); +console.log('Server started with PID:', process.pid); + +// 2. Wait a moment for the service to initialize +await new Promise(resolve => setTimeout(resolve, 2000)); + +// 3. Expose the port +const exposed = await sandbox.exposePort(3000, { + name: 'my-api' +}); + +// 4. Return the URL to the client +return Response.json({ + status: 'running', + url: exposed.exposedAt, + processId: process.id +}); +``` + +### Development workflow + +Complete development environment setup: + +```typescript +async function setupDevEnvironment(sandbox: Sandbox, repoUrl: string) { + // Clone the repository + await sandbox.gitCheckout(repoUrl); + + // Extract repo name for directory + const repoName = repoUrl.split('/').pop()?.replace('.git', '') || 'repo'; + + // Install dependencies + await sandbox.exec(`cd ${repoName} && npm install`); + + // Start the dev server in background + const server = await sandbox.startProcess( + `cd ${repoName} && npm run dev` + ); + + // Give server time to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Expose the dev server port + const exposed = await sandbox.exposePort(3000, { + name: 'dev-server' + }); + + return { + processId: server.id, + url: exposed.exposedAt, + repository: repoUrl + }; +} + +// Usage +const env = await setupDevEnvironment( + sandbox, + 'https://github.com/user/my-app' +); + +console.log('Dev environment ready:', env.url); +``` + +### Microservices architecture + +Run and expose multiple services: + +```typescript +interface Service { + name: string; + command: string; + port: number; +} + +const services: Service[] = [ + { name: 'api', command: 'node api/server.js', port: 3000 }, + { name: 'auth', command: 'node auth/server.js', port: 3001 }, + { name: 'websocket', command: 'node ws/server.js', port: 3002 }, + { name: 'admin', command: 'python admin/app.py', port: 8080 } +]; + +const exposedServices = []; + +for (const service of services) { + // Start the service + const process = await sandbox.startProcess(service.command); + + // Wait briefly + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Expose the port + const exposed = await sandbox.exposePort(service.port, { + name: service.name + }); + + exposedServices.push({ + name: service.name, + url: exposed.exposedAt, + processId: process.id + }); +} + +return Response.json({ + message: 'All services running', + services: exposedServices +}); +``` + +### Temporary preview URLs + +Create temporary preview URLs that clean up automatically: + +```typescript +async function createTemporaryPreview( + sandbox: Sandbox, + command: string, + port: number, + durationMs: number = 300000 // 5 minutes default +) { + // Start service and expose port + const process = await sandbox.startProcess(command); + await new Promise(resolve => setTimeout(resolve, 2000)); + + const exposed = await sandbox.exposePort(port, { + name: 'temporary-preview' + }); + + // Schedule cleanup + setTimeout(async () => { + await sandbox.killProcess(process.id); + await sandbox.unexposePort(port); + console.log('Temporary preview cleaned up'); + }, durationMs); + + return exposed.exposedAt; +} + +// Usage +const previewUrl = await createTemporaryPreview( + sandbox, + 'npm run preview', + 4173, + 600000 // 10 minutes +); + +console.log('Preview available for 10 minutes:', previewUrl); +``` + +### Health checking + +Verify a service is ready before exposing: + +```typescript +async function exposeWithHealthCheck( + sandbox: Sandbox, + command: string, + port: number, + maxRetries: number = 10 +): Promise { + // Start the service + const process = await sandbox.startProcess(command); + + // Poll until service is ready + for (let i = 0; i < maxRetries; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check if service is responding + const check = await sandbox.exec(`curl -f http://localhost:${port}/health || true`); + + if (check.success && check.stdout.includes('ok')) { + // Service is ready, expose it + const exposed = await sandbox.exposePort(port); + return exposed.exposedAt; + } + + console.log(`Waiting for service to be ready... (${i + 1}/${maxRetries})`); + } + + throw new Error('Service failed to become ready'); +} + +// Usage +try { + const url = await exposeWithHealthCheck( + sandbox, + 'node server.js', + 3000 + ); + console.log('Service ready:', url); +} catch (error) { + console.error('Service failed to start:', error.message); +} +``` + +## Error Handling + +### Port already exposed + +```typescript +try { + await sandbox.exposePort(3000); +} catch (error) { + if (error.message.includes('already exposed')) { + console.log('Port 3000 is already exposed'); + + // Get the existing URL + const { ports } = await sandbox.getExposedPorts(); + const existing = ports.find(p => p.port === 3000); + + if (existing) { + console.log('Using existing URL:', existing.exposedAt); + } + } else { + throw error; + } +} +``` + +### Port not in use + +```typescript +// Try to expose a port that nothing is listening on +try { + await sandbox.exposePort(9999); +} catch (error) { + console.error('Port 9999 is not in use'); + console.log('Make sure a service is running on this port first'); + + // Start a service on that port + await sandbox.startProcess('python -m http.server 9999'); + + // Wait and try again + await new Promise(resolve => setTimeout(resolve, 2000)); + await sandbox.exposePort(9999); +} +``` + +### Cleanup on error + +```typescript +async function safeExposePort(sandbox: Sandbox, port: number) { + try { + const exposed = await sandbox.exposePort(port); + return exposed.exposedAt; + } catch (error) { + console.error(`Failed to expose port ${port}:`, error.message); + + // Attempt cleanup + try { + await sandbox.unexposePort(port); + } catch { + // Ignore cleanup errors + } + + throw error; + } +} +``` + +## Best Practices + +### 1. Wait for services to start + +Always wait briefly after starting a process before exposing its port: + +```typescript +// ✅ Good - wait for service to start +await sandbox.startProcess('npm run dev'); +await new Promise(resolve => setTimeout(resolve, 2000)); +await sandbox.exposePort(3000); + +// ❌ Bad - expose immediately +await sandbox.startProcess('npm run dev'); +await sandbox.exposePort(3000); // May fail if service isn't ready +``` + +### 2. Use named ports for clarity + +When exposing multiple ports, use names to keep track: + +```typescript +// ✅ Good - named ports +await sandbox.exposePort(3000, { name: 'api' }); +await sandbox.exposePort(5173, { name: 'frontend' }); +await sandbox.exposePort(8080, { name: 'admin' }); + +// ❌ Less clear - no names +await sandbox.exposePort(3000); +await sandbox.exposePort(5173); +await sandbox.exposePort(8080); +``` + +### 3. Clean up exposed ports + +Always unexpose ports when done: + +```typescript +try { + await sandbox.exposePort(3000); + // Use the service... +} finally { + // Always clean up + await sandbox.unexposePort(3000); +} +``` + +### 4. Check before exposing + +Verify a port isn't already exposed: + +```typescript +const { ports } = await sandbox.getExposedPorts(); +const isExposed = ports.some(p => p.port === 3000); + +if (!isExposed) { + await sandbox.exposePort(3000); +} +``` + +### 5. Handle port ranges + +Use valid port numbers (1-65535), avoiding reserved ranges: + +```typescript +// ✅ Good - user port range +await sandbox.exposePort(3000); // Typical web dev +await sandbox.exposePort(8080); // Typical HTTP alternate +await sandbox.exposePort(5432); // Database port + +// ❌ Avoid system ports (0-1023) unless necessary +await sandbox.exposePort(80); // May require privileges +await sandbox.exposePort(443); // May require privileges +``` + +## Preview URL Format + +Preview URLs follow this pattern: + +``` +https://{sandbox-id}-{port}.sandbox.workers.dev +``` + +For example: +- Port 3000: `https://abc123-3000.sandbox.workers.dev` +- Port 8080: `https://abc123-8080.sandbox.workers.dev` + +The URL remains stable for the lifetime of the exposed port and automatically proxies requests to your service running inside the sandbox. + +## Security Considerations + +### Public accessibility + +**Important**: Exposed ports are publicly accessible on the internet. Anyone with the URL can access your service. + +```typescript +// Be aware: This API is now public +await sandbox.exposePort(3000); + +// Consider adding authentication in your service +await sandbox.writeFile('/workspace/api.js', ` +const express = require('express'); +const app = express(); + +// Add authentication middleware +app.use((req, res, next) => { + const token = req.headers.authorization; + if (token !== 'Bearer secret-token') { + return res.status(401).json({ error: 'Unauthorized' }); + } + next(); +}); + +app.get('/data', (req, res) => { + res.json({ message: 'Protected data' }); +}); + +app.listen(3000); +`); +``` + +### Temporary URLs + +For security-sensitive applications, unexpose ports when no longer needed: + +```typescript +// Expose temporarily +const exposed = await sandbox.exposePort(3000); + +// Use the service +await doWork(exposed.exposedAt); + +// Clean up immediately +await sandbox.unexposePort(3000); +``` + +## Related Resources + +- [Commands API](/sandbox/api/commands/) - Start background processes +- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running services +- [Build a dev environment tutorial](/sandbox/tutorials/dev-environment/) - Complete dev environment example +- [API Overview](/sandbox/api/) - Complete API reference diff --git a/src/content/docs/sandbox/api/sessions.mdx b/src/content/docs/sandbox/api/sessions.mdx new file mode 100644 index 000000000000000..b8c93505990a606 --- /dev/null +++ b/src/content/docs/sandbox/api/sessions.mdx @@ -0,0 +1,551 @@ +--- +title: Sessions +pcx_content_type: configuration +sidebar: + order: 5 +--- + +Sessions provide isolated execution contexts within a sandbox, similar to having multiple terminal panes open on your laptop. Each session maintains its own shell state, environment variables, and working directory across multiple invocations. + +:::note +Sessions are an advanced feature. Most applications work fine with the default session that's automatically created. Use explicit sessions when you need isolated environments or persistent shell state. +::: + +## When to use sessions + +### Use sessions when you need: + +- **Isolated environments** - Different users or workflows with separate configurations +- **Persistent shell state** - Environment variables and working directory that persist across commands +- **Parallel execution** - Multiple independent execution contexts running simultaneously +- **Session resumption** - Continue work from previous requests (e.g., in chat applications) + +### You don't need sessions for: + +- **Simple command execution** - The default session handles this automatically +- **Background processes** - Processes are visible across all sessions (like on your local machine) +- **File operations** - Files are shared across sessions + +## Default session behavior + +When you use sandbox methods without explicitly creating a session, a default session is automatically created with: + +```typescript +const sandbox = getSandbox(env.Sandbox, 'user-123'); + +// These automatically use the default session +await sandbox.exec('export API_KEY=secret'); +await sandbox.exec('echo $API_KEY'); // Prints: secret + +// Environment persists within the default session +await sandbox.writeFile('/workspace/app.js', code); +await sandbox.exec('node app.js'); // Can access API_KEY +``` + +The default session persists for the lifetime of the sandbox instance, maintaining shell state across all operations. + +## API methods + +### createSession() + +Create a new isolated execution session with custom environment and working directory. + +```typescript +const session = await sandbox.createSession(options?: SessionOptions): Promise +``` + +#### Parameters + +- **options** (`SessionOptions`, optional) - Session configuration options: + - `id` (`string`) - Custom session ID. Auto-generated if not provided. + - `env` (`Record`) - Environment variables for this session + - `cwd` (`string`) - Working directory for this session. Defaults to `/workspace` + +#### Returns + +`Promise` - Session wrapper with all sandbox methods bound to this session ID. + +#### Example: Multiple isolated environments + +```typescript +// Create session for production deployment +const prodSession = await sandbox.createSession({ + id: 'prod-deploy', + env: { + NODE_ENV: 'production', + API_URL: 'https://api.example.com' + }, + cwd: '/workspace/prod' +}); + +// Create session for test environment +const testSession = await sandbox.createSession({ + id: 'test-deploy', + env: { + NODE_ENV: 'test', + API_URL: 'http://localhost:3000' + }, + cwd: '/workspace/test' +}); + +// Run builds in parallel with different configurations +const [prodResult, testResult] = await Promise.all([ + prodSession.exec('npm run build'), + testSession.exec('npm run build') +]); + +console.log('Production build:', prodResult.stdout); +console.log('Test build:', testResult.stdout); +``` + +#### Example: Session per user in chat application + +```typescript +// Each user gets their own isolated session +async function handleUserMessage(userId: string, message: string) { + const sandbox = getSandbox(env.Sandbox, `chat-${userId}`); + + // Get or create user's session + const session = await sandbox.createSession({ + id: userId, + env: { + USER_ID: userId, + CHAT_HISTORY_PATH: `/workspace/${userId}/history.txt` + } + }); + + // User's environment persists across messages + await session.exec(`echo "${message}" >> $CHAT_HISTORY_PATH`); + const result = await session.exec('python analyze_sentiment.py $CHAT_HISTORY_PATH'); + + return result.stdout; +} +``` + +#### Example: Custom working directories + +```typescript +// Frontend session working in React app +const frontend = await sandbox.createSession({ + id: 'frontend', + cwd: '/workspace/client', + env: { PORT: '3000' } +}); + +// Backend session working in API server +const backend = await sandbox.createSession({ + id: 'backend', + cwd: '/workspace/server', + env: { PORT: '8080' } +}); + +// Commands run in their respective directories +await frontend.exec('npm install'); // Runs in /workspace/client +await backend.exec('npm install'); // Runs in /workspace/server + +// Start both services +await frontend.startProcess('npm start'); +await backend.startProcess('npm start'); +``` + +### getSession() + +Retrieve an existing session by ID. Useful for resuming sessions across different requests. + +```typescript +const session = await sandbox.getSession(sessionId: string): Promise +``` + +#### Parameters + +- **sessionId** (`string`, required) - ID of an existing session + +#### Returns + +`Promise` - Session wrapper bound to the specified session ID. + +:::note +If the session doesn't exist, operations will fail. There's no explicit session existence check - just try to use it and handle errors. +::: + +#### Example: Resume session across requests + +```typescript +// First request - create session +app.post('/start', async (req) => { + const { userId } = req.body; + const sandbox = getSandbox(env.Sandbox, userId); + + const session = await sandbox.createSession({ + id: `user-${userId}`, + env: { USER_ID: userId } + }); + + await session.exec('git clone https://github.com/user/repo.git'); + await session.exec('cd repo && npm install'); + + return { message: 'Setup complete', sessionId: session.id }; +}); + +// Second request - resume session +app.post('/build', async (req) => { + const { userId, sessionId } = req.body; + const sandbox = getSandbox(env.Sandbox, userId); + + // Resume existing session - environment and cwd are preserved + const session = await sandbox.getSession(sessionId); + + const result = await session.exec('cd repo && npm run build'); + return { output: result.stdout }; +}); +``` + +#### Example: Multi-step workflows + +```typescript +// Step 1: Initialize environment +async function initializeProject(projectId: string) { + const sandbox = getSandbox(env.Sandbox, projectId); + const session = await sandbox.createSession({ + id: `project-${projectId}`, + cwd: '/workspace/project' + }); + + await session.exec('npm init -y'); + await session.exec('npm install express'); + + return session.id; +} + +// Step 2: Add code (different request/context) +async function addCode(projectId: string, sessionId: string, code: string) { + const sandbox = getSandbox(env.Sandbox, projectId); + const session = await sandbox.getSession(sessionId); + + await session.writeFile('app.js', code); + return 'Code added'; +} + +// Step 3: Run application (different request/context) +async function runApp(projectId: string, sessionId: string) { + const sandbox = getSandbox(env.Sandbox, projectId); + const session = await sandbox.getSession(sessionId); + + const result = await session.exec('node app.js'); + return result.stdout; +} +``` + +### setEnvVars() + +Set environment variables in the default session or globally in the sandbox. + +```typescript +await sandbox.setEnvVars(envVars: Record): Promise +``` + +#### Parameters + +- **envVars** (`Record`, required) - Key-value pairs of environment variables to set + +#### Returns + +`Promise` + +:::caution[Call setEnvVars first] +Call `setEnvVars()` **before** any other sandbox operations to ensure environment variables are available from the start. +::: + +#### Example: Configure API credentials + +```typescript +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'); // Can read API_KEY +``` + +#### Example: Dynamic configuration + +```typescript +async function runUserCode(userId: string, code: string, config: Config) { + const sandbox = getSandbox(env.Sandbox, userId); + + // Configure environment based on user's settings + await sandbox.setEnvVars({ + USER_ID: userId, + TIMEOUT: config.timeout.toString(), + MAX_MEMORY: config.maxMemory, + ALLOWED_HOSTS: config.allowedHosts.join(',') + }); + + await sandbox.writeFile('/tmp/user_code.py', code); + const result = await sandbox.exec('python /tmp/user_code.py'); + + return result.stdout; +} +``` + +#### Example: Session-specific environment + +```typescript +// Set environment in a specific session +const session = await sandbox.createSession({ + id: 'test-session', + env: { + NODE_ENV: 'test' + } +}); + +// Update environment variables within this session +await session.setEnvVars({ + API_KEY: 'test-key-123', + DEBUG: 'true' +}); + +await session.exec('npm test'); // Uses test environment +``` + +### destroy() + +Destroy the sandbox container and free up resources. + +```typescript +await sandbox.destroy(): Promise +``` + +#### Returns + +`Promise` + +:::note[Resource management] +Containers automatically sleep after 3 minutes of inactivity, but they still count toward your account limits. Use `destroy()` to immediately free up container slots and stop billing. +::: + +#### When to use destroy() + +- **After one-time tasks** - Code execution, builds, or analysis that won't be reused +- **Resource cleanup** - Free up container slots when you're done +- **Cost optimization** - Stop billing for containers you no longer need +- **Account limits** - Stay within your concurrent container limit + +#### Example: One-time code execution + +```typescript +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 { + // Always clean up temporary sandboxes + await sandbox.destroy(); + } +} +``` + +#### Example: Batch processing with cleanup + +```typescript +async function processBatch(tasks: Task[]) { + const results = []; + + for (const task of tasks) { + const sandbox = getSandbox(env.Sandbox, `task-${task.id}`); + + try { + // Process task + await sandbox.writeFile('/workspace/input.json', JSON.stringify(task.data)); + const result = await sandbox.exec('python process.py'); + results.push(result.stdout); + } finally { + // Free up resources after each task + await sandbox.destroy(); + } + } + + return results; +} +``` + +#### Example: Graceful shutdown + +```typescript +// Create sandbox for long-running workflow +const sandbox = getSandbox(env.Sandbox, 'workflow-123'); + +// Set up cleanup handlers +process.on('SIGTERM', async () => { + console.log('Shutting down gracefully...'); + await sandbox.destroy(); + process.exit(0); +}); + +// Run workflow +await sandbox.exec('npm run workflow'); +``` + +## ExecutionSession API + +The `ExecutionSession` object returned by `createSession()` and `getSession()` provides all sandbox methods bound to that specific session: + +### Command execution +- `exec()` - Execute commands in this session +- `execStream()` - Stream command output + +### Process management +- `startProcess()` - Start background process +- `listProcesses()` - List all processes (visible across sessions) +- `getProcess()` - Get process info +- `killProcess()` - Terminate process +- `killAllProcesses()` - Terminate all processes +- `getProcessLogs()` - Get process output +- `streamProcessLogs()` - Stream process output + +### File operations +- `writeFile()` - Write file +- `readFile()` - Read file +- `mkdir()` - Create directory +- `deleteFile()` - Delete file +- `renameFile()` - Rename file +- `moveFile()` - Move file + +### Git operations +- `gitCheckout()` - Clone repository + +### Environment management +- `setEnvVars()` - Update environment variables in this session + +### Code interpreter +- `createCodeContext()` - Create code execution context +- `runCode()` - Execute Python/JavaScript code +- `listCodeContexts()` - List active contexts +- `deleteCodeContext()` - Delete context + +## Session behavior + +### Shell state isolation + +Each session maintains its own: +- **Environment variables** - Set via `env` option or `setEnvVars()` +- **Working directory** - Set via `cwd` option or `cd` commands +- **Shell history** - Independent command history per session + +### Shared resources + +All sessions in a sandbox share: +- **Filesystem** - Files written in one session are visible in others +- **Background processes** - Processes started in any session are visible to all sessions +- **Ports** - Exposed ports are shared across sessions + +Think of sessions like terminal tabs on your laptop - separate shells, but same filesystem and processes. + +### Session lifecycle + +- **Creation** - `createSession()` initializes a new session +- **Persistence** - Sessions persist for the container's lifetime +- **Resumption** - `getSession()` retrieves existing sessions +- **Cleanup** - Sessions are automatically cleaned up when container shuts down + +## Error handling + +```typescript +try { + // Try to use a session + const session = await sandbox.getSession('nonexistent-session'); + await session.exec('ls'); +} catch (error) { + if (error.message.includes('Session not found')) { + // Handle missing session + console.error('Session does not exist'); + } +} +``` + +## Best practices + +### 1. Use default session for simple cases + +```typescript +// Good - simple use case +await sandbox.exec('npm install'); +await sandbox.exec('npm test'); + +// Overkill - unnecessary session complexity +const session = await sandbox.createSession(); +await session.exec('npm install'); +await session.exec('npm test'); +``` + +### 2. Create sessions for isolation + +```typescript +// Good - isolated environments +const dev = await sandbox.createSession({ + id: 'dev', + env: { NODE_ENV: 'development' } +}); +const prod = await sandbox.createSession({ + id: 'prod', + env: { NODE_ENV: 'production' } +}); +``` + +### 3. Set environment variables early + +```typescript +// Good - set env vars first +await sandbox.setEnvVars({ API_KEY: 'secret' }); +await sandbox.exec('python script.py'); + +// Bad - env vars set too late +await sandbox.exec('python script.py'); // API_KEY not available +await sandbox.setEnvVars({ API_KEY: 'secret' }); +``` + +### 4. Clean up temporary sandboxes + +```typescript +// Good - cleanup after one-time use +try { + const result = await sandbox.exec('python analyze.py'); + return result.stdout; +} finally { + await sandbox.destroy(); +} + +// Bad - container keeps running unnecessarily +const result = await sandbox.exec('python analyze.py'); +return result.stdout; +// Container stays alive, using resources +``` + +### 5. Reuse sessions across requests + +```typescript +// Good - resume session +const sessionId = req.body.sessionId; +const session = await sandbox.getSession(sessionId); +await session.exec('npm run build'); + +// Bad - create new session every time +const session = await sandbox.createSession(); +await session.exec('npm run build'); +// Loses state from previous requests +``` + +## Related resources + +- [Commands API](/sandbox/api/commands/) - Execute commands in sessions +- [Files API](/sandbox/api/files/) - File operations in sessions +- [Get Started](/sandbox/get-started/) - Basic sandbox usage without sessions +- [Build a data analysis platform](/sandbox/tutorials/data-analysis/) - Multi-session example From a597295c266f5890d73de3b75071ba17dba6603c Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 13:32:50 +0100 Subject: [PATCH 05/22] Use TypeScriptExample component --- src/content/docs/sandbox/api/commands.mdx | 125 ++++++-- src/content/docs/sandbox/api/files.mdx | 207 ++++++++---- src/content/docs/sandbox/api/interpreter.mdx | 303 ++++++++++++------ src/content/docs/sandbox/api/ports.mdx | 287 +++++++++++------ src/content/docs/sandbox/api/sessions.mdx | 194 +++++++---- .../tutorials/build-an-ai-code-executor.mdx | 4 +- src/content/products/sandbox.yaml | 2 +- 7 files changed, 770 insertions(+), 352 deletions(-) diff --git a/src/content/docs/sandbox/api/commands.mdx b/src/content/docs/sandbox/api/commands.mdx index 9cbbad1a953d6b1..1a752764348141a 100644 --- a/src/content/docs/sandbox/api/commands.mdx +++ b/src/content/docs/sandbox/api/commands.mdx @@ -5,7 +5,7 @@ sidebar: order: 1 --- -import { Details } from "~/components"; +import { Details, TypeScriptExample } from "~/components"; The Commands API lets you execute commands in the sandbox and manage background processes. All command execution happens in the sandbox's isolated container environment. @@ -15,10 +15,11 @@ The Commands API lets you execute commands in the sandbox and manage background Execute a command and return the complete result. -```typescript +```ts const result = await sandbox.exec(command: string, options?: ExecOptions): Promise ``` + #### Parameters - **command** (`string`, required) - The command to execute. Can include arguments. @@ -40,17 +41,20 @@ Returns a `Promise` with: **Basic command execution:** -```typescript + +``` const result = await sandbox.exec('python --version'); console.log(result.stdout); // "Python 3.11.0" console.log(result.success); // true console.log(result.exitCode); // 0 ``` + **Command with arguments:** -```typescript + +``` const result = await sandbox.exec('python -c "print(2 + 2)"'); if (result.success) { @@ -59,10 +63,13 @@ if (result.success) { console.error('Error:', result.stderr); } ``` + + **Streaming output:** -```typescript + +``` const result = await sandbox.exec('npm install express', { stream: true, onOutput: (stream, data) => { @@ -74,10 +81,13 @@ const result = await sandbox.exec('npm install express', { // Still returns complete result when done console.log('Final exit code:', result.exitCode); ``` + + **Error handling:** -```typescript + +``` try { const result = await sandbox.exec('nonexistent-command'); @@ -89,14 +99,19 @@ try { console.error('Execution error:', error.message); } ``` + + **Complex shell command:** -```typescript + +``` // Use shell features like pipes and redirects const result = await sandbox.exec('ls -la | grep ".py" | wc -l'); console.log('Number of Python files:', result.stdout.trim()); ``` + + --- @@ -104,10 +119,11 @@ console.log('Number of Python files:', result.stdout.trim()); Execute a command and return a Server-Sent Events stream for real-time processing. -```typescript +```ts const stream = await sandbox.execStream(command: string, options?: ExecOptions): Promise ``` + #### Parameters - **command** (`string`, required) - The command to execute @@ -128,7 +144,8 @@ Event types: **Basic streaming:** -```typescript + +``` import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; const stream = await sandbox.execStream('npm run build'); @@ -153,10 +170,13 @@ for await (const event of parseSSEStream(stream)) { } } ``` + + **Streaming to client:** -```typescript + +``` export default { async fetch(request: Request, env: Env): Promise { const sandbox = getSandbox(env.Sandbox, 'user-123'); @@ -172,10 +192,13 @@ export default { } }; ``` + + **Progress tracking:** -```typescript + +``` let progress = 0; const stream = await sandbox.execStream('npm test'); @@ -193,6 +216,8 @@ for await (const event of parseSSEStream(stream)) { } } ``` + + --- @@ -200,10 +225,11 @@ for await (const event of parseSSEStream(stream)) { Start a long-running background process. -```typescript +```ts const process = await sandbox.startProcess(command: string, options?: ProcessOptions): Promise ``` + #### Parameters - **command** (`string`, required) - The command to start as a background process @@ -224,7 +250,8 @@ Returns a `Promise` with: **Start a web server:** -```typescript + +``` const server = await sandbox.startProcess('python -m http.server 8000'); console.log('Server started with PID:', server.pid); @@ -234,10 +261,13 @@ console.log('Process ID:', server.id); // You can expose the port await sandbox.exposePort(8000); ``` + + **Start multiple processes:** -```typescript + +``` // Start a backend server const api = await sandbox.startProcess('node api-server.js'); @@ -247,10 +277,13 @@ const frontend = await sandbox.startProcess('npm run dev'); console.log('Started API server:', api.pid); console.log('Started frontend:', frontend.pid); ``` + + **Process with custom environment:** -```typescript + +``` const process = await sandbox.startProcess('node app.js', { cwd: '/workspace/my-app', env: { @@ -260,6 +293,8 @@ const process = await sandbox.startProcess('node app.js', { } }); ``` + + --- @@ -267,17 +302,19 @@ const process = await sandbox.startProcess('node app.js', { List all running processes in the sandbox. -```typescript +```ts const processes = await sandbox.listProcesses(): Promise ``` + #### Return value Returns a `Promise` with information about each running process. #### Example -```typescript + +``` const processes = await sandbox.listProcesses(); for (const proc of processes) { @@ -287,6 +324,8 @@ for (const proc of processes) { console.log(` Status: ${proc.status}`); } ``` + + --- @@ -294,10 +333,11 @@ for (const proc of processes) { Terminate a specific process. -```typescript +```ts await sandbox.killProcess(processId: string, signal?: string): Promise ``` + #### Parameters - **processId** (`string`, required) - The process ID (from `startProcess()` or `listProcesses()`) @@ -305,7 +345,8 @@ await sandbox.killProcess(processId: string, signal?: string): Promise #### Example -```typescript + +``` const server = await sandbox.startProcess('python -m http.server 8000'); // Stop the server after some time @@ -314,6 +355,8 @@ setTimeout(async () => { console.log('Server stopped'); }, 60000); ``` + + --- @@ -321,17 +364,21 @@ setTimeout(async () => { Terminate all running processes in the sandbox. -```typescript +```ts await sandbox.killAllProcesses(): Promise ``` + #### Example -```typescript + +``` // Clean up all processes await sandbox.killAllProcesses(); console.log('All processes terminated'); ``` + + --- @@ -339,10 +386,11 @@ console.log('All processes terminated'); Stream logs from a running process in real-time. -```typescript +```ts const stream = await sandbox.streamProcessLogs(processId: string): Promise ``` + #### Parameters - **processId** (`string`, required) - The process ID @@ -353,7 +401,8 @@ Returns a `Promise` that emits `LogEvent` objects. #### Example -```typescript + +``` import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; const server = await sandbox.startProcess('node server.js'); @@ -368,6 +417,8 @@ for await (const log of parseSSEStream(logStream)) { } } ``` + + --- @@ -375,10 +426,11 @@ for await (const log of parseSSEStream(logStream)) { Get accumulated logs from a process. -```typescript +```ts const logs = await sandbox.getProcessLogs(processId: string): Promise ``` + #### Parameters - **processId** (`string`, required) - The process ID @@ -389,7 +441,8 @@ Returns a `Promise` with all accumulated output from the process. #### Example -```typescript + +``` const server = await sandbox.startProcess('node server.js'); // Wait a bit @@ -399,6 +452,8 @@ await new Promise(resolve => setTimeout(resolve, 5000)); const logs = await sandbox.getProcessLogs(server.id); console.log('Server logs:', logs); ``` + + ## Common patterns @@ -406,7 +461,8 @@ console.log('Server logs:', logs); Execute a command and wait for it to complete: -```typescript + +``` const result = await sandbox.exec('npm run build'); if (result.success) { @@ -415,24 +471,30 @@ if (result.success) { throw new Error(`Build failed: ${result.stderr}`); } ``` + + ### Run in background Start a process and let it run: -```typescript + +``` const process = await sandbox.startProcess('node server.js'); await sandbox.exposePort(3000); // Process continues running // Client can access it via the exposed URL ``` + + ### Monitor progress Stream output and track progress: -```typescript + +``` const stream = await sandbox.execStream('npm test'); let passed = 0; let failed = 0; @@ -448,12 +510,15 @@ for await (const event of parseSSEStream(stream)) { } } ``` + + ### Cleanup processes Stop all processes when done: -```typescript + +``` try { // Run your code await sandbox.exec('npm run build'); @@ -462,6 +527,8 @@ try { await sandbox.killAllProcesses(); } ``` + + ## Related resources diff --git a/src/content/docs/sandbox/api/files.mdx b/src/content/docs/sandbox/api/files.mdx index 993cfcc0f6dc515..00f3d54190b0622 100644 --- a/src/content/docs/sandbox/api/files.mdx +++ b/src/content/docs/sandbox/api/files.mdx @@ -5,6 +5,8 @@ sidebar: order: 2 --- +import { TypeScriptExample } from "~/components"; + The Files API provides methods for reading, writing, and managing files in the sandbox filesystem. All file operations use absolute paths within the container. ## Methods @@ -13,7 +15,7 @@ The Files API provides methods for reading, writing, and managing files in the s Write content to a file in the sandbox. -```typescript +```ts await sandbox.writeFile(path: string, content: string, options?: WriteFileOptions): Promise ``` @@ -29,24 +31,27 @@ await sandbox.writeFile(path: string, content: string, options?: WriteFileOption **Write a text file:** -```typescript -await sandbox.writeFile('/workspace/app.js', ` -console.log('Hello from sandbox!'); -`); + +``` +await sandbox.writeFile('/workspace/app.js', `console.log('Hello from sandbox!');`); ``` + **Write with specific encoding:** -```typescript + +``` // Write binary data as base64 await sandbox.writeFile('/tmp/image.png', base64ImageData, { encoding: 'base64' }); ``` + **Write configuration file:** -```typescript + +``` const config = { port: 3000, environment: 'production', @@ -54,14 +59,18 @@ const config = { }; await sandbox.writeFile( - '/workspace/config.json', - JSON.stringify(config, null, 2) +'/workspace/config.json', +JSON.stringify(config, null, 2) ); + ``` + + **Create and write multiple files:** -```typescript + +``` // Package.json await sandbox.writeFile('/workspace/package.json', JSON.stringify({ name: 'my-app', @@ -88,13 +97,15 @@ await sandbox.exec('npm install'); await sandbox.startProcess('node server.js'); ``` + + --- ### `readFile()` Read a file from the sandbox. -```typescript +```ts const file = await sandbox.readFile(path: string, options?: ReadFileOptions): Promise ``` @@ -115,24 +126,31 @@ Returns a `Promise` with: **Read a text file:** -```typescript + +``` const file = await sandbox.readFile('/workspace/app.js'); console.log(file.content); ``` + **Read and parse JSON:** -```typescript + +``` const file = await sandbox.readFile('/workspace/package.json'); const packageJson = JSON.parse(file.content); console.log('Project name:', packageJson.name); console.log('Version:', packageJson.version); + ``` + + **Read binary file:** -```typescript + +``` const file = await sandbox.readFile('/tmp/image.png', { encoding: 'base64' }); @@ -141,15 +159,21 @@ const file = await sandbox.readFile('/tmp/image.png', { const imageData = file.content; ``` + + **Check file contents:** -```typescript + +``` const file = await sandbox.readFile('/workspace/.env'); if (file.content.includes('API_KEY=')) { - console.log('API key is configured'); +console.log('API key is configured'); } + ``` + + --- @@ -157,7 +181,7 @@ if (file.content.includes('API_KEY=')) { Create a directory in the sandbox. -```typescript +```ts await sandbox.mkdir(path: string, options?: MkdirOptions): Promise ``` @@ -172,22 +196,27 @@ await sandbox.mkdir(path: string, options?: MkdirOptions): Promise **Create a single directory:** -```typescript + +``` await sandbox.mkdir('/workspace/src'); ``` + **Create nested directories:** -```typescript + +``` // Create entire path await sandbox.mkdir('/workspace/src/components/ui', { recursive: true }); ``` + **Create project structure:** -```typescript + +``` // Create multiple directories await sandbox.mkdir('/workspace/src', { recursive: true }); await sandbox.mkdir('/workspace/tests', { recursive: true }); @@ -196,7 +225,10 @@ await sandbox.mkdir('/workspace/public', { recursive: true }); // Then create files await sandbox.writeFile('/workspace/src/index.js', '// Main app'); await sandbox.writeFile('/workspace/tests/app.test.js', '// Tests'); + ``` + + --- @@ -204,7 +236,7 @@ await sandbox.writeFile('/workspace/tests/app.test.js', '// Tests'); Delete a file from the sandbox. -```typescript +```ts await sandbox.deleteFile(path: string): Promise ``` @@ -216,13 +248,16 @@ await sandbox.deleteFile(path: string): Promise **Delete a file:** -```typescript + +``` await sandbox.deleteFile('/workspace/temp.txt'); ``` + **Clean up temporary files:** -```typescript + +``` const tempFiles = [ '/tmp/build-output.log', '/tmp/cache.json', @@ -230,13 +265,16 @@ const tempFiles = [ ]; for (const file of tempFiles) { - try { - await sandbox.deleteFile(file); - } catch (error) { - console.log(`Could not delete ${file}`); - } +try { +await sandbox.deleteFile(file); +} catch (error) { +console.log(`Could not delete ${file}`); +} } + ``` + + --- @@ -244,7 +282,7 @@ for (const file of tempFiles) { Rename a file in the sandbox. -```typescript +```ts await sandbox.renameFile(oldPath: string, newPath: string): Promise ``` @@ -257,16 +295,16 @@ await sandbox.renameFile(oldPath: string, newPath: string): Promise **Rename a file:** -```typescript -await sandbox.renameFile( - '/workspace/draft.txt', - '/workspace/final.txt' -); + ``` +await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt'); +``` + **Rename with backup:** -```typescript + +``` // Create backup before overwriting await sandbox.renameFile( '/workspace/config.json', @@ -275,7 +313,10 @@ await sandbox.renameFile( // Write new config await sandbox.writeFile('/workspace/config.json', newConfig); + ``` + + --- @@ -283,7 +324,7 @@ await sandbox.writeFile('/workspace/config.json', newConfig); Move a file to a different directory. -```typescript +```ts await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise ``` @@ -296,27 +337,27 @@ await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise ``` +await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt'); +``` + **Organize files:** -```typescript + +``` // Move source files to src directory await sandbox.mkdir('/workspace/src', { recursive: true }); const files = ['app.js', 'server.js', 'utils.js']; for (const file of files) { - await sandbox.moveFile( - `/workspace/${file}`, - `/workspace/src/${file}` - ); + await sandbox.moveFile(`/workspace/${file}`, `/workspace/src/${file}`); } + ``` + + --- @@ -324,7 +365,7 @@ for (const file of files) { Clone a git repository into the sandbox. -```typescript +```ts await sandbox.gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promise ``` @@ -340,22 +381,27 @@ await sandbox.gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promis **Clone a repository:** -```typescript + +``` await sandbox.gitCheckout('https://github.com/user/repo'); ``` + **Clone specific branch:** -```typescript + +``` await sandbox.gitCheckout('https://github.com/user/repo', { branch: 'develop', targetDir: 'my-project' }); ``` + **Clone and build:** -```typescript + +``` // Clone the repository await sandbox.gitCheckout('https://github.com/user/app'); @@ -365,44 +411,53 @@ await sandbox.exec('cd app && npm run build'); // Read build output const buildOutput = await sandbox.readFile('/workspace/app/dist/index.html'); + ``` + + **Shallow clone for faster cloning:** -```typescript + +``` await sandbox.gitCheckout('https://github.com/user/large-repo', { depth: 1 // Only fetch latest commit }); ``` + + ## Common patterns ### Create a project structure -```typescript + +``` // Create directories await sandbox.mkdir('/workspace/my-app/src', { recursive: true }); await sandbox.mkdir('/workspace/my-app/tests', { recursive: true }); // Create package.json await sandbox.writeFile('/workspace/my-app/package.json', JSON.stringify({ - name: 'my-app', - version: '1.0.0', - scripts: { - start: 'node src/index.js', - test: 'jest' - } +name: 'my-app', +version: '1.0.0', +scripts: { +start: 'node src/index.js', +test: 'jest' +} })); // Create main file -await sandbox.writeFile('/workspace/my-app/src/index.js', ` -console.log('App started!'); -`); +await sandbox.writeFile('/workspace/my-app/src/index.js', `console.log('App started!');`); + ``` + + ### Read and modify files -```typescript + +``` // Read existing file const file = await sandbox.readFile('/workspace/config.json'); const config = JSON.parse(file.content); @@ -418,19 +473,26 @@ await sandbox.writeFile( ); ``` + + ### Copy files -```typescript + +``` // Read source file const source = await sandbox.readFile('/workspace/template.html'); // Write to new location await sandbox.writeFile('/workspace/index.html', source.content); + ``` + + ### Batch operations -```typescript + +``` const files = { '/workspace/src/app.js': 'console.log("app");', '/workspace/src/utils.js': 'console.log("utils");', @@ -445,15 +507,18 @@ await Promise.all( ); ``` + + ### Work with binary files -```typescript + +``` // Read binary file (e.g., from an upload) const imageBase64 = request.headers.get('X-Image-Data'); // Write to sandbox await sandbox.writeFile('/workspace/upload.png', imageBase64, { - encoding: 'base64' +encoding: 'base64' }); // Process with Python @@ -461,15 +526,19 @@ await sandbox.exec('python process-image.py /workspace/upload.png'); // Read processed result const result = await sandbox.readFile('/workspace/output.png', { - encoding: 'base64' +encoding: 'base64' }); + ``` + + ## Error handling File operations may fail if files don't exist or permissions are insufficient: -```typescript + +``` try { const file = await sandbox.readFile('/workspace/missing.txt'); } catch (error) { @@ -483,6 +552,8 @@ try { } ``` + + ## Path conventions All paths in the sandbox are absolute. Common directories: diff --git a/src/content/docs/sandbox/api/interpreter.mdx b/src/content/docs/sandbox/api/interpreter.mdx index dca6bf6bae5a78f..bab6660764ac73b 100644 --- a/src/content/docs/sandbox/api/interpreter.mdx +++ b/src/content/docs/sandbox/api/interpreter.mdx @@ -5,17 +5,21 @@ sidebar: order: 3 --- +import { TypeScriptExample } from "~/components"; + The Code Interpreter API provides a high-level interface for executing Python, JavaScript, and TypeScript code with rich output support. Unlike the Commands API which executes arbitrary shell commands, the Code Interpreter is designed specifically for interactive code execution with support for data visualizations, tables, charts, and formatted output. ## Overview The Code Interpreter API is ideal for: + - **Data analysis workflows** - Execute pandas, numpy, matplotlib code - **Interactive notebooks** - Build Jupyter-like experiences - **AI code execution** - Run LLM-generated code safely - **Multi-language execution** - Python, JavaScript, TypeScript in isolated contexts Key features: + - **Persistent contexts** - Maintain state across multiple executions - **Rich outputs** - Charts, tables, images, HTML, LaTeX, JSON - **Streaming support** - Real-time output with callbacks @@ -28,7 +32,7 @@ Key features: Create a persistent execution context for running code. Contexts maintain state (variables, imports, functions) across multiple code executions. -```typescript +```ts const context = await sandbox.createCodeContext(options?: CreateContextOptions): Promise ``` @@ -54,18 +58,23 @@ Returns a `Promise` with: **Create Python context:** -```typescript + +``` const pythonCtx = await sandbox.createCodeContext({ language: 'python', cwd: '/workspace' }); console.log('Context ID:', pythonCtx.id); + ``` + + **Create JavaScript context with environment variables:** -```typescript + +``` const jsCtx = await sandbox.createCodeContext({ language: 'javascript', envVars: { @@ -75,14 +84,18 @@ const jsCtx = await sandbox.createCodeContext({ }); ``` + + **Create TypeScript context:** -```typescript + +``` const tsCtx = await sandbox.createCodeContext({ language: 'typescript', cwd: '/workspace/app' }); ``` + --- @@ -90,7 +103,7 @@ const tsCtx = await sandbox.createCodeContext({ Execute code in a context and return the complete result with all outputs. -```typescript +```ts const result = await sandbox.runCode(code: string, options?: RunCodeOptions): Promise ``` @@ -124,20 +137,25 @@ Returns a `Promise` with: **Basic Python execution:** -```typescript + +``` const result = await sandbox.runCode(` print("Hello from Python!") x = 42 x * 2 `); -console.log('Stdout:', result.logs.stdout); // ["Hello from Python!"] -console.log('Result:', result.results[0].text); // "84" +console.log('Stdout:', result.logs.stdout); // ["Hello from Python!"] +console.log('Result:', result.results[0].text); // "84" + ``` + + **Python with data analysis:** -```typescript + +``` const ctx = await sandbox.createCodeContext({ language: 'python' }); const result = await sandbox.runCode(` @@ -161,9 +179,12 @@ console.log('Logs:', result.logs.stdout); console.log('Result:', result.results[0].text); // "5.5" ``` + + **JavaScript execution:** -```typescript + +``` const result = await sandbox.runCode(` const data = [1, 2, 3, 4, 5]; const sum = data.reduce((a, b) => a + b, 0); @@ -173,13 +194,17 @@ console.log('Average:', avg); avg; `, { language: 'javascript' }); -console.log(result.logs.stdout); // ["Average: 3"] -console.log(result.results[0].text); // "3" +console.log(result.logs.stdout); // ["Average: 3"] +console.log(result.results[0].text); // "3" + ``` + + **Execution with streaming callbacks:** -```typescript + +``` const result = await sandbox.runCode(` for i in range(5): print(f"Processing item {i}...") @@ -205,28 +230,35 @@ for i in range(5): // ... ``` + + **Error handling:** -```typescript + +``` try { const result = await sandbox.runCode(` x = 1 / 0 # Division by zero `, { language: 'python' }); - if (result.error) { - console.error('Execution error:'); - console.error('Name:', result.error.name); // "ZeroDivisionError" - console.error('Message:', result.error.value); // "division by zero" - console.error('Traceback:', result.error.traceback); - } +if (result.error) { +console.error('Execution error:'); +console.error('Name:', result.error.name); // "ZeroDivisionError" +console.error('Message:', result.error.value); // "division by zero" +console.error('Traceback:', result.error.traceback); +} } catch (error) { - console.error('Failed to execute:', error.message); +console.error('Failed to execute:', error.message); } + ``` + + **Working with persistent context:** -```typescript + +``` // Create context const ctx = await sandbox.createCodeContext({ language: 'python' }); @@ -246,32 +278,38 @@ area console.log('Result:', result.results[0].text); // "78.53981633974483" ``` + + **AI code execution example:** -```typescript + +``` import Anthropic from '@anthropic-ai/sdk'; // Generate code with Claude const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); const response = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-latest', - max_tokens: 1024, - messages: [{ - role: 'user', - content: 'Write Python code to calculate the factorial of 10' - }] +model: 'claude-3-5-sonnet-latest', +max_tokens: 1024, +messages: [{ +role: 'user', +content: 'Write Python code to calculate the factorial of 10' +}] }); const generatedCode = response.content[0].text; // Execute safely in sandbox const result = await sandbox.runCode(generatedCode, { - language: 'python', - timeout: 5000 // 5 second timeout +language: 'python', +timeout: 5000 // 5 second timeout }); console.log('Result:', result.results[0].text); + ``` + + --- @@ -279,7 +317,7 @@ console.log('Result:', result.results[0].text); List all active code execution contexts for the sandbox. -```typescript +```ts const contexts = await sandbox.listCodeContexts(): Promise ``` @@ -289,16 +327,20 @@ Returns a `Promise` with all active contexts. #### Example -```typescript + +``` const contexts = await sandbox.listCodeContexts(); for (const ctx of contexts) { - console.log(`Context: ${ctx.id}`); - console.log(` Language: ${ctx.language}`); - console.log(` Created: ${ctx.createdAt.toISOString()}`); - console.log(` Last used: ${ctx.lastUsed.toISOString()}`); +console.log(`Context: ${ctx.id}`); +console.log(` Language: ${ctx.language}`); +console.log(` Created: ${ctx.createdAt.toISOString()}`); +console.log(` Last used: ${ctx.lastUsed.toISOString()}`); } + ``` + + --- @@ -306,7 +348,7 @@ for (const ctx of contexts) { Delete a code execution context and free its resources. -```typescript +```ts await sandbox.deleteCodeContext(contextId: string): Promise ``` @@ -316,7 +358,8 @@ await sandbox.deleteCodeContext(contextId: string): Promise #### Example -```typescript + +``` const ctx = await sandbox.createCodeContext({ language: 'python' }); // Use the context... @@ -325,11 +368,15 @@ await sandbox.runCode('print("Hello")', { context: ctx }); // Clean up when done await sandbox.deleteCodeContext(ctx.id); console.log('Context deleted'); + ``` + + **Cleanup pattern:** -```typescript + +``` // List and clean up old contexts const contexts = await sandbox.listCodeContexts(); @@ -344,6 +391,8 @@ for (const ctx of contexts) { } ``` + + ## Rich Output Formats The Code Interpreter supports multiple output formats for data visualization and presentation: @@ -352,7 +401,8 @@ The Code Interpreter supports multiple output formats for data visualization and Each execution returns `Result` objects with these properties: -```typescript + +``` interface Result { text?: string; // Plain text representation html?: string; // HTML tables, formatted output @@ -366,15 +416,19 @@ interface Result { chart?: ChartData; // Chart/visualization data data?: any; // Raw data object - formats(): string[]; // List available formats +formats(): string[]; // List available formats } + ``` + + ### Working with Charts **Python with matplotlib:** -```typescript + +``` const result = await sandbox.runCode(` import matplotlib.pyplot as plt import numpy as np @@ -399,27 +453,34 @@ if (result.results[0]?.png) { } ``` + + **Return image to client:** -```typescript + +``` const result = await sandbox.runCode(visualizationCode, { language: 'python' }); // Return chart as image response if (result.results[0]?.png) { - const imageBuffer = Buffer.from(result.results[0].png, 'base64'); - return new Response(imageBuffer, { - headers: { 'Content-Type': 'image/png' } - }); +const imageBuffer = Buffer.from(result.results[0].png, 'base64'); +return new Response(imageBuffer, { +headers: { 'Content-Type': 'image/png' } +}); } + ``` + + ### Working with Tables **Python with pandas:** -```typescript + +``` const result = await sandbox.runCode(` import pandas as pd @@ -442,19 +503,22 @@ if (result.results[0]?.html) { } ``` + + ### Working with JSON Data -```typescript + +``` const result = await sandbox.runCode(` import json data = { - 'status': 'success', - 'items': [1, 2, 3, 4, 5], - 'summary': { - 'total': 15, - 'average': 3.0 - } +'status': 'success', +'items': [1, 2, 3, 4, 5], +'summary': { +'total': 15, +'average': 3.0 +} } json.dumps(data) @@ -462,16 +526,20 @@ json.dumps(data) // Access JSON result if (result.results[0]?.json) { - const jsonData = result.results[0].json; - return Response.json(jsonData); +const jsonData = result.results[0].json; +return Response.json(jsonData); } + ``` + + ## Common Patterns ### Interactive data analysis -```typescript + +``` // Create persistent context for analysis session const ctx = await sandbox.createCodeContext({ language: 'python' }); @@ -509,9 +577,12 @@ if (viz.results[0]?.png) { } ``` + + ### Multi-language workflow -```typescript + +``` // Python for data processing const pythonCtx = await sandbox.createCodeContext({ language: 'python' }); @@ -520,13 +591,14 @@ import json data = [1, 2, 3, 4, 5] result = { - 'sum': sum(data), - 'avg': sum(data) / len(data) +'sum': sum(data), +'avg': sum(data) / len(data) } # Write results to file + with open('/workspace/results.json', 'w') as f: - json.dump(result, f) +json.dump(result, f) result `, { context: pythonCtx }); @@ -543,11 +615,15 @@ data; `, { language: 'javascript' }); console.log(jsResult.logs.stdout); + ``` + + ### AI-powered data analysis -```typescript + +``` import OpenAI from 'openai'; async function analyzeData(question: string, dataPath: string) { @@ -587,17 +663,20 @@ if (analysis.results[0]) { } ``` + + ### Real-time execution monitoring -```typescript + +``` let progress = 0; const result = await sandbox.runCode(` import time for i in range(10): - print(f"Step {i+1}/10") - time.sleep(0.5) +print(f"Step {i+1}/10") +time.sleep(0.5) print("Complete!") `, { @@ -605,15 +684,19 @@ print("Complete!") onStdout: (output) => { if (output.text.startsWith('Step')) { progress++; - console.log(`Progress: ${progress * 10}%`); - } - } + console.log(`Progress: ${progress \* 10}%`); +} +} }); + ``` + + ### Error recovery -```typescript + +``` const ctx = await sandbox.createCodeContext({ language: 'python' }); async function executeWithRetry(code: string, maxRetries = 3) { @@ -649,13 +732,16 @@ try { } ``` + + ## Error Handling ### Execution Errors The Code Interpreter returns structured error information: -```typescript + +``` interface ExecutionError { name: string; // Error type (e.g., 'NameError', 'SyntaxError') value: string; // Error message @@ -663,34 +749,40 @@ interface ExecutionError { lineNumber?: number; // Line number where error occurred } ``` + **Example error handling:** -```typescript + +``` const result = await sandbox.runCode(` undefined_variable `, { language: 'python' }); if (result.error) { - console.error('Error Type:', result.error.name); - console.error('Error Message:', result.error.value); - console.error('Stack Trace:'); - result.error.traceback.forEach(line => console.error(line)); +console.error('Error Type:', result.error.name); +console.error('Error Message:', result.error.value); +console.error('Stack Trace:'); +result.error.traceback.forEach(line => console.error(line)); - if (result.error.lineNumber) { - console.error('Error at line:', result.error.lineNumber); - } +if (result.error.lineNumber) { +console.error('Error at line:', result.error.lineNumber); +} } // Output: // Error Type: NameError // Error Message: name 'undefined_variable' is not defined // Stack Trace: [traceback lines...] + ``` + + ### Timeout Handling -```typescript + +``` try { const result = await sandbox.runCode(` import time @@ -708,11 +800,14 @@ time.sleep(120) # Sleep for 2 minutes } ``` + + ### Context Not Ready The Code Interpreter includes automatic retry logic for initialization: -```typescript + +``` // First execution might need time to initialize const result = await sandbox.runCode(`print("Hello")`, { language: 'python' @@ -720,13 +815,17 @@ const result = await sandbox.runCode(`print("Hello")`, { // The SDK automatically retries if the interpreter is not ready // Default: 3 retries with exponential backoff + ``` + + ## Best Practices ### 1. Use persistent contexts for related executions -```typescript + +``` // ✅ Good - reuse context const ctx = await sandbox.createCodeContext({ language: 'python' }); await sandbox.runCode('import pandas as pd', { context: ctx }); @@ -738,9 +837,12 @@ await sandbox.runCode('import pandas as pd', { language: 'python' }); await sandbox.runCode('df = pd.read_csv("data.csv")', { language: 'python' }); // Import lost! ``` + + ### 2. Set appropriate timeouts -```typescript + +``` // Short timeout for quick operations await sandbox.runCode('2 + 2', { language: 'python', @@ -749,14 +851,18 @@ await sandbox.runCode('2 + 2', { // Longer timeout for data processing await sandbox.runCode(dataAnalysisCode, { - language: 'python', - timeout: 60000 // 1 minute +language: 'python', +timeout: 60000 // 1 minute }); + ``` + + ### 3. Clean up contexts -```typescript + +``` const ctx = await sandbox.createCodeContext({ language: 'python' }); try { @@ -768,9 +874,12 @@ try { } ``` + + ### 4. Handle errors gracefully -```typescript + +``` const result = await sandbox.runCode(userCode, { language: 'python', onError: (error) => { @@ -780,17 +889,21 @@ const result = await sandbox.runCode(userCode, { }); if (result.error) { - return Response.json({ - success: false, - error: result.error.value, - traceback: result.error.traceback - }, { status: 400 }); +return Response.json({ +success: false, +error: result.error.value, +traceback: result.error.traceback +}, { status: 400 }); } + ``` + + ### 5. Use callbacks for long-running operations -```typescript + +``` let output = ''; await sandbox.runCode(longRunningCode, { @@ -806,6 +919,8 @@ await sandbox.runCode(longRunningCode, { }); ``` + + ## Related Resources - [Build an AI Code Executor tutorial](/sandbox/tutorials/build-an-ai-code-executor/) - Complete AI integration example diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx index ea14a83acd67b44..509863101e2bc70 100644 --- a/src/content/docs/sandbox/api/ports.mdx +++ b/src/content/docs/sandbox/api/ports.mdx @@ -5,6 +5,8 @@ sidebar: order: 4 --- +import { TypeScriptExample } from "~/components"; + The Ports API enables you to expose services running in your sandbox to the internet via preview URLs. This allows you to run web servers, APIs, or any network service inside the sandbox and access them from outside. ## Overview @@ -17,6 +19,7 @@ When you start a service in the sandbox (like a web server), it runs on localhos - **Multi-service apps** - Expose multiple ports for microservices Key features: + - **Automatic URL generation** - Get a unique preview URL for each exposed port - **Secure proxying** - All traffic is proxied through Cloudflare's network - **Multiple ports** - Expose several services simultaneously @@ -28,7 +31,7 @@ Key features: Expose a port running in the sandbox and get a preview URL. -```typescript +```ts const response = await sandbox.exposePort(port: number, options?: ExposePortOptions): Promise ``` @@ -52,7 +55,8 @@ Returns a `Promise` with: **Expose a web server:** -```typescript + +``` import { getSandbox } from '@cloudflare/sandbox'; export default { @@ -70,20 +74,25 @@ export default { url: exposed.exposedAt, port: exposed.port }); - } + +} }; // Response: // { -// "message": "Server running", -// "url": "https://abc123-8000.sandbox.workers.dev", -// "port": 8000 +// "message": "Server running", +// "url": "https://abc123-8000.sandbox.workers.dev", +// "port": 8000 // } + ``` + + **Expose with a friendly name:** -```typescript + +``` // Start a Node.js API server await sandbox.startProcess('node api-server.js'); @@ -96,9 +105,12 @@ console.log('API available at:', api.exposedAt); console.log('Port name:', api.name); // "api-server" ``` + + **Expose multiple services:** -```typescript + +``` // Start backend API await sandbox.startProcess('node api.js'); const api = await sandbox.exposePort(3000, { name: 'api' }); @@ -112,17 +124,21 @@ await sandbox.startProcess('python db-admin.py'); const admin = await sandbox.exposePort(8080, { name: 'admin' }); return Response.json({ - services: { - api: api.exposedAt, - frontend: frontend.exposedAt, - admin: admin.exposedAt - } +services: { +api: api.exposedAt, +frontend: frontend.exposedAt, +admin: admin.exposedAt +} }); + ``` + + **Full development environment:** -```typescript + +``` // Clone a repository await sandbox.gitCheckout('https://github.com/user/my-app'); @@ -144,13 +160,15 @@ return Response.json({ }); ``` + + --- ### `unexposePort()` Remove a previously exposed port and close its preview URL. -```typescript +```ts await sandbox.unexposePort(port: number): Promise ``` @@ -170,7 +188,8 @@ Returns a `Promise` with: **Unexpose a port:** -```typescript + +``` // Expose a port const exposed = await sandbox.exposePort(8000); console.log('Exposed at:', exposed.exposedAt); @@ -178,11 +197,15 @@ console.log('Exposed at:', exposed.exposedAt); // Later, clean up await sandbox.unexposePort(8000); console.log('Port 8000 is no longer accessible'); + ``` + + **Cleanup multiple ports:** -```typescript + +``` const ports = [3000, 5173, 8080]; // Expose all ports @@ -198,20 +221,26 @@ for (const port of ports) { console.log('All ports unexposed'); ``` + + **Conditional unexpose:** -```typescript + +``` // Get currently exposed ports const { ports } = await sandbox.getExposedPorts(); // Unexpose only specific ports for (const portInfo of ports) { - if (portInfo.name === 'temporary-service') { - await sandbox.unexposePort(portInfo.port); - console.log(`Cleaned up ${portInfo.name}`); - } +if (portInfo.name === 'temporary-service') { +await sandbox.unexposePort(portInfo.port); +console.log(`Cleaned up ${portInfo.name}`); +} } + ``` + + --- @@ -219,7 +248,7 @@ for (const portInfo of ports) { Get information about all currently exposed ports. -```typescript +```ts const response = await sandbox.getExposedPorts(): Promise ``` @@ -239,22 +268,27 @@ Returns a `Promise` with: **List all exposed ports:** -```typescript + +``` const { ports, count } = await sandbox.getExposedPorts(); console.log(`${count} ports exposed:`); for (const port of ports) { - console.log(` Port ${port.port}: ${port.exposedAt}`); - if (port.name) { - console.log(` Name: ${port.name}`); - } +console.log(` Port ${port.port}: ${port.exposedAt}`); +if (port.name) { +console.log(` Name: ${port.name}`); +} } + ``` + + **Check if a specific port is exposed:** -```typescript + +``` const { ports } = await sandbox.getExposedPorts(); const isExposed = ports.some(p => p.port === 3000); @@ -265,9 +299,12 @@ if (!isExposed) { } ``` + + **Return exposed services to client:** -```typescript + +``` export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); @@ -286,13 +323,18 @@ export default { } return new Response('Not found', { status: 404 }); - } + +} }; + ``` + + **Monitor exposed ports:** -```typescript + +``` // Check which ports are exposed const before = await sandbox.getExposedPorts(); console.log('Ports before:', before.count); @@ -310,13 +352,16 @@ console.log('Ports after:', after.count); console.log('New ports:', after.ports.map(p => p.name)); ``` + + ## Common Patterns ### Start and expose a service The typical workflow for running a web service: -```typescript + +``` // 1. Start the service as a background process const process = await sandbox.startProcess('node server.js'); console.log('Server started with PID:', process.pid); @@ -326,22 +371,26 @@ await new Promise(resolve => setTimeout(resolve, 2000)); // 3. Expose the port const exposed = await sandbox.exposePort(3000, { - name: 'my-api' +name: 'my-api' }); // 4. Return the URL to the client return Response.json({ - status: 'running', - url: exposed.exposedAt, - processId: process.id +status: 'running', +url: exposed.exposedAt, +processId: process.id }); + ``` + + ### Development workflow Complete development environment setup: -```typescript + +``` async function setupDevEnvironment(sandbox: Sandbox, repoUrl: string) { // Clone the repository await sandbox.gitCheckout(repoUrl); @@ -381,11 +430,14 @@ const env = await setupDevEnvironment( console.log('Dev environment ready:', env.url); ``` + + ### Microservices architecture Run and expose multiple services: -```typescript + +``` interface Service { name: string; command: string; @@ -393,44 +445,48 @@ interface Service { } const services: Service[] = [ - { name: 'api', command: 'node api/server.js', port: 3000 }, - { name: 'auth', command: 'node auth/server.js', port: 3001 }, - { name: 'websocket', command: 'node ws/server.js', port: 3002 }, - { name: 'admin', command: 'python admin/app.py', port: 8080 } +{ name: 'api', command: 'node api/server.js', port: 3000 }, +{ name: 'auth', command: 'node auth/server.js', port: 3001 }, +{ name: 'websocket', command: 'node ws/server.js', port: 3002 }, +{ name: 'admin', command: 'python admin/app.py', port: 8080 } ]; const exposedServices = []; for (const service of services) { - // Start the service - const process = await sandbox.startProcess(service.command); +// Start the service +const process = await sandbox.startProcess(service.command); - // Wait briefly - await new Promise(resolve => setTimeout(resolve, 1500)); +// Wait briefly +await new Promise(resolve => setTimeout(resolve, 1500)); - // Expose the port - const exposed = await sandbox.exposePort(service.port, { - name: service.name - }); +// Expose the port +const exposed = await sandbox.exposePort(service.port, { +name: service.name +}); - exposedServices.push({ - name: service.name, - url: exposed.exposedAt, - processId: process.id - }); +exposedServices.push({ +name: service.name, +url: exposed.exposedAt, +processId: process.id +}); } return Response.json({ - message: 'All services running', - services: exposedServices +message: 'All services running', +services: exposedServices }); + ``` + + ### Temporary preview URLs Create temporary preview URLs that clean up automatically: -```typescript + +``` async function createTemporaryPreview( sandbox: Sandbox, command: string, @@ -466,11 +522,14 @@ const previewUrl = await createTemporaryPreview( console.log('Preview available for 10 minutes:', previewUrl); ``` + + ### Health checking Verify a service is ready before exposing: -```typescript + +``` async function exposeWithHealthCheck( sandbox: Sandbox, command: string, @@ -480,9 +539,9 @@ async function exposeWithHealthCheck( // Start the service const process = await sandbox.startProcess(command); - // Poll until service is ready - for (let i = 0; i < maxRetries; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); +// Poll until service is ready +for (let i = 0; i < maxRetries; i++) { +await new Promise(resolve => setTimeout(resolve, 1000)); // Check if service is responding const check = await sandbox.exec(`curl -f http://localhost:${port}/health || true`); @@ -494,29 +553,34 @@ async function exposeWithHealthCheck( } console.log(`Waiting for service to be ready... (${i + 1}/${maxRetries})`); - } - throw new Error('Service failed to become ready'); +} + +throw new Error('Service failed to become ready'); } // Usage try { - const url = await exposeWithHealthCheck( - sandbox, - 'node server.js', - 3000 - ); - console.log('Service ready:', url); +const url = await exposeWithHealthCheck( +sandbox, +'node server.js', +3000 +); +console.log('Service ready:', url); } catch (error) { - console.error('Service failed to start:', error.message); +console.error('Service failed to start:', error.message); } + ``` + + ## Error Handling ### Port already exposed -```typescript + +``` try { await sandbox.exposePort(3000); } catch (error) { @@ -536,9 +600,12 @@ try { } ``` + + ### Port not in use -```typescript + +``` // Try to expose a port that nothing is listening on try { await sandbox.exposePort(9999); @@ -546,18 +613,22 @@ try { console.error('Port 9999 is not in use'); console.log('Make sure a service is running on this port first'); - // Start a service on that port - await sandbox.startProcess('python -m http.server 9999'); +// Start a service on that port +await sandbox.startProcess('python -m http.server 9999'); - // Wait and try again - await new Promise(resolve => setTimeout(resolve, 2000)); - await sandbox.exposePort(9999); +// Wait and try again +await new Promise(resolve => setTimeout(resolve, 2000)); +await sandbox.exposePort(9999); } + ``` + + ### Cleanup on error -```typescript + +``` async function safeExposePort(sandbox: Sandbox, port: number) { try { const exposed = await sandbox.exposePort(port); @@ -577,13 +648,16 @@ async function safeExposePort(sandbox: Sandbox, port: number) { } ``` + + ## Best Practices ### 1. Wait for services to start Always wait briefly after starting a process before exposing its port: -```typescript + +``` // ✅ Good - wait for service to start await sandbox.startProcess('npm run dev'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -591,14 +665,18 @@ await sandbox.exposePort(3000); // ❌ Bad - expose immediately await sandbox.startProcess('npm run dev'); -await sandbox.exposePort(3000); // May fail if service isn't ready +await sandbox.exposePort(3000); // May fail if service isn't ready + ``` + + ### 2. Use named ports for clarity When exposing multiple ports, use names to keep track: -```typescript + +``` // ✅ Good - named ports await sandbox.exposePort(3000, { name: 'api' }); await sandbox.exposePort(5173, { name: 'frontend' }); @@ -610,11 +688,14 @@ await sandbox.exposePort(5173); await sandbox.exposePort(8080); ``` + + ### 3. Clean up exposed ports Always unexpose ports when done: -```typescript + +``` try { await sandbox.exposePort(3000); // Use the service... @@ -623,25 +704,31 @@ try { await sandbox.unexposePort(3000); } ``` + ### 4. Check before exposing Verify a port isn't already exposed: -```typescript + +``` const { ports } = await sandbox.getExposedPorts(); const isExposed = ports.some(p => p.port === 3000); if (!isExposed) { - await sandbox.exposePort(3000); +await sandbox.exposePort(3000); } + ``` + + ### 5. Handle port ranges Use valid port numbers (1-65535), avoiding reserved ranges: -```typescript + +``` // ✅ Good - user port range await sandbox.exposePort(3000); // Typical web dev await sandbox.exposePort(8080); // Typical HTTP alternate @@ -652,6 +739,8 @@ await sandbox.exposePort(80); // May require privileges await sandbox.exposePort(443); // May require privileges ``` + + ## Preview URL Format Preview URLs follow this pattern: @@ -661,6 +750,7 @@ https://{sandbox-id}-{port}.sandbox.workers.dev ``` For example: + - Port 3000: `https://abc123-3000.sandbox.workers.dev` - Port 8080: `https://abc123-8080.sandbox.workers.dev` @@ -672,7 +762,8 @@ The URL remains stable for the lifetime of the exposed port and automatically pr **Important**: Exposed ports are publicly accessible on the internet. Anyone with the URL can access your service. -```typescript + +``` // Be aware: This API is now public await sandbox.exposePort(3000); @@ -683,26 +774,30 @@ const app = express(); // Add authentication middleware app.use((req, res, next) => { - const token = req.headers.authorization; - if (token !== 'Bearer secret-token') { - return res.status(401).json({ error: 'Unauthorized' }); - } - next(); +const token = req.headers.authorization; +if (token !== 'Bearer secret-token') { +return res.status(401).json({ error: 'Unauthorized' }); +} +next(); }); app.get('/data', (req, res) => { - res.json({ message: 'Protected data' }); +res.json({ message: 'Protected data' }); }); app.listen(3000); `); + ``` + + ### Temporary URLs For security-sensitive applications, unexpose ports when no longer needed: -```typescript + +``` // Expose temporarily const exposed = await sandbox.exposePort(3000); @@ -713,6 +808,8 @@ await doWork(exposed.exposedAt); await sandbox.unexposePort(3000); ``` + + ## Related Resources - [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 index b8c93505990a606..c03d240fc09413b 100644 --- a/src/content/docs/sandbox/api/sessions.mdx +++ b/src/content/docs/sandbox/api/sessions.mdx @@ -5,6 +5,8 @@ sidebar: order: 5 --- +import { TypeScriptExample } from "~/components"; + Sessions provide isolated execution contexts within a sandbox, similar to having multiple terminal panes open on your laptop. Each session maintains its own shell state, environment variables, and working directory across multiple invocations. :::note @@ -30,7 +32,8 @@ Sessions are an advanced feature. Most applications work fine with the default s When you use sandbox methods without explicitly creating a session, a default session is automatically created with: -```typescript + +``` const sandbox = getSandbox(env.Sandbox, 'user-123'); // These automatically use the default session @@ -40,7 +43,10 @@ await sandbox.exec('echo $API_KEY'); // Prints: secret // Environment persists within the default session await sandbox.writeFile('/workspace/app.js', code); await sandbox.exec('node app.js'); // Can access API_KEY + ``` + + The default session persists for the lifetime of the sandbox instance, maintaining shell state across all operations. @@ -50,7 +56,7 @@ The default session persists for the lifetime of the sandbox instance, maintaini Create a new isolated execution session with custom environment and working directory. -```typescript +```ts const session = await sandbox.createSession(options?: SessionOptions): Promise ``` @@ -67,7 +73,8 @@ const session = await sandbox.createSession(options?: SessionOptions): Promise +``` // Create session for production deployment const prodSession = await sandbox.createSession({ id: 'prod-deploy', @@ -80,27 +87,31 @@ const prodSession = await sandbox.createSession({ // Create session for test environment const testSession = await sandbox.createSession({ - id: 'test-deploy', - env: { - NODE_ENV: 'test', - API_URL: 'http://localhost:3000' - }, - cwd: '/workspace/test' +id: 'test-deploy', +env: { +NODE_ENV: 'test', +API_URL: 'http://localhost:3000' +}, +cwd: '/workspace/test' }); // Run builds in parallel with different configurations const [prodResult, testResult] = await Promise.all([ - prodSession.exec('npm run build'), - testSession.exec('npm run build') +prodSession.exec('npm run build'), +testSession.exec('npm run build') ]); console.log('Production build:', prodResult.stdout); console.log('Test build:', testResult.stdout); + ``` + + #### Example: Session per user in chat application -```typescript + +``` // Each user gets their own isolated session async function handleUserMessage(userId: string, message: string) { const sandbox = getSandbox(env.Sandbox, `chat-${userId}`); @@ -122,9 +133,12 @@ async function handleUserMessage(userId: string, message: string) { } ``` + + #### Example: Custom working directories -```typescript + +``` // Frontend session working in React app const frontend = await sandbox.createSession({ id: 'frontend', @@ -134,25 +148,28 @@ const frontend = await sandbox.createSession({ // Backend session working in API server const backend = await sandbox.createSession({ - id: 'backend', - cwd: '/workspace/server', - env: { PORT: '8080' } +id: 'backend', +cwd: '/workspace/server', +env: { PORT: '8080' } }); // Commands run in their respective directories await frontend.exec('npm install'); // Runs in /workspace/client -await backend.exec('npm install'); // Runs in /workspace/server +await backend.exec('npm install'); // Runs in /workspace/server // Start both services await frontend.startProcess('npm start'); await backend.startProcess('npm start'); + ``` + + ### getSession() Retrieve an existing session by ID. Useful for resuming sessions across different requests. -```typescript +```ts const session = await sandbox.getSession(sessionId: string): Promise ``` @@ -170,39 +187,44 @@ If the session doesn't exist, operations will fail. There's no explicit session #### Example: Resume session across requests -```typescript + +``` // First request - create session app.post('/start', async (req) => { const { userId } = req.body; const sandbox = getSandbox(env.Sandbox, userId); - const session = await sandbox.createSession({ - id: `user-${userId}`, - env: { USER_ID: userId } - }); +const session = await sandbox.createSession({ +id: `user-${userId}`, +env: { USER_ID: userId } +}); - await session.exec('git clone https://github.com/user/repo.git'); - await session.exec('cd repo && npm install'); +await session.exec('git clone https://github.com/user/repo.git'); +await session.exec('cd repo && npm install'); - return { message: 'Setup complete', sessionId: session.id }; +return { message: 'Setup complete', sessionId: session.id }; }); // Second request - resume session app.post('/build', async (req) => { - const { userId, sessionId } = req.body; - const sandbox = getSandbox(env.Sandbox, userId); +const { userId, sessionId } = req.body; +const sandbox = getSandbox(env.Sandbox, userId); - // Resume existing session - environment and cwd are preserved - const session = await sandbox.getSession(sessionId); +// Resume existing session - environment and cwd are preserved +const session = await sandbox.getSession(sessionId); - const result = await session.exec('cd repo && npm run build'); - return { output: result.stdout }; +const result = await session.exec('cd repo && npm run build'); +return { output: result.stdout }; }); + ``` + + #### Example: Multi-step workflows -```typescript + +``` // Step 1: Initialize environment async function initializeProject(projectId: string) { const sandbox = getSandbox(env.Sandbox, projectId); @@ -236,11 +258,13 @@ async function runApp(projectId: string, sessionId: string) { } ``` + + ### setEnvVars() Set environment variables in the default session or globally in the sandbox. -```typescript +```ts await sandbox.setEnvVars(envVars: Record): Promise ``` @@ -258,23 +282,28 @@ Call `setEnvVars()` **before** any other sandbox operations to ensure environmen #### Example: Configure API credentials -```typescript + +``` 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' +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'); // Can read API_KEY + ``` + + #### Example: Dynamic configuration -```typescript + +``` async function runUserCode(userId: string, code: string, config: Config) { const sandbox = getSandbox(env.Sandbox, userId); @@ -293,9 +322,12 @@ async function runUserCode(userId: string, code: string, config: Config) { } ``` + + #### Example: Session-specific environment -```typescript + +``` // Set environment in a specific session const session = await sandbox.createSession({ id: 'test-session', @@ -306,18 +338,21 @@ const session = await sandbox.createSession({ // Update environment variables within this session await session.setEnvVars({ - API_KEY: 'test-key-123', - DEBUG: 'true' +API_KEY: 'test-key-123', +DEBUG: 'true' }); await session.exec('npm test'); // Uses test environment + ``` + + ### destroy() Destroy the sandbox container and free up resources. -```typescript +```ts await sandbox.destroy(): Promise ``` @@ -338,24 +373,29 @@ Containers automatically sleep after 3 minutes of inactivity, but they still cou #### Example: One-time code execution -```typescript + +``` 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 { - // Always clean up temporary sandboxes - await sandbox.destroy(); - } +try { +await sandbox.writeFile('/tmp/code.py', code); +const result = await sandbox.exec('python /tmp/code.py'); +return result.stdout; +} finally { +// Always clean up temporary sandboxes +await sandbox.destroy(); +} } + ``` + + #### Example: Batch processing with cleanup -```typescript + +``` async function processBatch(tasks: Task[]) { const results = []; @@ -377,22 +417,28 @@ async function processBatch(tasks: Task[]) { } ``` + + #### Example: Graceful shutdown -```typescript + +``` // Create sandbox for long-running workflow const sandbox = getSandbox(env.Sandbox, 'workflow-123'); // Set up cleanup handlers process.on('SIGTERM', async () => { - console.log('Shutting down gracefully...'); - await sandbox.destroy(); - process.exit(0); +console.log('Shutting down gracefully...'); +await sandbox.destroy(); +process.exit(0); }); // Run workflow await sandbox.exec('npm run workflow'); + ``` + + ## ExecutionSession API @@ -458,7 +504,8 @@ Think of sessions like terminal tabs on your laptop - separate shells, but same ## Error handling -```typescript + +``` try { // Try to use a session const session = await sandbox.getSession('nonexistent-session'); @@ -471,11 +518,14 @@ try { } ``` + + ## Best practices ### 1. Use default session for simple cases -```typescript + +``` // Good - simple use case await sandbox.exec('npm install'); await sandbox.exec('npm test'); @@ -484,11 +534,15 @@ await sandbox.exec('npm test'); const session = await sandbox.createSession(); await session.exec('npm install'); await session.exec('npm test'); + ``` + + ### 2. Create sessions for isolation -```typescript + +``` // Good - isolated environments const dev = await sandbox.createSession({ id: 'dev', @@ -500,9 +554,12 @@ const prod = await sandbox.createSession({ }); ``` + + ### 3. Set environment variables early -```typescript + +``` // Good - set env vars first await sandbox.setEnvVars({ API_KEY: 'secret' }); await sandbox.exec('python script.py'); @@ -510,11 +567,15 @@ await sandbox.exec('python script.py'); // Bad - env vars set too late await sandbox.exec('python script.py'); // API_KEY not available await sandbox.setEnvVars({ API_KEY: 'secret' }); + ``` + + ### 4. Clean up temporary sandboxes -```typescript + +``` // Good - cleanup after one-time use try { const result = await sandbox.exec('python analyze.py'); @@ -529,9 +590,12 @@ return result.stdout; // Container stays alive, using resources ``` + + ### 5. Reuse sessions across requests -```typescript + +``` // Good - resume session const sessionId = req.body.sessionId; const session = await sandbox.getSession(sessionId); @@ -541,7 +605,10 @@ await session.exec('npm run build'); const session = await sandbox.createSession(); await session.exec('npm run build'); // Loses state from previous requests + ``` + + ## Related resources @@ -549,3 +616,4 @@ await session.exec('npm run build'); - [Files API](/sandbox/api/files/) - File operations in sessions - [Get Started](/sandbox/get-started/) - Basic sandbox usage without sessions - [Build a data analysis platform](/sandbox/tutorials/data-analysis/) - Multi-session example +``` diff --git a/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx index 2a9eae4a8cf3d67..bca9b356a673e9d 100644 --- a/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx +++ b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx @@ -120,7 +120,7 @@ Your `wrangler.jsonc` file already contains the required configuration for Sandb Replace the contents of `src/index.ts` with the following code: -```typescript +```typescript collapse={1-16, 18-35} import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; import Anthropic from '@anthropic-ai/sdk'; @@ -298,7 +298,7 @@ curl -X POST http://localhost:8787/execute \ You should see a response like: -```json +```json output { "success": true, "question": "What is the 10th Fibonacci number?", diff --git a/src/content/products/sandbox.yaml b/src/content/products/sandbox.yaml index 6066c2ff51b6262..194bb04c05a126a 100644 --- a/src/content/products/sandbox.yaml +++ b/src/content/products/sandbox.yaml @@ -1,4 +1,4 @@ -name: Sandbox +name: Sandbox SDK product: title: Sandbox SDK From 1ca020334f7a9e351fbaccd0f5888e230f946ca1 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 13:48:52 +0100 Subject: [PATCH 06/22] Fix broken code snippets --- src/content/docs/sandbox/api/interpreter.mdx | 68 ++++++-------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/src/content/docs/sandbox/api/interpreter.mdx b/src/content/docs/sandbox/api/interpreter.mdx index bab6660764ac73b..eabb0e339064fc0 100644 --- a/src/content/docs/sandbox/api/interpreter.mdx +++ b/src/content/docs/sandbox/api/interpreter.mdx @@ -66,7 +66,6 @@ const pythonCtx = await sandbox.createCodeContext({ }); console.log('Context ID:', pythonCtx.id); - ``` @@ -83,7 +82,6 @@ const jsCtx = await sandbox.createCodeContext({ } }); ``` - **Create TypeScript context:** @@ -147,7 +145,6 @@ x * 2 console.log('Stdout:', result.logs.stdout); // ["Hello from Python!"] console.log('Result:', result.results[0].text); // "84" - ``` @@ -196,7 +193,6 @@ avg; console.log(result.logs.stdout); // ["Average: 3"] console.log(result.results[0].text); // "3" - ``` @@ -229,7 +225,6 @@ for i in range(5): // [STDOUT] Processing item 1... // ... ``` - **Error handling:** @@ -250,7 +245,6 @@ console.error('Traceback:', result.error.traceback); } catch (error) { console.error('Failed to execute:', error.message); } - ``` @@ -277,7 +271,6 @@ area console.log('Result:', result.results[0].text); // "78.53981633974483" ``` - **AI code execution example:** @@ -306,7 +299,6 @@ timeout: 5000 // 5 second timeout }); console.log('Result:', result.results[0].text); - ``` @@ -337,7 +329,6 @@ console.log(` Language: ${ctx.language}`); console.log(` Created: ${ctx.createdAt.toISOString()}`); console.log(` Last used: ${ctx.lastUsed.toISOString()}`); } - ``` @@ -368,7 +359,6 @@ await sandbox.runCode('print("Hello")', { context: ctx }); // Clean up when done await sandbox.deleteCodeContext(ctx.id); console.log('Context deleted'); - ``` @@ -390,7 +380,6 @@ for (const ctx of contexts) { } } ``` - ## Rich Output Formats @@ -401,8 +390,7 @@ The Code Interpreter supports multiple output formats for data visualization and Each execution returns `Result` objects with these properties: - -``` +```ts interface Result { text?: string; // Plain text representation html?: string; // HTML tables, formatted output @@ -418,9 +406,7 @@ interface Result { formats(): string[]; // List available formats } - ``` - ### Working with Charts @@ -452,7 +438,6 @@ if (result.results[0]?.png) { console.log('Chart generated'); } ``` - **Return image to client:** @@ -502,7 +487,6 @@ if (result.results[0]?.html) { }); } ``` - ### Working with JSON Data @@ -513,12 +497,12 @@ const result = await sandbox.runCode(` import json data = { -'status': 'success', -'items': [1, 2, 3, 4, 5], -'summary': { -'total': 15, -'average': 3.0 -} + 'status': 'success', + 'items': [1, 2, 3, 4, 5], + 'summary': { + 'total': 15, + 'average': 3.0 + } } json.dumps(data) @@ -526,10 +510,9 @@ json.dumps(data) // Access JSON result if (result.results[0]?.json) { -const jsonData = result.results[0].json; -return Response.json(jsonData); + const jsonData = result.results[0].json; + return Response.json(jsonData); } - ``` @@ -576,7 +559,6 @@ if (viz.results[0]?.png) { // Use chart image... } ``` - ### Multi-language workflow @@ -591,8 +573,8 @@ import json data = [1, 2, 3, 4, 5] result = { -'sum': sum(data), -'avg': sum(data) / len(data) + 'sum': sum(data), + 'avg': sum(data) / len(data) } # Write results to file @@ -615,7 +597,6 @@ data; `, { language: 'javascript' }); console.log(jsResult.logs.stdout); - ``` @@ -662,7 +643,6 @@ if (analysis.results[0]) { console.log('Result:', analysis.results[0].text); } ``` - ### Real-time execution monitoring @@ -684,11 +664,10 @@ print("Complete!") onStdout: (output) => { if (output.text.startsWith('Step')) { progress++; - console.log(`Progress: ${progress \* 10}%`); -} -} + console.log(`Progress: ${progress * 10}%`); + } + } }); - ``` @@ -731,7 +710,6 @@ try { console.error('Failed:', error.message); } ``` - ## Error Handling @@ -740,8 +718,7 @@ try { The Code Interpreter returns structured error information: - -``` +```ts interface ExecutionError { name: string; // Error type (e.g., 'NameError', 'SyntaxError') value: string; // Error message @@ -749,7 +726,6 @@ interface ExecutionError { lineNumber?: number; // Line number where error occurred } ``` - **Example error handling:** @@ -799,7 +775,6 @@ time.sleep(120) # Sleep for 2 minutes } } ``` - ### Context Not Ready @@ -836,7 +811,6 @@ await sandbox.runCode('df.describe()', { context: ctx }); await sandbox.runCode('import pandas as pd', { language: 'python' }); await sandbox.runCode('df = pd.read_csv("data.csv")', { language: 'python' }); // Import lost! ``` - ### 2. Set appropriate timeouts @@ -889,13 +863,12 @@ const result = await sandbox.runCode(userCode, { }); if (result.error) { -return Response.json({ -success: false, -error: result.error.value, -traceback: result.error.traceback -}, { status: 400 }); + return Response.json({ + success: false, + error: result.error.value, + traceback: result.error.traceback + }, { status: 400 }); } - ``` @@ -918,7 +891,6 @@ await sandbox.runCode(longRunningCode, { } }); ``` - ## Related Resources From 10c3f8d3518f6d78e2f949dff171958ce67bbfbb Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 13:55:40 +0100 Subject: [PATCH 07/22] Update codeowners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) 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 From d8380931c0c32374a015235dc43be5f95689f05e Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 14:09:23 +0100 Subject: [PATCH 08/22] Add how-to guides --- .../sandbox/guides/background-processes.mdx | 582 ++++++++++++++++ .../docs/sandbox/guides/code-execution.mdx | 623 +++++++++++++++++ .../docs/sandbox/guides/execute-commands.mdx | 363 ++++++++++ .../docs/sandbox/guides/expose-services.mdx | 650 ++++++++++++++++++ .../docs/sandbox/guides/git-workflows.mdx | 571 +++++++++++++++ src/content/docs/sandbox/guides/index.mdx | 24 + .../docs/sandbox/guides/manage-files.mdx | 564 +++++++++++++++ .../docs/sandbox/guides/streaming-output.mdx | 530 ++++++++++++++ 8 files changed, 3907 insertions(+) create mode 100644 src/content/docs/sandbox/guides/background-processes.mdx create mode 100644 src/content/docs/sandbox/guides/code-execution.mdx create mode 100644 src/content/docs/sandbox/guides/execute-commands.mdx create mode 100644 src/content/docs/sandbox/guides/expose-services.mdx create mode 100644 src/content/docs/sandbox/guides/git-workflows.mdx create mode 100644 src/content/docs/sandbox/guides/index.mdx create mode 100644 src/content/docs/sandbox/guides/manage-files.mdx create mode 100644 src/content/docs/sandbox/guides/streaming-output.mdx 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..07e972c964dd535 --- /dev/null +++ b/src/content/docs/sandbox/guides/background-processes.mdx @@ -0,0 +1,582 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Understanding of process management concepts + +## 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 + + +``` +// Start process in specific directory +const process = await sandbox.startProcess('npm run dev', { + cwd: '/workspace/my-app' +}); + +// Equivalent to: +// cd /workspace/my-app && npm run dev +``` + + +### Set environment variables + + +``` +const process = await sandbox.startProcess('node server.js', { + cwd: '/workspace/api', + env: { + NODE_ENV: 'production', + PORT: '3000', + API_KEY: env.API_KEY, + DATABASE_URL: env.DATABASE_URL, + LOG_LEVEL: 'debug' + } +}); + +console.log('API server started on port 3000'); +``` + + +## Monitor process status + +### List all processes + + +``` +const processes = await sandbox.listProcesses(); + +console.log(`Running ${processes.length} processes:`); + +for (const proc of processes) { + console.log(` + ID: ${proc.id} + PID: ${proc.pid} + Command: ${proc.command} + Status: ${proc.status} + `); +} +``` + + +### Check if process is running + + +``` +async function isProcessRunning(processId: string): Promise { + const processes = await sandbox.listProcesses(); + return processes.some(p => p.id === processId && p.status === 'running'); +} + +// Usage +const server = await sandbox.startProcess('node server.js'); + +if (await isProcessRunning(server.id)) { + console.log('Server is running'); +} else { + console.log('Server stopped or crashed'); +} +``` + + +## Monitor process logs + +### Stream logs in real-time + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +// Start a web server +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.timestamp}] ${log.data}`); + + // Wait for server to be ready + if (log.data.includes('Server listening on port')) { + console.log('Server is ready!'); + break; // Stop watching logs + } +} +``` + + +### Get accumulated logs + + +``` +const server = await sandbox.startProcess('npm run build'); + +// Wait for build to complete +await new Promise(resolve => setTimeout(resolve, 10000)); + +// Get all logs +const logs = await sandbox.getProcessLogs(server.id); + +console.log('Build output:', logs); + +// Parse logs for specific information +if (logs.includes('Build successful')) { + console.log('✓ Build completed'); +} else if (logs.includes('Error:')) { + console.error('✗ Build failed'); +} +``` + + +## Stop processes + +### Stop specific process + + +``` +const server = await sandbox.startProcess('python -m http.server 8000'); + +// Stop gracefully (SIGTERM) +await sandbox.killProcess(server.id); +console.log('Server stopped'); + +// Force kill (SIGKILL) +await sandbox.killProcess(server.id, 'SIGKILL'); +``` + + +### Stop all processes + + +``` +// Clean up everything +await sandbox.killAllProcesses(); +console.log('All processes stopped'); +``` + + +### Stop after timeout + + +``` +const process = await sandbox.startProcess('node app.js'); + +// Auto-stop after 1 hour +setTimeout(async () => { + await sandbox.killProcess(process.id); + console.log('Process stopped after timeout'); +}, 60 * 60 * 1000); +``` + + +## Run multiple processes + +### Start multiple services + + +``` +// Start backend API +const api = await sandbox.startProcess('node api-server.js', { + cwd: '/workspace/backend', + env: { PORT: '3000' } +}); + +// Start frontend dev server +const frontend = await sandbox.startProcess('npm run dev', { + cwd: '/workspace/frontend', + env: { PORT: '5173' } +}); + +// Start database +const db = await sandbox.startProcess('redis-server', { + env: { PORT: '6379' } +}); + +console.log('All services started:'); +console.log('- API:', api.pid); +console.log('- Frontend:', frontend.pid); +console.log('- Database:', db.pid); +``` + + +### Coordinate multiple processes + + +``` +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')) { + console.log('Database ready'); + break; + } +} + +// Now start API server (depends on database) +const api = await sandbox.startProcess('node api-server.js', { + env: { DATABASE_URL: 'redis://localhost:6379' } +}); + +// Wait for API to be ready +const apiLogs = await sandbox.streamProcessLogs(api.id); +for await (const log of parseSSEStream(apiLogs)) { + if (log.data.includes('API listening')) { + console.log('API ready'); + break; + } +} + +console.log('All services ready'); +``` + + +## Expose services via ports + +### Start and expose web server + + +``` +// Start web server +const server = await sandbox.startProcess('python -m http.server 8000'); + +// Expose port to get public URL +const port = await sandbox.exposePort(8000); + +console.log('Server accessible at:', port.previewUrl); +// Returns: https://{random-subdomain}.cloudflare-sandbox.com + +// Send URL to user +return new Response(JSON.stringify({ + status: 'Server started', + url: port.previewUrl +})); +``` + + +### Wait for service before exposing + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +// Start Node.js application +const app = await sandbox.startProcess('node server.js', { + env: { PORT: '3000' } +}); + +// Wait for server to be ready +const logs = await sandbox.streamProcessLogs(app.id); +let serverReady = false; + +for await (const log of parseSSEStream(logs)) { + if (log.data.includes('Server listening')) { + serverReady = true; + break; + } +} + +if (serverReady) { + // Now expose port + const port = await sandbox.exposePort(3000); + console.log('Application ready:', port.previewUrl); +} +``` + + +## Best practices + +### Process lifecycle + +- **Check readiness** - Wait for processes to be ready before using them +- **Monitor health** - Watch logs for errors or crashes +- **Clean up** - Always stop processes when done +- **Handle failures** - Restart crashed processes automatically + +### Resource management + +- **Limit concurrent processes** - Don't start too many processes +- **Set timeouts** - Stop processes that run too long +- **Monitor memory** - Track resource usage +- **Clean up on errors** - Use try/finally to ensure cleanup + +### Error handling + +- **Check process status** - Verify process started successfully +- **Parse logs for errors** - Watch for error messages +- **Retry failed starts** - Handle transient failures +- **Alert on crashes** - Notify when processes die unexpectedly + +### Security + +- **Validate commands** - Don't execute arbitrary user input +- **Limit environment access** - Only pass necessary env vars +- **Isolate processes** - Use separate sandboxes for untrusted code +- **Set resource limits** - Prevent resource exhaustion + +## Common patterns + +### Supervised process + +Automatically restart processes that crash: + + +``` +async function supervisedProcess( + command: string, + maxRestarts: number = 3 +): Promise { + let restarts = 0; + + while (restarts < maxRestarts) { + const process = await sandbox.startProcess(command); + console.log(`Started process (attempt ${restarts + 1})`); + + // Monitor process + const logs = await sandbox.streamProcessLogs(process.id); + + for await (const log of parseSSEStream(logs)) { + console.log(log.data); + + // Check for errors + if (log.data.includes('FATAL') || log.data.includes('crashed')) { + console.error('Process crashed, restarting...'); + await sandbox.killProcess(process.id); + restarts++; + break; + } + } + } + + throw new Error(`Process failed after ${maxRestarts} restarts`); +} + +// Use it +await supervisedProcess('node unstable-server.js', 5); +``` + + +### Health check endpoint + + +``` +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === '/health') { + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + const processes = await sandbox.listProcesses(); + + // Check if required services are running + const required = ['api-server', 'database', 'cache']; + const running = processes.filter(p => + required.some(r => p.command.includes(r)) + ); + + const healthy = running.length === required.length; + + return new Response(JSON.stringify({ + healthy, + processes: running.length, + required: required.length + }), { + status: healthy ? 200 : 503, + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response('Not found', { status: 404 }); + } +}; +``` + + +### Graceful shutdown + + +``` +async function gracefulShutdown(): Promise { + console.log('Shutting down...'); + + // Get all processes + const processes = await sandbox.listProcesses(); + + // Stop each process gracefully + for (const proc of processes) { + try { + console.log(`Stopping ${proc.command}...`); + + // Try graceful stop (SIGTERM) + await sandbox.killProcess(proc.id, 'SIGTERM'); + + // Wait for process to stop + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Check if still running + if (await isProcessRunning(proc.id)) { + // Force kill if needed + console.log(`Force killing ${proc.command}...`); + await sandbox.killProcess(proc.id, 'SIGKILL'); + } + } catch (error) { + console.error(`Error stopping ${proc.command}:`, error); + } + } + + console.log('All processes stopped'); +} + +// Call on shutdown +await gracefulShutdown(); +``` + + +## Common issues + +### Process exits immediately + +**Problem**: Process starts but exits right away. + +**Solution**: Check logs to see why it exited: + + +``` +const process = await sandbox.startProcess('node server.js'); + +// Wait a moment +await new Promise(resolve => setTimeout(resolve, 1000)); + +// Check if still running +const processes = await sandbox.listProcesses(); +const running = processes.find(p => p.id === process.id); + +if (!running) { + // Process exited - check logs + const logs = await sandbox.getProcessLogs(process.id); + console.error('Process exited:', logs); + + // Common causes: + // - Missing dependencies: "Cannot find module 'express'" + // - Port already in use: "EADDRINUSE" + // - Configuration error: "Invalid config" +} +``` + + +### Port already in use + +**Problem**: "Address already in use" error when starting process. + +**Solution**: Kill existing process on that port or use different port: + + +``` +// Option 1: Kill processes first +await sandbox.killAllProcesses(); +const server = await sandbox.startProcess('node server.js'); + +// Option 2: Use dynamic port +const port = 3000 + Math.floor(Math.random() * 1000); +const server = await sandbox.startProcess(`node server.js`, { + env: { PORT: port.toString() } +}); + +// Option 3: Find and kill specific process +const processes = await sandbox.listProcesses(); +const existing = processes.find(p => p.command.includes('server.js')); +if (existing) { + await sandbox.killProcess(existing.id); +} + +const server = await sandbox.startProcess('node server.js'); +``` + + +### Process hangs or doesn't respond + +**Problem**: Process is running but not responding to requests. + +**Solution**: Check logs and add timeout: + + +``` +const process = await sandbox.startProcess('node app.js'); + +// Add timeout +const timeout = setTimeout(async () => { + console.error('Process not responding, restarting...'); + await sandbox.killProcess(process.id); + + // Start new instance + await sandbox.startProcess('node app.js'); +}, 30000); // 30 second timeout + +// Monitor logs for ready signal +const logs = await sandbox.streamProcessLogs(process.id); +for await (const log of parseSSEStream(logs)) { + if (log.data.includes('Ready')) { + clearTimeout(timeout); + console.log('Process ready'); + break; + } +} +``` + + +## 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..54c36df5bbf8e97 --- /dev/null +++ b/src/content/docs/sandbox/guides/code-execution.mdx @@ -0,0 +1,623 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Understanding of when to use code interpreter vs direct command execution + +## When to use code interpreter + +Use the Code Interpreter API when you need: + +- **Rich outputs** - Charts, tables, images, HTML, LaTeX from code execution +- **Persistent state** - Variables and imports preserved between executions +- **Multiple languages** - Python, JavaScript, and TypeScript support +- **Interactive workflows** - Iterative data analysis and exploration +- **AI integration** - Execute LLM-generated code safely + +Use `exec()` for: + +- **Simple scripts** - One-time commands without state +- **System operations** - File operations, installs, builds +- **Text-only output** - When you only need stdout/stderr + +## 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 + +### Get all available outputs + + +``` +const result = await sandbox.runCode(context.id, ` +import matplotlib.pyplot as plt +import numpy as np + +# Generate data +x = np.linspace(0, 10, 100) +y = np.sin(x) + +# Create plot +plt.figure(figsize=(10, 6)) +plt.plot(x, y) +plt.title('Sine Wave') +plt.xlabel('X') +plt.ylabel('sin(X)') +plt.grid(True) +plt.show() +`); + +// Check available formats +console.log('Formats:', result.formats); +// ['text', 'png'] + +// Access specific outputs +if (result.outputs.png) { + console.log('Chart available as PNG'); + // result.outputs.png contains base64-encoded image +} + +if (result.outputs.text) { + console.log('Text output:', result.outputs.text); +} +``` + + +### Return images + + +``` +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() +`); + +// Return image to client +if (result.outputs.png) { + const imageData = atob(result.outputs.png); + return new Response(imageData, { + headers: { 'Content-Type': 'image/png' } + }); +} +``` + + +### Return data tables + + +``` +const result = await sandbox.runCode(context.id, ` +import pandas as pd + +# Create DataFrame +df = pd.DataFrame({ + 'name': ['Alice', 'Bob', 'Charlie'], + 'age': [25, 30, 35], + 'city': ['NYC', 'SF', 'LA'] +}) + +# Display as HTML table +df +`); + +// Get HTML table +if (result.outputs.html) { + return new Response(result.outputs.html, { + headers: { 'Content-Type': 'text/html' } + }); +} +``` + + +### Return JSON data + + +``` +const result = await sandbox.runCode(context.id, ` +data = { + "users": [ + {"name": "Alice", "score": 95}, + {"name": "Bob", "score": 87} + ], + "total": 2 +} + +# Display as JSON +data +`); + +if (result.outputs.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); + } + } +); +``` + + +## Data analysis workflows + +### Load and analyze data + + +``` +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +// Load CSV data +await sandbox.writeFile('/workspace/data.csv', csvData); + +const result = await sandbox.runCode(context.id, ` +import pandas as pd +import matplotlib.pyplot as plt + +# Load data +df = pd.read_csv('/workspace/data.csv') + +# Basic statistics +print("Dataset shape:", df.shape) +print("\\nSummary statistics:") +print(df.describe()) + +# Create visualization +plt.figure(figsize=(10, 6)) +df.plot(kind='bar') +plt.title('Data Visualization') +plt.show() + +# Return summary +{ + "rows": len(df), + "columns": list(df.columns), + "mean_value": float(df['value'].mean()) +} +`); + +// Access results +console.log('Text output:', result.outputs.text); +console.log('Chart:', result.outputs.png ? 'Available' : 'Not available'); +console.log('Data:', result.outputs.json); +``` + + +### Multi-step analysis + + +``` +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +// Step 1: Load and clean data +await sandbox.runCode(context.id, ` +import pandas as pd + +df = pd.read_csv('/workspace/sales.csv') +df = df.dropna() # Remove missing values +df['date'] = pd.to_datetime(df['date']) +print(f"Loaded {len(df)} records") +`); + +// Step 2: Aggregate data +await sandbox.runCode(context.id, ` +monthly_sales = df.groupby(df['date'].dt.to_period('M'))['amount'].sum() +print("Monthly totals calculated") +`); + +// Step 3: Create visualizations +const result = await sandbox.runCode(context.id, ` +import matplotlib.pyplot as plt + +plt.figure(figsize=(12, 6)) +monthly_sales.plot(kind='bar') +plt.title('Monthly Sales') +plt.xlabel('Month') +plt.ylabel('Sales ($)') +plt.xticks(rotation=45) +plt.tight_layout() +plt.show() + +# Return summary +{ + "total_sales": float(monthly_sales.sum()), + "average_monthly": float(monthly_sales.mean()), + "best_month": str(monthly_sales.idxmax()) +} +`); + +console.log('Summary:', result.outputs.json); +``` + + +## AI code execution + +Execute LLM-generated code safely: + + +``` +async function executeAIGeneratedCode( + userPrompt: string, + env: Env +): Promise { + const sandbox = getSandbox(env.Sandbox, 'ai-executor'); + + // 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 ${userPrompt}. Return only the code, no explanations.` + }] + }) + }); + + const data = await response.json(); + const code = data.content[0].text; + + // 2. Create execution context + const context = await sandbox.createCodeContext({ + language: 'python' + }); + + // 3. Execute generated code + const result = await sandbox.runCode(context.id, code, { + stream: true, + onOutput: (output) => console.log(output), + onError: (error) => console.error('Execution error:', error) + }); + + // 4. Return results + return { + prompt: userPrompt, + code, + output: result.output, + success: result.success, + formats: result.formats, + outputs: result.outputs + }; +} + +// Usage +const result = await executeAIGeneratedCode( + 'analyze the fibonacci sequence up to 100', + env +); + +console.log('Generated code:', result.code); +console.log('Output:', result.output); +``` + + +## Handle errors + + +``` +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +const result = await sandbox.runCode(context.id, ` +# This code has an error +undefined_variable = some_variable + 1 +`); + +if (!result.success) { + console.error('Execution failed'); + console.error('Error:', result.error.name); // "NameError" + console.error('Message:', result.error.value); // "name 'some_variable' is not defined" + console.error('Line:', result.error.lineNumber); // Line where error occurred + + if (result.error.traceback) { + console.error('Traceback:', result.error.traceback.join('\\n')); + } +} +``` + + +## 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 + +### Context management + +- **One context per task** - Keep contexts focused on specific tasks +- **Clean up** - Delete contexts when done to free resources +- **Name contexts** - Use meaningful IDs to track purposes +- **Reuse contexts** - Share contexts for related operations + +### Code execution + +- **Validate AI code** - Check generated code before execution +- **Use timeouts** - Set execution timeouts for safety +- **Stream long operations** - Provide feedback for slow code +- **Handle errors gracefully** - Always check success and error fields + +### Performance + +- **Limit context count** - Don't create too many concurrent contexts +- **Batch operations** - Execute multiple statements in one call +- **Cache results** - Store expensive computation results +- **Clean up resources** - Delete unused contexts promptly + +### Security + +- **Validate inputs** - Sanitize user inputs before including in code +- **Limit capabilities** - Use sessions to isolate untrusted code +- **Set resource limits** - Prevent resource exhaustion +- **Review generated code** - Inspect AI-generated code for safety + +## Common patterns + +### Interactive data exploration + + +``` +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const sandbox = getSandbox(env.Sandbox, 'data-explorer'); + + if (url.pathname === '/analyze') { + const { code } = await request.json(); + + // Create or reuse context + let contexts = await sandbox.listCodeContexts(); + let context = contexts.find(c => c.language === 'python'); + + if (!context) { + context = await sandbox.createCodeContext({ + language: 'python' + }); + + // Initialize with data + await sandbox.runCode(context.id, ` + import pandas as pd + import numpy as np + import matplotlib.pyplot as plt + + df = pd.read_csv('/workspace/dataset.csv') + print(f"Dataset loaded: {len(df)} rows") + `); + } + + // Execute user code + const result = await sandbox.runCode(context.id, code); + + return Response.json({ + output: result.output, + formats: result.formats, + outputs: result.outputs, + success: result.success + }); + } + + return new Response('Not found', { status: 404 }); + } +}; +``` + + +### Chart generation service + + +``` +async function generateChart( + chartType: string, + data: number[], + options: any +): Promise { + const context = await sandbox.createCodeContext({ + language: 'python' + }); + + const result = await sandbox.runCode(context.id, ` +import matplotlib.pyplot as plt +import numpy as np + +data = ${JSON.stringify(data)} + +plt.figure(figsize=(10, 6)) + +if "${chartType}" == "line": + plt.plot(data) +elif "${chartType}" == "bar": + plt.bar(range(len(data)), data) +elif "${chartType}" == "scatter": + plt.scatter(range(len(data)), data) + +plt.title("${options.title || 'Chart'}") +plt.xlabel("${options.xlabel || 'X'}") +plt.ylabel("${options.ylabel || 'Y'}") +plt.grid(True) +plt.show() + `); + + // Clean up + await sandbox.deleteCodeContext(context.id); + + // Return base64 PNG + return result.outputs.png || ''; +} + +// Usage +const chartData = [1, 4, 2, 8, 5, 7]; +const chart = await generateChart('line', chartData, { + title: 'Sample Data', + xlabel: 'Index', + ylabel: 'Value' +}); + +// Return as image +return new Response(atob(chart), { + headers: { 'Content-Type': 'image/png' } +}); +``` + + +## Related resources + +- [Code Interpreter API reference](/sandbox/api/interpreter/) - Complete API documentation +- [AI code executor tutorial](/sandbox/tutorials/build-an-ai-code-executor/) - Build complete AI executor +- [Execute commands guide](/sandbox/guides/execute-commands/) - Lower-level command execution +- [Best practices](/sandbox/best-practices/) - Production patterns 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..fde71b455aa0b13 --- /dev/null +++ b/src/content/docs/sandbox/guides/execute-commands.mdx @@ -0,0 +1,363 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Familiar with async/await patterns + +## 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; +} +``` + + +## Stream output in real-time + +For long-running commands like builds or installations, use streaming to provide immediate feedback: + + +``` +// Option 1: Using exec() with callbacks (simpler) +const result = await sandbox.exec('npm install && npm run build', { + stream: true, + onOutput: (stream, data) => { + if (stream === 'stdout') { + console.log(data); + } else { + console.error(data); + } + } +}); + +console.log('Build complete, exit code:', result.exitCode); +``` + + + +``` +// Option 2: Using execStream() (more control) +import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; + +const stream = await sandbox.execStream('npm test'); + +for await (const event of parseSSEStream(stream)) { + switch (event.type) { + case 'start': + console.log('Tests started'); + break; + + case 'stdout': + // Parse and process output in real-time + if (event.data.includes('PASS')) { + console.log('✓ Test passed'); + } + break; + + case 'stderr': + console.error('Error:', event.data); + break; + + case 'complete': + console.log('Tests finished, exit code:', event.exitCode); + break; + + case 'error': + console.error('Execution failed:', event.error); + break; + } +} +``` + + +## Execute shell commands + +The sandbox supports shell features like pipes, redirects, and variable expansion: + + +``` +// 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'); + +// Environment variables +await sandbox.exec('export API_KEY=secret && python app.py'); +``` + + +## Run commands with timeouts + +Prevent commands from running indefinitely: + + +``` +try { + const result = await sandbox.exec('python slow-script.py', { + timeout: 30000 // 30 second timeout + }); + + console.log('Completed in time:', result.stdout); + +} catch (error) { + if (error.message.includes('timeout')) { + console.error('Command timed out after 30 seconds'); + // Handle timeout - maybe kill related processes + await sandbox.killAllProcesses(); + } else { + throw error; + } +} +``` + + +## Execute Python scripts + +Common patterns for running Python code: + + +``` +// 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 +import json + +data = json.loads(sys.argv[1]) +result = sum(data) +print(json.dumps({"sum": result})) +`); + +const output = await sandbox.exec( + `python /workspace/analyze.py '${JSON.stringify([1, 2, 3])}'` +); + +const parsed = JSON.parse(output.stdout); +console.log('Result:', parsed.sum); // 6 +``` + + +## Execute Node.js scripts + + +``` +// Install dependencies first +await sandbox.exec('npm install lodash'); + +// Run Node.js script +await sandbox.writeFile('/workspace/process.js', ` +const _ = require('lodash'); +const data = [1, 2, 3, 4, 5]; +console.log(JSON.stringify({ + sum: _.sum(data), + mean: _.mean(data) +})); +`); + +const result = await sandbox.exec('node /workspace/process.js'); +const stats = JSON.parse(result.stdout); + +console.log('Sum:', stats.sum); // 15 +console.log('Mean:', stats.mean); // 3 +``` + + +## Best practices + +### Command design + +- **Keep commands simple** - Break complex operations into multiple commands +- **Make commands idempotent** - Commands should be safe to retry +- **Validate inputs** - Never trust user input in commands +- **Use absolute paths** - Avoid ambiguity with working directories + +### Error handling + +- **Always check exit codes** - Non-zero means failure even if no exception +- **Capture stderr** - Error messages help debug failures +- **Provide context** - Log the command that failed for debugging +- **Handle timeouts** - Don't let commands hang indefinitely + +### Performance + +- **Use streaming for long operations** - Provide immediate feedback to users +- **Batch related commands** - Use `&&` to chain dependent operations +- **Cache installations** - Don't reinstall packages every time +- **Set appropriate timeouts** - Balance between too short and too long + +### Security + +- **Escape user input** - Prevent command injection attacks +- **Limit command scope** - Use working directories to restrict access +- **Validate command output** - Don't trust command results blindly +- **Use environment variables** - Safer than inline secrets + +## Common issues + +### Command not found + +**Problem**: Command fails with "command not found" error. + +**Solution**: Verify the command exists or install it first: + + +``` +// Check if command exists +const check = await sandbox.exec('which python3'); +if (!check.success) { + // Install if needed + await sandbox.exec('apt-get update && apt-get install -y python3'); +} + +// Now safe to use +await sandbox.exec('python3 script.py'); +``` + + +### Working directory issues + +**Problem**: Command can't find files because it's running in wrong directory. + +**Solution**: Use absolute paths or change directory in command: + + +``` +// Option 1: Use absolute paths +await sandbox.exec('python /workspace/my-app/script.py'); + +// Option 2: Change directory in command +await sandbox.exec('cd /workspace/my-app && python script.py'); + +// Option 3: Use startProcess() with cwd option (for background processes) +await sandbox.startProcess('python script.py', { + cwd: '/workspace/my-app' +}); +``` + + +### Output too large + +**Problem**: Command produces huge output that causes performance issues. + +**Solution**: Stream output and process incrementally, or redirect to file: + + +``` +// Option 1: Redirect to file +await sandbox.exec('python generate-large-data.py > /tmp/output.txt'); +const result = await sandbox.readFile('/tmp/output.txt'); + +// Option 2: Stream and process incrementally +const stream = await sandbox.execStream('python generate-data.py'); +let processedCount = 0; + +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout') { + // Process each line immediately + processLine(event.data); + processedCount++; + } +} + +console.log(`Processed ${processedCount} lines`); +``` + + +## 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..dc19ee5a995042f --- /dev/null +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -0,0 +1,650 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Service or application running on a port inside the sandbox + +## 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. + + +``` +import { getSandbox } 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. Use the preview URL +console.log('Server accessible at:', exposed.exposedAt); +// Returns: https://abc123-8000.sandbox.workers.dev +``` + + +## Name your exposed ports + +When exposing multiple ports, use names to stay organized: + + +``` +// Start and expose API server +await sandbox.startProcess('node api.js'); +await new Promise(resolve => setTimeout(resolve, 2000)); +const api = await sandbox.exposePort(3000, { name: 'api' }); + +// Start and expose frontend +await sandbox.startProcess('npm run dev'); +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: + +### Simple delay + + +``` +// Start service +await sandbox.startProcess('npm run dev'); + +// Wait 2-3 seconds +await new Promise(resolve => setTimeout(resolve, 2000)); + +// Now expose +await sandbox.exposePort(3000); +``` + + +### Monitor logs for ready signal + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +// Start service +const process = await sandbox.startProcess('node server.js'); + +// Watch logs for ready message +const logs = await sandbox.streamProcessLogs(process.id); +let serverReady = false; + +for await (const log of parseSSEStream(logs)) { + console.log(log.data); + + if (log.data.includes('Server listening') || log.data.includes('ready')) { + serverReady = true; + break; + } +} + +if (serverReady) { + const exposed = await sandbox.exposePort(3000); + console.log('Server ready:', exposed.exposedAt); +} +``` + + +### Health check polling + + +``` +async function waitForHealthCheck(port: number, maxRetries: number = 10) { + for (let i = 0; i < maxRetries; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Try to reach health endpoint + const check = await sandbox.exec( + `curl -f http://localhost:${port}/health || echo "not ready"` + ); + + if (check.stdout.includes('ok') || check.success) { + console.log('Service is healthy'); + return true; + } + + console.log(`Waiting for service... (${i + 1}/${maxRetries})`); + } + + throw new Error('Service failed to become ready'); +} + +// Usage +await sandbox.startProcess('node api-server.js'); +await waitForHealthCheck(3000); +await sandbox.exposePort(3000); +``` + + +## Expose complete dev environments + +### Clone, build, and expose + + +``` +async function setupDevEnvironment(repoUrl: string) { + // Clone repository + await sandbox.gitCheckout(repoUrl); + + // Extract repo name + const repoName = repoUrl.split('/').pop()?.replace('.git', '') || 'repo'; + + // Install dependencies + await sandbox.exec(`cd ${repoName} && npm install`); + + // Start dev server + await sandbox.startProcess(`cd ${repoName} && npm run dev`); + + // Wait for server + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Expose port + const exposed = await sandbox.exposePort(3000, { + name: 'dev-server' + }); + + return { + repository: repoUrl, + url: exposed.exposedAt + }; +} + +// Usage +const env = await setupDevEnvironment('https://github.com/user/my-app'); +console.log('Dev environment ready:', env.url); +``` + + +### Full-stack application + + +``` +// Start database +await sandbox.startProcess('redis-server --port 6379'); +await new Promise(resolve => setTimeout(resolve, 1000)); + +// Start backend API +await sandbox.startProcess('node api/server.js', { + env: { + DATABASE_URL: 'redis://localhost:6379', + PORT: '3000' + } +}); +await new Promise(resolve => setTimeout(resolve, 2000)); + +// Start frontend +await sandbox.startProcess('npm run dev', { + cwd: '/workspace/frontend', + env: { + API_URL: 'http://localhost:3000', + PORT: '5173' + } +}); +await new Promise(resolve => setTimeout(resolve, 3000)); + +// Expose both services +const api = await sandbox.exposePort(3000, { name: 'api' }); +const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); + +return Response.json({ + services: { + 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}`); + } +} +``` + + +### Check if port is already exposed + + +``` +const { ports } = await sandbox.getExposedPorts(); +const isExposed = ports.some(p => p.port === 3000); + +if (isExposed) { + console.log('Port 3000 is already exposed'); + const existing = ports.find(p => p.port === 3000); + console.log('URL:', existing?.exposedAt); +} else { + await sandbox.exposePort(3000); +} +``` + + +### Unexpose ports + + +``` +// Unexpose a single port +await sandbox.unexposePort(8000); +console.log('Port 8000 is no longer accessible'); + +// Unexpose multiple ports +const portsToClose = [3000, 5173, 8080]; +for (const port of portsToClose) { + await sandbox.unexposePort(port); +} + +// Clean up with error handling +async function safeUnexposePort(port: number) { + try { + await sandbox.unexposePort(port); + console.log(`Port ${port} unexposed`); + } catch (error) { + console.log(`Port ${port} was not exposed`); + } +} +``` + + +## Temporary preview URLs + +Create preview URLs that automatically clean up: + + +``` +async function createTemporaryPreview( + command: string, + port: number, + durationMinutes: number = 5 +) { + // Start service + const process = await sandbox.startProcess(command); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Expose port + const exposed = await sandbox.exposePort(port, { + name: 'temporary-preview' + }); + + console.log(`Preview URL (expires in ${durationMinutes}min):`, exposed.exposedAt); + + // Schedule cleanup + setTimeout(async () => { + await sandbox.killProcess(process.id); + await sandbox.unexposePort(port); + console.log('Temporary preview cleaned up'); + }, durationMinutes * 60 * 1000); + + return exposed.exposedAt; +} + +// Usage +const previewUrl = await createTemporaryPreview( + 'npm run preview', + 4173, + 10 // 10 minutes +); +``` + + +## Microservices architecture + +Run and expose multiple services simultaneously: + + +``` +interface Service { + name: string; + command: string; + port: number; + cwd?: string; + env?: Record; +} + +const services: Service[] = [ + { + name: 'api', + command: 'node server.js', + port: 3000, + cwd: '/workspace/api', + env: { NODE_ENV: 'development' } + }, + { + name: 'auth', + command: 'node auth-server.js', + port: 3001, + cwd: '/workspace/auth' + }, + { + name: 'websocket', + command: 'node ws-server.js', + port: 3002, + cwd: '/workspace/websocket' + }, + { + name: 'admin', + command: 'python app.py', + port: 8080, + cwd: '/workspace/admin', + env: { FLASK_ENV: 'development' } + } +]; + +// Start and expose all services +const exposedServices = []; + +for (const service of services) { + // Start process + const process = await sandbox.startProcess(service.command, { + cwd: service.cwd, + env: service.env + }); + + console.log(`Started ${service.name}`); + + // Wait briefly + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Expose port + const exposed = await sandbox.exposePort(service.port, { + name: service.name + }); + + exposedServices.push({ + name: service.name, + url: exposed.exposedAt, + port: service.port, + processId: process.id + }); +} + +return Response.json({ + message: 'All services running', + services: exposedServices +}); +``` + + +## Best practices + +### Service startup + +- **Always wait** - Don't expose ports immediately after starting processes +- **Monitor logs** - Watch for ready signals before exposing +- **Use health checks** - Poll health endpoints to verify readiness +- **Handle failures** - Check process status if service doesn't start + +### Port management + +- **Use named ports** - Easier to track when exposing multiple ports +- **Check before exposing** - Avoid errors by checking if port is already exposed +- **Clean up** - Unexpose ports when done to prevent abandoned URLs +- **Use standard ports** - Stick to common ports (3000, 8000, 8080) for familiarity + +### Security + +- **Add authentication** - Preview URLs are public; protect sensitive services +- **Limit lifetime** - Close preview URLs for temporary demos +- **Validate origins** - Check request origins in your service +- **Use HTTPS** - Preview URLs use HTTPS automatically + +### Performance + +- **Minimize exposed ports** - Only expose what you need +- **Reuse ports** - Check for existing exposures before creating new ones +- **Clean up on errors** - Use try/finally to ensure cleanup +- **Monitor usage** - Track which services are actually being accessed + +## Common issues + +### Port not ready + +**Problem**: Exposing fails because service isn't listening yet. + +**Solution**: Wait longer or check logs for ready signal: + + +``` +// ❌ Too fast +await sandbox.startProcess('npm run dev'); +await sandbox.exposePort(3000); // May fail + +// ✅ Wait for service +await sandbox.startProcess('npm run dev'); +await new Promise(resolve => setTimeout(resolve, 3000)); +await sandbox.exposePort(3000); + +// ✅ Even better - check logs +const process = await sandbox.startProcess('npm run dev'); +const logs = await sandbox.streamProcessLogs(process.id); + +for await (const log of parseSSEStream(logs)) { + if (log.data.includes('ready')) { + break; + } +} + +await sandbox.exposePort(3000); +``` + + +### Port already exposed + +**Problem**: Trying to expose a port that's already exposed. + +**Solution**: Check first or handle the error: + + +``` +// Option 1: Check before exposing +const { ports } = await sandbox.getExposedPorts(); +if (!ports.some(p => p.port === 3000)) { + await sandbox.exposePort(3000); +} + +// Option 2: Handle error +try { + await sandbox.exposePort(3000); +} catch (error) { + if (error.message.includes('already exposed')) { + const { ports } = await sandbox.getExposedPorts(); + const existing = ports.find(p => p.port === 3000); + console.log('Using existing URL:', existing?.exposedAt); + } else { + throw error; + } +} +``` + + +### Service crashes after exposure + +**Problem**: Service starts but crashes shortly after. + +**Solution**: Monitor process status and logs: + + +``` +const process = await sandbox.startProcess('node server.js'); +await new Promise(resolve => setTimeout(resolve, 2000)); +await sandbox.exposePort(3000); + +// Monitor process health +setInterval(async () => { + const processes = await sandbox.listProcesses(); + const running = processes.find(p => p.id === process.id); + + if (!running) { + console.error('Service crashed!'); + + // Get logs to see why + const logs = await sandbox.getProcessLogs(process.id); + console.error('Crash logs:', logs); + + // Clean up exposed port + await sandbox.unexposePort(3000); + + // Optionally restart + const newProcess = await sandbox.startProcess('node server.js'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await sandbox.exposePort(3000); + } +}, 10000); // Check every 10 seconds +``` + + +### Preview URL not accessible + +**Problem**: Preview URL returns errors or timeouts. + +**Solution**: Verify service is actually listening: + + +``` +// After exposing, test the service locally first +await sandbox.exposePort(3000); + +// Test from inside the sandbox +const check = await sandbox.exec('curl http://localhost:3000'); + +if (!check.success) { + console.error('Service not responding on port 3000'); + console.error(check.stderr); + + // Check what's running + const processes = await sandbox.listProcesses(); + console.log('Running processes:', processes); + + // Check if port is actually in use + const portCheck = await sandbox.exec('lsof -i :3000 || echo "Port not in use"'); + console.log(portCheck.stdout); +} +``` + + +## Preview 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` + +The URL remains stable for the lifetime of the exposed port. + +## Security considerations + +### Public accessibility + +:::caution +Exposed ports are publicly accessible. Anyone with the URL can access your service. +::: + + +``` +// Add authentication to your service +await sandbox.writeFile('/workspace/protected-api.js', ` +const express = require('express'); +const app = express(); + +// Simple token authentication +app.use((req, res, next) => { + const token = req.headers.authorization; + + if (token !== 'Bearer secret-token-123') { + return res.status(401).json({ error: 'Unauthorized' }); + } + + next(); +}); + +// Protected endpoints +app.get('/data', (req, res) => { + res.json({ message: 'Protected data', user: 'authenticated' }); +}); + +app.listen(3000); +`); + +await sandbox.startProcess('node protected-api.js'); +await new Promise(resolve => setTimeout(resolve, 2000)); +await sandbox.exposePort(3000); +``` + + +### Temporary access + +For demos or testing, clean up immediately after use: + + +``` +try { + // Expose for testing + const exposed = await sandbox.exposePort(3000); + + // Run tests or demo + await runTests(exposed.exposedAt); + +} finally { + // Always clean up + await sandbox.unexposePort(3000); + console.log('Preview URL closed'); +} +``` + + +## 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 +- [Dev environment tutorial](/sandbox/tutorials/dev-environment/) - Complete example 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..5ec52df0e73d326 --- /dev/null +++ b/src/content/docs/sandbox/guides/git-workflows.mdx @@ -0,0 +1,571 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Git repository URLs (public or with authentication) + +## Clone repositories + +### Basic clone + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Clone a public repository +await sandbox.gitCheckout('https://github.com/user/repo'); + +console.log('Repository cloned to /workspace/repo'); +``` + + +### Clone to specific directory + + +``` +// Clone to custom location +await sandbox.gitCheckout('https://github.com/user/my-app', { + targetDir: '/workspace/project' +}); + +console.log('Repository cloned to /workspace/project'); +``` + + +### Clone specific branch + + +``` +// Clone a specific branch +await sandbox.gitCheckout('https://github.com/user/repo', { + branch: 'develop', + targetDir: '/workspace/dev-branch' +}); + +console.log('Cloned develop branch'); +``` + + +### Shallow clone + +For faster cloning of large repositories: + + +``` +// Clone only the latest commit +await sandbox.gitCheckout('https://github.com/user/large-repo', { + depth: 1 +}); + +console.log('Shallow clone completed'); +``` + + +## Clone private repositories + +### Using personal access token + + +``` +// GitHub personal access token +const token = env.GITHUB_TOKEN; +const repoUrl = `https://${token}@github.com/user/private-repo.git`; + +await sandbox.gitCheckout(repoUrl); +``` + + +### Using SSH key + + +``` +// Set up SSH key +await sandbox.writeFile('/root/.ssh/id_rsa', env.SSH_PRIVATE_KEY, { + mode: 0o600 +}); + +await sandbox.writeFile('/root/.ssh/config', ` +Host github.com + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null +`); + +// Clone via SSH +await sandbox.gitCheckout('git@github.com:user/private-repo.git'); +``` + + +## Clone and build + +Complete workflow for cloning and building projects: + + +``` +async function cloneAndBuild(repoUrl: string) { + // Clone repository + await sandbox.gitCheckout(repoUrl); + + // Extract repo name + const repoName = repoUrl.split('/').pop()?.replace('.git', '') || 'repo'; + + // Install dependencies + console.log('Installing dependencies...'); + const install = await sandbox.exec(`cd ${repoName} && npm install`); + + if (!install.success) { + throw new Error(`Install failed: ${install.stderr}`); + } + + // Run build + console.log('Building project...'); + const build = await sandbox.exec(`cd ${repoName} && npm run build`); + + if (!build.success) { + throw new Error(`Build failed: ${build.stderr}`); + } + + console.log('Build complete'); + + return { + repository: repoUrl, + directory: `/workspace/${repoName}`, + buildOutput: build.stdout + }; +} + +// Usage +const result = await cloneAndBuild('https://github.com/user/my-app'); +console.log('Project ready at:', result.directory); +``` + + +## Work with branches + +### List branches + + +``` +await sandbox.gitCheckout('https://github.com/user/repo'); + +// List all branches +const branches = await sandbox.exec('cd repo && git branch -a'); +console.log('Branches:', branches.stdout); +``` + + +### Switch branches + + +``` +// Clone repository +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Switch to different branch +await sandbox.exec('cd repo && git checkout feature-branch'); + +// Or fetch and checkout remote branch +await sandbox.exec('cd repo && git fetch origin && git checkout -b local-branch origin/remote-branch'); +``` + + +### Create and switch to new branch + + +``` +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Create new branch +await sandbox.exec('cd repo && git checkout -b new-feature'); + +console.log('Created and switched to new-feature branch'); +``` + + +## Make changes + +### Modify files and commit + + +``` +// Clone repository +await sandbox.gitCheckout('https://github.com/user/repo'); + +// Read and modify file +const readme = await sandbox.readFile('/workspace/repo/README.md'); +const updated = readme.content + '\\n\\n## New Section\\n\\nAdded via Sandbox SDK'; + +await sandbox.writeFile('/workspace/repo/README.md', updated); + +// 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"'); + +console.log('Changes committed'); +``` + + +### Create pull request + + +``` +// Make changes and commit +await sandbox.exec('cd repo && git checkout -b update-docs'); +// ... make changes ... +await sandbox.exec('cd repo && git add . && git commit -m "Update documentation"'); + +// Push to remote +await sandbox.exec(`cd repo && git push https://${env.GITHUB_TOKEN}@github.com/user/repo.git update-docs`); + +// Create PR using GitHub API +const pr = await fetch('https://api.github.com/repos/user/repo/pulls', { + method: 'POST', + headers: { + 'Authorization': `token ${env.GITHUB_TOKEN}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + title: 'Update documentation', + head: 'update-docs', + base: 'main', + body: 'Automated documentation updates' + }) +}); + +const prData = await pr.json(); +console.log('Pull request created:', prData.html_url); +``` + + +## Automated workflows + +### Clone multiple repositories + + +``` +const repositories = [ + 'https://github.com/user/frontend', + 'https://github.com/user/backend', + 'https://github.com/user/shared' +]; + +// Clone all repositories in parallel +await Promise.all( + repositories.map(repo => sandbox.gitCheckout(repo)) +); + +console.log('All repositories cloned'); +``` + + +### Sync repository changes + + +``` +async function syncRepository(repoPath: string) { + // Fetch latest changes + const fetch = await sandbox.exec(`cd ${repoPath} && git fetch origin`); + + if (!fetch.success) { + throw new Error('Failed to fetch updates'); + } + + // Check if behind remote + const status = await sandbox.exec(`cd ${repoPath} && git status -uno`); + + if (status.stdout.includes('behind')) { + // Pull changes + const pull = await sandbox.exec(`cd ${repoPath} && git pull origin main`); + + if (pull.success) { + console.log('Repository updated'); + return { updated: true, changes: pull.stdout }; + } else { + throw new Error(`Pull failed: ${pull.stderr}`); + } + } + + console.log('Repository up to date'); + return { updated: false }; +} + +// Usage +const result = await syncRepository('/workspace/my-repo'); +if (result.updated) { + console.log('Changes pulled:', result.changes); +} +``` + + +### Monorepo workflows + + +``` +async function setupMonorepo(repoUrl: string) { + // Clone monorepo + await sandbox.gitCheckout(repoUrl, { + targetDir: '/workspace/monorepo' + }); + + // List packages + const packages = await sandbox.exec('cd monorepo && ls -1 packages/'); + const packageList = packages.stdout.trim().split('\\n'); + + console.log(`Found ${packageList.length} packages:`, packageList); + + // Install dependencies for all packages + await sandbox.exec('cd monorepo && npm install'); + + // Build all packages + const build = await sandbox.exec('cd monorepo && npm run build --workspaces'); + + if (!build.success) { + throw new Error(`Monorepo build failed: ${build.stderr}`); + } + + return { + packages: packageList, + buildOutput: build.stdout + }; +} + +// Usage +const monorepo = await setupMonorepo('https://github.com/user/monorepo'); +console.log('Monorepo packages:', monorepo.packages); +``` + + +## CI/CD integration + +### Run tests on clone + + +``` +async function cloneAndTest(repoUrl: string, branch: string = 'main') { + // Clone specific branch + await sandbox.gitCheckout(repoUrl, { branch }); + + const repoName = repoUrl.split('/').pop()?.replace('.git', ''); + + // Install dependencies + await sandbox.exec(`cd ${repoName} && npm install`); + + // Run tests + const test = await sandbox.exec(`cd ${repoName} && npm test`); + + return { + success: test.success, + output: test.stdout, + errors: test.stderr, + exitCode: test.exitCode + }; +} + +// Usage in CI webhook +export default { + async fetch(request: Request, env: Env): Promise { + const webhook = await request.json(); + + if (webhook.ref === 'refs/heads/main') { + const sandbox = getSandbox(env.Sandbox, `test-${webhook.after}`); + + const results = await cloneAndTest( + webhook.repository.clone_url, + 'main' + ); + + // Report results + await reportToGitHub(webhook, results); + + return Response.json(results); + } + + return new Response('OK', { status: 200 }); + } +}; +``` + + +### Automated deployment + + +``` +async function deployFromGit(repoUrl: string, branch: string = 'main') { + // Clone repository + await sandbox.gitCheckout(repoUrl, { branch }); + + const repoName = repoUrl.split('/').pop()?.replace('.git', ''); + + // Install and build + await sandbox.exec(`cd ${repoName} && npm install && npm run build`); + + // Start application + const process = await sandbox.startProcess(`cd ${repoName} && npm start`); + + // Wait for server to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Expose port + const exposed = await sandbox.exposePort(3000, { + name: 'deployment' + }); + + return { + status: 'deployed', + url: exposed.exposedAt, + processId: process.id, + branch + }; +} + +// Usage +const deployment = await deployFromGit( + 'https://github.com/user/app', + 'production' +); + +console.log('Deployed at:', deployment.url); +``` + + +## Best practices + +### Repository management + +- **Use shallow clones** - Faster for large repos with `depth: 1` +- **Clone to specific directories** - Avoid name conflicts with `targetDir` +- **Clean up old clones** - Delete unused repositories to save space +- **Cache credentials** - Store tokens in environment variables + +### Branch management + +- **Specify branches** - Clone specific branches when possible +- **Check out sparse** - Use sparse checkout for large monorepos +- **Track remote branches** - Keep local branches synced with remote +- **Clean up branches** - Delete merged branches to save resources + +### Security + +- **Protect tokens** - Never hardcode tokens in code +- **Use environment variables** - Store credentials securely +- **Limit token scope** - Use minimal permissions needed +- **Rotate tokens regularly** - Update credentials periodically + +### Performance + +- **Shallow clones** - Use `depth: 1` for CI/CD workflows +- **Parallel clones** - Clone multiple repos concurrently +- **Incremental updates** - Pull instead of re-cloning when possible +- **Clean up** - Remove `.git` directory if not needed after clone + +## Common issues + +### Clone fails with authentication + +**Problem**: "Authentication failed" or "Permission denied" errors. + +**Solution**: Check credentials and repository access: + + +``` +// Verify token has required permissions +const token = env.GITHUB_TOKEN; + +if (!token) { + throw new Error('GITHUB_TOKEN not configured'); +} + +// Test token with API call +const test = await fetch('https://api.github.com/user', { + headers: { + 'Authorization': `token ${token}` + } +}); + +if (!test.ok) { + throw new Error('Invalid GitHub token'); +} + +// Now try cloning +const repoUrl = `https://${token}@github.com/user/private-repo.git`; +await sandbox.gitCheckout(repoUrl); +``` + + +### Large repository timeout + +**Problem**: Clone times out for large repositories. + +**Solution**: Use shallow clone or sparse checkout: + + +``` +// Option 1: Shallow clone (fastest) +await sandbox.gitCheckout('https://github.com/user/large-repo', { + depth: 1 +}); + +// Option 2: Sparse checkout (specific paths only) +await sandbox.exec(` + git clone --filter=blob:none --sparse https://github.com/user/large-repo + cd large-repo + git sparse-checkout set packages/my-package +`); +``` + + +### Merge conflicts + +**Problem**: Pull fails due to merge conflicts. + +**Solution**: Stash changes or hard reset: + + +``` +// Option 1: Stash changes +await sandbox.exec('cd repo && git stash'); +await sandbox.exec('cd repo && git pull origin main'); +await sandbox.exec('cd repo && git stash pop'); + +// Option 2: Hard reset (discards local changes) +await sandbox.exec('cd repo && git fetch origin'); +await sandbox.exec('cd repo && git reset --hard origin/main'); +``` + + +### Submodule issues + +**Problem**: Repository has submodules that aren't cloned. + +**Solution**: Clone with submodules: + + +``` +// Clone with submodules +await sandbox.exec('git clone --recursive https://github.com/user/repo'); + +// Or update submodules after cloning +await sandbox.gitCheckout('https://github.com/user/repo'); +await sandbox.exec('cd repo && git submodule update --init --recursive'); +``` + + +## 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 +- [CI/CD tutorial](/sandbox/tutorials/build-system/) - Automated testing workflows diff --git a/src/content/docs/sandbox/guides/index.mdx b/src/content/docs/sandbox/guides/index.mdx new file mode 100644 index 000000000000000..66f01bd2fb85ac8 --- /dev/null +++ b/src/content/docs/sandbox/guides/index.mdx @@ -0,0 +1,24 @@ +--- +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 +- [Best practices](/sandbox/best-practices/) - Performance, security, and production patterns 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..18b5ef959c8b74a --- /dev/null +++ b/src/content/docs/sandbox/guides/manage-files.mdx @@ -0,0 +1,564 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Understanding of absolute vs relative paths + +## Path conventions + +All file operations use absolute paths in the sandbox: + +- `/workspace` - Default working directory for application files (recommended) +- `/tmp` - Temporary files (may be cleared) +- `/home` - User home directory + + +``` +// ✅ Use absolute paths +await sandbox.writeFile('/workspace/app.js', code); + +// ❌ Don't use relative paths +await sandbox.writeFile('app.js', code); // Will fail +``` + + +## Write files + +### Write text files + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Write a simple text file +await sandbox.writeFile('/workspace/app.js', ` +console.log('Hello from sandbox!'); +`); + +// Write with specific encoding +await sandbox.writeFile('/workspace/data.txt', content, { + encoding: 'utf-8' +}); +``` + + +### Write JSON configuration + + +``` +const config = { + name: 'my-app', + version: '1.0.0', + environment: 'production', + database: { + host: 'localhost', + port: 5432 + } +}; + +await sandbox.writeFile( + '/workspace/config.json', + JSON.stringify(config, null, 2) +); +``` + + +### Write binary files + + +``` +// Write base64-encoded image +const imageBase64 = '...'; // Base64 image data + +await sandbox.writeFile('/workspace/logo.png', imageBase64, { + encoding: 'base64' +}); + +// Write buffer data +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 files + + +``` +// Read a file +const file = await sandbox.readFile('/workspace/app.js'); +console.log(file.content); +console.log(file.encoding); // 'utf-8' +``` + + +### Read and parse JSON + + +``` +const file = await sandbox.readFile('/workspace/package.json'); +const packageJson = JSON.parse(file.content); + +console.log('Project:', packageJson.name); +console.log('Version:', packageJson.version); +console.log('Dependencies:', Object.keys(packageJson.dependencies || {})); +``` + + +### Read binary files + + +``` +const file = await sandbox.readFile('/workspace/image.png', { + encoding: 'base64' +}); + +// file.content is base64-encoded +const dataUrl = `data:image/png;base64,${file.content}`; + +// Or send to client +return new Response(atob(file.content), { + headers: { 'Content-Type': 'image/png' } +}); +``` + + +## Organize files + +### Create directories + + +``` +// Create single directory +await sandbox.mkdir('/workspace/src'); + +// Create nested directories +await sandbox.mkdir('/workspace/src/components/ui', { + recursive: true +}); + +// Create project structure +await sandbox.mkdir('/workspace/src', { recursive: true }); +await sandbox.mkdir('/workspace/tests', { recursive: true }); +await sandbox.mkdir('/workspace/public', { recursive: true }); +``` + + +### Move and rename files + + +``` +// Rename a file +await sandbox.renameFile( + '/workspace/draft.txt', + '/workspace/final.txt' +); + +// Move to different directory +await sandbox.moveFile( + '/tmp/download.txt', + '/workspace/data.txt' +); + +// Organize files into structure +const files = ['app.js', 'server.js', 'utils.js']; +await sandbox.mkdir('/workspace/src', { recursive: true }); + +for (const file of files) { + await sandbox.moveFile( + `/workspace/${file}`, + `/workspace/src/${file}` + ); +} +``` + + +### Delete files + + +``` +// Delete a single file +await sandbox.deleteFile('/workspace/temp.txt'); + +// Clean up multiple files +const tempFiles = [ + '/tmp/build-output.log', + '/tmp/cache.json', + '/workspace/.env.local' +]; + +for (const file of tempFiles) { + try { + await sandbox.deleteFile(file); + } catch (error) { + console.log(`Could not delete ${file}:`, error.message); + } +} +``` + + +## Work with project structures + +### Create a complete project + + +``` +// Create directory structure +await sandbox.mkdir('/workspace/my-app/src', { recursive: true }); +await sandbox.mkdir('/workspace/my-app/tests', { recursive: true }); + +// Create package.json +await sandbox.writeFile('/workspace/my-app/package.json', JSON.stringify({ + name: 'my-app', + version: '1.0.0', + scripts: { + start: 'node src/index.js', + test: 'jest' + }, + dependencies: { + express: '^4.18.0' + } +}, null, 2)); + +// Create main application file +await sandbox.writeFile('/workspace/my-app/src/index.js', ` +const express = require('express'); +const app = express(); + +app.get('/', (req, res) => { + res.json({ status: 'running' }); +}); + +app.listen(3000, () => { + console.log('Server started on port 3000'); +}); +`); + +// Install and run +await sandbox.exec('cd /workspace/my-app && npm install'); +await sandbox.startProcess('node src/index.js', { + cwd: '/workspace/my-app' +}); +``` + + +### Clone from Git and modify + + +``` +// Clone repository +await sandbox.gitCheckout('https://github.com/user/repo', { + targetDir: '/workspace/project' +}); + +// Read and modify configuration +const configFile = await sandbox.readFile('/workspace/project/config.json'); +const config = JSON.parse(configFile.content); + +config.environment = 'development'; +config.apiUrl = 'https://api.example.com'; + +// Write back +await sandbox.writeFile( + '/workspace/project/config.json', + JSON.stringify(config, null, 2) +); + +// Build project +await sandbox.exec('cd /workspace/project && npm install && npm run build'); +``` + + +## Batch operations + +### Write multiple files + + +``` +const files = { + '/workspace/src/app.js': 'console.log("app");', + '/workspace/src/utils.js': 'console.log("utils");', + '/workspace/src/config.js': 'module.exports = { port: 3000 };', + '/workspace/README.md': '# My Project\n\nA sample project.' +}; + +// Write all files in parallel +await Promise.all( + Object.entries(files).map(([path, content]) => + sandbox.writeFile(path, content) + ) +); + +console.log('All files created'); +``` + + +### Copy files + +The SDK doesn't have a direct copy method, but you can read and write: + + +``` +// Copy a single file +async function copyFile(source: string, destination: string) { + const file = await sandbox.readFile(source); + await sandbox.writeFile(destination, file.content); +} + +await copyFile('/workspace/template.html', '/workspace/index.html'); + +// Copy with transformation +const template = await sandbox.readFile('/workspace/template.html'); +const customized = template.content.replace('{{title}}', 'My App'); +await sandbox.writeFile('/workspace/index.html', customized); +``` + + +## Handle file errors + +### Check if file exists + + +``` +async function fileExists(path: string): Promise { + try { + await sandbox.readFile(path); + return true; + } catch (error) { + if (error.code === 'FILE_NOT_FOUND') { + return false; + } + throw error; // Other errors should still throw + } +} + +// Use it +if (await fileExists('/workspace/config.json')) { + console.log('Config file exists'); +} else { + // Create default config + await sandbox.writeFile('/workspace/config.json', '{}'); +} +``` + + +### Create file with backup + + +``` +async function writeFileWithBackup(path: string, content: string) { + // Check if file exists + try { + await sandbox.readFile(path); + + // File exists - create backup + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = `${path}.${timestamp}.backup`; + + await sandbox.renameFile(path, backupPath); + console.log('Created backup:', backupPath); + + } catch (error) { + if (error.code !== 'FILE_NOT_FOUND') { + throw error; + } + // File doesn't exist - no backup needed + } + + // Write new file + await sandbox.writeFile(path, content); +} + +await writeFileWithBackup('/workspace/config.json', JSON.stringify(newConfig)); +``` + + +## Best practices + +### File organization + +- **Use `/workspace` for app files** - This is the standard working directory +- **Use `/tmp` for temporary files** - These may be cleared periodically +- **Create logical directory structures** - Organize code like a real project +- **Use consistent naming** - Follow conventions (lowercase, hyphens) + +### Performance + +- **Batch file operations** - Use `Promise.all()` for independent writes +- **Avoid reading large files repeatedly** - Cache file contents when possible +- **Use streaming for large files** - Write to file instead of keeping in memory +- **Clean up temporary files** - Don't let `/tmp` accumulate files + +### Security + +- **Validate file paths** - Prevent path traversal attacks +- **Limit file sizes** - Don't write unbounded data +- **Be careful with user content** - Sanitize filenames and content +- **Use appropriate permissions** - Set file modes when needed + +### Reliability + +- **Handle file errors gracefully** - Files may not exist or be readable +- **Create parent directories** - Use `recursive: true` for nested paths +- **Check operation results** - Verify writes succeeded +- **Back up important files** - Before overwriting + +## Common patterns + +### Template files + + +``` +// Read template +const template = await sandbox.readFile('/workspace/templates/email.html'); + +// Replace placeholders +const email = template.content + .replace('{{name}}', userName) + .replace('{{date}}', new Date().toISOString()) + .replace('{{content}}', emailContent); + +// Write final file +await sandbox.writeFile('/workspace/output/email.html', email); +``` + + +### Configuration management + + +``` +// Load base config +const baseConfig = await sandbox.readFile('/workspace/config.base.json'); +const config = JSON.parse(baseConfig.content); + +// Override with environment-specific settings +config.environment = 'production'; +config.debug = false; +config.apiUrl = env.API_URL; + +// Write final config +await sandbox.writeFile( + '/workspace/config.json', + JSON.stringify(config, null, 2) +); +``` + + +### Data processing pipeline + + +``` +// 1. Read input data +const input = await sandbox.readFile('/workspace/data/input.json'); +const data = JSON.parse(input.content); + +// 2. Process data +const processed = data.map(item => ({ + id: item.id, + value: item.value * 2, + timestamp: new Date().toISOString() +})); + +// 3. Write output +await sandbox.writeFile( + '/workspace/data/output.json', + JSON.stringify(processed, null, 2) +); + +// 4. Generate report +const report = ` +Processed ${data.length} items +Output: /workspace/data/output.json +Timestamp: ${new Date().toISOString()} +`; + +await sandbox.writeFile('/workspace/reports/report.txt', report); +``` + + +## Common issues + +### Path not found + +**Problem**: "No such file or directory" error. + +**Solution**: Create parent directories first: + + +``` +// ❌ This will fail if /workspace/data doesn't exist +await sandbox.writeFile('/workspace/data/file.txt', content); + +// ✅ Create directory first +await sandbox.mkdir('/workspace/data', { recursive: true }); +await sandbox.writeFile('/workspace/data/file.txt', content); +``` + + +### File encoding issues + +**Problem**: File content appears corrupted or garbled. + +**Solution**: Specify correct encoding: + + +``` +// For binary files, use base64 +await sandbox.writeFile('/workspace/image.png', base64Data, { + encoding: 'base64' +}); + +// For text files, utf-8 is default +await sandbox.writeFile('/workspace/data.txt', textContent, { + encoding: 'utf-8' +}); +``` + + +### Large file handling + +**Problem**: Large files cause performance issues or memory errors. + +**Solution**: Process files in chunks or use command-line tools: + + +``` +// Instead of reading entire file +// const file = await sandbox.readFile('/workspace/huge-file.txt'); + +// Use command-line tools to process in chunks +await sandbox.exec('head -n 1000 /workspace/huge-file.txt > /tmp/chunk.txt'); +const chunk = await sandbox.readFile('/tmp/chunk.txt'); + +// Or process line by line with streaming +await sandbox.exec(` + while IFS= read -r line; do + echo "Processing: $line" + done < /workspace/huge-file.txt +`); +``` + + +## 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..74029f056a60cc5 --- /dev/null +++ b/src/content/docs/sandbox/guides/streaming-output.mdx @@ -0,0 +1,530 @@ +--- +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. + +## Prerequisites + +- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) +- Understanding of async streams and Server-Sent Events (SSE) + +## 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 + +### Using exec() with callbacks + +The simplest approach for streaming output: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Execute with streaming callbacks +const result = await sandbox.exec('npm install express', { + stream: true, + onOutput: (stream, data) => { + if (stream === 'stdout') { + console.log('OUT:', data); + } else { + console.error('ERR:', data); + } + } +}); + +console.log('Installation complete, exit code:', result.exitCode); +``` + + +### Using execStream() for full control + +For more control over stream handling: + + +``` +import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; + +// Execute and get stream +const stream = await sandbox.execStream('npm run build'); + +// Process events +for await (const event of parseSSEStream(stream)) { + switch (event.type) { + case 'start': + console.log('Build started:', event.command); + break; + + case 'stdout': + console.log('OUTPUT:', event.data); + break; + + case 'stderr': + console.error('ERROR:', event.data); + break; + + case 'complete': + console.log('Build finished with exit code:', event.exitCode); + break; + + case 'error': + console.error('Execution failed:', event.error); + break; + } +} +``` + + +## Stream to client + +Return streaming output directly to users: + + +``` +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === '/build') { + const sandbox = getSandbox(env.Sandbox, 'builder'); + + // Get stream from command + const stream = await sandbox.execStream('npm run build'); + + // Return stream directly to client + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + } + }); + } + + return new Response('Not found', { status: 404 }); + } +}; +``` + + +### Client-side consumption + + +``` +// Browser JavaScript to consume SSE stream +const eventSource = new EventSource('/build'); + +eventSource.addEventListener('stdout', (event) => { + const data = JSON.parse(event.data); + console.log('Build output:', data.data); + document.getElementById('output').textContent += data.data + '\\n'; +}); + +eventSource.addEventListener('complete', (event) => { + const data = JSON.parse(event.data); + console.log('Build complete, exit code:', data.exitCode); + eventSource.close(); +}); + +eventSource.addEventListener('error', (event) => { + console.error('Stream error:', event); + eventSource.close(); +}); +``` + + +## Track progress + +### Parse output for progress indicators + + +``` +const stream = await sandbox.execStream('npm test'); + +let testsRun = 0; +let testsPassed = 0; +let testsFailed = 0; + +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout') { + // Count test results + if (event.data.includes('✓')) { + testsPassed++; + testsRun++; + console.log(`Tests: ${testsPassed} passed, ${testsFailed} failed (${testsRun} total)`); + } else if (event.data.includes('✗')) { + testsFailed++; + testsRun++; + console.log(`Tests: ${testsPassed} passed, ${testsFailed} failed (${testsRun} total)`); + } + } + + if (event.type === 'complete') { + console.log(`Test run complete: ${testsPassed}/${testsRun} passed`); + } +} +``` + + +### Real-time progress updates + + +``` +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'build-system'); + + // Start build with streaming + const stream = await sandbox.execStream('npm run build'); + + // Transform stream to add progress info + const transformStream = new TransformStream({ + transform(chunk, controller) { + // Parse SSE event + const text = new TextDecoder().decode(chunk); + + if (text.includes('Building')) { + controller.enqueue( + new TextEncoder().encode( + `data: ${JSON.stringify({ type: 'progress', percent: 25 })}\\n\\n` + ) + ); + } else if (text.includes('Optimizing')) { + controller.enqueue( + new TextEncoder().encode( + `data: ${JSON.stringify({ type: 'progress', percent: 75 })}\\n\\n` + ) + ); + } + + // Pass through original + controller.enqueue(chunk); + } + }); + + return new Response(stream.pipeThrough(transformStream), { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache' + } + }); + } +}; +``` + + +## Stream process logs + +Monitor background process output: + + +``` +import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; + +// Start background process +const process = await sandbox.startProcess('node server.js'); + +// Stream logs in real-time +const logStream = await sandbox.streamProcessLogs(process.id); + +for await (const log of parseSSEStream(logStream)) { + console.log(`[${log.timestamp}] ${log.data}`); + + // React to specific log messages + if (log.data.includes('Server listening on port')) { + console.log('Server is ready'); + // Expose port now that server is ready + await sandbox.exposePort(3000); + break; + } + + if (log.data.includes('ERROR') || log.data.includes('FATAL')) { + console.error('Server error detected:', log.data); + // Handle error + } +} +``` + + +## Stream code execution + +Stream output from code interpreter: + + +``` +// Create code context +const context = await sandbox.createCodeContext({ + language: 'python' +}); + +// Execute with streaming +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("Complete!") +`, + { + stream: true, + onOutput: (data) => { + console.log('Output:', data); + // Send to client via WebSocket or SSE + }, + onResult: (result) => { + console.log('Execution complete'); + console.log('Outputs:', result.outputs); + }, + onError: (error) => { + console.error('Execution error:', error); + } + } +); + +console.log('Final result:', result); +``` + + +## Buffer and batch output + +For high-frequency output, buffer updates: + + +``` +const stream = await sandbox.execStream('npm test -- --verbose'); + +let buffer = ''; +let lastUpdate = Date.now(); +const UPDATE_INTERVAL = 100; // ms + +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout' || event.type === 'stderr') { + buffer += event.data; + + // Batch updates every 100ms + if (Date.now() - lastUpdate > UPDATE_INTERVAL) { + console.log(buffer); + buffer = ''; + lastUpdate = Date.now(); + } + } + + if (event.type === 'complete') { + // Flush remaining buffer + if (buffer) { + console.log(buffer); + } + console.log('Complete'); + } +} +``` + + +## Filter and transform output + +Process streaming output selectively: + + +``` +const stream = await sandbox.execStream('npm install'); + +for await (const event of parseSSEStream(stream)) { + if (event.type === 'stdout') { + // Filter out verbose npm output + if (event.data.includes('http fetch') || event.data.includes('timing')) { + continue; // Skip verbose messages + } + + // Transform output + const cleaned = event.data + .replace(/\u001b\[[0-9;]*m/g, '') // Remove ANSI colors + .trim(); + + if (cleaned) { + console.log(cleaned); + } + } else if (event.type === 'stderr') { + // Always show errors + console.error('ERROR:', event.data); + } +} +``` + + +## Handle streaming errors + + +``` +try { + 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 'error': + // Stream error occurred + throw new Error(`Build failed: ${event.error}`); + + case 'complete': + if (event.exitCode !== 0) { + throw new Error(`Build failed with exit code ${event.exitCode}`); + } + console.log('Build succeeded'); + break; + } + } +} catch (error) { + console.error('Streaming failed:', error); + + // Get accumulated logs if stream failed + try { + const logs = await sandbox.getProcessLogs(processId); + console.error('Full logs:', logs); + } catch { + // Ignore if can't get logs + } +} +``` + + +## Best practices + +### Stream handling + +- **Always consume streams** - Don't let streams hang unconsumed +- **Handle all event types** - Process start, output, complete, and error events +- **Close connections** - Clean up streams when done +- **Use timeouts** - Set limits for long-running streams + +### Performance + +- **Buffer high-frequency output** - Batch updates to reduce overhead +- **Filter unnecessary output** - Skip verbose messages early +- **Use backpressure** - Let streams control flow +- **Clean up resources** - Close streams promptly + +### User experience + +- **Show progress indicators** - Parse output for progress info +- **Provide feedback** - Show something immediately, even if just "Starting..." +- **Handle errors gracefully** - Show clear error messages +- **Allow cancellation** - Let users stop long operations + +### Error handling + +- **Check exit codes** - Non-zero means failure +- **Parse stderr** - Error details usually in stderr +- **Save full output** - Keep complete logs for debugging +- **Retry on failures** - Handle transient stream errors + +## Common patterns + +### Build pipeline with progress + + +``` +async function buildWithProgress(sandbox: Sandbox) { + const stages = [ + { name: 'Installing dependencies', command: 'npm install' }, + { name: 'Running tests', command: 'npm test' }, + { name: 'Building project', command: 'npm run build' }, + { name: 'Running linter', command: 'npm run lint' } + ]; + + for (let i = 0; i < stages.length; i++) { + const stage = stages[i]; + const progress = ((i + 1) / stages.length) * 100; + + console.log(`[${progress.toFixed(0)}%] ${stage.name}...`); + + const result = await sandbox.exec(stage.command, { + stream: true, + onOutput: (stream, data) => { + console.log(` ${data}`); + } + }); + + if (!result.success) { + throw new Error(`${stage.name} failed: ${result.stderr}`); + } + } + + console.log('[100%] Build pipeline complete'); +} +``` + + +### Real-time log viewer + + +``` +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === '/logs') { + const processId = url.searchParams.get('process'); + + if (!processId) { + return new Response('Missing process ID', { status: 400 }); + } + + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + + // Stream logs to client + const logStream = await sandbox.streamProcessLogs(processId); + + return new Response(logStream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' // Disable nginx buffering + } + }); + } + + return new Response('Not found', { status: 404 }); + } +}; +``` + + +## 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 From 4ab654634ea7ce013bc2943463f2de56a5a3ae05 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 14:17:54 +0100 Subject: [PATCH 09/22] Add concepts --- .../docs/sandbox/concepts/architecture.mdx | 427 ++++++++++++ .../docs/sandbox/concepts/containers.mdx | 501 ++++++++++++++ src/content/docs/sandbox/concepts/index.mdx | 23 + .../docs/sandbox/concepts/preview-urls.mdx | 519 ++++++++++++++ .../docs/sandbox/concepts/sandboxes.mdx | 458 +++++++++++++ .../docs/sandbox/concepts/security.mdx | 631 ++++++++++++++++++ .../docs/sandbox/concepts/sessions.mdx | 507 ++++++++++++++ 7 files changed, 3066 insertions(+) create mode 100644 src/content/docs/sandbox/concepts/architecture.mdx create mode 100644 src/content/docs/sandbox/concepts/containers.mdx create mode 100644 src/content/docs/sandbox/concepts/index.mdx create mode 100644 src/content/docs/sandbox/concepts/preview-urls.mdx create mode 100644 src/content/docs/sandbox/concepts/sandboxes.mdx create mode 100644 src/content/docs/sandbox/concepts/security.mdx create mode 100644 src/content/docs/sandbox/concepts/sessions.mdx diff --git a/src/content/docs/sandbox/concepts/architecture.mdx b/src/content/docs/sandbox/concepts/architecture.mdx new file mode 100644 index 000000000000000..d946937826d5349 --- /dev/null +++ b/src/content/docs/sandbox/concepts/architecture.mdx @@ -0,0 +1,427 @@ +--- +title: Architecture +pcx_content_type: concept +sidebar: + order: 1 +--- + +import { Render } from "~/components"; + +This page explains how the Sandbox SDK is structured, why it's designed this way, and how the pieces fit together. + +## Overview + +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 + +This architecture enables running untrusted code safely while maintaining the benefits of edge computing: global distribution, low latency, and automatic scaling. + +## Three-layer architecture + +The SDK uses a layered design where each layer has a distinct purpose and pattern: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Your Application │ +│ (Cloudflare Worker) │ +└───────────────────────────┬───────────────────────────────┘ + │ + ├─ getSandbox() + ├─ exec() + ├─ writeFile() + ├─ exposePort() + │ + ┌───────▼───────┐ + │ Client SDK │ Layer 1: Developer Interface + │ (TypeScript) │ + └───────┬───────┘ + │ HTTP/JSON + │ + ┌───────▼───────┐ + │ Durable Object │ Layer 2: State Management + │ (Persistent) │ + └───────┬───────┘ + │ Container Protocol + │ + ┌───────▼───────┐ + │ Container │ Layer 3: Isolated Execution + │ (Linux + Bun) │ + └───────────────┘ +``` + +### Layer 1: Client SDK + +The Client SDK provides the developer-facing API. It's what you import and 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. + +**Key characteristics**: +- Domain-organized clients (CommandClient, FileClient, ProcessClient, etc.) +- Direct response interfaces (not wrapped in service results) +- Throws typed errors on failures +- Handles HTTP communication with Durable Object +- Maps container errors to user-friendly exceptions + +**Why this design**: Developers expect simple, direct APIs. The SDK hides complexity and provides clear error messages. + +### Layer 2: Durable Object + +The 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. + +**Key characteristics**: +- Each sandbox is a unique Durable Object instance +- Survives across requests (persistent state) +- Manages container lifecycle (create, pause, resume) +- Routes commands to container runtime +- Generates and manages preview URLs +- Handles security and access control + +**Why Durable Objects**: They provide: +- **Persistent identity** - Same sandbox ID always routes to same instance +- **State management** - File system and processes persist between requests +- **Geographic distribution** - Sandboxes run close to users +- **Automatic scaling** - Cloudflare manages provisioning + +### Layer 3: Container Runtime + +The Container Runtime executes code in isolation: + +**Purpose**: Safely execute untrusted code with full Linux capabilities. + +**Key characteristics**: +- Linux environment with Bun runtime +- Service-oriented architecture (ProcessService, FileService, etc.) +- ServiceResult pattern for all operations (success/error) +- Security validation on all inputs +- Streaming support for real-time output +- HTTP API for command execution + +**Why containers**: They provide: +- **True isolation** - Process-level isolation with namespaces +- **Full environment** - Real Linux with Python, Node.js, Git, etc. +- **Resource limits** - CPU, memory, disk constraints +- **No escape** - Sandboxed execution prevents host access + +## Request flow + +Here's what happens when you execute a command: + +```typescript +await sandbox.exec('python script.py') +``` + +1. **Client SDK** (Your Worker) + - Validates parameters + - Builds HTTP request + - Sends to Durable Object + +2. **Durable Object** + - Receives request from client + - Checks authentication + - Routes to container runtime + - Returns response to client + +3. **Container Runtime** + - Receives command via HTTP + - Validates inputs for security + - Executes command with Bun.spawn() + - Captures stdout/stderr + - Returns structured result + +4. **Response flows back** + - Container → Durable Object → Client SDK → Your code + - Errors are transformed at each layer + - Streaming data flows through all layers + +## Why this architecture? + +### Separation of concerns + +Each layer has a single, clear responsibility: + +- **Client SDK**: Developer experience +- **Durable Object**: State and routing +- **Container**: Execution and isolation + +This separation makes the system: +- Easier to understand and debug +- Simpler to test (mock each layer independently) +- More maintainable (changes are localized) +- More reliable (failures are contained) + +### Edge-first design + +The SDK is built for Cloudflare's edge network: + +- **Global distribution** - Sandboxes run in 300+ cities worldwide +- **Low latency** - Code executes close to users +- **Automatic scaling** - No capacity planning needed +- **Integrated platform** - Works seamlessly with other Cloudflare products + +### Security by design + +Multiple layers of security: + +1. **Container isolation** - Linux namespaces prevent host access +2. **Input validation** - All inputs validated before execution +3. **Resource limits** - CPU, memory, disk quotas enforced +4. **Path security** - File operations restricted to safe directories +5. **Command sanitization** - Dangerous commands blocked + +### Developer experience + +The architecture prioritizes ease of use: + +- **Simple API** - Clean, TypeScript-first interface +- **Error handling** - Clear, actionable error messages +- **Type safety** - Full TypeScript support +- **Familiar patterns** - Standard async/await, no callbacks +- **Rich features** - Streaming, sessions, preview URLs built-in + +## Execution models + +The SDK supports two execution patterns: + +### Direct execution + +One-time commands that complete and exit: + +```typescript +const result = await sandbox.exec('npm test'); +// Command runs, completes, returns result +``` + +**Use for**: +- Scripts and utilities +- Build commands +- One-time data processing +- Testing + +**Characteristics**: +- Waits for completion +- Returns full output +- No persistent state needed +- Simple error handling + +### Background processes + +Long-running services that continue after starting: + +```typescript +const process = await sandbox.startProcess('node server.js'); +await sandbox.exposePort(3000); +// Server runs continuously in background +``` + +**Use for**: +- Web servers and APIs +- Development environments +- Long-running services +- Multi-process applications + +**Characteristics**: +- Returns immediately (doesn't wait) +- Process ID for later management +- Can expose ports for network access +- Requires explicit cleanup + +## State management + +Sandboxes maintain state across requests: + +### Filesystem state + +Files persist between commands: + +```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 +``` + +### Process state + +Background processes continue running: + +```typescript +// Request 1 +await sandbox.startProcess('node server.js'); +await sandbox.exposePort(3000); + +// Request 2 (minutes later) +const processes = await sandbox.listProcesses(); +// Server still running +``` + +### Code context state + +Code interpreter contexts remember variables: + +```typescript +// Execution 1 +await sandbox.runCode(contextId, 'x = 42'); + +// Execution 2 +await sandbox.runCode(contextId, 'print(x)'); +// Outputs '42' - variable persisted +``` + +## Design trade-offs + +Every architecture involves trade-offs. Here's what we chose and why: + +### Three layers vs. two + +**Choice**: Client SDK + Durable Object + Container (not just Client + Container) + +**Why**: Durable Objects provide: +- Persistent sandbox identity +- State management +- Global distribution +- Access control + +**Trade-off**: Extra network hop, but worth it for statefulness and routing. + +### HTTP vs. custom protocol + +**Choice**: HTTP/JSON between layers + +**Why**: +- Simple to implement and debug +- Works with existing tools (curl, fetch) +- Easy to add middleware (CORS, logging) +- Standard error handling + +**Trade-off**: Slight overhead vs. binary protocol, but negligible for I/O-bound operations. + +### ServiceResult pattern + +**Choice**: Container services return `ServiceResult` (success/error union type) + +**Why**: +- Explicit error handling (can't forget) +- Type-safe errors +- No exceptions in service layer +- Easy to compose operations + +**Trade-off**: More verbose than throwing errors, but much safer. + +### Bun runtime + +**Choice**: Bun instead of Node.js in containers + +**Why**: +- Faster startup (critical for cold starts) +- Built-in TypeScript support +- Better subprocess management +- Modern APIs + +**Trade-off**: Newer runtime, but stable for our use case. + +## Scalability and performance + +### How sandboxes scale + +1. **Per-sandbox isolation** + - Each sandbox is a separate Durable Object + - No shared state between sandboxes + - Sandboxes scale independently + +2. **Geographic distribution** + - Durable Objects run close to users + - Containers are colocated with Durable Objects + - First request determines location + +3. **Automatic provisioning** + - Cloudflare handles scaling + - No capacity planning needed + - Scales from 1 to millions of sandboxes + +### Performance characteristics + +**Cold start**: 100-300ms (container initialization) +**Warm start**: \<10ms (reuse existing container) +**Command execution**: Depends on command (I/O-bound) +**Network latency**: 10-50ms (edge-to-edge) + +**Optimization strategies**: +- Keep sandboxes warm with periodic requests +- Batch operations when possible +- Use streaming for long operations +- Cache computation results + +## Comparison to alternatives + +### vs. Traditional VMs + +**Sandbox SDK**: +- ✅ Instant provisioning (<1s) +- ✅ Global distribution +- ✅ Pay-per-use pricing +- ❌ Linux-only (no Windows) + +**VMs**: +- ❌ Slow provisioning (minutes) +- ❌ Regional deployment +- ❌ Always-on costs +- ✅ Any OS + +### vs. Serverless Functions + +**Sandbox SDK**: +- ✅ Stateful (filesystem, processes) +- ✅ Long-running operations +- ✅ Full Linux environment +- ❌ Slightly slower cold starts + +**Functions**: +- ❌ Stateless +- ❌ Strict timeouts +- ❌ Limited environment +- ✅ Very fast cold starts + +### vs. Remote Code Execution APIs + +**Sandbox SDK**: +- ✅ Persistent state +- ✅ Multi-language support +- ✅ Full control +- ✅ Preview URLs +- ❌ Manage sandboxes yourself + +**Remote APIs**: +- ❌ No state +- ❌ Limited languages +- ❌ Restricted capabilities +- ❌ No custom URLs +- ✅ Fully managed + +## 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..cd429acf8bbb74e --- /dev/null +++ b/src/content/docs/sandbox/concepts/containers.mdx @@ -0,0 +1,501 @@ +--- +title: Container runtime +pcx_content_type: concept +sidebar: + order: 3 +--- + +This page explains what's inside a sandbox container, how code executes, and what capabilities are available. + +## Container environment + +Each sandbox runs in an isolated Linux container with: + +- **Operating System**: Linux (Ubuntu-based) +- **Runtime**: Bun (JavaScript/TypeScript) +- **Shell**: Bash +- **Python**: Python 3.11+ +- **Node.js**: Available via Bun compatibility +- **Package managers**: npm, pip, apt +- **Git**: Full Git client +- **Common tools**: curl, wget, sed, awk, grep, etc. + +## Why Linux containers? + +Containers provide the right balance of isolation, performance, and capabilities: + +### True isolation + +Unlike shared runtimes, containers provide: + +- **Process isolation** - Each sandbox has its own process namespace +- **Filesystem isolation** - Separate root filesystem per sandbox +- **Network isolation** - Isolated network stack +- **Resource isolation** - CPU and memory limits per sandbox + +This means code in one sandbox cannot see or affect code in another sandbox, even if they run on the same physical machine. + +### Full environment + +Containers offer a complete Linux environment: + +- **Real filesystem** - Not a virtual filesystem, actual disk access +- **Process management** - Fork, exec, signals all work normally +- **Network stack** - Full TCP/IP, can run servers +- **System tools** - Access to standard Linux utilities + +This enables running complex applications that expect a real operating system. + +### Compatibility + +Standard Linux environment means: + +- **Existing code runs** - Most Linux software works without modification +- **Package managers work** - Install dependencies with apt, pip, npm +- **Scripts are portable** - Bash scripts run as expected +- **Tools are familiar** - Standard Linux commands available + +## Why Bun? + +The container uses Bun as its primary runtime: + +### Fast startup + +Bun starts almost instantly: + +- **Cold start**: 100-300ms total (vs. 500ms+ with Node.js) +- **Warm start**: \<10ms +- **Process spawning**: Very fast subprocess creation + +Fast startup is critical for edge computing where containers may start and stop frequently. + +### Built-in TypeScript + +No transpilation step needed: + +```typescript +// This works directly in the container +import type { Config } from './types'; + +const config: Config = { + port: 3000, + environment: 'production' +}; +``` + +Developers can write TypeScript without build steps. + +### Better subprocess management + +Bun.spawn() provides: + +- **Streaming output** - Read stdout/stderr in real-time +- **Process control** - Signals, timeouts, environment variables +- **Performance** - Faster than Node.js child_process +- **Type safety** - Full TypeScript support + +This is core to the SDK's command execution capabilities. + +### Node.js compatibility + +Bun is compatible with Node.js: + +- **npm packages work** - Install and use npm packages normally +- **Node.js APIs available** - fs, path, http, etc. +- **Existing code runs** - Most Node.js code works without changes + +## Available runtimes + +### Python + +Python 3.11+ is pre-installed: + +```python +# Python works out of the box +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +data = np.array([1, 2, 3, 4, 5]) +print(f"Mean: {np.mean(data)}") +``` + +**Pre-installed packages**: +- NumPy - Numerical computing +- pandas - Data analysis +- Matplotlib - Plotting and visualization +- Requests - HTTP library + +**Install additional packages**: +```bash +pip install scikit-learn tensorflow +``` + +### JavaScript/TypeScript + +Via Bun runtime: + +```javascript +// Run with Bun +const response = await fetch('https://api.example.com/data'); +const data = await response.json(); + +console.log(data); +``` + +**Built-in capabilities**: +- Fetch API +- WebSocket +- Streams +- File system operations +- Subprocess management + +**Install packages**: +```bash +npm install express +``` + +### Shell scripts + +Full Bash shell available: + +```bash +#!/bin/bash + +# Standard bash scripting +for file in *.txt; do + echo "Processing $file" + sed 's/old/new/g' "$file" > "${file}.new" +done +``` + +**Available commands**: +- Text processing: sed, awk, grep, cut, tr +- File operations: cp, mv, rm, mkdir, chmod +- Networking: curl, wget, nc +- Compression: tar, gzip, zip +- Version control: git + +## Filesystem structure + +### Standard directories + +- **/workspace** - Default working directory, use for application files +- **/tmp** - Temporary files (may be cleared periodically) +- **/home** - User home directory +- **/usr/bin** - System binaries +- **/usr/local** - Locally installed software + +### Writeable locations + +Code can write to: + +- `/workspace` - Recommended for all application data +- `/tmp` - Temporary files +- `/home` - User configuration + +Cannot write to: + +- `/usr`, `/bin`, `/lib` - System directories (read-only) +- `/proc`, `/sys` - Virtual filesystems + +### Disk space + +Sandboxes have limited disk space: + +- **Default**: Implementation-dependent (Cloudflare manages) +- **Best practice**: Clean up temporary files regularly +- **Monitoring**: Check with `df -h` + +If you hit disk limits, clean up: + +```bash +# Remove temporary files +rm -rf /tmp/* + +# Remove package caches +rm -rf ~/.cache/pip +rm -rf node_modules +``` + +## Process management + +### Process isolation + +Each sandbox has its own process namespace: + +```bash +# ps shows only processes in this sandbox +ps aux +# Won't see processes from other sandboxes or host +``` + +### Resource limits + +Processes are constrained by: + +- **CPU**: Limited CPU time (enforced by container runtime) +- **Memory**: Limited RAM per sandbox +- **Processes**: Limited number of concurrent processes +- **Files**: Limited number of open file descriptors + +These limits prevent one sandbox from affecting others. + +### Process lifecycle + +**Foreground processes** (via `exec()`): +- Run until completion +- Block the calling code +- Return complete output + +**Background processes** (via `startProcess()`): +- Run indefinitely +- Don't block +- Require explicit cleanup + +## Network capabilities + +### Outbound connections + +Sandboxes can make outbound connections: + +```bash +# HTTP requests work +curl https://api.example.com/data + +# WebSocket connections work +wscat -c wss://echo.websocket.org + +# Raw TCP works +nc example.com 80 +``` + +Use cases: +- API calls +- Database connections +- External service integration + +### Inbound connections + +Containers cannot receive inbound connections directly. Instead, use port exposure: + +```typescript +// Start a web server +await sandbox.startProcess('python -m http.server 8000'); + +// Expose port to get public URL +const exposed = await sandbox.exposePort(8000); +console.log(exposed.exposedAt); // Public URL +``` + +The preview URL proxies requests to the container. + +### Localhost + +Services can bind to localhost and communicate within the sandbox: + +```bash +# Start a database +redis-server & + +# Connect to it locally +redis-cli ping +``` + +This enables multi-process applications within one sandbox. + +## Security boundaries + +### What's isolated + +Between sandboxes: + +- **Filesystem** - Each sandbox has its own root filesystem +- **Processes** - Cannot see processes in other sandboxes +- **Network** - Separate network namespace +- **Memory** - Cannot access memory of other sandboxes + +### What's shared + +Within the sandbox, processes share: + +- **Filesystem** - All processes see the same files +- **Environment** - Same environment variables +- **Network** - Can communicate via localhost + +This means code within one sandbox is NOT isolated from other code in the same sandbox. + +### Untrusted code + +To run untrusted code: + +**Option 1: Separate sandboxes** +```typescript +// Each user gets their own sandbox +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); +``` + +**Option 2: Sessions** (advanced) +```typescript +// Isolated execution contexts within one sandbox +const session = await sandbox.createSession(); +``` + +## Performance characteristics + +### Startup time + +- **Cold start**: 100-300ms (first request to new sandbox) +- **Warm start**: \<10ms (subsequent requests) + +Cold starts include: +- Container provisioning +- Image pulling (if needed) +- Runtime initialization + +### Execution speed + +Command execution speed depends on: + +- **I/O-bound operations**: Limited by disk/network (most operations) +- **CPU-bound operations**: Limited by CPU allocation +- **Memory access**: Fast, in-memory operations are quick + +Typical performance: + +```bash +# File operations: Very fast +time dd if=/dev/zero of=/tmp/test bs=1M count=100 +# <100ms for 100MB + +# Command execution: Fast +time echo "Hello" +# <1ms + +# Python startup: Moderate +time python -c "print('hello')" +# ~50-100ms + +# Package installation: Slow (network-bound) +time pip install requests +# ~2-5 seconds +``` + +### Optimization strategies + +**1. Cache installations**: +```bash +# Don't reinstall every time +if [ ! -d "node_modules" ]; then + npm install +fi +``` + +**2. Use warm sandboxes**: +```typescript +// Reuse sandboxes instead of creating new ones +const sandbox = getSandbox(env.Sandbox, userId); +``` + +**3. Batch operations**: +```bash +# One command instead of multiple +npm install && npm test && npm run build +``` + +**4. Stream large outputs**: +```typescript +// Don't wait for complete output +const stream = await sandbox.execStream('npm test'); +``` + +## Comparison to other runtimes + +### vs. Node.js containers + +**Sandbox SDK (Bun)**: +- ✅ Faster startup (2-3x) +- ✅ Built-in TypeScript +- ✅ Better subprocess management +- ⚠️ Newer runtime (less mature) + +**Node.js**: +- ✅ More mature ecosystem +- ✅ Wider compatibility +- ❌ Slower startup +- ❌ No built-in TypeScript + +### vs. Python-only environments + +**Sandbox SDK**: +- ✅ Multiple languages (Python + JS + Shell) +- ✅ Full Linux environment +- ✅ Can install system packages +- ⚠️ More complex + +**Python-only**: +- ✅ Simpler +- ✅ Faster for pure Python +- ❌ Limited to Python +- ❌ No system-level operations + +### vs. WebAssembly + +**Sandbox SDK**: +- ✅ Full Linux environment +- ✅ Any language/tool +- ✅ System operations +- ❌ Slower startup than Wasm + +**WebAssembly**: +- ✅ Very fast startup +- ✅ Strong isolation +- ❌ Limited language support +- ❌ No system operations + +## Limitations + +### What you can't do + +Containers have some restrictions: + +**No privileged operations**: +- Can't load kernel modules +- Can't access host hardware directly +- Can't modify firewall rules +- Can't run Docker inside (no nested containers) + +**No GUI**: +- No X11 or display server +- Can't run GUI applications +- Terminal/headless only + +**No persistent hardware identity**: +- MAC addresses may change +- Hardware IDs are virtualized + +**Resource limits**: +- CPU and memory are capped +- Disk space is limited +- Network bandwidth is limited + +### Workarounds + +For GUI apps: +- Use headless rendering (e.g., Matplotlib's Agg backend) +- Generate images/PDFs instead of interactive displays + +For privileged operations: +- Design applications to avoid them +- Use Cloudflare services instead (e.g., R2 instead of local disk) + +For persistent identity: +- Use sandbox ID for identity +- Store state in filesystem or external services + +## Related resources + +- [Architecture](/sandbox/concepts/architecture/) - How containers fit in the system +- [Security model](/sandbox/concepts/security/) - Container isolation details +- [Execute commands guide](/sandbox/guides/execute-commands/) - Running code in containers +- [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..1c4d4b8b0d6f2b8 --- /dev/null +++ b/src/content/docs/sandbox/concepts/preview-urls.mdx @@ -0,0 +1,519 @@ +--- +title: Preview URLs +pcx_content_type: concept +sidebar: + order: 5 +--- + +This page explains how preview URLs work, how traffic is routed, and the design decisions behind them. + +## What are preview URLs? + +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 +``` + +Anyone with this URL can access your service running on port 8000 inside the sandbox. + +## How preview URLs work + +### URL format + +Preview URLs follow a predictable pattern: + +``` +https://{sandbox-id}-{port}.sandbox.workers.dev +``` + +Components: + +- **sandbox-id**: Unique identifier for the sandbox +- **port**: The exposed port number +- **sandbox.workers.dev**: Cloudflare-managed domain + +Examples: + +- Port 3000: `https://abc123-3000.sandbox.workers.dev` +- Port 8080: `https://abc123-8080.sandbox.workers.dev` +- Port 5173: `https://abc123-5173.sandbox.workers.dev` + +### URL stability + +URLs remain stable: + +- **Same port** - Always gets the same URL +- **Across requests** - URL doesn't change +- **After restart** - Same sandbox ID = same URL + +This means you can: +- Share URLs with others +- Bookmark preview URLs +- Use URLs in webhooks +- Embed URLs in documentation + +## Request routing + +Understanding how requests flow helps with debugging and optimization. + +### The complete path + +``` +User's Browser + ↓ HTTPS +Cloudflare Edge (nearest location) + ↓ Internal routing +Durable Object (sandbox location) + ↓ Container protocol +Container (sandbox) + ↓ HTTP (localhost) +Your Service (on exposed port) +``` + +### Step by step + +1. **User makes request** + ``` + GET https://abc123-3000.sandbox.workers.dev/api/data + ``` + +2. **Cloudflare Edge receives it** + - Request hits nearest Cloudflare data center + - TLS termination happens here + - Request is authenticated and validated + +3. **Routed to Durable Object** + - Sandbox ID extracted from hostname + - Request routed to the Durable Object managing that sandbox + - May cross regions if sandbox is in different location + +4. **Durable Object forwards to container** + - Port number extracted from hostname (3000) + - Request forwarded to container + - Container receives HTTP request + +5. **Container proxies to service** + - Container makes localhost request to port 3000 + - Your service running on port 3000 handles it + - Response flows back through the chain + +### Latency components + +Total latency = Edge latency + Routing latency + Container latency + Service latency + +**Edge latency**: 1-5ms +- User to nearest Cloudflare data center + +**Routing latency**: 10-100ms +- Edge to Durable Object (may cross regions) + +**Container latency**: 1-10ms +- Durable Object to container (local) + +**Service latency**: Varies +- Your application's response time + +**Typical total**: 20-150ms for simple requests + +## Why this design? + +### Global accessibility + +Preview URLs are globally accessible: + +- **No VPN needed** - Access from anywhere +- **HTTPS by default** - Secure by default +- **Cloudflare CDN** - Fast from anywhere in the world + +This enables: +- Sharing demos with clients +- Testing from mobile devices +- Accessing dev environments remotely +- Webhook integration + +### Security + +The URL provides access control: + +- **Unpredictable URLs** - Sandbox IDs are random (hard to guess) +- **HTTPS enforced** - All traffic encrypted +- **No default auth** - Your service controls authentication + +This means: + +**URLs are not secret**: +```typescript +// Anyone with the URL can access +const url = 'https://abc123-3000.sandbox.workers.dev'; +// No password needed - URL grants access +``` + +**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'} +``` + +### Simplicity + +One exposure model for all protocols: + +**HTTP/HTTPS**: Works directly +```typescript +await sandbox.exposePort(3000); +// Access via https://abc123-3000.sandbox.workers.dev +``` + +**WebSocket**: Works through HTTP upgrade +```typescript +await sandbox.exposePort(8080); +// Connect via wss://abc123-8080.sandbox.workers.dev +``` + +**Server-Sent Events**: Works through HTTP +```typescript +await sandbox.exposePort(5000); +// Stream from https://abc123-5000.sandbox.workers.dev/events +``` + +**Custom protocols**: Require HTTP wrapping +```bash +# Can't expose raw TCP/UDP directly +# Must wrap in HTTP service first +``` + +## Port exposure lifecycle + +### Exposure + +```typescript +const exposed = await sandbox.exposePort(8000, { + name: 'my-service' // Optional: For organization +}); + +console.log('Port:', exposed.port); // 8000 +console.log('URL:', exposed.exposedAt); // https://... +console.log('Name:', exposed.name); // 'my-service' +console.log('Time:', exposed.timestamp); // ISO timestamp +``` + +**What happens**: +1. SDK validates port number (1-65535) +2. Checks if port is already exposed +3. Verifies service is listening on port +4. Creates routing entry +5. Returns preview URL + +**Cost**: Minimal - just metadata + +### Active + +While exposed: +- Traffic flows to your service +- URL remains accessible +- No time limit (stays open) +- No request limit + +**Charges** (if any): +- Based on data transfer +- Based on request count +- No idle charges + +### Closure + +```typescript +await sandbox.unexposePort(8000); +``` + +**What happens**: +1. Routing entry removed +2. URL becomes inaccessible +3. Any in-flight requests complete +4. New requests get 404 + +**Effect**: +- Service still runs (process not killed) +- Just not accessible externally +- Can re-expose same port later + +## Multiple ports + +You can expose multiple ports simultaneously: + +```typescript +// Start multiple services +await sandbox.startProcess('node api.js'); // Port 3000 +await sandbox.startProcess('node admin.js'); // Port 3001 +await sandbox.startProcess('python metrics.py'); // Port 8080 + +// Expose all ports +const api = await sandbox.exposePort(3000, { name: 'api' }); +const admin = await sandbox.exposePort(3001, { name: 'admin' }); +const metrics = await sandbox.exposePort(8080, { name: 'metrics' }); + +return { + api: api.exposedAt, + admin: admin.exposedAt, + metrics: metrics.exposedAt +}; +``` + +Each gets its own unique URL: +- API: `https://abc123-3000.sandbox.workers.dev` +- Admin: `https://abc123-3001.sandbox.workers.dev` +- Metrics: `https://abc123-8080.sandbox.workers.dev` + +## Limitations and constraints + +### What works + +- **HTTP/HTTPS** - Full support +- **WebSocket (WSS)** - Works via HTTP upgrade +- **Server-Sent Events** - Works via HTTP +- **Any HTTP method** - GET, POST, PUT, DELETE, etc. +- **Request headers** - Forwarded to service +- **Response headers** - Forwarded to client +- **Large payloads** - No size limits (within reason) + +### What doesn't work + +- **Raw TCP** - Must use HTTP +- **UDP** - Not supported +- **Custom protocols** - Must wrap in HTTP +- **Port 80/443** - Use higher ports (1024+) + +### Rate limits + +Preview URLs may have limits: + +- **Requests per second** - Implementation-dependent +- **Concurrent connections** - Implementation-dependent +- **Bandwidth** - Implementation-dependent + +For production use, consider: +- Adding caching (Cloudflare Cache API) +- Rate limiting in your service +- Monitoring usage + +## Design trade-offs + +### Why subdomains? + +**Choice**: Use subdomains for routing (`abc123-3000.sandbox.workers.dev`) + +**Alternative**: Use paths (`sandbox.workers.dev/abc123/3000/`) + +**Why subdomains**: +- ✅ Proper CORS behavior +- ✅ Cleaner URLs +- ✅ Standard HTTP semantics +- ✅ Works with all frameworks + +**Trade-off**: Slightly longer URLs, but much better compatibility + +### Why include port in URL? + +**Choice**: Port number in hostname (`-3000.`) + +**Alternative**: Use default ports, document which service + +**Why in URL**: +- ✅ Multiple services per sandbox +- ✅ Clear which port you're accessing +- ✅ No ambiguity +- ✅ Easy to expose/unexpose individually + +**Trade-off**: Longer URLs, but explicit and clear + +### Why not custom domains? + +**Current**: Fixed `.sandbox.workers.dev` domain + +**Alternative**: Allow custom domains + +**Why fixed domain**: +- ✅ Simpler implementation +- ✅ No DNS configuration needed +- ✅ Immediate availability +- ✅ Cloudflare-managed SSL + +**Future**: Custom domains may be possible (not currently available) + +## Security considerations + +### Public accessibility + +:::caution +Preview URLs are publicly accessible. Anyone with the URL can access your service. +::: + +**Implications**: +- Don't put sensitive data in URLs +- Don't rely on URL secrecy for security +- Add authentication in your service +- Monitor access logs + +**Authentication patterns**: + +```typescript +// Token-based auth +app.use((req, res, next) => { + const token = req.headers.authorization; + if (token !== `Bearer ${process.env.AUTH_TOKEN}`) { + return res.status(401).json({ error: 'Unauthorized' }); + } + next(); +}); +``` + +```python +# Basic auth +from functools import wraps +from flask import request, abort + +def require_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if not auth or auth.password != 'secret': + abort(401) + return f(*args, **kwargs) + return decorated + +@app.route('/data') +@require_auth +def get_data(): + return {'data': 'protected'} +``` + +### HTTPS enforcement + +All traffic is HTTPS: + +- **HTTP redirects to HTTPS** - Automatically +- **TLS 1.2+** - Modern encryption +- **Cloudflare certificates** - Managed automatically + +No need to handle SSL in your service. + +### Rate limiting + +Consider adding rate limiting: + +```typescript +import { RateLimiter } from 'rate-limiter-flexible'; + +const limiter = new RateLimiter({ + points: 100, // Requests + duration: 60 // Per 60 seconds +}); + +app.use(async (req, res, next) => { + try { + await limiter.consume(req.ip); + next(); + } catch { + res.status(429).json({ error: 'Too many requests' }); + } +}); +``` + +## Debugging + +### URL not accessible + +**Problem**: Preview URL returns errors. + +**Checks**: + +1. **Is service running?** + ```typescript + const processes = await sandbox.listProcesses(); + console.log(processes); // Check if service is running + ``` + +2. **Is port exposed?** + ```typescript + const { ports } = await sandbox.getExposedPorts(); + console.log(ports); // Check if port is listed + ``` + +3. **Is service listening on correct port?** + ```bash + # In sandbox + netstat -tuln | grep 3000 # Check if port is bound + ``` + +4. **Is service binding to 0.0.0.0 (not 127.0.0.1)?** + ```python + # Good - accessible + app.run(host='0.0.0.0', port=3000) + + # Bad - localhost only + app.run(host='127.0.0.1', port=3000) + ``` + +### Slow responses + +**Problem**: Preview URLs are slow. + +**Analysis**: + +```bash +# Measure latency +curl -w "@curl-format.txt" -s https://abc123-3000.sandbox.workers.dev + +# Check service health +time curl http://localhost:3000/health # Inside sandbox +``` + +**Optimize**: +- Cache responses (HTTP headers) +- Reduce response sizes (compression) +- Optimize service code +- Use CDN features (Cloudflare Cache API) + +## Best practices + +### URL sharing + +- **Share responsibly** - URLs are public +- **Document authentication** - If required +- **Set expectations** - Temporary vs permanent +- **Monitor usage** - Track who's accessing + +### Service design + +- **Bind to 0.0.0.0** - Make accessible +- **Add health checks** - `/health` endpoint +- **Implement auth** - Don't rely on URL secrecy +- **Handle CORS** - If accessed from browsers +- **Log requests** - For debugging and monitoring + +### Cleanup + +- **Unexpose when done** - Free resources +- **Stop processes** - Don't leave servers running +- **Document URLs** - If sharing with others +- **Rotate periodically** - Create new sandboxes for fresh URLs + +## Related resources + +- [Ports API reference](/sandbox/api/ports/) - Complete port exposure API +- [Expose services guide](/sandbox/guides/expose-services/) - Practical patterns +- [Architecture](/sandbox/concepts/architecture/) - How routing works +- [Security model](/sandbox/concepts/security/) - Security implications diff --git a/src/content/docs/sandbox/concepts/sandboxes.mdx b/src/content/docs/sandbox/concepts/sandboxes.mdx new file mode 100644 index 000000000000000..0a72478a87d1684 --- /dev/null +++ b/src/content/docs/sandbox/concepts/sandboxes.mdx @@ -0,0 +1,458 @@ +--- +title: Sandbox lifecycle +pcx_content_type: concept +sidebar: + order: 2 +--- + +This page explains how sandboxes are created, managed, and destroyed, and what that means for your applications. + +## What is a sandbox? + +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 +- Can run multiple processes simultaneously +- Exists as a Cloudflare Durable Object + +Think of a sandbox like a persistent virtual machine, but provisioned instantly at the edge. + +## Lifecycle states + +A sandbox progresses through several states during its lifetime: + +### 1. 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 +``` + +**What happens**: +1. Cloudflare provisions a new Durable Object +2. The container image is pulled (if needed) +3. The container starts with a fresh Linux environment +4. The request is processed +5. The sandbox enters "active" state + +**Duration**: 100-300ms (cold start) or \<10ms (if warm) + +**Cost**: Billed per sandbox, per active time + +### 2. Active + +The sandbox is running and processing requests: + +```typescript +// Sandbox is active and responding +const result = await sandbox.exec('python script.py'); +``` + +**Characteristics**: +- Container is running +- Filesystem is accessible +- Processes can be started +- Ports can be exposed +- Fast response times (\<10ms for simple operations) + +**Persistence**: +- Files written to disk remain +- Background processes continue running +- Environment variables persist +- Code interpreter contexts stay alive + +### 3. Idle + +After a period of inactivity, the sandbox may enter idle state: + +**What happens**: +- Container may be paused or stopped +- Filesystem state is preserved +- Background processes may be suspended +- Next request triggers "warm start" + +**Duration**: Implementation-dependent (Cloudflare manages this) + +**Cost**: Reduced or no charges while idle + +### 4. Destruction + +Sandboxes are explicitly destroyed or automatically cleaned up: + +```typescript +// Explicit cleanup +await sandbox.destroy(); +// Sandbox is gone, all state deleted +``` + +**What's deleted**: +- All files in the filesystem +- All running processes +- All exposed ports +- All code interpreter contexts +- The Durable Object itself + +**Irreversible**: Once destroyed, the sandbox cannot be recovered. + +## Persistence guarantees + +### What persists + +Between requests to the same sandbox: + +**Filesystem**: +- Files in `/workspace` +- Files in `/tmp` +- Files in `/home` +- File permissions and ownership + +**Processes**: +- Background processes (started with `startProcess()`) +- Their PIDs and status +- Their accumulated logs + +**Code contexts**: +- Python/JavaScript execution contexts +- Variables and imports +- Module state + +**Configuration**: +- Environment variables +- Working directory +- Port exposures + +### What doesn't persist + +Across sandbox destruction: + +**Nothing persists** - Destruction is complete cleanup. + +**After container restart** (rare, managed by Cloudflare): +- Background processes (will stop) +- Exposed ports (will close) +- In-memory state (will be lost) + +Note: Filesystem usually survives container restarts, but this isn't guaranteed. + +## Naming strategies + +How you name sandboxes affects behavior: + +### Per-user sandboxes + +One sandbox per user: + +```typescript +const userId = 'user-123'; +const sandbox = getSandbox(env.Sandbox, userId); +``` + +**Benefits**: +- User's work persists across sessions +- Files and processes stay alive +- Personal workspace feeling + +**Use for**: +- Interactive dev environments +- Code playgrounds +- Personal notebooks + +### Per-session sandboxes + +Temporary sandboxes for each session: + +```typescript +const sessionId = `session-${Date.now()}-${Math.random()}`; +const sandbox = getSandbox(env.Sandbox, sessionId); + +// Later, clean up +await sandbox.destroy(); +``` + +**Benefits**: +- Fresh environment each time +- No cross-session contamination +- Explicit resource management + +**Use for**: +- One-time code execution +- CI/CD builds +- Isolated test runs + +### Per-task sandboxes + +One sandbox per specific task: + +```typescript +const taskId = `build-${repoName}-${commit}`; +const sandbox = getSandbox(env.Sandbox, taskId); +``` + +**Benefits**: +- Idempotent operations +- Can resume interrupted tasks +- Clear task-to-sandbox mapping + +**Use for**: +- Build systems +- Data processing pipelines +- Background jobs + +### Shared sandboxes + +Multiple users sharing one sandbox: + +```typescript +const sandbox = getSandbox(env.Sandbox, 'shared-demo'); +``` + +**Benefits**: +- Reduced costs (fewer sandboxes) +- Shared state between users +- Collaborative environments + +**Risks**: +- No isolation between users +- Potential conflicts (file overwrites) +- Security concerns + +**Use for**: +- Public demos +- Read-only environments +- Trusted teams only + +## Lifecycle management + +### When to create sandboxes + +Sandboxes are created automatically on first use, but you should consider: + +**For user environments**: +- Create on user signup +- Create on first interaction +- Reuse across sessions + +**For temporary work**: +- Create per request +- Destroy after completion +- Use unique IDs + +### When to destroy sandboxes + +Explicit destruction prevents resource leaks: + +```typescript +try { + const sandbox = getSandbox(env.Sandbox, sessionId); + await sandbox.exec('npm run build'); +} finally { + // Always clean up temporary sandboxes + await sandbox.destroy(); +} +``` + +**Destroy sandboxes when**: +- Session ends +- Task completes +- User logs out +- Resources are no longer needed + +**Don't destroy when**: +- User's personal environment (persist across sessions) +- Long-running services are needed +- Background processes are still working + +### Automatic cleanup + +Cloudflare may automatically clean up sandboxes that are: + +- Completely idle for extended periods +- Using excessive resources +- Violating terms of service + +Always design applications to recreate sandboxes if needed. + +## Request routing + +Understanding where sandboxes run helps with performance: + +### Geographic location + +The first request to a sandbox determines its location: + +```typescript +// User in San Francisco makes first request +const sandbox = getSandbox(env.Sandbox, 'user-123'); +// Sandbox created in San Francisco region +``` + +**Subsequent requests** to the same sandbox ID route to the same location, even if the user moves. + +**Why this matters**: +- First request has higher latency (provisioning) +- Later requests are fast (already running) +- Users far from sandbox location see higher latency + +### Multi-region considerations + +For global applications: + +**Option 1: User-local sandboxes** +```typescript +const region = getRegionFromRequest(request); +const sandbox = getSandbox(env.Sandbox, `${userId}-${region}`); +``` + +**Benefits**: Low latency for all users +**Drawbacks**: Multiple sandboxes per user, higher cost + +**Option 2: Single sandbox per user** +```typescript +const sandbox = getSandbox(env.Sandbox, userId); +``` + +**Benefits**: Simpler, lower cost +**Drawbacks**: Some users see higher latency + +## Cost implications + +Sandbox lifecycle affects costs: + +### Billable states + +- **Active time** - When sandbox is processing requests or running processes +- **Idle time** - May have reduced or no charges (Cloudflare-specific) + +### Cost optimization strategies + +**1. Destroy temporary sandboxes**: +```typescript +// Bad - sandbox stays alive forever +const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); +await sandbox.exec('npm test'); + +// Good - explicit cleanup +try { + const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); + await sandbox.exec('npm test'); +} finally { + await sandbox.destroy(); +} +``` + +**2. Reuse long-lived sandboxes**: +```typescript +// Good - one sandbox per user +const sandbox = getSandbox(env.Sandbox, userId); +``` + +**3. Stop unused processes**: +```typescript +// Stop processes when done +await sandbox.killAllProcesses(); +``` + +**4. Batch operations**: +```typescript +// Good - multiple commands in one request +await sandbox.exec('npm install && npm test && npm run build'); + +// Less efficient - three separate requests +await sandbox.exec('npm install'); +await sandbox.exec('npm test'); +await sandbox.exec('npm run build'); +``` + +## Failure modes + +Understanding how sandboxes fail helps with resilience: + +### Container crashes + +If the container crashes (rare): + +**Symptoms**: +- Commands fail with connection errors +- Background processes disappear +- Filesystem may be lost + +**Recovery**: +```typescript +try { + await sandbox.exec('command'); +} catch (error) { + if (error.message.includes('container') || error.message.includes('connection')) { + // Retry - container will be recreated + await sandbox.exec('command'); + } +} +``` + +### Durable Object eviction + +If the Durable Object is evicted (very rare): + +**Symptoms**: +- Similar to container crash +- State is lost +- Next request creates new instance + +**Recovery**: +- Application should handle gracefully +- Recreate necessary state +- Consider persisting critical data elsewhere (R2, D1) + +### Out of resources + +If sandbox hits resource limits: + +**Symptoms**: +- Commands fail with memory/disk errors +- Slow performance +- Timeouts + +**Recovery**: +```typescript +// Clean up to free resources +await sandbox.killAllProcesses(); +await sandbox.exec('rm -rf /tmp/*'); + +// Or start fresh +await sandbox.destroy(); +const newSandbox = getSandbox(env.Sandbox, `${oldId}-v2`); +``` + +## Best practices + +### Lifecycle management + +- **Name consistently** - Use clear, predictable naming schemes +- **Clean up temporary sandboxes** - Destroy when done +- **Reuse long-lived sandboxes** - Don't create unnecessarily +- **Handle failures** - Retry or recreate on errors + +### State management + +- **Design for state** - Embrace persistence, don't fight it +- **Clean up resources** - Stop processes, delete files when done +- **Persist critical data externally** - Don't rely solely on sandbox state +- **Version sandbox IDs** - Add version suffix for clean slate + +### Performance + +- **Keep sandboxes warm** - Periodic requests prevent cold starts +- **Batch operations** - Combine multiple commands +- **Colocate users** - Consider geography for global apps +- **Monitor latency** - Track cold vs warm start performance + +## Related resources + +- [Architecture](/sandbox/concepts/architecture/) - How sandboxes fit in the overall 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..827e39cc3bbb849 --- /dev/null +++ b/src/content/docs/sandbox/concepts/security.mdx @@ -0,0 +1,631 @@ +--- +title: Security model +pcx_content_type: concept +sidebar: + order: 6 +--- + +This page explains the security mechanisms in the Sandbox SDK, how isolation works, and what protections are in place. + +## Security layers + +The SDK implements multiple layers of security: + +``` +┌─────────────────────────────────────────────────┐ +│ Application Layer │ +│ (Your validation and authentication) │ +└────────────────────┬────────────────────────────┘ + │ +┌────────────────────▼────────────────────────────┐ +│ SDK Client Layer │ +│ (Input validation, error mapping) │ +└────────────────────┬────────────────────────────┘ + │ +┌────────────────────▼────────────────────────────┐ +│ Durable Object Layer │ +│ (Authentication, rate limiting) │ +└────────────────────┬────────────────────────────┘ + │ +┌────────────────────▼────────────────────────────┐ +│ Container Runtime Layer │ +│ (Path validation, command sanitization) │ +└────────────────────┬────────────────────────────┘ + │ +┌────────────────────▼────────────────────────────┐ +│ Container Isolation │ +│ (Linux namespaces, resource limits) │ +└──────────────────────────────────────────────────┘ +``` + +Each layer provides independent security controls. + +## Container isolation + +### Process isolation + +Containers use Linux namespaces for isolation: + +**PID namespace**: +- Each sandbox has its own process tree +- Processes cannot see processes in other sandboxes +- Cannot send signals to processes in other sandboxes + +```bash +# Inside sandbox +ps aux +# Only shows processes in this sandbox +``` + +**Network namespace**: +- Isolated network stack +- Cannot sniff traffic from other sandboxes +- Cannot interfere with other sandbox's network + +**Mount namespace**: +- Isolated filesystem view +- Cannot access files in other sandboxes +- Cannot see host filesystem + +**User namespace**: +- User ID remapping +- Root in container ≠ root on host +- Limits privilege escalation + +### Resource limits + +Containers enforce resource quotas: + +**CPU limits**: +- Time-based CPU allocation +- Prevents CPU monopolization +- Fair scheduling between sandboxes + +**Memory limits**: +- Maximum RAM per sandbox +- Prevents memory exhaustion +- OOM killer activates if exceeded + +**Disk limits**: +- Maximum disk space per sandbox +- Prevents disk filling +- Quota enforcement + +**Process limits**: +- Maximum number of processes +- Prevents fork bombs +- Process count restrictions + +### Network isolation + +**Outbound connections**: +- Allowed (can call external APIs) +- Rate limited (prevents abuse) +- Monitored (unusual patterns detected) + +**Inbound connections**: +- Blocked by default +- Only through preview URLs (controlled exposure) +- No direct container access + +**Inter-sandbox communication**: +- Prohibited (sandboxes cannot talk directly) +- Must use external services +- No shared network + +## Input validation + +### Path validation + +File operations validate paths: + +**Allowed paths**: +- `/workspace` - Application files +- `/tmp` - Temporary files +- `/home` - User files + +**Blocked paths**: +- `/etc` - System configuration +- `/sys` - System interfaces +- `/proc` - Process information +- `..` - Parent directory traversal + +```typescript +// Validation example +await sandbox.writeFile('/workspace/safe.txt', 'data'); // ✅ Allowed + +await sandbox.writeFile('/etc/passwd', 'data'); // ❌ Blocked +// Error: Access denied - path outside allowed directories + +await sandbox.writeFile('../../../etc/passwd', 'data'); // ❌ Blocked +// Error: Path traversal detected +``` + +### Command validation + +Commands are validated for dangerous patterns: + +**Dangerous patterns blocked**: +- Privilege escalation attempts (`sudo`, `su`) +- Kernel module loading (`insmod`, `modprobe`) +- Host filesystem access attempts +- Network scanning tools (in some configurations) + +```bash +# These may be blocked or restricted +sudo apt install package # Blocked - no sudo +insmod kernel_module.ko # Blocked - no kernel access +``` + +**Safe patterns allowed**: +- Package installation (`apt`, `pip`, `npm`) +- File operations (`cp`, `mv`, `rm`) +- Process management (`ps`, `kill`) +- Standard utilities + +### Port validation + +Port exposure validates port numbers: + +**Allowed ports**: +- User ports: 1024-65535 +- Standard web: 3000, 8000, 8080, 5173 + +**Blocked ports**: +- System ports: 0-1023 (require privileges) +- Reserved ranges (platform-specific) + +```typescript +await sandbox.exposePort(3000); // ✅ Allowed + +await sandbox.exposePort(80); // ❌ May be blocked +// Error: Port 80 is reserved +``` + +## Authentication and authorization + +### Sandbox access control + +Access to sandboxes is controlled: + +**Sandbox ID as access token**: +- Knowing the sandbox ID grants access +- IDs are unpredictable (hard to guess) +- But not cryptographically secure + +**Best practice**: Add application-level auth: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Verify user authentication + 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({ sandbox: 'authorized' }); + } +}; +``` + +### Preview URL access + +Preview URLs are public: + +**No built-in authentication**: +- URLs are publicly accessible +- Anyone with URL can access +- No password required + +**Add authentication in your service**: + +```python +from flask import Flask, request, abort + +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'} +``` + +## Code execution safety + +### Untrusted code + +When executing untrusted code: + +**Use separate sandboxes**: +```typescript +// Each user gets their own sandbox +const userSandbox = getSandbox(env.Sandbox, `user-${userId}`); + +// Execute user's code in their sandbox +await userSandbox.exec('python user_script.py'); +``` + +**Don't share sandboxes**: +```typescript +// ❌ Bad - all users share one sandbox +const sharedSandbox = getSandbox(env.Sandbox, 'shared'); +await sharedSandbox.exec(untrustedCode); // Users can affect each other + +// ✅ Good - separate sandboxes +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); +await sandbox.exec(untrustedCode); // Isolated +``` + +### Command injection + +Prevent command injection: + +**Dangerous**: +```typescript +// ❌ User input directly in command +const filename = userInput; +await sandbox.exec(`cat ${filename}`); // Vulnerable! + +// User could input: "file.txt; rm -rf /" +``` + +**Safe**: +```typescript +// ✅ Option 1: Validate input +const filename = userInput.replace(/[^a-zA-Z0-9._-]/g, ''); +await sandbox.exec(`cat ${filename}`); + +// ✅ Option 2: Use file API +await sandbox.writeFile('/tmp/input', userInput); +await sandbox.exec('cat /tmp/input'); +``` + +### Resource exhaustion + +Prevent resource abuse: + +**CPU exhaustion**: +```typescript +// ❌ Unbounded execution +await sandbox.exec('python infinite_loop.py'); // Runs forever + +// ✅ With timeout +try { + await sandbox.exec('python script.py', { timeout: 30000 }); +} catch (error) { + if (error.message.includes('timeout')) { + console.log('Script timed out'); + await sandbox.killAllProcesses(); + } +} +``` + +**Memory exhaustion**: +```typescript +// Monitor and limit +const processes = await sandbox.listProcesses(); +if (processes.length > 10) { + console.log('Too many processes, cleaning up'); + await sandbox.killAllProcesses(); +} +``` + +## Data security + +### Sensitive data + +Handle sensitive data carefully: + +**Environment variables**: +```typescript +// ✅ Good - use environment variables +await sandbox.startProcess('node app.js', { + env: { + API_KEY: env.API_KEY, // From Worker env binding + DB_PASSWORD: env.DB_PASSWORD + } +}); + +// ❌ Bad - hardcoded secrets +await sandbox.writeFile('/workspace/config.js', ` + const API_KEY = 'sk_live_abc123'; // Exposed in filesystem +`); +``` + +**Temporary data**: +```typescript +// Clean up after use +try { + await sandbox.writeFile('/tmp/sensitive.txt', secretData); + await sandbox.exec('python process.py /tmp/sensitive.txt'); +} finally { + await sandbox.deleteFile('/tmp/sensitive.txt'); +} +``` + +**Persistent data**: +```typescript +// Don't store secrets in sandbox +// Use external secret management (Cloudflare secrets) +const apiKey = env.API_KEY; // From environment, not filesystem +``` + +### Data isolation + +Data is isolated per sandbox: + +**Between sandboxes**: +- No shared filesystem +- No shared memory +- No shared processes + +**Within sandbox**: +- All processes share filesystem +- All sessions share filesystem +- No isolation within sandbox + +For complete isolation: +```typescript +// ✅ Each user in separate sandbox +const sandbox1 = getSandbox(env.Sandbox, 'user-1'); +const sandbox2 = getSandbox(env.Sandbox, 'user-2'); + +// ❌ Users sharing one sandbox +const shared = getSandbox(env.Sandbox, 'shared'); +// user-1 can read user-2's files +``` + +## Network security + +### Outbound requests + +Sandboxes can make outbound requests: + +**Consider**: +- Rate limiting (prevent abuse) +- Allowlisting (only allow specific domains) +- Monitoring (detect unusual patterns) + +**Example allowlist**: +```typescript +const ALLOWED_HOSTS = [ + 'api.openai.com', + 'api.anthropic.com', + 'api.github.com' +]; + +// Validate before execution +const url = new URL(userProvidedUrl); +if (!ALLOWED_HOSTS.includes(url.hostname)) { + throw new Error('Domain not allowed'); +} +``` + +### Inbound access + +Preview URLs expose services: + +**Public by default**: +- Anyone with URL can access +- No IP restrictions +- No geographic restrictions + +**Add protection**: +```typescript +// Rate limiting +const rateLimit = new Map(); + +app.use((req, res, next) => { + const ip = req.headers['cf-connecting-ip']; + const count = rateLimit.get(ip) || 0; + + if (count > 100) { + return res.status(429).send('Rate limit exceeded'); + } + + rateLimit.set(ip, count + 1); + next(); +}); + +// Authentication +app.use((req, res, next) => { + const token = req.headers.authorization; + if (!isValidToken(token)) { + return res.status(401).send('Unauthorized'); + } + next(); +}); +``` + +## Best practices + +### Defense in depth + +Layer multiple security controls: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Layer 1: Authentication + const userId = await authenticateUser(request); + if (!userId) { + return new Response('Unauthorized', { status: 401 }); + } + + // Layer 2: Authorization + if (!await canAccessResource(userId, resourceId)) { + return new Response('Forbidden', { status: 403 }); + } + + // Layer 3: Input validation + const input = await request.json(); + if (!validateInput(input)) { + return new Response('Invalid input', { status: 400 }); + } + + // Layer 4: Rate limiting + if (await isRateLimited(userId)) { + return new Response('Rate limited', { status: 429 }); + } + + // Layer 5: Sandbox isolation + const sandbox = getSandbox(env.Sandbox, userId); + + // Layer 6: Command sanitization + const safeInput = sanitizeInput(input); + await sandbox.exec(`python script.py "${safeInput}"`); + + return new Response('Success'); + } +}; +``` + +### Least privilege + +Grant minimal necessary access: + +```typescript +// ✅ Good - user can only access their sandbox +const userSandbox = getSandbox(env.Sandbox, userId); + +// ❌ Bad - user can access any sandbox +const anySandbox = getSandbox(env.Sandbox, request.params.sandboxId); +``` + +### Secure by default + +Design for security from the start: + +```typescript +// ✅ Default deny +const ALLOWED_COMMANDS = ['python', 'node', 'npm', 'pip']; + +function isAllowed(command: string): boolean { + const cmd = command.split(' ')[0]; + return ALLOWED_COMMANDS.includes(cmd); +} + +// ❌ Default allow +await sandbox.exec(anyCommand); // No validation +``` + +### Regular cleanup + +Clean up resources to prevent leaks: + +```typescript +try { + const sandbox = getSandbox(env.Sandbox, sessionId); + await sandbox.exec('npm test'); +} finally { + // Always clean up temporary sandboxes + await sandbox.destroy(); +} +``` + +### Monitoring + +Monitor for security issues: + +```typescript +// Log security events +console.log('Security event:', { + userId, + action: 'sandbox_access', + sandboxId, + timestamp: new Date().toISOString() +}); + +// Alert on suspicious patterns +if (requestCount > threshold) { + await sendAlert('High request rate detected', { userId }); +} +``` + +## Threat model + +### What the SDK protects against + +**Container escapes**: +- ✅ Prevented by Linux namespaces +- ✅ Container runtime isolation +- ✅ Cloudflare infrastructure security + +**Resource exhaustion**: +- ✅ CPU limits enforced +- ✅ Memory limits enforced +- ✅ Disk quotas enforced + +**Path traversal**: +- ✅ Validated at runtime +- ✅ Restricted to safe directories +- ✅ Parent traversal blocked + +**Command injection**: +- ✅ Command sanitization +- ⚠️ Application must validate inputs + +**Data leaks between sandboxes**: +- ✅ Filesystem isolation +- ✅ Process isolation +- ✅ Network isolation + +### What the SDK doesn't protect against + +**Application-level vulnerabilities**: +- ❌ SQL injection in your code +- ❌ XSS in your web service +- ❌ Authentication bypass in your app + +**Social engineering**: +- ❌ Users sharing sandbox IDs +- ❌ Phishing for preview URLs + +**Denial of service (application-level)**: +- ❌ Expensive computations +- ❌ Large file uploads +- ❌ Slow database queries + +**Data exfiltration**: +- ❌ Sending data to external services +- ❌ DNS tunneling +- ❌ Steganography + +**You must implement**: +- Authentication and authorization +- Input validation +- Rate limiting +- Monitoring and alerting +- Audit logging + +## Security checklist + +Before deploying to production: + +- [ ] **Authentication** - Users must authenticate +- [ ] **Authorization** - Users can only access their sandboxes +- [ ] **Input validation** - All user inputs validated +- [ ] **Output encoding** - Prevent XSS in preview URLs +- [ ] **Rate limiting** - Prevent abuse +- [ ] **Monitoring** - Track usage and anomalies +- [ ] **Secrets management** - No hardcoded secrets +- [ ] **HTTPS only** - Enforce secure connections +- [ ] **Regular cleanup** - Destroy temporary sandboxes +- [ ] **Error handling** - Don't leak sensitive info in errors +- [ ] **Audit logging** - Log security-relevant events +- [ ] **Incident response** - Plan for security incidents + +## Related resources + +- [Architecture](/sandbox/concepts/architecture/) - How security is layered +- [Container runtime](/sandbox/concepts/containers/) - Isolation mechanisms +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Resource management +- [Best practices](/sandbox/best-practices/) - Security recommendations diff --git a/src/content/docs/sandbox/concepts/sessions.mdx b/src/content/docs/sandbox/concepts/sessions.mdx new file mode 100644 index 000000000000000..293bc9630006700 --- /dev/null +++ b/src/content/docs/sandbox/concepts/sessions.mdx @@ -0,0 +1,507 @@ +--- +title: Session management +pcx_content_type: concept +sidebar: + order: 4 +--- + +This page explains what sessions are, when to use them, and how they differ from sandboxes. + +## What are sessions? + +Sessions provide isolated execution contexts within a sandbox. While sandboxes provide container-level isolation, sessions provide logical isolation within the same container. + +Think of it like this: + +- **Sandbox** = A computer (container) +- **Session** = A user account on that computer (execution context) + +Multiple sessions can run in one sandbox, each with: +- Isolated environment variables +- Separate working directories +- Independent shell state +- Isolated code interpreter contexts + +But sessions share: +- The same filesystem +- The same running processes +- The same exposed ports + +## Why use sessions? + +### When sessions make sense + +**Multi-tenant applications**: +```typescript +// Each user gets a session in a shared sandbox +const session = await sandbox.createSession({ + id: `user-${userId}`, + env: { + USER_ID: userId, + API_KEY: userApiKey + } +}); +``` + +**Isolated test environments**: +```typescript +// Each test run in its own session +const session = await sandbox.createSession({ + id: `test-${testName}`, + workingDirectory: `/workspace/test-${testName}` +}); +``` + +**Temporary isolated contexts**: +```typescript +// One-time code execution +const session = await sandbox.createSession(); +await session.exec('python untrusted-script.py'); +await sandbox.deleteSession(session.id); +``` + +### When separate sandboxes make more sense + +Use separate sandboxes instead of sessions when you need: + +**Complete isolation**: +```typescript +// Untrusted users - separate sandboxes +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); +``` + +**Long-running services**: +```typescript +// Each service in its own sandbox +const apiSandbox = getSandbox(env.Sandbox, 'api-server'); +const workerSandbox = getSandbox(env.Sandbox, 'background-worker'); +``` + +**Different lifecycle**: +```typescript +// Some sandboxes persist, others are temporary +const persistentSandbox = getSandbox(env.Sandbox, userId); +const tempSandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); +``` + +## How sessions work + +### Session creation + +Sessions are lightweight execution contexts: + +```typescript +const session = await sandbox.createSession({ + id: 'my-session', // Optional: Auto-generated if not provided + workingDirectory: '/workspace/project', // Optional: Defaults to /workspace + env: { + NODE_ENV: 'production', + API_KEY: 'secret' + } +}); + +console.log('Session ID:', session.id); +``` + +**What happens**: +1. Sandbox creates an execution context +2. Environment variables are set +3. Working directory is configured +4. Session object is returned + +**Cost**: Minimal - just metadata tracking + +### Session API + +Sessions provide the same API as sandboxes: + +```typescript +// Execute in session +const result = await session.exec('node server.js'); + +// File operations +await session.writeFile('/workspace/config.json', data); + +// Background processes +const process = await session.startProcess('npm run dev'); + +// Code interpreter +const context = await session.createCodeContext({ language: 'python' }); +``` + +All operations run in the session's context. + +### Session lifecycle + +Sessions exist until explicitly deleted or sandbox is destroyed: + +```typescript +// Create session +const session = await sandbox.createSession({ id: 'temp' }); + +// Use session +await session.exec('python script.py'); + +// Clean up +await sandbox.deleteSession('temp'); +// Session is gone, but sandbox and its filesystem remain +``` + +## Session isolation + +### What's isolated + +Each session has its own: + +**Environment variables**: +```typescript +const session1 = await sandbox.createSession({ + env: { API_KEY: 'key-1' } +}); + +const session2 = await sandbox.createSession({ + env: { API_KEY: 'key-2' } +}); + +// Sessions see different environment variables +``` + +**Working directory**: +```typescript +const session1 = await sandbox.createSession({ + workingDirectory: '/workspace/project1' +}); + +const session2 = await sandbox.createSession({ + workingDirectory: '/workspace/project2' +}); + +// Relative paths resolve differently in each session +``` + +**Shell state**: +```typescript +// In session1 +await session1.exec('export MY_VAR=hello'); + +// In session2 +await session2.exec('echo $MY_VAR'); +// Outputs nothing - MY_VAR not set in session2 +``` + +### What's shared + +Sessions in the same sandbox share: + +**Filesystem**: +```typescript +// Session1 writes a file +await session1.writeFile('/workspace/shared.txt', 'data'); + +// Session2 can read it +const file = await session2.readFile('/workspace/shared.txt'); +// Returns 'data' - filesystem is shared +``` + +**Processes**: +```typescript +// Session1 starts a server +await session1.startProcess('node server.js'); + +// Session2 sees it +const processes = await session2.listProcesses(); +// Includes server.js process +``` + +**Ports**: +```typescript +// Session1 exposes a port +await session1.exposePort(3000); + +// Session2 sees it +const ports = await session2.getExposedPorts(); +// Includes port 3000 +``` + +**Code interpreter contexts**: +```typescript +// Session1 creates a context +const context = await session1.createCodeContext({ + language: 'python' +}); + +// Session2 can access it using the context ID +await session2.runCode(context.id, 'print("hello")'); +``` + +## Common patterns + +### Per-user sessions + +Isolate users within a shared sandbox: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const userId = getUserId(request); + + // Shared sandbox + const sandbox = getSandbox(env.Sandbox, 'shared-environment'); + + // User-specific session + const session = await sandbox.getSession(`user-${userId}`); + + if (!session) { + // Create session on first request + await sandbox.createSession({ + id: `user-${userId}`, + workingDirectory: `/workspace/users/${userId}`, + env: { + USER_ID: userId, + USER_API_KEY: await getUserApiKey(userId) + } + }); + } + + // Use session for user-specific operations + const userSession = await sandbox.getSession(`user-${userId}`); + const result = await userSession.exec('python analyze.py'); + + return Response.json({ result }); + } +}; +``` + +**Benefits**: +- Lower cost (one sandbox for many users) +- Isolated environments per user +- Shared filesystem for common resources + +**Use when**: +- Users are trusted +- Cost optimization is important +- Shared resources are beneficial + +### Temporary execution contexts + +One-time isolated execution: + +```typescript +async function executeUntrustedCode(code: string) { + const sandbox = getSandbox(env.Sandbox, 'code-executor'); + + // Create temporary session + const session = await sandbox.createSession({ + id: `temp-${Date.now()}`, + workingDirectory: `/tmp/exec-${Date.now()}`, + env: { + EXECUTION_ID: Date.now().toString() + } + }); + + try { + // Execute code in session + await session.writeFile('script.py', code); + const result = await session.exec('python script.py'); + + return result; + } finally { + // Always clean up + await sandbox.deleteSession(session.id); + } +} +``` + +**Benefits**: +- Clean state for each execution +- Resource cleanup after use +- Isolated environment variables + +### Test isolation + +Separate test contexts: + +```typescript +async function runTests() { + const sandbox = getSandbox(env.Sandbox, 'test-runner'); + + const tests = ['auth', 'api', 'database']; + const results = []; + + for (const test of tests) { + // Each test in its own session + const session = await sandbox.createSession({ + id: `test-${test}`, + workingDirectory: `/workspace/tests/${test}`, + env: { + TEST_NAME: test, + NODE_ENV: 'test' + } + }); + + const result = await session.exec(`npm test -- ${test}`); + results.push({ test, passed: result.success }); + + // Clean up after each test + await sandbox.deleteSession(session.id); + } + + return results; +} +``` + +**Benefits**: +- Isolated test environments +- No test pollution +- Parallel test execution potential + +## Design considerations + +### When sessions are sufficient + +Sessions work well when: + +**Isolation needs are limited**: +- Environment variables need to differ +- Working directories need to differ +- Shell state needs to differ + +**Resources are shared**: +- Multiple users share compute +- Common files are accessed +- Processes are shared services + +**Cost is a priority**: +- One sandbox is cheaper than many +- Shared resources reduce overhead + +### When to use separate sandboxes + +Use separate sandboxes when: + +**Strong isolation is required**: +- Untrusted code execution +- Security-sensitive applications +- Regulatory compliance needs + +**Independent lifecycle**: +- Some environments are long-lived +- Some are temporary +- Different destruction times + +**Resource separation**: +- Each user needs dedicated compute +- Independent processes required +- No shared state desired + +## Performance implications + +### Session creation cost + +Sessions are lightweight: + +- **Creation time**: <1ms (just metadata) +- **Memory overhead**: Minimal (environment variables only) +- **No container startup**: Uses existing sandbox + +Compare to sandbox creation: + +- **Cold start**: 100-300ms (container provisioning) +- **Memory overhead**: Full Linux environment +- **Isolation overhead**: Container boundaries + +### Execution performance + +Sessions have no performance overhead: + +```typescript +// Direct sandbox execution +const result1 = await sandbox.exec('python script.py'); + +// Session execution - same speed +const result2 = await session.exec('python script.py'); +``` + +Both execute identically in the container. + +### Resource limits + +Sessions share sandbox resources: + +- **CPU**: Shared between all sessions in sandbox +- **Memory**: Shared pool +- **Disk**: Shared filesystem +- **Network**: Shared bandwidth + +If one session uses excessive resources, it affects all sessions in that sandbox. + +## Best practices + +### Session naming + +Use descriptive, unique session IDs: + +```typescript +// ✅ Good - clear and unique +await sandbox.createSession({ id: `user-${userId}` }); +await sandbox.createSession({ id: `test-${testName}-${timestamp}` }); + +// ❌ Bad - generic or conflicting +await sandbox.createSession({ id: 'session' }); +await sandbox.createSession({ id: 'temp' }); // Might conflict +``` + +### Session cleanup + +Always clean up temporary sessions: + +```typescript +// ✅ Good - explicit cleanup +try { + const session = await sandbox.createSession({ id: 'temp' }); + await session.exec('command'); +} finally { + await sandbox.deleteSession('temp'); +} + +// ❌ Bad - session lingers +const session = await sandbox.createSession(); +await session.exec('command'); +// Session never deleted +``` + +### Environment isolation + +Don't rely on sessions for security: + +```typescript +// ❌ Bad - sessions share filesystem +const userSession = await sandbox.createSession({ id: userId }); +await userSession.exec('rm -rf /workspace/*'); // Affects all users! + +// ✅ Good - use separate sandboxes for untrusted users +const userSandbox = getSandbox(env.Sandbox, userId); +await userSandbox.exec('rm -rf /workspace/*'); // Only affects this user +``` + +### Working directories + +Use session working directories for organization: + +```typescript +// ✅ Good - organized per session +await sandbox.createSession({ + id: 'user-123', + workingDirectory: '/workspace/users/123' +}); + +// Commands run in user's directory automatically +await session.exec('npm install'); // Installs in /workspace/users/123 +``` + +## Related resources + +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox vs session lifecycle +- [Architecture](/sandbox/concepts/architecture/) - How sessions fit in the system +- [Security model](/sandbox/concepts/security/) - Session isolation limits +- [Sessions API](/sandbox/api/sessions/) - Complete session management API From b8e7a1253a7cc0c720b5b3c383492a2cf390e617 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 14:42:45 +0100 Subject: [PATCH 10/22] Fix syntax errors --- src/content/docs/sandbox/concepts/architecture.mdx | 2 +- src/content/docs/sandbox/concepts/sessions.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/sandbox/concepts/architecture.mdx b/src/content/docs/sandbox/concepts/architecture.mdx index d946937826d5349..ec9624603c4c335 100644 --- a/src/content/docs/sandbox/concepts/architecture.mdx +++ b/src/content/docs/sandbox/concepts/architecture.mdx @@ -378,7 +378,7 @@ Every architecture involves trade-offs. Here's what we chose and why: ### vs. Traditional VMs **Sandbox SDK**: -- ✅ Instant provisioning (<1s) +- ✅ Instant provisioning (\<1s) - ✅ Global distribution - ✅ Pay-per-use pricing - ❌ Linux-only (no Windows) diff --git a/src/content/docs/sandbox/concepts/sessions.mdx b/src/content/docs/sandbox/concepts/sessions.mdx index 293bc9630006700..4014b3134cc544e 100644 --- a/src/content/docs/sandbox/concepts/sessions.mdx +++ b/src/content/docs/sandbox/concepts/sessions.mdx @@ -400,7 +400,7 @@ Use separate sandboxes when: Sessions are lightweight: -- **Creation time**: <1ms (just metadata) +- **Creation time**: \<1ms (just metadata) - **Memory overhead**: Minimal (environment variables only) - **No container startup**: Uses existing sandbox From 3c881fbb0d331e519f4382a59189e9e502d92a11 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 15:04:20 +0100 Subject: [PATCH 11/22] Add tutorials --- .../docs/sandbox/configuration/dockerfile.mdx | 560 +++++++++++++ .../configuration/environment-variables.mdx | 636 ++++++++++++++ .../docs/sandbox/configuration/index.mdx | 107 +++ .../docs/sandbox/configuration/wrangler.mdx | 625 ++++++++++++++ .../tutorials/analyze-data-with-ai.mdx | 704 ++++++++++++++++ .../tutorials/automated-testing-pipeline.mdx | 784 ++++++++++++++++++ .../sandbox/tutorials/code-review-bot.mdx | 683 +++++++++++++++ src/content/docs/sandbox/tutorials/index.mdx | 27 + 8 files changed, 4126 insertions(+) create mode 100644 src/content/docs/sandbox/configuration/dockerfile.mdx create mode 100644 src/content/docs/sandbox/configuration/environment-variables.mdx create mode 100644 src/content/docs/sandbox/configuration/index.mdx create mode 100644 src/content/docs/sandbox/configuration/wrangler.mdx create mode 100644 src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx create mode 100644 src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx create mode 100644 src/content/docs/sandbox/tutorials/code-review-bot.mdx diff --git a/src/content/docs/sandbox/configuration/dockerfile.mdx b/src/content/docs/sandbox/configuration/dockerfile.mdx new file mode 100644 index 000000000000000..bc4e2869e78b586 --- /dev/null +++ b/src/content/docs/sandbox/configuration/dockerfile.mdx @@ -0,0 +1,560 @@ +--- +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 ghcr.io/cloudflare/sandbox-runtime:latest +``` + +**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 + +### Basic customization + +Create a `Dockerfile` in your project root: + +```dockerfile title="Dockerfile" +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# 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/* +``` + +### Build and publish + +Build your custom image: + +```bash +# Build for your platform +docker build -t your-registry/custom-sandbox:latest . + +# Build for multiple platforms (required for Cloudflare) +docker buildx build --platform linux/amd64,linux/arm64 \ + -t your-registry/custom-sandbox:latest \ + --push . +``` + +Update `wrangler.jsonc` to use your custom image: + +```jsonc title="wrangler.jsonc" +{ + "containers": [ + { + "binding": "CONTAINER", + "image": "your-registry/custom-sandbox:latest" + } + ] +} +``` + +## Common customizations + +### Python packages + +Install additional Python libraries: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Data science stack +RUN pip install --no-cache-dir \ + scipy \ + seaborn \ + plotly \ + jupyter + +# Machine learning +RUN pip install --no-cache-dir \ + torch \ + torchvision \ + scikit-learn + +# Web scraping +RUN pip install --no-cache-dir \ + beautifulsoup4 \ + selenium \ + requests-html + +# Always pin versions in production +RUN pip install --no-cache-dir \ + fastapi==0.104.0 \ + pydantic==2.4.0 +``` + +### Node.js packages + +Install npm packages globally: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# TypeScript tooling +RUN npm install -g \ + typescript@5.2.2 \ + ts-node@10.9.1 \ + @types/node@20.8.0 + +# Build tools +RUN npm install -g \ + webpack@5.89.0 \ + vite@4.5.0 \ + esbuild@0.19.5 + +# Testing frameworks +RUN npm install -g \ + jest@29.7.0 \ + vitest@0.34.0 \ + playwright@1.39.0 +``` + +### System tools + +Install system-level packages: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Database clients +RUN apt-get update && apt-get install -y \ + postgresql-client \ + mysql-client \ + redis-tools \ + mongodb-clients + +# Development tools +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + pkg-config + +# Image processing +RUN apt-get update && apt-get install -y \ + imagemagick \ + ffmpeg \ + libvips-tools + +# Clean up to reduce image size +RUN rm -rf /var/lib/apt/lists/* +``` + +### Programming languages + +Add additional language runtimes: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Go +RUN wget https://go.dev/dl/go1.21.3.linux-amd64.tar.gz && \ + tar -C /usr/local -xzf go1.21.3.linux-amd64.tar.gz && \ + rm go1.21.3.linux-amd64.tar.gz +ENV PATH="/usr/local/go/bin:${PATH}" + +# Rust +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Ruby +RUN apt-get update && apt-get install -y ruby-full && \ + gem install bundler +``` + +### Custom scripts + +Add utility scripts to the image: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Copy custom scripts +COPY scripts/cleanup.sh /usr/local/bin/cleanup +COPY scripts/setup-project.sh /usr/local/bin/setup-project +COPY scripts/run-tests.sh /usr/local/bin/run-tests + +# Make executable +RUN chmod +x /usr/local/bin/cleanup \ + /usr/local/bin/setup-project \ + /usr/local/bin/run-tests +``` + +Then use in your sandboxes: + +```typescript +await sandbox.exec('cleanup'); +await sandbox.exec('setup-project /workspace/repo'); +await sandbox.exec('run-tests'); +``` + +### Configuration files + +Include default configuration: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Git config +COPY config/.gitconfig /root/.gitconfig + +# Python config +COPY config/pip.conf /root/.config/pip/pip.conf + +# NPM config +COPY config/.npmrc /root/.npmrc + +# Custom environment +COPY config/.bashrc /root/.bashrc +``` + +## Production best practices + +### Pin versions + +Always specify exact versions in production: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:1.2.3 # Not :latest + +RUN pip install \ + pandas==2.0.3 \ # Not pandas>=2.0 + numpy==1.25.2 \ + scikit-learn==1.3.0 + +RUN npm install -g \ + typescript@5.2.2 \ # Not typescript@^5.0 + prettier@3.0.3 +``` + +### Multi-stage builds + +Reduce image size with multi-stage builds: + +```dockerfile +# Build stage +FROM ghcr.io/cloudflare/sandbox-runtime:latest AS builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake + +# Build custom tools +COPY src/ /build/ +WORKDIR /build +RUN make build + +# Runtime stage +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Copy only built artifacts +COPY --from=builder /build/bin/* /usr/local/bin/ + +# Runtime dependencies only +RUN apt-get update && apt-get install -y \ + libssl3 \ + && rm -rf /var/lib/apt/lists/* +``` + +### Layer optimization + +Order instructions to maximize cache efficiency: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# 1. Install system packages (changes rarely) +RUN apt-get update && apt-get install -y \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# 2. Install Python packages (changes occasionally) +COPY requirements.txt /tmp/ +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +# 3. Install Node packages (changes occasionally) +COPY package.json package-lock.json /tmp/ +WORKDIR /tmp +RUN npm ci --global +WORKDIR / + +# 4. Copy custom files (changes frequently) +COPY scripts/ /usr/local/bin/ +COPY config/ /root/.config/ +``` + +### Security hardening + +Remove unnecessary tools and set proper permissions: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Install only what you need +RUN apt-get update && apt-get install -y \ + postgresql-client \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Remove potentially dangerous tools if not needed +RUN apt-get remove -y \ + gcc \ + make \ + && apt-get autoremove -y + +# Set secure defaults +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Restrict file permissions +RUN chmod 755 /usr/local/bin/* +``` + +### Health checks + +Add health check support: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +COPY scripts/healthcheck.sh /usr/local/bin/healthcheck +RUN chmod +x /usr/local/bin/healthcheck + +HEALTHCHECK --interval=30s --timeout=3s \ + CMD /usr/local/bin/healthcheck || exit 1 +``` + +## Platform-specific images + +### Machine learning image + +Optimized for ML/AI workloads: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# CUDA support (if available) +RUN pip install --no-cache-dir \ + torch==2.1.0 \ + torchvision==0.16.0 \ + tensorflow==2.14.0 + +# ML libraries +RUN pip install --no-cache-dir \ + transformers==4.35.0 \ + langchain==0.0.335 \ + scikit-learn==1.3.2 \ + xgboost==2.0.0 + +# Data processing +RUN pip install --no-cache-dir \ + pandas==2.1.3 \ + polars==0.19.12 \ + duckdb==0.9.2 +``` + +### Web development image + +Optimized for full-stack development: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Node.js LTS (additional version) +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs + +# Frontend build tools +RUN npm install -g \ + vite@5.0.0 \ + webpack@5.89.0 \ + turbopack@1.10.0 + +# Testing frameworks +RUN npm install -g \ + jest@29.7.0 \ + playwright@1.40.0 \ + cypress@13.6.0 + +# Python web frameworks +RUN pip install --no-cache-dir \ + fastapi==0.104.0 \ + django==4.2.7 \ + flask==3.0.0 +``` + +### Data science image + +Optimized for data analysis: + +```dockerfile +FROM ghcr.io/cloudflare/sandbox-runtime:latest + +# Core data science stack +RUN pip install --no-cache-dir \ + jupyter==1.0.0 \ + ipython==8.17.2 \ + pandas==2.1.3 \ + numpy==1.26.2 \ + scipy==1.11.4 + +# Visualization +RUN pip install --no-cache-dir \ + matplotlib==3.8.2 \ + seaborn==0.13.0 \ + plotly==5.18.0 \ + bokeh==3.3.1 + +# Statistics +RUN pip install --no-cache-dir \ + statsmodels==0.14.0 \ + scikit-learn==1.3.2 \ + scipy==1.11.4 +``` + +## Testing your image + +### Local testing + +Test your custom image locally before deploying: + +```bash +# Build the image +docker build -t test-sandbox:local . + +# Run interactively +docker run -it --rm test-sandbox:local bash + +# Test Python packages +docker run --rm test-sandbox:local python3 -c "import pandas; print(pandas.__version__)" + +# Test Node packages +docker run --rm test-sandbox:local npm list -g --depth=0 + +# Test custom scripts +docker run --rm test-sandbox:local which cleanup +``` + +### Automated testing + +Create a test script: + +```dockerfile title="Dockerfile.test" +FROM your-registry/custom-sandbox:latest + +# Run tests +RUN python3 -c "import pandas, numpy, sklearn" && \ + python3 --version && \ + node --version && \ + npm --version && \ + git --version + +# Verify custom scripts +RUN test -x /usr/local/bin/cleanup && \ + test -x /usr/local/bin/setup-project + +CMD ["echo", "All tests passed"] +``` + +Run tests: + +```bash +docker build -f Dockerfile.test -t test-image . +docker run --rm test-image +``` + +## Troubleshooting + +### Image too large + +**Problem**: Container image exceeds size limits + +**Solutions**: + +```dockerfile +# 1. Use multi-stage builds +FROM ghcr.io/cloudflare/sandbox-runtime:latest AS builder +# ... build steps +FROM ghcr.io/cloudflare/sandbox-runtime:latest +COPY --from=builder /build/output /usr/local/bin/ + +# 2. Clean up in the same layer +RUN apt-get update && apt-get install -y package \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# 3. Use --no-cache-dir for pip +RUN pip install --no-cache-dir package + +# 4. Remove unnecessary files +RUN rm -rf /tmp/* /var/tmp/* ~/.cache +``` + +### Package conflicts + +**Problem**: Conflicting package versions + +**Solutions**: + +```dockerfile +# Use virtual environments +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +RUN pip install package==1.0.0 + +# Or use specific Python versions +RUN apt-get install -y python3.11 +RUN python3.11 -m pip install package +``` + +### Build failures + +**Problem**: Docker build fails + +**Debug**: + +```bash +# Build with more output +docker build --progress=plain -t custom-sandbox . + +# Test specific layers +docker build --target builder -t test-layer . +docker run -it test-layer bash + +# Check disk space +docker system df +docker system prune +``` + +## Related resources + +- [Wrangler configuration](/sandbox/configuration/wrangler/) - Using custom images in wrangler.jsonc +- [Docker documentation](https://docs.docker.com/engine/reference/builder/) - Complete Dockerfile syntax +- [Container concepts](/sandbox/concepts/containers/) - Understanding the runtime environment +- [Security model](/sandbox/concepts/security/) - Container isolation details 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..27155e07e3a5887 --- /dev/null +++ b/src/content/docs/sandbox/configuration/environment-variables.mdx @@ -0,0 +1,636 @@ +--- +title: Environment variables +pcx_content_type: configuration +sidebar: + order: 3 +--- + +Pass configuration, secrets, and runtime settings to your sandboxes using environment variables. + +## How environment variables work + +Environment variables can be set at three levels: + +``` +Worker Environment (wrangler.jsonc) + ↓ +Command/Process Execution + ↓ +Sandbox Operating System +``` + +### Worker-level variables + +Defined in `wrangler.jsonc`, available in your Worker code: + +```jsonc title="wrangler.jsonc" +{ + "vars": { + "ENVIRONMENT": "production", + "LOG_LEVEL": "info" + } +} +``` + +Access in Worker: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + console.log(`Environment: ${env.ENVIRONMENT}`); + // These are NOT automatically passed to sandboxes + } +}; +``` + +### Command-level variables + +Passed when executing commands: + +```typescript +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +await sandbox.exec('node app.js', { + env: { + NODE_ENV: 'production', + API_KEY: env.API_KEY, // Pass from Worker env + PORT: '3000' + } +}); +``` + +### Process-level variables + +Passed to background processes: + +```typescript +await sandbox.startProcess('python server.py', { + env: { + FLASK_ENV: 'production', + DATABASE_URL: env.DATABASE_URL, + SECRET_KEY: env.SECRET_KEY + } +}); +``` + +## Common patterns + +### Pass Worker secrets to sandbox + +Securely pass secrets from Worker environment: + +```typescript +interface Env { + Sandbox: DurableObjectNamespace; + // Secrets (set with `wrangler secret put`) + OPENAI_API_KEY: string; + ANTHROPIC_API_KEY: string; + DATABASE_URL: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-sandbox'); + + // Pass secrets to sandbox command + const result = await sandbox.exec('python analyze.py', { + env: { + OPENAI_API_KEY: env.OPENAI_API_KEY, + ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY + } + }); + + return Response.json({ result }); + } +}; +``` + +Python script can access them: + +```python +import os +import openai + +# Environment variables are available in the sandbox +openai.api_key = os.environ['OPENAI_API_KEY'] + +# Use the API +response = openai.chat.completions.create(...) +``` + +### Default environment variables + +Create a helper to set default env vars: + +```typescript +function getDefaultEnv(env: Env): Record { + return { + // Always include these + NODE_ENV: env.ENVIRONMENT || 'production', + LOG_LEVEL: env.LOG_LEVEL || 'info', + TZ: 'UTC', + + // API keys from Worker secrets + OPENAI_API_KEY: env.OPENAI_API_KEY, + ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY, + + // Feature flags + ENABLE_CACHING: 'true', + MAX_RETRIES: '3' + }; +} + +// Use in commands +await sandbox.exec('npm start', { + env: { + ...getDefaultEnv(env), + PORT: '3000' // Command-specific overrides + } +}); +``` + +### Per-user configuration + +Pass user-specific configuration: + +```typescript +async function createUserSandbox( + env: Env, + userId: string, + userConfig: UserConfig +) { + const sandbox = getSandbox(env.Sandbox, `user-${userId}`); + + // Set user-specific environment + const userEnv = { + USER_ID: userId, + USER_TIER: userConfig.tier, + MAX_EXECUTION_TIME: userConfig.maxExecutionTime.toString(), + ALLOWED_PACKAGES: userConfig.allowedPackages.join(','), + + // Global secrets + API_KEY: env.API_KEY + }; + + return { sandbox, env: userEnv }; +} + +// Use it +const { sandbox, env: userEnv } = await createUserSandbox(env, userId, config); +await sandbox.exec('python script.py', { env: userEnv }); +``` + +### Dynamic configuration + +Build environment from request data: + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const { language, memory, timeout } = await request.json(); + + const sandbox = getSandbox(env.Sandbox, 'dynamic-sandbox'); + + // Build dynamic environment + const dynamicEnv: Record = { + LANGUAGE: language, + MEMORY_LIMIT: memory, + TIMEOUT: timeout.toString(), + }; + + // Add language-specific vars + if (language === 'python') { + dynamicEnv.PYTHONUNBUFFERED = '1'; + dynamicEnv.PYTHON_VERSION = '3.11'; + } else if (language === 'node') { + dynamicEnv.NODE_ENV = 'production'; + dynamicEnv.NODE_OPTIONS = '--max-old-space-size=512'; + } + + await sandbox.exec('run-code', { env: dynamicEnv }); + + return Response.json({ success: true }); + } +}; +``` + +## Environment variable precedence + +When the same variable is set at multiple levels: + +1. **Command-level** (highest priority) - Passed to `exec()` or `startProcess()` +2. **Session-level** - Set with `setEnvVars()` +3. **Container default** - Built into the Docker image +4. **System default** (lowest priority) - 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 +}); +``` + +Result: `NODE_ENV=production` in the command. + +## Session-level environment variables + +Set environment variables for all commands in a session: + +```typescript +// Create a session +const session = await sandbox.createSession(); + +// Set environment for the entire session +await session.setEnvVars({ + DATABASE_URL: env.DATABASE_URL, + REDIS_URL: env.REDIS_URL, + SECRET_KEY: env.SECRET_KEY +}); + +// All commands in this session have these vars +await session.exec('python migrate.py'); // Has DATABASE_URL +await session.exec('python seed.py'); // Has DATABASE_URL +await session.exec('python test.py'); // Has DATABASE_URL +``` + +## Common environment variables + +### Node.js + +```typescript +await sandbox.exec('node app.js', { + env: { + NODE_ENV: 'production', + NODE_OPTIONS: '--max-old-space-size=512', + NPM_CONFIG_LOGLEVEL: 'error', + PORT: '3000' + } +}); +``` + +### Python + +```typescript +await sandbox.exec('python app.py', { + env: { + PYTHONUNBUFFERED: '1', // Disable output buffering + PYTHONDONTWRITEBYTECODE: '1', // Don't create .pyc files + PYTHONPATH: '/workspace/src', // Add to Python path + FLASK_ENV: 'production', + DJANGO_SETTINGS_MODULE: 'settings.production' + } +}); +``` + +### Git + +```typescript +await sandbox.exec('git clone ...', { + env: { + GIT_TERMINAL_PROMPT: '0', // Disable prompts + GIT_ASKPASS: 'echo', // Non-interactive + GIT_USERNAME: 'bot', + GIT_COMMITTER_NAME: 'Bot', + GIT_COMMITTER_EMAIL: 'bot@example.com' + } +}); +``` + +### Database connections + +```typescript +await sandbox.exec('npm run migrate', { + env: { + DATABASE_URL: env.DATABASE_URL, + POSTGRES_HOST: 'localhost', + POSTGRES_PORT: '5432', + POSTGRES_USER: 'admin', + POSTGRES_PASSWORD: env.DB_PASSWORD, + POSTGRES_DB: 'production' + } +}); +``` + +## 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 +# Enter your secret value when prompted +``` + +### Validate before passing + +Validate user-provided environment variables: + +```typescript +function validateEnv(userEnv: Record): Record { + const safe: Record = {}; + + for (const [key, value] of Object.entries(userEnv)) { + // Only allow specific keys + if (!['PORT', 'TIMEOUT', 'MEMORY'].includes(key)) { + throw new Error(`Invalid environment variable: ${key}`); + } + + // Validate values + if (key === 'PORT') { + const port = parseInt(value); + if (isNaN(port) || port < 1024 || port > 65535) { + throw new Error('Invalid port number'); + } + } + + safe[key] = value; + } + + return safe; +} + +// Use it +const userEnv = await request.json(); +const safeEnv = validateEnv(userEnv); +await sandbox.exec('node app.js', { env: safeEnv }); +``` + +### Sanitize sensitive data in logs + +Don't log sensitive environment variables: + +```typescript +function sanitizeEnv(env: Record): Record { + const sanitized = { ...env }; + const sensitiveKeys = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN']; + + for (const key of Object.keys(sanitized)) { + if (sensitiveKeys.some(k => key.includes(k))) { + sanitized[key] = '***REDACTED***'; + } + } + + return sanitized; +} + +const env = { API_KEY: 'secret', PORT: '3000' }; +console.log('Environment:', sanitizeEnv(env)); +// Logs: Environment: { API_KEY: '***REDACTED***', PORT: '3000' } +``` + +### Use short-lived credentials + +Rotate credentials regularly: + +```typescript +async function getTemporaryCredentials(env: Env): Promise { + // Generate a temporary token valid for 1 hour + const token = await generateToken(env.SECRET_KEY, '1h'); + return token; +} + +// Use temporary credentials +const tempToken = await getTemporaryCredentials(env); +await sandbox.exec('python script.py', { + env: { + TEMP_TOKEN: tempToken // Expires after 1 hour + } +}); +``` + +## Debugging environment variables + +### List all environment variables + +```typescript +const result = await sandbox.exec('env'); +console.log('Environment variables:', result.stdout); +``` + +### Check specific variable + +```typescript +const result = await sandbox.exec('echo $NODE_ENV'); +console.log('NODE_ENV:', result.stdout.trim()); +``` + +### Debug with a script + +```typescript +await sandbox.writeFile('/workspace/debug-env.sh', ` +#!/bin/bash +echo "=== Environment Variables ===" +echo "NODE_ENV: $NODE_ENV" +echo "PORT: $PORT" +echo "API_KEY: ${API_KEY:0:10}..." # Show first 10 chars only +echo "PATH: $PATH" +`); + +const result = await sandbox.exec('bash /workspace/debug-env.sh', { + env: { + NODE_ENV: 'production', + PORT: '3000', + API_KEY: env.API_KEY + } +}); + +console.log(result.stdout); +``` + +## Advanced patterns + +### Environment variable templates + +Create templates for different scenarios: + +```typescript +const ENV_TEMPLATES = { + development: { + NODE_ENV: 'development', + LOG_LEVEL: 'debug', + CACHE_ENABLED: 'false' + }, + staging: { + NODE_ENV: 'staging', + LOG_LEVEL: 'info', + CACHE_ENABLED: 'true' + }, + production: { + NODE_ENV: 'production', + LOG_LEVEL: 'error', + CACHE_ENABLED: 'true', + STRICT_MODE: 'true' + } +}; + +// Use template +const envTemplate = ENV_TEMPLATES[env.ENVIRONMENT] || ENV_TEMPLATES.production; +await sandbox.exec('node app.js', { + env: { + ...envTemplate, + API_KEY: env.API_KEY // Add secrets on top + } +}); +``` + +### Conditional environment variables + +Set variables based on conditions: + +```typescript +const sandboxEnv: Record = { + BASE_URL: 'https://api.example.com' +}; + +// Add debug vars in development +if (env.ENVIRONMENT === 'development') { + sandboxEnv.DEBUG = '*'; + sandboxEnv.VERBOSE = 'true'; +} + +// Add monitoring in production +if (env.ENVIRONMENT === 'production') { + sandboxEnv.SENTRY_DSN = env.SENTRY_DSN; + sandboxEnv.DD_API_KEY = env.DD_API_KEY; +} + +// Add feature flags +if (env.FEATURE_NEW_PARSER === 'true') { + sandboxEnv.USE_NEW_PARSER = 'true'; +} + +await sandbox.exec('node app.js', { env: sandboxEnv }); +``` + +### Encrypted environment variables + +Encrypt sensitive values before passing: + +```typescript +async function encryptValue(value: string, key: string): Promise { + // Implement encryption (example using Web Crypto API) + const encoder = new TextEncoder(); + const data = encoder.encode(value); + + const cryptoKey = await crypto.subtle.importKey( + 'raw', + encoder.encode(key), + { name: 'AES-GCM' }, + false, + ['encrypt'] + ); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + cryptoKey, + data + ); + + return btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted))); +} + +// Encrypt before passing +const encryptedKey = await encryptValue(env.API_KEY, env.ENCRYPTION_KEY); + +await sandbox.exec('python app.py', { + env: { + ENCRYPTED_API_KEY: encryptedKey, + ENCRYPTION_KEY: env.ENCRYPTION_KEY + } +}); +``` + +## Troubleshooting + +### Variable not accessible + +**Problem**: Command can't access environment variable + +**Check**: + +```typescript +// 1. Verify it's being passed +console.log('Passing env:', { NODE_ENV: env.NODE_ENV }); + +await sandbox.exec('echo $NODE_ENV', { + env: { NODE_ENV: env.NODE_ENV } +}); + +// 2. Check if shell expansion is needed +await sandbox.exec('node -e "console.log(process.env.NODE_ENV)"', { + env: { NODE_ENV: 'production' } +}); +``` + +### Variable value is empty + +**Problem**: Environment variable is empty or undefined + +**Debug**: + +```typescript +// Check Worker environment +console.log('Worker env:', env.API_KEY ? 'Set' : 'Missing'); + +// Check sandbox environment +const result = await sandbox.exec('env | grep API_KEY', { + env: { API_KEY: env.API_KEY } +}); +console.log('Sandbox env:', result.stdout); +``` + +### Special characters in values + +**Problem**: Values with special characters break + +**Solution**: + +```typescript +// Escape special characters +const value = "value with 'quotes' and $dollar"; + +// Option 1: Use proper escaping +await sandbox.exec(`echo "${value.replace(/"/g, '\\"')}"`, { + env: { VALUE: value } +}); + +// Option 2: Write to file instead +await sandbox.writeFile('/tmp/config', value); +await sandbox.exec('cat /tmp/config'); +``` + +## 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..99679fd1330f684 --- /dev/null +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -0,0 +1,625 @@ +--- +title: Wrangler configuration +pcx_content_type: configuration +sidebar: + order: 1 +--- + +Configure your Sandbox SDK deployment in `wrangler.jsonc` with Durable Objects bindings, container images, and Worker settings. + +## Minimal configuration + +The minimum required configuration for using Sandbox SDK: + +```jsonc title="wrangler.jsonc" +{ + "name": "my-sandbox-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" + } + ] +} +``` + +## Required settings + +### compatibility_flags + +**Required**: `nodejs_compat` + +The Sandbox SDK requires Node.js compatibility mode: + +```jsonc +{ + "compatibility_flags": ["nodejs_compat"] +} +``` + +This enables: +- Node.js built-in modules (`fs`, `path`, `crypto`, etc.) +- `process.env` access +- Buffer and other Node.js globals + +### durable_objects.bindings + +**Required**: Sandbox Durable Object binding + +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "name": "Sandbox", // Binding name (used in your code) + "class_name": "Sandbox", // Durable Object class name + "script_name": "@cloudflare/sandbox" // Package providing the DO + } + ] + } +} +``` + +**Parameters**: + +- **name** (string, required) - The binding name you'll use in your Worker code. Conventionally `"Sandbox"`. +- **class_name** (string, required) - Must be `"Sandbox"` - the exported Durable Object class. +- **script_name** (string, required) - Must be `"@cloudflare/sandbox"` - the npm package name. + +### containers + +**Required**: Container runtime binding + +```jsonc +{ + "containers": [ + { + "binding": "CONTAINER", + "image": "ghcr.io/cloudflare/sandbox-runtime:latest" + } + ] +} +``` + +**Parameters**: + +- **binding** (string, required) - The binding name. Conventionally `"CONTAINER"`. +- **image** (string, required) - The Docker image to use. Options: + - `"ghcr.io/cloudflare/sandbox-runtime:latest"` - Latest stable runtime + - `"ghcr.io/cloudflare/sandbox-runtime:1.2.3"` - Specific version + - `"your-registry/custom-image:tag"` - Your custom image + +## Optional settings + +### 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; +} +``` + +### KV bindings + +Add KV for caching or state storage: + +```jsonc +{ + "kv_namespaces": [ + { + "binding": "CACHE", + "id": "your-kv-namespace-id" + } + ] +} +``` + +Use with sandboxes: + +```typescript +// Cache sandbox configurations +await env.CACHE.put(`sandbox:${sandboxId}`, JSON.stringify(config)); + +// Retrieve cached data +const cached = await env.CACHE.get(`sandbox:${sandboxId}`, 'json'); +``` + +### R2 bindings + +Add R2 for file storage: + +```jsonc +{ + "r2_buckets": [ + { + "binding": "STORAGE", + "bucket_name": "sandbox-artifacts" + } + ] +} +``` + +Use for storing sandbox outputs: + +```typescript +// Store generated files +const file = await sandbox.readFile('/workspace/output.pdf'); +await env.STORAGE.put(`results/${sandboxId}/output.pdf`, file); + +// Retrieve later +const stored = await env.STORAGE.get(`results/${sandboxId}/output.pdf`); +``` + +### D1 bindings + +Add D1 for persistent database storage: + +```jsonc +{ + "d1_databases": [ + { + "binding": "DB", + "database_name": "sandbox-db", + "database_id": "your-database-id" + } + ] +} +``` + +Use for tracking sandbox usage: + +```typescript +// Log sandbox creation +await env.DB.prepare( + 'INSERT INTO sandbox_logs (id, created_at, user_id) VALUES (?, ?, ?)' +).bind(sandboxId, Date.now(), userId).run(); + +// Query usage +const usage = await env.DB.prepare( + 'SELECT COUNT(*) as total FROM sandbox_logs WHERE user_id = ?' +).bind(userId).first(); +``` + +### Queues + +Add Queues for background processing: + +```jsonc +{ + "queues": { + "producers": [ + { + "binding": "SANDBOX_QUEUE", + "queue": "sandbox-tasks" + } + ], + "consumers": [ + { + "queue": "sandbox-tasks", + "max_batch_size": 10, + "max_batch_timeout": 30 + } + ] + } +} +``` + +Use for async sandbox operations: + +```typescript +// Queue a long-running task +await env.SANDBOX_QUEUE.send({ + type: 'run_tests', + repoUrl: 'https://github.com/owner/repo', + branch: 'main' +}); + +// Consumer handler +export default { + async queue(batch: MessageBatch, env: Env): Promise { + for (const message of batch.messages) { + const { type, repoUrl, branch } = message.body; + + if (type === 'run_tests') { + const sandbox = getSandbox(env.Sandbox, `test-${message.id}`); + // Run tests... + await sandbox.destroy(); + } + } + } +}; +``` + +### Workers AI + +Add Workers AI for LLM integration: + +```jsonc +{ + "ai": { + "binding": "AI" + } +} +``` + +Use with sandboxes: + +```typescript +// Generate code with Workers AI +const response = await env.AI.run('@cf/meta/llama-2-7b-chat-int8', { + messages: [ + { role: 'user', content: 'Write a Python function that sorts a list' } + ] +}); + +// Execute in sandbox +const sandbox = getSandbox(env.Sandbox, 'ai-sandbox'); +await sandbox.exec(`python3 -c "${response.response}"`); +``` + +### Limits and resources + +Configure resource limits: + +```jsonc +{ + "limits": { + "cpu_ms": 50 // Maximum CPU time per request (milliseconds) + } +} +``` + +:::note +Worker CPU limits don't apply to sandbox execution time. Sandboxes have their own resource limits managed by Cloudflare Containers. +::: + +### Triggers + +#### Cron triggers + +Run sandboxes on a schedule: + +```jsonc +{ + "triggers": { + "crons": [ + "0 0 * * *" // Run daily at midnight + ] + } +} +``` + +Handler: + +```typescript +export default { + async scheduled(event: ScheduledEvent, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'scheduled-task'); + + try { + // Run daily data processing + await sandbox.exec('python3 /workspace/daily-report.py'); + } finally { + await sandbox.destroy(); + } + } +}; +``` + +## Complete example + +A production-ready configuration with all common settings: + +```jsonc title="wrangler.jsonc" +{ + "name": "production-sandbox-worker", + "main": "src/index.ts", + "compatibility_date": "2024-09-02", + "compatibility_flags": ["nodejs_compat"], + + // Sandbox SDK bindings + "durable_objects": { + "bindings": [ + { + "name": "Sandbox", + "class_name": "Sandbox", + "script_name": "@cloudflare/sandbox" + } + ] + }, + + // Container runtime + "containers": [ + { + "binding": "CONTAINER", + "image": "ghcr.io/cloudflare/sandbox-runtime:latest" + } + ], + + // Environment variables (non-sensitive) + "vars": { + "ENVIRONMENT": "production", + "MAX_SANDBOX_LIFETIME": "3600000", + "DEFAULT_TIMEOUT": "30000" + }, + + // KV for caching + "kv_namespaces": [ + { + "binding": "CACHE", + "id": "your-kv-namespace-id" + } + ], + + // R2 for file storage + "r2_buckets": [ + { + "binding": "STORAGE", + "bucket_name": "sandbox-outputs" + } + ], + + // D1 for analytics + "d1_databases": [ + { + "binding": "DB", + "database_name": "sandbox-analytics", + "database_id": "your-database-id" + } + ], + + // Queue for background tasks + "queues": { + "producers": [ + { + "binding": "TASKS", + "queue": "sandbox-tasks" + } + ] + }, + + // Observability + "observability": { + "enabled": true, + "head_sampling_rate": 0.1 + }, + + // Scheduled tasks + "triggers": { + "crons": [ + "*/15 * * * *" // Every 15 minutes + ] + } +} +``` + +## Environment-specific configurations + +Use different configs for development and production: + +### wrangler.jsonc (development) + +```jsonc title="wrangler.jsonc" +{ + "name": "sandbox-worker-dev", + "vars": { + "ENVIRONMENT": "development" + }, + "containers": [ + { + "binding": "CONTAINER", + "image": "ghcr.io/cloudflare/sandbox-runtime:latest" + } + ] +} +``` + +### wrangler.prod.jsonc (production) + +```jsonc title="wrangler.prod.jsonc" +{ + "name": "sandbox-worker-prod", + "vars": { + "ENVIRONMENT": "production" + }, + "containers": [ + { + "binding": "CONTAINER", + "image": "ghcr.io/cloudflare/sandbox-runtime:1.2.3" // Pinned version + } + ], + "limits": { + "cpu_ms": 50 + } +} +``` + +Deploy with: + +```bash +# Development +wrangler deploy + +# Production +wrangler deploy --config wrangler.prod.jsonc +``` + +## TypeScript types + +Define your environment interface: + +```typescript title="src/index.ts" +interface Env { + // Required + Sandbox: DurableObjectNamespace; + + // Secrets + ANTHROPIC_API_KEY: string; + GITHUB_TOKEN: string; + + // Variables + ENVIRONMENT: string; + MAX_SANDBOX_LIFETIME: string; + + // Optional bindings + CACHE?: KVNamespace; + STORAGE?: R2Bucket; + DB?: D1Database; + TASKS?: Queue; + AI?: Ai; +} + +export default { + async fetch(request: Request, env: Env): Promise { + // All bindings are type-safe + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + // ... + } +}; +``` + +## Common patterns + +### Multiple container images + +Use different images for different purposes: + +```jsonc +{ + "containers": [ + { + "binding": "PYTHON_CONTAINER", + "image": "ghcr.io/your-org/python-sandbox:latest" + }, + { + "binding": "NODE_CONTAINER", + "image": "ghcr.io/your-org/node-sandbox:latest" + } + ] +} +``` + +### Feature flags + +Control features with environment variables: + +```jsonc +{ + "vars": { + "FEATURE_STREAMING": "true", + "FEATURE_GPU_SUPPORT": "false", + "MAX_CONCURRENT_SANDBOXES": "10" + } +} +``` + +```typescript +if (env.FEATURE_STREAMING === 'true') { + return handleStreamingRequest(request, env); +} +``` + +## Troubleshooting + +### Binding not found + +**Error**: `TypeError: env.Sandbox is undefined` + +**Solution**: Ensure your `wrangler.jsonc` includes the Durable Objects binding: + +```jsonc +{ + "durable_objects": { + "bindings": [ + { + "name": "Sandbox", // Must match your code + "class_name": "Sandbox", + "script_name": "@cloudflare/sandbox" + } + ] + } +} +``` + +### Container image pull fails + +**Error**: `Failed to pull container image` + +**Solution**: Verify the image exists and is accessible: + +```bash +# Test locally +docker pull ghcr.io/cloudflare/sandbox-runtime:latest + +# Use a specific version +"image": "ghcr.io/cloudflare/sandbox-runtime:1.2.3" +``` + +### nodejs_compat missing + +**Error**: Module "crypto" is not available + +**Solution**: Add the compatibility flag: + +```jsonc +{ + "compatibility_flags": ["nodejs_compat"] +} +``` + +## Related resources + +- [Wrangler documentation](/workers/wrangler/) - Complete Wrangler reference +- [Durable Objects configuration](/durable-objects/configuration/) - DO-specific settings +- [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/tutorials/analyze-data-with-ai.mdx b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx new file mode 100644 index 000000000000000..1e5ad93d4001cae --- /dev/null +++ b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx @@ -0,0 +1,704 @@ +--- +title: Analyze data with AI +pcx_content_type: tutorial +sidebar: + order: 2 +--- + +import { Render, PackageManagers, TabItem, Tabs } from "~/components"; + +Build a complete AI-powered data analysis system that uploads CSV datasets, generates Python analysis code using Claude, executes it securely in sandboxes, and returns visualizations. + +## What you'll build + +A production-ready data analysis Worker that: + +- Accepts CSV file uploads from users +- Uses Claude to generate Python analysis code based on user questions +- Executes the generated code safely in a sandbox +- Returns charts, statistics, and insights +- Handles errors gracefully with proper validation + +**Time to complete**: 25 minutes + +**What you'll learn**: +- Upload files to sandboxes +- Integrate Claude's function calling with sandboxes +- Execute AI-generated Python code safely +- Handle rich outputs (charts, tables, text) +- Download files from sandboxes +- Production error handling patterns + +## Prerequisites + + + +You'll also need: +- An [Anthropic API key](https://console.anthropic.com/settings/keys) (Claude) +- Basic understanding of async/await in TypeScript +- Familiarity with CSV data + +## 1. Create your Worker project + +Create a new Worker project: + + + + + +## 2. Install dependencies + +Install the Sandbox SDK and Anthropic SDK: + + + +## 3. Configure your Worker + +Update `wrangler.jsonc` to include the Sandbox binding and Anthropic API key: + +```jsonc title="wrangler.jsonc" +{ + "name": "analyze-data-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" + } + ], + "vars": { + "ANTHROPIC_API_KEY": "your-anthropic-api-key-here" + } +} +``` + +:::caution[Security] +For production, use [Wrangler secrets](/workers/configuration/secrets/) instead of `vars`: +```bash +wrangler secret put ANTHROPIC_API_KEY +``` +::: + +Update your TypeScript environment configuration: + +```typescript title="src/index.ts" +interface Env { + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; +} +``` + +## 4. Download example dataset + +For this tutorial, we'll use a real dataset. Download this sample movies dataset: + +1. Visit [TMDB 10K Movies Dataset](https://www.kaggle.com/datasets/muqarrishzaib/tmdb-10000-movies-dataset) +2. Download the CSV file +3. Save it as `test-dataset.csv` in your project root + +The dataset contains columns like: +- `title` - Movie name +- `release_date` - Release date (YYYY-MM-DD) +- `vote_average` - Rating (0-10) +- `vote_count` - Number of votes +- `popularity` - Popularity score +- `original_language` - Language code + +We'll use this for testing locally. + +## 5. Build the data analysis handler + +Create the main handler that orchestrates the data analysis workflow: + +```typescript title="src/index.ts" +import { getSandbox } from '@cloudflare/sandbox'; +import Anthropic from '@anthropic-ai/sdk'; + +export default { + async fetch(request: Request, env: Env): Promise { + // Only accept POST requests + if (request.method !== 'POST') { + return Response.json( + { error: 'Method not allowed. Use POST to upload CSV and question.' }, + { status: 405 } + ); + } + + try { + // Parse the multipart form data + const formData = await request.formData(); + const csvFile = formData.get('file') as File; + const question = formData.get('question') as string; + + // Validate inputs + if (!csvFile || !question) { + return Response.json( + { error: 'Missing required fields: file and question' }, + { status: 400 } + ); + } + + if (!csvFile.name.endsWith('.csv')) { + return Response.json( + { error: 'File must be a CSV file' }, + { status: 400 } + ); + } + + // Create a unique sandbox for this request + const sandboxId = `analysis-${Date.now()}-${Math.random().toString(36).substring(7)}`; + const sandbox = getSandbox(env.Sandbox, sandboxId); + + try { + // Step 1: Upload the CSV to the sandbox + const csvContent = await csvFile.arrayBuffer(); + const csvPath = '/workspace/dataset.csv'; + await sandbox.writeFile(csvPath, new Uint8Array(csvContent)); + + console.log(`Uploaded ${csvFile.name} to sandbox at ${csvPath}`); + + // Step 2: Analyze CSV structure + const structureResult = await sandbox.exec( + `python3 -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print('Rows:', len(df)); print('Columns:', ', '.join(df.columns.tolist())); print('Types:', df.dtypes.to_dict())"` + ); + + if (!structureResult.success) { + throw new Error(`Failed to analyze CSV: ${structureResult.stderr}`); + } + + console.log('CSV structure:', structureResult.stdout); + + // Step 3: Generate analysis code with Claude + const analysisCode = await generateAnalysisCode( + env.ANTHROPIC_API_KEY, + csvPath, + question, + structureResult.stdout + ); + + console.log('Generated code:', analysisCode); + + // Step 4: Execute the analysis code + const analysisResult = await sandbox.exec( + `python3 -c "${analysisCode.replace(/"/g, '\\"')}"` + ); + + if (!analysisResult.success) { + return Response.json( + { + error: 'Analysis code failed', + details: analysisResult.stderr, + code: analysisCode + }, + { status: 500 } + ); + } + + // Step 5: Check for generated charts (saved as PNG) + let chartData: string | null = null; + try { + const chartPath = '/workspace/chart.png'; + const chartBuffer = await sandbox.readFile(chartPath); + // Convert to base64 for JSON response + chartData = btoa(String.fromCharCode(...new Uint8Array(chartBuffer))); + } catch (error) { + console.log('No chart generated (this is OK)'); + } + + // Step 6: Return the results + return Response.json({ + success: true, + question, + output: analysisResult.stdout, + error: analysisResult.stderr || null, + chart: chartData ? `data:image/png;base64,${chartData}` : null, + executionTime: analysisResult.duration, + code: analysisCode + }); + + } finally { + // Always clean up the sandbox + await sandbox.destroy(); + } + + } catch (error) { + console.error('Error:', error); + return Response.json( + { + error: 'Internal server error', + message: error instanceof Error ? error.message : String(error) + }, + { status: 500 } + ); + } + } +}; +``` + +## 6. Implement Claude integration + +Add the function that generates Python analysis code using Claude's function calling: + +```typescript title="src/index.ts" +async function generateAnalysisCode( + apiKey: string, + csvPath: string, + question: string, + csvStructure: string +): Promise { + const client = new Anthropic({ apiKey }); + + const prompt = ` +I have a CSV file located at ${csvPath} with the following structure: +${csvStructure} + +The user wants to analyze this data. Their question is: +"${question}" + +Generate Python code that: +1. Reads the CSV using pandas +2. Answers the user's question with appropriate analysis +3. If visualization would help, create a chart and save it as '/workspace/chart.png' +4. Prints the key findings to stdout + +Important: +- Use pandas, numpy, and matplotlib (they're installed) +- Save any charts to '/workspace/chart.png' +- Print clear, concise findings +- Handle missing data appropriately +- Keep output focused and relevant +`; + + const response = await client.messages.create({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 2048, + messages: [ + { + role: 'user', + content: prompt + } + ], + tools: [ + { + name: 'generate_python_code', + description: 'Generate Python code for data analysis', + input_schema: { + type: 'object', + properties: { + code: { + type: 'string', + description: 'Complete Python code to analyze the data' + }, + explanation: { + type: 'string', + description: 'Brief explanation of what the code does' + } + }, + required: ['code'] + } + } + ] + }); + + // Extract the generated code from tool use + for (const block of response.content) { + if (block.type === 'tool_use' && block.name === 'generate_python_code') { + const input = block.input as { code: string; explanation?: string }; + console.log('Claude explanation:', input.explanation); + return input.code; + } + } + + // Fallback: extract code from text response + const textBlock = response.content.find(block => block.type === 'text'); + if (textBlock && textBlock.type === 'text') { + // Try to extract code from markdown code blocks + const codeMatch = textBlock.text.match(/```python\n([\s\S]*?)\n```/); + if (codeMatch) { + return codeMatch[1]; + } + return textBlock.text; + } + + throw new Error('Claude did not generate code'); +} +``` + +## 7. Test locally + +Start your development server: + +```bash +wrangler dev +``` + +In a separate terminal, test the data analysis endpoint: + +```bash +curl -X POST http://localhost:8787/ \ + -F "file=@test-dataset.csv" \ + -F "question=What is the trend in movie ratings over time?" +``` + +You should see a response like: + +```json +{ + "success": true, + "question": "What is the trend in movie ratings over time?", + "output": "Analysis complete:\n- Average rating from 1900-1980: 6.2\n- Average rating from 1980-2000: 6.5\n- Average rating from 2000-2020: 6.8\n- Overall trend: Ratings have increased over time", + "chart": "data:image/png;base64,iVBORw0KGgoAAAANS...", + "executionTime": 2341, + "code": "import pandas as pd\nimport matplotlib.pyplot as plt\n..." +} +``` + +Try different questions: + +```bash +# Find most popular languages +curl -X POST http://localhost:8787/ \ + -F "file=@test-dataset.csv" \ + -F "question=Which languages have the most movies?" + +# Analyze vote patterns +curl -X POST http://localhost:8787/ \ + -F "file=@test-dataset.csv" \ + -F "question=What's the relationship between vote count and rating?" +``` + +## 8. Add a web interface (optional) + +Create a simple HTML interface for easier testing: + +```typescript title="src/index.ts" +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Serve HTML form for GET requests + if (request.method === 'GET' && url.pathname === '/') { + return new Response(HTML_FORM, { + headers: { 'Content-Type': 'text/html' } + }); + } + + // Handle POST requests (data analysis) + if (request.method === 'POST' && url.pathname === '/analyze') { + // ... existing analysis code ... + } + + return Response.json({ error: 'Not found' }, { status: 404 }); + } +}; + +const HTML_FORM = ` + + + + AI Data Analysis + + + +

📊 AI Data Analysis

+

Upload a CSV file and ask a question about your data. Claude will generate Python code to analyze it.

+ +
+
+ + +
+
+ + +
+ +
+ +
+ + + + +`; +``` + +Update the form POST URL to `/analyze` and test at `http://localhost:8787`. + +## 9. Deploy to production + +Deploy your Worker to Cloudflare's global network: + +```bash +wrangler deploy +``` + +Your data analysis system is now live! Test it: + +```bash +curl -X POST https://analyze-data-worker.YOUR_SUBDOMAIN.workers.dev/analyze \ + -F "file=@test-dataset.csv" \ + -F "question=What trends do you see in this data?" +``` + +## Complete code + +
+View the complete Worker code + +```typescript title="src/index.ts" +import { getSandbox } from '@cloudflare/sandbox'; +import Anthropic from '@anthropic-ai/sdk'; + +interface Env { + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (request.method === 'GET' && url.pathname === '/') { + return new Response(HTML_FORM, { + headers: { 'Content-Type': 'text/html' } + }); + } + + if (request.method !== 'POST') { + return Response.json( + { error: 'Method not allowed' }, + { 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 required fields: file and question' }, + { status: 400 } + ); + } + + if (!csvFile.name.endsWith('.csv')) { + return Response.json( + { error: 'File must be a CSV file' }, + { status: 400 } + ); + } + + const sandboxId = `analysis-${Date.now()}-${Math.random().toString(36).substring(7)}`; + const sandbox = getSandbox(env.Sandbox, sandboxId); + + try { + const csvContent = await csvFile.arrayBuffer(); + const csvPath = '/workspace/dataset.csv'; + await sandbox.writeFile(csvPath, new Uint8Array(csvContent)); + + const structureResult = await sandbox.exec( + `python3 -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print('Rows:', len(df)); print('Columns:', ', '.join(df.columns.tolist()))"` + ); + + if (!structureResult.success) { + throw new Error(`Failed to analyze CSV: ${structureResult.stderr}`); + } + + const analysisCode = await generateAnalysisCode( + env.ANTHROPIC_API_KEY, + csvPath, + question, + structureResult.stdout + ); + + const analysisResult = await sandbox.exec( + `python3 -c "${analysisCode.replace(/"/g, '\\"')}"` + ); + + if (!analysisResult.success) { + return Response.json( + { + error: 'Analysis code failed', + details: analysisResult.stderr, + code: analysisCode + }, + { status: 500 } + ); + } + + let chartData: string | null = null; + try { + const chartBuffer = await sandbox.readFile('/workspace/chart.png'); + chartData = btoa(String.fromCharCode(...new Uint8Array(chartBuffer))); + } catch { + console.log('No chart generated'); + } + + return Response.json({ + success: true, + question, + output: analysisResult.stdout, + chart: chartData ? `data:image/png;base64,${chartData}` : null, + executionTime: analysisResult.duration, + code: analysisCode + }); + + } finally { + await sandbox.destroy(); + } + + } catch (error) { + console.error('Error:', error); + return Response.json( + { + error: 'Internal server error', + message: error instanceof Error ? error.message : String(error) + }, + { status: 500 } + ); + } + } +}; + +async function generateAnalysisCode( + apiKey: string, + csvPath: string, + question: string, + csvStructure: string +): Promise { + const client = new Anthropic({ apiKey }); + + const prompt = ` +I have a CSV file at ${csvPath} with structure: +${csvStructure} + +Question: "${question}" + +Generate Python code that answers this question. If visualization helps, save chart as '/workspace/chart.png'. +Use pandas, numpy, matplotlib. Print findings to stdout. +`; + + const response = await client.messages.create({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 2048, + messages: [{ role: 'user', content: prompt }], + tools: [ + { + name: 'generate_python_code', + description: 'Generate Python code for data analysis', + input_schema: { + type: 'object', + properties: { + code: { type: 'string' } + }, + 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('Claude did not generate code'); +} + +const HTML_FORM = `...`; // HTML from step 8 +``` +
+ +## What you learned + +You built a complete AI data analysis system that: + +- ✅ Uploads CSV files to sandboxes +- ✅ Uses Claude's function calling to generate analysis code +- ✅ Executes Python code safely in isolated sandboxes +- ✅ Returns text output and charts to users +- ✅ Handles errors gracefully +- ✅ Cleans up resources automatically + +## Next steps + +Enhance your data analysis system: + +- **Support multiple file formats** - Add support for Excel, JSON, Parquet +- **Persist sandboxes** - Keep user sandboxes alive for multi-step analysis +- **Stream results** - Use `execStream()` for real-time progress updates +- **Add authentication** - Protect your API with [Workers Auth](/workers/runtime-apis/web-crypto/) +- **Store results** - Save analyses to [R2](/r2/) or [D1](/d1/) + +## Related resources + +- [File operations guide](/sandbox/guides/manage-files/) - Complete file management patterns +- [Code execution guide](/sandbox/guides/code-execution/) - Advanced code interpreter usage +- [Streaming output guide](/sandbox/guides/streaming-output/) - Real-time progress updates +- [Files API reference](/sandbox/api/files/) - Complete file operations API +- [Security model](/sandbox/concepts/security/) - Understanding isolation and safety 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..3482a6c5f1c785f --- /dev/null +++ b/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx @@ -0,0 +1,784 @@ +--- +title: Create an automated testing pipeline +pcx_content_type: tutorial +sidebar: + order: 4 +--- + +import { Render, PackageManagers, TabItem, Tabs } from "~/components"; + +Build a CI/CD testing pipeline that clones repositories, installs dependencies, runs test suites, and generates detailed HTML reports with streaming output. + +## What you'll build + +A production-ready testing pipeline that: + +- Clones Git repositories on-demand +- Detects project type (Node.js, Python, Go, etc.) +- Installs dependencies automatically +- Runs test suites with streaming output +- Generates HTML test reports +- Caches dependencies for faster runs +- Handles timeouts and errors gracefully + +**Time to complete**: 25 minutes + +**What you'll learn**: +- Clone and build projects in sandboxes +- Stream test output in real-time +- Install dependencies for multiple languages +- Generate and download test reports +- Implement caching strategies +- Handle long-running processes + +## Prerequisites + + + +You'll also need: +- A GitHub repository with tests (public or with access token) +- Basic understanding of package managers (npm, pip, etc.) + +## 1. Create your Worker project + +Create a new Worker project: + + + + + +## 2. Install dependencies + +Install the Sandbox SDK: + + + +## 3. Configure your Worker + +Update `wrangler.jsonc` to include the Sandbox binding: + +```jsonc title="wrangler.jsonc" +{ + "name": "test-pipeline-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" + } + ] +} +``` + +Update your TypeScript types: + +```typescript title="src/index.ts" +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN?: string; // Optional, for private repos +} +``` + +## 4. Create the pipeline handler + +Build the main handler that orchestrates the testing workflow: + +```typescript title="src/index.ts" +import { getSandbox } from '@cloudflare/sandbox'; + +interface TestRequest { + repoUrl: string; + branch?: string; + testCommand?: string; + installCommand?: string; +} + +interface TestResult { + success: boolean; + output: string; + exitCode: number; + duration: number; + report?: string; // HTML report if available +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Health check + if (url.pathname === '/health') { + return Response.json({ status: 'ok', service: 'test-pipeline' }); + } + + // Main test endpoint + if (url.pathname === '/test' && request.method === 'POST') { + return handleTestRequest(request, env); + } + + // Streaming test endpoint + if (url.pathname === '/test/stream' && request.method === 'POST') { + return handleStreamingTest(request, env); + } + + return new Response(LANDING_PAGE, { + headers: { 'Content-Type': 'text/html' } + }); + } +}; + +async function handleTestRequest( + request: Request, + env: Env +): Promise { + try { + const body = await request.json() as TestRequest; + + // Validate input + if (!body.repoUrl) { + return Response.json( + { error: 'Missing required field: repoUrl' }, + { status: 400 } + ); + } + + // Create a unique sandbox + const sandboxId = `test-${Date.now()}-${Math.random().toString(36).substring(7)}`; + const sandbox = getSandbox(env.Sandbox, sandboxId); + + try { + console.log(`Starting test pipeline for ${body.repoUrl}`); + + // Run the complete test pipeline + const result = await runTestPipeline( + sandbox, + body.repoUrl, + body.branch || 'main', + body.testCommand, + body.installCommand, + env.GITHUB_TOKEN + ); + + return Response.json(result); + + } finally { + // Always clean up + await sandbox.destroy(); + } + + } catch (error) { + console.error('Test pipeline error:', error); + return Response.json( + { + error: 'Pipeline failed', + message: error instanceof Error ? error.message : String(error) + }, + { status: 500 } + ); + } +} +``` + +## 5. Implement the test pipeline + +Add the core pipeline logic that detects project type and runs tests: + +```typescript title="src/index.ts" +async function runTestPipeline( + sandbox: any, + repoUrl: string, + branch: string, + customTestCmd?: string, + customInstallCmd?: string, + githubToken?: string +): Promise { + const startTime = Date.now(); + + // Step 1: Clone the repository + console.log('Step 1: Cloning repository...'); + let cloneUrl = repoUrl; + + // Add GitHub token if provided (for private repos) + if (githubToken && repoUrl.includes('github.com')) { + cloneUrl = repoUrl.replace('https://', `https://${githubToken}@`); + } + + const cloneResult = await sandbox.exec( + `git clone --depth=1 --branch=${branch} ${cloneUrl} /workspace/repo`, + { timeout: 60000 } + ); + + if (!cloneResult.success) { + return { + success: false, + output: `Clone failed: ${cloneResult.stderr}`, + exitCode: cloneResult.exitCode, + duration: Date.now() - startTime + }; + } + + console.log('Repository cloned successfully'); + + // Step 2: Detect project type + console.log('Step 2: Detecting project type...'); + const projectType = await detectProjectType(sandbox); + console.log(`Detected project type: ${projectType}`); + + // Step 3: Install dependencies + console.log('Step 3: Installing dependencies...'); + const installCmd = customInstallCmd || getInstallCommand(projectType); + + if (installCmd) { + const installResult = await sandbox.exec( + `cd /workspace/repo && ${installCmd}`, + { timeout: 300000 } // 5 minutes for npm install + ); + + if (!installResult.success) { + return { + success: false, + output: `Install failed: ${installResult.stderr}`, + exitCode: installResult.exitCode, + duration: Date.now() - startTime + }; + } + + console.log('Dependencies installed'); + } + + // Step 4: Run tests + console.log('Step 4: Running tests...'); + const testCmd = customTestCmd || getTestCommand(projectType); + + const testResult = await sandbox.exec( + `cd /workspace/repo && ${testCmd}`, + { timeout: 300000 } // 5 minutes for tests + ); + + // Step 5: Try to find and read test report + let report: string | undefined; + const reportPaths = [ + '/workspace/repo/coverage/lcov-report/index.html', + '/workspace/repo/test-results.html', + '/workspace/repo/htmlcov/index.html', // Python coverage + '/workspace/repo/coverage.html' + ]; + + for (const reportPath of reportPaths) { + try { + const reportBuffer = await sandbox.readFile(reportPath); + report = new TextDecoder().decode(reportBuffer); + console.log(`Found test report at ${reportPath}`); + break; + } catch { + // Report not found, continue + } + } + + const duration = Date.now() - startTime; + + return { + success: testResult.exitCode === 0, + output: testResult.stdout + '\n' + testResult.stderr, + exitCode: testResult.exitCode, + duration, + report + }; +} + +async function detectProjectType(sandbox: any): Promise { + // Check for package.json (Node.js) + try { + await sandbox.readFile('/workspace/repo/package.json'); + return 'nodejs'; + } catch {} + + // Check for requirements.txt or setup.py (Python) + try { + await sandbox.readFile('/workspace/repo/requirements.txt'); + return 'python'; + } catch {} + + try { + await sandbox.readFile('/workspace/repo/setup.py'); + return 'python'; + } catch {} + + // Check for go.mod (Go) + try { + await sandbox.readFile('/workspace/repo/go.mod'); + return 'go'; + } catch {} + + // Check for Cargo.toml (Rust) + try { + await sandbox.readFile('/workspace/repo/Cargo.toml'); + return 'rust'; + } 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'; + case 'rust': + return 'cargo fetch'; + 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 ./...'; + case 'rust': + return 'cargo test'; + default: + return 'echo "No test command for this project type"'; + } +} +``` + +## 6. Add streaming support + +Implement real-time test output streaming using Server-Sent Events: + +```typescript title="src/index.ts" +async function handleStreamingTest( + request: Request, + env: Env +): Promise { + try { + const body = await request.json() as TestRequest; + + if (!body.repoUrl) { + return Response.json( + { error: 'Missing required field: repoUrl' }, + { status: 400 } + ); + } + + // Create SSE stream + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + + // Send SSE helper + const sendEvent = (event: string, data: any) => { + const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + writer.write(encoder.encode(message)); + }; + + // Run pipeline in background + (async () => { + const sandboxId = `test-stream-${Date.now()}`; + const sandbox = getSandbox(env.Sandbox, sandboxId); + + try { + sendEvent('status', { message: 'Cloning repository...' }); + + // Clone + let cloneUrl = body.repoUrl; + if (env.GITHUB_TOKEN && body.repoUrl.includes('github.com')) { + cloneUrl = body.repoUrl.replace('https://', `https://${env.GITHUB_TOKEN}@`); + } + + const cloneResult = await sandbox.exec( + `git clone --depth=1 ${cloneUrl} /workspace/repo`, + { + timeout: 60000, + onStdout: (chunk) => sendEvent('output', { type: 'stdout', data: chunk }), + onStderr: (chunk) => sendEvent('output', { type: 'stderr', data: chunk }) + } + ); + + if (!cloneResult.success) { + sendEvent('error', { message: 'Clone failed', details: cloneResult.stderr }); + sendEvent('done', { success: false }); + await writer.close(); + return; + } + + sendEvent('status', { message: 'Detecting project type...' }); + const projectType = await detectProjectType(sandbox); + sendEvent('status', { message: `Detected ${projectType} project` }); + + // Install dependencies + const installCmd = body.installCommand || getInstallCommand(projectType); + if (installCmd) { + sendEvent('status', { message: 'Installing dependencies...' }); + + const installResult = await sandbox.exec( + `cd /workspace/repo && ${installCmd}`, + { + timeout: 300000, + onStdout: (chunk) => sendEvent('output', { type: 'stdout', data: chunk }), + onStderr: (chunk) => sendEvent('output', { type: 'stderr', data: chunk }) + } + ); + + if (!installResult.success) { + sendEvent('error', { message: 'Install failed' }); + sendEvent('done', { success: false }); + await writer.close(); + return; + } + } + + // Run tests + sendEvent('status', { message: 'Running tests...' }); + const testCmd = body.testCommand || getTestCommand(projectType); + + const testResult = await sandbox.exec( + `cd /workspace/repo && ${testCmd}`, + { + timeout: 300000, + onStdout: (chunk) => sendEvent('output', { type: 'stdout', data: chunk }), + onStderr: (chunk) => sendEvent('output', { type: 'stderr', data: chunk }) + } + ); + + sendEvent('done', { + success: testResult.exitCode === 0, + exitCode: testResult.exitCode + }); + + } catch (error) { + sendEvent('error', { + message: error instanceof Error ? error.message : 'Unknown error' + }); + sendEvent('done', { success: false }); + } finally { + await sandbox.destroy(); + await writer.close(); + } + })(); + + return new Response(readable, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + } + }); + + } catch (error) { + return Response.json( + { error: 'Failed to start streaming test' }, + { status: 500 } + ); + } +} +``` + +## 7. Add a web interface + +Create a simple web interface for testing: + +```typescript title="src/index.ts" +const LANDING_PAGE = ` + + + + Test Pipeline + + + +

🧪 Test Pipeline

+

Run automated tests for any Git repository with streaming output.

+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
Running tests...
+ + + + + +`; +``` + +## 8. Test locally + +Start your development server: + +```bash +wrangler dev +``` + +Visit `http://localhost:8787` in your browser and test with a public repository: + +**Example repositories to test:** +- Node.js: `https://github.com/vercel/next.js` (may take a while) +- Python: `https://github.com/psf/requests` +- Simple Node: `https://github.com/sindresorhus/is-promise` + +Try both regular and streaming modes to see the difference! + +## 9. Deploy to production + +Deploy your testing pipeline: + +```bash +wrangler deploy +``` + +Test the deployed endpoint: + +```bash +curl -X POST https://test-pipeline-worker.YOUR_SUBDOMAIN.workers.dev/test \ + -H "Content-Type: application/json" \ + -d '{ + "repoUrl": "https://github.com/sindresorhus/is-promise", + "branch": "main" + }' +``` + +## Production enhancements + +### Add caching for dependencies + +Use persistent sandboxes to cache installed dependencies: + +```typescript +// Instead of creating temporary sandboxes +const sandboxId = `test-${Date.now()}`; + +// Use project-specific sandboxes +const repoSlug = repoUrl.split('/').slice(-2).join('-'); +const sandboxId = `test-cache-${repoSlug}`; + +// Don't destroy after use +// await sandbox.destroy(); // Skip this for caching +``` + +### Add timeout handling + +```typescript +const timeout = 600000; // 10 minutes +const controller = new AbortController(); +const timeoutId = setTimeout(() => controller.abort(), timeout); + +try { + await sandbox.exec(command, { signal: controller.signal }); +} catch (error) { + if (error.name === 'AbortError') { + return { error: 'Test timed out after 10 minutes' }; + } + throw error; +} finally { + clearTimeout(timeoutId); +} +``` + +### Add webhook integration + +Trigger tests on Git push events: + +```typescript +if (url.pathname === '/webhook/github' && request.method === 'POST') { + const payload = await request.json(); + + if (payload.ref === 'refs/heads/main') { + // Trigger test pipeline + await runTestPipeline( + sandbox, + payload.repository.clone_url, + 'main' + ); + } +} +``` + +## Complete code + +
+View the complete Worker code + +```typescript title="src/index.ts" +import { getSandbox } from '@cloudflare/sandbox'; + +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN?: string; +} + +// ... (include all code from steps 4-7) +``` +
+ +## What you learned + +You built a complete automated testing pipeline that: + +- ✅ Clones Git repositories into sandboxes +- ✅ Detects project types automatically +- ✅ Installs dependencies for multiple languages +- ✅ Runs test suites with proper error handling +- ✅ Streams output in real-time +- ✅ Generates and returns test reports +- ✅ Manages sandbox lifecycle efficiently + +## Next steps + +Enhance your testing pipeline: + +- **Matrix testing** - Run tests across multiple Node.js/Python versions +- **Parallel execution** - Run tests in multiple sandboxes simultaneously +- **Result storage** - Save test results to [D1](/d1/) or [R2](/r2/) +- **Notifications** - Send test results to Slack/Discord +- **GitHub integration** - Post test results as PR status checks +- **Coverage tracking** - Parse and track test coverage over time + +## Related resources + +- [Git workflows guide](/sandbox/guides/git-workflows/) - Advanced Git operations +- [Streaming output guide](/sandbox/guides/streaming-output/) - Real-time output patterns +- [Background processes guide](/sandbox/guides/background-processes/) - Long-running jobs +- [Sessions API](/sandbox/api/sessions/) - Persistent sandbox state +- [Security model](/sandbox/concepts/security/) - Understanding isolation 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..e4448cebc004871 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/code-review-bot.mdx @@ -0,0 +1,683 @@ +--- +title: Build a code review bot +pcx_content_type: tutorial +sidebar: + order: 3 +--- + +import { Render, PackageManagers, TabItem, Tabs } from "~/components"; + +Build an automated code review system that responds to GitHub pull requests, clones the repository, analyzes code changes with Claude, and posts detailed feedback as PR comments. + +## What you'll build + +A production-ready GitHub bot that: + +- Receives GitHub webhook events for new pull requests +- Clones repositories securely into sandboxes +- Analyzes code changes with Claude's code review capabilities +- Posts structured feedback as PR comments +- Handles multiple programming languages +- Manages sandbox lifecycle automatically + +**Time to complete**: 30 minutes + +**What you'll learn**: +- Handle GitHub webhook events in Workers +- Clone Git repositories with authentication +- Analyze file diffs and generate reviews +- Post comments to GitHub Pull Requests +- Manage long-running sandbox operations +- Production security patterns + +## Prerequisites + + + +You'll also need: +- A [GitHub account](https://github.com/) and personal access token +- An [Anthropic API key](https://console.anthropic.com/settings/keys) (Claude) +- A GitHub repository for testing (can be private) +- Basic understanding of Git workflows + +## 1. Create your Worker project + +Create a new Worker project: + + + + + +## 2. Install dependencies + +Install the required packages: + + + +The packages: +- `@cloudflare/sandbox` - Sandbox SDK for isolated code execution +- `@anthropic-ai/sdk` - Claude API client +- `@octokit/rest` - GitHub API client + +## 3. Configure your Worker + +Update `wrangler.jsonc` to include bindings and secrets: + +```jsonc title="wrangler.jsonc" +{ + "name": "code-review-bot", + "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" + } + ] +} +``` + +Set your secrets (never commit these to Git): + +```bash +# GitHub personal access token (needs repo permissions) +wrangler secret put GITHUB_TOKEN + +# Anthropic API key +wrangler secret put ANTHROPIC_API_KEY + +# Webhook secret (generate a random string) +wrangler secret put WEBHOOK_SECRET +``` + +Update your TypeScript types: + +```typescript title="src/index.ts" +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN: string; + ANTHROPIC_API_KEY: string; + WEBHOOK_SECRET: string; +} +``` + +## 4. Create the webhook handler + +Build the main handler that processes GitHub webhook events: + +```typescript title="src/index.ts" +import { getSandbox } from '@cloudflare/sandbox'; +import { Octokit } from '@octokit/rest'; +import Anthropic from '@anthropic-ai/sdk'; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Health check endpoint + if (url.pathname === '/health') { + return Response.json({ status: 'ok', service: 'code-review-bot' }); + } + + // Webhook endpoint + if (url.pathname === '/webhook' && request.method === 'POST') { + return handleWebhook(request, env); + } + + return Response.json( + { error: 'Not found. Use POST /webhook for GitHub events.' }, + { status: 404 } + ); + } +}; + +async function handleWebhook(request: Request, env: Env): Promise { + try { + // Verify webhook signature + const signature = request.headers.get('x-hub-signature-256'); + const body = await request.text(); + + if (!signature || !(await verifySignature(body, signature, env.WEBHOOK_SECRET))) { + return Response.json({ error: 'Invalid signature' }, { status: 401 }); + } + + // Parse the event + const event = request.headers.get('x-github-event'); + const payload = JSON.parse(body); + + console.log(`Received ${event} event from GitHub`); + + // Only handle pull request events + if (event !== 'pull_request') { + return Response.json({ message: 'Event ignored (not a PR)' }); + } + + // Only handle opened or synchronize (new commits) actions + if (payload.action !== 'opened' && payload.action !== 'synchronize') { + return Response.json({ message: `PR action '${payload.action}' ignored` }); + } + + // Start the review process (don't wait for completion) + // In production, you'd use a Queue or Durable Object for this + handlePullRequestReview(payload, env).catch(error => { + console.error('Error in review process:', error); + }); + + return Response.json({ + message: 'Review started', + pr: payload.pull_request.number + }); + + } catch (error) { + console.error('Webhook error:', error); + return Response.json( + { error: 'Webhook processing failed' }, + { status: 500 } + ); + } +} + +// Verify GitHub webhook signature +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 expectedSignature = 'sha256=' + Array.from(new Uint8Array(signatureBytes)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + return signature === expectedSignature; +} +``` + +## 5. Implement the review process + +Add the main review logic that clones, analyzes, and comments: + +```typescript title="src/index.ts" +async function handlePullRequestReview(payload: any, env: Env): Promise { + const pr = payload.pull_request; + const repo = payload.repository; + const owner = repo.owner.login; + const repoName = repo.name; + const prNumber = pr.number; + + console.log(`Reviewing PR #${prNumber} in ${owner}/${repoName}`); + + // Create GitHub client + const octokit = new Octokit({ auth: env.GITHUB_TOKEN }); + + // Post initial comment + await octokit.issues.createComment({ + owner, + repo: repoName, + issue_number: prNumber, + body: '🤖 Code review in progress... This may take 1-2 minutes.' + }); + + // Create a unique sandbox for this review + const sandboxId = `review-${owner}-${repoName}-${prNumber}`; + const sandbox = getSandbox(env.Sandbox, sandboxId); + + try { + // Step 1: Clone the repository + console.log('Cloning repository...'); + const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${owner}/${repoName}.git`; + const cloneResult = await sandbox.exec( + `git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`, + { timeout: 60000 } + ); + + if (!cloneResult.success) { + throw new Error(`Failed to clone: ${cloneResult.stderr}`); + } + + // Step 2: Get the diff for this PR + console.log('Fetching PR diff...'); + const comparison = await octokit.repos.compareCommits({ + owner, + repo: repoName, + base: pr.base.sha, + head: pr.head.sha + }); + + const changedFiles = comparison.data.files || []; + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + await octokit.issues.createComment({ + owner, + repo: repoName, + issue_number: prNumber, + body: '✅ No files to review.' + }); + return; + } + + // Step 3: Read the changed files from the sandbox + const filesContent: Array<{ path: string; content: string; patch: string }> = []; + + for (const file of changedFiles.slice(0, 10)) { // Limit to 10 files + if (file.status === 'removed') continue; + + try { + const content = await sandbox.readFile(`/workspace/repo/${file.filename}`); + const contentStr = new TextDecoder().decode(content); + filesContent.push({ + path: file.filename, + content: contentStr, + patch: file.patch || '' + }); + } catch (error) { + console.log(`Could not read ${file.filename}:`, error); + } + } + + // Step 4: Generate review with Claude + console.log('Generating review with Claude...'); + const review = await generateCodeReview( + env.ANTHROPIC_API_KEY, + pr.title, + pr.body || '', + filesContent + ); + + // Step 5: Post the review as a comment + await octokit.issues.createComment({ + owner, + repo: repoName, + issue_number: prNumber, + body: formatReviewComment(review, filesContent.length, changedFiles.length) + }); + + console.log(`Review posted for PR #${prNumber}`); + + } catch (error) { + console.error('Review error:', error); + + // Post error comment + await octokit.issues.createComment({ + owner, + repo: repoName, + issue_number: prNumber, + body: `❌ Code review failed: ${error instanceof Error ? error.message : 'Unknown error'}` + }); + + } finally { + // Always clean up the sandbox + await sandbox.destroy(); + } +} +``` + +## 6. Implement Claude code review + +Add the function that uses Claude to review code: + +```typescript title="src/index.ts" +interface CodeReview { + summary: string; + issues: Array<{ + file: string; + line?: number; + severity: 'critical' | 'warning' | 'suggestion'; + message: string; + }>; + positives: string[]; + suggestions: string[]; +} + +async function generateCodeReview( + apiKey: string, + prTitle: string, + prDescription: string, + files: Array<{ path: string; content: string; patch: string }> +): Promise { + const client = new Anthropic({ apiKey }); + + const filesContext = files.map(f => ` +File: ${f.path} +Diff: +${f.patch} + +Full content: +\`\`\` +${f.content.slice(0, 5000)} ${f.content.length > 5000 ? '... (truncated)' : ''} +\`\`\` +`).join('\n---\n'); + + const prompt = `You are an expert code reviewer. Review this pull request: + +Title: ${prTitle} +Description: ${prDescription} + +Changed files: +${filesContext} + +Provide a thorough code review focusing on: +1. Potential bugs or logic errors +2. Security vulnerabilities +3. Performance issues +4. Code style and best practices +5. Missing error handling +6. Positive aspects worth highlighting + +Format your response as structured feedback with: +- Overall summary +- Specific issues (file, line, severity, message) +- Positive observations +- General suggestions for improvement +`; + + const response = await client.messages.create({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 4096, + messages: [ + { + role: 'user', + content: prompt + } + ], + tools: [ + { + name: 'provide_code_review', + description: 'Provide structured code review feedback', + input_schema: { + type: 'object', + properties: { + summary: { + type: 'string', + description: 'Overall summary of the changes and review' + }, + issues: { + type: 'array', + description: 'Specific issues found in the code', + items: { + type: 'object', + properties: { + file: { type: 'string' }, + line: { type: 'number' }, + severity: { type: 'string', enum: ['critical', 'warning', 'suggestion'] }, + message: { type: 'string' } + }, + required: ['file', 'severity', 'message'] + } + }, + positives: { + type: 'array', + description: 'Positive aspects of the code', + items: { type: 'string' } + }, + suggestions: { + type: 'array', + description: 'General suggestions for improvement', + items: { type: 'string' } + } + }, + required: ['summary', 'issues', 'positives', 'suggestions'] + } + } + ] + }); + + // Extract review from tool use + for (const block of response.content) { + if (block.type === 'tool_use' && block.name === 'provide_code_review') { + return block.input as CodeReview; + } + } + + throw new Error('Claude did not provide structured review'); +} + +function formatReviewComment( + review: CodeReview, + reviewedFiles: number, + totalFiles: number +): string { + let comment = '## 🤖 AI Code Review\n\n'; + + comment += `**Files reviewed:** ${reviewedFiles}/${totalFiles}\n\n`; + + // Summary + comment += `### Summary\n${review.summary}\n\n`; + + // Issues + if (review.issues.length > 0) { + comment += '### Issues Found\n\n'; + + const criticalIssues = review.issues.filter(i => i.severity === 'critical'); + const warnings = review.issues.filter(i => i.severity === 'warning'); + const suggestions = review.issues.filter(i => i.severity === 'suggestion'); + + if (criticalIssues.length > 0) { + comment += '#### 🔴 Critical\n'; + for (const issue of criticalIssues) { + comment += `- **${issue.file}${issue.line ? `:${issue.line}` : ''}** - ${issue.message}\n`; + } + comment += '\n'; + } + + if (warnings.length > 0) { + comment += '#### ⚠️ Warnings\n'; + for (const issue of warnings) { + comment += `- **${issue.file}${issue.line ? `:${issue.line}` : ''}** - ${issue.message}\n`; + } + comment += '\n'; + } + + if (suggestions.length > 0) { + comment += '#### 💡 Suggestions\n'; + for (const issue of suggestions) { + comment += `- **${issue.file}${issue.line ? `:${issue.line}` : ''}** - ${issue.message}\n`; + } + comment += '\n'; + } + } else { + comment += '### ✅ No issues found\n\n'; + } + + // Positives + if (review.positives.length > 0) { + comment += '### 👍 Positive Aspects\n'; + for (const positive of review.positives) { + comment += `- ${positive}\n`; + } + comment += '\n'; + } + + // General suggestions + if (review.suggestions.length > 0) { + comment += '### 💭 General Suggestions\n'; + for (const suggestion of review.suggestions) { + comment += `- ${suggestion}\n`; + } + comment += '\n'; + } + + comment += '\n---\n'; + comment += '*This review was generated automatically using Claude and Cloudflare Sandbox SDK.*'; + + return comment; +} +``` + +## 7. Set up GitHub webhook + +Configure your repository to send webhook events to your Worker: + +1. Go to your GitHub repository settings +2. Navigate to **Settings** > **Webhooks** > **Add webhook** +3. Configure: + - **Payload URL**: `https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook` + - **Content type**: `application/json` + - **Secret**: The same value you set for `WEBHOOK_SECRET` + - **Events**: Select "Let me select individual events" → check **Pull requests** + - Click **Add webhook** + +## 8. Test locally + +Start your development server: + +```bash +wrangler dev --remote +``` + +:::note +We use `--remote` because webhooks need a public URL. Alternatively, use a tunnel service like [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](/cloudflare-one/connections/connect-networks/). +::: + +Test the webhook endpoint: + +```bash +curl -X POST http://localhost:8787/webhook \ + -H "Content-Type: application/json" \ + -H "x-github-event: pull_request" \ + -H "x-hub-signature-256: sha256=$(echo -n '{}' | openssl dgst -sha256 -hmac 'your-webhook-secret' | cut -d' ' -f2)" \ + -d '{ + "action": "opened", + "pull_request": { + "number": 1, + "title": "Test PR", + "body": "Test description", + "head": { "ref": "feature-branch", "sha": "abc123" }, + "base": { "sha": "def456" } + }, + "repository": { + "name": "test-repo", + "owner": { "login": "your-username" } + } + }' +``` + +## 9. Deploy and test with a real PR + +Deploy your bot: + +```bash +wrangler deploy +``` + +Create a test pull request in your repository: + +1. Create a new branch: + ```bash + git checkout -b test-code-review + ``` + +2. Make some changes and commit: + ```bash + echo "console.log('test');" > test.js + git add test.js + git commit -m "Add test file" + git push origin test-code-review + ``` + +3. Open a pull request on GitHub + +4. Watch for the bot's comment within 1-2 minutes! + +## Complete code + +
+View the complete Worker code + +```typescript title="src/index.ts" +import { getSandbox } from '@cloudflare/sandbox'; +import { Octokit } from '@octokit/rest'; +import Anthropic from '@anthropic-ai/sdk'; + +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN: string; + ANTHROPIC_API_KEY: string; + WEBHOOK_SECRET: string; +} + +interface CodeReview { + summary: string; + issues: Array<{ + file: string; + line?: number; + severity: 'critical' | 'warning' | 'suggestion'; + message: string; + }>; + positives: string[]; + suggestions: string[]; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === '/health') { + return Response.json({ status: 'ok' }); + } + + if (url.pathname === '/webhook' && request.method === 'POST') { + return handleWebhook(request, env); + } + + return Response.json({ error: 'Not found' }, { status: 404 }); + } +}; + +// ... (include all functions from steps 4-6) +``` +
+ +## What you learned + +You built a complete automated code review system that: + +- ✅ Receives GitHub webhook events securely +- ✅ Clones repositories into isolated sandboxes +- ✅ Analyzes code changes with Claude +- ✅ Posts structured feedback to pull requests +- ✅ Handles errors and cleanup automatically +- ✅ Works with any programming language + +## Next steps + +Enhance your code review bot: + +- **Add inline comments** - Use GitHub's review API to comment on specific lines +- **Multiple reviewers** - Get reviews from different AI models and combine them +- **Custom rules** - Add project-specific checks and linting +- **Review history** - Store reviews in [D1](/d1/) for analysis +- **Queue system** - Use [Queues](/queues/) for reliable processing +- **Metrics tracking** - Monitor review quality and performance + +## Related resources + +- [Git workflows guide](/sandbox/guides/git-workflows/) - Advanced Git operations +- [Background processes guide](/sandbox/guides/background-processes/) - Long-running operations +- [Security model](/sandbox/concepts/security/) - Understanding sandbox isolation +- [Sessions API](/sandbox/api/sessions/) - Managing sandbox lifecycle +- [GitHub Apps documentation](https://docs.github.com/en/apps) - Building GitHub integrations diff --git a/src/content/docs/sandbox/tutorials/index.mdx b/src/content/docs/sandbox/tutorials/index.mdx index dd458e2c8e799ab..8ac696585a39fa7 100644 --- a/src/content/docs/sandbox/tutorials/index.mdx +++ b/src/content/docs/sandbox/tutorials/index.mdx @@ -19,6 +19,30 @@ Learn how to build complete applications with Sandbox SDK through step-by-step t Build a production-ready AI code execution system using Claude and Sandbox SDK. Generate Python code from natural language and execute it securely. + + Upload CSV datasets, generate Python analysis code with LLMs, execute in sandboxes, and save visualizations. Complete data analysis workflow. + + + + Create an automated code review system that clones repositories, analyzes code with LLMs, and posts feedback to GitHub pull requests. + + + + Build a CI/CD pipeline that clones repositories, installs dependencies, runs tests, and generates detailed test reports. + + ## What you'll learn @@ -26,6 +50,9 @@ Learn how to build complete applications with Sandbox SDK through step-by-step t These tutorials cover complete, real-world applications: - **AI Code Execution** - Integrate LLMs with secure code execution +- **Data Analysis** - Upload datasets, generate analysis code, create visualizations +- **Code Review Automation** - Clone repositories, analyze code, post feedback +- **CI/CD Pipelines** - Automated testing and reporting workflows - **File Operations** - Work with files and directories in sandboxes - **Error Handling** - Production-ready validation and error management - **Deployment** - Deploy to Cloudflare's global network From 72d409358212f07ad8eef30c11d66de423920134 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 15:28:38 +0100 Subject: [PATCH 12/22] Simplify configuration reference --- .../docs/sandbox/configuration/dockerfile.mdx | 519 +-------------- .../configuration/environment-variables.mdx | 613 +++-------------- .../docs/sandbox/configuration/wrangler.mdx | 614 ++++-------------- 3 files changed, 222 insertions(+), 1524 deletions(-) diff --git a/src/content/docs/sandbox/configuration/dockerfile.mdx b/src/content/docs/sandbox/configuration/dockerfile.mdx index bc4e2869e78b586..8bc255fa301b03e 100644 --- a/src/content/docs/sandbox/configuration/dockerfile.mdx +++ b/src/content/docs/sandbox/configuration/dockerfile.mdx @@ -12,10 +12,15 @@ Customize the sandbox container image with your own packages, tools, and configu The Sandbox SDK uses a Ubuntu-based Linux container with Python, Node.js (via Bun), and common development tools pre-installed: ```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest +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) @@ -25,12 +30,10 @@ FROM ghcr.io/cloudflare/sandbox-runtime:latest ## Creating a custom image -### Basic customization - Create a `Dockerfile` in your project root: ```dockerfile title="Dockerfile" -FROM ghcr.io/cloudflare/sandbox-runtime:latest +FROM docker.io/cloudflare/sandbox:0.3.3 # Install additional Python packages RUN pip install --no-cache-dir \ @@ -48,513 +51,23 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* ``` -### Build and publish - -Build your custom image: - -```bash -# Build for your platform -docker build -t your-registry/custom-sandbox:latest . - -# Build for multiple platforms (required for Cloudflare) -docker buildx build --platform linux/amd64,linux/arm64 \ - -t your-registry/custom-sandbox:latest \ - --push . -``` - -Update `wrangler.jsonc` to use your custom image: +Update `wrangler.jsonc` to reference your Dockerfile: ```jsonc title="wrangler.jsonc" { - "containers": [ - { - "binding": "CONTAINER", - "image": "your-registry/custom-sandbox:latest" - } - ] + "containers": [ + { + "binding": "CONTAINER", + "dockerfile": "./Dockerfile", + }, + ], } ``` -## Common customizations - -### Python packages - -Install additional Python libraries: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Data science stack -RUN pip install --no-cache-dir \ - scipy \ - seaborn \ - plotly \ - jupyter - -# Machine learning -RUN pip install --no-cache-dir \ - torch \ - torchvision \ - scikit-learn - -# Web scraping -RUN pip install --no-cache-dir \ - beautifulsoup4 \ - selenium \ - requests-html - -# Always pin versions in production -RUN pip install --no-cache-dir \ - fastapi==0.104.0 \ - pydantic==2.4.0 -``` - -### Node.js packages - -Install npm packages globally: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# TypeScript tooling -RUN npm install -g \ - typescript@5.2.2 \ - ts-node@10.9.1 \ - @types/node@20.8.0 - -# Build tools -RUN npm install -g \ - webpack@5.89.0 \ - vite@4.5.0 \ - esbuild@0.19.5 - -# Testing frameworks -RUN npm install -g \ - jest@29.7.0 \ - vitest@0.34.0 \ - playwright@1.39.0 -``` - -### System tools - -Install system-level packages: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Database clients -RUN apt-get update && apt-get install -y \ - postgresql-client \ - mysql-client \ - redis-tools \ - mongodb-clients - -# Development tools -RUN apt-get update && apt-get install -y \ - build-essential \ - cmake \ - pkg-config - -# Image processing -RUN apt-get update && apt-get install -y \ - imagemagick \ - ffmpeg \ - libvips-tools - -# Clean up to reduce image size -RUN rm -rf /var/lib/apt/lists/* -``` - -### Programming languages - -Add additional language runtimes: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Go -RUN wget https://go.dev/dl/go1.21.3.linux-amd64.tar.gz && \ - tar -C /usr/local -xzf go1.21.3.linux-amd64.tar.gz && \ - rm go1.21.3.linux-amd64.tar.gz -ENV PATH="/usr/local/go/bin:${PATH}" - -# Rust -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -ENV PATH="/root/.cargo/bin:${PATH}" - -# Ruby -RUN apt-get update && apt-get install -y ruby-full && \ - gem install bundler -``` - -### Custom scripts - -Add utility scripts to the image: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Copy custom scripts -COPY scripts/cleanup.sh /usr/local/bin/cleanup -COPY scripts/setup-project.sh /usr/local/bin/setup-project -COPY scripts/run-tests.sh /usr/local/bin/run-tests - -# Make executable -RUN chmod +x /usr/local/bin/cleanup \ - /usr/local/bin/setup-project \ - /usr/local/bin/run-tests -``` - -Then use in your sandboxes: - -```typescript -await sandbox.exec('cleanup'); -await sandbox.exec('setup-project /workspace/repo'); -await sandbox.exec('run-tests'); -``` - -### Configuration files - -Include default configuration: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Git config -COPY config/.gitconfig /root/.gitconfig - -# Python config -COPY config/pip.conf /root/.config/pip/pip.conf - -# NPM config -COPY config/.npmrc /root/.npmrc - -# Custom environment -COPY config/.bashrc /root/.bashrc -``` - -## Production best practices - -### Pin versions - -Always specify exact versions in production: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:1.2.3 # Not :latest - -RUN pip install \ - pandas==2.0.3 \ # Not pandas>=2.0 - numpy==1.25.2 \ - scikit-learn==1.3.0 - -RUN npm install -g \ - typescript@5.2.2 \ # Not typescript@^5.0 - prettier@3.0.3 -``` - -### Multi-stage builds - -Reduce image size with multi-stage builds: - -```dockerfile -# Build stage -FROM ghcr.io/cloudflare/sandbox-runtime:latest AS builder - -# Install build dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - cmake - -# Build custom tools -COPY src/ /build/ -WORKDIR /build -RUN make build - -# Runtime stage -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Copy only built artifacts -COPY --from=builder /build/bin/* /usr/local/bin/ - -# Runtime dependencies only -RUN apt-get update && apt-get install -y \ - libssl3 \ - && rm -rf /var/lib/apt/lists/* -``` - -### Layer optimization - -Order instructions to maximize cache efficiency: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# 1. Install system packages (changes rarely) -RUN apt-get update && apt-get install -y \ - postgresql-client \ - && rm -rf /var/lib/apt/lists/* - -# 2. Install Python packages (changes occasionally) -COPY requirements.txt /tmp/ -RUN pip install --no-cache-dir -r /tmp/requirements.txt - -# 3. Install Node packages (changes occasionally) -COPY package.json package-lock.json /tmp/ -WORKDIR /tmp -RUN npm ci --global -WORKDIR / - -# 4. Copy custom files (changes frequently) -COPY scripts/ /usr/local/bin/ -COPY config/ /root/.config/ -``` - -### Security hardening - -Remove unnecessary tools and set proper permissions: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Install only what you need -RUN apt-get update && apt-get install -y \ - postgresql-client \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -# Remove potentially dangerous tools if not needed -RUN apt-get remove -y \ - gcc \ - make \ - && apt-get autoremove -y - -# Set secure defaults -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -# Restrict file permissions -RUN chmod 755 /usr/local/bin/* -``` - -### Health checks - -Add health check support: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -COPY scripts/healthcheck.sh /usr/local/bin/healthcheck -RUN chmod +x /usr/local/bin/healthcheck - -HEALTHCHECK --interval=30s --timeout=3s \ - CMD /usr/local/bin/healthcheck || exit 1 -``` - -## Platform-specific images - -### Machine learning image - -Optimized for ML/AI workloads: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# CUDA support (if available) -RUN pip install --no-cache-dir \ - torch==2.1.0 \ - torchvision==0.16.0 \ - tensorflow==2.14.0 - -# ML libraries -RUN pip install --no-cache-dir \ - transformers==4.35.0 \ - langchain==0.0.335 \ - scikit-learn==1.3.2 \ - xgboost==2.0.0 - -# Data processing -RUN pip install --no-cache-dir \ - pandas==2.1.3 \ - polars==0.19.12 \ - duckdb==0.9.2 -``` - -### Web development image - -Optimized for full-stack development: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Node.js LTS (additional version) -RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ - apt-get install -y nodejs - -# Frontend build tools -RUN npm install -g \ - vite@5.0.0 \ - webpack@5.89.0 \ - turbopack@1.10.0 - -# Testing frameworks -RUN npm install -g \ - jest@29.7.0 \ - playwright@1.40.0 \ - cypress@13.6.0 - -# Python web frameworks -RUN pip install --no-cache-dir \ - fastapi==0.104.0 \ - django==4.2.7 \ - flask==3.0.0 -``` - -### Data science image - -Optimized for data analysis: - -```dockerfile -FROM ghcr.io/cloudflare/sandbox-runtime:latest - -# Core data science stack -RUN pip install --no-cache-dir \ - jupyter==1.0.0 \ - ipython==8.17.2 \ - pandas==2.1.3 \ - numpy==1.26.2 \ - scipy==1.11.4 - -# Visualization -RUN pip install --no-cache-dir \ - matplotlib==3.8.2 \ - seaborn==0.13.0 \ - plotly==5.18.0 \ - bokeh==3.3.1 - -# Statistics -RUN pip install --no-cache-dir \ - statsmodels==0.14.0 \ - scikit-learn==1.3.2 \ - scipy==1.11.4 -``` - -## Testing your image - -### Local testing - -Test your custom image locally before deploying: - -```bash -# Build the image -docker build -t test-sandbox:local . - -# Run interactively -docker run -it --rm test-sandbox:local bash - -# Test Python packages -docker run --rm test-sandbox:local python3 -c "import pandas; print(pandas.__version__)" - -# Test Node packages -docker run --rm test-sandbox:local npm list -g --depth=0 - -# Test custom scripts -docker run --rm test-sandbox:local which cleanup -``` - -### Automated testing - -Create a test script: - -```dockerfile title="Dockerfile.test" -FROM your-registry/custom-sandbox:latest - -# Run tests -RUN python3 -c "import pandas, numpy, sklearn" && \ - python3 --version && \ - node --version && \ - npm --version && \ - git --version - -# Verify custom scripts -RUN test -x /usr/local/bin/cleanup && \ - test -x /usr/local/bin/setup-project - -CMD ["echo", "All tests passed"] -``` - -Run tests: - -```bash -docker build -f Dockerfile.test -t test-image . -docker run --rm test-image -``` - -## Troubleshooting - -### Image too large - -**Problem**: Container image exceeds size limits - -**Solutions**: - -```dockerfile -# 1. Use multi-stage builds -FROM ghcr.io/cloudflare/sandbox-runtime:latest AS builder -# ... build steps -FROM ghcr.io/cloudflare/sandbox-runtime:latest -COPY --from=builder /build/output /usr/local/bin/ - -# 2. Clean up in the same layer -RUN apt-get update && apt-get install -y package \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# 3. Use --no-cache-dir for pip -RUN pip install --no-cache-dir package - -# 4. Remove unnecessary files -RUN rm -rf /tmp/* /var/tmp/* ~/.cache -``` - -### Package conflicts - -**Problem**: Conflicting package versions - -**Solutions**: - -```dockerfile -# Use virtual environments -RUN python3 -m venv /opt/venv -ENV PATH="/opt/venv/bin:$PATH" -RUN pip install package==1.0.0 - -# Or use specific Python versions -RUN apt-get install -y python3.11 -RUN python3.11 -m pip install package -``` - -### Build failures - -**Problem**: Docker build fails - -**Debug**: - -```bash -# Build with more output -docker build --progress=plain -t custom-sandbox . - -# Test specific layers -docker build --target builder -t test-layer . -docker run -it test-layer bash - -# Check disk space -docker system df -docker system prune -``` +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/engine/reference/builder/) - Complete Dockerfile syntax +- [Docker documentation](https://docs.docker.com/reference/dockerfile/) - Complete Dockerfile syntax - [Container concepts](/sandbox/concepts/containers/) - Understanding the runtime environment -- [Security model](/sandbox/concepts/security/) - Container isolation details diff --git a/src/content/docs/sandbox/configuration/environment-variables.mdx b/src/content/docs/sandbox/configuration/environment-variables.mdx index 27155e07e3a5887..a70cfca35e774e2 100644 --- a/src/content/docs/sandbox/configuration/environment-variables.mdx +++ b/src/content/docs/sandbox/configuration/environment-variables.mdx @@ -7,70 +7,44 @@ sidebar: Pass configuration, secrets, and runtime settings to your sandboxes using environment variables. -## How environment variables work +### Command and process variables -Environment variables can be set at three levels: - -``` -Worker Environment (wrangler.jsonc) - ↓ -Command/Process Execution - ↓ -Sandbox Operating System -``` - -### Worker-level variables - -Defined in `wrangler.jsonc`, available in your Worker code: - -```jsonc title="wrangler.jsonc" -{ - "vars": { - "ENVIRONMENT": "production", - "LOG_LEVEL": "info" - } -} -``` - -Access in Worker: +Pass environment variables when executing commands or starting processes: ```typescript -export default { - async fetch(request: Request, env: Env): Promise { - console.log(`Environment: ${env.ENVIRONMENT}`); - // These are NOT automatically passed to sandboxes - } -}; -``` - -### Command-level variables - -Passed when executing commands: +// Commands +await sandbox.exec("node app.js", { + env: { + NODE_ENV: "production", + API_KEY: env.API_KEY, // Pass from Worker env + PORT: "3000", + }, +}); -```typescript -const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); - -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, + }, }); ``` -### Process-level variables +### Session-level variables -Passed to background processes: +Set environment variables for all commands in a session: ```typescript -await sandbox.startProcess('python server.py', { - env: { - FLASK_ENV: 'production', - DATABASE_URL: env.DATABASE_URL, - SECRET_KEY: env.SECRET_KEY - } +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 @@ -81,240 +55,65 @@ Securely pass secrets from Worker environment: ```typescript interface Env { - Sandbox: DurableObjectNamespace; - // Secrets (set with `wrangler secret put`) - OPENAI_API_KEY: string; - ANTHROPIC_API_KEY: string; - DATABASE_URL: string; + 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'); - - // Pass secrets to sandbox command - const result = await sandbox.exec('python analyze.py', { - env: { - OPENAI_API_KEY: env.OPENAI_API_KEY, - ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY - } - }); - - return Response.json({ result }); - } + 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 }); + }, }; ``` -Python script can access them: - -```python -import os -import openai - -# Environment variables are available in the sandbox -openai.api_key = os.environ['OPENAI_API_KEY'] - -# Use the API -response = openai.chat.completions.create(...) -``` - -### Default environment variables +### Default values with spreading -Create a helper to set default env vars: +Combine default and command-specific variables: ```typescript -function getDefaultEnv(env: Env): Record { - return { - // Always include these - NODE_ENV: env.ENVIRONMENT || 'production', - LOG_LEVEL: env.LOG_LEVEL || 'info', - TZ: 'UTC', - - // API keys from Worker secrets - OPENAI_API_KEY: env.OPENAI_API_KEY, - ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY, - - // Feature flags - ENABLE_CACHING: 'true', - MAX_RETRIES: '3' - }; -} +const defaults = { + NODE_ENV: env.ENVIRONMENT || "production", + LOG_LEVEL: "info", + TZ: "UTC", +}; -// Use in commands -await sandbox.exec('npm start', { - env: { - ...getDefaultEnv(env), - PORT: '3000' // Command-specific overrides - } +await sandbox.exec("npm start", { + env: { + ...defaults, + PORT: "3000", // Command-specific override + API_KEY: env.API_KEY, + }, }); ``` -### Per-user configuration - -Pass user-specific configuration: - -```typescript -async function createUserSandbox( - env: Env, - userId: string, - userConfig: UserConfig -) { - const sandbox = getSandbox(env.Sandbox, `user-${userId}`); - - // Set user-specific environment - const userEnv = { - USER_ID: userId, - USER_TIER: userConfig.tier, - MAX_EXECUTION_TIME: userConfig.maxExecutionTime.toString(), - ALLOWED_PACKAGES: userConfig.allowedPackages.join(','), - - // Global secrets - API_KEY: env.API_KEY - }; - - return { sandbox, env: userEnv }; -} - -// Use it -const { sandbox, env: userEnv } = await createUserSandbox(env, userId, config); -await sandbox.exec('python script.py', { env: userEnv }); -``` - -### Dynamic configuration - -Build environment from request data: - -```typescript -export default { - async fetch(request: Request, env: Env): Promise { - const { language, memory, timeout } = await request.json(); - - const sandbox = getSandbox(env.Sandbox, 'dynamic-sandbox'); - - // Build dynamic environment - const dynamicEnv: Record = { - LANGUAGE: language, - MEMORY_LIMIT: memory, - TIMEOUT: timeout.toString(), - }; - - // Add language-specific vars - if (language === 'python') { - dynamicEnv.PYTHONUNBUFFERED = '1'; - dynamicEnv.PYTHON_VERSION = '3.11'; - } else if (language === 'node') { - dynamicEnv.NODE_ENV = 'production'; - dynamicEnv.NODE_OPTIONS = '--max-old-space-size=512'; - } - - await sandbox.exec('run-code', { env: dynamicEnv }); - - return Response.json({ success: true }); - } -}; -``` - ## Environment variable precedence When the same variable is set at multiple levels: -1. **Command-level** (highest priority) - Passed to `exec()` or `startProcess()` +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 priority) - Operating system defaults +4. **System default** (lowest) - Operating system defaults Example: ```typescript // In Dockerfile: ENV NODE_ENV=development - -// In session: -await sandbox.setEnvVars({ NODE_ENV: 'staging' }); +// In session: await sandbox.setEnvVars({ NODE_ENV: 'staging' }); // In command (overrides all): -await sandbox.exec('node app.js', { - env: { NODE_ENV: 'production' } // This wins -}); -``` - -Result: `NODE_ENV=production` in the command. - -## Session-level environment variables - -Set environment variables for all commands in a session: - -```typescript -// Create a session -const session = await sandbox.createSession(); - -// Set environment for the entire session -await session.setEnvVars({ - DATABASE_URL: env.DATABASE_URL, - REDIS_URL: env.REDIS_URL, - SECRET_KEY: env.SECRET_KEY -}); - -// All commands in this session have these vars -await session.exec('python migrate.py'); // Has DATABASE_URL -await session.exec('python seed.py'); // Has DATABASE_URL -await session.exec('python test.py'); // Has DATABASE_URL -``` - -## Common environment variables - -### Node.js - -```typescript -await sandbox.exec('node app.js', { - env: { - NODE_ENV: 'production', - NODE_OPTIONS: '--max-old-space-size=512', - NPM_CONFIG_LOGLEVEL: 'error', - PORT: '3000' - } -}); -``` - -### Python - -```typescript -await sandbox.exec('python app.py', { - env: { - PYTHONUNBUFFERED: '1', // Disable output buffering - PYTHONDONTWRITEBYTECODE: '1', // Don't create .pyc files - PYTHONPATH: '/workspace/src', // Add to Python path - FLASK_ENV: 'production', - DJANGO_SETTINGS_MODULE: 'settings.production' - } -}); -``` - -### Git - -```typescript -await sandbox.exec('git clone ...', { - env: { - GIT_TERMINAL_PROMPT: '0', // Disable prompts - GIT_ASKPASS: 'echo', // Non-interactive - GIT_USERNAME: 'bot', - GIT_COMMITTER_NAME: 'Bot', - GIT_COMMITTER_EMAIL: 'bot@example.com' - } -}); -``` - -### Database connections - -```typescript -await sandbox.exec('npm run migrate', { - env: { - DATABASE_URL: env.DATABASE_URL, - POSTGRES_HOST: 'localhost', - POSTGRES_PORT: '5432', - POSTGRES_USER: 'admin', - POSTGRES_PASSWORD: env.DB_PASSWORD, - POSTGRES_DB: 'production' - } +await sandbox.exec("node app.js", { + env: { NODE_ENV: "production" }, // This wins }); ``` @@ -322,23 +121,23 @@ await sandbox.exec('npm run migrate', { ### Never hardcode secrets -❌ **Bad** - Secrets in code: +**Bad** - Secrets in code: ```typescript -await sandbox.exec('python app.py', { - env: { - API_KEY: 'sk-1234567890abcdef' // NEVER DO THIS - } +await sandbox.exec("python app.py", { + env: { + API_KEY: "sk-1234567890abcdef", // NEVER DO THIS + }, }); ``` -✅ **Good** - Secrets from Worker environment: +**Good** - Secrets from Worker environment: ```typescript -await sandbox.exec('python app.py', { - env: { - API_KEY: env.API_KEY // From Wrangler secret - } +await sandbox.exec("python app.py", { + env: { + API_KEY: env.API_KEY, // From Wrangler secret + }, }); ``` @@ -346,286 +145,48 @@ Set secrets with Wrangler: ```bash wrangler secret put API_KEY -# Enter your secret value when prompted ``` -### Validate before passing +## Debugging -Validate user-provided environment variables: +List all environment variables: ```typescript -function validateEnv(userEnv: Record): Record { - const safe: Record = {}; - - for (const [key, value] of Object.entries(userEnv)) { - // Only allow specific keys - if (!['PORT', 'TIMEOUT', 'MEMORY'].includes(key)) { - throw new Error(`Invalid environment variable: ${key}`); - } - - // Validate values - if (key === 'PORT') { - const port = parseInt(value); - if (isNaN(port) || port < 1024 || port > 65535) { - throw new Error('Invalid port number'); - } - } - - safe[key] = value; - } - - return safe; -} - -// Use it -const userEnv = await request.json(); -const safeEnv = validateEnv(userEnv); -await sandbox.exec('node app.js', { env: safeEnv }); -``` - -### Sanitize sensitive data in logs - -Don't log sensitive environment variables: - -```typescript -function sanitizeEnv(env: Record): Record { - const sanitized = { ...env }; - const sensitiveKeys = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN']; - - for (const key of Object.keys(sanitized)) { - if (sensitiveKeys.some(k => key.includes(k))) { - sanitized[key] = '***REDACTED***'; - } - } - - return sanitized; -} - -const env = { API_KEY: 'secret', PORT: '3000' }; -console.log('Environment:', sanitizeEnv(env)); -// Logs: Environment: { API_KEY: '***REDACTED***', PORT: '3000' } -``` - -### Use short-lived credentials - -Rotate credentials regularly: - -```typescript -async function getTemporaryCredentials(env: Env): Promise { - // Generate a temporary token valid for 1 hour - const token = await generateToken(env.SECRET_KEY, '1h'); - return token; -} - -// Use temporary credentials -const tempToken = await getTemporaryCredentials(env); -await sandbox.exec('python script.py', { - env: { - TEMP_TOKEN: tempToken // Expires after 1 hour - } -}); -``` - -## Debugging environment variables - -### List all environment variables - -```typescript -const result = await sandbox.exec('env'); -console.log('Environment variables:', result.stdout); -``` - -### Check specific variable - -```typescript -const result = await sandbox.exec('echo $NODE_ENV'); -console.log('NODE_ENV:', result.stdout.trim()); -``` - -### Debug with a script - -```typescript -await sandbox.writeFile('/workspace/debug-env.sh', ` -#!/bin/bash -echo "=== Environment Variables ===" -echo "NODE_ENV: $NODE_ENV" -echo "PORT: $PORT" -echo "API_KEY: ${API_KEY:0:10}..." # Show first 10 chars only -echo "PATH: $PATH" -`); - -const result = await sandbox.exec('bash /workspace/debug-env.sh', { - env: { - NODE_ENV: 'production', - PORT: '3000', - API_KEY: env.API_KEY - } -}); - +const result = await sandbox.exec("env"); console.log(result.stdout); ``` -## Advanced patterns - -### Environment variable templates - -Create templates for different scenarios: +Check specific variable: ```typescript -const ENV_TEMPLATES = { - development: { - NODE_ENV: 'development', - LOG_LEVEL: 'debug', - CACHE_ENABLED: 'false' - }, - staging: { - NODE_ENV: 'staging', - LOG_LEVEL: 'info', - CACHE_ENABLED: 'true' - }, - production: { - NODE_ENV: 'production', - LOG_LEVEL: 'error', - CACHE_ENABLED: 'true', - STRICT_MODE: 'true' - } -}; - -// Use template -const envTemplate = ENV_TEMPLATES[env.ENVIRONMENT] || ENV_TEMPLATES.production; -await sandbox.exec('node app.js', { - env: { - ...envTemplate, - API_KEY: env.API_KEY // Add secrets on top - } -}); -``` - -### Conditional environment variables - -Set variables based on conditions: - -```typescript -const sandboxEnv: Record = { - BASE_URL: 'https://api.example.com' -}; - -// Add debug vars in development -if (env.ENVIRONMENT === 'development') { - sandboxEnv.DEBUG = '*'; - sandboxEnv.VERBOSE = 'true'; -} - -// Add monitoring in production -if (env.ENVIRONMENT === 'production') { - sandboxEnv.SENTRY_DSN = env.SENTRY_DSN; - sandboxEnv.DD_API_KEY = env.DD_API_KEY; -} - -// Add feature flags -if (env.FEATURE_NEW_PARSER === 'true') { - sandboxEnv.USE_NEW_PARSER = 'true'; -} - -await sandbox.exec('node app.js', { env: sandboxEnv }); -``` - -### Encrypted environment variables - -Encrypt sensitive values before passing: - -```typescript -async function encryptValue(value: string, key: string): Promise { - // Implement encryption (example using Web Crypto API) - const encoder = new TextEncoder(); - const data = encoder.encode(value); - - const cryptoKey = await crypto.subtle.importKey( - 'raw', - encoder.encode(key), - { name: 'AES-GCM' }, - false, - ['encrypt'] - ); - - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encrypted = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, - cryptoKey, - data - ); - - return btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted))); -} - -// Encrypt before passing -const encryptedKey = await encryptValue(env.API_KEY, env.ENCRYPTION_KEY); - -await sandbox.exec('python app.py', { - env: { - ENCRYPTED_API_KEY: encryptedKey, - ENCRYPTION_KEY: env.ENCRYPTION_KEY - } -}); +const result = await sandbox.exec("echo $NODE_ENV"); +console.log("NODE_ENV:", result.stdout.trim()); ``` ## Troubleshooting -### Variable not accessible +### Variable not set -**Problem**: Command can't access environment variable - -**Check**: +Verify the variable is being passed: ```typescript -// 1. Verify it's being passed -console.log('Passing env:', { NODE_ENV: env.NODE_ENV }); - -await sandbox.exec('echo $NODE_ENV', { - env: { NODE_ENV: env.NODE_ENV } -}); +console.log("Worker env:", env.API_KEY ? "Set" : "Missing"); -// 2. Check if shell expansion is needed -await sandbox.exec('node -e "console.log(process.env.NODE_ENV)"', { - env: { NODE_ENV: 'production' } +const result = await sandbox.exec("env | grep API_KEY", { + env: { API_KEY: env.API_KEY }, }); +console.log("Sandbox:", result.stdout); ``` -### Variable value is empty - -**Problem**: Environment variable is empty or undefined +### Shell expansion issues -**Debug**: +Use runtime-specific access instead of shell variables: ```typescript -// Check Worker environment -console.log('Worker env:', env.API_KEY ? 'Set' : 'Missing'); - -// Check sandbox environment -const result = await sandbox.exec('env | grep API_KEY', { - env: { API_KEY: env.API_KEY } -}); -console.log('Sandbox env:', result.stdout); -``` - -### Special characters in values - -**Problem**: Values with special characters break - -**Solution**: - -```typescript -// Escape special characters -const value = "value with 'quotes' and $dollar"; - -// Option 1: Use proper escaping -await sandbox.exec(`echo "${value.replace(/"/g, '\\"')}"`, { - env: { VALUE: value } +// Instead of: await sandbox.exec('echo $NODE_ENV') +await sandbox.exec('node -e "console.log(process.env.NODE_ENV)"', { + env: { NODE_ENV: "production" }, }); - -// Option 2: Write to file instead -await sandbox.writeFile('/tmp/config', value); -await sandbox.exec('cat /tmp/config'); ``` ## Related resources diff --git a/src/content/docs/sandbox/configuration/wrangler.mdx b/src/content/docs/sandbox/configuration/wrangler.mdx index 99679fd1330f684..83810f0e7390ab0 100644 --- a/src/content/docs/sandbox/configuration/wrangler.mdx +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -5,114 +5,129 @@ sidebar: order: 1 --- -Configure your Sandbox SDK deployment in `wrangler.jsonc` with Durable Objects bindings, container images, and Worker settings. - ## Minimal configuration The minimum required configuration for using Sandbox SDK: ```jsonc title="wrangler.jsonc" { - "name": "my-sandbox-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" - } - ] + "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 -### compatibility_flags +### 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**: -**Required**: `nodejs_compat` +- **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. -The Sandbox SDK requires Node.js compatibility mode: +For custom images, use a Dockerfile: ```jsonc { - "compatibility_flags": ["nodejs_compat"] + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile", + }, + ], } ``` -This enables: -- Node.js built-in modules (`fs`, `path`, `crypto`, etc.) -- `process.env` access -- Buffer and other Node.js globals +See [Dockerfile reference](/sandbox/configuration/dockerfile/) for customization. ### durable_objects.bindings -**Required**: Sandbox Durable Object binding +Bind the Sandbox Durable Object to your Worker: ```jsonc { - "durable_objects": { - "bindings": [ - { - "name": "Sandbox", // Binding name (used in your code) - "class_name": "Sandbox", // Durable Object class name - "script_name": "@cloudflare/sandbox" // Package providing the DO - } - ] - } + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox", + }, + ], + }, } ``` **Parameters**: -- **name** (string, required) - The binding name you'll use in your Worker code. Conventionally `"Sandbox"`. -- **class_name** (string, required) - Must be `"Sandbox"` - the exported Durable Object class. -- **script_name** (string, required) - Must be `"@cloudflare/sandbox"` - the npm package name. +- **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"`. -### containers +### migrations -**Required**: Container runtime binding +Required for Durable Object initialization: ```jsonc { - "containers": [ - { - "binding": "CONTAINER", - "image": "ghcr.io/cloudflare/sandbox-runtime:latest" - } - ] + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1", + }, + ], } ``` -**Parameters**: - -- **binding** (string, required) - The binding name. Conventionally `"CONTAINER"`. -- **image** (string, required) - The Docker image to use. Options: - - `"ghcr.io/cloudflare/sandbox-runtime:latest"` - Latest stable runtime - - `"ghcr.io/cloudflare/sandbox-runtime:1.2.3"` - Specific version - - `"your-registry/custom-image:tag"` - Your custom image +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" - } + "vars": { + "ENVIRONMENT": "production", + "LOG_LEVEL": "info", + }, } ``` @@ -120,10 +135,10 @@ Access in your Worker: ```typescript export default { - async fetch(request: Request, env: Env): Promise { - console.log(`Running in ${env.ENVIRONMENT} mode`); - // ... - } + async fetch(request: Request, env: Env): Promise { + console.log(`Running in ${env.ENVIRONMENT} mode`); + // ... + }, }; ``` @@ -142,432 +157,34 @@ Access like environment variables: ```typescript interface Env { - Sandbox: DurableObjectNamespace; - ANTHROPIC_API_KEY: string; - GITHUB_TOKEN: string; -} -``` - -### KV bindings - -Add KV for caching or state storage: - -```jsonc -{ - "kv_namespaces": [ - { - "binding": "CACHE", - "id": "your-kv-namespace-id" - } - ] -} -``` - -Use with sandboxes: - -```typescript -// Cache sandbox configurations -await env.CACHE.put(`sandbox:${sandboxId}`, JSON.stringify(config)); - -// Retrieve cached data -const cached = await env.CACHE.get(`sandbox:${sandboxId}`, 'json'); -``` - -### R2 bindings - -Add R2 for file storage: - -```jsonc -{ - "r2_buckets": [ - { - "binding": "STORAGE", - "bucket_name": "sandbox-artifacts" - } - ] + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; + GITHUB_TOKEN: string; } ``` -Use for storing sandbox outputs: - -```typescript -// Store generated files -const file = await sandbox.readFile('/workspace/output.pdf'); -await env.STORAGE.put(`results/${sandboxId}/output.pdf`, file); - -// Retrieve later -const stored = await env.STORAGE.get(`results/${sandboxId}/output.pdf`); -``` - -### D1 bindings - -Add D1 for persistent database storage: - -```jsonc -{ - "d1_databases": [ - { - "binding": "DB", - "database_name": "sandbox-db", - "database_id": "your-database-id" - } - ] -} -``` - -Use for tracking sandbox usage: - -```typescript -// Log sandbox creation -await env.DB.prepare( - 'INSERT INTO sandbox_logs (id, created_at, user_id) VALUES (?, ?, ?)' -).bind(sandboxId, Date.now(), userId).run(); - -// Query usage -const usage = await env.DB.prepare( - 'SELECT COUNT(*) as total FROM sandbox_logs WHERE user_id = ?' -).bind(userId).first(); -``` - -### Queues - -Add Queues for background processing: - -```jsonc -{ - "queues": { - "producers": [ - { - "binding": "SANDBOX_QUEUE", - "queue": "sandbox-tasks" - } - ], - "consumers": [ - { - "queue": "sandbox-tasks", - "max_batch_size": 10, - "max_batch_timeout": 30 - } - ] - } -} -``` - -Use for async sandbox operations: - -```typescript -// Queue a long-running task -await env.SANDBOX_QUEUE.send({ - type: 'run_tests', - repoUrl: 'https://github.com/owner/repo', - branch: 'main' -}); - -// Consumer handler -export default { - async queue(batch: MessageBatch, env: Env): Promise { - for (const message of batch.messages) { - const { type, repoUrl, branch } = message.body; - - if (type === 'run_tests') { - const sandbox = getSandbox(env.Sandbox, `test-${message.id}`); - // Run tests... - await sandbox.destroy(); - } - } - } -}; -``` - -### Workers AI - -Add Workers AI for LLM integration: - -```jsonc -{ - "ai": { - "binding": "AI" - } -} -``` - -Use with sandboxes: - -```typescript -// Generate code with Workers AI -const response = await env.AI.run('@cf/meta/llama-2-7b-chat-int8', { - messages: [ - { role: 'user', content: 'Write a Python function that sorts a list' } - ] -}); - -// Execute in sandbox -const sandbox = getSandbox(env.Sandbox, 'ai-sandbox'); -await sandbox.exec(`python3 -c "${response.response}"`); -``` - -### Limits and resources - -Configure resource limits: - -```jsonc -{ - "limits": { - "cpu_ms": 50 // Maximum CPU time per request (milliseconds) - } -} -``` - -:::note -Worker CPU limits don't apply to sandbox execution time. Sandboxes have their own resource limits managed by Cloudflare Containers. -::: - -### Triggers - -#### Cron triggers +### Cron triggers Run sandboxes on a schedule: ```jsonc { - "triggers": { - "crons": [ - "0 0 * * *" // Run daily at midnight - ] - } + "triggers": { + "crons": ["0 0 * * *"], // Daily at midnight + }, } ``` -Handler: - ```typescript export default { - async scheduled(event: ScheduledEvent, env: Env): Promise { - const sandbox = getSandbox(env.Sandbox, 'scheduled-task'); - - try { - // Run daily data processing - await sandbox.exec('python3 /workspace/daily-report.py'); - } finally { - await sandbox.destroy(); - } - } -}; -``` - -## Complete example - -A production-ready configuration with all common settings: - -```jsonc title="wrangler.jsonc" -{ - "name": "production-sandbox-worker", - "main": "src/index.ts", - "compatibility_date": "2024-09-02", - "compatibility_flags": ["nodejs_compat"], - - // Sandbox SDK bindings - "durable_objects": { - "bindings": [ - { - "name": "Sandbox", - "class_name": "Sandbox", - "script_name": "@cloudflare/sandbox" - } - ] - }, - - // Container runtime - "containers": [ - { - "binding": "CONTAINER", - "image": "ghcr.io/cloudflare/sandbox-runtime:latest" - } - ], - - // Environment variables (non-sensitive) - "vars": { - "ENVIRONMENT": "production", - "MAX_SANDBOX_LIFETIME": "3600000", - "DEFAULT_TIMEOUT": "30000" - }, - - // KV for caching - "kv_namespaces": [ - { - "binding": "CACHE", - "id": "your-kv-namespace-id" - } - ], - - // R2 for file storage - "r2_buckets": [ - { - "binding": "STORAGE", - "bucket_name": "sandbox-outputs" - } - ], - - // D1 for analytics - "d1_databases": [ - { - "binding": "DB", - "database_name": "sandbox-analytics", - "database_id": "your-database-id" - } - ], - - // Queue for background tasks - "queues": { - "producers": [ - { - "binding": "TASKS", - "queue": "sandbox-tasks" - } - ] - }, - - // Observability - "observability": { - "enabled": true, - "head_sampling_rate": 0.1 - }, - - // Scheduled tasks - "triggers": { - "crons": [ - "*/15 * * * *" // Every 15 minutes - ] - } -} -``` - -## Environment-specific configurations - -Use different configs for development and production: - -### wrangler.jsonc (development) - -```jsonc title="wrangler.jsonc" -{ - "name": "sandbox-worker-dev", - "vars": { - "ENVIRONMENT": "development" - }, - "containers": [ - { - "binding": "CONTAINER", - "image": "ghcr.io/cloudflare/sandbox-runtime:latest" - } - ] -} -``` - -### wrangler.prod.jsonc (production) - -```jsonc title="wrangler.prod.jsonc" -{ - "name": "sandbox-worker-prod", - "vars": { - "ENVIRONMENT": "production" - }, - "containers": [ - { - "binding": "CONTAINER", - "image": "ghcr.io/cloudflare/sandbox-runtime:1.2.3" // Pinned version - } - ], - "limits": { - "cpu_ms": 50 - } -} -``` - -Deploy with: - -```bash -# Development -wrangler deploy - -# Production -wrangler deploy --config wrangler.prod.jsonc -``` - -## TypeScript types - -Define your environment interface: - -```typescript title="src/index.ts" -interface Env { - // Required - Sandbox: DurableObjectNamespace; - - // Secrets - ANTHROPIC_API_KEY: string; - GITHUB_TOKEN: string; - - // Variables - ENVIRONMENT: string; - MAX_SANDBOX_LIFETIME: string; - - // Optional bindings - CACHE?: KVNamespace; - STORAGE?: R2Bucket; - DB?: D1Database; - TASKS?: Queue; - AI?: Ai; -} - -export default { - async fetch(request: Request, env: Env): Promise { - // All bindings are type-safe - const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); - // ... - } + 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(); + }, }; ``` -## Common patterns - -### Multiple container images - -Use different images for different purposes: - -```jsonc -{ - "containers": [ - { - "binding": "PYTHON_CONTAINER", - "image": "ghcr.io/your-org/python-sandbox:latest" - }, - { - "binding": "NODE_CONTAINER", - "image": "ghcr.io/your-org/node-sandbox:latest" - } - ] -} -``` - -### Feature flags - -Control features with environment variables: - -```jsonc -{ - "vars": { - "FEATURE_STREAMING": "true", - "FEATURE_GPU_SUPPORT": "false", - "MAX_CONCURRENT_SANDBOXES": "10" - } -} -``` - -```typescript -if (env.FEATURE_STREAMING === 'true') { - return handleStreamingRequest(request, env); -} -``` - ## Troubleshooting ### Binding not found @@ -578,15 +195,14 @@ if (env.FEATURE_STREAMING === 'true') { ```jsonc { - "durable_objects": { - "bindings": [ - { - "name": "Sandbox", // Must match your code - "class_name": "Sandbox", - "script_name": "@cloudflare/sandbox" - } - ] - } + "durable_objects": { + "bindings": [ + { + "class_name": "Sandbox", + "name": "Sandbox", + }, + ], + }, } ``` @@ -594,25 +210,33 @@ if (env.FEATURE_STREAMING === 'true') { **Error**: `Failed to pull container image` -**Solution**: Verify the image exists and is accessible: - -```bash -# Test locally -docker pull ghcr.io/cloudflare/sandbox-runtime:latest +**Solution**: Ensure you're using the correct image version (must match npm package): -# Use a specific version -"image": "ghcr.io/cloudflare/sandbox-runtime:1.2.3" +```jsonc +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "docker.io/cloudflare/sandbox:0.3.3", + }, + ], +} ``` -### nodejs_compat missing +### Missing migrations -**Error**: Module "crypto" is not available +**Error**: Durable Object not initialized -**Solution**: Add the compatibility flag: +**Solution**: Add migrations for the Sandbox class: ```jsonc { - "compatibility_flags": ["nodejs_compat"] + "migrations": [ + { + "new_sqlite_classes": ["Sandbox"], + "tag": "v1", + }, + ], } ``` From 097e85aae011ba0fe7f422b076411115be180b42 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 16:22:40 +0100 Subject: [PATCH 13/22] Simplify guides --- .../sandbox/guides/background-processes.mdx | 425 +------------- .../docs/sandbox/guides/code-execution.mdx | 451 ++------------- .../docs/sandbox/guides/execute-commands.mdx | 219 +------ .../docs/sandbox/guides/expose-services.mdx | 533 +++--------------- .../docs/sandbox/guides/git-workflows.mdx | 480 +--------------- .../docs/sandbox/guides/manage-files.mdx | 490 ++-------------- .../docs/sandbox/guides/streaming-output.mdx | 434 ++------------ 7 files changed, 289 insertions(+), 2743 deletions(-) diff --git a/src/content/docs/sandbox/guides/background-processes.mdx b/src/content/docs/sandbox/guides/background-processes.mdx index 07e972c964dd535..92836feb15f1ae8 100644 --- a/src/content/docs/sandbox/guides/background-processes.mdx +++ b/src/content/docs/sandbox/guides/background-processes.mdx @@ -51,21 +51,7 @@ console.log('Status:', server.status); // 'running' ## Configure process environment -### Set working directory - - -``` -// Start process in specific directory -const process = await sandbox.startProcess('npm run dev', { - cwd: '/workspace/my-app' -}); - -// Equivalent to: -// cd /workspace/my-app && npm run dev -``` - - -### Set environment variables +Set working directory and environment variables: ``` @@ -73,20 +59,19 @@ const process = await sandbox.startProcess('node server.js', { cwd: '/workspace/api', env: { NODE_ENV: 'production', - PORT: '3000', + PORT: '8080', API_KEY: env.API_KEY, - DATABASE_URL: env.DATABASE_URL, - LOG_LEVEL: 'debug' + DATABASE_URL: env.DATABASE_URL } }); -console.log('API server started on port 3000'); +console.log('API server started'); ``` ## Monitor process status -### List all processes +List and check running processes: ``` @@ -95,157 +80,65 @@ const processes = await sandbox.listProcesses(); console.log(`Running ${processes.length} processes:`); for (const proc of processes) { - console.log(` - ID: ${proc.id} - PID: ${proc.pid} - Command: ${proc.command} - Status: ${proc.status} - `); + console.log(`${proc.id}: ${proc.command} (${proc.status})`); } -``` - -### Check if process is running - - -``` -async function isProcessRunning(processId: string): Promise { - const processes = await sandbox.listProcesses(); - return processes.some(p => p.id === processId && p.status === 'running'); -} - -// Usage -const server = await sandbox.startProcess('node server.js'); - -if (await isProcessRunning(server.id)) { - console.log('Server is running'); -} else { - console.log('Server stopped or crashed'); -} +// 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 +Stream logs in real-time to detect when a service is ready: ``` import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; -// Start a web server 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.timestamp}] ${log.data}`); + console.log(log.data); - // Wait for server to be ready - if (log.data.includes('Server listening on port')) { + if (log.data.includes('Server listening')) { console.log('Server is ready!'); - break; // Stop watching logs + break; } } ``` -### Get accumulated logs +Or get accumulated logs: ``` -const server = await sandbox.startProcess('npm run build'); - -// Wait for build to complete -await new Promise(resolve => setTimeout(resolve, 10000)); - -// Get all logs const logs = await sandbox.getProcessLogs(server.id); - -console.log('Build output:', logs); - -// Parse logs for specific information -if (logs.includes('Build successful')) { - console.log('✓ Build completed'); -} else if (logs.includes('Error:')) { - console.error('✗ Build failed'); -} +console.log('Logs:', logs); ``` ## Stop processes -### Stop specific process - ``` -const server = await sandbox.startProcess('python -m http.server 8000'); - -// Stop gracefully (SIGTERM) +// Stop specific process await sandbox.killProcess(server.id); -console.log('Server stopped'); -// Force kill (SIGKILL) +// Force kill if needed await sandbox.killProcess(server.id, 'SIGKILL'); -``` - -### Stop all processes - - -``` -// Clean up everything +// Stop all processes await sandbox.killAllProcesses(); -console.log('All processes stopped'); -``` - - -### Stop after timeout - - -``` -const process = await sandbox.startProcess('node app.js'); - -// Auto-stop after 1 hour -setTimeout(async () => { - await sandbox.killProcess(process.id); - console.log('Process stopped after timeout'); -}, 60 * 60 * 1000); ``` ## Run multiple processes -### Start multiple services - - -``` -// Start backend API -const api = await sandbox.startProcess('node api-server.js', { - cwd: '/workspace/backend', - env: { PORT: '3000' } -}); - -// Start frontend dev server -const frontend = await sandbox.startProcess('npm run dev', { - cwd: '/workspace/frontend', - env: { PORT: '5173' } -}); - -// Start database -const db = await sandbox.startProcess('redis-server', { - env: { PORT: '6379' } -}); - -console.log('All services started:'); -console.log('- API:', api.pid); -console.log('- Frontend:', frontend.pid); -console.log('- Database:', db.pid); -``` - - -### Coordinate multiple processes +Start services in sequence, waiting for dependencies: ``` @@ -258,7 +151,6 @@ const db = await sandbox.startProcess('redis-server'); const dbLogs = await sandbox.streamProcessLogs(db.id); for await (const log of parseSSEStream(dbLogs)) { if (log.data.includes('Ready to accept connections')) { - console.log('Database ready'); break; } } @@ -268,309 +160,44 @@ const api = await sandbox.startProcess('node api-server.js', { env: { DATABASE_URL: 'redis://localhost:6379' } }); -// Wait for API to be ready -const apiLogs = await sandbox.streamProcessLogs(api.id); -for await (const log of parseSSEStream(apiLogs)) { - if (log.data.includes('API listening')) { - console.log('API ready'); - break; - } -} - -console.log('All services ready'); -``` - - -## Expose services via ports - -### Start and expose web server - - -``` -// Start web server -const server = await sandbox.startProcess('python -m http.server 8000'); - -// Expose port to get public URL -const port = await sandbox.exposePort(8000); - -console.log('Server accessible at:', port.previewUrl); -// Returns: https://{random-subdomain}.cloudflare-sandbox.com - -// Send URL to user -return new Response(JSON.stringify({ - status: 'Server started', - url: port.previewUrl -})); -``` - - -### Wait for service before exposing - - -``` -import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; - -// Start Node.js application -const app = await sandbox.startProcess('node server.js', { - env: { PORT: '3000' } -}); - -// Wait for server to be ready -const logs = await sandbox.streamProcessLogs(app.id); -let serverReady = false; - -for await (const log of parseSSEStream(logs)) { - if (log.data.includes('Server listening')) { - serverReady = true; - break; - } -} - -if (serverReady) { - // Now expose port - const port = await sandbox.exposePort(3000); - console.log('Application ready:', port.previewUrl); -} +console.log('All services running'); ``` ## Best practices -### Process lifecycle - -- **Check readiness** - Wait for processes to be ready before using them -- **Monitor health** - Watch logs for errors or crashes +- **Wait for readiness** - Stream logs to detect when services are ready - **Clean up** - Always stop processes when done -- **Handle failures** - Restart crashed processes automatically - -### Resource management - -- **Limit concurrent processes** - Don't start too many processes -- **Set timeouts** - Stop processes that run too long -- **Monitor memory** - Track resource usage -- **Clean up on errors** - Use try/finally to ensure cleanup - -### Error handling - -- **Check process status** - Verify process started successfully -- **Parse logs for errors** - Watch for error messages -- **Retry failed starts** - Handle transient failures -- **Alert on crashes** - Notify when processes die unexpectedly - -### Security - -- **Validate commands** - Don't execute arbitrary user input -- **Limit environment access** - Only pass necessary env vars -- **Isolate processes** - Use separate sandboxes for untrusted code -- **Set resource limits** - Prevent resource exhaustion - -## Common patterns - -### Supervised process - -Automatically restart processes that crash: - - -``` -async function supervisedProcess( - command: string, - maxRestarts: number = 3 -): Promise { - let restarts = 0; - - while (restarts < maxRestarts) { - const process = await sandbox.startProcess(command); - console.log(`Started process (attempt ${restarts + 1})`); - - // Monitor process - const logs = await sandbox.streamProcessLogs(process.id); - - for await (const log of parseSSEStream(logs)) { - console.log(log.data); - - // Check for errors - if (log.data.includes('FATAL') || log.data.includes('crashed')) { - console.error('Process crashed, restarting...'); - await sandbox.killProcess(process.id); - restarts++; - break; - } - } - } - - throw new Error(`Process failed after ${maxRestarts} restarts`); -} - -// Use it -await supervisedProcess('node unstable-server.js', 5); -``` - - -### Health check endpoint - - -``` -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - if (url.pathname === '/health') { - const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); - const processes = await sandbox.listProcesses(); - - // Check if required services are running - const required = ['api-server', 'database', 'cache']; - const running = processes.filter(p => - required.some(r => p.command.includes(r)) - ); - - const healthy = running.length === required.length; - - return new Response(JSON.stringify({ - healthy, - processes: running.length, - required: required.length - }), { - status: healthy ? 200 : 503, - headers: { 'Content-Type': 'application/json' } - }); - } - - return new Response('Not found', { status: 404 }); - } -}; -``` - - -### Graceful shutdown - - -``` -async function gracefulShutdown(): Promise { - console.log('Shutting down...'); - - // Get all processes - const processes = await sandbox.listProcesses(); - - // Stop each process gracefully - for (const proc of processes) { - try { - console.log(`Stopping ${proc.command}...`); - - // Try graceful stop (SIGTERM) - await sandbox.killProcess(proc.id, 'SIGTERM'); - - // Wait for process to stop - await new Promise(resolve => setTimeout(resolve, 5000)); - - // Check if still running - if (await isProcessRunning(proc.id)) { - // Force kill if needed - console.log(`Force killing ${proc.command}...`); - await sandbox.killProcess(proc.id, 'SIGKILL'); - } - } catch (error) { - console.error(`Error stopping ${proc.command}:`, error); - } - } - - console.log('All processes stopped'); -} +- **Handle failures** - Monitor logs for errors and restart if needed +- **Use try/finally** - Ensure cleanup happens even on errors -// Call on shutdown -await gracefulShutdown(); -``` - - -## Common issues +## Troubleshooting ### Process exits immediately -**Problem**: Process starts but exits right away. - -**Solution**: Check logs to see why it exited: +Check logs to see why: ``` const process = await sandbox.startProcess('node server.js'); - -// Wait a moment await new Promise(resolve => setTimeout(resolve, 1000)); -// Check if still running const processes = await sandbox.listProcesses(); -const running = processes.find(p => p.id === process.id); - -if (!running) { - // Process exited - check logs +if (!processes.find(p => p.id === process.id)) { const logs = await sandbox.getProcessLogs(process.id); console.error('Process exited:', logs); - - // Common causes: - // - Missing dependencies: "Cannot find module 'express'" - // - Port already in use: "EADDRINUSE" - // - Configuration error: "Invalid config" } ``` ### Port already in use -**Problem**: "Address already in use" error when starting process. - -**Solution**: Kill existing process on that port or use different port: +Kill existing processes before starting: ``` -// Option 1: Kill processes first await sandbox.killAllProcesses(); const server = await sandbox.startProcess('node server.js'); - -// Option 2: Use dynamic port -const port = 3000 + Math.floor(Math.random() * 1000); -const server = await sandbox.startProcess(`node server.js`, { - env: { PORT: port.toString() } -}); - -// Option 3: Find and kill specific process -const processes = await sandbox.listProcesses(); -const existing = processes.find(p => p.command.includes('server.js')); -if (existing) { - await sandbox.killProcess(existing.id); -} - -const server = await sandbox.startProcess('node server.js'); -``` - - -### Process hangs or doesn't respond - -**Problem**: Process is running but not responding to requests. - -**Solution**: Check logs and add timeout: - - -``` -const process = await sandbox.startProcess('node app.js'); - -// Add timeout -const timeout = setTimeout(async () => { - console.error('Process not responding, restarting...'); - await sandbox.killProcess(process.id); - - // Start new instance - await sandbox.startProcess('node app.js'); -}, 30000); // 30 second timeout - -// Monitor logs for ready signal -const logs = await sandbox.streamProcessLogs(process.id); -for await (const log of parseSSEStream(logs)) { - if (log.data.includes('Ready')) { - clearTimeout(timeout); - console.log('Process ready'); - break; - } -} ``` diff --git a/src/content/docs/sandbox/guides/code-execution.mdx b/src/content/docs/sandbox/guides/code-execution.mdx index 54c36df5bbf8e97..624605bbc0e26cf 100644 --- a/src/content/docs/sandbox/guides/code-execution.mdx +++ b/src/content/docs/sandbox/guides/code-execution.mdx @@ -16,19 +16,19 @@ This guide shows you how to execute Python and JavaScript code with rich outputs ## When to use code interpreter -Use the Code Interpreter API when you need: +Use the Code Interpreter API for **simple, direct code execution** with minimal setup: -- **Rich outputs** - Charts, tables, images, HTML, LaTeX from code execution -- **Persistent state** - Variables and imports preserved between executions -- **Multiple languages** - Python, JavaScript, and TypeScript support -- **Interactive workflows** - Iterative data analysis and exploration -- **AI integration** - Execute LLM-generated code safely +- **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: +Use `exec()` for **advanced or custom workflows**: -- **Simple scripts** - One-time commands without state -- **System operations** - File operations, installs, builds -- **Text-only output** - When you only need stdout/stderr +- **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 @@ -109,45 +109,7 @@ console.log(result.output); // "Mean: 3.0" ## Handle rich outputs -### Get all available outputs - - -``` -const result = await sandbox.runCode(context.id, ` -import matplotlib.pyplot as plt -import numpy as np - -# Generate data -x = np.linspace(0, 10, 100) -y = np.sin(x) - -# Create plot -plt.figure(figsize=(10, 6)) -plt.plot(x, y) -plt.title('Sine Wave') -plt.xlabel('X') -plt.ylabel('sin(X)') -plt.grid(True) -plt.show() -`); - -// Check available formats -console.log('Formats:', result.formats); -// ['text', 'png'] - -// Access specific outputs -if (result.outputs.png) { - console.log('Chart available as PNG'); - // result.outputs.png contains base64-encoded image -} - -if (result.outputs.text) { - console.log('Text output:', result.outputs.text); -} -``` - - -### Return images +The code interpreter returns multiple output formats: ``` @@ -159,61 +121,26 @@ plt.title('Simple Chart') plt.show() `); -// Return image to client +// Check available formats +console.log('Formats:', result.formats); // ['text', 'png'] + +// Access outputs if (result.outputs.png) { - const imageData = atob(result.outputs.png); - return new Response(imageData, { + // Return as image + return new Response(atob(result.outputs.png), { headers: { 'Content-Type': 'image/png' } }); } -``` - - -### Return data tables - - -``` -const result = await sandbox.runCode(context.id, ` -import pandas as pd - -# Create DataFrame -df = pd.DataFrame({ - 'name': ['Alice', 'Bob', 'Charlie'], - 'age': [25, 30, 35], - 'city': ['NYC', 'SF', 'LA'] -}) -# Display as HTML table -df -`); - -// Get HTML table if (result.outputs.html) { + // Return as HTML (pandas DataFrames) return new Response(result.outputs.html, { headers: { 'Content-Type': 'text/html' } }); } -``` - - -### Return JSON data - - -``` -const result = await sandbox.runCode(context.id, ` -data = { - "users": [ - {"name": "Alice", "score": 95}, - {"name": "Bob", "score": 87} - ], - "total": 2 -} - -# Display as JSON -data -`); if (result.outputs.json) { + // Return as JSON return Response.json(result.outputs.json); } ``` @@ -256,191 +183,40 @@ print("Done!") ``` -## Data analysis workflows +## Execute AI-generated code -### Load and analyze data +Run LLM-generated code safely in a sandbox: ``` -const context = await sandbox.createCodeContext({ - language: 'python' +// 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' + }] + }) }); -// Load CSV data -await sandbox.writeFile('/workspace/data.csv', csvData); - -const result = await sandbox.runCode(context.id, ` -import pandas as pd -import matplotlib.pyplot as plt - -# Load data -df = pd.read_csv('/workspace/data.csv') - -# Basic statistics -print("Dataset shape:", df.shape) -print("\\nSummary statistics:") -print(df.describe()) - -# Create visualization -plt.figure(figsize=(10, 6)) -df.plot(kind='bar') -plt.title('Data Visualization') -plt.show() - -# Return summary -{ - "rows": len(df), - "columns": list(df.columns), - "mean_value": float(df['value'].mean()) -} -`); - -// Access results -console.log('Text output:', result.outputs.text); -console.log('Chart:', result.outputs.png ? 'Available' : 'Not available'); -console.log('Data:', result.outputs.json); -``` - - -### Multi-step analysis - - -``` -const context = await sandbox.createCodeContext({ - language: 'python' -}); - -// Step 1: Load and clean data -await sandbox.runCode(context.id, ` -import pandas as pd - -df = pd.read_csv('/workspace/sales.csv') -df = df.dropna() # Remove missing values -df['date'] = pd.to_datetime(df['date']) -print(f"Loaded {len(df)} records") -`); - -// Step 2: Aggregate data -await sandbox.runCode(context.id, ` -monthly_sales = df.groupby(df['date'].dt.to_period('M'))['amount'].sum() -print("Monthly totals calculated") -`); - -// Step 3: Create visualizations -const result = await sandbox.runCode(context.id, ` -import matplotlib.pyplot as plt - -plt.figure(figsize=(12, 6)) -monthly_sales.plot(kind='bar') -plt.title('Monthly Sales') -plt.xlabel('Month') -plt.ylabel('Sales ($)') -plt.xticks(rotation=45) -plt.tight_layout() -plt.show() - -# Return summary -{ - "total_sales": float(monthly_sales.sum()), - "average_monthly": float(monthly_sales.mean()), - "best_month": str(monthly_sales.idxmax()) -} -`); - -console.log('Summary:', result.outputs.json); -``` - - -## AI code execution - -Execute LLM-generated code safely: - - -``` -async function executeAIGeneratedCode( - userPrompt: string, - env: Env -): Promise { - const sandbox = getSandbox(env.Sandbox, 'ai-executor'); - - // 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 ${userPrompt}. Return only the code, no explanations.` - }] - }) - }); - - const data = await response.json(); - const code = data.content[0].text; - - // 2. Create execution context - const context = await sandbox.createCodeContext({ - language: 'python' - }); +const { content } = await response.json(); +const code = content[0].text; - // 3. Execute generated code - const result = await sandbox.runCode(context.id, code, { - stream: true, - onOutput: (output) => console.log(output), - onError: (error) => console.error('Execution error:', error) - }); - - // 4. Return results - return { - prompt: userPrompt, - code, - output: result.output, - success: result.success, - formats: result.formats, - outputs: result.outputs - }; -} +// 2. Execute in sandbox +const context = await sandbox.createCodeContext({ language: 'python' }); +const result = await sandbox.runCode(context.id, code); -// Usage -const result = await executeAIGeneratedCode( - 'analyze the fibonacci sequence up to 100', - env -); - -console.log('Generated code:', result.code); +console.log('Generated code:', code); console.log('Output:', result.output); -``` - - -## Handle errors - - -``` -const context = await sandbox.createCodeContext({ - language: 'python' -}); - -const result = await sandbox.runCode(context.id, ` -# This code has an error -undefined_variable = some_variable + 1 -`); - -if (!result.success) { - console.error('Execution failed'); - console.error('Error:', result.error.name); // "NameError" - console.error('Message:', result.error.value); // "name 'some_variable' is not defined" - console.error('Line:', result.error.lineNumber); // Line where error occurred - - if (result.error.traceback) { - console.error('Traceback:', result.error.traceback.join('\\n')); - } -} +console.log('Success:', result.success); ``` @@ -479,141 +255,10 @@ console.log('All contexts deleted'); ## Best practices -### Context management - -- **One context per task** - Keep contexts focused on specific tasks -- **Clean up** - Delete contexts when done to free resources -- **Name contexts** - Use meaningful IDs to track purposes -- **Reuse contexts** - Share contexts for related operations - -### Code execution - -- **Validate AI code** - Check generated code before execution -- **Use timeouts** - Set execution timeouts for safety -- **Stream long operations** - Provide feedback for slow code -- **Handle errors gracefully** - Always check success and error fields - -### Performance - -- **Limit context count** - Don't create too many concurrent contexts -- **Batch operations** - Execute multiple statements in one call -- **Cache results** - Store expensive computation results -- **Clean up resources** - Delete unused contexts promptly - -### Security - -- **Validate inputs** - Sanitize user inputs before including in code -- **Limit capabilities** - Use sessions to isolate untrusted code -- **Set resource limits** - Prevent resource exhaustion -- **Review generated code** - Inspect AI-generated code for safety - -## Common patterns - -### Interactive data exploration - - -``` -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - const sandbox = getSandbox(env.Sandbox, 'data-explorer'); - - if (url.pathname === '/analyze') { - const { code } = await request.json(); - - // Create or reuse context - let contexts = await sandbox.listCodeContexts(); - let context = contexts.find(c => c.language === 'python'); - - if (!context) { - context = await sandbox.createCodeContext({ - language: 'python' - }); - - // Initialize with data - await sandbox.runCode(context.id, ` - import pandas as pd - import numpy as np - import matplotlib.pyplot as plt - - df = pd.read_csv('/workspace/dataset.csv') - print(f"Dataset loaded: {len(df)} rows") - `); - } - - // Execute user code - const result = await sandbox.runCode(context.id, code); - - return Response.json({ - output: result.output, - formats: result.formats, - outputs: result.outputs, - success: result.success - }); - } - - return new Response('Not found', { status: 404 }); - } -}; -``` - - -### Chart generation service - - -``` -async function generateChart( - chartType: string, - data: number[], - options: any -): Promise { - const context = await sandbox.createCodeContext({ - language: 'python' - }); - - const result = await sandbox.runCode(context.id, ` -import matplotlib.pyplot as plt -import numpy as np - -data = ${JSON.stringify(data)} - -plt.figure(figsize=(10, 6)) - -if "${chartType}" == "line": - plt.plot(data) -elif "${chartType}" == "bar": - plt.bar(range(len(data)), data) -elif "${chartType}" == "scatter": - plt.scatter(range(len(data)), data) - -plt.title("${options.title || 'Chart'}") -plt.xlabel("${options.xlabel || 'X'}") -plt.ylabel("${options.ylabel || 'Y'}") -plt.grid(True) -plt.show() - `); - - // Clean up - await sandbox.deleteCodeContext(context.id); - - // Return base64 PNG - return result.outputs.png || ''; -} - -// Usage -const chartData = [1, 4, 2, 8, 5, 7]; -const chart = await generateChart('line', chartData, { - title: 'Sample Data', - xlabel: 'Index', - ylabel: 'Value' -}); - -// Return as image -return new Response(atob(chart), { - headers: { 'Content-Type': 'image/png' } -}); -``` - +- **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 diff --git a/src/content/docs/sandbox/guides/execute-commands.mdx b/src/content/docs/sandbox/guides/execute-commands.mdx index fde71b455aa0b13..4108f71722afce3 100644 --- a/src/content/docs/sandbox/guides/execute-commands.mdx +++ b/src/content/docs/sandbox/guides/execute-commands.mdx @@ -46,15 +46,15 @@ When passing user input or dynamic values, avoid string interpolation to prevent ``` -// ❌ Unsafe - vulnerable to injection +// Unsafe - vulnerable to injection const filename = userInput; await sandbox.exec(`cat ${filename}`); -// ✅ Safe - use proper escaping or validation +// 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 +// Better - write to file and execute await sandbox.writeFile('/tmp/input.txt', userInput); await sandbox.exec('python process.py /tmp/input.txt'); ``` @@ -96,67 +96,9 @@ try { ``` -## Stream output in real-time - -For long-running commands like builds or installations, use streaming to provide immediate feedback: - - -``` -// Option 1: Using exec() with callbacks (simpler) -const result = await sandbox.exec('npm install && npm run build', { - stream: true, - onOutput: (stream, data) => { - if (stream === 'stdout') { - console.log(data); - } else { - console.error(data); - } - } -}); - -console.log('Build complete, exit code:', result.exitCode); -``` - - - -``` -// Option 2: Using execStream() (more control) -import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; - -const stream = await sandbox.execStream('npm test'); - -for await (const event of parseSSEStream(stream)) { - switch (event.type) { - case 'start': - console.log('Tests started'); - break; - - case 'stdout': - // Parse and process output in real-time - if (event.data.includes('PASS')) { - console.log('✓ Test passed'); - } - break; - - case 'stderr': - console.error('Error:', event.data); - break; - - case 'complete': - console.log('Tests finished, exit code:', event.exitCode); - break; - - case 'error': - console.error('Execution failed:', event.error); - break; - } -} -``` - - ## Execute shell commands -The sandbox supports shell features like pipes, redirects, and variable expansion: +The sandbox supports shell features like pipes, redirects, and chaining: ``` @@ -169,41 +111,11 @@ await sandbox.exec('python generate.py > output.txt 2> errors.txt'); // Multiple commands await sandbox.exec('cd /workspace && npm install && npm test'); - -// Environment variables -await sandbox.exec('export API_KEY=secret && python app.py'); -``` - - -## Run commands with timeouts - -Prevent commands from running indefinitely: - - -``` -try { - const result = await sandbox.exec('python slow-script.py', { - timeout: 30000 // 30 second timeout - }); - - console.log('Completed in time:', result.stdout); - -} catch (error) { - if (error.message.includes('timeout')) { - console.error('Command timed out after 30 seconds'); - // Handle timeout - maybe kill related processes - await sandbox.killAllProcesses(); - } else { - throw error; - } -} ``` ## Execute Python scripts -Common patterns for running Python code: - ``` // Run inline Python @@ -213,145 +125,46 @@ console.log('Sum:', result.stdout.trim()); // "15" // Run a script file await sandbox.writeFile('/workspace/analyze.py', ` import sys -import json - -data = json.loads(sys.argv[1]) -result = sum(data) -print(json.dumps({"sum": result})) +print(f"Argument: {sys.argv[1]}") `); -const output = await sandbox.exec( - `python /workspace/analyze.py '${JSON.stringify([1, 2, 3])}'` -); - -const parsed = JSON.parse(output.stdout); -console.log('Result:', parsed.sum); // 6 -``` - - -## Execute Node.js scripts - - -``` -// Install dependencies first -await sandbox.exec('npm install lodash'); - -// Run Node.js script -await sandbox.writeFile('/workspace/process.js', ` -const _ = require('lodash'); -const data = [1, 2, 3, 4, 5]; -console.log(JSON.stringify({ - sum: _.sum(data), - mean: _.mean(data) -})); -`); - -const result = await sandbox.exec('node /workspace/process.js'); -const stats = JSON.parse(result.stdout); - -console.log('Sum:', stats.sum); // 15 -console.log('Mean:', stats.mean); // 3 +await sandbox.exec('python /workspace/analyze.py data.csv'); ``` ## Best practices -### Command design - -- **Keep commands simple** - Break complex operations into multiple commands -- **Make commands idempotent** - Commands should be safe to retry -- **Validate inputs** - Never trust user input in commands -- **Use absolute paths** - Avoid ambiguity with working directories - -### Error handling - -- **Always check exit codes** - Non-zero means failure even if no exception -- **Capture stderr** - Error messages help debug failures -- **Provide context** - Log the command that failed for debugging -- **Handle timeouts** - Don't let commands hang indefinitely - -### Performance - -- **Use streaming for long operations** - Provide immediate feedback to users -- **Batch related commands** - Use `&&` to chain dependent operations -- **Cache installations** - Don't reinstall packages every time -- **Set appropriate timeouts** - Balance between too short and too long - -### Security +- **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 -- **Escape user input** - Prevent command injection attacks -- **Limit command scope** - Use working directories to restrict access -- **Validate command output** - Don't trust command results blindly -- **Use environment variables** - Safer than inline secrets - -## Common issues +## Troubleshooting ### Command not found -**Problem**: Command fails with "command not found" error. - -**Solution**: Verify the command exists or install it first: +Verify the command exists in the container: ``` -// Check if command exists const check = await sandbox.exec('which python3'); if (!check.success) { - // Install if needed - await sandbox.exec('apt-get update && apt-get install -y python3'); + console.error('python3 not found'); } - -// Now safe to use -await sandbox.exec('python3 script.py'); ``` ### Working directory issues -**Problem**: Command can't find files because it's running in wrong directory. - -**Solution**: Use absolute paths or change directory in command: +Use absolute paths or change directory: ``` -// Option 1: Use absolute paths +// Use absolute path await sandbox.exec('python /workspace/my-app/script.py'); -// Option 2: Change directory in command +// Or change directory await sandbox.exec('cd /workspace/my-app && python script.py'); - -// Option 3: Use startProcess() with cwd option (for background processes) -await sandbox.startProcess('python script.py', { - cwd: '/workspace/my-app' -}); -``` - - -### Output too large - -**Problem**: Command produces huge output that causes performance issues. - -**Solution**: Stream output and process incrementally, or redirect to file: - - -``` -// Option 1: Redirect to file -await sandbox.exec('python generate-large-data.py > /tmp/output.txt'); -const result = await sandbox.readFile('/tmp/output.txt'); - -// Option 2: Stream and process incrementally -const stream = await sandbox.execStream('python generate-data.py'); -let processedCount = 0; - -for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout') { - // Process each line immediately - processLine(event.data); - processedCount++; - } -} - -console.log(`Processed ${processedCount} lines`); ``` diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx index dc19ee5a995042f..1fdb9ebe6b8c417 100644 --- a/src/content/docs/sandbox/guides/expose-services.mdx +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -26,11 +26,11 @@ Expose ports when you need to: ## Basic port exposure -The typical workflow is: start service → wait for ready → expose port. +The typical workflow is: start service → wait for ready → expose port → handle requests with `proxyToSandbox`. ``` -import { getSandbox } from '@cloudflare/sandbox'; +import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox'; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); @@ -43,12 +43,24 @@ await new Promise(resolve => setTimeout(resolve, 2000)); // 3. Expose the port const exposed = await sandbox.exposePort(8000); -// 4. Use the preview URL +// 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: @@ -56,12 +68,12 @@ When exposing multiple ports, use names to stay organized: ``` // Start and expose API server -await sandbox.startProcess('node api.js'); +await sandbox.startProcess('node api.js', { env: { PORT: '8080' } }); await new Promise(resolve => setTimeout(resolve, 2000)); -const api = await sandbox.exposePort(3000, { name: 'api' }); +const api = await sandbox.exposePort(8080, { name: 'api' }); // Start and expose frontend -await sandbox.startProcess('npm run dev'); +await sandbox.startProcess('npm run dev', { env: { PORT: '5173' } }); await new Promise(resolve => setTimeout(resolve, 2000)); const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); @@ -73,158 +85,67 @@ console.log('- Frontend:', frontend.exposedAt); ## Wait for service readiness -Always verify a service is ready before exposing: - -### Simple delay +Always verify a service is ready before exposing. Use a simple delay for most cases: ``` // Start service -await sandbox.startProcess('npm run dev'); +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(3000); +await sandbox.exposePort(8080); ``` -### Monitor logs for ready signal +For critical services, poll the health endpoint: ``` -import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; - -// Start service -const process = await sandbox.startProcess('node server.js'); +await sandbox.startProcess('node api-server.js', { env: { PORT: '8080' } }); -// Watch logs for ready message -const logs = await sandbox.streamProcessLogs(process.id); -let serverReady = false; +// Wait for health check +for (let i = 0; i < 10; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); -for await (const log of parseSSEStream(logs)) { - console.log(log.data); - - if (log.data.includes('Server listening') || log.data.includes('ready')) { - serverReady = true; + const check = await sandbox.exec('curl -f http://localhost:8080/health || echo "not ready"'); + if (check.stdout.includes('ok')) { break; } } -if (serverReady) { - const exposed = await sandbox.exposePort(3000); - console.log('Server ready:', exposed.exposedAt); -} -``` - - -### Health check polling - - -``` -async function waitForHealthCheck(port: number, maxRetries: number = 10) { - for (let i = 0; i < maxRetries; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Try to reach health endpoint - const check = await sandbox.exec( - `curl -f http://localhost:${port}/health || echo "not ready"` - ); - - if (check.stdout.includes('ok') || check.success) { - console.log('Service is healthy'); - return true; - } - - console.log(`Waiting for service... (${i + 1}/${maxRetries})`); - } - - throw new Error('Service failed to become ready'); -} - -// Usage -await sandbox.startProcess('node api-server.js'); -await waitForHealthCheck(3000); -await sandbox.exposePort(3000); +await sandbox.exposePort(8080); ``` -## Expose complete dev environments - -### Clone, build, and expose - - -``` -async function setupDevEnvironment(repoUrl: string) { - // Clone repository - await sandbox.gitCheckout(repoUrl); - - // Extract repo name - const repoName = repoUrl.split('/').pop()?.replace('.git', '') || 'repo'; - - // Install dependencies - await sandbox.exec(`cd ${repoName} && npm install`); - - // Start dev server - await sandbox.startProcess(`cd ${repoName} && npm run dev`); - - // Wait for server - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Expose port - const exposed = await sandbox.exposePort(3000, { - name: 'dev-server' - }); - - return { - repository: repoUrl, - url: exposed.exposedAt - }; -} - -// Usage -const env = await setupDevEnvironment('https://github.com/user/my-app'); -console.log('Dev environment ready:', env.url); -``` - +## Multiple services -### Full-stack application +Expose multiple ports for full-stack applications: ``` -// Start database -await sandbox.startProcess('redis-server --port 6379'); -await new Promise(resolve => setTimeout(resolve, 1000)); - -// Start backend API +// Start backend await sandbox.startProcess('node api/server.js', { - env: { - DATABASE_URL: 'redis://localhost:6379', - PORT: '3000' - } + env: { PORT: '8080' } }); await new Promise(resolve => setTimeout(resolve, 2000)); // Start frontend await sandbox.startProcess('npm run dev', { cwd: '/workspace/frontend', - env: { - API_URL: 'http://localhost:3000', - PORT: '5173' - } + env: { PORT: '5173', API_URL: 'http://localhost:8080' } }); await new Promise(resolve => setTimeout(resolve, 3000)); -// Expose both services -const api = await sandbox.exposePort(3000, { name: 'api' }); +// Expose both +const api = await sandbox.exposePort(8080, { name: 'api' }); const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); return Response.json({ - services: { - api: api.exposedAt, - frontend: frontend.exposedAt - } + api: api.exposedAt, + frontend: frontend.exposedAt }); ``` @@ -248,399 +169,105 @@ for (const port of ports) { ``` -### Check if port is already exposed - - -``` -const { ports } = await sandbox.getExposedPorts(); -const isExposed = ports.some(p => p.port === 3000); - -if (isExposed) { - console.log('Port 3000 is already exposed'); - const existing = ports.find(p => p.port === 3000); - console.log('URL:', existing?.exposedAt); -} else { - await sandbox.exposePort(3000); -} -``` - - ### Unexpose ports ``` // Unexpose a single port await sandbox.unexposePort(8000); -console.log('Port 8000 is no longer accessible'); // Unexpose multiple ports -const portsToClose = [3000, 5173, 8080]; -for (const port of portsToClose) { +for (const port of [3000, 5173, 8080]) { await sandbox.unexposePort(port); } - -// Clean up with error handling -async function safeUnexposePort(port: number) { - try { - await sandbox.unexposePort(port); - console.log(`Port ${port} unexposed`); - } catch (error) { - console.log(`Port ${port} was not exposed`); - } -} -``` - - -## Temporary preview URLs - -Create preview URLs that automatically clean up: - - -``` -async function createTemporaryPreview( - command: string, - port: number, - durationMinutes: number = 5 -) { - // Start service - const process = await sandbox.startProcess(command); - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Expose port - const exposed = await sandbox.exposePort(port, { - name: 'temporary-preview' - }); - - console.log(`Preview URL (expires in ${durationMinutes}min):`, exposed.exposedAt); - - // Schedule cleanup - setTimeout(async () => { - await sandbox.killProcess(process.id); - await sandbox.unexposePort(port); - console.log('Temporary preview cleaned up'); - }, durationMinutes * 60 * 1000); - - return exposed.exposedAt; -} - -// Usage -const previewUrl = await createTemporaryPreview( - 'npm run preview', - 4173, - 10 // 10 minutes -); -``` - - -## Microservices architecture - -Run and expose multiple services simultaneously: - - -``` -interface Service { - name: string; - command: string; - port: number; - cwd?: string; - env?: Record; -} - -const services: Service[] = [ - { - name: 'api', - command: 'node server.js', - port: 3000, - cwd: '/workspace/api', - env: { NODE_ENV: 'development' } - }, - { - name: 'auth', - command: 'node auth-server.js', - port: 3001, - cwd: '/workspace/auth' - }, - { - name: 'websocket', - command: 'node ws-server.js', - port: 3002, - cwd: '/workspace/websocket' - }, - { - name: 'admin', - command: 'python app.py', - port: 8080, - cwd: '/workspace/admin', - env: { FLASK_ENV: 'development' } - } -]; - -// Start and expose all services -const exposedServices = []; - -for (const service of services) { - // Start process - const process = await sandbox.startProcess(service.command, { - cwd: service.cwd, - env: service.env - }); - - console.log(`Started ${service.name}`); - - // Wait briefly - await new Promise(resolve => setTimeout(resolve, 1500)); - - // Expose port - const exposed = await sandbox.exposePort(service.port, { - name: service.name - }); - - exposedServices.push({ - name: service.name, - url: exposed.exposedAt, - port: service.port, - processId: process.id - }); -} - -return Response.json({ - message: 'All services running', - services: exposedServices -}); ``` ## Best practices -### Service startup - -- **Always wait** - Don't expose ports immediately after starting processes -- **Monitor logs** - Watch for ready signals before exposing -- **Use health checks** - Poll health endpoints to verify readiness -- **Handle failures** - Check process status if service doesn't start - -### Port management - +- **Wait for readiness** - Don't expose ports immediately after starting processes - **Use named ports** - Easier to track when exposing multiple ports -- **Check before exposing** - Avoid errors by checking if port is already exposed - **Clean up** - Unexpose ports when done to prevent abandoned URLs -- **Use standard ports** - Stick to common ports (3000, 8000, 8080) for familiarity - -### Security - - **Add authentication** - Preview URLs are public; protect sensitive services -- **Limit lifetime** - Close preview URLs for temporary demos -- **Validate origins** - Check request origins in your service -- **Use HTTPS** - Preview URLs use HTTPS automatically -### Performance +## Local development -- **Minimize exposed ports** - Only expose what you need -- **Reuse ports** - Check for existing exposures before creating new ones -- **Clean up on errors** - Use try/finally to ensure cleanup -- **Monitor usage** - Track which services are actually being accessed +When developing locally with `wrangler dev`, you must expose ports in your Dockerfile: -## Common issues +```dockerfile title="Dockerfile" +FROM docker.io/cloudflare/sandbox:0.3.3 -### Port not ready - -**Problem**: Exposing fails because service isn't listening yet. - -**Solution**: Wait longer or check logs for ready signal: - - +# Expose ports you plan to use +EXPOSE 8000 +EXPOSE 8080 +EXPOSE 5173 ``` -// ❌ Too fast -await sandbox.startProcess('npm run dev'); -await sandbox.exposePort(3000); // May fail -// ✅ Wait for service -await sandbox.startProcess('npm run dev'); -await new Promise(resolve => setTimeout(resolve, 3000)); -await sandbox.exposePort(3000); +Update `wrangler.jsonc` to use your Dockerfile: -// ✅ Even better - check logs -const process = await sandbox.startProcess('npm run dev'); -const logs = await sandbox.streamProcessLogs(process.id); - -for await (const log of parseSSEStream(logs)) { - if (log.data.includes('ready')) { - break; - } +```jsonc title="wrangler.jsonc" +{ + "containers": [ + { + "class_name": "Sandbox", + "image": "./Dockerfile" + } + ] } - -await sandbox.exposePort(3000); ``` - -### Port already exposed +In production, all ports are available and controlled programmatically via `exposePort()` / `unexposePort()`. + +## Troubleshooting -**Problem**: Trying to expose a port that's already exposed. +### Port 3000 is reserved -**Solution**: Check first or handle the error: +Port 3000 is used by the internal Bun server and cannot be exposed: ``` -// Option 1: Check before exposing -const { ports } = await sandbox.getExposedPorts(); -if (!ports.some(p => p.port === 3000)) { - await sandbox.exposePort(3000); -} +// ❌ This will fail +await sandbox.exposePort(3000); // Error: Port 3000 is reserved -// Option 2: Handle error -try { - await sandbox.exposePort(3000); -} catch (error) { - if (error.message.includes('already exposed')) { - const { ports } = await sandbox.getExposedPorts(); - const existing = ports.find(p => p.port === 3000); - console.log('Using existing URL:', existing?.exposedAt); - } else { - throw error; - } -} +// ✅ Use a different port +await sandbox.startProcess('node server.js', { env: { PORT: '8080' } }); +await sandbox.exposePort(8080); ``` -### Service crashes after exposure - -**Problem**: Service starts but crashes shortly after. +### Port not ready -**Solution**: Monitor process status and logs: +Wait for the service to start before exposing: ``` -const process = await sandbox.startProcess('node server.js'); -await new Promise(resolve => setTimeout(resolve, 2000)); -await sandbox.exposePort(3000); - -// Monitor process health -setInterval(async () => { - const processes = await sandbox.listProcesses(); - const running = processes.find(p => p.id === process.id); - - if (!running) { - console.error('Service crashed!'); - - // Get logs to see why - const logs = await sandbox.getProcessLogs(process.id); - console.error('Crash logs:', logs); - - // Clean up exposed port - await sandbox.unexposePort(3000); - - // Optionally restart - const newProcess = await sandbox.startProcess('node server.js'); - await new Promise(resolve => setTimeout(resolve, 2000)); - await sandbox.exposePort(3000); - } -}, 10000); // Check every 10 seconds +await sandbox.startProcess('npm run dev'); +await new Promise(resolve => setTimeout(resolve, 3000)); +await sandbox.exposePort(8080); ``` -### Preview URL not accessible - -**Problem**: Preview URL returns errors or timeouts. +### Port already exposed -**Solution**: Verify service is actually listening: +Check before exposing to avoid errors: ``` -// After exposing, test the service locally first -await sandbox.exposePort(3000); - -// Test from inside the sandbox -const check = await sandbox.exec('curl http://localhost:3000'); - -if (!check.success) { - console.error('Service not responding on port 3000'); - console.error(check.stderr); - - // Check what's running - const processes = await sandbox.listProcesses(); - console.log('Running processes:', processes); - - // Check if port is actually in use - const portCheck = await sandbox.exec('lsof -i :3000 || echo "Port not in use"'); - console.log(portCheck.stdout); +const { ports } = await sandbox.getExposedPorts(); +if (!ports.some(p => p.port === 8080)) { + await sandbox.exposePort(8080); } ``` ## Preview URL format -Preview URLs follow this pattern: +Preview URLs follow the pattern `https://{sandbox-id}-{port}.sandbox.workers.dev`: -``` -https://{sandbox-id}-{port}.sandbox.workers.dev -``` - -Examples: -- Port 3000: `https://abc123-3000.sandbox.workers.dev` - Port 8080: `https://abc123-8080.sandbox.workers.dev` +- Port 5173: `https://abc123-5173.sandbox.workers.dev` -The URL remains stable for the lifetime of the exposed port. - -## Security considerations - -### Public accessibility - -:::caution -Exposed ports are publicly accessible. Anyone with the URL can access your service. -::: - - -``` -// Add authentication to your service -await sandbox.writeFile('/workspace/protected-api.js', ` -const express = require('express'); -const app = express(); - -// Simple token authentication -app.use((req, res, next) => { - const token = req.headers.authorization; - - if (token !== 'Bearer secret-token-123') { - return res.status(401).json({ error: 'Unauthorized' }); - } - - next(); -}); - -// Protected endpoints -app.get('/data', (req, res) => { - res.json({ message: 'Protected data', user: 'authenticated' }); -}); - -app.listen(3000); -`); - -await sandbox.startProcess('node protected-api.js'); -await new Promise(resolve => setTimeout(resolve, 2000)); -await sandbox.exposePort(3000); -``` - - -### Temporary access - -For demos or testing, clean up immediately after use: - - -``` -try { - // Expose for testing - const exposed = await sandbox.exposePort(3000); - - // Run tests or demo - await runTests(exposed.exposedAt); - -} finally { - // Always clean up - await sandbox.unexposePort(3000); - console.log('Preview URL closed'); -} -``` - +**Note**: Port 3000 is reserved for the internal Bun server and cannot be exposed. ## Related resources diff --git a/src/content/docs/sandbox/guides/git-workflows.mdx b/src/content/docs/sandbox/guides/git-workflows.mdx index 5ec52df0e73d326..a89c02945ca8833 100644 --- a/src/content/docs/sandbox/guides/git-workflows.mdx +++ b/src/content/docs/sandbox/guides/git-workflows.mdx @@ -16,70 +16,38 @@ This guide shows you how to clone repositories, manage branches, and automate Gi ## Clone repositories -### Basic clone - ``` import { getSandbox } from '@cloudflare/sandbox'; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); -// Clone a public repository +// Basic clone await sandbox.gitCheckout('https://github.com/user/repo'); -console.log('Repository cloned to /workspace/repo'); -``` - - -### Clone to specific directory - - -``` -// Clone to custom location -await sandbox.gitCheckout('https://github.com/user/my-app', { - targetDir: '/workspace/project' -}); - -console.log('Repository cloned to /workspace/project'); -``` - - -### Clone specific branch - - -``` -// Clone a specific branch +// Clone specific branch await sandbox.gitCheckout('https://github.com/user/repo', { - branch: 'develop', - targetDir: '/workspace/dev-branch' + branch: 'develop' }); -console.log('Cloned develop branch'); -``` - - -### Shallow clone - -For faster cloning of large repositories: - - -``` -// Clone only the latest commit +// Shallow clone (faster for large repos) await sandbox.gitCheckout('https://github.com/user/large-repo', { depth: 1 }); -console.log('Shallow clone completed'); +// Clone to specific directory +await sandbox.gitCheckout('https://github.com/user/my-app', { + targetDir: '/workspace/project' +}); ``` ## Clone private repositories -### Using personal access token +Use a personal access token in the URL: ``` -// GitHub personal access token const token = env.GITHUB_TOKEN; const repoUrl = `https://${token}@github.com/user/private-repo.git`; @@ -87,479 +55,88 @@ await sandbox.gitCheckout(repoUrl); ``` -### Using SSH key - - -``` -// Set up SSH key -await sandbox.writeFile('/root/.ssh/id_rsa', env.SSH_PRIVATE_KEY, { - mode: 0o600 -}); - -await sandbox.writeFile('/root/.ssh/config', ` -Host github.com - StrictHostKeyChecking no - UserKnownHostsFile=/dev/null -`); - -// Clone via SSH -await sandbox.gitCheckout('git@github.com:user/private-repo.git'); -``` - - ## Clone and build -Complete workflow for cloning and building projects: +Clone a repository and run build steps: ``` -async function cloneAndBuild(repoUrl: string) { - // Clone repository - await sandbox.gitCheckout(repoUrl); - - // Extract repo name - const repoName = repoUrl.split('/').pop()?.replace('.git', '') || 'repo'; - - // Install dependencies - console.log('Installing dependencies...'); - const install = await sandbox.exec(`cd ${repoName} && npm install`); +await sandbox.gitCheckout('https://github.com/user/my-app'); - if (!install.success) { - throw new Error(`Install failed: ${install.stderr}`); - } +const repoName = 'my-app'; - // Run build - console.log('Building project...'); - const build = await sandbox.exec(`cd ${repoName} && npm run build`); +// Install and build +await sandbox.exec(`cd ${repoName} && npm install`); +await sandbox.exec(`cd ${repoName} && npm run build`); - if (!build.success) { - throw new Error(`Build failed: ${build.stderr}`); - } - - console.log('Build complete'); - - return { - repository: repoUrl, - directory: `/workspace/${repoName}`, - buildOutput: build.stdout - }; -} - -// Usage -const result = await cloneAndBuild('https://github.com/user/my-app'); -console.log('Project ready at:', result.directory); +console.log('Build complete'); ``` ## Work with branches -### List branches - ``` await sandbox.gitCheckout('https://github.com/user/repo'); -// List all branches -const branches = await sandbox.exec('cd repo && git branch -a'); -console.log('Branches:', branches.stdout); -``` - - -### Switch branches - - -``` -// Clone repository -await sandbox.gitCheckout('https://github.com/user/repo'); - -// Switch to different branch +// Switch branches await sandbox.exec('cd repo && git checkout feature-branch'); -// Or fetch and checkout remote branch -await sandbox.exec('cd repo && git fetch origin && git checkout -b local-branch origin/remote-branch'); -``` - - -### Create and switch to new branch - - -``` -await sandbox.gitCheckout('https://github.com/user/repo'); - // Create new branch await sandbox.exec('cd repo && git checkout -b new-feature'); - -console.log('Created and switched to new-feature branch'); ``` -## Make changes - -### Modify files and commit +## Make changes and commit ``` -// Clone repository await sandbox.gitCheckout('https://github.com/user/repo'); -// Read and modify file +// Modify a file const readme = await sandbox.readFile('/workspace/repo/README.md'); -const updated = readme.content + '\\n\\n## New Section\\n\\nAdded via Sandbox SDK'; - -await sandbox.writeFile('/workspace/repo/README.md', updated); +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"'); - -console.log('Changes committed'); -``` - - -### Create pull request - - -``` -// Make changes and commit -await sandbox.exec('cd repo && git checkout -b update-docs'); -// ... make changes ... -await sandbox.exec('cd repo && git add . && git commit -m "Update documentation"'); - -// Push to remote -await sandbox.exec(`cd repo && git push https://${env.GITHUB_TOKEN}@github.com/user/repo.git update-docs`); - -// Create PR using GitHub API -const pr = await fetch('https://api.github.com/repos/user/repo/pulls', { - method: 'POST', - headers: { - 'Authorization': `token ${env.GITHUB_TOKEN}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - title: 'Update documentation', - head: 'update-docs', - base: 'main', - body: 'Automated documentation updates' - }) -}); - -const prData = await pr.json(); -console.log('Pull request created:', prData.html_url); -``` - - -## Automated workflows - -### Clone multiple repositories - - -``` -const repositories = [ - 'https://github.com/user/frontend', - 'https://github.com/user/backend', - 'https://github.com/user/shared' -]; - -// Clone all repositories in parallel -await Promise.all( - repositories.map(repo => sandbox.gitCheckout(repo)) -); - -console.log('All repositories cloned'); -``` - - -### Sync repository changes - - -``` -async function syncRepository(repoPath: string) { - // Fetch latest changes - const fetch = await sandbox.exec(`cd ${repoPath} && git fetch origin`); - - if (!fetch.success) { - throw new Error('Failed to fetch updates'); - } - - // Check if behind remote - const status = await sandbox.exec(`cd ${repoPath} && git status -uno`); - - if (status.stdout.includes('behind')) { - // Pull changes - const pull = await sandbox.exec(`cd ${repoPath} && git pull origin main`); - - if (pull.success) { - console.log('Repository updated'); - return { updated: true, changes: pull.stdout }; - } else { - throw new Error(`Pull failed: ${pull.stderr}`); - } - } - - console.log('Repository up to date'); - return { updated: false }; -} - -// Usage -const result = await syncRepository('/workspace/my-repo'); -if (result.updated) { - console.log('Changes pulled:', result.changes); -} -``` - - -### Monorepo workflows - - -``` -async function setupMonorepo(repoUrl: string) { - // Clone monorepo - await sandbox.gitCheckout(repoUrl, { - targetDir: '/workspace/monorepo' - }); - - // List packages - const packages = await sandbox.exec('cd monorepo && ls -1 packages/'); - const packageList = packages.stdout.trim().split('\\n'); - - console.log(`Found ${packageList.length} packages:`, packageList); - - // Install dependencies for all packages - await sandbox.exec('cd monorepo && npm install'); - - // Build all packages - const build = await sandbox.exec('cd monorepo && npm run build --workspaces'); - - if (!build.success) { - throw new Error(`Monorepo build failed: ${build.stderr}`); - } - - return { - packages: packageList, - buildOutput: build.stdout - }; -} - -// Usage -const monorepo = await setupMonorepo('https://github.com/user/monorepo'); -console.log('Monorepo packages:', monorepo.packages); -``` - - -## CI/CD integration - -### Run tests on clone - - -``` -async function cloneAndTest(repoUrl: string, branch: string = 'main') { - // Clone specific branch - await sandbox.gitCheckout(repoUrl, { branch }); - - const repoName = repoUrl.split('/').pop()?.replace('.git', ''); - - // Install dependencies - await sandbox.exec(`cd ${repoName} && npm install`); - - // Run tests - const test = await sandbox.exec(`cd ${repoName} && npm test`); - - return { - success: test.success, - output: test.stdout, - errors: test.stderr, - exitCode: test.exitCode - }; -} - -// Usage in CI webhook -export default { - async fetch(request: Request, env: Env): Promise { - const webhook = await request.json(); - - if (webhook.ref === 'refs/heads/main') { - const sandbox = getSandbox(env.Sandbox, `test-${webhook.after}`); - - const results = await cloneAndTest( - webhook.repository.clone_url, - 'main' - ); - - // Report results - await reportToGitHub(webhook, results); - - return Response.json(results); - } - - return new Response('OK', { status: 200 }); - } -}; -``` - - -### Automated deployment - - -``` -async function deployFromGit(repoUrl: string, branch: string = 'main') { - // Clone repository - await sandbox.gitCheckout(repoUrl, { branch }); - - const repoName = repoUrl.split('/').pop()?.replace('.git', ''); - - // Install and build - await sandbox.exec(`cd ${repoName} && npm install && npm run build`); - - // Start application - const process = await sandbox.startProcess(`cd ${repoName} && npm start`); - - // Wait for server to start - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Expose port - const exposed = await sandbox.exposePort(3000, { - name: 'deployment' - }); - - return { - status: 'deployed', - url: exposed.exposedAt, - processId: process.id, - branch - }; -} - -// Usage -const deployment = await deployFromGit( - 'https://github.com/user/app', - 'production' -); - -console.log('Deployed at:', deployment.url); ``` ## Best practices -### Repository management - - **Use shallow clones** - Faster for large repos with `depth: 1` -- **Clone to specific directories** - Avoid name conflicts with `targetDir` -- **Clean up old clones** - Delete unused repositories to save space -- **Cache credentials** - Store tokens in environment variables - -### Branch management - -- **Specify branches** - Clone specific branches when possible -- **Check out sparse** - Use sparse checkout for large monorepos -- **Track remote branches** - Keep local branches synced with remote -- **Clean up branches** - Delete merged branches to save resources - -### Security +- **Store credentials securely** - Use environment variables for tokens +- **Clean up** - Delete unused repositories to save space -- **Protect tokens** - Never hardcode tokens in code -- **Use environment variables** - Store credentials securely -- **Limit token scope** - Use minimal permissions needed -- **Rotate tokens regularly** - Update credentials periodically +## Troubleshooting -### Performance +### Authentication fails -- **Shallow clones** - Use `depth: 1` for CI/CD workflows -- **Parallel clones** - Clone multiple repos concurrently -- **Incremental updates** - Pull instead of re-cloning when possible -- **Clean up** - Remove `.git` directory if not needed after clone - -## Common issues - -### Clone fails with authentication - -**Problem**: "Authentication failed" or "Permission denied" errors. - -**Solution**: Check credentials and repository access: +Verify your token is set: ``` -// Verify token has required permissions -const token = env.GITHUB_TOKEN; - -if (!token) { +if (!env.GITHUB_TOKEN) { throw new Error('GITHUB_TOKEN not configured'); } -// Test token with API call -const test = await fetch('https://api.github.com/user', { - headers: { - 'Authorization': `token ${token}` - } -}); - -if (!test.ok) { - throw new Error('Invalid GitHub token'); -} - -// Now try cloning -const repoUrl = `https://${token}@github.com/user/private-repo.git`; +const repoUrl = `https://${env.GITHUB_TOKEN}@github.com/user/private-repo.git`; await sandbox.gitCheckout(repoUrl); ``` ### Large repository timeout -**Problem**: Clone times out for large repositories. - -**Solution**: Use shallow clone or sparse checkout: +Use shallow clone: ``` -// Option 1: Shallow clone (fastest) await sandbox.gitCheckout('https://github.com/user/large-repo', { depth: 1 }); - -// Option 2: Sparse checkout (specific paths only) -await sandbox.exec(` - git clone --filter=blob:none --sparse https://github.com/user/large-repo - cd large-repo - git sparse-checkout set packages/my-package -`); -``` - - -### Merge conflicts - -**Problem**: Pull fails due to merge conflicts. - -**Solution**: Stash changes or hard reset: - - -``` -// Option 1: Stash changes -await sandbox.exec('cd repo && git stash'); -await sandbox.exec('cd repo && git pull origin main'); -await sandbox.exec('cd repo && git stash pop'); - -// Option 2: Hard reset (discards local changes) -await sandbox.exec('cd repo && git fetch origin'); -await sandbox.exec('cd repo && git reset --hard origin/main'); -``` - - -### Submodule issues - -**Problem**: Repository has submodules that aren't cloned. - -**Solution**: Clone with submodules: - - -``` -// Clone with submodules -await sandbox.exec('git clone --recursive https://github.com/user/repo'); - -// Or update submodules after cloning -await sandbox.gitCheckout('https://github.com/user/repo'); -await sandbox.exec('cd repo && git submodule update --init --recursive'); ``` @@ -568,4 +145,3 @@ await sandbox.exec('cd repo && git submodule update --init --recursive'); - [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 -- [CI/CD tutorial](/sandbox/tutorials/build-system/) - Automated testing workflows diff --git a/src/content/docs/sandbox/guides/manage-files.mdx b/src/content/docs/sandbox/guides/manage-files.mdx index 18b5ef959c8b74a..03250824cf909f6 100644 --- a/src/content/docs/sandbox/guides/manage-files.mdx +++ b/src/content/docs/sandbox/guides/manage-files.mdx @@ -16,125 +16,62 @@ This guide shows you how to read, write, organize, and synchronize files in the ## Path conventions -All file operations use absolute paths in the sandbox: +File operations support both absolute and relative paths: -- `/workspace` - Default working directory for application files (recommended) +- `/workspace` - Default working directory for application files - `/tmp` - Temporary files (may be cleared) - `/home` - User home directory ``` -// ✅ Use absolute paths +// Absolute paths await sandbox.writeFile('/workspace/app.js', code); -// ❌ Don't use relative paths -await sandbox.writeFile('app.js', code); // Will fail +// 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 -### Write text files - ``` import { getSandbox } from '@cloudflare/sandbox'; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); -// Write a simple text file -await sandbox.writeFile('/workspace/app.js', ` -console.log('Hello from sandbox!'); -`); - -// Write with specific encoding -await sandbox.writeFile('/workspace/data.txt', content, { - encoding: 'utf-8' -}); -``` - - -### Write JSON configuration - - -``` -const config = { - name: 'my-app', - version: '1.0.0', - environment: 'production', - database: { - host: 'localhost', - port: 5432 - } -}; +// Write text file +await sandbox.writeFile('/workspace/app.js', `console.log('Hello from sandbox!');`); -await sandbox.writeFile( - '/workspace/config.json', - JSON.stringify(config, null, 2) -); -``` - - -### Write binary files - - -``` -// Write base64-encoded image -const imageBase64 = '...'; // Base64 image data - -await sandbox.writeFile('/workspace/logo.png', imageBase64, { - encoding: 'base64' -}); +// Write JSON +const config = { name: 'my-app', version: '1.0.0' }; +await sandbox.writeFile('/workspace/config.json', JSON.stringify(config, null, 2)); -// Write buffer data +// 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' -}); +await sandbox.writeFile('/workspace/image.png', base64, { encoding: 'base64' }); ``` ## Read files -### Read text files - ``` -// Read a file +// Read text file const file = await sandbox.readFile('/workspace/app.js'); console.log(file.content); -console.log(file.encoding); // 'utf-8' -``` - - -### Read and parse JSON - - -``` -const file = await sandbox.readFile('/workspace/package.json'); -const packageJson = JSON.parse(file.content); - -console.log('Project:', packageJson.name); -console.log('Version:', packageJson.version); -console.log('Dependencies:', Object.keys(packageJson.dependencies || {})); -``` - - -### Read binary files - - -``` -const file = await sandbox.readFile('/workspace/image.png', { - encoding: 'base64' -}); -// file.content is base64-encoded -const dataUrl = `data:image/png;base64,${file.content}`; +// Read and parse JSON +const configFile = await sandbox.readFile('/workspace/config.json'); +const config = JSON.parse(configFile.content); -// Or send to client -return new Response(atob(file.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' } }); ``` @@ -142,420 +79,99 @@ return new Response(atob(file.content), { ## Organize files -### Create directories - ``` -// Create single directory -await sandbox.mkdir('/workspace/src'); - -// Create nested directories -await sandbox.mkdir('/workspace/src/components/ui', { - recursive: true -}); - -// Create project structure +// Create directories await sandbox.mkdir('/workspace/src', { recursive: true }); await sandbox.mkdir('/workspace/tests', { recursive: true }); -await sandbox.mkdir('/workspace/public', { recursive: true }); -``` - -### Move and rename files +// Rename file +await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt'); - -``` -// Rename a file -await sandbox.renameFile( - '/workspace/draft.txt', - '/workspace/final.txt' -); +// Move file +await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt'); -// Move to different directory -await sandbox.moveFile( - '/tmp/download.txt', - '/workspace/data.txt' -); - -// Organize files into structure -const files = ['app.js', 'server.js', 'utils.js']; -await sandbox.mkdir('/workspace/src', { recursive: true }); - -for (const file of files) { - await sandbox.moveFile( - `/workspace/${file}`, - `/workspace/src/${file}` - ); -} -``` - - -### Delete files - - -``` -// Delete a single file +// Delete file await sandbox.deleteFile('/workspace/temp.txt'); - -// Clean up multiple files -const tempFiles = [ - '/tmp/build-output.log', - '/tmp/cache.json', - '/workspace/.env.local' -]; - -for (const file of tempFiles) { - try { - await sandbox.deleteFile(file); - } catch (error) { - console.log(`Could not delete ${file}:`, error.message); - } -} -``` - - -## Work with project structures - -### Create a complete project - - -``` -// Create directory structure -await sandbox.mkdir('/workspace/my-app/src', { recursive: true }); -await sandbox.mkdir('/workspace/my-app/tests', { recursive: true }); - -// Create package.json -await sandbox.writeFile('/workspace/my-app/package.json', JSON.stringify({ - name: 'my-app', - version: '1.0.0', - scripts: { - start: 'node src/index.js', - test: 'jest' - }, - dependencies: { - express: '^4.18.0' - } -}, null, 2)); - -// Create main application file -await sandbox.writeFile('/workspace/my-app/src/index.js', ` -const express = require('express'); -const app = express(); - -app.get('/', (req, res) => { - res.json({ status: 'running' }); -}); - -app.listen(3000, () => { - console.log('Server started on port 3000'); -}); -`); - -// Install and run -await sandbox.exec('cd /workspace/my-app && npm install'); -await sandbox.startProcess('node src/index.js', { - cwd: '/workspace/my-app' -}); -``` - - -### Clone from Git and modify - - -``` -// Clone repository -await sandbox.gitCheckout('https://github.com/user/repo', { - targetDir: '/workspace/project' -}); - -// Read and modify configuration -const configFile = await sandbox.readFile('/workspace/project/config.json'); -const config = JSON.parse(configFile.content); - -config.environment = 'development'; -config.apiUrl = 'https://api.example.com'; - -// Write back -await sandbox.writeFile( - '/workspace/project/config.json', - JSON.stringify(config, null, 2) -); - -// Build project -await sandbox.exec('cd /workspace/project && npm install && npm run build'); ``` ## Batch operations -### Write multiple files +Write multiple files in parallel: ``` const files = { '/workspace/src/app.js': 'console.log("app");', '/workspace/src/utils.js': 'console.log("utils");', - '/workspace/src/config.js': 'module.exports = { port: 3000 };', - '/workspace/README.md': '# My Project\n\nA sample project.' + '/workspace/README.md': '# My Project' }; -// Write all files in parallel await Promise.all( Object.entries(files).map(([path, content]) => sandbox.writeFile(path, content) ) ); - -console.log('All files created'); ``` -### Copy files - -The SDK doesn't have a direct copy method, but you can read and write: +## Check if file exists ``` -// Copy a single file -async function copyFile(source: string, destination: string) { - const file = await sandbox.readFile(source); - await sandbox.writeFile(destination, file.content); -} - -await copyFile('/workspace/template.html', '/workspace/index.html'); - -// Copy with transformation -const template = await sandbox.readFile('/workspace/template.html'); -const customized = template.content.replace('{{title}}', 'My App'); -await sandbox.writeFile('/workspace/index.html', customized); -``` - - -## Handle file errors - -### Check if file exists - - -``` -async function fileExists(path: string): Promise { - try { - await sandbox.readFile(path); - return true; - } catch (error) { - if (error.code === 'FILE_NOT_FOUND') { - return false; - } - throw error; // Other errors should still throw - } -} - -// Use it -if (await fileExists('/workspace/config.json')) { - console.log('Config file exists'); -} else { - // Create default config - await sandbox.writeFile('/workspace/config.json', '{}'); -} -``` - - -### Create file with backup - - -``` -async function writeFileWithBackup(path: string, content: string) { - // Check if file exists - try { - await sandbox.readFile(path); - - // File exists - create backup - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupPath = `${path}.${timestamp}.backup`; - - await sandbox.renameFile(path, backupPath); - console.log('Created backup:', backupPath); - - } catch (error) { - if (error.code !== 'FILE_NOT_FOUND') { - throw error; - } - // File doesn't exist - no backup needed +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', '{}'); } - - // Write new file - await sandbox.writeFile(path, content); } - -await writeFileWithBackup('/workspace/config.json', JSON.stringify(newConfig)); ``` ## Best practices -### File organization - -- **Use `/workspace` for app files** - This is the standard working directory -- **Use `/tmp` for temporary files** - These may be cleared periodically -- **Create logical directory structures** - Organize code like a real project -- **Use consistent naming** - Follow conventions (lowercase, hyphens) - -### Performance - -- **Batch file operations** - Use `Promise.all()` for independent writes -- **Avoid reading large files repeatedly** - Cache file contents when possible -- **Use streaming for large files** - Write to file instead of keeping in memory -- **Clean up temporary files** - Don't let `/tmp` accumulate files - -### Security - -- **Validate file paths** - Prevent path traversal attacks -- **Limit file sizes** - Don't write unbounded data -- **Be careful with user content** - Sanitize filenames and content -- **Use appropriate permissions** - Set file modes when needed - -### Reliability +- **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 -- **Handle file errors gracefully** - Files may not exist or be readable -- **Create parent directories** - Use `recursive: true` for nested paths -- **Check operation results** - Verify writes succeeded -- **Back up important files** - Before overwriting +## Troubleshooting -## Common patterns +### Directory doesn't exist -### Template files +Create parent directories first: ``` -// Read template -const template = await sandbox.readFile('/workspace/templates/email.html'); - -// Replace placeholders -const email = template.content - .replace('{{name}}', userName) - .replace('{{date}}', new Date().toISOString()) - .replace('{{content}}', emailContent); - -// Write final file -await sandbox.writeFile('/workspace/output/email.html', email); -``` - - -### Configuration management - - -``` -// Load base config -const baseConfig = await sandbox.readFile('/workspace/config.base.json'); -const config = JSON.parse(baseConfig.content); - -// Override with environment-specific settings -config.environment = 'production'; -config.debug = false; -config.apiUrl = env.API_URL; - -// Write final config -await sandbox.writeFile( - '/workspace/config.json', - JSON.stringify(config, null, 2) -); -``` - - -### Data processing pipeline - - -``` -// 1. Read input data -const input = await sandbox.readFile('/workspace/data/input.json'); -const data = JSON.parse(input.content); - -// 2. Process data -const processed = data.map(item => ({ - id: item.id, - value: item.value * 2, - timestamp: new Date().toISOString() -})); - -// 3. Write output -await sandbox.writeFile( - '/workspace/data/output.json', - JSON.stringify(processed, null, 2) -); - -// 4. Generate report -const report = ` -Processed ${data.length} items -Output: /workspace/data/output.json -Timestamp: ${new Date().toISOString()} -`; - -await sandbox.writeFile('/workspace/reports/report.txt', report); -``` - - -## Common issues - -### Path not found - -**Problem**: "No such file or directory" error. - -**Solution**: Create parent directories first: - - -``` -// ❌ This will fail if /workspace/data doesn't exist -await sandbox.writeFile('/workspace/data/file.txt', content); - -// ✅ Create directory first +// Create directory, then write file await sandbox.mkdir('/workspace/data', { recursive: true }); await sandbox.writeFile('/workspace/data/file.txt', content); ``` -### File encoding issues - -**Problem**: File content appears corrupted or garbled. +### Binary file encoding -**Solution**: Specify correct encoding: +Use base64 for binary files: ``` -// For binary files, use base64 +// Write binary await sandbox.writeFile('/workspace/image.png', base64Data, { encoding: 'base64' }); -// For text files, utf-8 is default -await sandbox.writeFile('/workspace/data.txt', textContent, { - encoding: 'utf-8' +// Read binary +const file = await sandbox.readFile('/workspace/image.png', { + encoding: 'base64' }); ``` -### Large file handling - -**Problem**: Large files cause performance issues or memory errors. - -**Solution**: Process files in chunks or use command-line tools: - - -``` -// Instead of reading entire file -// const file = await sandbox.readFile('/workspace/huge-file.txt'); - -// Use command-line tools to process in chunks -await sandbox.exec('head -n 1000 /workspace/huge-file.txt > /tmp/chunk.txt'); -const chunk = await sandbox.readFile('/tmp/chunk.txt'); - -// Or process line by line with streaming -await sandbox.exec(` - while IFS= read -r line; do - echo "Processing: $line" - done < /workspace/huge-file.txt -`); -``` - - ## Related resources - [Files API reference](/sandbox/api/files/) - Complete method documentation diff --git a/src/content/docs/sandbox/guides/streaming-output.mdx b/src/content/docs/sandbox/guides/streaming-output.mdx index 74029f056a60cc5..d7d22a0a78cefdb 100644 --- a/src/content/docs/sandbox/guides/streaming-output.mdx +++ b/src/content/docs/sandbox/guides/streaming-output.mdx @@ -32,64 +32,32 @@ Use non-streaming (`exec()`) for: ## Stream command execution -### Using exec() with callbacks - -The simplest approach for streaming output: +Use `execStream()` to get real-time output: ``` -import { getSandbox } from '@cloudflare/sandbox'; +import { getSandbox, parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); -// Execute with streaming callbacks -const result = await sandbox.exec('npm install express', { - stream: true, - onOutput: (stream, data) => { - if (stream === 'stdout') { - console.log('OUT:', data); - } else { - console.error('ERR:', data); - } - } -}); - -console.log('Installation complete, exit code:', result.exitCode); -``` - - -### Using execStream() for full control - -For more control over stream handling: - - -``` -import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; - -// Execute and get stream const stream = await sandbox.execStream('npm run build'); -// Process events for await (const event of parseSSEStream(stream)) { switch (event.type) { - case 'start': - console.log('Build started:', event.command); - break; - case 'stdout': - console.log('OUTPUT:', event.data); + console.log(event.data); break; case 'stderr': - console.error('ERROR:', event.data); + console.error(event.data); break; case 'complete': - console.log('Build finished with exit code:', event.exitCode); + console.log('Exit code:', event.exitCode); break; case 'error': - console.error('Execution failed:', event.error); + console.error('Failed:', event.error); break; } } @@ -98,139 +66,44 @@ for await (const event of parseSSEStream(stream)) { ## Stream to client -Return streaming output directly to users: +Return streaming output to users via Server-Sent Events: ``` export default { async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); + const sandbox = getSandbox(env.Sandbox, 'builder'); - if (url.pathname === '/build') { - const sandbox = getSandbox(env.Sandbox, 'builder'); - - // Get stream from command - const stream = await sandbox.execStream('npm run build'); - - // Return stream directly to client - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive' - } - }); - } + const stream = await sandbox.execStream('npm run build'); - return new Response('Not found', { status: 404 }); + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache' + } + }); } }; ``` -### Client-side consumption +Client-side consumption: ``` -// Browser JavaScript to consume SSE stream +// Browser JavaScript const eventSource = new EventSource('/build'); eventSource.addEventListener('stdout', (event) => { const data = JSON.parse(event.data); - console.log('Build output:', data.data); - document.getElementById('output').textContent += data.data + '\\n'; + console.log(data.data); }); eventSource.addEventListener('complete', (event) => { const data = JSON.parse(event.data); - console.log('Build complete, exit code:', data.exitCode); + console.log('Exit code:', data.exitCode); eventSource.close(); }); - -eventSource.addEventListener('error', (event) => { - console.error('Stream error:', event); - eventSource.close(); -}); -``` - - -## Track progress - -### Parse output for progress indicators - - -``` -const stream = await sandbox.execStream('npm test'); - -let testsRun = 0; -let testsPassed = 0; -let testsFailed = 0; - -for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout') { - // Count test results - if (event.data.includes('✓')) { - testsPassed++; - testsRun++; - console.log(`Tests: ${testsPassed} passed, ${testsFailed} failed (${testsRun} total)`); - } else if (event.data.includes('✗')) { - testsFailed++; - testsRun++; - console.log(`Tests: ${testsPassed} passed, ${testsFailed} failed (${testsRun} total)`); - } - } - - if (event.type === 'complete') { - console.log(`Test run complete: ${testsPassed}/${testsRun} passed`); - } -} -``` - - -### Real-time progress updates - - -``` -export default { - async fetch(request: Request, env: Env): Promise { - const sandbox = getSandbox(env.Sandbox, 'build-system'); - - // Start build with streaming - const stream = await sandbox.execStream('npm run build'); - - // Transform stream to add progress info - const transformStream = new TransformStream({ - transform(chunk, controller) { - // Parse SSE event - const text = new TextDecoder().decode(chunk); - - if (text.includes('Building')) { - controller.enqueue( - new TextEncoder().encode( - `data: ${JSON.stringify({ type: 'progress', percent: 25 })}\\n\\n` - ) - ); - } else if (text.includes('Optimizing')) { - controller.enqueue( - new TextEncoder().encode( - `data: ${JSON.stringify({ type: 'progress', percent: 75 })}\\n\\n` - ) - ); - } - - // Pass through original - controller.enqueue(chunk); - } - }); - - return new Response(stream.pipeThrough(transformStream), { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache' - } - }); - } -}; ``` @@ -242,178 +115,43 @@ Monitor background process output: ``` import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox'; -// Start background process const process = await sandbox.startProcess('node server.js'); -// Stream logs in real-time const logStream = await sandbox.streamProcessLogs(process.id); for await (const log of parseSSEStream(logStream)) { - console.log(`[${log.timestamp}] ${log.data}`); + console.log(log.data); - // React to specific log messages - if (log.data.includes('Server listening on port')) { + if (log.data.includes('Server listening')) { console.log('Server is ready'); - // Expose port now that server is ready - await sandbox.exposePort(3000); break; } - - if (log.data.includes('ERROR') || log.data.includes('FATAL')) { - console.error('Server error detected:', log.data); - // Handle error - } } ``` -## Stream code execution +## Handle errors -Stream output from code interpreter: +Check exit codes and handle stream errors: ``` -// Create code context -const context = await sandbox.createCodeContext({ - language: 'python' -}); - -// Execute with streaming -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("Complete!") -`, - { - stream: true, - onOutput: (data) => { - console.log('Output:', data); - // Send to client via WebSocket or SSE - }, - onResult: (result) => { - console.log('Execution complete'); - console.log('Outputs:', result.outputs); - }, - onError: (error) => { - console.error('Execution error:', error); - } - } -); - -console.log('Final result:', result); -``` - - -## Buffer and batch output - -For high-frequency output, buffer updates: - - -``` -const stream = await sandbox.execStream('npm test -- --verbose'); - -let buffer = ''; -let lastUpdate = Date.now(); -const UPDATE_INTERVAL = 100; // ms - -for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout' || event.type === 'stderr') { - buffer += event.data; - - // Batch updates every 100ms - if (Date.now() - lastUpdate > UPDATE_INTERVAL) { - console.log(buffer); - buffer = ''; - lastUpdate = Date.now(); - } - } - - if (event.type === 'complete') { - // Flush remaining buffer - if (buffer) { - console.log(buffer); - } - console.log('Complete'); - } -} -``` - - -## Filter and transform output - -Process streaming output selectively: - - -``` -const stream = await sandbox.execStream('npm install'); +const stream = await sandbox.execStream('npm run build'); for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout') { - // Filter out verbose npm output - if (event.data.includes('http fetch') || event.data.includes('timing')) { - continue; // Skip verbose messages - } - - // Transform output - const cleaned = event.data - .replace(/\u001b\[[0-9;]*m/g, '') // Remove ANSI colors - .trim(); - - if (cleaned) { - console.log(cleaned); - } - } else if (event.type === 'stderr') { - // Always show errors - console.error('ERROR:', event.data); - } -} -``` - + switch (event.type) { + case 'stdout': + console.log(event.data); + break; -## Handle streaming errors + case 'error': + throw new Error(`Build failed: ${event.error}`); - -``` -try { - 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 'error': - // Stream error occurred - throw new Error(`Build failed: ${event.error}`); - - case 'complete': - if (event.exitCode !== 0) { - throw new Error(`Build failed with exit code ${event.exitCode}`); - } - console.log('Build succeeded'); - break; - } - } -} catch (error) { - console.error('Streaming failed:', error); - - // Get accumulated logs if stream failed - try { - const logs = await sandbox.getProcessLogs(processId); - console.error('Full logs:', logs); - } catch { - // Ignore if can't get logs + case 'complete': + if (event.exitCode !== 0) { + throw new Error(`Build failed with exit code ${event.exitCode}`); + } + break; } } ``` @@ -421,106 +159,10 @@ try { ## Best practices -### Stream handling - - **Always consume streams** - Don't let streams hang unconsumed -- **Handle all event types** - Process start, output, complete, and error events -- **Close connections** - Clean up streams when done -- **Use timeouts** - Set limits for long-running streams - -### Performance - -- **Buffer high-frequency output** - Batch updates to reduce overhead -- **Filter unnecessary output** - Skip verbose messages early -- **Use backpressure** - Let streams control flow -- **Clean up resources** - Close streams promptly - -### User experience - -- **Show progress indicators** - Parse output for progress info -- **Provide feedback** - Show something immediately, even if just "Starting..." -- **Handle errors gracefully** - Show clear error messages -- **Allow cancellation** - Let users stop long operations - -### Error handling - -- **Check exit codes** - Non-zero means failure -- **Parse stderr** - Error details usually in stderr -- **Save full output** - Keep complete logs for debugging -- **Retry on failures** - Handle transient stream errors - -## Common patterns - -### Build pipeline with progress - - -``` -async function buildWithProgress(sandbox: Sandbox) { - const stages = [ - { name: 'Installing dependencies', command: 'npm install' }, - { name: 'Running tests', command: 'npm test' }, - { name: 'Building project', command: 'npm run build' }, - { name: 'Running linter', command: 'npm run lint' } - ]; - - for (let i = 0; i < stages.length; i++) { - const stage = stages[i]; - const progress = ((i + 1) / stages.length) * 100; - - console.log(`[${progress.toFixed(0)}%] ${stage.name}...`); - - const result = await sandbox.exec(stage.command, { - stream: true, - onOutput: (stream, data) => { - console.log(` ${data}`); - } - }); - - if (!result.success) { - throw new Error(`${stage.name} failed: ${result.stderr}`); - } - } - - console.log('[100%] Build pipeline complete'); -} -``` - - -### Real-time log viewer - - -``` -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - if (url.pathname === '/logs') { - const processId = url.searchParams.get('process'); - - if (!processId) { - return new Response('Missing process ID', { status: 400 }); - } - - const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); - - // Stream logs to client - const logStream = await sandbox.streamProcessLogs(processId); - - return new Response(logStream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'X-Accel-Buffering': 'no' // Disable nginx buffering - } - }); - } - - return new Response('Not found', { status: 404 }); - } -}; -``` - +- **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 From 7ae09f25f4ad450beefd8cf57475f131860f6dff Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 16:30:34 +0100 Subject: [PATCH 14/22] Remove unnecessary prerequisites --- src/content/docs/sandbox/guides/background-processes.mdx | 5 ----- src/content/docs/sandbox/guides/code-execution.mdx | 5 ----- src/content/docs/sandbox/guides/execute-commands.mdx | 5 ----- src/content/docs/sandbox/guides/expose-services.mdx | 5 ----- src/content/docs/sandbox/guides/git-workflows.mdx | 5 ----- src/content/docs/sandbox/guides/manage-files.mdx | 5 ----- src/content/docs/sandbox/guides/streaming-output.mdx | 5 ----- 7 files changed, 35 deletions(-) diff --git a/src/content/docs/sandbox/guides/background-processes.mdx b/src/content/docs/sandbox/guides/background-processes.mdx index 92836feb15f1ae8..eabf973ebbbf997 100644 --- a/src/content/docs/sandbox/guides/background-processes.mdx +++ b/src/content/docs/sandbox/guides/background-processes.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to start, monitor, and manage long-running background processes in the sandbox. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Understanding of process management concepts - ## When to use background processes Use `startProcess()` instead of `exec()` when: diff --git a/src/content/docs/sandbox/guides/code-execution.mdx b/src/content/docs/sandbox/guides/code-execution.mdx index 624605bbc0e26cf..b7bc8cfb289421b 100644 --- a/src/content/docs/sandbox/guides/code-execution.mdx +++ b/src/content/docs/sandbox/guides/code-execution.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to execute Python and JavaScript code with rich outputs using the Code Interpreter API. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Understanding of when to use code interpreter vs direct command execution - ## When to use code interpreter Use the Code Interpreter API for **simple, direct code execution** with minimal setup: diff --git a/src/content/docs/sandbox/guides/execute-commands.mdx b/src/content/docs/sandbox/guides/execute-commands.mdx index 4108f71722afce3..8564ae320474638 100644 --- a/src/content/docs/sandbox/guides/execute-commands.mdx +++ b/src/content/docs/sandbox/guides/execute-commands.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to execute commands in the sandbox, handle output, and manage errors effectively. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Familiar with async/await patterns - ## Choose the right method The SDK provides two methods for command execution: diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx index 1fdb9ebe6b8c417..2616357a1c21460 100644 --- a/src/content/docs/sandbox/guides/expose-services.mdx +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to expose services running in your sandbox to the internet via preview URLs. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Service or application running on a port inside the sandbox - ## When to expose ports Expose ports when you need to: diff --git a/src/content/docs/sandbox/guides/git-workflows.mdx b/src/content/docs/sandbox/guides/git-workflows.mdx index a89c02945ca8833..87656296362a795 100644 --- a/src/content/docs/sandbox/guides/git-workflows.mdx +++ b/src/content/docs/sandbox/guides/git-workflows.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to clone repositories, manage branches, and automate Git operations in the sandbox. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Git repository URLs (public or with authentication) - ## Clone repositories diff --git a/src/content/docs/sandbox/guides/manage-files.mdx b/src/content/docs/sandbox/guides/manage-files.mdx index 03250824cf909f6..69b1bd40d972860 100644 --- a/src/content/docs/sandbox/guides/manage-files.mdx +++ b/src/content/docs/sandbox/guides/manage-files.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to read, write, organize, and synchronize files in the sandbox filesystem. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Understanding of absolute vs relative paths - ## Path conventions File operations support both absolute and relative paths: diff --git a/src/content/docs/sandbox/guides/streaming-output.mdx b/src/content/docs/sandbox/guides/streaming-output.mdx index d7d22a0a78cefdb..d1ac81f09e6b380 100644 --- a/src/content/docs/sandbox/guides/streaming-output.mdx +++ b/src/content/docs/sandbox/guides/streaming-output.mdx @@ -9,11 +9,6 @@ import { TypeScriptExample } from "~/components"; This guide shows you how to handle real-time output from commands, processes, and code execution. -## Prerequisites - -- Existing sandbox instance (see [Get Started](/sandbox/get-started/)) -- Understanding of async streams and Server-Sent Events (SSE) - ## When to use streaming Use streaming when you need: From b24830a02b7b74e72abc654e5eda8ef427234c69 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 17:12:11 +0100 Subject: [PATCH 15/22] Simplify concepts --- .../docs/sandbox/concepts/architecture.mdx | 358 ++--------- .../docs/sandbox/concepts/containers.mdx | 502 ++------------- .../docs/sandbox/concepts/preview-urls.mdx | 508 +++------------ .../docs/sandbox/concepts/sandboxes.mdx | 393 ++---------- .../docs/sandbox/concepts/security.mdx | 591 ++---------------- .../docs/sandbox/concepts/sessions.mdx | 494 +++------------ 6 files changed, 359 insertions(+), 2487 deletions(-) diff --git a/src/content/docs/sandbox/concepts/architecture.mdx b/src/content/docs/sandbox/concepts/architecture.mdx index ec9624603c4c335..c904a01d100292c 100644 --- a/src/content/docs/sandbox/concepts/architecture.mdx +++ b/src/content/docs/sandbox/concepts/architecture.mdx @@ -5,39 +5,27 @@ sidebar: order: 1 --- -import { Render } from "~/components"; - -This page explains how the Sandbox SDK is structured, why it's designed this way, and how the pieces fit together. - -## Overview - 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 -This architecture enables running untrusted code safely while maintaining the benefits of edge computing: global distribution, low latency, and automatic scaling. - ## Three-layer architecture -The SDK uses a layered design where each layer has a distinct purpose and pattern: - ``` ┌─────────────────────────────────────────────────────────┐ -│ Your Application │ -│ (Cloudflare Worker) │ -└───────────────────────────┬───────────────────────────────┘ - │ +│ Your Application │ +│ (Cloudflare Worker) │ +└───────────────────────────┬─────────────────────────────┘ ├─ getSandbox() ├─ exec() ├─ writeFile() - ├─ exposePort() │ - ┌───────▼───────┐ - │ Client SDK │ Layer 1: Developer Interface - │ (TypeScript) │ - └───────┬───────┘ + ┌────────────────▼──────────────────┐ + │ Container-enabled Durable Object │ + │ (SDK methods via RPC from Worker) │ + └───────────────────────────────────┘ │ HTTP/JSON │ ┌───────▼───────┐ @@ -54,371 +42,95 @@ The SDK uses a layered design where each layer has a distinct purpose and patter ### Layer 1: Client SDK -The Client SDK provides the developer-facing API. It's what you import and use in your Workers: +The developer-facing API you use in your Workers: ```typescript -import { getSandbox } from '@cloudflare/sandbox'; +import { getSandbox } from "@cloudflare/sandbox"; -const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); -const result = await sandbox.exec('python script.py'); +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. -**Key characteristics**: -- Domain-organized clients (CommandClient, FileClient, ProcessClient, etc.) -- Direct response interfaces (not wrapped in service results) -- Throws typed errors on failures -- Handles HTTP communication with Durable Object -- Maps container errors to user-friendly exceptions - -**Why this design**: Developers expect simple, direct APIs. The SDK hides complexity and provides clear error messages. - ### Layer 2: Durable Object -The Durable Object manages sandbox lifecycle and routing: +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 + // 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. -**Key characteristics**: -- Each sandbox is a unique Durable Object instance -- Survives across requests (persistent state) -- Manages container lifecycle (create, pause, resume) -- Routes commands to container runtime -- Generates and manages preview URLs -- Handles security and access control +**Why Durable Objects**: -**Why Durable Objects**: They provide: - **Persistent identity** - Same sandbox ID always routes to same instance -- **State management** - File system and processes persist between requests +- **State management** - Filesystem and processes persist between requests - **Geographic distribution** - Sandboxes run close to users - **Automatic scaling** - Cloudflare manages provisioning ### Layer 3: Container Runtime -The Container Runtime executes code in isolation: +Executes code in isolation with full Linux capabilities. -**Purpose**: Safely execute untrusted code with full Linux capabilities. +**Purpose**: Safely execute untrusted code. -**Key characteristics**: -- Linux environment with Bun runtime -- Service-oriented architecture (ProcessService, FileService, etc.) -- ServiceResult pattern for all operations (success/error) -- Security validation on all inputs -- Streaming support for real-time output -- HTTP API for command execution +**Why containers**: -**Why containers**: They provide: - **True isolation** - Process-level isolation with namespaces - **Full environment** - Real Linux with Python, Node.js, Git, etc. - **Resource limits** - CPU, memory, disk constraints -- **No escape** - Sandboxed execution prevents host access ## Request flow -Here's what happens when you execute a command: - -```typescript -await sandbox.exec('python script.py') -``` - -1. **Client SDK** (Your Worker) - - Validates parameters - - Builds HTTP request - - Sends to Durable Object - -2. **Durable Object** - - Receives request from client - - Checks authentication - - Routes to container runtime - - Returns response to client - -3. **Container Runtime** - - Receives command via HTTP - - Validates inputs for security - - Executes command with Bun.spawn() - - Captures stdout/stderr - - Returns structured result - -4. **Response flows back** - - Container → Durable Object → Client SDK → Your code - - Errors are transformed at each layer - - Streaming data flows through all layers - -## Why this architecture? - -### Separation of concerns - -Each layer has a single, clear responsibility: - -- **Client SDK**: Developer experience -- **Durable Object**: State and routing -- **Container**: Execution and isolation - -This separation makes the system: -- Easier to understand and debug -- Simpler to test (mock each layer independently) -- More maintainable (changes are localized) -- More reliable (failures are contained) - -### Edge-first design - -The SDK is built for Cloudflare's edge network: - -- **Global distribution** - Sandboxes run in 300+ cities worldwide -- **Low latency** - Code executes close to users -- **Automatic scaling** - No capacity planning needed -- **Integrated platform** - Works seamlessly with other Cloudflare products - -### Security by design - -Multiple layers of security: - -1. **Container isolation** - Linux namespaces prevent host access -2. **Input validation** - All inputs validated before execution -3. **Resource limits** - CPU, memory, disk quotas enforced -4. **Path security** - File operations restricted to safe directories -5. **Command sanitization** - Dangerous commands blocked - -### Developer experience - -The architecture prioritizes ease of use: - -- **Simple API** - Clean, TypeScript-first interface -- **Error handling** - Clear, actionable error messages -- **Type safety** - Full TypeScript support -- **Familiar patterns** - Standard async/await, no callbacks -- **Rich features** - Streaming, sessions, preview URLs built-in - -## Execution models - -The SDK supports two execution patterns: - -### Direct execution - -One-time commands that complete and exit: +When you execute a command: ```typescript -const result = await sandbox.exec('npm test'); -// Command runs, completes, returns result +await sandbox.exec("python script.py"); ``` -**Use for**: -- Scripts and utilities -- Build commands -- One-time data processing -- Testing - -**Characteristics**: -- Waits for completion -- Returns full output -- No persistent state needed -- Simple error handling +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 -### Background processes - -Long-running services that continue after starting: - -```typescript -const process = await sandbox.startProcess('node server.js'); -await sandbox.exposePort(3000); -// Server runs continuously in background -``` - -**Use for**: -- Web servers and APIs -- Development environments -- Long-running services -- Multi-process applications - -**Characteristics**: -- Returns immediately (doesn't wait) -- Process ID for later management -- Can expose ports for network access -- Requires explicit cleanup - -## State management +## State persistence Sandboxes maintain state across requests: -### Filesystem state - -Files persist between commands: +**Filesystem**: ```typescript // Request 1 -await sandbox.writeFile('/workspace/data.txt', 'hello'); +await sandbox.writeFile("/workspace/data.txt", "hello"); // Request 2 (minutes later) -const file = await sandbox.readFile('/workspace/data.txt'); +const file = await sandbox.readFile("/workspace/data.txt"); // Returns 'hello' - file persisted ``` -### Process state - -Background processes continue running: +**Processes**: ```typescript // Request 1 -await sandbox.startProcess('node server.js'); -await sandbox.exposePort(3000); +await sandbox.startProcess("node server.js"); // Request 2 (minutes later) const processes = await sandbox.listProcesses(); // Server still running ``` -### Code context state - -Code interpreter contexts remember variables: - -```typescript -// Execution 1 -await sandbox.runCode(contextId, 'x = 42'); - -// Execution 2 -await sandbox.runCode(contextId, 'print(x)'); -// Outputs '42' - variable persisted -``` - -## Design trade-offs - -Every architecture involves trade-offs. Here's what we chose and why: - -### Three layers vs. two - -**Choice**: Client SDK + Durable Object + Container (not just Client + Container) - -**Why**: Durable Objects provide: -- Persistent sandbox identity -- State management -- Global distribution -- Access control - -**Trade-off**: Extra network hop, but worth it for statefulness and routing. - -### HTTP vs. custom protocol +## Performance -**Choice**: HTTP/JSON between layers - -**Why**: -- Simple to implement and debug -- Works with existing tools (curl, fetch) -- Easy to add middleware (CORS, logging) -- Standard error handling - -**Trade-off**: Slight overhead vs. binary protocol, but negligible for I/O-bound operations. - -### ServiceResult pattern - -**Choice**: Container services return `ServiceResult` (success/error union type) - -**Why**: -- Explicit error handling (can't forget) -- Type-safe errors -- No exceptions in service layer -- Easy to compose operations - -**Trade-off**: More verbose than throwing errors, but much safer. - -### Bun runtime - -**Choice**: Bun instead of Node.js in containers - -**Why**: -- Faster startup (critical for cold starts) -- Built-in TypeScript support -- Better subprocess management -- Modern APIs - -**Trade-off**: Newer runtime, but stable for our use case. - -## Scalability and performance - -### How sandboxes scale - -1. **Per-sandbox isolation** - - Each sandbox is a separate Durable Object - - No shared state between sandboxes - - Sandboxes scale independently - -2. **Geographic distribution** - - Durable Objects run close to users - - Containers are colocated with Durable Objects - - First request determines location - -3. **Automatic provisioning** - - Cloudflare handles scaling - - No capacity planning needed - - Scales from 1 to millions of sandboxes - -### Performance characteristics - -**Cold start**: 100-300ms (container initialization) -**Warm start**: \<10ms (reuse existing container) -**Command execution**: Depends on command (I/O-bound) +**Cold start**: 100-300ms (container initialization) +**Warm start**: \<10ms (reuse existing container) **Network latency**: 10-50ms (edge-to-edge) -**Optimization strategies**: -- Keep sandboxes warm with periodic requests -- Batch operations when possible -- Use streaming for long operations -- Cache computation results - -## Comparison to alternatives - -### vs. Traditional VMs - -**Sandbox SDK**: -- ✅ Instant provisioning (\<1s) -- ✅ Global distribution -- ✅ Pay-per-use pricing -- ❌ Linux-only (no Windows) - -**VMs**: -- ❌ Slow provisioning (minutes) -- ❌ Regional deployment -- ❌ Always-on costs -- ✅ Any OS - -### vs. Serverless Functions - -**Sandbox SDK**: -- ✅ Stateful (filesystem, processes) -- ✅ Long-running operations -- ✅ Full Linux environment -- ❌ Slightly slower cold starts - -**Functions**: -- ❌ Stateless -- ❌ Strict timeouts -- ❌ Limited environment -- ✅ Very fast cold starts - -### vs. Remote Code Execution APIs - -**Sandbox SDK**: -- ✅ Persistent state -- ✅ Multi-language support -- ✅ Full control -- ✅ Preview URLs -- ❌ Manage sandboxes yourself - -**Remote APIs**: -- ❌ No state -- ❌ Limited languages -- ❌ Restricted capabilities -- ❌ No custom URLs -- ✅ Fully managed - ## Related resources - [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - How sandboxes are created and managed diff --git a/src/content/docs/sandbox/concepts/containers.mdx b/src/content/docs/sandbox/concepts/containers.mdx index cd429acf8bbb74e..d80298576c0e1c7 100644 --- a/src/content/docs/sandbox/concepts/containers.mdx +++ b/src/content/docs/sandbox/concepts/containers.mdx @@ -5,497 +5,127 @@ sidebar: order: 3 --- -This page explains what's inside a sandbox container, how code executes, and what capabilities are available. +Each sandbox runs in an isolated Linux container based on Ubuntu 22.04. -## Container environment +## Pre-installed software -Each sandbox runs in an isolated Linux container with: +The base container comes pre-packaged with a full development environment: -- **Operating System**: Linux (Ubuntu-based) -- **Runtime**: Bun (JavaScript/TypeScript) -- **Shell**: Bash -- **Python**: Python 3.11+ -- **Node.js**: Available via Bun compatibility -- **Package managers**: npm, pip, apt -- **Git**: Full Git client -- **Common tools**: curl, wget, sed, awk, grep, etc. +**Languages and runtimes**: +- Python 3.11 (with pip) +- Node.js 20 LTS (with npm) +- Bun (JavaScript/TypeScript runtime) -## Why Linux containers? - -Containers provide the right balance of isolation, performance, and capabilities: - -### True isolation - -Unlike shared runtimes, containers provide: - -- **Process isolation** - Each sandbox has its own process namespace -- **Filesystem isolation** - Separate root filesystem per sandbox -- **Network isolation** - Isolated network stack -- **Resource isolation** - CPU and memory limits per sandbox - -This means code in one sandbox cannot see or affect code in another sandbox, even if they run on the same physical machine. - -### Full environment - -Containers offer a complete Linux environment: - -- **Real filesystem** - Not a virtual filesystem, actual disk access -- **Process management** - Fork, exec, signals all work normally -- **Network stack** - Full TCP/IP, can run servers -- **System tools** - Access to standard Linux utilities - -This enables running complex applications that expect a real operating system. - -### Compatibility - -Standard Linux environment means: - -- **Existing code runs** - Most Linux software works without modification -- **Package managers work** - Install dependencies with apt, pip, npm -- **Scripts are portable** - Bash scripts run as expected -- **Tools are familiar** - Standard Linux commands available - -## Why Bun? - -The container uses Bun as its primary runtime: - -### Fast startup - -Bun starts almost instantly: - -- **Cold start**: 100-300ms total (vs. 500ms+ with Node.js) -- **Warm start**: \<10ms -- **Process spawning**: Very fast subprocess creation - -Fast startup is critical for edge computing where containers may start and stop frequently. - -### Built-in TypeScript - -No transpilation step needed: - -```typescript -// This works directly in the container -import type { Config } from './types'; - -const config: Config = { - port: 3000, - environment: 'production' -}; -``` - -Developers can write TypeScript without build steps. - -### Better subprocess management - -Bun.spawn() provides: - -- **Streaming output** - Read stdout/stderr in real-time -- **Process control** - Signals, timeouts, environment variables -- **Performance** - Faster than Node.js child_process -- **Type safety** - Full TypeScript support - -This is core to the SDK's command execution capabilities. - -### Node.js compatibility - -Bun is compatible with Node.js: - -- **npm packages work** - Install and use npm packages normally -- **Node.js APIs available** - fs, path, http, etc. -- **Existing code runs** - Most Node.js code works without changes - -## Available runtimes - -### Python - -Python 3.11+ is pre-installed: - -```python -# Python works out of the box -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -data = np.array([1, 2, 3, 4, 5]) -print(f"Mean: {np.mean(data)}") -``` - -**Pre-installed packages**: +**Python packages**: - NumPy - Numerical computing - pandas - Data analysis - Matplotlib - Plotting and visualization -- Requests - HTTP library - -**Install additional packages**: -```bash -pip install scikit-learn tensorflow -``` - -### JavaScript/TypeScript +- IPython - Interactive Python -Via Bun runtime: +**Development tools**: +- Git - Version control +- Build tools (gcc, make, pkg-config) +- Text editors (vim, nano) +- Process monitoring (htop, procps) -```javascript -// Run with Bun -const response = await fetch('https://api.example.com/data'); -const data = await response.json(); +**Utilities**: +- curl, wget - HTTP clients +- jq - JSON processor +- Network tools (ping, dig, netstat) +- Compression (zip, unzip) -console.log(data); -``` +Install additional software at runtime or [customize the base image](/sandbox/configuration/dockerfile/): -**Built-in capabilities**: -- Fetch API -- WebSocket -- Streams -- File system operations -- Subprocess management - -**Install packages**: ```bash -npm install express -``` - -### Shell scripts - -Full Bash shell available: +# Python packages +pip install scikit-learn tensorflow -```bash -#!/bin/bash +# Node.js packages +npm install express -# Standard bash scripting -for file in *.txt; do - echo "Processing $file" - sed 's/old/new/g' "$file" > "${file}.new" -done +# System packages +apt-get install redis-server ``` -**Available commands**: -- Text processing: sed, awk, grep, cut, tr -- File operations: cp, mv, rm, mkdir, chmod -- Networking: curl, wget, nc -- Compression: tar, gzip, zip -- Version control: git - -## Filesystem structure - -### Standard directories - -- **/workspace** - Default working directory, use for application files -- **/tmp** - Temporary files (may be cleared periodically) -- **/home** - User home directory -- **/usr/bin** - System binaries -- **/usr/local** - Locally installed software +## Filesystem -### Writeable locations +The container provides a standard Linux filesystem. You can read and write anywhere you have permissions. -Code can write to: - -- `/workspace` - Recommended for all application data +**Standard directories**: +- `/workspace` - Default working directory for user code - `/tmp` - Temporary files -- `/home` - User configuration - -Cannot write to: - -- `/usr`, `/bin`, `/lib` - System directories (read-only) -- `/proc`, `/sys` - Virtual filesystems - -### Disk space - -Sandboxes have limited disk space: - -- **Default**: Implementation-dependent (Cloudflare manages) -- **Best practice**: Clean up temporary files regularly -- **Monitoring**: Check with `df -h` - -If you hit disk limits, clean up: - -```bash -# Remove temporary files -rm -rf /tmp/* +- `/home` - User home directory +- `/usr/bin`, `/usr/local/bin` - Executable binaries -# Remove package caches -rm -rf ~/.cache/pip -rm -rf node_modules +**Example**: +```typescript +await sandbox.writeFile('/workspace/app.py', 'print("Hello")'); +await sandbox.writeFile('/tmp/cache.json', '{}'); +await sandbox.exec('ls -la /workspace'); ``` ## Process management -### Process isolation - -Each sandbox has its own process namespace: +Processes run as you'd expect in a regular Linux environment. -```bash -# ps shows only processes in this sandbox -ps aux -# Won't see processes from other sandboxes or host +**Foreground processes** (`exec()`): +```typescript +const result = await sandbox.exec('npm test'); +// Waits for completion, returns output ``` -### Resource limits - -Processes are constrained by: - -- **CPU**: Limited CPU time (enforced by container runtime) -- **Memory**: Limited RAM per sandbox -- **Processes**: Limited number of concurrent processes -- **Files**: Limited number of open file descriptors - -These limits prevent one sandbox from affecting others. - -### Process lifecycle - -**Foreground processes** (via `exec()`): -- Run until completion -- Block the calling code -- Return complete output - -**Background processes** (via `startProcess()`): -- Run indefinitely -- Don't block -- Require explicit cleanup +**Background processes** (`startProcess()`): +```typescript +const process = await sandbox.startProcess('node server.js'); +// Returns immediately, process runs in background +``` ## Network capabilities -### Outbound connections - -Sandboxes can make outbound connections: - +**Outbound connections** work: ```bash -# HTTP requests work curl https://api.example.com/data - -# WebSocket connections work -wscat -c wss://echo.websocket.org - -# Raw TCP works -nc example.com 80 +pip install requests +npm install express ``` -Use cases: -- API calls -- Database connections -- External service integration - -### Inbound connections - -Containers cannot receive inbound connections directly. Instead, use port exposure: - +**Inbound connections** require port exposure: ```typescript -// Start a web server await sandbox.startProcess('python -m http.server 8000'); - -// Expose port to get public URL const exposed = await sandbox.exposePort(8000); console.log(exposed.exposedAt); // Public URL ``` -The preview URL proxies requests to the container. - -### Localhost - -Services can bind to localhost and communicate within the sandbox: - +**Localhost** works within sandbox: ```bash -# Start a database -redis-server & - -# Connect to it locally -redis-cli ping +redis-server & # Start server +redis-cli ping # Connect locally ``` -This enables multi-process applications within one sandbox. - -## Security boundaries - -### What's isolated - -Between sandboxes: +## Security -- **Filesystem** - Each sandbox has its own root filesystem -- **Processes** - Cannot see processes in other sandboxes -- **Network** - Separate network namespace -- **Memory** - Cannot access memory of other sandboxes +**Between sandboxes** (isolated): +- Each sandbox is a separate container +- Filesystem, memory and network are all isolated -### What's shared +**Within sandbox** (shared): +- All processes see the same files +- Processes can communicate with each other +- Environment variables are session-scoped -Within the sandbox, processes share: - -- **Filesystem** - All processes see the same files -- **Environment** - Same environment variables -- **Network** - Can communicate via localhost - -This means code within one sandbox is NOT isolated from other code in the same sandbox. - -### Untrusted code - -To run untrusted code: - -**Option 1: Separate sandboxes** +To run untrusted code, use separate sandboxes per user: ```typescript -// Each user gets their own sandbox const sandbox = getSandbox(env.Sandbox, `user-${userId}`); ``` -**Option 2: Sessions** (advanced) -```typescript -// Isolated execution contexts within one sandbox -const session = await sandbox.createSession(); -``` - -## Performance characteristics - -### Startup time - -- **Cold start**: 100-300ms (first request to new sandbox) -- **Warm start**: \<10ms (subsequent requests) - -Cold starts include: -- Container provisioning -- Image pulling (if needed) -- Runtime initialization - -### Execution speed - -Command execution speed depends on: - -- **I/O-bound operations**: Limited by disk/network (most operations) -- **CPU-bound operations**: Limited by CPU allocation -- **Memory access**: Fast, in-memory operations are quick - -Typical performance: - -```bash -# File operations: Very fast -time dd if=/dev/zero of=/tmp/test bs=1M count=100 -# <100ms for 100MB - -# Command execution: Fast -time echo "Hello" -# <1ms - -# Python startup: Moderate -time python -c "print('hello')" -# ~50-100ms - -# Package installation: Slow (network-bound) -time pip install requests -# ~2-5 seconds -``` - -### Optimization strategies - -**1. Cache installations**: -```bash -# Don't reinstall every time -if [ ! -d "node_modules" ]; then - npm install -fi -``` - -**2. Use warm sandboxes**: -```typescript -// Reuse sandboxes instead of creating new ones -const sandbox = getSandbox(env.Sandbox, userId); -``` - -**3. Batch operations**: -```bash -# One command instead of multiple -npm install && npm test && npm run build -``` - -**4. Stream large outputs**: -```typescript -// Don't wait for complete output -const stream = await sandbox.execStream('npm test'); -``` - -## Comparison to other runtimes - -### vs. Node.js containers - -**Sandbox SDK (Bun)**: -- ✅ Faster startup (2-3x) -- ✅ Built-in TypeScript -- ✅ Better subprocess management -- ⚠️ Newer runtime (less mature) - -**Node.js**: -- ✅ More mature ecosystem -- ✅ Wider compatibility -- ❌ Slower startup -- ❌ No built-in TypeScript - -### vs. Python-only environments - -**Sandbox SDK**: -- ✅ Multiple languages (Python + JS + Shell) -- ✅ Full Linux environment -- ✅ Can install system packages -- ⚠️ More complex - -**Python-only**: -- ✅ Simpler -- ✅ Faster for pure Python -- ❌ Limited to Python -- ❌ No system-level operations - -### vs. WebAssembly - -**Sandbox SDK**: -- ✅ Full Linux environment -- ✅ Any language/tool -- ✅ System operations -- ❌ Slower startup than Wasm - -**WebAssembly**: -- ✅ Very fast startup -- ✅ Strong isolation -- ❌ Limited language support -- ❌ No system operations - ## Limitations -### What you can't do - -Containers have some restrictions: - -**No privileged operations**: -- Can't load kernel modules -- Can't access host hardware directly -- Can't modify firewall rules -- Can't run Docker inside (no nested containers) - -**No GUI**: -- No X11 or display server -- Can't run GUI applications -- Terminal/headless only - -**No persistent hardware identity**: -- MAC addresses may change -- Hardware IDs are virtualized - -**Resource limits**: -- CPU and memory are capped -- Disk space is limited -- Network bandwidth is limited - -### Workarounds - -For GUI apps: -- Use headless rendering (e.g., Matplotlib's Agg backend) -- Generate images/PDFs instead of interactive displays - -For privileged operations: -- Design applications to avoid them -- Use Cloudflare services instead (e.g., R2 instead of local disk) - -For persistent identity: -- Use sandbox ID for identity -- Store state in filesystem or external services +**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 -- [Execute commands guide](/sandbox/guides/execute-commands/) - Running code in containers - [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Container lifecycle management diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx index 1c4d4b8b0d6f2b8..9dbcc26226895b0 100644 --- a/src/content/docs/sandbox/concepts/preview-urls.mdx +++ b/src/content/docs/sandbox/concepts/preview-urls.mdx @@ -5,10 +5,6 @@ sidebar: order: 5 --- -This page explains how preview URLs work, how traffic is routed, and the design decisions behind them. - -## What are preview URLs? - 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 @@ -19,501 +15,159 @@ console.log(exposed.exposedAt); // https://abc123-8000.sandbox.workers.dev ``` -Anyone with this URL can access your service running on port 8000 inside the sandbox. - -## How preview URLs work +## URL format -### URL format - -Preview URLs follow a predictable pattern: +Preview URLs follow this pattern: ``` https://{sandbox-id}-{port}.sandbox.workers.dev ``` -Components: - -- **sandbox-id**: Unique identifier for the sandbox -- **port**: The exposed port number -- **sandbox.workers.dev**: Cloudflare-managed domain - -Examples: - +**Examples**: - Port 3000: `https://abc123-3000.sandbox.workers.dev` - Port 8080: `https://abc123-8080.sandbox.workers.dev` -- Port 5173: `https://abc123-5173.sandbox.workers.dev` - -### URL stability - -URLs remain stable: -- **Same port** - Always gets the same URL -- **Across requests** - URL doesn't change -- **After restart** - Same sandbox ID = same URL - -This means you can: -- Share URLs with others -- Bookmark preview URLs -- Use URLs in webhooks -- Embed URLs in documentation +**URL stability**: URLs remain the same for a given sandbox ID and port. You can share, bookmark, or use them in webhooks. ## Request routing -Understanding how requests flow helps with debugging and optimization. - -### The complete path - ``` User's Browser ↓ HTTPS -Cloudflare Edge (nearest location) - ↓ Internal routing -Durable Object (sandbox location) - ↓ Container protocol -Container (sandbox) - ↓ HTTP (localhost) +Your Worker + ↓ +Durable Object (sandbox) + ↓ HTTP Your Service (on exposed port) ``` -### Step by step - -1. **User makes request** - ``` - GET https://abc123-3000.sandbox.workers.dev/api/data - ``` - -2. **Cloudflare Edge receives it** - - Request hits nearest Cloudflare data center - - TLS termination happens here - - Request is authenticated and validated - -3. **Routed to Durable Object** - - Sandbox ID extracted from hostname - - Request routed to the Durable Object managing that sandbox - - May cross regions if sandbox is in different location - -4. **Durable Object forwards to container** - - Port number extracted from hostname (3000) - - Request forwarded to container - - Container receives HTTP request - -5. **Container proxies to service** - - Container makes localhost request to port 3000 - - Your service running on port 3000 handles it - - Response flows back through the chain - -### Latency components - -Total latency = Edge latency + Routing latency + Container latency + Service latency - -**Edge latency**: 1-5ms -- User to nearest Cloudflare data center - -**Routing latency**: 10-100ms -- Edge to Durable Object (may cross regions) - -**Container latency**: 1-10ms -- Durable Object to container (local) - -**Service latency**: Varies -- Your application's response time - -**Typical total**: 20-150ms for simple requests - -## Why this design? - -### Global accessibility - -Preview URLs are globally accessible: - -- **No VPN needed** - Access from anywhere -- **HTTPS by default** - Secure by default -- **Cloudflare CDN** - Fast from anywhere in the world - -This enables: -- Sharing demos with clients -- Testing from mobile devices -- Accessing dev environments remotely -- Webhook integration - -### Security - -The URL provides access control: - -- **Unpredictable URLs** - Sandbox IDs are random (hard to guess) -- **HTTPS enforced** - All traffic encrypted -- **No default auth** - Your service controls authentication - -This means: - -**URLs are not secret**: -```typescript -// Anyone with the URL can access -const url = 'https://abc123-3000.sandbox.workers.dev'; -// No password needed - URL grants access -``` - -**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'} -``` - -### Simplicity - -One exposure model for all protocols: - -**HTTP/HTTPS**: Works directly -```typescript -await sandbox.exposePort(3000); -// Access via https://abc123-3000.sandbox.workers.dev -``` - -**WebSocket**: Works through HTTP upgrade -```typescript -await sandbox.exposePort(8080); -// Connect via wss://abc123-8080.sandbox.workers.dev -``` - -**Server-Sent Events**: Works through HTTP -```typescript -await sandbox.exposePort(5000); -// Stream from https://abc123-5000.sandbox.workers.dev/events -``` - -**Custom protocols**: Require HTTP wrapping -```bash -# Can't expose raw TCP/UDP directly -# Must wrap in HTTP service first -``` - -## Port exposure lifecycle - -### Exposure +**Important**: You must handle preview URL routing in your Worker using `proxyToSandbox()`: ```typescript -const exposed = await sandbox.exposePort(8000, { - name: 'my-service' // Optional: For organization -}); - -console.log('Port:', exposed.port); // 8000 -console.log('URL:', exposed.exposedAt); // https://... -console.log('Name:', exposed.name); // 'my-service' -console.log('Time:', exposed.timestamp); // ISO timestamp -``` - -**What happens**: -1. SDK validates port number (1-65535) -2. Checks if port is already exposed -3. Verifies service is listening on port -4. Creates routing entry -5. Returns preview URL - -**Cost**: Minimal - just metadata - -### Active - -While exposed: -- Traffic flows to your service -- URL remains accessible -- No time limit (stays open) -- No request limit +import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox"; -**Charges** (if any): -- Based on data transfer -- Based on request count -- No idle charges +export default { + async fetch(request, env) { + // Route preview URL requests to sandboxes + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; -### Closure - -```typescript -await sandbox.unexposePort(8000); + // Your custom routes here + // ... + } +}; ``` -**What happens**: -1. Routing entry removed -2. URL becomes inaccessible -3. Any in-flight requests complete -4. New requests get 404 - -**Effect**: -- Service still runs (process not killed) -- Just not accessible externally -- Can re-expose same port later +Without this, preview URLs won't work. ## Multiple ports -You can expose multiple ports simultaneously: +Expose multiple services simultaneously: ```typescript -// Start multiple services -await sandbox.startProcess('node api.js'); // Port 3000 -await sandbox.startProcess('node admin.js'); // Port 3001 -await sandbox.startProcess('python metrics.py'); // Port 8080 +await sandbox.startProcess('node api.js'); // Port 3000 +await sandbox.startProcess('node admin.js'); // Port 3001 -// Expose all ports const api = await sandbox.exposePort(3000, { name: 'api' }); const admin = await sandbox.exposePort(3001, { name: 'admin' }); -const metrics = await sandbox.exposePort(8080, { name: 'metrics' }); -return { - api: api.exposedAt, - admin: admin.exposedAt, - metrics: metrics.exposedAt -}; +// Each gets its own URL: +// https://abc123-3000.sandbox.workers.dev +// https://abc123-3001.sandbox.workers.dev ``` -Each gets its own unique URL: -- API: `https://abc123-3000.sandbox.workers.dev` -- Admin: `https://abc123-3001.sandbox.workers.dev` -- Metrics: `https://abc123-8080.sandbox.workers.dev` - -## Limitations and constraints - -### What works - -- **HTTP/HTTPS** - Full support -- **WebSocket (WSS)** - Works via HTTP upgrade -- **Server-Sent Events** - Works via HTTP -- **Any HTTP method** - GET, POST, PUT, DELETE, etc. -- **Request headers** - Forwarded to service -- **Response headers** - Forwarded to client -- **Large payloads** - No size limits (within reason) - -### What doesn't work - -- **Raw TCP** - Must use HTTP -- **UDP** - Not supported -- **Custom protocols** - Must wrap in HTTP -- **Port 80/443** - Use higher ports (1024+) - -### Rate limits - -Preview URLs may have limits: - -- **Requests per second** - Implementation-dependent -- **Concurrent connections** - Implementation-dependent -- **Bandwidth** - Implementation-dependent - -For production use, consider: -- Adding caching (Cloudflare Cache API) -- Rate limiting in your service -- Monitoring usage - -## Design trade-offs - -### Why subdomains? - -**Choice**: Use subdomains for routing (`abc123-3000.sandbox.workers.dev`) - -**Alternative**: Use paths (`sandbox.workers.dev/abc123/3000/`) - -**Why subdomains**: -- ✅ Proper CORS behavior -- ✅ Cleaner URLs -- ✅ Standard HTTP semantics -- ✅ Works with all frameworks - -**Trade-off**: Slightly longer URLs, but much better compatibility - -### Why include port in URL? - -**Choice**: Port number in hostname (`-3000.`) - -**Alternative**: Use default ports, document which service - -**Why in URL**: -- ✅ Multiple services per sandbox -- ✅ Clear which port you're accessing -- ✅ No ambiguity -- ✅ Easy to expose/unexpose individually - -**Trade-off**: Longer URLs, but explicit and clear - -### Why not custom domains? - -**Current**: Fixed `.sandbox.workers.dev` domain +## What works -**Alternative**: Allow custom domains +- HTTP/HTTPS requests +- WebSocket (WSS) via HTTP upgrade +- Server-Sent Events +- All HTTP methods (GET, POST, PUT, DELETE, etc.) +- Request and response headers -**Why fixed domain**: -- ✅ Simpler implementation -- ✅ No DNS configuration needed -- ✅ Immediate availability -- ✅ Cloudflare-managed SSL +## What doesn't work -**Future**: Custom domains may be possible (not currently available) +- Raw TCP/UDP connections +- Custom protocols (must wrap in HTTP) +- Ports 80/443 (use 1024+) -## Security considerations - -### Public accessibility +## Security :::caution Preview URLs are publicly accessible. Anyone with the URL can access your service. ::: -**Implications**: -- Don't put sensitive data in URLs -- Don't rely on URL secrecy for security -- Add authentication in your service -- Monitor access logs - -**Authentication patterns**: - -```typescript -// Token-based auth -app.use((req, res, next) => { - const token = req.headers.authorization; - if (token !== `Bearer ${process.env.AUTH_TOKEN}`) { - return res.status(401).json({ error: 'Unauthorized' }); - } - next(); -}); -``` +**Add authentication in your service**: ```python -# Basic auth -from functools import wraps -from flask import request, abort - -def require_auth(f): - @wraps(f) - def decorated(*args, **kwargs): - auth = request.authorization - if not auth or auth.password != 'secret': - abort(401) - return f(*args, **kwargs) - return decorated +from flask import Flask, request, abort + +app = Flask(__name__) @app.route('/data') -@require_auth def get_data(): + token = request.headers.get('Authorization') + if token != 'Bearer secret-token': + abort(401) return {'data': 'protected'} ``` -### HTTPS enforcement - -All traffic is HTTPS: - -- **HTTP redirects to HTTPS** - Automatically -- **TLS 1.2+** - Modern encryption -- **Cloudflare certificates** - Managed automatically - -No need to handle SSL in your service. - -### Rate limiting +**Security features**: +- All traffic is HTTPS (automatic TLS) +- URLs use random sandbox IDs (hard to guess) +- You control authentication in your service -Consider adding rate limiting: - -```typescript -import { RateLimiter } from 'rate-limiter-flexible'; - -const limiter = new RateLimiter({ - points: 100, // Requests - duration: 60 // Per 60 seconds -}); - -app.use(async (req, res, next) => { - try { - await limiter.consume(req.ip); - next(); - } catch { - res.status(429).json({ error: 'Too many requests' }); - } -}); -``` - -## Debugging +## Troubleshooting ### URL not accessible -**Problem**: Preview URL returns errors. +Check if service is running and listening: -**Checks**: +```typescript +// 1. Is service running? +const processes = await sandbox.listProcesses(); -1. **Is service running?** - ```typescript - const processes = await sandbox.listProcesses(); - console.log(processes); // Check if service is running - ``` +// 2. Is port exposed? +const ports = await sandbox.getExposedPorts(); -2. **Is port exposed?** - ```typescript - const { ports } = await sandbox.getExposedPorts(); - console.log(ports); // Check if port is listed - ``` +// 3. Is service binding to 0.0.0.0 (not 127.0.0.1)? +// Good: +app.run(host='0.0.0.0', port=3000) -3. **Is service listening on correct port?** - ```bash - # In sandbox - netstat -tuln | grep 3000 # Check if port is bound - ``` +// Bad (localhost only): +app.run(host='127.0.0.1', port=3000) +``` -4. **Is service binding to 0.0.0.0 (not 127.0.0.1)?** - ```python - # Good - accessible - app.run(host='0.0.0.0', port=3000) +## Best practices - # Bad - localhost only - app.run(host='127.0.0.1', port=3000) - ``` +**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 -### Slow responses +**Cleanup**: +- Unexpose ports when done: `await sandbox.unexposePort(port)` +- Stop processes: `await sandbox.killAllProcesses()` -**Problem**: Preview URLs are slow. +## Local development -**Analysis**: +:::caution[Local development only] +When using `wrangler dev`, you must expose ports in your Dockerfile: -```bash -# Measure latency -curl -w "@curl-format.txt" -s https://abc123-3000.sandbox.workers.dev +```dockerfile +FROM docker.io/cloudflare/sandbox:0.3.3 -# Check service health -time curl http://localhost:3000/health # Inside sandbox +# Required for local development +EXPOSE 3000 +EXPOSE 8080 ``` -**Optimize**: -- Cache responses (HTTP headers) -- Reduce response sizes (compression) -- Optimize service code -- Use CDN features (Cloudflare Cache API) +Without `EXPOSE`, you'll see: `connect(): Connection refused: container port not found` -## Best practices - -### URL sharing - -- **Share responsibly** - URLs are public -- **Document authentication** - If required -- **Set expectations** - Temporary vs permanent -- **Monitor usage** - Track who's accessing - -### Service design - -- **Bind to 0.0.0.0** - Make accessible -- **Add health checks** - `/health` endpoint -- **Implement auth** - Don't rely on URL secrecy -- **Handle CORS** - If accessed from browsers -- **Log requests** - For debugging and monitoring - -### Cleanup - -- **Unexpose when done** - Free resources -- **Stop processes** - Don't leave servers running -- **Document URLs** - If sharing with others -- **Rotate periodically** - Create new sandboxes for fresh URLs +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 -- [Architecture](/sandbox/concepts/architecture/) - How routing works -- [Security model](/sandbox/concepts/security/) - Security implications diff --git a/src/content/docs/sandbox/concepts/sandboxes.mdx b/src/content/docs/sandbox/concepts/sandboxes.mdx index 0a72478a87d1684..335d52f0139f472 100644 --- a/src/content/docs/sandbox/concepts/sandboxes.mdx +++ b/src/content/docs/sandbox/concepts/sandboxes.mdx @@ -5,26 +5,17 @@ sidebar: order: 2 --- -This page explains how sandboxes are created, managed, and destroyed, and what that means for your applications. - -## What is a sandbox? - 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 -- Can run multiple processes simultaneously - Exists as a Cloudflare Durable Object -Think of a sandbox like a persistent virtual machine, but provisioned instantly at the edge. - ## Lifecycle states -A sandbox progresses through several states during its lifetime: - -### 1. Creation +### Creation A sandbox is created the first time you reference its ID: @@ -33,426 +24,118 @@ const sandbox = getSandbox(env.Sandbox, 'user-123'); await sandbox.exec('echo "Hello"'); // First request creates sandbox ``` -**What happens**: -1. Cloudflare provisions a new Durable Object -2. The container image is pulled (if needed) -3. The container starts with a fresh Linux environment -4. The request is processed -5. The sandbox enters "active" state - **Duration**: 100-300ms (cold start) or \<10ms (if warm) -**Cost**: Billed per sandbox, per active time - -### 2. Active - -The sandbox is running and processing requests: - -```typescript -// Sandbox is active and responding -const result = await sandbox.exec('python script.py'); -``` - -**Characteristics**: -- Container is running -- Filesystem is accessible -- Processes can be started -- Ports can be exposed -- Fast response times (\<10ms for simple operations) - -**Persistence**: -- Files written to disk remain -- Background processes continue running -- Environment variables persist -- Code interpreter contexts stay alive - -### 3. Idle - -After a period of inactivity, the sandbox may enter idle state: +### Active -**What happens**: -- Container may be paused or stopped -- Filesystem state is preserved -- Background processes may be suspended -- Next request triggers "warm start" +The sandbox is running and processing requests. Filesystem, processes, and environment variables persist across requests. -**Duration**: Implementation-dependent (Cloudflare manages this) +### Idle -**Cost**: Reduced or no charges while 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. -### 4. Destruction +### Destruction Sandboxes are explicitly destroyed or automatically cleaned up: ```typescript -// Explicit cleanup await sandbox.destroy(); -// Sandbox is gone, all state deleted +// All files, processes, and state deleted permanently ``` -**What's deleted**: -- All files in the filesystem -- All running processes -- All exposed ports -- All code interpreter contexts -- The Durable Object itself - -**Irreversible**: Once destroyed, the sandbox cannot be recovered. - -## Persistence guarantees - -### What persists +## Persistence Between requests to the same sandbox: -**Filesystem**: -- Files in `/workspace` -- Files in `/tmp` -- Files in `/home` -- File permissions and ownership - -**Processes**: +**What persists**: +- Files in `/workspace`, `/tmp`, `/home` - Background processes (started with `startProcess()`) -- Their PIDs and status -- Their accumulated logs - -**Code contexts**: -- Python/JavaScript execution contexts -- Variables and imports -- Module state - -**Configuration**: -- Environment variables -- Working directory -- Port exposures - -### What doesn't persist +- Code interpreter contexts and variables +- Environment variables and port exposures -Across sandbox destruction: - -**Nothing persists** - Destruction is complete cleanup. - -**After container restart** (rare, managed by Cloudflare): -- Background processes (will stop) -- Exposed ports (will close) -- In-memory state (will be lost) - -Note: Filesystem usually survives container restarts, but this isn't guaranteed. +**What doesn't persist**: +- Nothing survives `destroy()` +- Background processes may stop after container restarts (rare) ## Naming strategies -How you name sandboxes affects behavior: - ### Per-user sandboxes -One sandbox per user: - ```typescript -const userId = 'user-123'; -const sandbox = getSandbox(env.Sandbox, userId); +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); ``` -**Benefits**: -- User's work persists across sessions -- Files and processes stay alive -- Personal workspace feeling - -**Use for**: -- Interactive dev environments -- Code playgrounds -- Personal notebooks +User's work persists across sessions. Good for interactive environments, playgrounds, and notebooks. ### Per-session sandboxes -Temporary sandboxes for each session: - ```typescript const sessionId = `session-${Date.now()}-${Math.random()}`; const sandbox = getSandbox(env.Sandbox, sessionId); - -// Later, clean up +// Later: await sandbox.destroy(); ``` -**Benefits**: -- Fresh environment each time -- No cross-session contamination -- Explicit resource management - -**Use for**: -- One-time code execution -- CI/CD builds -- Isolated test runs +Fresh environment each time. Good for one-time execution, CI/CD, and isolated tests. ### Per-task sandboxes -One sandbox per specific task: - ```typescript -const taskId = `build-${repoName}-${commit}`; -const sandbox = getSandbox(env.Sandbox, taskId); +const sandbox = getSandbox(env.Sandbox, `build-${repoName}-${commit}`); ``` -**Benefits**: -- Idempotent operations -- Can resume interrupted tasks -- Clear task-to-sandbox mapping - -**Use for**: -- Build systems -- Data processing pipelines -- Background jobs - -### Shared sandboxes - -Multiple users sharing one sandbox: - -```typescript -const sandbox = getSandbox(env.Sandbox, 'shared-demo'); -``` +Idempotent operations with clear task-to-sandbox mapping. Good for builds, pipelines, and background jobs. -**Benefits**: -- Reduced costs (fewer sandboxes) -- Shared state between users -- Collaborative environments +## Request routing -**Risks**: -- No isolation between users -- Potential conflicts (file overwrites) -- Security concerns +The first request to a sandbox determines its geographic location. Subsequent requests route to the same location. -**Use for**: -- Public demos -- Read-only environments -- Trusted teams only +**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 create sandboxes - -Sandboxes are created automatically on first use, but you should consider: - -**For user environments**: -- Create on user signup -- Create on first interaction -- Reuse across sessions - -**For temporary work**: -- Create per request -- Destroy after completion -- Use unique IDs - -### When to destroy sandboxes - -Explicit destruction prevents resource leaks: +### When to destroy ```typescript try { const sandbox = getSandbox(env.Sandbox, sessionId); await sandbox.exec('npm run build'); } finally { - // Always clean up temporary sandboxes - await sandbox.destroy(); -} -``` - -**Destroy sandboxes when**: -- Session ends -- Task completes -- User logs out -- Resources are no longer needed - -**Don't destroy when**: -- User's personal environment (persist across sessions) -- Long-running services are needed -- Background processes are still working - -### Automatic cleanup - -Cloudflare may automatically clean up sandboxes that are: - -- Completely idle for extended periods -- Using excessive resources -- Violating terms of service - -Always design applications to recreate sandboxes if needed. - -## Request routing - -Understanding where sandboxes run helps with performance: - -### Geographic location - -The first request to a sandbox determines its location: - -```typescript -// User in San Francisco makes first request -const sandbox = getSandbox(env.Sandbox, 'user-123'); -// Sandbox created in San Francisco region -``` - -**Subsequent requests** to the same sandbox ID route to the same location, even if the user moves. - -**Why this matters**: -- First request has higher latency (provisioning) -- Later requests are fast (already running) -- Users far from sandbox location see higher latency - -### Multi-region considerations - -For global applications: - -**Option 1: User-local sandboxes** -```typescript -const region = getRegionFromRequest(request); -const sandbox = getSandbox(env.Sandbox, `${userId}-${region}`); -``` - -**Benefits**: Low latency for all users -**Drawbacks**: Multiple sandboxes per user, higher cost - -**Option 2: Single sandbox per user** -```typescript -const sandbox = getSandbox(env.Sandbox, userId); -``` - -**Benefits**: Simpler, lower cost -**Drawbacks**: Some users see higher latency - -## Cost implications - -Sandbox lifecycle affects costs: - -### Billable states - -- **Active time** - When sandbox is processing requests or running processes -- **Idle time** - May have reduced or no charges (Cloudflare-specific) - -### Cost optimization strategies - -**1. Destroy temporary sandboxes**: -```typescript -// Bad - sandbox stays alive forever -const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); -await sandbox.exec('npm test'); - -// Good - explicit cleanup -try { - const sandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); - await sandbox.exec('npm test'); -} finally { - await sandbox.destroy(); + await sandbox.destroy(); // Clean up temporary sandboxes } ``` -**2. Reuse long-lived sandboxes**: -```typescript -// Good - one sandbox per user -const sandbox = getSandbox(env.Sandbox, userId); -``` - -**3. Stop unused processes**: -```typescript -// Stop processes when done -await sandbox.killAllProcesses(); -``` - -**4. Batch operations**: -```typescript -// Good - multiple commands in one request -await sandbox.exec('npm install && npm test && npm run build'); - -// Less efficient - three separate requests -await sandbox.exec('npm install'); -await sandbox.exec('npm test'); -await sandbox.exec('npm run build'); -``` - -## Failure modes +**Destroy when**: Session ends, task completes, resources no longer needed -Understanding how sandboxes fail helps with resilience: +**Don't destroy**: Personal environments, long-running services -### Container crashes +### Failure recovery -If the container crashes (rare): +If container crashes or Durable Object is evicted (rare): -**Symptoms**: -- Commands fail with connection errors -- Background processes disappear -- Filesystem may be lost - -**Recovery**: ```typescript try { await sandbox.exec('command'); } catch (error) { if (error.message.includes('container') || error.message.includes('connection')) { - // Retry - container will be recreated - await sandbox.exec('command'); + await sandbox.exec('command'); // Retry - container recreates } } ``` -### Durable Object eviction - -If the Durable Object is evicted (very rare): - -**Symptoms**: -- Similar to container crash -- State is lost -- Next request creates new instance - -**Recovery**: -- Application should handle gracefully -- Recreate necessary state -- Consider persisting critical data elsewhere (R2, D1) - -### Out of resources - -If sandbox hits resource limits: - -**Symptoms**: -- Commands fail with memory/disk errors -- Slow performance -- Timeouts - -**Recovery**: -```typescript -// Clean up to free resources -await sandbox.killAllProcesses(); -await sandbox.exec('rm -rf /tmp/*'); - -// Or start fresh -await sandbox.destroy(); -const newSandbox = getSandbox(env.Sandbox, `${oldId}-v2`); -``` - ## Best practices -### Lifecycle management - - **Name consistently** - Use clear, predictable naming schemes -- **Clean up temporary sandboxes** - Destroy when done -- **Reuse long-lived sandboxes** - Don't create unnecessarily -- **Handle failures** - Retry or recreate on errors - -### State management - -- **Design for state** - Embrace persistence, don't fight it -- **Clean up resources** - Stop processes, delete files when done -- **Persist critical data externally** - Don't rely solely on sandbox state -- **Version sandbox IDs** - Add version suffix for clean slate - -### Performance - -- **Keep sandboxes warm** - Periodic requests prevent cold starts -- **Batch operations** - Combine multiple commands -- **Colocate users** - Consider geography for global apps -- **Monitor latency** - Track cold vs warm start performance +- **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 overall system +- [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 index 827e39cc3bbb849..0c8717ab42f9a39 100644 --- a/src/content/docs/sandbox/concepts/security.mdx +++ b/src/content/docs/sandbox/concepts/security.mdx @@ -5,200 +5,68 @@ sidebar: order: 6 --- -This page explains the security mechanisms in the Sandbox SDK, how isolation works, and what protections are in place. - -## Security layers - -The SDK implements multiple layers of security: - -``` -┌─────────────────────────────────────────────────┐ -│ Application Layer │ -│ (Your validation and authentication) │ -└────────────────────┬────────────────────────────┘ - │ -┌────────────────────▼────────────────────────────┐ -│ SDK Client Layer │ -│ (Input validation, error mapping) │ -└────────────────────┬────────────────────────────┘ - │ -┌────────────────────▼────────────────────────────┐ -│ Durable Object Layer │ -│ (Authentication, rate limiting) │ -└────────────────────┬────────────────────────────┘ - │ -┌────────────────────▼────────────────────────────┐ -│ Container Runtime Layer │ -│ (Path validation, command sanitization) │ -└────────────────────┬────────────────────────────┘ - │ -┌────────────────────▼────────────────────────────┐ -│ Container Isolation │ -│ (Linux namespaces, resource limits) │ -└──────────────────────────────────────────────────┘ -``` - -Each layer provides independent security controls. +The Sandbox SDK is built on [Containers](/containers/), which run each sandbox in its own VM for strong isolation. ## Container isolation -### Process isolation - -Containers use Linux namespaces for isolation: - -**PID namespace**: -- Each sandbox has its own process tree -- Processes cannot see processes in other sandboxes -- Cannot send signals to processes in other sandboxes - -```bash -# Inside sandbox -ps aux -# Only shows processes in this sandbox -``` - -**Network namespace**: -- Isolated network stack -- Cannot sniff traffic from other sandboxes -- Cannot interfere with other sandbox's network - -**Mount namespace**: -- Isolated filesystem view -- Cannot access files in other sandboxes -- Cannot see host filesystem +Each sandbox runs in a separate VM, providing complete isolation: -**User namespace**: -- User ID remapping -- Root in container ≠ root on host -- Limits privilege escalation +- **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 -### Resource limits - -Containers enforce resource quotas: - -**CPU limits**: -- Time-based CPU allocation -- Prevents CPU monopolization -- Fair scheduling between sandboxes - -**Memory limits**: -- Maximum RAM per sandbox -- Prevents memory exhaustion -- OOM killer activates if exceeded - -**Disk limits**: -- Maximum disk space per sandbox -- Prevents disk filling -- Quota enforcement - -**Process limits**: -- Maximum number of processes -- Prevents fork bombs -- Process count restrictions - -### Network isolation - -**Outbound connections**: -- Allowed (can call external APIs) -- Rate limited (prevents abuse) -- Monitored (unusual patterns detected) - -**Inbound connections**: -- Blocked by default -- Only through preview URLs (controlled exposure) -- No direct container access - -**Inter-sandbox communication**: -- Prohibited (sandboxes cannot talk directly) -- Must use external services -- No shared network - -## Input validation +For complete security details about the underlying container platform, see [Containers architecture](/containers/platform-details/architecture/). -### Path validation +## Within a sandbox -File operations validate paths: +All code within a single sandbox shares resources: -**Allowed paths**: -- `/workspace` - Application files -- `/tmp` - Temporary files -- `/home` - User files +- **Filesystem** - All processes see the same files +- **Processes** - All sessions can see all processes +- **Network** - Processes can communicate via localhost -**Blocked paths**: -- `/etc` - System configuration -- `/sys` - System interfaces -- `/proc` - Process information -- `..` - Parent directory traversal +For complete isolation, use separate sandboxes per user: ```typescript -// Validation example -await sandbox.writeFile('/workspace/safe.txt', 'data'); // ✅ Allowed - -await sandbox.writeFile('/etc/passwd', 'data'); // ❌ Blocked -// Error: Access denied - path outside allowed directories - -await sandbox.writeFile('../../../etc/passwd', 'data'); // ❌ Blocked -// Error: Path traversal detected -``` - -### Command validation - -Commands are validated for dangerous patterns: - -**Dangerous patterns blocked**: -- Privilege escalation attempts (`sudo`, `su`) -- Kernel module loading (`insmod`, `modprobe`) -- Host filesystem access attempts -- Network scanning tools (in some configurations) +// Good - Each user in separate sandbox +const userSandbox = getSandbox(env.Sandbox, `user-${userId}`); -```bash -# These may be blocked or restricted -sudo apt install package # Blocked - no sudo -insmod kernel_module.ko # Blocked - no kernel access +// Bad - Users sharing one sandbox +const shared = getSandbox(env.Sandbox, 'shared'); +// Users can read each other's files! ``` -**Safe patterns allowed**: -- Package installation (`apt`, `pip`, `npm`) -- File operations (`cp`, `mv`, `rm`) -- Process management (`ps`, `kill`) -- Standard utilities - -### Port validation - -Port exposure validates port numbers: +## Input validation -**Allowed ports**: -- User ports: 1024-65535 -- Standard web: 3000, 8000, 8080, 5173 +### Command injection -**Blocked ports**: -- System ports: 0-1023 (require privileges) -- Reserved ranges (platform-specific) +Always validate user input before using it in commands: ```typescript -await sandbox.exposePort(3000); // ✅ Allowed - -await sandbox.exposePort(80); // ❌ May be blocked -// Error: Port 80 is reserved -``` +// Dangerous - user input directly in command +const filename = userInput; +await sandbox.exec(`cat ${filename}`); +// User could input: "file.txt; rm -rf /" -## Authentication and authorization +// Safe - validate input +const filename = userInput.replace(/[^a-zA-Z0-9._-]/g, ''); +await sandbox.exec(`cat ${filename}`); -### Sandbox access control +// Better - use file API +await sandbox.writeFile('/tmp/input', userInput); +await sandbox.exec('cat /tmp/input'); +``` -Access to sandboxes is controlled: +## Authentication -**Sandbox ID as access token**: -- Knowing the sandbox ID grants access -- IDs are unpredictable (hard to guess) -- But not cryptographically secure +### Sandbox access -**Best practice**: Add application-level auth: +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 { - // Verify user authentication const userId = await authenticate(request); if (!userId) { return new Response('Unauthorized', { status: 401 }); @@ -206,24 +74,18 @@ export default { // User can only access their sandbox const sandbox = getSandbox(env.Sandbox, userId); - return Response.json({ sandbox: 'authorized' }); + return Response.json({ authorized: true }); } }; ``` -### Preview URL access - -Preview URLs are public: +### Preview URLs -**No built-in authentication**: -- URLs are publicly accessible -- Anyone with URL can access -- No password required - -**Add authentication in your service**: +Preview URLs are public. Add authentication in your service: ```python from flask import Flask, request, abort +import os app = Flask(__name__) @@ -238,111 +100,27 @@ def get_data(): return {'data': 'protected'} ``` -## Code execution safety - -### Untrusted code - -When executing untrusted code: - -**Use separate sandboxes**: -```typescript -// Each user gets their own sandbox -const userSandbox = getSandbox(env.Sandbox, `user-${userId}`); - -// Execute user's code in their sandbox -await userSandbox.exec('python user_script.py'); -``` - -**Don't share sandboxes**: -```typescript -// ❌ Bad - all users share one sandbox -const sharedSandbox = getSandbox(env.Sandbox, 'shared'); -await sharedSandbox.exec(untrustedCode); // Users can affect each other - -// ✅ Good - separate sandboxes -const sandbox = getSandbox(env.Sandbox, `user-${userId}`); -await sandbox.exec(untrustedCode); // Isolated -``` - -### Command injection - -Prevent command injection: - -**Dangerous**: -```typescript -// ❌ User input directly in command -const filename = userInput; -await sandbox.exec(`cat ${filename}`); // Vulnerable! - -// User could input: "file.txt; rm -rf /" -``` - -**Safe**: -```typescript -// ✅ Option 1: Validate input -const filename = userInput.replace(/[^a-zA-Z0-9._-]/g, ''); -await sandbox.exec(`cat ${filename}`); - -// ✅ Option 2: Use file API -await sandbox.writeFile('/tmp/input', userInput); -await sandbox.exec('cat /tmp/input'); -``` - -### Resource exhaustion +## Secrets management -Prevent resource abuse: +Use environment variables, not hardcoded secrets: -**CPU exhaustion**: ```typescript -// ❌ Unbounded execution -await sandbox.exec('python infinite_loop.py'); // Runs forever - -// ✅ With timeout -try { - await sandbox.exec('python script.py', { timeout: 30000 }); -} catch (error) { - if (error.message.includes('timeout')) { - console.log('Script timed out'); - await sandbox.killAllProcesses(); - } -} -``` - -**Memory exhaustion**: -```typescript -// Monitor and limit -const processes = await sandbox.listProcesses(); -if (processes.length > 10) { - console.log('Too many processes, cleaning up'); - await sandbox.killAllProcesses(); -} -``` - -## Data security - -### Sensitive data - -Handle sensitive data carefully: +// Bad - hardcoded in file +await sandbox.writeFile('/workspace/config.js', ` + const API_KEY = 'sk_live_abc123'; +`); -**Environment variables**: -```typescript -// ✅ Good - use environment variables +// Good - use environment variables await sandbox.startProcess('node app.js', { env: { - API_KEY: env.API_KEY, // From Worker env binding - DB_PASSWORD: env.DB_PASSWORD + API_KEY: env.API_KEY, // From Worker environment binding } }); - -// ❌ Bad - hardcoded secrets -await sandbox.writeFile('/workspace/config.js', ` - const API_KEY = 'sk_live_abc123'; // Exposed in filesystem -`); ``` -**Temporary data**: +Clean up temporary sensitive data: + ```typescript -// Clean up after use try { await sandbox.writeFile('/tmp/sensitive.txt', secretData); await sandbox.exec('python process.py /tmp/sensitive.txt'); @@ -351,281 +129,50 @@ try { } ``` -**Persistent data**: -```typescript -// Don't store secrets in sandbox -// Use external secret management (Cloudflare secrets) -const apiKey = env.API_KEY; // From environment, not filesystem -``` - -### Data isolation - -Data is isolated per sandbox: - -**Between sandboxes**: -- No shared filesystem -- No shared memory -- No shared processes - -**Within sandbox**: -- All processes share filesystem -- All sessions share filesystem -- No isolation within sandbox - -For complete isolation: -```typescript -// ✅ Each user in separate sandbox -const sandbox1 = getSandbox(env.Sandbox, 'user-1'); -const sandbox2 = getSandbox(env.Sandbox, 'user-2'); - -// ❌ Users sharing one sandbox -const shared = getSandbox(env.Sandbox, 'shared'); -// user-1 can read user-2's files -``` - -## Network security - -### Outbound requests - -Sandboxes can make outbound requests: - -**Consider**: -- Rate limiting (prevent abuse) -- Allowlisting (only allow specific domains) -- Monitoring (detect unusual patterns) - -**Example allowlist**: -```typescript -const ALLOWED_HOSTS = [ - 'api.openai.com', - 'api.anthropic.com', - 'api.github.com' -]; - -// Validate before execution -const url = new URL(userProvidedUrl); -if (!ALLOWED_HOSTS.includes(url.hostname)) { - throw new Error('Domain not allowed'); -} -``` - -### Inbound access - -Preview URLs expose services: - -**Public by default**: -- Anyone with URL can access -- No IP restrictions -- No geographic restrictions - -**Add protection**: -```typescript -// Rate limiting -const rateLimit = new Map(); - -app.use((req, res, next) => { - const ip = req.headers['cf-connecting-ip']; - const count = rateLimit.get(ip) || 0; +## What the SDK protects against - if (count > 100) { - return res.status(429).send('Rate limit exceeded'); - } +- Sandbox-to-sandbox access (VM isolation) +- Resource exhaustion (enforced quotas) +- Container escapes (VM-based isolation) - rateLimit.set(ip, count + 1); - next(); -}); +## What you must implement -// Authentication -app.use((req, res, next) => { - const token = req.headers.authorization; - if (!isValidToken(token)) { - return res.status(401).send('Unauthorized'); - } - next(); -}); -``` +- Authentication and authorization +- Input validation and sanitization +- Rate limiting +- Application-level security (SQL injection, XSS, etc.) ## Best practices -### Defense in depth - -Layer multiple security controls: - +**Use separate sandboxes for isolation**: ```typescript -export default { - async fetch(request: Request, env: Env): Promise { - // Layer 1: Authentication - const userId = await authenticateUser(request); - if (!userId) { - return new Response('Unauthorized', { status: 401 }); - } - - // Layer 2: Authorization - if (!await canAccessResource(userId, resourceId)) { - return new Response('Forbidden', { status: 403 }); - } - - // Layer 3: Input validation - const input = await request.json(); - if (!validateInput(input)) { - return new Response('Invalid input', { status: 400 }); - } - - // Layer 4: Rate limiting - if (await isRateLimited(userId)) { - return new Response('Rate limited', { status: 429 }); - } - - // Layer 5: Sandbox isolation - const sandbox = getSandbox(env.Sandbox, userId); - - // Layer 6: Command sanitization - const safeInput = sanitizeInput(input); - await sandbox.exec(`python script.py "${safeInput}"`); - - return new Response('Success'); - } -}; +const sandbox = getSandbox(env.Sandbox, `user-${userId}`); ``` -### Least privilege - -Grant minimal necessary access: - +**Validate all inputs**: ```typescript -// ✅ Good - user can only access their sandbox -const userSandbox = getSandbox(env.Sandbox, userId); - -// ❌ Bad - user can access any sandbox -const anySandbox = getSandbox(env.Sandbox, request.params.sandboxId); +const safe = input.replace(/[^a-zA-Z0-9._-]/g, ''); +await sandbox.exec(`command ${safe}`); ``` -### Secure by default - -Design for security from the start: - +**Use environment variables for secrets**: ```typescript -// ✅ Default deny -const ALLOWED_COMMANDS = ['python', 'node', 'npm', 'pip']; - -function isAllowed(command: string): boolean { - const cmd = command.split(' ')[0]; - return ALLOWED_COMMANDS.includes(cmd); -} - -// ❌ Default allow -await sandbox.exec(anyCommand); // No validation +await sandbox.startProcess('node app.js', { + env: { API_KEY: env.API_KEY } +}); ``` -### Regular cleanup - -Clean up resources to prevent leaks: - +**Clean up temporary resources**: ```typescript try { const sandbox = getSandbox(env.Sandbox, sessionId); await sandbox.exec('npm test'); } finally { - // Always clean up temporary sandboxes await sandbox.destroy(); } ``` -### Monitoring - -Monitor for security issues: - -```typescript -// Log security events -console.log('Security event:', { - userId, - action: 'sandbox_access', - sandboxId, - timestamp: new Date().toISOString() -}); - -// Alert on suspicious patterns -if (requestCount > threshold) { - await sendAlert('High request rate detected', { userId }); -} -``` - -## Threat model - -### What the SDK protects against - -**Container escapes**: -- ✅ Prevented by Linux namespaces -- ✅ Container runtime isolation -- ✅ Cloudflare infrastructure security - -**Resource exhaustion**: -- ✅ CPU limits enforced -- ✅ Memory limits enforced -- ✅ Disk quotas enforced - -**Path traversal**: -- ✅ Validated at runtime -- ✅ Restricted to safe directories -- ✅ Parent traversal blocked - -**Command injection**: -- ✅ Command sanitization -- ⚠️ Application must validate inputs - -**Data leaks between sandboxes**: -- ✅ Filesystem isolation -- ✅ Process isolation -- ✅ Network isolation - -### What the SDK doesn't protect against - -**Application-level vulnerabilities**: -- ❌ SQL injection in your code -- ❌ XSS in your web service -- ❌ Authentication bypass in your app - -**Social engineering**: -- ❌ Users sharing sandbox IDs -- ❌ Phishing for preview URLs - -**Denial of service (application-level)**: -- ❌ Expensive computations -- ❌ Large file uploads -- ❌ Slow database queries - -**Data exfiltration**: -- ❌ Sending data to external services -- ❌ DNS tunneling -- ❌ Steganography - -**You must implement**: -- Authentication and authorization -- Input validation -- Rate limiting -- Monitoring and alerting -- Audit logging - -## Security checklist - -Before deploying to production: - -- [ ] **Authentication** - Users must authenticate -- [ ] **Authorization** - Users can only access their sandboxes -- [ ] **Input validation** - All user inputs validated -- [ ] **Output encoding** - Prevent XSS in preview URLs -- [ ] **Rate limiting** - Prevent abuse -- [ ] **Monitoring** - Track usage and anomalies -- [ ] **Secrets management** - No hardcoded secrets -- [ ] **HTTPS only** - Enforce secure connections -- [ ] **Regular cleanup** - Destroy temporary sandboxes -- [ ] **Error handling** - Don't leak sensitive info in errors -- [ ] **Audit logging** - Log security-relevant events -- [ ] **Incident response** - Plan for security incidents - ## Related resources -- [Architecture](/sandbox/concepts/architecture/) - How security is layered -- [Container runtime](/sandbox/concepts/containers/) - Isolation mechanisms +- [Containers architecture](/containers/platform-details/architecture/) - Underlying platform security - [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Resource management -- [Best practices](/sandbox/best-practices/) - Security recommendations diff --git a/src/content/docs/sandbox/concepts/sessions.mdx b/src/content/docs/sandbox/concepts/sessions.mdx index 4014b3134cc544e..035617fb8090a18 100644 --- a/src/content/docs/sandbox/concepts/sessions.mdx +++ b/src/content/docs/sandbox/concepts/sessions.mdx @@ -5,503 +5,149 @@ sidebar: order: 4 --- -This page explains what sessions are, when to use them, and how they differ from sandboxes. - -## What are sessions? - -Sessions provide isolated execution contexts within a sandbox. While sandboxes provide container-level isolation, sessions provide logical isolation within the same container. - -Think of it like this: +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 user account on that computer (execution context) - -Multiple sessions can run in one sandbox, each with: -- Isolated environment variables -- Separate working directories -- Independent shell state -- Isolated code interpreter contexts +- **Session** = A terminal shell session in that computer -But sessions share: -- The same filesystem -- The same running processes -- The same exposed ports +## Default session -## Why use sessions? +Every sandbox has a default session that maintains shell state across commands: -### When sessions make sense - -**Multi-tenant applications**: ```typescript -// Each user gets a session in a shared sandbox -const session = await sandbox.createSession({ - id: `user-${userId}`, - env: { - USER_ID: userId, - API_KEY: userApiKey - } -}); -``` +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); -**Isolated test environments**: -```typescript -// Each test run in its own session -const session = await sandbox.createSession({ - id: `test-${testName}`, - workingDirectory: `/workspace/test-${testName}` -}); -``` +// These commands run in the default session +await sandbox.exec("cd /app"); +await sandbox.exec("pwd"); // Output: /app -**Temporary isolated contexts**: -```typescript -// One-time code execution -const session = await sandbox.createSession(); -await session.exec('python untrusted-script.py'); -await sandbox.deleteSession(session.id); +await sandbox.exec("export MY_VAR=hello"); +await sandbox.exec("echo $MY_VAR"); // Output: hello ``` -### When separate sandboxes make more sense - -Use separate sandboxes instead of sessions when you need: +Shell state persists: working directory, environment variables, exported variables all carry over between commands. -**Complete isolation**: -```typescript -// Untrusted users - separate sandboxes -const sandbox = getSandbox(env.Sandbox, `user-${userId}`); -``` +## Creating sessions -**Long-running services**: -```typescript -// Each service in its own sandbox -const apiSandbox = getSandbox(env.Sandbox, 'api-server'); -const workerSandbox = getSandbox(env.Sandbox, 'background-worker'); -``` +Create additional sessions for isolated shell contexts: -**Different lifecycle**: ```typescript -// Some sandboxes persist, others are temporary -const persistentSandbox = getSandbox(env.Sandbox, userId); -const tempSandbox = getSandbox(env.Sandbox, `temp-${Date.now()}`); -``` - -## How sessions work - -### Session creation - -Sessions are lightweight execution contexts: +const buildSession = await sandbox.createSession({ + name: "build", + env: { NODE_ENV: "production" }, + cwd: "/build" +}); -```typescript -const session = await sandbox.createSession({ - id: 'my-session', // Optional: Auto-generated if not provided - workingDirectory: '/workspace/project', // Optional: Defaults to /workspace - env: { - NODE_ENV: 'production', - API_KEY: 'secret' - } +const testSession = await sandbox.createSession({ + name: "test", + env: { NODE_ENV: "test" }, + cwd: "/test" }); -console.log('Session ID:', session.id); +// Different shell contexts +await buildSession.exec("npm run build"); +await testSession.exec("npm test"); ``` -**What happens**: -1. Sandbox creates an execution context -2. Environment variables are set -3. Working directory is configured -4. Session object is returned - -**Cost**: Minimal - just metadata tracking - -### Session API +## What's isolated per session -Sessions provide the same API as sandboxes: +Each session has its own: +**Shell environment**: ```typescript -// Execute in session -const result = await session.exec('node server.js'); - -// File operations -await session.writeFile('/workspace/config.json', data); - -// Background processes -const process = await session.startProcess('npm run dev'); - -// Code interpreter -const context = await session.createCodeContext({ language: 'python' }); +await session1.exec("export MY_VAR=hello"); +await session2.exec("echo $MY_VAR"); // Empty - different shell ``` -All operations run in the session's context. - -### Session lifecycle - -Sessions exist until explicitly deleted or sandbox is destroyed: - +**Working directory**: ```typescript -// Create session -const session = await sandbox.createSession({ id: 'temp' }); - -// Use session -await session.exec('python script.py'); - -// Clean up -await sandbox.deleteSession('temp'); -// Session is gone, but sandbox and its filesystem remain +await session1.exec("cd /workspace/project1"); +await session2.exec("pwd"); // Different working directory ``` -## Session isolation - -### What's isolated - -Each session has its own: - -**Environment variables**: +**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' } }); - -// Sessions see different environment variables ``` -**Working directory**: -```typescript -const session1 = await sandbox.createSession({ - workingDirectory: '/workspace/project1' -}); +## What's shared -const session2 = await sandbox.createSession({ - workingDirectory: '/workspace/project2' -}); - -// Relative paths resolve differently in each session -``` - -**Shell state**: -```typescript -// In session1 -await session1.exec('export MY_VAR=hello'); - -// In session2 -await session2.exec('echo $MY_VAR'); -// Outputs nothing - MY_VAR not set in session2 -``` - -### What's shared - -Sessions in the same sandbox share: +All sessions in a sandbox share: **Filesystem**: ```typescript -// Session1 writes a file -await session1.writeFile('/workspace/shared.txt', 'data'); - -// Session2 can read it -const file = await session2.readFile('/workspace/shared.txt'); -// Returns 'data' - filesystem is shared +await session1.writeFile('/workspace/file.txt', 'data'); +await session2.readFile('/workspace/file.txt'); // Can read it ``` **Processes**: ```typescript -// Session1 starts a server await session1.startProcess('node server.js'); - -// Session2 sees it -const processes = await session2.listProcesses(); -// Includes server.js process +await session2.listProcesses(); // Sees the server ``` **Ports**: ```typescript -// Session1 exposes a port await session1.exposePort(3000); - -// Session2 sees it -const ports = await session2.getExposedPorts(); -// Includes port 3000 +await session2.getExposedPorts(); // Sees port 3000 ``` -**Code interpreter contexts**: -```typescript -// Session1 creates a context -const context = await session1.createCodeContext({ - language: 'python' -}); - -// Session2 can access it using the context ID -await session2.runCode(context.id, 'print("hello")'); -``` - -## Common patterns - -### Per-user sessions +## When to use sessions -Isolate users within a shared sandbox: +**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 -export default { - async fetch(request: Request, env: Env): Promise { - const userId = getUserId(request); - - // Shared sandbox - const sandbox = getSandbox(env.Sandbox, 'shared-environment'); - - // User-specific session - const session = await sandbox.getSession(`user-${userId}`); - - if (!session) { - // Create session on first request - await sandbox.createSession({ - id: `user-${userId}`, - workingDirectory: `/workspace/users/${userId}`, - env: { - USER_ID: userId, - USER_API_KEY: await getUserApiKey(userId) - } - }); - } - - // Use session for user-specific operations - const userSession = await sandbox.getSession(`user-${userId}`); - const result = await userSession.exec('python analyze.py'); - - return Response.json({ result }); - } -}; -``` - -**Benefits**: -- Lower cost (one sandbox for many users) -- Isolated environments per user -- Shared filesystem for common resources - -**Use when**: -- Users are trusted -- Cost optimization is important -- Shared resources are beneficial - -### Temporary execution contexts - -One-time isolated execution: - -```typescript -async function executeUntrustedCode(code: string) { - const sandbox = getSandbox(env.Sandbox, 'code-executor'); - - // Create temporary session - const session = await sandbox.createSession({ - id: `temp-${Date.now()}`, - workingDirectory: `/tmp/exec-${Date.now()}`, - env: { - EXECUTION_ID: Date.now().toString() - } - }); - - try { - // Execute code in session - await session.writeFile('script.py', code); - const result = await session.exec('python script.py'); - - return result; - } finally { - // Always clean up - await sandbox.deleteSession(session.id); - } -} -``` - -**Benefits**: -- Clean state for each execution -- Resource cleanup after use -- Isolated environment variables - -### Test isolation - -Separate test contexts: - -```typescript -async function runTests() { - const sandbox = getSandbox(env.Sandbox, 'test-runner'); - - const tests = ['auth', 'api', 'database']; - const results = []; - - for (const test of tests) { - // Each test in its own session - const session = await sandbox.createSession({ - id: `test-${test}`, - workingDirectory: `/workspace/tests/${test}`, - env: { - TEST_NAME: test, - NODE_ENV: 'test' - } - }); - - const result = await session.exec(`npm test -- ${test}`); - results.push({ test, passed: result.success }); - - // Clean up after each test - await sandbox.deleteSession(session.id); - } - - return results; -} -``` - -**Benefits**: -- Isolated test environments -- No test pollution -- Parallel test execution potential - -## Design considerations - -### When sessions are sufficient - -Sessions work well when: - -**Isolation needs are limited**: -- Environment variables need to differ -- Working directories need to differ -- Shell state needs to differ - -**Resources are shared**: -- Multiple users share compute -- Common files are accessed -- Processes are shared services - -**Cost is a priority**: -- One sandbox is cheaper than many -- Shared resources reduce overhead - -### When to use separate sandboxes - -Use separate sandboxes when: - -**Strong isolation is required**: -- Untrusted code execution -- Security-sensitive applications -- Regulatory compliance needs - -**Independent lifecycle**: -- Some environments are long-lived -- Some are temporary -- Different destruction times - -**Resource separation**: -- Each user needs dedicated compute -- Independent processes required -- No shared state desired - -## Performance implications - -### Session creation cost - -Sessions are lightweight: - -- **Creation time**: \<1ms (just metadata) -- **Memory overhead**: Minimal (environment variables only) -- **No container startup**: Uses existing sandbox - -Compare to sandbox creation: - -- **Cold start**: 100-300ms (container provisioning) -- **Memory overhead**: Full Linux environment -- **Isolation overhead**: Container boundaries - -### Execution performance - -Sessions have no performance overhead: - -```typescript -// Direct sandbox execution -const result1 = await sandbox.exec('python script.py'); +// 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"'); -// Session execution - same speed -const result2 = await session.exec('python script.py'); +// Phase 2: Run the code (without API keys) +const appSession = await sandbox.createSession({ + name: "app", + env: { PORT: "3000" } +}); +await appSession.exec("node server.js"); ``` -Both execute identically in the container. - -### Resource limits - -Sessions share sandbox resources: - -- **CPU**: Shared between all sessions in sandbox -- **Memory**: Shared pool -- **Disk**: Shared filesystem -- **Network**: Shared bandwidth - -If one session uses excessive resources, it affects all sessions in that sandbox. +**Use separate sandboxes when**: +- You need complete isolation (untrusted code) +- Different users require fully separated environments +- Independent resource allocation is needed ## Best practices -### Session naming - -Use descriptive, unique session IDs: - +**Clean up temporary sessions**: ```typescript -// ✅ Good - clear and unique -await sandbox.createSession({ id: `user-${userId}` }); -await sandbox.createSession({ id: `test-${testName}-${timestamp}` }); - -// ❌ Bad - generic or conflicting -await sandbox.createSession({ id: 'session' }); -await sandbox.createSession({ id: 'temp' }); // Might conflict -``` - -### Session cleanup - -Always clean up temporary sessions: - -```typescript -// ✅ Good - explicit cleanup try { - const session = await sandbox.createSession({ id: 'temp' }); + const session = await sandbox.createSession({ name: 'temp' }); await session.exec('command'); } finally { await sandbox.deleteSession('temp'); } - -// ❌ Bad - session lingers -const session = await sandbox.createSession(); -await session.exec('command'); -// Session never deleted ``` -### Environment isolation - -Don't rely on sessions for security: - +**Sessions share filesystem**: ```typescript -// ❌ Bad - sessions share filesystem -const userSession = await sandbox.createSession({ id: userId }); -await userSession.exec('rm -rf /workspace/*'); // Affects all users! +// Bad - affects all sessions +await session.exec('rm -rf /workspace/*'); -// ✅ Good - use separate sandboxes for untrusted users +// Use separate sandboxes for untrusted isolation const userSandbox = getSandbox(env.Sandbox, userId); -await userSandbox.exec('rm -rf /workspace/*'); // Only affects this user -``` - -### Working directories - -Use session working directories for organization: - -```typescript -// ✅ Good - organized per session -await sandbox.createSession({ - id: 'user-123', - workingDirectory: '/workspace/users/123' -}); - -// Commands run in user's directory automatically -await session.exec('npm install'); // Installs in /workspace/users/123 ``` ## Related resources -- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox vs session lifecycle -- [Architecture](/sandbox/concepts/architecture/) - How sessions fit in the system -- [Security model](/sandbox/concepts/security/) - Session isolation limits -- [Sessions API](/sandbox/api/sessions/) - Complete session management API +- [Sandbox lifecycle](/sandbox/concepts/sandboxes/) - Understanding sandbox management +- [Sessions API](/sandbox/api/sessions/) - Complete session API reference From 41020c479c2d1e8ed141a27c260a2367c0efece4 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 17:56:46 +0100 Subject: [PATCH 16/22] Cleanup API reference --- src/content/docs/sandbox/api/commands.mdx | 407 +-------- src/content/docs/sandbox/api/files.mdx | 489 +---------- src/content/docs/sandbox/api/index.mdx | 153 ---- src/content/docs/sandbox/api/interpreter.mdx | 831 ++----------------- src/content/docs/sandbox/api/ports.mdx | 774 +---------------- src/content/docs/sandbox/api/sessions.mdx | 574 ++----------- 6 files changed, 221 insertions(+), 3007 deletions(-) diff --git a/src/content/docs/sandbox/api/commands.mdx b/src/content/docs/sandbox/api/commands.mdx index 1a752764348141a..d96ed92db4505a1 100644 --- a/src/content/docs/sandbox/api/commands.mdx +++ b/src/content/docs/sandbox/api/commands.mdx @@ -7,7 +7,7 @@ sidebar: import { Details, TypeScriptExample } from "~/components"; -The Commands API lets you execute commands in the sandbox and manage background processes. All command execution happens in the sandbox's isolated container environment. +Execute commands and manage background processes in the sandbox's isolated container environment. ## Methods @@ -19,102 +19,33 @@ Execute a command and return the complete result. 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 -#### Parameters - -- **command** (`string`, required) - The command to execute. Can include arguments. -- **options** (`ExecOptions`, optional) - Execution options: - - `stream` (`boolean`) - Enable streaming callbacks (default: false) - - `onOutput` (`(stream: 'stdout' | 'stderr', data: string) => void`) - Callback for real-time output - - `timeout` (`number`) - Maximum execution time in milliseconds - -#### Return value - -Returns a `Promise` with: - -- `success` (`boolean`) - Whether the command succeeded (exitCode === 0) -- `stdout` (`string`) - Standard output from the command -- `stderr` (`string`) - Standard error from the command -- `exitCode` (`number`) - Process exit code - -#### Examples - -**Basic command execution:** +**Returns**: `Promise` with `success`, `stdout`, `stderr`, `exitCode` ``` -const result = await sandbox.exec('python --version'); - -console.log(result.stdout); // "Python 3.11.0" -console.log(result.success); // true -console.log(result.exitCode); // 0 -``` - - -**Command with arguments:** - - -``` -const result = await sandbox.exec('python -c "print(2 + 2)"'); +const result = await sandbox.exec('npm run build'); if (result.success) { - console.log('Result:', result.stdout); // "4" + console.log('Build output:', result.stdout); } else { - console.error('Error:', result.stderr); + console.error('Build failed:', result.stderr); } -``` - - - -**Streaming output:** - -``` -const result = await sandbox.exec('npm install express', { +// With streaming +await sandbox.exec('npm install', { stream: true, - onOutput: (stream, data) => { - console.log(`[${stream}] ${data}`); - } + onOutput: (stream, data) => console.log(`[${stream}] ${data}`) }); - -// Logs installation progress in real-time -// Still returns complete result when done -console.log('Final exit code:', result.exitCode); -``` - - - -**Error handling:** - - -``` -try { - const result = await sandbox.exec('nonexistent-command'); - - if (!result.success) { - console.log('Command failed with exit code:', result.exitCode); - console.log('Error output:', result.stderr); - } -} catch (error) { - console.error('Execution error:', error.message); -} ``` - -**Complex shell command:** - - -``` -// Use shell features like pipes and redirects -const result = await sandbox.exec('ls -la | grep ".py" | wc -l'); -console.log('Number of Python files:', result.stdout.trim()); -``` - - - ---- - ### `execStream()` Execute a command and return a Server-Sent Events stream for real-time processing. @@ -123,26 +54,11 @@ Execute a command and return a Server-Sent Events stream for real-time processin const stream = await sandbox.execStream(command: string, options?: ExecOptions): Promise ``` +**Parameters**: +- `command` - The command to execute +- `options` - Same as `exec()` -#### Parameters - -- **command** (`string`, required) - The command to execute -- **options** (`ExecOptions`, optional) - Same as `exec()` - -#### Return value - -Returns a `Promise` that emits `ExecEvent` objects via Server-Sent Events. - -Event types: -- `start` - Command execution started -- `stdout` - Standard output data -- `stderr` - Standard error data -- `complete` - Command finished -- `error` - Execution error occurred - -#### Examples - -**Basic streaming:** +**Returns**: `Promise` emitting `ExecEvent` objects (`start`, `stdout`, `stderr`, `complete`, `error`) ``` @@ -152,75 +68,20 @@ const stream = await sandbox.execStream('npm run build'); for await (const event of parseSSEStream(stream)) { switch (event.type) { - case 'start': - console.log('Build started:', event.command); - break; case 'stdout': console.log('Output:', event.data); break; - case 'stderr': - console.error('Error:', event.data); - break; case 'complete': - console.log('Build finished with exit code:', event.exitCode); + console.log('Exit code:', event.exitCode); break; case 'error': - console.error('Execution failed:', event.error); + console.error('Failed:', event.error); break; } } ``` - -**Streaming to client:** - - -``` -export default { - async fetch(request: Request, env: Env): Promise { - const sandbox = getSandbox(env.Sandbox, 'user-123'); - const stream = await sandbox.execStream('python long-running-script.py'); - - // Return stream directly to client - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - } - }); - } -}; -``` - - - -**Progress tracking:** - - -``` -let progress = 0; - -const stream = await sandbox.execStream('npm test'); - -for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout') { - if (event.data.includes('PASS')) { - progress++; - console.log(`Tests passed: ${progress}`); - } - } - - if (event.type === 'complete') { - console.log(`All tests complete. ${progress} passed.`); - } -} -``` - - - ---- - ### `startProcess()` Start a long-running background process. @@ -229,106 +90,45 @@ Start a long-running background process. 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 -#### Parameters - -- **command** (`string`, required) - The command to start as a background process -- **options** (`ProcessOptions`, optional): - - `cwd` (`string`) - Working directory - - `env` (`Record`) - Environment variables - -#### Return value - -Returns a `Promise` with: - -- `id` (`string`) - Unique process identifier -- `pid` (`number`) - Process ID -- `command` (`string`) - Command that was executed -- `status` (`string`) - Process status (running, exited, etc.) - -#### Examples - -**Start a web server:** +**Returns**: `Promise` with `id`, `pid`, `command`, `status` ``` const server = await sandbox.startProcess('python -m http.server 8000'); +console.log('Started with PID:', server.pid); -console.log('Server started with PID:', server.pid); -console.log('Process ID:', server.id); - -// Process runs in the background -// You can expose the port -await sandbox.exposePort(8000); -``` - - - -**Start multiple processes:** - - -``` -// Start a backend server -const api = await sandbox.startProcess('node api-server.js'); - -// Start a frontend dev server -const frontend = await sandbox.startProcess('npm run dev'); - -console.log('Started API server:', api.pid); -console.log('Started frontend:', frontend.pid); -``` - - - -**Process with custom environment:** - - -``` -const process = await sandbox.startProcess('node app.js', { +// With custom environment +const app = await sandbox.startProcess('node app.js', { cwd: '/workspace/my-app', - env: { - NODE_ENV: 'production', - PORT: '3000', - API_KEY: 'secret-key' - } + env: { NODE_ENV: 'production', PORT: '3000' } }); ``` - ---- - ### `listProcesses()` -List all running processes in the sandbox. +List all running processes. ```ts const processes = await sandbox.listProcesses(): Promise ``` - -#### Return value - -Returns a `Promise` with information about each running process. - -#### Example - ``` const processes = await sandbox.listProcesses(); for (const proc of processes) { - console.log(`Process ${proc.id}:`); - console.log(` PID: ${proc.pid}`); - console.log(` Command: ${proc.command}`); - console.log(` Status: ${proc.status}`); + console.log(`${proc.id}: ${proc.command} (PID ${proc.pid})`); } ``` - ---- - ### `killProcess()` Terminate a specific process. @@ -337,51 +137,31 @@ Terminate a specific process. await sandbox.killProcess(processId: string, signal?: string): Promise ``` - -#### Parameters - -- **processId** (`string`, required) - The process ID (from `startProcess()` or `listProcesses()`) -- **signal** (`string`, optional) - Signal to send (default: 'SIGTERM') - -#### Example +**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'); - -// Stop the server after some time -setTimeout(async () => { - await sandbox.killProcess(server.id); - console.log('Server stopped'); -}, 60000); +await sandbox.killProcess(server.id); ``` - ---- - ### `killAllProcesses()` -Terminate all running processes in the sandbox. +Terminate all running processes. ```ts await sandbox.killAllProcesses(): Promise ``` - -#### Example - ``` -// Clean up all processes await sandbox.killAllProcesses(); -console.log('All processes terminated'); ``` - ---- - ### `streamProcessLogs()` Stream logs from a running process in real-time. @@ -390,16 +170,10 @@ Stream logs from a running process in real-time. const stream = await sandbox.streamProcessLogs(processId: string): Promise ``` +**Parameters**: +- `processId` - The process ID -#### Parameters - -- **processId** (`string`, required) - The process ID - -#### Return value - -Returns a `Promise` that emits `LogEvent` objects. - -#### Example +**Returns**: `Promise` emitting `LogEvent` objects ``` @@ -411,17 +185,11 @@ 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')) { - console.log('Server is ready!'); - break; // Stop watching logs - } + if (log.data.includes('Server started')) break; } ``` - ---- - ### `getProcessLogs()` Get accumulated logs from a process. @@ -430,109 +198,22 @@ Get accumulated logs from a process. const logs = await sandbox.getProcessLogs(processId: string): Promise ``` +**Parameters**: +- `processId` - The process ID -#### Parameters - -- **processId** (`string`, required) - The process ID - -#### Return value - -Returns a `Promise` with all accumulated output from the process. - -#### Example +**Returns**: `Promise` with all accumulated output ``` const server = await sandbox.startProcess('node server.js'); - -// Wait a bit await new Promise(resolve => setTimeout(resolve, 5000)); -// Get all logs so far const logs = await sandbox.getProcessLogs(server.id); console.log('Server logs:', logs); ``` - -## Common patterns - -### Run and wait - -Execute a command and wait for it to complete: - - -``` -const result = await sandbox.exec('npm run build'); - -if (result.success) { - console.log('Build succeeded'); -} else { - throw new Error(`Build failed: ${result.stderr}`); -} -``` - - - -### Run in background - -Start a process and let it run: - - -``` -const process = await sandbox.startProcess('node server.js'); -await sandbox.exposePort(3000); - -// Process continues running -// Client can access it via the exposed URL -``` - - - -### Monitor progress - -Stream output and track progress: - - -``` -const stream = await sandbox.execStream('npm test'); -let passed = 0; -let failed = 0; - -for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout') { - if (event.data.includes('✓')) passed++; - if (event.data.includes('✗')) failed++; - } - - if (event.type === 'complete') { - console.log(`Tests: ${passed} passed, ${failed} failed`); - } -} -``` - - - -### Cleanup processes - -Stop all processes when done: - - -``` -try { - // Run your code - await sandbox.exec('npm run build'); -} finally { - // Always clean up - await sandbox.killAllProcesses(); -} -``` - - - ## Related resources -- [Execute commands guide](/sandbox/guides/execute-commands/) - Detailed guide with best practices - [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running processes -- [Streaming output guide](/sandbox/guides/streaming-output/) - Working with real-time output - [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 index 00f3d54190b0622..adfd7c8149c8150 100644 --- a/src/content/docs/sandbox/api/files.mdx +++ b/src/content/docs/sandbox/api/files.mdx @@ -7,100 +7,33 @@ sidebar: import { TypeScriptExample } from "~/components"; -The Files API provides methods for reading, writing, and managing files in the sandbox filesystem. All file operations use absolute paths within the container. +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 in the sandbox. +Write content to a file. ```ts await sandbox.writeFile(path: string, content: string, options?: WriteFileOptions): Promise ``` -#### Parameters - -- **path** (`string`, required) - Absolute path to the file -- **content** (`string`, required) - Content to write to the file -- **options** (`WriteFileOptions`, optional): - - `encoding` (`string`) - File encoding (default: 'utf-8') - - `mode` (`number`) - File permissions (default: 0o644) - -#### Examples - -**Write a text file:** - - -``` -await sandbox.writeFile('/workspace/app.js', `console.log('Hello from sandbox!');`); -``` - - -**Write with specific encoding:** - - -``` -// Write binary data as base64 -await sandbox.writeFile('/tmp/image.png', base64ImageData, { - encoding: 'base64' -}); -``` - - -**Write configuration file:** +**Parameters**: +- `path` - Absolute path to the file +- `content` - Content to write +- `options` (optional): + - `encoding` - File encoding (default: `"utf-8"`) ``` -const config = { - port: 3000, - environment: 'production', - apiKey: process.env.API_KEY -}; - -await sandbox.writeFile( -'/workspace/config.json', -JSON.stringify(config, null, 2) -); - -``` - - - -**Create and write multiple files:** - - -``` -// Package.json -await sandbox.writeFile('/workspace/package.json', JSON.stringify({ - name: 'my-app', - version: '1.0.0', - dependencies: { - express: '^4.18.0' - } -})); - -// Application code -await sandbox.writeFile('/workspace/server.js', ` -const express = require('express'); -const app = express(); - -app.get('/', (req, res) => { - res.json({ status: 'running' }); -}); +await sandbox.writeFile('/workspace/app.js', `console.log('Hello!');`); -app.listen(3000); -`); - -// Install and run -await sandbox.exec('npm install'); -await sandbox.startProcess('node server.js'); +// Binary data +await sandbox.writeFile('/tmp/image.png', base64Data, { encoding: 'base64' }); ``` - ---- - ### `readFile()` Read a file from the sandbox. @@ -109,144 +42,55 @@ Read a file from the sandbox. const file = await sandbox.readFile(path: string, options?: ReadFileOptions): Promise ``` -#### Parameters - -- **path** (`string`, required) - Absolute path to the file -- **options** (`ReadFileOptions`, optional): - - `encoding` (`string`) - File encoding (default: 'utf-8') - -#### Return value - -Returns a `Promise` with: - -- `content` (`string`) - File contents -- `encoding` (`string`) - Encoding used - -#### Examples - -**Read a text file:** - - -``` -const file = await sandbox.readFile('/workspace/app.js'); -console.log(file.content); -``` - +**Parameters**: +- `path` - Absolute path to the file +- `options` (optional): + - `encoding` - File encoding (default: `"utf-8"`) -**Read and parse JSON:** +**Returns**: `Promise` with `content` and `encoding` ``` const file = await sandbox.readFile('/workspace/package.json'); -const packageJson = JSON.parse(file.content); - -console.log('Project name:', packageJson.name); -console.log('Version:', packageJson.version); +const pkg = JSON.parse(file.content); +// Binary data +const image = await sandbox.readFile('/tmp/image.png', { encoding: 'base64' }); ``` - -**Read binary file:** - - -``` -const file = await sandbox.readFile('/tmp/image.png', { - encoding: 'base64' -}); - -// file.content is base64-encoded -const imageData = file.content; -``` - - - -**Check file contents:** - - -``` -const file = await sandbox.readFile('/workspace/.env'); - -if (file.content.includes('API_KEY=')) { -console.log('API key is configured'); -} - -``` - - - ---- - ### `mkdir()` -Create a directory in the sandbox. +Create a directory. ```ts await sandbox.mkdir(path: string, options?: MkdirOptions): Promise ``` -#### Parameters - -- **path** (`string`, required) - Absolute path to the directory -- **options** (`MkdirOptions`, optional): - - `recursive` (`boolean`) - Create parent directories if needed (default: false) - - `mode` (`number`) - Directory permissions (default: 0o755) - -#### Examples - -**Create a single directory:** +**Parameters**: +- `path` - Absolute path to the directory +- `options` (optional): + - `recursive` - Create parent directories if needed (default: `false`) ``` await sandbox.mkdir('/workspace/src'); -``` - - -**Create nested directories:** - -``` -// Create entire path -await sandbox.mkdir('/workspace/src/components/ui', { - recursive: true -}); +// Nested directories +await sandbox.mkdir('/workspace/src/components/ui', { recursive: true }); ``` -**Create project structure:** - - -``` -// Create multiple directories -await sandbox.mkdir('/workspace/src', { recursive: true }); -await sandbox.mkdir('/workspace/tests', { recursive: true }); -await sandbox.mkdir('/workspace/public', { recursive: true }); - -// Then create files -await sandbox.writeFile('/workspace/src/index.js', '// Main app'); -await sandbox.writeFile('/workspace/tests/app.test.js', '// Tests'); - -``` - - - ---- - ### `deleteFile()` -Delete a file from the sandbox. +Delete a file. ```ts await sandbox.deleteFile(path: string): Promise ``` -#### Parameters - -- **path** (`string`, required) - Absolute path to the file - -#### Examples - -**Delete a file:** +**Parameters**: +- `path` - Absolute path to the file ``` @@ -254,46 +98,17 @@ await sandbox.deleteFile('/workspace/temp.txt'); ``` -**Clean up temporary files:** - - -``` -const tempFiles = [ - '/tmp/build-output.log', - '/tmp/cache.json', - '/workspace/.env.local' -]; - -for (const file of tempFiles) { -try { -await sandbox.deleteFile(file); -} catch (error) { -console.log(`Could not delete ${file}`); -} -} - -``` - - - ---- - ### `renameFile()` -Rename a file in the sandbox. +Rename a file. ```ts await sandbox.renameFile(oldPath: string, newPath: string): Promise ``` -#### Parameters - -- **oldPath** (`string`, required) - Current file path -- **newPath** (`string`, required) - New file path - -#### Examples - -**Rename a file:** +**Parameters**: +- `oldPath` - Current file path +- `newPath` - New file path ``` @@ -301,25 +116,6 @@ await sandbox.renameFile('/workspace/draft.txt', '/workspace/final.txt'); ``` -**Rename with backup:** - - -``` -// Create backup before overwriting -await sandbox.renameFile( - '/workspace/config.json', - '/workspace/config.json.backup' -); - -// Write new config -await sandbox.writeFile('/workspace/config.json', newConfig); - -``` - - - ---- - ### `moveFile()` Move a file to a different directory. @@ -328,14 +124,9 @@ Move a file to a different directory. await sandbox.moveFile(sourcePath: string, destinationPath: string): Promise ``` -#### Parameters - -- **sourcePath** (`string`, required) - Current file path -- **destinationPath** (`string`, required) - Destination path - -#### Examples - -**Move a file:** +**Parameters**: +- `sourcePath` - Current file path +- `destinationPath` - Destination path ``` @@ -343,54 +134,26 @@ await sandbox.moveFile('/tmp/download.txt', '/workspace/data.txt'); ``` -**Organize files:** - - -``` -// Move source files to src directory -await sandbox.mkdir('/workspace/src', { recursive: true }); - -const files = ['app.js', 'server.js', 'utils.js']; -for (const file of files) { - await sandbox.moveFile(`/workspace/${file}`, `/workspace/src/${file}`); -} - -``` - - - ---- - ### `gitCheckout()` -Clone a git repository into the sandbox. +Clone a git repository. ```ts await sandbox.gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promise ``` -#### Parameters - -- **repoUrl** (`string`, required) - Git repository URL -- **options** (`GitCheckoutOptions`, optional): - - `branch` (`string`) - Branch to checkout (default: main branch) - - `targetDir` (`string`) - Directory to clone into (default: repo name) - - `depth` (`number`) - Clone depth for shallow clone - -#### Examples - -**Clone a repository:** +**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'); -``` - -**Clone specific branch:** - - -``` +// Specific branch await sandbox.gitCheckout('https://github.com/user/repo', { branch: 'develop', targetDir: 'my-project' @@ -398,173 +161,7 @@ await sandbox.gitCheckout('https://github.com/user/repo', { ``` -**Clone and build:** - - -``` -// Clone the repository -await sandbox.gitCheckout('https://github.com/user/app'); - -// Navigate and build -await sandbox.exec('cd app && npm install'); -await sandbox.exec('cd app && npm run build'); - -// Read build output -const buildOutput = await sandbox.readFile('/workspace/app/dist/index.html'); - -``` - - - -**Shallow clone for faster cloning:** - - -``` -await sandbox.gitCheckout('https://github.com/user/large-repo', { - depth: 1 // Only fetch latest commit -}); -``` - - - -## Common patterns - -### Create a project structure - - -``` -// Create directories -await sandbox.mkdir('/workspace/my-app/src', { recursive: true }); -await sandbox.mkdir('/workspace/my-app/tests', { recursive: true }); - -// Create package.json -await sandbox.writeFile('/workspace/my-app/package.json', JSON.stringify({ -name: 'my-app', -version: '1.0.0', -scripts: { -start: 'node src/index.js', -test: 'jest' -} -})); - -// Create main file -await sandbox.writeFile('/workspace/my-app/src/index.js', `console.log('App started!');`); - -``` - - - -### Read and modify files - - -``` -// Read existing file -const file = await sandbox.readFile('/workspace/config.json'); -const config = JSON.parse(file.content); - -// Modify -config.lastUpdated = new Date().toISOString(); -config.version = '2.0.0'; - -// Write back -await sandbox.writeFile( - '/workspace/config.json', - JSON.stringify(config, null, 2) -); -``` - - - -### Copy files - - -``` -// Read source file -const source = await sandbox.readFile('/workspace/template.html'); - -// Write to new location -await sandbox.writeFile('/workspace/index.html', source.content); - -``` - - - -### Batch operations - - -``` -const files = { - '/workspace/src/app.js': 'console.log("app");', - '/workspace/src/utils.js': 'console.log("utils");', - '/workspace/README.md': '# My Project' -}; - -// Write all files -await Promise.all( - Object.entries(files).map(([path, content]) => - sandbox.writeFile(path, content) - ) -); -``` - - - -### Work with binary files - - -``` -// Read binary file (e.g., from an upload) -const imageBase64 = request.headers.get('X-Image-Data'); - -// Write to sandbox -await sandbox.writeFile('/workspace/upload.png', imageBase64, { -encoding: 'base64' -}); - -// Process with Python -await sandbox.exec('python process-image.py /workspace/upload.png'); - -// Read processed result -const result = await sandbox.readFile('/workspace/output.png', { -encoding: 'base64' -}); - -``` - - - -## Error handling - -File operations may fail if files don't exist or permissions are insufficient: - - -``` -try { - const file = await sandbox.readFile('/workspace/missing.txt'); -} catch (error) { - if (error.code === 'FILE_NOT_FOUND') { - console.log('File does not exist'); - // Create it - await sandbox.writeFile('/workspace/missing.txt', 'default content'); - } else { - throw error; - } -} -``` - - - -## Path conventions - -All paths in the sandbox are absolute. Common directories: - -- `/workspace` - Default working directory, recommended for application files -- `/tmp` - Temporary files (may be cleared) -- `/home` - User home directory - ## Related resources - [Manage files guide](/sandbox/guides/manage-files/) - Detailed guide with best practices - [Commands API](/sandbox/api/commands/) - Execute commands -- [Code Interpreter API](/sandbox/api/interpreter/) - Execute code with rich outputs -- [Examples](/sandbox/examples/) - Complete code examples diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 34523af99fd7797..75b94f0678d281c 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -66,156 +66,3 @@ The Sandbox SDK is organized into focused APIs: - -## Quick reference - -### Command execution - -| Method | Description | -|--------|-------------| -| `exec()` | Execute a command and return results | -| `execStream()` | Execute with streaming Server-Sent Events | -| `startProcess()` | Start a background process | -| `listProcesses()` | List all running processes | -| `killProcess()` | Terminate a specific process | - -### File operations - -| Method | Description | -|--------|-------------| -| `writeFile()` | Write content to a file | -| `readFile()` | Read a file's contents | -| `mkdir()` | Create a directory | -| `deleteFile()` | Delete a file | -| `renameFile()` | Rename a file | -| `moveFile()` | Move a file to a new location | - -### Code interpreter - -| Method | Description | -|--------|-------------| -| `createCodeContext()` | Create a persistent execution context | -| `runCode()` | Execute code with optional streaming | -| `runCodeStream()` | Execute code with SSE streaming | -| `listCodeContexts()` | List all active contexts | -| `deleteCodeContext()` | Delete a specific context | - -### Port management - -| Method | Description | -|--------|-------------| -| `exposePort()` | Expose a port and get a preview URL | -| `unexposePort()` | Remove port exposure | -| `getExposedPorts()` | List all exposed ports with URLs | - -### Git operations - -| Method | Description | -|--------|-------------| -| `gitCheckout()` | Clone a repository into the sandbox | - -## Common patterns - -### Error handling - -All methods return results or throw errors. Use try-catch for error handling: - -```typescript -try { - const result = await sandbox.exec('python script.py'); - if (!result.success) { - console.error('Command failed:', result.stderr); - } -} catch (error) { - console.error('Sandbox error:', error.message); -} -``` - -### Streaming operations - -Many methods support streaming for real-time output: - -```typescript -import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox'; - -const stream = await sandbox.execStream('npm install'); -for await (const event of parseSSEStream(stream)) { - if (event.type === 'stdout') { - console.log(event.data); - } -} -``` - -### Working with files - -File operations use absolute paths within the sandbox: - -```typescript -// Write a file -await sandbox.writeFile('/workspace/app.js', code); - -// Read it back -const file = await sandbox.readFile('/workspace/app.js'); -console.log(file.content); - -// Create directories -await sandbox.mkdir('/workspace/src', { recursive: true }); -``` - -## Response types - -### ExecuteResponse - -Returned by `exec()` and similar methods: - -```typescript -interface ExecuteResponse { - success: boolean; // true if exitCode === 0 - stdout: string; // Standard output - stderr: string; // Standard error - exitCode: number; // Process exit code -} -``` - -### ProcessInfo - -Returned by `startProcess()` and `listProcesses()`: - -```typescript -interface ProcessInfo { - id: string; // Unique process identifier - pid: number; // Process ID - command: string; // Command that was executed - status: string; // Process status (running, exited, etc.) - exitCode?: number; // Exit code if process has terminated -} -``` - -### FileInfo - -Returned by `readFile()`: - -```typescript -interface FileInfo { - content: string; // File contents - encoding: string; // File encoding (utf-8, base64, etc.) -} -``` - -## TypeScript support - -The SDK is fully typed. Install the package to get automatic type checking and IntelliSense in your editor: - -```typescript -import type { Sandbox, ExecuteResponse, ProcessInfo } from '@cloudflare/sandbox'; - -const sandbox: Sandbox = getSandbox(env.Sandbox, 'user-123'); -const result: ExecuteResponse = await sandbox.exec('echo test'); -``` - -## Related resources - -- [Get Started](/sandbox/get-started/) - Quick introduction to Sandbox SDK -- [Execute Commands](/sandbox/guides/execute-commands/) - Guide to command execution -- [Manage Files](/sandbox/guides/manage-files/) - Guide to file operations -- [Examples](/sandbox/examples/) - Complete code examples diff --git a/src/content/docs/sandbox/api/interpreter.mdx b/src/content/docs/sandbox/api/interpreter.mdx index eabb0e339064fc0..c10a305e8c33b38 100644 --- a/src/content/docs/sandbox/api/interpreter.mdx +++ b/src/content/docs/sandbox/api/interpreter.mdx @@ -7,334 +7,118 @@ sidebar: import { TypeScriptExample } from "~/components"; -The Code Interpreter API provides a high-level interface for executing Python, JavaScript, and TypeScript code with rich output support. Unlike the Commands API which executes arbitrary shell commands, the Code Interpreter is designed specifically for interactive code execution with support for data visualizations, tables, charts, and formatted output. - -## Overview - -The Code Interpreter API is ideal for: - -- **Data analysis workflows** - Execute pandas, numpy, matplotlib code -- **Interactive notebooks** - Build Jupyter-like experiences -- **AI code execution** - Run LLM-generated code safely -- **Multi-language execution** - Python, JavaScript, TypeScript in isolated contexts - -Key features: - -- **Persistent contexts** - Maintain state across multiple executions -- **Rich outputs** - Charts, tables, images, HTML, LaTeX, JSON -- **Streaming support** - Real-time output with callbacks -- **Multi-language** - Python 3.11, Node.js with TypeScript support -- **Automatic retries** - Built-in retry logic for transient errors +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. Contexts maintain state (variables, imports, functions) across multiple code executions. +Create a persistent execution context for running code. ```ts const context = await sandbox.createCodeContext(options?: CreateContextOptions): Promise ``` -#### Parameters - -- **options** (`CreateContextOptions`, optional): - - `language` (`"python" | "javascript" | "typescript"`) - Programming language (default: `"python"`) - - `cwd` (`string`) - Working directory (default: `"/workspace"`) - - `envVars` (`Record`) - Environment variables - - `timeout` (`number`) - Request timeout in milliseconds (default: 30000) +**Parameters**: +- `options` (optional): + - `language` - `"python" | "javascript" | "typescript"` (default: `"python"`) + - `cwd` - Working directory (default: `"/workspace"`) + - `envVars` - Environment variables + - `timeout` - Request timeout in milliseconds (default: 30000) -#### Return value - -Returns a `Promise` with: - -- `id` (`string`) - Unique context identifier -- `language` (`string`) - Programming language -- `cwd` (`string`) - Working directory -- `createdAt` (`Date`) - When context was created -- `lastUsed` (`Date`) - When context was last used - -#### Examples - -**Create Python context:** +**Returns**: `Promise` with `id`, `language`, `cwd`, `createdAt`, `lastUsed` ``` -const pythonCtx = await sandbox.createCodeContext({ +const ctx = await sandbox.createCodeContext({ language: 'python', - cwd: '/workspace' -}); - -console.log('Context ID:', pythonCtx.id); -``` - - - -**Create JavaScript context with environment variables:** - - -``` -const jsCtx = await sandbox.createCodeContext({ - language: 'javascript', - envVars: { - NODE_ENV: 'production', - API_KEY: process.env.API_KEY - } -}); -``` - - -**Create TypeScript context:** - - -``` -const tsCtx = await sandbox.createCodeContext({ - language: 'typescript', - cwd: '/workspace/app' + envVars: { API_KEY: env.API_KEY } }); ``` ---- - ### `runCode()` -Execute code in a context and return the complete result with all outputs. +Execute code in a context and return the complete result. ```ts const result = await sandbox.runCode(code: string, options?: RunCodeOptions): Promise ``` -#### Parameters - -- **code** (`string`, required) - The code to execute -- **options** (`RunCodeOptions`, optional): - - `context` (`CodeContext`) - Context to run in (creates default if not provided) - - `language` (`"python" | "javascript" | "typescript"`) - Language if no context provided - - `envVars` (`Record`) - Environment variables for this execution - - `timeout` (`number`) - Execution timeout in milliseconds (default: 60000) - - `signal` (`AbortSignal`) - For cancelling execution - - `onStdout` (`(output: OutputMessage) => void`) - Callback for stdout - - `onStderr` (`(output: OutputMessage) => void`) - Callback for stderr - - `onResult` (`(result: Result) => void`) - Callback for results - - `onError` (`(error: ExecutionError) => void`) - Callback for errors - -#### Return value - -Returns a `Promise` with: - -- `code` (`string`) - Code that was executed -- `logs` (`object`) - Output logs - - `stdout` (`string[]`) - Standard output lines - - `stderr` (`string[]`) - Standard error lines -- `results` (`Result[]`) - Rich execution results (charts, tables, etc.) -- `error` (`ExecutionError`) - Execution error if any -- `executionCount` (`number`) - Execution counter +**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 -#### Examples +**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 -**Basic Python execution:** - - -``` -const result = await sandbox.runCode(` -print("Hello from Python!") -x = 42 -x * 2 -`); - -console.log('Stdout:', result.logs.stdout); // ["Hello from Python!"] -console.log('Result:', result.results[0].text); // "84" -``` - - - -**Python with data analysis:** +**Recommended usage - create explicit context**: ``` const ctx = await sandbox.createCodeContext({ language: 'python' }); -const result = await sandbox.runCode(` -import statistics +await sandbox.runCode('import math; radius = 5', { context: ctx }); +const result = await sandbox.runCode('math.pi * radius ** 2', { context: ctx }); -data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -mean = statistics.mean(data) -median = statistics.median(data) -stdev = statistics.stdev(data) - -print(f"Mean: {mean}") -print(f"Median: {median}") -print(f"Std Dev: {stdev:.2f}") - -mean # Last expression is returned -`, { context: ctx }); - -console.log('Logs:', result.logs.stdout); -// ["Mean: 5.5", "Median: 5.5", "Std Dev: 3.03"] - -console.log('Result:', result.results[0].text); // "5.5" +console.log(result.results[0].text); // "78.53981633974483" ``` - -**JavaScript execution:** +:::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(` -const data = [1, 2, 3, 4, 5]; -const sum = data.reduce((a, b) => a + b, 0); -const avg = sum / data.length; - -console.log('Average:', avg); -avg; -`, { language: 'javascript' }); - -console.log(result.logs.stdout); // ["Average: 3"] -console.log(result.results[0].text); // "3" -``` - - - -**Execution with streaming callbacks:** - - -``` -const result = await sandbox.runCode(` -for i in range(5): - print(f"Processing item {i}...") -`, { - language: 'python', - onStdout: (output) => { - console.log('[STDOUT]', output.text); - }, - onStderr: (output) => { - console.error('[STDERR]', output.text); - }, - onResult: (result) => { - console.log('[RESULT]', result.text); - }, - onError: (error) => { - console.error('[ERROR]', error.name, error.value); - } -}); +data = [1, 2, 3, 4, 5] +print(f"Sum: {sum(data)}") +sum(data) +`, { language: 'python' }); -// Logs appear in real-time: -// [STDOUT] Processing item 0... -// [STDOUT] Processing item 1... -// ... +console.log(result.logs.stdout); // ["Sum: 15"] +console.log(result.results[0].text); // "15" ``` +::: -**Error handling:** +**Error handling**: ``` -try { - const result = await sandbox.runCode(` -x = 1 / 0 # Division by zero -`, { language: 'python' }); +const result = await sandbox.runCode('x = 1 / 0', { language: 'python' }); if (result.error) { -console.error('Execution error:'); -console.error('Name:', result.error.name); // "ZeroDivisionError" -console.error('Message:', result.error.value); // "division by zero" -console.error('Traceback:', result.error.traceback); -} -} catch (error) { -console.error('Failed to execute:', error.message); + console.error(result.error.name); // "ZeroDivisionError" + console.error(result.error.value); // "division by zero" + console.error(result.error.traceback); // Stack trace array } ``` - -**Working with persistent context:** - - -``` -// Create context -const ctx = await sandbox.createCodeContext({ language: 'python' }); - -// First execution - define variables -await sandbox.runCode(` -import math -radius = 5 -`, { context: ctx }); - -// Second execution - use variables from previous execution -const result = await sandbox.runCode(` -area = math.pi * radius ** 2 -print(f"Area: {area:.2f}") -area -`, { context: ctx }); - -console.log('Result:', result.results[0].text); // "78.53981633974483" -``` - - -**AI code execution example:** - - -``` -import Anthropic from '@anthropic-ai/sdk'; - -// Generate code with Claude -const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); -const response = await anthropic.messages.create({ -model: 'claude-3-5-sonnet-latest', -max_tokens: 1024, -messages: [{ -role: 'user', -content: 'Write Python code to calculate the factorial of 10' -}] -}); - -const generatedCode = response.content[0].text; - -// Execute safely in sandbox -const result = await sandbox.runCode(generatedCode, { -language: 'python', -timeout: 5000 // 5 second timeout -}); - -console.log('Result:', result.results[0].text); -``` - - - ---- - ### `listCodeContexts()` -List all active code execution contexts for the sandbox. +List all active code execution contexts. ```ts const contexts = await sandbox.listCodeContexts(): Promise ``` -#### Return value - -Returns a `Promise` with all active contexts. - -#### Example - ``` const contexts = await sandbox.listCodeContexts(); - -for (const ctx of contexts) { -console.log(`Context: ${ctx.id}`); -console.log(` Language: ${ctx.language}`); -console.log(` Created: ${ctx.createdAt.toISOString()}`); -console.log(` Last used: ${ctx.lastUsed.toISOString()}`); -} +console.log(`Found ${contexts.length} contexts`); ``` - ---- - ### `deleteCodeContext()` Delete a code execution context and free its resources. @@ -343,75 +127,19 @@ Delete a code execution context and free its resources. await sandbox.deleteCodeContext(contextId: string): Promise ``` -#### Parameters - -- **contextId** (`string`, required) - The context ID to delete - -#### Example - ``` const ctx = await sandbox.createCodeContext({ language: 'python' }); - -// Use the context... await sandbox.runCode('print("Hello")', { context: ctx }); - -// Clean up when done await sandbox.deleteCodeContext(ctx.id); -console.log('Context deleted'); -``` - - - -**Cleanup pattern:** - - -``` -// List and clean up old contexts -const contexts = await sandbox.listCodeContexts(); - -for (const ctx of contexts) { - const ageMinutes = (Date.now() - ctx.lastUsed.getTime()) / 1000 / 60; - - // Delete contexts older than 30 minutes - if (ageMinutes > 30) { - await sandbox.deleteCodeContext(ctx.id); - console.log(`Deleted old context: ${ctx.id}`); - } -} ``` ## Rich Output Formats -The Code Interpreter supports multiple output formats for data visualization and presentation: - -### Result Object +Results include: `text`, `html`, `png`, `jpeg`, `svg`, `latex`, `markdown`, `json`, `chart`, `data` -Each execution returns `Result` objects with these properties: - -```ts -interface Result { - text?: string; // Plain text representation - html?: string; // HTML tables, formatted output - png?: string; // PNG image (base64) - jpeg?: string; // JPEG image (base64) - svg?: string; // SVG vector graphics - latex?: string; // LaTeX mathematical notation - markdown?: string; // Markdown formatted text - javascript?: string; // JavaScript code - json?: any; // JSON data - chart?: ChartData; // Chart/visualization data - data?: any; // Raw data object - -formats(): string[]; // List available formats -} -``` - - -### Working with Charts - -**Python with matplotlib:** +**Charts (matplotlib)**: ``` @@ -420,482 +148,39 @@ import matplotlib.pyplot as plt import numpy as np x = np.linspace(0, 10, 100) -y = np.sin(x) - -plt.figure(figsize=(10, 6)) -plt.plot(x, y) -plt.title('Sine Wave') -plt.xlabel('x') -plt.ylabel('sin(x)') -plt.grid(True) +plt.plot(x, np.sin(x)) plt.show() `, { language: 'python' }); -// Access the chart image -if (result.results[0]?.png) { - const imageData = result.results[0].png; - // imageData is base64-encoded PNG - console.log('Chart generated'); -} -``` - - -**Return image to client:** - - -``` -const result = await sandbox.runCode(visualizationCode, { - language: 'python' -}); - -// Return chart as image response if (result.results[0]?.png) { -const imageBuffer = Buffer.from(result.results[0].png, 'base64'); -return new Response(imageBuffer, { -headers: { 'Content-Type': 'image/png' } -}); + const imageBuffer = Buffer.from(result.results[0].png, 'base64'); + return new Response(imageBuffer, { + headers: { 'Content-Type': 'image/png' } + }); } - ``` - -### Working with Tables - -**Python with pandas:** +**Tables (pandas)**: ``` const result = await sandbox.runCode(` import pandas as pd - -data = { - 'Name': ['Alice', 'Bob', 'Charlie'], - 'Age': [25, 30, 35], - 'City': ['NYC', 'LA', 'Chicago'] -} - -df = pd.DataFrame(data) -df # Returns HTML table representation +df = pd.DataFrame({'Name': ['Alice', 'Bob'], 'Age': [25, 30]}) +df `, { language: 'python' }); -// Access HTML table if (result.results[0]?.html) { - const tableHtml = result.results[0].html; - return new Response(tableHtml, { + return new Response(result.results[0].html, { headers: { 'Content-Type': 'text/html' } }); } ``` -### Working with JSON Data - - -``` -const result = await sandbox.runCode(` -import json - -data = { - 'status': 'success', - 'items': [1, 2, 3, 4, 5], - 'summary': { - 'total': 15, - 'average': 3.0 - } -} - -json.dumps(data) -`, { language: 'python' }); - -// Access JSON result -if (result.results[0]?.json) { - const jsonData = result.results[0].json; - return Response.json(jsonData); -} -``` - - - -## Common Patterns - -### Interactive data analysis - - -``` -// Create persistent context for analysis session -const ctx = await sandbox.createCodeContext({ language: 'python' }); - -// Load data -await sandbox.runCode(` -import pandas as pd -import numpy as np - -# Load dataset -df = pd.read_csv('/workspace/data.csv') -print(f"Loaded {len(df)} rows") -`, { context: ctx }); - -// Analyze data -const analysis = await sandbox.runCode(` -# Calculate statistics -summary = df.describe() -summary -`, { context: ctx }); - -console.log('Stats:', analysis.results[0].html); - -// Visualize data -const viz = await sandbox.runCode(` -import matplotlib.pyplot as plt - -df['value'].hist(bins=20) -plt.title('Distribution') -plt.show() -`, { context: ctx }); - -// Return chart -if (viz.results[0]?.png) { - // Use chart image... -} -``` - - -### Multi-language workflow - - -``` -// Python for data processing -const pythonCtx = await sandbox.createCodeContext({ language: 'python' }); - -const processed = await sandbox.runCode(` -import json - -data = [1, 2, 3, 4, 5] -result = { - 'sum': sum(data), - 'avg': sum(data) / len(data) -} - -# Write results to file - -with open('/workspace/results.json', 'w') as f: -json.dump(result, f) - -result -`, { context: pythonCtx }); - -// JavaScript for presentation -const jsResult = await sandbox.runCode(` -const fs = require('fs'); -const data = JSON.parse(fs.readFileSync('/workspace/results.json', 'utf8')); - -console.log('Sum:', data.sum); -console.log('Average:', data.avg); - -data; -`, { language: 'javascript' }); - -console.log(jsResult.logs.stdout); -``` - - - -### AI-powered data analysis - - -``` -import OpenAI from 'openai'; - -async function analyzeData(question: string, dataPath: string) { - const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY }); - - // Ask AI to generate analysis code - const completion = await openai.chat.completions.create({ - model: 'gpt-4', - messages: [{ - role: 'user', - content: `Generate Python pandas code to answer: "${question}". -Data is in: ${dataPath}. Return only code, no markdown.` - }] - }); - - const code = completion.choices[0].message.content; - - // Execute in sandbox - const result = await sandbox.runCode(code, { - language: 'python', - timeout: 30000, - onStdout: (output) => console.log(output.text) - }); - - return result; -} - -// Use it -const analysis = await analyzeData( - 'What is the average value by category?', - '/workspace/sales.csv' -); - -console.log(analysis.logs.stdout); -if (analysis.results[0]) { - console.log('Result:', analysis.results[0].text); -} -``` - - -### Real-time execution monitoring - - -``` -let progress = 0; - -const result = await sandbox.runCode(` -import time - -for i in range(10): -print(f"Step {i+1}/10") -time.sleep(0.5) - -print("Complete!") -`, { - language: 'python', - onStdout: (output) => { - if (output.text.startsWith('Step')) { - progress++; - console.log(`Progress: ${progress * 10}%`); - } - } -}); -``` - - - -### Error recovery - - -``` -const ctx = await sandbox.createCodeContext({ language: 'python' }); - -async function executeWithRetry(code: string, maxRetries = 3) { - for (let attempt = 0; attempt < maxRetries; attempt++) { - const result = await sandbox.runCode(code, { - context: ctx, - timeout: 10000 - }); - - if (!result.error) { - return result; - } - - console.log(`Attempt ${attempt + 1} failed:`, result.error.value); - - // Don't retry on syntax errors - if (result.error.name === 'SyntaxError') { - throw new Error('Syntax error in code'); - } - - // Wait before retry - await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); - } - - throw new Error('Max retries exceeded'); -} - -try { - const result = await executeWithRetry('import requests; ...'); - console.log('Success:', result.results[0].text); -} catch (error) { - console.error('Failed:', error.message); -} -``` - - -## Error Handling - -### Execution Errors - -The Code Interpreter returns structured error information: - -```ts -interface ExecutionError { - name: string; // Error type (e.g., 'NameError', 'SyntaxError') - value: string; // Error message - traceback: string[]; // Full stack trace - lineNumber?: number; // Line number where error occurred -} -``` - -**Example error handling:** - - -``` -const result = await sandbox.runCode(` -undefined_variable -`, { language: 'python' }); - -if (result.error) { -console.error('Error Type:', result.error.name); -console.error('Error Message:', result.error.value); -console.error('Stack Trace:'); -result.error.traceback.forEach(line => console.error(line)); - -if (result.error.lineNumber) { -console.error('Error at line:', result.error.lineNumber); -} -} - -// Output: -// Error Type: NameError -// Error Message: name 'undefined_variable' is not defined -// Stack Trace: [traceback lines...] - -``` - - - -### Timeout Handling - - -``` -try { - const result = await sandbox.runCode(` -import time -time.sleep(120) # Sleep for 2 minutes -`, { - language: 'python', - timeout: 5000 // 5 second timeout -}); -} catch (error) { - if (error.message.includes('timeout')) { - console.error('Code execution timed out'); - } else { - throw error; - } -} -``` - - -### Context Not Ready - -The Code Interpreter includes automatic retry logic for initialization: - - -``` -// First execution might need time to initialize -const result = await sandbox.runCode(`print("Hello")`, { - language: 'python' -}); - -// The SDK automatically retries if the interpreter is not ready -// Default: 3 retries with exponential backoff - -``` - - - -## Best Practices - -### 1. Use persistent contexts for related executions - - -``` -// ✅ Good - reuse context -const ctx = await sandbox.createCodeContext({ language: 'python' }); -await sandbox.runCode('import pandas as pd', { context: ctx }); -await sandbox.runCode('df = pd.read_csv("data.csv")', { context: ctx }); -await sandbox.runCode('df.describe()', { context: ctx }); - -// ❌ Bad - creates new context each time -await sandbox.runCode('import pandas as pd', { language: 'python' }); -await sandbox.runCode('df = pd.read_csv("data.csv")', { language: 'python' }); // Import lost! -``` - - -### 2. Set appropriate timeouts - - -``` -// Short timeout for quick operations -await sandbox.runCode('2 + 2', { - language: 'python', - timeout: 1000 // 1 second -}); - -// Longer timeout for data processing -await sandbox.runCode(dataAnalysisCode, { -language: 'python', -timeout: 60000 // 1 minute -}); - -``` - - - -### 3. Clean up contexts - - -``` -const ctx = await sandbox.createCodeContext({ language: 'python' }); - -try { - // Use context... - await sandbox.runCode(code, { context: ctx }); -} finally { - // Always clean up - await sandbox.deleteCodeContext(ctx.id); -} -``` - - - -### 4. Handle errors gracefully - - -``` -const result = await sandbox.runCode(userCode, { - language: 'python', - onError: (error) => { - // Log error but don't crash - console.error('Execution error:', error.name, error.value); - } -}); - -if (result.error) { - return Response.json({ - success: false, - error: result.error.value, - traceback: result.error.traceback - }, { status: 400 }); -} -``` - - - -### 5. Use callbacks for long-running operations - - -``` -let output = ''; - -await sandbox.runCode(longRunningCode, { - language: 'python', - onStdout: (msg) => { - output += msg.text + '\n'; - // Stream to client, log, update UI, etc. - }, - onResult: (result) => { - // Process result as it becomes available - console.log('Got result:', result.text); - } -}); -``` - - -## Related Resources +## Related resources -- [Build an AI Code Executor tutorial](/sandbox/tutorials/build-an-ai-code-executor/) - Complete AI integration example +- [Build an AI Code Executor](/sandbox/tutorials/build-an-ai-code-executor/) - Complete tutorial - [Commands API](/sandbox/api/commands/) - Lower-level command execution - [Files API](/sandbox/api/files/) - File operations -- [API Overview](/sandbox/api/) - Complete API reference diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx index 509863101e2bc70..ffd6fb522674680 100644 --- a/src/content/docs/sandbox/api/ports.mdx +++ b/src/content/docs/sandbox/api/ports.mdx @@ -7,243 +7,59 @@ sidebar: import { TypeScriptExample } from "~/components"; -The Ports API enables you to expose services running in your sandbox to the internet via preview URLs. This allows you to run web servers, APIs, or any network service inside the sandbox and access them from outside. - -## Overview - -When you start a service in the sandbox (like a web server), it runs on localhost inside the container. The Ports API creates a publicly accessible URL that proxies requests to your service, enabling: - -- **Web development** - Run development servers and preview changes -- **API testing** - Test backend services in isolation -- **Interactive demos** - Share running applications with others -- **Multi-service apps** - Expose multiple ports for microservices - -Key features: - -- **Automatic URL generation** - Get a unique preview URL for each exposed port -- **Secure proxying** - All traffic is proxied through Cloudflare's network -- **Multiple ports** - Expose several services simultaneously -- **Named ports** - Assign friendly names to organize multiple exposed ports +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 running in the sandbox and get a preview URL. +Expose a port and get a preview URL. ```ts const response = await sandbox.exposePort(port: number, options?: ExposePortOptions): Promise ``` -#### Parameters - -- **port** (`number`, required) - Port number to expose (1-65535) -- **options** (`ExposePortOptions`, optional): - - `name` (`string`) - Friendly name for the port (useful when exposing multiple ports) - -#### Return value - -Returns a `Promise` with: - -- `port` (`number`) - The exposed port number -- `exposedAt` (`string`) - The public preview URL -- `name` (`string`, optional) - The name if provided -- `success` (`boolean`) - Whether the operation succeeded -- `timestamp` (`string`) - ISO timestamp of the operation - -#### Examples - -**Expose a web server:** - - -``` -import { getSandbox } from '@cloudflare/sandbox'; - -export default { - async fetch(request: Request, env: Env): Promise { - const sandbox = getSandbox(env.Sandbox, 'demo-user'); - - // Start a Python web server - await sandbox.startProcess('python -m http.server 8000'); - - // Expose port 8000 - const exposed = await sandbox.exposePort(8000); - - return Response.json({ - message: 'Server running', - url: exposed.exposedAt, - port: exposed.port - }); - -} -}; - -// Response: -// { -// "message": "Server running", -// "url": "https://abc123-8000.sandbox.workers.dev", -// "port": 8000 -// } - -``` - - +**Parameters**: +- `port` - Port number to expose (1024-65535) +- `options` (optional): + - `name` - Friendly name for the port -**Expose with a friendly name:** +**Returns**: `Promise` with `port`, `exposedAt` (preview URL), `name` ``` -// Start a Node.js API server -await sandbox.startProcess('node api-server.js'); - -// Expose with a descriptive name -const api = await sandbox.exposePort(3000, { - name: 'api-server' -}); - -console.log('API available at:', api.exposedAt); -console.log('Port name:', api.name); // "api-server" -``` - - +await sandbox.startProcess('python -m http.server 8000'); +const exposed = await sandbox.exposePort(8000); -**Expose multiple services:** +console.log('Available at:', exposed.exposedAt); +// https://abc123-8000.sandbox.workers.dev - -``` -// Start backend API +// Multiple services with names await sandbox.startProcess('node api.js'); const api = await sandbox.exposePort(3000, { name: 'api' }); -// Start frontend dev server await sandbox.startProcess('npm run dev'); const frontend = await sandbox.exposePort(5173, { name: 'frontend' }); - -// Start database admin panel -await sandbox.startProcess('python db-admin.py'); -const admin = await sandbox.exposePort(8080, { name: 'admin' }); - -return Response.json({ -services: { -api: api.exposedAt, -frontend: frontend.exposedAt, -admin: admin.exposedAt -} -}); - -``` - - - -**Full development environment:** - - ``` -// Clone a repository -await sandbox.gitCheckout('https://github.com/user/my-app'); - -// Install dependencies -await sandbox.exec('cd my-app && npm install'); - -// Start the development server -await sandbox.startProcess('cd my-app && npm run dev'); - -// Wait a moment for server to start -await new Promise(resolve => setTimeout(resolve, 2000)); - -// Expose the dev server -const dev = await sandbox.exposePort(3000, { name: 'dev-server' }); - -return Response.json({ - message: 'Development environment ready', - url: dev.exposedAt -}); -``` - ---- - ### `unexposePort()` -Remove a previously exposed port and close its preview URL. +Remove an exposed port and close its preview URL. ```ts -await sandbox.unexposePort(port: number): Promise +await sandbox.unexposePort(port: number): Promise ``` -#### Parameters - -- **port** (`number`, required) - Port number to unexpose - -#### Return value - -Returns a `Promise` with: - -- `port` (`number`) - The port that was unexposed -- `success` (`boolean`) - Whether the operation succeeded -- `timestamp` (`string`) - ISO timestamp of the operation - -#### Examples - -**Unexpose a port:** +**Parameters**: +- `port` - Port number to unexpose ``` -// Expose a port -const exposed = await sandbox.exposePort(8000); -console.log('Exposed at:', exposed.exposedAt); - -// Later, clean up await sandbox.unexposePort(8000); -console.log('Port 8000 is no longer accessible'); - -``` - - - -**Cleanup multiple ports:** - - ``` -const ports = [3000, 5173, 8080]; - -// Expose all ports -for (const port of ports) { - await sandbox.exposePort(port); -} - -// Later, clean up all ports -for (const port of ports) { - await sandbox.unexposePort(port); -} - -console.log('All ports unexposed'); -``` - -**Conditional unexpose:** - - -``` -// Get currently exposed ports -const { ports } = await sandbox.getExposedPorts(); - -// Unexpose only specific ports -for (const portInfo of ports) { -if (portInfo.name === 'temporary-service') { -await sandbox.unexposePort(portInfo.port); -console.log(`Cleaned up ${portInfo.name}`); -} -} - -``` - - - ---- - ### `getExposedPorts()` Get information about all currently exposed ports. @@ -252,567 +68,19 @@ Get information about all currently exposed ports. const response = await sandbox.getExposedPorts(): Promise ``` -#### Return value - -Returns a `Promise` with: - -- `ports` (`ExposedPortInfo[]`) - Array of exposed port information - - `port` (`number`) - Port number - - `exposedAt` (`string`) - Preview URL - - `name` (`string`, optional) - Port name if provided -- `count` (`number`) - Total number of exposed ports -- `success` (`boolean`) - Whether the operation succeeded -- `timestamp` (`string`) - ISO timestamp of the operation - -#### Examples - -**List all exposed ports:** - - -``` -const { ports, count } = await sandbox.getExposedPorts(); - -console.log(`${count} ports exposed:`); - -for (const port of ports) { -console.log(` Port ${port.port}: ${port.exposedAt}`); -if (port.name) { -console.log(` Name: ${port.name}`); -} -} - -``` - - - -**Check if a specific port is exposed:** +**Returns**: `Promise` with `ports` array (containing `port`, `exposedAt`, `name`) ``` const { ports } = await sandbox.getExposedPorts(); -const isExposed = ports.some(p => p.port === 3000); - -if (!isExposed) { - console.log('Port 3000 is not exposed, exposing now...'); - await sandbox.exposePort(3000); -} -``` - - - -**Return exposed services to client:** - - -``` -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - const sandbox = getSandbox(env.Sandbox, 'user-123'); - - if (url.pathname === '/services') { - const { ports } = await sandbox.getExposedPorts(); - - const services = ports.map(p => ({ - name: p.name || `Port ${p.port}`, - port: p.port, - url: p.exposedAt - })); - - return Response.json({ services }); - } - - return new Response('Not found', { status: 404 }); - -} -}; - -``` - - - -**Monitor exposed ports:** - - -``` -// Check which ports are exposed -const before = await sandbox.getExposedPorts(); -console.log('Ports before:', before.count); - -// Start some services -await sandbox.startProcess('node api.js'); -await sandbox.exposePort(3000, { name: 'api' }); - -await sandbox.startProcess('npm run dev'); -await sandbox.exposePort(5173, { name: 'frontend' }); - -// Check again -const after = await sandbox.getExposedPorts(); -console.log('Ports after:', after.count); -console.log('New ports:', after.ports.map(p => p.name)); -``` - - - -## Common Patterns - -### Start and expose a service - -The typical workflow for running a web service: - - -``` -// 1. Start the service as a background process -const process = await sandbox.startProcess('node server.js'); -console.log('Server started with PID:', process.pid); - -// 2. Wait a moment for the service to initialize -await new Promise(resolve => setTimeout(resolve, 2000)); - -// 3. Expose the port -const exposed = await sandbox.exposePort(3000, { -name: 'my-api' -}); - -// 4. Return the URL to the client -return Response.json({ -status: 'running', -url: exposed.exposedAt, -processId: process.id -}); - -``` - - - -### Development workflow - -Complete development environment setup: - - -``` -async function setupDevEnvironment(sandbox: Sandbox, repoUrl: string) { - // Clone the repository - await sandbox.gitCheckout(repoUrl); - - // Extract repo name for directory - const repoName = repoUrl.split('/').pop()?.replace('.git', '') || 'repo'; - - // Install dependencies - await sandbox.exec(`cd ${repoName} && npm install`); - - // Start the dev server in background - const server = await sandbox.startProcess( - `cd ${repoName} && npm run dev` - ); - - // Give server time to start - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Expose the dev server port - const exposed = await sandbox.exposePort(3000, { - name: 'dev-server' - }); - - return { - processId: server.id, - url: exposed.exposedAt, - repository: repoUrl - }; -} - -// Usage -const env = await setupDevEnvironment( - sandbox, - 'https://github.com/user/my-app' -); - -console.log('Dev environment ready:', env.url); -``` - - - -### Microservices architecture - -Run and expose multiple services: - - -``` -interface Service { - name: string; - command: string; - port: number; -} - -const services: Service[] = [ -{ name: 'api', command: 'node api/server.js', port: 3000 }, -{ name: 'auth', command: 'node auth/server.js', port: 3001 }, -{ name: 'websocket', command: 'node ws/server.js', port: 3002 }, -{ name: 'admin', command: 'python admin/app.py', port: 8080 } -]; - -const exposedServices = []; - -for (const service of services) { -// Start the service -const process = await sandbox.startProcess(service.command); - -// Wait briefly -await new Promise(resolve => setTimeout(resolve, 1500)); - -// Expose the port -const exposed = await sandbox.exposePort(service.port, { -name: service.name -}); - -exposedServices.push({ -name: service.name, -url: exposed.exposedAt, -processId: process.id -}); -} - -return Response.json({ -message: 'All services running', -services: exposedServices -}); - -``` - - - -### Temporary preview URLs - -Create temporary preview URLs that clean up automatically: - - -``` -async function createTemporaryPreview( - sandbox: Sandbox, - command: string, - port: number, - durationMs: number = 300000 // 5 minutes default -) { - // Start service and expose port - const process = await sandbox.startProcess(command); - await new Promise(resolve => setTimeout(resolve, 2000)); - - const exposed = await sandbox.exposePort(port, { - name: 'temporary-preview' - }); - - // Schedule cleanup - setTimeout(async () => { - await sandbox.killProcess(process.id); - await sandbox.unexposePort(port); - console.log('Temporary preview cleaned up'); - }, durationMs); - - return exposed.exposedAt; -} - -// Usage -const previewUrl = await createTemporaryPreview( - sandbox, - 'npm run preview', - 4173, - 600000 // 10 minutes -); - -console.log('Preview available for 10 minutes:', previewUrl); -``` - - - -### Health checking - -Verify a service is ready before exposing: - - -``` -async function exposeWithHealthCheck( - sandbox: Sandbox, - command: string, - port: number, - maxRetries: number = 10 -): Promise { - // Start the service - const process = await sandbox.startProcess(command); - -// Poll until service is ready -for (let i = 0; i < maxRetries; i++) { -await new Promise(resolve => setTimeout(resolve, 1000)); - - // Check if service is responding - const check = await sandbox.exec(`curl -f http://localhost:${port}/health || true`); - - if (check.success && check.stdout.includes('ok')) { - // Service is ready, expose it - const exposed = await sandbox.exposePort(port); - return exposed.exposedAt; - } - - console.log(`Waiting for service to be ready... (${i + 1}/${maxRetries})`); - -} - -throw new Error('Service failed to become ready'); -} - -// Usage -try { -const url = await exposeWithHealthCheck( -sandbox, -'node server.js', -3000 -); -console.log('Service ready:', url); -} catch (error) { -console.error('Service failed to start:', error.message); -} - -``` - - - -## Error Handling - -### Port already exposed - - -``` -try { - await sandbox.exposePort(3000); -} catch (error) { - if (error.message.includes('already exposed')) { - console.log('Port 3000 is already exposed'); - - // Get the existing URL - const { ports } = await sandbox.getExposedPorts(); - const existing = ports.find(p => p.port === 3000); - - if (existing) { - console.log('Using existing URL:', existing.exposedAt); - } - } else { - throw error; - } -} -``` - - - -### Port not in use - - -``` -// Try to expose a port that nothing is listening on -try { - await sandbox.exposePort(9999); -} catch (error) { - console.error('Port 9999 is not in use'); - console.log('Make sure a service is running on this port first'); - -// Start a service on that port -await sandbox.startProcess('python -m http.server 9999'); - -// Wait and try again -await new Promise(resolve => setTimeout(resolve, 2000)); -await sandbox.exposePort(9999); -} - -``` - - - -### Cleanup on error - - -``` -async function safeExposePort(sandbox: Sandbox, port: number) { - try { - const exposed = await sandbox.exposePort(port); - return exposed.exposedAt; - } catch (error) { - console.error(`Failed to expose port ${port}:`, error.message); - - // Attempt cleanup - try { - await sandbox.unexposePort(port); - } catch { - // Ignore cleanup errors - } - - throw error; - } -} -``` - - - -## Best Practices - -### 1. Wait for services to start - -Always wait briefly after starting a process before exposing its port: - - -``` -// ✅ Good - wait for service to start -await sandbox.startProcess('npm run dev'); -await new Promise(resolve => setTimeout(resolve, 2000)); -await sandbox.exposePort(3000); - -// ❌ Bad - expose immediately -await sandbox.startProcess('npm run dev'); -await sandbox.exposePort(3000); // May fail if service isn't ready - -``` - - - -### 2. Use named ports for clarity - -When exposing multiple ports, use names to keep track: - - -``` -// ✅ Good - named ports -await sandbox.exposePort(3000, { name: 'api' }); -await sandbox.exposePort(5173, { name: 'frontend' }); -await sandbox.exposePort(8080, { name: 'admin' }); - -// ❌ Less clear - no names -await sandbox.exposePort(3000); -await sandbox.exposePort(5173); -await sandbox.exposePort(8080); -``` - - - -### 3. Clean up exposed ports - -Always unexpose ports when done: - - -``` -try { - await sandbox.exposePort(3000); - // Use the service... -} finally { - // Always clean up - await sandbox.unexposePort(3000); -} -``` - - -### 4. Check before exposing - -Verify a port isn't already exposed: - - -``` -const { ports } = await sandbox.getExposedPorts(); -const isExposed = ports.some(p => p.port === 3000); - -if (!isExposed) { -await sandbox.exposePort(3000); -} - -``` - - - -### 5. Handle port ranges - -Use valid port numbers (1-65535), avoiding reserved ranges: - - -``` -// ✅ Good - user port range -await sandbox.exposePort(3000); // Typical web dev -await sandbox.exposePort(8080); // Typical HTTP alternate -await sandbox.exposePort(5432); // Database port - -// ❌ Avoid system ports (0-1023) unless necessary -await sandbox.exposePort(80); // May require privileges -await sandbox.exposePort(443); // May require privileges -``` - - - -## Preview URL Format - -Preview URLs follow this pattern: - -``` -https://{sandbox-id}-{port}.sandbox.workers.dev -``` - -For example: - -- Port 3000: `https://abc123-3000.sandbox.workers.dev` -- Port 8080: `https://abc123-8080.sandbox.workers.dev` - -The URL remains stable for the lifetime of the exposed port and automatically proxies requests to your service running inside the sandbox. - -## Security Considerations - -### Public accessibility - -**Important**: Exposed ports are publicly accessible on the internet. Anyone with the URL can access your service. - - -``` -// Be aware: This API is now public -await sandbox.exposePort(3000); - -// Consider adding authentication in your service -await sandbox.writeFile('/workspace/api.js', ` -const express = require('express'); -const app = express(); - -// Add authentication middleware -app.use((req, res, next) => { -const token = req.headers.authorization; -if (token !== 'Bearer secret-token') { -return res.status(401).json({ error: 'Unauthorized' }); +for (const port of ports) { + console.log(`${port.name || port.port}: ${port.exposedAt}`); } -next(); -}); - -app.get('/data', (req, res) => { -res.json({ message: 'Protected data' }); -}); - -app.listen(3000); -`); - -``` - - - -### Temporary URLs - -For security-sensitive applications, unexpose ports when no longer needed: - - ``` -// Expose temporarily -const exposed = await sandbox.exposePort(3000); - -// Use the service -await doWork(exposed.exposedAt); - -// Clean up immediately -await sandbox.unexposePort(3000); -``` - -## Related Resources +## Related resources +- [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work - [Commands API](/sandbox/api/commands/) - Start background processes -- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running services -- [Build a dev environment tutorial](/sandbox/tutorials/dev-environment/) - Complete dev environment example -- [API Overview](/sandbox/api/) - Complete API reference diff --git a/src/content/docs/sandbox/api/sessions.mdx b/src/content/docs/sandbox/api/sessions.mdx index c03d240fc09413b..90ff309d9693a6a 100644 --- a/src/content/docs/sandbox/api/sessions.mdx +++ b/src/content/docs/sandbox/api/sessions.mdx @@ -7,348 +7,111 @@ sidebar: import { TypeScriptExample } from "~/components"; -Sessions provide isolated execution contexts within a sandbox, similar to having multiple terminal panes open on your laptop. Each session maintains its own shell state, environment variables, and working directory across multiple invocations. +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 -Sessions are an advanced feature. Most applications work fine with the default session that's automatically created. Use explicit sessions when you need isolated environments or persistent shell state. +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. ::: -## When to use sessions +## Methods -### Use sessions when you need: +### `createSession()` -- **Isolated environments** - Different users or workflows with separate configurations -- **Persistent shell state** - Environment variables and working directory that persist across commands -- **Parallel execution** - Multiple independent execution contexts running simultaneously -- **Session resumption** - Continue work from previous requests (e.g., in chat applications) - -### You don't need sessions for: - -- **Simple command execution** - The default session handles this automatically -- **Background processes** - Processes are visible across all sessions (like on your local machine) -- **File operations** - Files are shared across sessions - -## Default session behavior - -When you use sandbox methods without explicitly creating a session, a default session is automatically created with: - - -``` -const sandbox = getSandbox(env.Sandbox, 'user-123'); - -// These automatically use the default session -await sandbox.exec('export API_KEY=secret'); -await sandbox.exec('echo $API_KEY'); // Prints: secret - -// Environment persists within the default session -await sandbox.writeFile('/workspace/app.js', code); -await sandbox.exec('node app.js'); // Can access API_KEY - -``` - - - -The default session persists for the lifetime of the sandbox instance, maintaining shell state across all operations. - -## API methods - -### createSession() - -Create a new isolated execution session with custom environment and working directory. +Create a new isolated execution session. ```ts const session = await sandbox.createSession(options?: SessionOptions): Promise ``` -#### Parameters - -- **options** (`SessionOptions`, optional) - Session configuration options: - - `id` (`string`) - Custom session ID. Auto-generated if not provided. - - `env` (`Record`) - Environment variables for this session - - `cwd` (`string`) - Working directory for this session. Defaults to `/workspace` - -#### Returns +**Parameters**: +- `options` (optional): + - `id` - Custom session ID (auto-generated if not provided) + - `env` - Environment variables for this session + - `cwd` - Working directory (default: `"/workspace"`) -`Promise` - Session wrapper with all sandbox methods bound to this session ID. - -#### Example: Multiple isolated environments +**Returns**: `Promise` with all sandbox methods bound to this session ``` -// Create session for production deployment +// Multiple isolated environments const prodSession = await sandbox.createSession({ - id: 'prod-deploy', - env: { - NODE_ENV: 'production', - API_URL: 'https://api.example.com' - }, + id: 'prod', + env: { NODE_ENV: 'production', API_URL: 'https://api.example.com' }, cwd: '/workspace/prod' }); -// Create session for test environment const testSession = await sandbox.createSession({ -id: 'test-deploy', -env: { -NODE_ENV: 'test', -API_URL: 'http://localhost:3000' -}, -cwd: '/workspace/test' + id: 'test', + env: { NODE_ENV: 'test', API_URL: 'http://localhost:3000' }, + cwd: '/workspace/test' }); -// Run builds in parallel with different configurations +// Run in parallel const [prodResult, testResult] = await Promise.all([ -prodSession.exec('npm run build'), -testSession.exec('npm run build') + prodSession.exec('npm run build'), + testSession.exec('npm run build') ]); - -console.log('Production build:', prodResult.stdout); -console.log('Test build:', testResult.stdout); - -``` - - - -#### Example: Session per user in chat application - - -``` -// Each user gets their own isolated session -async function handleUserMessage(userId: string, message: string) { - const sandbox = getSandbox(env.Sandbox, `chat-${userId}`); - - // Get or create user's session - const session = await sandbox.createSession({ - id: userId, - env: { - USER_ID: userId, - CHAT_HISTORY_PATH: `/workspace/${userId}/history.txt` - } - }); - - // User's environment persists across messages - await session.exec(`echo "${message}" >> $CHAT_HISTORY_PATH`); - const result = await session.exec('python analyze_sentiment.py $CHAT_HISTORY_PATH'); - - return result.stdout; -} -``` - - - -#### Example: Custom working directories - - -``` -// Frontend session working in React app -const frontend = await sandbox.createSession({ - id: 'frontend', - cwd: '/workspace/client', - env: { PORT: '3000' } -}); - -// Backend session working in API server -const backend = await sandbox.createSession({ -id: 'backend', -cwd: '/workspace/server', -env: { PORT: '8080' } -}); - -// Commands run in their respective directories -await frontend.exec('npm install'); // Runs in /workspace/client -await backend.exec('npm install'); // Runs in /workspace/server - -// Start both services -await frontend.startProcess('npm start'); -await backend.startProcess('npm start'); - ``` +### `getSession()` -### getSession() - -Retrieve an existing session by ID. Useful for resuming sessions across different requests. +Retrieve an existing session by ID. ```ts const session = await sandbox.getSession(sessionId: string): Promise ``` -#### Parameters - -- **sessionId** (`string`, required) - ID of an existing session - -#### Returns - -`Promise` - Session wrapper bound to the specified session ID. - -:::note -If the session doesn't exist, operations will fail. There's no explicit session existence check - just try to use it and handle errors. -::: +**Parameters**: +- `sessionId` - ID of an existing session -#### Example: Resume session across requests +**Returns**: `Promise` bound to the specified session ``` // First request - create session -app.post('/start', async (req) => { - const { userId } = req.body; - const sandbox = getSandbox(env.Sandbox, userId); - -const session = await sandbox.createSession({ -id: `user-${userId}`, -env: { USER_ID: userId } -}); - +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'); -return { message: 'Setup complete', sessionId: session.id }; -}); - -// Second request - resume session -app.post('/build', async (req) => { -const { userId, sessionId } = req.body; -const sandbox = getSandbox(env.Sandbox, userId); - -// Resume existing session - environment and cwd are preserved -const session = await sandbox.getSession(sessionId); - +// 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'); -return { output: result.stdout }; -}); - -``` - - - -#### Example: Multi-step workflows - - ``` -// Step 1: Initialize environment -async function initializeProject(projectId: string) { - const sandbox = getSandbox(env.Sandbox, projectId); - const session = await sandbox.createSession({ - id: `project-${projectId}`, - cwd: '/workspace/project' - }); - - await session.exec('npm init -y'); - await session.exec('npm install express'); - - return session.id; -} - -// Step 2: Add code (different request/context) -async function addCode(projectId: string, sessionId: string, code: string) { - const sandbox = getSandbox(env.Sandbox, projectId); - const session = await sandbox.getSession(sessionId); - - await session.writeFile('app.js', code); - return 'Code added'; -} - -// Step 3: Run application (different request/context) -async function runApp(projectId: string, sessionId: string) { - const sandbox = getSandbox(env.Sandbox, projectId); - const session = await sandbox.getSession(sessionId); - - const result = await session.exec('node app.js'); - return result.stdout; -} -``` - -### setEnvVars() +### `setEnvVars()` -Set environment variables in the default session or globally in the sandbox. +Set environment variables in the sandbox. ```ts await sandbox.setEnvVars(envVars: Record): Promise ``` -#### Parameters - -- **envVars** (`Record`, required) - Key-value pairs of environment variables to set - -#### Returns +**Parameters**: +- `envVars` - Key-value pairs of environment variables to set -`Promise` - -:::caution[Call setEnvVars first] +:::caution Call `setEnvVars()` **before** any other sandbox operations to ensure environment variables are available from the start. ::: -#### Example: Configure API credentials - ``` 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' + 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'); // Can read API_KEY - -``` - - - -#### Example: Dynamic configuration - - -``` -async function runUserCode(userId: string, code: string, config: Config) { - const sandbox = getSandbox(env.Sandbox, userId); - - // Configure environment based on user's settings - await sandbox.setEnvVars({ - USER_ID: userId, - TIMEOUT: config.timeout.toString(), - MAX_MEMORY: config.maxMemory, - ALLOWED_HOSTS: config.allowedHosts.join(',') - }); - - await sandbox.writeFile('/tmp/user_code.py', code); - const result = await sandbox.exec('python /tmp/user_code.py'); - - return result.stdout; -} -``` - - - -#### Example: Session-specific environment - - -``` -// Set environment in a specific session -const session = await sandbox.createSession({ - id: 'test-session', - env: { - NODE_ENV: 'test' - } -}); - -// Update environment variables within this session -await session.setEnvVars({ -API_KEY: 'test-key-123', -DEBUG: 'true' -}); - -await session.exec('npm test'); // Uses test environment - +await sandbox.exec('python script.py'); ``` - -### destroy() +### `destroy()` Destroy the sandbox container and free up resources. @@ -356,264 +119,37 @@ Destroy the sandbox container and free up resources. await sandbox.destroy(): Promise ``` -#### Returns - -`Promise` - -:::note[Resource management] -Containers automatically sleep after 3 minutes of inactivity, but they still count toward your account limits. Use `destroy()` to immediately free up container slots and stop billing. +:::note +Containers automatically sleep after 3 minutes of inactivity, but still count toward account limits. Use `destroy()` to immediately free up resources. ::: -#### When to use destroy() - -- **After one-time tasks** - Code execution, builds, or analysis that won't be reused -- **Resource cleanup** - Free up container slots when you're done -- **Cost optimization** - Stop billing for containers you no longer need -- **Account limits** - Stay within your concurrent container limit - -#### Example: One-time code execution - ``` 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 { -// Always clean up temporary sandboxes -await sandbox.destroy(); -} -} - -``` - - - -#### Example: Batch processing with cleanup - - -``` -async function processBatch(tasks: Task[]) { - const results = []; - - for (const task of tasks) { - const sandbox = getSandbox(env.Sandbox, `task-${task.id}`); - - try { - // Process task - await sandbox.writeFile('/workspace/input.json', JSON.stringify(task.data)); - const result = await sandbox.exec('python process.py'); - results.push(result.stdout); - } finally { - // Free up resources after each task - await sandbox.destroy(); - } + try { + await sandbox.writeFile('/tmp/code.py', code); + const result = await sandbox.exec('python /tmp/code.py'); + return result.stdout; + } finally { + await sandbox.destroy(); } - - return results; } -``` - - - -#### Example: Graceful shutdown - - -``` -// Create sandbox for long-running workflow -const sandbox = getSandbox(env.Sandbox, 'workflow-123'); - -// Set up cleanup handlers -process.on('SIGTERM', async () => { -console.log('Shutting down gracefully...'); -await sandbox.destroy(); -process.exit(0); -}); - -// Run workflow -await sandbox.exec('npm run workflow'); - ``` +## ExecutionSession methods -## ExecutionSession API - -The `ExecutionSession` object returned by `createSession()` and `getSession()` provides all sandbox methods bound to that specific session: - -### Command execution -- `exec()` - Execute commands in this session -- `execStream()` - Stream command output - -### Process management -- `startProcess()` - Start background process -- `listProcesses()` - List all processes (visible across sessions) -- `getProcess()` - Get process info -- `killProcess()` - Terminate process -- `killAllProcesses()` - Terminate all processes -- `getProcessLogs()` - Get process output -- `streamProcessLogs()` - Stream process output - -### File operations -- `writeFile()` - Write file -- `readFile()` - Read file -- `mkdir()` - Create directory -- `deleteFile()` - Delete file -- `renameFile()` - Rename file -- `moveFile()` - Move file - -### Git operations -- `gitCheckout()` - Clone repository - -### Environment management -- `setEnvVars()` - Update environment variables in this session - -### Code interpreter -- `createCodeContext()` - Create code execution context -- `runCode()` - Execute Python/JavaScript code -- `listCodeContexts()` - List active contexts -- `deleteCodeContext()` - Delete context - -## Session behavior - -### Shell state isolation - -Each session maintains its own: -- **Environment variables** - Set via `env` option or `setEnvVars()` -- **Working directory** - Set via `cwd` option or `cd` commands -- **Shell history** - Independent command history per session - -### Shared resources - -All sessions in a sandbox share: -- **Filesystem** - Files written in one session are visible in others -- **Background processes** - Processes started in any session are visible to all sessions -- **Ports** - Exposed ports are shared across sessions - -Think of sessions like terminal tabs on your laptop - separate shells, but same filesystem and processes. - -### Session lifecycle - -- **Creation** - `createSession()` initializes a new session -- **Persistence** - Sessions persist for the container's lifetime -- **Resumption** - `getSession()` retrieves existing sessions -- **Cleanup** - Sessions are automatically cleaned up when container shuts down - -## Error handling - - -``` -try { - // Try to use a session - const session = await sandbox.getSession('nonexistent-session'); - await session.exec('ls'); -} catch (error) { - if (error.message.includes('Session not found')) { - // Handle missing session - console.error('Session does not exist'); - } -} -``` - - - -## Best practices - -### 1. Use default session for simple cases - - -``` -// Good - simple use case -await sandbox.exec('npm install'); -await sandbox.exec('npm test'); - -// Overkill - unnecessary session complexity -const session = await sandbox.createSession(); -await session.exec('npm install'); -await session.exec('npm test'); - -``` - - - -### 2. Create sessions for isolation - - -``` -// Good - isolated environments -const dev = await sandbox.createSession({ - id: 'dev', - env: { NODE_ENV: 'development' } -}); -const prod = await sandbox.createSession({ - id: 'prod', - env: { NODE_ENV: 'production' } -}); -``` - - - -### 3. Set environment variables early - - -``` -// Good - set env vars first -await sandbox.setEnvVars({ API_KEY: 'secret' }); -await sandbox.exec('python script.py'); - -// Bad - env vars set too late -await sandbox.exec('python script.py'); // API_KEY not available -await sandbox.setEnvVars({ API_KEY: 'secret' }); - -``` - - - -### 4. Clean up temporary sandboxes - - -``` -// Good - cleanup after one-time use -try { - const result = await sandbox.exec('python analyze.py'); - return result.stdout; -} finally { - await sandbox.destroy(); -} - -// Bad - container keeps running unnecessarily -const result = await sandbox.exec('python analyze.py'); -return result.stdout; -// Container stays alive, using resources -``` - - - -### 5. Reuse sessions across requests - - -``` -// Good - resume session -const sessionId = req.body.sessionId; -const session = await sandbox.getSession(sessionId); -await session.exec('npm run build'); - -// Bad - create new session every time -const session = await sandbox.createSession(); -await session.exec('npm run build'); -// Loses state from previous requests - -``` - +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 -- [Commands API](/sandbox/api/commands/) - Execute commands in sessions -- [Files API](/sandbox/api/files/) - File operations in sessions -- [Get Started](/sandbox/get-started/) - Basic sandbox usage without sessions -- [Build a data analysis platform](/sandbox/tutorials/data-analysis/) - Multi-session example -``` +- [Session management concept](/sandbox/concepts/sessions/) - How sessions work +- [Commands API](/sandbox/api/commands/) - Execute commands From cf1ede0e76eb8a0a64ea761fb9947268cbcad909 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 18:07:28 +0100 Subject: [PATCH 17/22] Update icons --- src/content/docs/sandbox/api/index.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 75b94f0678d281c..6799a789554b406 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -28,7 +28,7 @@ The Sandbox SDK is organized into focused APIs: Execute commands and stream output. Includes `exec()`, `execStream()`, and background process management. @@ -52,7 +52,7 @@ The Sandbox SDK is organized into focused APIs: Expose services running in the sandbox via preview URLs. Access web servers and APIs from the internet. @@ -60,7 +60,7 @@ The Sandbox SDK is organized into focused APIs: Advanced: Create isolated execution contexts with persistent shell state. Configure environment variables and manage container lifecycle. From b5ae1e09bb81a00b39e85afd7cb173f1fea74165 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 18:18:16 +0100 Subject: [PATCH 18/22] Simplify getting started page --- src/content/docs/sandbox/get-started.mdx | 262 +++++++++++------------ 1 file changed, 124 insertions(+), 138 deletions(-) diff --git a/src/content/docs/sandbox/get-started.mdx b/src/content/docs/sandbox/get-started.mdx index c44a9be5808166b..7ceb7feb714898e 100644 --- a/src/content/docs/sandbox/get-started.mdx +++ b/src/content/docs/sandbox/get-started.mdx @@ -7,14 +7,11 @@ sidebar: import { Render, PackageManagers, Steps, WranglerConfig } from "~/components"; -This guide will walk you through creating your first secure code execution environment using Sandbox SDK. You'll learn how to: +Build your first application with Sandbox SDK - a secure code execution environment running on Cloudflare's global network. In this guide, you'll create a Worker that can execute Python code and work with files in isolated containers. -- Create a Worker that can execute code in isolated sandboxes -- Run commands and capture output -- Work with files in the sandbox -- Deploy your application globally - -By the end of this guide, you'll have a working sandbox that can safely execute Python code. +:::note[What you're building] +A simple API that can safely execute Python code and perform file operations, all running in isolated sandbox environments on Cloudflare's edge network. +::: ## Prerequisites @@ -22,205 +19,194 @@ By the end of this guide, you'll have a working sandbox that can safely execute ### Ensure Docker is running locally -Sandbox SDK uses [Docker](https://www.docker.com/) to build and push container images alongside your Worker. Docker must be running locally when you run `wrangler deploy`. The easiest way to install Docker is to follow the [Docker Desktop installation guide](https://docs.docker.com/desktop/). +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/). -You can verify Docker is running by executing: +Verify Docker is running: ```sh docker info ``` -If Docker is running, the command will succeed. If not, it will hang or return "Cannot connect to the Docker daemon". +If Docker is not running, this command will hang or return "Cannot connect to the Docker daemon". -## 1. Create your first sandbox project +## 1. Create a new project -Create a new project using the Sandbox SDK template: +Create a new Sandbox SDK project: -This creates a new `my-sandbox` directory with: +This creates a `my-sandbox` directory with everything you need: -- A Worker at `src/index.ts` configured to handle sandbox requests -- A `wrangler.jsonc` configuration file with container and Durable Objects setup -- A `Dockerfile` that defines the sandbox container environment - -Change into your new project directory: +- `src/index.ts` - Worker with sandbox integration +- `wrangler.jsonc` - Configuration for Workers and Containers +- `Dockerfile` - Container environment definition ```sh cd my-sandbox ``` -## 2. Understanding the configuration - -Your `wrangler.jsonc` file contains the configuration for both your Worker and the sandbox container: - - - -```jsonc -{ - "containers": [ - { - "class_name": "Sandbox", - "image": "./Dockerfile", - "name": "sandbox", - "max_instances": 1 - } - ], - "durable_objects": { - "bindings": [ - { - "class_name": "Sandbox", - "name": "Sandbox" - } - ] - }, - "migrations": [ - { - "new_sqlite_classes": ["Sandbox"], - "tag": "v1" - } - ] -} -``` - - - -Key points: - -- `containers` - Defines the container image and configuration -- `durable_objects` - Each sandbox runs in its own Durable Object for isolation -- `migrations` - Required for Durable Objects using SQLite storage +## 2. Explore the template -## 3. Create your first sandbox Worker - -Replace the contents of `src/index.ts` with this simple example: +The template provides a minimal Worker that demonstrates core sandbox capabilities: ```typescript -import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; export { Sandbox } from '@cloudflare/sandbox'; type Env = { - Sandbox: DurableObjectNamespace; + Sandbox: DurableObjectNamespace; }; export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - // Get a sandbox instance with a unique ID - const sandbox = getSandbox(env.Sandbox, 'my-first-sandbox'); - - // Execute Python code in the sandbox - if (url.pathname === '/run') { - const result = await sandbox.exec('python', [ - '-c', - 'print("Hello from Sandbox SDK!")' - ]); - - return Response.json({ - stdout: result.stdout, - stderr: result.stderr, - exitCode: result.exitCode, - success: result.success - }); - } - - // Write and read a file - if (url.pathname === '/file') { - await sandbox.writeFile('/tmp/message.txt', 'Hello, World!'); - const content = await sandbox.readFile('/tmp/message.txt'); - - return Response.json({ - content: content, - message: 'File written and read successfully' - }); - } - - return new Response('Try /run or /file endpoints', { status: 200 }); - }, + 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'); + }, }; ``` -This Worker demonstrates two key Sandbox SDK features: +**Key concepts**: -1. **Command Execution** - Run Python code and capture output -2. **File Operations** - Write and read files in the sandbox +- `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 -## 4. Test locally +## 3. Test locally -Before deploying, test your sandbox locally: +Start the development server: ```sh npm run dev ``` :::note -The first time you run `wrangler dev`, it will build the Docker container image. This may take a few minutes. Subsequent runs will be much faster due to Docker's layer caching. +First run builds the Docker container (2-3 minutes). Subsequent runs are much faster due to caching. ::: -Once the dev server is running, open your browser and visit: +Test the endpoints: -- `http://localhost:8787/run` - Execute Python code -- `http://localhost:8787/file` - Test file operations +```sh +# Execute Python code +curl http://localhost:8787/run -You should see JSON responses with the output from each operation. +# File operations +curl http://localhost:8787/file +``` -## 5. Deploy your sandbox +You should see JSON responses with the command output and file contents. -Deploy your Worker and sandbox container to Cloudflare's global network: +## 4. Deploy to production + +Deploy your Worker and container: ```sh npx wrangler deploy ``` -When you run `wrangler deploy`: - -1. Wrangler builds your container image using Docker -2. The image is pushed to Cloudflare's Container Registry -3. Your Worker is deployed with the sandbox configuration +This will: +1. Build your container image using Docker +2. Push it to Cloudflare's Container Registry +3. Deploy your Worker globally -:::note -After your first deployment, wait 2-3 minutes for the container to be fully provisioned before making requests. During this time, the Worker is deployed but sandbox operations may error. +:::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 - -Verify your sandbox is ready: +Check deployment status: ```sh npx wrangler containers list ``` -This shows all containers in your account and their deployment status. - -## 6. Test your deployed sandbox +## 5. Test your deployment -Visit your deployed Worker URL (shown in the deploy output): +Visit your Worker URL (shown in deploy output): -``` -https://my-sandbox..workers.dev/run +```sh +# Replace with your actual URL +curl https://my-sandbox.YOUR_SUBDOMAIN.workers.dev/run ``` -You should see the same JSON response as when testing locally, but now running on Cloudflare's global network. +Your sandbox is now running globally on Cloudflare's network. + +## 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" + } + ] +} +``` -## Summary + -In this guide, you: +- **containers** - Your Dockerfile defines the execution environment +- **durable_objects** - Makes the `Sandbox` binding available in your Worker +- **migrations** - Initializes Durable Object storage (required once) -- Created a Worker with Sandbox SDK integration -- Executed Python code in an isolated sandbox -- Worked with files in the sandbox environment -- Deployed your sandbox globally +For detailed configuration options including environment variables, secrets, and custom images, see the [Wrangler configuration reference](/sandbox/configuration/wrangler/). ## Next steps -- Learn about [command execution](/sandbox/guides/execute-commands/) with streaming output -- Explore [file operations](/sandbox/guides/manage-files/) and working with projects -- Set up [preview URLs](/sandbox/guides/expose-services/) to expose services running in your sandbox -- Review the [API reference](/sandbox/api/) for all available methods -- Check out [examples](/sandbox/examples/) including AI code execution and data analysis +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 From 39e8d920d1d08477fc8d58f85a2e0d14fcbec719 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 18:30:49 +0100 Subject: [PATCH 19/22] Update code executor example --- src/content/docs/sandbox/get-started.mdx | 6 +- .../sandbox/tutorials/ai-code-executor.mdx | 232 +++++++++ .../tutorials/build-an-ai-code-executor.mdx | 489 ------------------ 3 files changed, 235 insertions(+), 492 deletions(-) create mode 100644 src/content/docs/sandbox/tutorials/ai-code-executor.mdx delete mode 100644 src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx diff --git a/src/content/docs/sandbox/get-started.mdx b/src/content/docs/sandbox/get-started.mdx index 7ceb7feb714898e..f1d496f64d1399d 100644 --- a/src/content/docs/sandbox/get-started.mdx +++ b/src/content/docs/sandbox/get-started.mdx @@ -7,10 +7,10 @@ sidebar: import { Render, PackageManagers, Steps, WranglerConfig } from "~/components"; -Build your first application with Sandbox SDK - a secure code execution environment running on Cloudflare's global network. In this guide, you'll create a Worker that can execute Python code and work with files in isolated containers. +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, all running in isolated sandbox environments on Cloudflare's edge network. +A simple API that can safely execute Python code and perform file operations in isolated sandbox environments. ::: ## Prerequisites @@ -161,7 +161,7 @@ Visit your Worker URL (shown in deploy output): curl https://my-sandbox.YOUR_SUBDOMAIN.workers.dev/run ``` -Your sandbox is now running globally on Cloudflare's network. +Your sandbox is now deployed. ## Understanding the configuration 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..911951c163217f1 --- /dev/null +++ b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx @@ -0,0 +1,232 @@ +--- +pcx_content_type: tutorial +title: Build an AI code executor +difficulty: Beginner +products: + - Workers + - Sandbox +tags: + - AI + - Python + - Security +sidebar: + order: 1 +description: Build an AI code execution system that safely runs Python code generated by Claude. +--- + +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/build-an-ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx deleted file mode 100644 index bca9b356a673e9d..000000000000000 --- a/src/content/docs/sandbox/tutorials/build-an-ai-code-executor.mdx +++ /dev/null @@ -1,489 +0,0 @@ ---- -pcx_content_type: tutorial -title: Build an AI code executor -difficulty: Beginner -products: - - Workers - - Sandbox -tags: - - AI - - Python - - Security -sidebar: - order: 1 -description: Build a production-ready AI code execution system that safely runs Python code generated by Claude. ---- - -import { Render, PackageManagers, WranglerConfig } from "~/components"; - -In this tutorial, you will build a production-ready AI code execution system using Sandbox SDK and Anthropic's Claude. The system will accept natural language questions, generate Python code to answer them, execute the code securely, and return results with proper error handling. - -By the end of this tutorial, you will have: -- A Worker that integrates Claude with Sandbox SDK -- Secure code execution with proper isolation -- Streaming output support -- Error handling and validation -- A deployed application on Cloudflare's global network - -**Time to complete:** 20 minutes -**Level:** Beginner (no prior Cloudflare experience needed) - -## What you will build - -A production-ready code executor that: -- Accepts natural language questions (e.g., "What's the 100th Fibonacci number?") -- Uses Claude to generate Python code -- Executes the code securely in a sandbox -- Returns results with proper error handling -- Supports streaming output for long-running operations - -## Prerequisites - - - -You will also need: -- An [Anthropic API key](https://console.anthropic.com/) to use Claude -- [Docker](https://www.docker.com/) running locally for container builds - -## 1. Create your project - -Create a new Sandbox SDK project using the template: - - - - - -Change into your new project directory: - -```sh -cd ai-code-executor -``` - -## 2. Install dependencies - -Install the Anthropic SDK to interact with Claude: - - - -## 3. Configure your Worker - -Your `wrangler.jsonc` file already contains the required configuration for Sandbox SDK (containers and Durable Objects). Verify it looks like this: - - - -```jsonc -{ - "name": "ai-code-executor", - "main": "src/index.ts", - "compatibility_date": "2025-05-06", - "compatibility_flags": ["nodejs_compat"], - "containers": [ - { - "class_name": "Sandbox", - "image": "./Dockerfile", - "name": "sandbox", - "max_instances": 10 - } - ], - "durable_objects": { - "bindings": [ - { - "class_name": "Sandbox", - "name": "Sandbox" - } - ] - }, - "migrations": [ - { - "new_sqlite_classes": ["Sandbox"], - "tag": "v1" - } - ] -} -``` - - - -## 4. Build your AI code executor - -Replace the contents of `src/index.ts` with the following code: - -```typescript collapse={1-16, 18-35} -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; -} - -interface CodeExecutionRequest { - question: string; -} - -export default { - async fetch(request: Request, env: Env): Promise { - // Handle CORS preflight - if (request.method === 'OPTIONS') { - return new Response(null, { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - } - }); - } - - const url = new URL(request.url); - - // Health check endpoint - if (url.pathname === '/health') { - return Response.json({ - status: 'healthy', - timestamp: new Date().toISOString() - }); - } - - // Main code execution endpoint - if (url.pathname === '/execute' && request.method === 'POST') { - try { - const { question }: CodeExecutionRequest = await request.json(); - - if (!question) { - return Response.json( - { error: 'Question is required' }, - { status: 400 } - ); - } - - // Initialize Claude client - const anthropic = new Anthropic({ - apiKey: env.ANTHROPIC_API_KEY, - }); - - // Get sandbox instance - use user ID for multi-tenancy in production - const sandboxId = 'demo-user'; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - // Step 1: Ask Claude to generate Python code - const codeGeneration = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-latest', - max_tokens: 1024, - messages: [ - { - role: 'user', - content: `You are a Python code generator. Generate Python code to answer this question: "${question}" - -Requirements: -- Write complete, executable Python code -- Print the final result using print() -- Keep code simple and safe -- Only use Python standard library -- Do not use any external packages -- The code should be self-contained - -Return ONLY the Python code, no explanations or markdown formatting.` - } - ], - }); - - // Extract the generated code - const generatedCode = codeGeneration.content[0]?.type === 'text' - ? codeGeneration.content[0].text - : ''; - - if (!generatedCode) { - return Response.json( - { error: 'Failed to generate code' }, - { status: 500 } - ); - } - - // Step 2: Write the code to a file in the sandbox - const codeFilePath = '/tmp/generated_code.py'; - await sandbox.writeFile(codeFilePath, generatedCode); - - // Step 3: Execute the code in the sandbox - const result = await sandbox.exec('python', [codeFilePath]); - - // Step 4: Return the results - return Response.json({ - success: result.success, - question, - code: generatedCode, - output: result.stdout, - error: result.stderr, - exitCode: result.exitCode, - }, { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - }); - - } catch (error: any) { - console.error('Execution error:', error); - return Response.json( - { - error: 'Internal server error', - message: error.message - }, - { status: 500 } - ); - } - } - - // Default response - return new Response( - 'AI Code Executor\n\nPOST /execute with { "question": "your question" }', - { - status: 200, - headers: { 'Content-Type': 'text/plain' } - } - ); - }, -}; -``` - -This Worker: -1. **Receives questions** via POST requests to `/execute` -2. **Uses Claude** to generate Python code that answers the question -3. **Writes the code** to a file in the sandbox -4. **Executes** the code securely -5. **Returns** both the generated code and execution results - -## 5. Set your Anthropic API key - -Before deploying, store your Anthropic API key as a secret: - -```sh -npx wrangler secret put ANTHROPIC_API_KEY -``` - -When prompted, paste your API key from the [Anthropic Console](https://console.anthropic.com/). - -## 6. Test locally - -Start the local development server: - -```sh -npm run dev -``` - -:::note -The first run will build the Docker container image. This takes 2-3 minutes. Subsequent runs are much faster due to Docker's layer caching. -::: - -Once running, test your code executor with curl: - -```sh -curl -X POST http://localhost:8787/execute \ - -H "Content-Type: application/json" \ - -d '{"question": "What is the 10th Fibonacci number?"}' -``` - -You should see a response like: - -```json output -{ - "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\nresult = fibonacci(10)\nprint(result)", - "output": "55\n", - "error": "", - "exitCode": 0 -} -``` - -## 7. Deploy to production - -Deploy your Worker to Cloudflare's global network: - -```sh -npx wrangler deploy -``` - -When deployment completes, Wrangler will output your Worker's URL: - -```txt output -Published ai-code-executor (1.23 sec) - https://ai-code-executor..workers.dev -``` - -:::note -After your first deployment, wait 2-3 minutes for the container to be fully provisioned. During this time, requests may fail. Check the status with: - -```sh -npx wrangler containers list -``` -::: - -## 8. Test your deployed Worker - -Test your production deployment: - -```sh -curl -X POST https://ai-code-executor..workers.dev/execute \ - -H "Content-Type: application/json" \ - -d '{"question": "Calculate the factorial of 5"}' -``` - -Try different questions: - -```sh -# Statistical calculation -curl -X POST https://ai-code-executor..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..workers.dev/execute \ - -H "Content-Type: application/json" \ - -d '{"question": "Reverse the string \"Hello World\""}' - -# Mathematical computation -curl -X POST https://ai-code-executor..workers.dev/execute \ - -H "Content-Type: application/json" \ - -d '{"question": "What is 2 to the power of 10?"}' -``` - -## 9. Add error handling (optional) - -For production use, enhance error handling by creating a validation function. Add this to your Worker: - -```typescript -function validateCode(code: string): { safe: boolean; reason?: string } { - // Basic safety checks - const dangerousPatterns = [ - /import\s+os/i, - /import\s+sys/i, - /import\s+subprocess/i, - /exec\(/i, - /eval\(/i, - /__import__/i, - /open\(/i, - ]; - - for (const pattern of dangerousPatterns) { - if (pattern.test(code)) { - return { - safe: false, - reason: `Code contains potentially dangerous pattern: ${pattern}` - }; - } - } - - return { safe: true }; -} -``` - -Then update your code execution logic: - -```typescript -// After generating code, before executing -const validation = validateCode(generatedCode); -if (!validation.safe) { - return Response.json( - { - error: 'Generated code failed safety validation', - reason: validation.reason - }, - { status: 400 } - ); -} -``` - -## 10. Add streaming support (optional) - -For long-running code, add streaming support to show real-time output: - -```typescript -// Streaming execution endpoint -if (url.pathname === '/execute/stream' && request.method === 'POST') { - const { question }: CodeExecutionRequest = await request.json(); - - // ... Claude code generation ... - - const sandbox = getSandbox(env.Sandbox, 'demo-user'); - await sandbox.writeFile('/tmp/generated_code.py', generatedCode); - - // Create a streaming response - const { readable, writable } = new TransformStream(); - const writer = writable.getWriter(); - const encoder = new TextEncoder(); - - // Execute with streaming - (async () => { - try { - const result = await sandbox.exec('python', ['/tmp/generated_code.py']); - - // Stream the output - await writer.write(encoder.encode(JSON.stringify({ - type: 'output', - data: result.stdout - }) + '\n')); - - await writer.write(encoder.encode(JSON.stringify({ - type: 'complete', - exitCode: result.exitCode - }) + '\n')); - } catch (error: any) { - await writer.write(encoder.encode(JSON.stringify({ - type: 'error', - message: error.message - }) + '\n')); - } finally { - await writer.close(); - } - })(); - - return new Response(readable, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Access-Control-Allow-Origin': '*', - } - }); -} -``` - -## Summary - -In this tutorial, you built a production-ready AI code execution system that: - -- ✅ Integrates Claude API with Sandbox SDK -- ✅ Generates Python code from natural language -- ✅ Executes code securely in isolated sandboxes -- ✅ Returns results with proper error handling -- ✅ Runs on Cloudflare's global network - -## Next steps - -Explore more ways to use Sandbox SDK: - -- [Build a data analysis platform](/sandbox/tutorials/data-analysis/) with pandas and matplotlib -- [Create an interactive development environment](/sandbox/tutorials/dev-environment/) with preview URLs -- Learn about [file operations](/sandbox/guides/manage-files/) for working with larger projects -- Explore [background processes](/sandbox/guides/background-processes/) for long-running tasks -- Review the [complete API reference](/sandbox/api/) for all available methods - -## Related resources - -- [Anthropic Claude documentation](https://docs.anthropic.com/) -- [Sandbox SDK API reference](/sandbox/api/) -- [Workers AI integration](/workers-ai/) for using Cloudflare's built-in models -- [Best practices for production deployments](/sandbox/best-practices/) From 8dfcf479336e1a19828d6d769797e3c471a14b29 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 18:38:47 +0100 Subject: [PATCH 20/22] Update tutorials --- .../tutorials/analyze-data-with-ai.mdx | 807 ++++------------ .../tutorials/automated-testing-pipeline.mdx | 859 +++--------------- .../sandbox/tutorials/code-review-bot.mdx | 797 ++++------------ src/content/docs/sandbox/tutorials/index.mdx | 34 +- 4 files changed, 504 insertions(+), 1993 deletions(-) diff --git a/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx index 1e5ad93d4001cae..227a4b45faa3de7 100644 --- a/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx +++ b/src/content/docs/sandbox/tutorials/analyze-data-with-ai.mdx @@ -5,700 +5,233 @@ sidebar: order: 2 --- -import { Render, PackageManagers, TabItem, Tabs } from "~/components"; +import { Render, PackageManagers } from "~/components"; -Build a complete AI-powered data analysis system that uploads CSV datasets, generates Python analysis code using Claude, executes it securely in sandboxes, and returns visualizations. - -## What you'll build - -A production-ready data analysis Worker that: - -- Accepts CSV file uploads from users -- Uses Claude to generate Python analysis code based on user questions -- Executes the generated code safely in a sandbox -- Returns charts, statistics, and insights -- Handles errors gracefully with proper validation +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 -**What you'll learn**: -- Upload files to sandboxes -- Integrate Claude's function calling with sandboxes -- Execute AI-generated Python code safely -- Handle rich outputs (charts, tables, text) -- Download files from sandboxes -- Production error handling patterns - ## Prerequisites You'll also need: -- An [Anthropic API key](https://console.anthropic.com/settings/keys) (Claude) -- Basic understanding of async/await in TypeScript -- Familiarity with CSV data +- An [Anthropic API key](https://console.anthropic.com/) for Claude +- [Docker](https://www.docker.com/) running locally -## 1. Create your Worker project +## 1. Create your project -Create a new Worker project: +Create a new Sandbox SDK project: - + - +```sh +cd analyze-data +``` ## 2. Install dependencies -Install the Sandbox SDK and Anthropic SDK: + - +## 3. Build the analysis handler -## 3. Configure your Worker - -Update `wrangler.jsonc` to include the Sandbox binding and Anthropic API key: - -```jsonc title="wrangler.jsonc" -{ - "name": "analyze-data-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" - } - ], - "vars": { - "ANTHROPIC_API_KEY": "your-anthropic-api-key-here" - } -} -``` +Replace `src/index.ts`: -:::caution[Security] -For production, use [Wrangler secrets](/workers/configuration/secrets/) instead of `vars`: -```bash -wrangler secret put ANTHROPIC_API_KEY -``` -::: +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; +import Anthropic from '@anthropic-ai/sdk'; -Update your TypeScript environment configuration: +export { Sandbox } from '@cloudflare/sandbox'; -```typescript title="src/index.ts" interface Env { - Sandbox: DurableObjectNamespace; - ANTHROPIC_API_KEY: string; + Sandbox: DurableObjectNamespace; + ANTHROPIC_API_KEY: string; } -``` - -## 4. Download example dataset - -For this tutorial, we'll use a real dataset. Download this sample movies dataset: - -1. Visit [TMDB 10K Movies Dataset](https://www.kaggle.com/datasets/muqarrishzaib/tmdb-10000-movies-dataset) -2. Download the CSV file -3. Save it as `test-dataset.csv` in your project root - -The dataset contains columns like: -- `title` - Movie name -- `release_date` - Release date (YYYY-MM-DD) -- `vote_average` - Rating (0-10) -- `vote_count` - Number of votes -- `popularity` - Popularity score -- `original_language` - Language code - -We'll use this for testing locally. - -## 5. Build the data analysis handler - -Create the main handler that orchestrates the data analysis workflow: - -```typescript title="src/index.ts" -import { getSandbox } from '@cloudflare/sandbox'; -import Anthropic from '@anthropic-ai/sdk'; export default { - async fetch(request: Request, env: Env): Promise { - // Only accept POST requests - if (request.method !== 'POST') { - return Response.json( - { error: 'Method not allowed. Use POST to upload CSV and question.' }, - { status: 405 } - ); - } - - try { - // Parse the multipart form data - const formData = await request.formData(); - const csvFile = formData.get('file') as File; - const question = formData.get('question') as string; - - // Validate inputs - if (!csvFile || !question) { - return Response.json( - { error: 'Missing required fields: file and question' }, - { status: 400 } - ); - } - - if (!csvFile.name.endsWith('.csv')) { - return Response.json( - { error: 'File must be a CSV file' }, - { status: 400 } - ); - } - - // Create a unique sandbox for this request - const sandboxId = `analysis-${Date.now()}-${Math.random().toString(36).substring(7)}`; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - try { - // Step 1: Upload the CSV to the sandbox - const csvContent = await csvFile.arrayBuffer(); - const csvPath = '/workspace/dataset.csv'; - await sandbox.writeFile(csvPath, new Uint8Array(csvContent)); - - console.log(`Uploaded ${csvFile.name} to sandbox at ${csvPath}`); - - // Step 2: Analyze CSV structure - const structureResult = await sandbox.exec( - `python3 -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print('Rows:', len(df)); print('Columns:', ', '.join(df.columns.tolist())); print('Types:', df.dtypes.to_dict())"` - ); - - if (!structureResult.success) { - throw new Error(`Failed to analyze CSV: ${structureResult.stderr}`); - } - - console.log('CSV structure:', structureResult.stdout); - - // Step 3: Generate analysis code with Claude - const analysisCode = await generateAnalysisCode( - env.ANTHROPIC_API_KEY, - csvPath, - question, - structureResult.stdout - ); - - console.log('Generated code:', analysisCode); - - // Step 4: Execute the analysis code - const analysisResult = await sandbox.exec( - `python3 -c "${analysisCode.replace(/"/g, '\\"')}"` - ); - - if (!analysisResult.success) { - return Response.json( - { - error: 'Analysis code failed', - details: analysisResult.stderr, - code: analysisCode - }, - { status: 500 } - ); - } - - // Step 5: Check for generated charts (saved as PNG) - let chartData: string | null = null; - try { - const chartPath = '/workspace/chart.png'; - const chartBuffer = await sandbox.readFile(chartPath); - // Convert to base64 for JSON response - chartData = btoa(String.fromCharCode(...new Uint8Array(chartBuffer))); - } catch (error) { - console.log('No chart generated (this is OK)'); - } - - // Step 6: Return the results - return Response.json({ - success: true, - question, - output: analysisResult.stdout, - error: analysisResult.stderr || null, - chart: chartData ? `data:image/png;base64,${chartData}` : null, - executionTime: analysisResult.duration, - code: analysisCode - }); - - } finally { - // Always clean up the sandbox - await sandbox.destroy(); - } - - } catch (error) { - console.error('Error:', error); - return Response.json( - { - error: 'Internal server error', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } - } + 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 }); + } + }, }; -``` -## 6. Implement Claude integration - -Add the function that generates Python analysis code using Claude's function calling: - -```typescript title="src/index.ts" async function generateAnalysisCode( - apiKey: string, - csvPath: string, - question: string, - csvStructure: string + apiKey: string, + csvPath: string, + question: string, + csvStructure: string ): Promise { - const client = new Anthropic({ apiKey }); - - const prompt = ` -I have a CSV file located at ${csvPath} with the following structure: + 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} -The user wants to analyze this data. Their question is: -"${question}" +Question: "${question}" Generate Python code that: -1. Reads the CSV using pandas -2. Answers the user's question with appropriate analysis -3. If visualization would help, create a chart and save it as '/workspace/chart.png' -4. Prints the key findings to stdout - -Important: -- Use pandas, numpy, and matplotlib (they're installed) -- Save any charts to '/workspace/chart.png' -- Print clear, concise findings -- Handle missing data appropriately -- Keep output focused and relevant -`; - - const response = await client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 2048, - messages: [ - { - role: 'user', - content: prompt - } - ], - tools: [ - { - name: 'generate_python_code', - description: 'Generate Python code for data analysis', - input_schema: { - type: 'object', - properties: { - code: { - type: 'string', - description: 'Complete Python code to analyze the data' - }, - explanation: { - type: 'string', - description: 'Brief explanation of what the code does' - } - }, - required: ['code'] - } - } - ] - }); - - // Extract the generated code from tool use - for (const block of response.content) { - if (block.type === 'tool_use' && block.name === 'generate_python_code') { - const input = block.input as { code: string; explanation?: string }; - console.log('Claude explanation:', input.explanation); - return input.code; - } - } - - // Fallback: extract code from text response - const textBlock = response.content.find(block => block.type === 'text'); - if (textBlock && textBlock.type === 'text') { - // Try to extract code from markdown code blocks - const codeMatch = textBlock.text.match(/```python\n([\s\S]*?)\n```/); - if (codeMatch) { - return codeMatch[1]; - } - return textBlock.text; - } - - throw new Error('Claude did not generate code'); +- 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'); } ``` -## 7. Test locally +## 4. Set your API key -Start your development server: - -```bash -wrangler dev +```sh +npx wrangler secret put ANTHROPIC_API_KEY ``` -In a separate terminal, test the data analysis endpoint: - -```bash -curl -X POST http://localhost:8787/ \ - -F "file=@test-dataset.csv" \ - -F "question=What is the trend in movie ratings over time?" -``` +## 5. Test locally -You should see a response like: +Download a sample CSV: -```json -{ - "success": true, - "question": "What is the trend in movie ratings over time?", - "output": "Analysis complete:\n- Average rating from 1900-1980: 6.2\n- Average rating from 1980-2000: 6.5\n- Average rating from 2000-2020: 6.8\n- Overall trend: Ratings have increased over time", - "chart": "data:image/png;base64,iVBORw0KGgoAAAANS...", - "executionTime": 2341, - "code": "import pandas as pd\nimport matplotlib.pyplot as plt\n..." -} +```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 ``` -Try different questions: - -```bash -# Find most popular languages -curl -X POST http://localhost:8787/ \ - -F "file=@test-dataset.csv" \ - -F "question=Which languages have the most movies?" +Start the dev server: -# Analyze vote patterns -curl -X POST http://localhost:8787/ \ - -F "file=@test-dataset.csv" \ - -F "question=What's the relationship between vote count and rating?" +```sh +npm run dev ``` -## 8. Add a web interface (optional) +Test with curl: -Create a simple HTML interface for easier testing: - -```typescript title="src/index.ts" -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - // Serve HTML form for GET requests - if (request.method === 'GET' && url.pathname === '/') { - return new Response(HTML_FORM, { - headers: { 'Content-Type': 'text/html' } - }); - } - - // Handle POST requests (data analysis) - if (request.method === 'POST' && url.pathname === '/analyze') { - // ... existing analysis code ... - } - - return Response.json({ error: 'Not found' }, { status: 404 }); - } -}; - -const HTML_FORM = ` - - - - AI Data Analysis - - - -

📊 AI Data Analysis

-

Upload a CSV file and ask a question about your data. Claude will generate Python code to analyze it.

- -
-
- - -
-
- - -
- -
- -
- - - - -`; -``` - -Update the form POST URL to `/analyze` and test at `http://localhost:8787`. - -## 9. Deploy to production - -Deploy your Worker to Cloudflare's global network: - -```bash -wrangler deploy +```sh +curl -X POST http://localhost:8787 \ + -F "file=@test.csv" \ + -F "question=What is the average rating by year?" ``` -Your data analysis system is now live! Test it: +Response: -```bash -curl -X POST https://analyze-data-worker.YOUR_SUBDOMAIN.workers.dev/analyze \ - -F "file=@test-dataset.csv" \ - -F "question=What trends do you see in this data?" -``` - -## Complete code - -
-View the complete Worker code - -```typescript title="src/index.ts" -import { getSandbox } from '@cloudflare/sandbox'; -import Anthropic from '@anthropic-ai/sdk'; - -interface Env { - Sandbox: DurableObjectNamespace; - ANTHROPIC_API_KEY: string; +```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..." } +``` -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - if (request.method === 'GET' && url.pathname === '/') { - return new Response(HTML_FORM, { - headers: { 'Content-Type': 'text/html' } - }); - } - - if (request.method !== 'POST') { - return Response.json( - { error: 'Method not allowed' }, - { 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 required fields: file and question' }, - { status: 400 } - ); - } - - if (!csvFile.name.endsWith('.csv')) { - return Response.json( - { error: 'File must be a CSV file' }, - { status: 400 } - ); - } - - const sandboxId = `analysis-${Date.now()}-${Math.random().toString(36).substring(7)}`; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - try { - const csvContent = await csvFile.arrayBuffer(); - const csvPath = '/workspace/dataset.csv'; - await sandbox.writeFile(csvPath, new Uint8Array(csvContent)); - - const structureResult = await sandbox.exec( - `python3 -c "import pandas as pd; df = pd.read_csv('${csvPath}'); print('Rows:', len(df)); print('Columns:', ', '.join(df.columns.tolist()))"` - ); - - if (!structureResult.success) { - throw new Error(`Failed to analyze CSV: ${structureResult.stderr}`); - } - - const analysisCode = await generateAnalysisCode( - env.ANTHROPIC_API_KEY, - csvPath, - question, - structureResult.stdout - ); - - const analysisResult = await sandbox.exec( - `python3 -c "${analysisCode.replace(/"/g, '\\"')}"` - ); - - if (!analysisResult.success) { - return Response.json( - { - error: 'Analysis code failed', - details: analysisResult.stderr, - code: analysisCode - }, - { status: 500 } - ); - } - - let chartData: string | null = null; - try { - const chartBuffer = await sandbox.readFile('/workspace/chart.png'); - chartData = btoa(String.fromCharCode(...new Uint8Array(chartBuffer))); - } catch { - console.log('No chart generated'); - } - - return Response.json({ - success: true, - question, - output: analysisResult.stdout, - chart: chartData ? `data:image/png;base64,${chartData}` : null, - executionTime: analysisResult.duration, - code: analysisCode - }); - - } finally { - await sandbox.destroy(); - } - - } catch (error) { - console.error('Error:', error); - return Response.json( - { - error: 'Internal server error', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } - } -}; - -async function generateAnalysisCode( - apiKey: string, - csvPath: string, - question: string, - csvStructure: string -): Promise { - const client = new Anthropic({ apiKey }); - - const prompt = ` -I have a CSV file at ${csvPath} with structure: -${csvStructure} - -Question: "${question}" - -Generate Python code that answers this question. If visualization helps, save chart as '/workspace/chart.png'. -Use pandas, numpy, matplotlib. Print findings to stdout. -`; - - const response = await client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 2048, - messages: [{ role: 'user', content: prompt }], - tools: [ - { - name: 'generate_python_code', - description: 'Generate Python code for data analysis', - input_schema: { - type: 'object', - properties: { - code: { type: 'string' } - }, - 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('Claude did not generate code'); -} +## 6. Deploy -const HTML_FORM = `...`; // HTML from step 8 +```sh +npx wrangler deploy ``` -
-## What you learned +:::caution +Wait 2-3 minutes after first deployment for container provisioning. +::: -You built a complete AI data analysis system that: +## What you built -- ✅ Uploads CSV files to sandboxes -- ✅ Uses Claude's function calling to generate analysis code -- ✅ Executes Python code safely in isolated sandboxes -- ✅ Returns text output and charts to users -- ✅ Handles errors gracefully -- ✅ Cleans up resources automatically +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 -Enhance your data analysis system: - -- **Support multiple file formats** - Add support for Excel, JSON, Parquet -- **Persist sandboxes** - Keep user sandboxes alive for multi-step analysis -- **Stream results** - Use `execStream()` for real-time progress updates -- **Add authentication** - Protect your API with [Workers Auth](/workers/runtime-apis/web-crypto/) -- **Store results** - Save analyses to [R2](/r2/) or [D1](/d1/) - -## Related resources - -- [File operations guide](/sandbox/guides/manage-files/) - Complete file management patterns -- [Code execution guide](/sandbox/guides/code-execution/) - Advanced code interpreter usage -- [Streaming output guide](/sandbox/guides/streaming-output/) - Real-time progress updates -- [Files API reference](/sandbox/api/files/) - Complete file operations API -- [Security model](/sandbox/concepts/security/) - Understanding isolation and safety +- [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 index 3482a6c5f1c785f..7324de9daea228a 100644 --- a/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx +++ b/src/content/docs/sandbox/tutorials/automated-testing-pipeline.mdx @@ -1,677 +1,163 @@ --- -title: Create an automated testing pipeline +title: Automated testing pipeline pcx_content_type: tutorial sidebar: order: 4 --- -import { Render, PackageManagers, TabItem, Tabs } from "~/components"; +import { Render, PackageManagers } from "~/components"; -Build a CI/CD testing pipeline that clones repositories, installs dependencies, runs test suites, and generates detailed HTML reports with streaming output. - -## What you'll build - -A production-ready testing pipeline that: - -- Clones Git repositories on-demand -- Detects project type (Node.js, Python, Go, etc.) -- Installs dependencies automatically -- Runs test suites with streaming output -- Generates HTML test reports -- Caches dependencies for faster runs -- Handles timeouts and errors gracefully +Build a testing pipeline that clones Git repositories, installs dependencies, runs tests, and reports results. **Time to complete**: 25 minutes -**What you'll learn**: -- Clone and build projects in sandboxes -- Stream test output in real-time -- Install dependencies for multiple languages -- Generate and download test reports -- Implement caching strategies -- Handle long-running processes - ## Prerequisites -You'll also need: -- A GitHub repository with tests (public or with access token) -- Basic understanding of package managers (npm, pip, etc.) - -## 1. Create your Worker project - -Create a new Worker project: - - +You'll also need a GitHub repository with tests (public or private with access token). - +## 1. Create your project -## 2. Install dependencies + -Install the Sandbox SDK: - - - -## 3. Configure your Worker - -Update `wrangler.jsonc` to include the Sandbox binding: - -```jsonc title="wrangler.jsonc" -{ - "name": "test-pipeline-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" - } - ] -} +```sh +cd test-pipeline ``` -Update your TypeScript types: +## 2. Build the pipeline -```typescript title="src/index.ts" -interface Env { - Sandbox: DurableObjectNamespace; - GITHUB_TOKEN?: string; // Optional, for private repos -} -``` - -## 4. Create the pipeline handler +Replace `src/index.ts`: -Build the main handler that orchestrates the testing workflow: - -```typescript title="src/index.ts" -import { getSandbox } from '@cloudflare/sandbox'; +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; -interface TestRequest { - repoUrl: string; - branch?: string; - testCommand?: string; - installCommand?: string; -} +export { Sandbox } from '@cloudflare/sandbox'; -interface TestResult { - success: boolean; - output: string; - exitCode: number; - duration: number; - report?: string; // HTML report if available +interface Env { + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN?: string; } export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - // Health check - if (url.pathname === '/health') { - return Response.json({ status: 'ok', service: 'test-pipeline' }); - } - - // Main test endpoint - if (url.pathname === '/test' && request.method === 'POST') { - return handleTestRequest(request, env); - } - - // Streaming test endpoint - if (url.pathname === '/test/stream' && request.method === 'POST') { - return handleStreamingTest(request, env); - } - - return new Response(LANDING_PAGE, { - headers: { 'Content-Type': 'text/html' } - }); - } + 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 handleTestRequest( - request: Request, - env: Env -): Promise { - try { - const body = await request.json() as TestRequest; - - // Validate input - if (!body.repoUrl) { - return Response.json( - { error: 'Missing required field: repoUrl' }, - { status: 400 } - ); - } - - // Create a unique sandbox - const sandboxId = `test-${Date.now()}-${Math.random().toString(36).substring(7)}`; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - try { - console.log(`Starting test pipeline for ${body.repoUrl}`); - - // Run the complete test pipeline - const result = await runTestPipeline( - sandbox, - body.repoUrl, - body.branch || 'main', - body.testCommand, - body.installCommand, - env.GITHUB_TOKEN - ); - - return Response.json(result); - - } finally { - // Always clean up - await sandbox.destroy(); - } - - } catch (error) { - console.error('Test pipeline error:', error); - return Response.json( - { - error: 'Pipeline failed', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } -} -``` - -## 5. Implement the test pipeline - -Add the core pipeline logic that detects project type and runs tests: - -```typescript title="src/index.ts" -async function runTestPipeline( - sandbox: any, - repoUrl: string, - branch: string, - customTestCmd?: string, - customInstallCmd?: string, - githubToken?: string -): Promise { - const startTime = Date.now(); - - // Step 1: Clone the repository - console.log('Step 1: Cloning repository...'); - let cloneUrl = repoUrl; - - // Add GitHub token if provided (for private repos) - if (githubToken && repoUrl.includes('github.com')) { - cloneUrl = repoUrl.replace('https://', `https://${githubToken}@`); - } - - const cloneResult = await sandbox.exec( - `git clone --depth=1 --branch=${branch} ${cloneUrl} /workspace/repo`, - { timeout: 60000 } - ); - - if (!cloneResult.success) { - return { - success: false, - output: `Clone failed: ${cloneResult.stderr}`, - exitCode: cloneResult.exitCode, - duration: Date.now() - startTime - }; - } - - console.log('Repository cloned successfully'); - - // Step 2: Detect project type - console.log('Step 2: Detecting project type...'); - const projectType = await detectProjectType(sandbox); - console.log(`Detected project type: ${projectType}`); - - // Step 3: Install dependencies - console.log('Step 3: Installing dependencies...'); - const installCmd = customInstallCmd || getInstallCommand(projectType); - - if (installCmd) { - const installResult = await sandbox.exec( - `cd /workspace/repo && ${installCmd}`, - { timeout: 300000 } // 5 minutes for npm install - ); - - if (!installResult.success) { - return { - success: false, - output: `Install failed: ${installResult.stderr}`, - exitCode: installResult.exitCode, - duration: Date.now() - startTime - }; - } - - console.log('Dependencies installed'); - } - - // Step 4: Run tests - console.log('Step 4: Running tests...'); - const testCmd = customTestCmd || getTestCommand(projectType); - - const testResult = await sandbox.exec( - `cd /workspace/repo && ${testCmd}`, - { timeout: 300000 } // 5 minutes for tests - ); - - // Step 5: Try to find and read test report - let report: string | undefined; - const reportPaths = [ - '/workspace/repo/coverage/lcov-report/index.html', - '/workspace/repo/test-results.html', - '/workspace/repo/htmlcov/index.html', // Python coverage - '/workspace/repo/coverage.html' - ]; - - for (const reportPath of reportPaths) { - try { - const reportBuffer = await sandbox.readFile(reportPath); - report = new TextDecoder().decode(reportBuffer); - console.log(`Found test report at ${reportPath}`); - break; - } catch { - // Report not found, continue - } - } - - const duration = Date.now() - startTime; - - return { - success: testResult.exitCode === 0, - output: testResult.stdout + '\n' + testResult.stderr, - exitCode: testResult.exitCode, - duration, - report - }; -} - async function detectProjectType(sandbox: any): Promise { - // Check for package.json (Node.js) - try { - await sandbox.readFile('/workspace/repo/package.json'); - return 'nodejs'; - } catch {} - - // Check for requirements.txt or setup.py (Python) - try { - await sandbox.readFile('/workspace/repo/requirements.txt'); - return 'python'; - } catch {} - - try { - await sandbox.readFile('/workspace/repo/setup.py'); - return 'python'; - } catch {} - - // Check for go.mod (Go) - try { - await sandbox.readFile('/workspace/repo/go.mod'); - return 'go'; - } catch {} - - // Check for Cargo.toml (Rust) - try { - await sandbox.readFile('/workspace/repo/Cargo.toml'); - return 'rust'; - } catch {} - - return 'unknown'; + 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'; - case 'rust': - return 'cargo fetch'; - default: - return ''; - } + 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 ./...'; - case 'rust': - return 'cargo test'; - default: - return 'echo "No test command for this project type"'; - } + 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"'; + } } ``` -## 6. Add streaming support - -Implement real-time test output streaming using Server-Sent Events: - -```typescript title="src/index.ts" -async function handleStreamingTest( - request: Request, - env: Env -): Promise { - try { - const body = await request.json() as TestRequest; - - if (!body.repoUrl) { - return Response.json( - { error: 'Missing required field: repoUrl' }, - { status: 400 } - ); - } - - // Create SSE stream - const { readable, writable } = new TransformStream(); - const writer = writable.getWriter(); - const encoder = new TextEncoder(); - - // Send SSE helper - const sendEvent = (event: string, data: any) => { - const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; - writer.write(encoder.encode(message)); - }; - - // Run pipeline in background - (async () => { - const sandboxId = `test-stream-${Date.now()}`; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - try { - sendEvent('status', { message: 'Cloning repository...' }); - - // Clone - let cloneUrl = body.repoUrl; - if (env.GITHUB_TOKEN && body.repoUrl.includes('github.com')) { - cloneUrl = body.repoUrl.replace('https://', `https://${env.GITHUB_TOKEN}@`); - } - - const cloneResult = await sandbox.exec( - `git clone --depth=1 ${cloneUrl} /workspace/repo`, - { - timeout: 60000, - onStdout: (chunk) => sendEvent('output', { type: 'stdout', data: chunk }), - onStderr: (chunk) => sendEvent('output', { type: 'stderr', data: chunk }) - } - ); - - if (!cloneResult.success) { - sendEvent('error', { message: 'Clone failed', details: cloneResult.stderr }); - sendEvent('done', { success: false }); - await writer.close(); - return; - } - - sendEvent('status', { message: 'Detecting project type...' }); - const projectType = await detectProjectType(sandbox); - sendEvent('status', { message: `Detected ${projectType} project` }); - - // Install dependencies - const installCmd = body.installCommand || getInstallCommand(projectType); - if (installCmd) { - sendEvent('status', { message: 'Installing dependencies...' }); - - const installResult = await sandbox.exec( - `cd /workspace/repo && ${installCmd}`, - { - timeout: 300000, - onStdout: (chunk) => sendEvent('output', { type: 'stdout', data: chunk }), - onStderr: (chunk) => sendEvent('output', { type: 'stderr', data: chunk }) - } - ); - - if (!installResult.success) { - sendEvent('error', { message: 'Install failed' }); - sendEvent('done', { success: false }); - await writer.close(); - return; - } - } - - // Run tests - sendEvent('status', { message: 'Running tests...' }); - const testCmd = body.testCommand || getTestCommand(projectType); - - const testResult = await sandbox.exec( - `cd /workspace/repo && ${testCmd}`, - { - timeout: 300000, - onStdout: (chunk) => sendEvent('output', { type: 'stdout', data: chunk }), - onStderr: (chunk) => sendEvent('output', { type: 'stderr', data: chunk }) - } - ); - - sendEvent('done', { - success: testResult.exitCode === 0, - exitCode: testResult.exitCode - }); - - } catch (error) { - sendEvent('error', { - message: error instanceof Error ? error.message : 'Unknown error' - }); - sendEvent('done', { success: false }); - } finally { - await sandbox.destroy(); - await writer.close(); - } - })(); - - return new Response(readable, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive' - } - }); - - } catch (error) { - return Response.json( - { error: 'Failed to start streaming test' }, - { status: 500 } - ); - } -} -``` - -## 7. Add a web interface - -Create a simple web interface for testing: - -```typescript title="src/index.ts" -const LANDING_PAGE = ` - - - - Test Pipeline - - - -

🧪 Test Pipeline

-

Run automated tests for any Git repository with streaming output.

- -
-
- - -
-
- - -
-
- - -
- - -
- -
Running tests...
- - - - - -`; -``` - -## 8. Test locally - -Start your development server: - -```bash -wrangler dev -``` - -Visit `http://localhost:8787` in your browser and test with a public repository: - -**Example repositories to test:** -- Node.js: `https://github.com/vercel/next.js` (may take a while) -- Python: `https://github.com/psf/requests` -- Simple Node: `https://github.com/sindresorhus/is-promise` - -Try both regular and streaming modes to see the difference! - -## 9. Deploy to production +## 3. Test locally -Deploy your testing pipeline: +Start the dev server: -```bash -wrangler deploy +```sh +npm run dev ``` -Test the deployed endpoint: +Test with a repository: -```bash -curl -X POST https://test-pipeline-worker.YOUR_SUBDOMAIN.workers.dev/test \ +```sh +curl -X POST http://localhost:8787 \ -H "Content-Type: application/json" \ -d '{ "repoUrl": "https://github.com/sindresorhus/is-promise", @@ -679,106 +165,39 @@ curl -X POST https://test-pipeline-worker.YOUR_SUBDOMAIN.workers.dev/test \ }' ``` -## Production enhancements - -### Add caching for dependencies - -Use persistent sandboxes to cache installed dependencies: - -```typescript -// Instead of creating temporary sandboxes -const sandboxId = `test-${Date.now()}`; - -// Use project-specific sandboxes -const repoSlug = repoUrl.split('/').slice(-2).join('-'); -const sandboxId = `test-cache-${repoSlug}`; - -// Don't destroy after use -// await sandbox.destroy(); // Skip this for caching -``` - -### Add timeout handling +Response: -```typescript -const timeout = 600000; // 10 minutes -const controller = new AbortController(); -const timeoutId = setTimeout(() => controller.abort(), timeout); - -try { - await sandbox.exec(command, { signal: controller.signal }); -} catch (error) { - if (error.name === 'AbortError') { - return { error: 'Test timed out after 10 minutes' }; - } - throw error; -} finally { - clearTimeout(timeoutId); +```json +{ + "success": true, + "exitCode": 0, + "output": "...test output...", + "projectType": "nodejs" } ``` -### Add webhook integration +## 4. Deploy -Trigger tests on Git push events: - -```typescript -if (url.pathname === '/webhook/github' && request.method === 'POST') { - const payload = await request.json(); - - if (payload.ref === 'refs/heads/main') { - // Trigger test pipeline - await runTestPipeline( - sandbox, - payload.repository.clone_url, - 'main' - ); - } -} +```sh +npx wrangler deploy ``` -## Complete code - -
-View the complete Worker code +For private repositories, set your GitHub token: -```typescript title="src/index.ts" -import { getSandbox } from '@cloudflare/sandbox'; - -interface Env { - Sandbox: DurableObjectNamespace; - GITHUB_TOKEN?: string; -} - -// ... (include all code from steps 4-7) +```sh +npx wrangler secret put GITHUB_TOKEN ``` -
-## What you learned +## What you built -You built a complete automated testing pipeline that: - -- ✅ Clones Git repositories into sandboxes -- ✅ Detects project types automatically -- ✅ Installs dependencies for multiple languages -- ✅ Runs test suites with proper error handling -- ✅ Streams output in real-time -- ✅ Generates and returns test reports -- ✅ Manages sandbox lifecycle efficiently +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 -Enhance your testing pipeline: - -- **Matrix testing** - Run tests across multiple Node.js/Python versions -- **Parallel execution** - Run tests in multiple sandboxes simultaneously -- **Result storage** - Save test results to [D1](/d1/) or [R2](/r2/) -- **Notifications** - Send test results to Slack/Discord -- **GitHub integration** - Post test results as PR status checks -- **Coverage tracking** - Parse and track test coverage over time - -## Related resources - -- [Git workflows guide](/sandbox/guides/git-workflows/) - Advanced Git operations -- [Streaming output guide](/sandbox/guides/streaming-output/) - Real-time output patterns -- [Background processes guide](/sandbox/guides/background-processes/) - Long-running jobs -- [Sessions API](/sandbox/api/sessions/) - Persistent sandbox state -- [Security model](/sandbox/concepts/security/) - Understanding isolation +- [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 index e4448cebc004871..87df1c451defdc4 100644 --- a/src/content/docs/sandbox/tutorials/code-review-bot.mdx +++ b/src/content/docs/sandbox/tutorials/code-review-bot.mdx @@ -5,679 +5,238 @@ sidebar: order: 3 --- -import { Render, PackageManagers, TabItem, Tabs } from "~/components"; +import { Render, PackageManagers } from "~/components"; -Build an automated code review system that responds to GitHub pull requests, clones the repository, analyzes code changes with Claude, and posts detailed feedback as PR comments. - -## What you'll build - -A production-ready GitHub bot that: - -- Receives GitHub webhook events for new pull requests -- Clones repositories securely into sandboxes -- Analyzes code changes with Claude's code review capabilities -- Posts structured feedback as PR comments -- Handles multiple programming languages -- Manages sandbox lifecycle automatically +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 -**What you'll learn**: -- Handle GitHub webhook events in Workers -- Clone Git repositories with authentication -- Analyze file diffs and generate reviews -- Post comments to GitHub Pull Requests -- Manage long-running sandbox operations -- Production security patterns - ## Prerequisites You'll also need: -- A [GitHub account](https://github.com/) and personal access token -- An [Anthropic API key](https://console.anthropic.com/settings/keys) (Claude) -- A GitHub repository for testing (can be private) -- Basic understanding of Git workflows +- 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 Worker project +## 1. Create your project -Create a new Worker project: + - - - +```sh +cd code-review-bot +``` ## 2. Install dependencies -Install the required packages: - - - -The packages: -- `@cloudflare/sandbox` - Sandbox SDK for isolated code execution -- `@anthropic-ai/sdk` - Claude API client -- `@octokit/rest` - GitHub API client - -## 3. Configure your Worker - -Update `wrangler.jsonc` to include bindings and secrets: - -```jsonc title="wrangler.jsonc" -{ - "name": "code-review-bot", - "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" - } - ] -} -``` + -Set your secrets (never commit these to Git): +## 3. Build the webhook handler -```bash -# GitHub personal access token (needs repo permissions) -wrangler secret put GITHUB_TOKEN +Replace `src/index.ts`: -# Anthropic API key -wrangler secret put ANTHROPIC_API_KEY - -# Webhook secret (generate a random string) -wrangler secret put WEBHOOK_SECRET -``` +```typescript +import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox'; +import { Octokit } from '@octokit/rest'; +import Anthropic from '@anthropic-ai/sdk'; -Update your TypeScript types: +export { Sandbox } from '@cloudflare/sandbox'; -```typescript title="src/index.ts" interface Env { - Sandbox: DurableObjectNamespace; - GITHUB_TOKEN: string; - ANTHROPIC_API_KEY: string; - WEBHOOK_SECRET: string; + Sandbox: DurableObjectNamespace; + GITHUB_TOKEN: string; + ANTHROPIC_API_KEY: string; + WEBHOOK_SECRET: string; } -``` - -## 4. Create the webhook handler - -Build the main handler that processes GitHub webhook events: - -```typescript title="src/index.ts" -import { getSandbox } from '@cloudflare/sandbox'; -import { Octokit } from '@octokit/rest'; -import Anthropic from '@anthropic-ai/sdk'; export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - // Health check endpoint - if (url.pathname === '/health') { - return Response.json({ status: 'ok', service: 'code-review-bot' }); - } - - // Webhook endpoint - if (url.pathname === '/webhook' && request.method === 'POST') { - return handleWebhook(request, env); - } - - return Response.json( - { error: 'Not found. Use POST /webhook for GitHub events.' }, - { status: 404 } - ); - } -}; + async fetch(request: Request, env: Env): Promise { + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; -async function handleWebhook(request: Request, env: Env): Promise { - try { - // Verify webhook signature - const signature = request.headers.get('x-hub-signature-256'); - const body = await request.text(); - - if (!signature || !(await verifySignature(body, signature, env.WEBHOOK_SECRET))) { - return Response.json({ error: 'Invalid signature' }, { status: 401 }); - } - - // Parse the event - const event = request.headers.get('x-github-event'); - const payload = JSON.parse(body); - - console.log(`Received ${event} event from GitHub`); - - // Only handle pull request events - if (event !== 'pull_request') { - return Response.json({ message: 'Event ignored (not a PR)' }); - } - - // Only handle opened or synchronize (new commits) actions - if (payload.action !== 'opened' && payload.action !== 'synchronize') { - return Response.json({ message: `PR action '${payload.action}' ignored` }); - } - - // Start the review process (don't wait for completion) - // In production, you'd use a Queue or Durable Object for this - handlePullRequestReview(payload, env).catch(error => { - console.error('Error in review process:', error); - }); - - return Response.json({ - message: 'Review started', - pr: payload.pull_request.number - }); - - } catch (error) { - console.error('Webhook error:', error); - return Response.json( - { error: 'Webhook processing failed' }, - { status: 500 } - ); - } -} + const url = new URL(request.url); -// Verify GitHub webhook signature -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 expectedSignature = 'sha256=' + Array.from(new Uint8Array(signatureBytes)) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); - - return signature === expectedSignature; -} -``` + if (url.pathname === '/webhook' && request.method === 'POST') { + const signature = request.headers.get('x-hub-signature-256'); + const body = await request.text(); -## 5. Implement the review process - -Add the main review logic that clones, analyzes, and comments: - -```typescript title="src/index.ts" -async function handlePullRequestReview(payload: any, env: Env): Promise { - const pr = payload.pull_request; - const repo = payload.repository; - const owner = repo.owner.login; - const repoName = repo.name; - const prNumber = pr.number; - - console.log(`Reviewing PR #${prNumber} in ${owner}/${repoName}`); - - // Create GitHub client - const octokit = new Octokit({ auth: env.GITHUB_TOKEN }); - - // Post initial comment - await octokit.issues.createComment({ - owner, - repo: repoName, - issue_number: prNumber, - body: '🤖 Code review in progress... This may take 1-2 minutes.' - }); - - // Create a unique sandbox for this review - const sandboxId = `review-${owner}-${repoName}-${prNumber}`; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - try { - // Step 1: Clone the repository - console.log('Cloning repository...'); - const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${owner}/${repoName}.git`; - const cloneResult = await sandbox.exec( - `git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`, - { timeout: 60000 } - ); - - if (!cloneResult.success) { - throw new Error(`Failed to clone: ${cloneResult.stderr}`); - } - - // Step 2: Get the diff for this PR - console.log('Fetching PR diff...'); - const comparison = await octokit.repos.compareCommits({ - owner, - repo: repoName, - base: pr.base.sha, - head: pr.head.sha - }); - - const changedFiles = comparison.data.files || []; - console.log(`Found ${changedFiles.length} changed files`); - - if (changedFiles.length === 0) { - await octokit.issues.createComment({ - owner, - repo: repoName, - issue_number: prNumber, - body: '✅ No files to review.' - }); - return; - } - - // Step 3: Read the changed files from the sandbox - const filesContent: Array<{ path: string; content: string; patch: string }> = []; - - for (const file of changedFiles.slice(0, 10)) { // Limit to 10 files - if (file.status === 'removed') continue; - - try { - const content = await sandbox.readFile(`/workspace/repo/${file.filename}`); - const contentStr = new TextDecoder().decode(content); - filesContent.push({ - path: file.filename, - content: contentStr, - patch: file.patch || '' - }); - } catch (error) { - console.log(`Could not read ${file.filename}:`, error); - } - } - - // Step 4: Generate review with Claude - console.log('Generating review with Claude...'); - const review = await generateCodeReview( - env.ANTHROPIC_API_KEY, - pr.title, - pr.body || '', - filesContent - ); - - // Step 5: Post the review as a comment - await octokit.issues.createComment({ - owner, - repo: repoName, - issue_number: prNumber, - body: formatReviewComment(review, filesContent.length, changedFiles.length) - }); - - console.log(`Review posted for PR #${prNumber}`); - - } catch (error) { - console.error('Review error:', error); - - // Post error comment - await octokit.issues.createComment({ - owner, - repo: repoName, - issue_number: prNumber, - body: `❌ Code review failed: ${error instanceof Error ? error.message : 'Unknown error'}` - }); - - } finally { - // Always clean up the sandbox - await sandbox.destroy(); - } -} -``` + // Verify webhook signature + if (!signature || !(await verifySignature(body, signature, env.WEBHOOK_SECRET))) { + return Response.json({ error: 'Invalid signature' }, { status: 401 }); + } -## 6. Implement Claude code review - -Add the function that uses Claude to review code: - -```typescript title="src/index.ts" -interface CodeReview { - summary: string; - issues: Array<{ - file: string; - line?: number; - severity: 'critical' | 'warning' | 'suggestion'; - message: string; - }>; - positives: string[]; - suggestions: string[]; -} + const event = request.headers.get('x-github-event'); + const payload = JSON.parse(body); -async function generateCodeReview( - apiKey: string, - prTitle: string, - prDescription: string, - files: Array<{ path: string; content: string; patch: string }> -): Promise { - const client = new Anthropic({ apiKey }); + // Only handle opened PRs + if (event === 'pull_request' && payload.action === 'opened') { + reviewPullRequest(payload, env).catch(console.error); + return Response.json({ message: 'Review started' }); + } - const filesContext = files.map(f => ` -File: ${f.path} -Diff: -${f.patch} + return Response.json({ message: 'Event ignored' }); + } -Full content: -\`\`\` -${f.content.slice(0, 5000)} ${f.content.length > 5000 ? '... (truncated)' : ''} -\`\`\` -`).join('\n---\n'); + return new Response('Code Review Bot\n\nConfigure GitHub webhook to POST /webhook'); + }, +}; - const prompt = `You are an expert code reviewer. Review this pull request: +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; +} -Title: ${prTitle} -Description: ${prDescription} +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: -${filesContext} - -Provide a thorough code review focusing on: -1. Potential bugs or logic errors -2. Security vulnerabilities -3. Performance issues -4. Code style and best practices -5. Missing error handling -6. Positive aspects worth highlighting - -Format your response as structured feedback with: -- Overall summary -- Specific issues (file, line, severity, message) -- Positive observations -- General suggestions for improvement -`; - - const response = await client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 4096, - messages: [ - { - role: 'user', - content: prompt - } - ], - tools: [ - { - name: 'provide_code_review', - description: 'Provide structured code review feedback', - input_schema: { - type: 'object', - properties: { - summary: { - type: 'string', - description: 'Overall summary of the changes and review' - }, - issues: { - type: 'array', - description: 'Specific issues found in the code', - items: { - type: 'object', - properties: { - file: { type: 'string' }, - line: { type: 'number' }, - severity: { type: 'string', enum: ['critical', 'warning', 'suggestion'] }, - message: { type: 'string' } - }, - required: ['file', 'severity', 'message'] - } - }, - positives: { - type: 'array', - description: 'Positive aspects of the code', - items: { type: 'string' } - }, - suggestions: { - type: 'array', - description: 'General suggestions for improvement', - items: { type: 'string' } - } - }, - required: ['summary', 'issues', 'positives', 'suggestions'] - } - } - ] - }); - - // Extract review from tool use - for (const block of response.content) { - if (block.type === 'tool_use' && block.name === 'provide_code_review') { - return block.input as CodeReview; - } - } - - throw new Error('Claude did not provide structured review'); -} - -function formatReviewComment( - review: CodeReview, - reviewedFiles: number, - totalFiles: number -): string { - let comment = '## 🤖 AI Code Review\n\n'; - - comment += `**Files reviewed:** ${reviewedFiles}/${totalFiles}\n\n`; - - // Summary - comment += `### Summary\n${review.summary}\n\n`; - - // Issues - if (review.issues.length > 0) { - comment += '### Issues Found\n\n'; - - const criticalIssues = review.issues.filter(i => i.severity === 'critical'); - const warnings = review.issues.filter(i => i.severity === 'warning'); - const suggestions = review.issues.filter(i => i.severity === 'suggestion'); - - if (criticalIssues.length > 0) { - comment += '#### 🔴 Critical\n'; - for (const issue of criticalIssues) { - comment += `- **${issue.file}${issue.line ? `:${issue.line}` : ''}** - ${issue.message}\n`; - } - comment += '\n'; - } - - if (warnings.length > 0) { - comment += '#### ⚠️ Warnings\n'; - for (const issue of warnings) { - comment += `- **${issue.file}${issue.line ? `:${issue.line}` : ''}** - ${issue.message}\n`; - } - comment += '\n'; - } - - if (suggestions.length > 0) { - comment += '#### 💡 Suggestions\n'; - for (const issue of suggestions) { - comment += `- **${issue.file}${issue.line ? `:${issue.line}` : ''}** - ${issue.message}\n`; - } - comment += '\n'; - } - } else { - comment += '### ✅ No issues found\n\n'; - } - - // Positives - if (review.positives.length > 0) { - comment += '### 👍 Positive Aspects\n'; - for (const positive of review.positives) { - comment += `- ${positive}\n`; - } - comment += '\n'; - } - - // General suggestions - if (review.suggestions.length > 0) { - comment += '### 💭 General Suggestions\n'; - for (const suggestion of review.suggestions) { - comment += `- ${suggestion}\n`; - } - comment += '\n'; - } - - comment += '\n---\n'; - comment += '*This review was generated automatically using Claude and Cloudflare Sandbox SDK.*'; - - return comment; +${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(); + } } ``` -## 7. Set up GitHub webhook - -Configure your repository to send webhook events to your Worker: - -1. Go to your GitHub repository settings -2. Navigate to **Settings** > **Webhooks** > **Add webhook** -3. Configure: - - **Payload URL**: `https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook` - - **Content type**: `application/json` - - **Secret**: The same value you set for `WEBHOOK_SECRET` - - **Events**: Select "Let me select individual events" → check **Pull requests** - - Click **Add webhook** +## 4. Set your secrets -## 8. Test locally +```sh +# GitHub token (needs repo permissions) +npx wrangler secret put GITHUB_TOKEN -Start your development server: - -```bash -wrangler dev --remote -``` +# Anthropic API key +npx wrangler secret put ANTHROPIC_API_KEY -:::note -We use `--remote` because webhooks need a public URL. Alternatively, use a tunnel service like [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](/cloudflare-one/connections/connect-networks/). -::: - -Test the webhook endpoint: - -```bash -curl -X POST http://localhost:8787/webhook \ - -H "Content-Type: application/json" \ - -H "x-github-event: pull_request" \ - -H "x-hub-signature-256: sha256=$(echo -n '{}' | openssl dgst -sha256 -hmac 'your-webhook-secret' | cut -d' ' -f2)" \ - -d '{ - "action": "opened", - "pull_request": { - "number": 1, - "title": "Test PR", - "body": "Test description", - "head": { "ref": "feature-branch", "sha": "abc123" }, - "base": { "sha": "def456" } - }, - "repository": { - "name": "test-repo", - "owner": { "login": "your-username" } - } - }' +# Webhook secret (generate a random string) +npx wrangler secret put WEBHOOK_SECRET ``` -## 9. Deploy and test with a real PR +## 5. Deploy -Deploy your bot: - -```bash -wrangler deploy +```sh +npx wrangler deploy ``` -Create a test pull request in your repository: - -1. Create a new branch: - ```bash - git checkout -b test-code-review - ``` - -2. Make some changes and commit: - ```bash - echo "console.log('test');" > test.js - git add test.js - git commit -m "Add test file" - git push origin test-code-review - ``` - -3. Open a pull request on GitHub +## 6. Configure GitHub webhook -4. Watch for the bot's comment within 1-2 minutes! +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** -## Complete code +## 7. Test with a pull request -
-View the complete Worker code - -```typescript title="src/index.ts" -import { getSandbox } from '@cloudflare/sandbox'; -import { Octokit } from '@octokit/rest'; -import Anthropic from '@anthropic-ai/sdk'; +Create a test PR: -interface Env { - Sandbox: DurableObjectNamespace; - GITHUB_TOKEN: string; - ANTHROPIC_API_KEY: string; - WEBHOOK_SECRET: string; -} - -interface CodeReview { - summary: string; - issues: Array<{ - file: string; - line?: number; - severity: 'critical' | 'warning' | 'suggestion'; - message: string; - }>; - positives: string[]; - suggestions: string[]; -} - -export default { - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - - if (url.pathname === '/health') { - return Response.json({ status: 'ok' }); - } - - if (url.pathname === '/webhook' && request.method === 'POST') { - return handleWebhook(request, env); - } - - return Response.json({ error: 'Not found' }, { status: 404 }); - } -}; - -// ... (include all functions from steps 4-6) +```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 ``` -
-## What you learned +Open the PR on GitHub and watch for the bot's review comment! -You built a complete automated code review system that: +## What you built -- ✅ Receives GitHub webhook events securely -- ✅ Clones repositories into isolated sandboxes -- ✅ Analyzes code changes with Claude -- ✅ Posts structured feedback to pull requests -- ✅ Handles errors and cleanup automatically -- ✅ Works with any programming language +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 -Enhance your code review bot: - -- **Add inline comments** - Use GitHub's review API to comment on specific lines -- **Multiple reviewers** - Get reviews from different AI models and combine them -- **Custom rules** - Add project-specific checks and linting -- **Review history** - Store reviews in [D1](/d1/) for analysis -- **Queue system** - Use [Queues](/queues/) for reliable processing -- **Metrics tracking** - Monitor review quality and performance - -## Related resources - -- [Git workflows guide](/sandbox/guides/git-workflows/) - Advanced Git operations -- [Background processes guide](/sandbox/guides/background-processes/) - Long-running operations -- [Security model](/sandbox/concepts/security/) - Understanding sandbox isolation -- [Sessions API](/sandbox/api/sessions/) - Managing sandbox lifecycle -- [GitHub Apps documentation](https://docs.github.com/en/apps) - Building GitHub integrations +- [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 index 8ac696585a39fa7..e2359797b80f5b2 100644 --- a/src/content/docs/sandbox/tutorials/index.mdx +++ b/src/content/docs/sandbox/tutorials/index.mdx @@ -7,16 +7,16 @@ sidebar: import { LinkTitleCard, CardGrid } from "~/components"; -Learn how to build complete applications with Sandbox SDK through step-by-step tutorials. Each tutorial is production-ready and designed to be completed in 20-30 minutes. +Learn how to build applications with Sandbox SDK through step-by-step tutorials. Each tutorial takes 20-30 minutes. - Build a production-ready AI code execution system using Claude and Sandbox SDK. Generate Python code from natural language and execute it securely. + Use Claude to generate Python code from natural language and execute it securely in sandboxes. - Upload CSV datasets, generate Python analysis code with LLMs, execute in sandboxes, and save visualizations. Complete data analysis workflow. + Upload CSV files, generate analysis code with Claude, and return visualizations. - Create an automated code review system that clones repositories, analyzes code with LLMs, and posts feedback to GitHub pull requests. + Clone repositories, analyze code with Claude, and post review comments to GitHub PRs. - Build a CI/CD pipeline that clones repositories, installs dependencies, runs tests, and generates detailed test reports. + Clone repositories, install dependencies, run tests, and report results. ## What you'll learn -These tutorials cover complete, real-world applications: +These tutorials cover real-world applications: -- **AI Code Execution** - Integrate LLMs with secure code execution -- **Data Analysis** - Upload datasets, generate analysis code, create visualizations -- **Code Review Automation** - Clone repositories, analyze code, post feedback -- **CI/CD Pipelines** - Automated testing and reporting workflows -- **File Operations** - Work with files and directories in sandboxes -- **Error Handling** - Production-ready validation and error management -- **Deployment** - Deploy to Cloudflare's global network +- **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 From 5b3fea89486fb342aef6e59a7e9544688758e218 Mon Sep 17 00:00:00 2001 From: Naresh Date: Mon, 13 Oct 2025 18:43:24 +0100 Subject: [PATCH 21/22] Fix frontmatter --- .../docs/sandbox/tutorials/ai-code-executor.mdx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/content/docs/sandbox/tutorials/ai-code-executor.mdx b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx index 911951c163217f1..33351b1d8db3531 100644 --- a/src/content/docs/sandbox/tutorials/ai-code-executor.mdx +++ b/src/content/docs/sandbox/tutorials/ai-code-executor.mdx @@ -1,17 +1,8 @@ --- -pcx_content_type: tutorial title: Build an AI code executor -difficulty: Beginner -products: - - Workers - - Sandbox -tags: - - AI - - Python - - Security +pcx_content_type: tutorial sidebar: order: 1 -description: Build an AI code execution system that safely runs Python code generated by Claude. --- import { Render, PackageManagers } from "~/components"; From 822007b3eb1e3ba793ade4350d82b0185ad9c9b6 Mon Sep 17 00:00:00 2001 From: Naresh Date: Wed, 15 Oct 2025 13:55:45 +0100 Subject: [PATCH 22/22] Fix links --- src/content/docs/sandbox/api/interpreter.mdx | 2 +- src/content/docs/sandbox/configuration/wrangler.mdx | 2 +- src/content/docs/sandbox/guides/code-execution.mdx | 3 +-- src/content/docs/sandbox/guides/expose-services.mdx | 1 - src/content/docs/sandbox/guides/index.mdx | 1 - src/content/docs/sandbox/tutorials/index.mdx | 2 -- 6 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/content/docs/sandbox/api/interpreter.mdx b/src/content/docs/sandbox/api/interpreter.mdx index c10a305e8c33b38..2e61518d314d3f7 100644 --- a/src/content/docs/sandbox/api/interpreter.mdx +++ b/src/content/docs/sandbox/api/interpreter.mdx @@ -181,6 +181,6 @@ if (result.results[0]?.html) { ## Related resources -- [Build an AI Code Executor](/sandbox/tutorials/build-an-ai-code-executor/) - Complete tutorial +- [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/configuration/wrangler.mdx b/src/content/docs/sandbox/configuration/wrangler.mdx index 83810f0e7390ab0..91b409f6279181d 100644 --- a/src/content/docs/sandbox/configuration/wrangler.mdx +++ b/src/content/docs/sandbox/configuration/wrangler.mdx @@ -243,7 +243,7 @@ export default { ## Related resources - [Wrangler documentation](/workers/wrangler/) - Complete Wrangler reference -- [Durable Objects configuration](/durable-objects/configuration/) - DO-specific settings +- [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/guides/code-execution.mdx b/src/content/docs/sandbox/guides/code-execution.mdx index b7bc8cfb289421b..85945f6c037d6e8 100644 --- a/src/content/docs/sandbox/guides/code-execution.mdx +++ b/src/content/docs/sandbox/guides/code-execution.mdx @@ -258,6 +258,5 @@ console.log('All contexts deleted'); ## Related resources - [Code Interpreter API reference](/sandbox/api/interpreter/) - Complete API documentation -- [AI code executor tutorial](/sandbox/tutorials/build-an-ai-code-executor/) - Build complete AI executor +- [AI code executor tutorial](/sandbox/tutorials/ai-code-executor/) - Build complete AI executor - [Execute commands guide](/sandbox/guides/execute-commands/) - Lower-level command execution -- [Best practices](/sandbox/best-practices/) - Production patterns diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx index 2616357a1c21460..713ba3ff6c3d2e6 100644 --- a/src/content/docs/sandbox/guides/expose-services.mdx +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -269,4 +269,3 @@ Preview URLs follow the pattern `https://{sandbox-id}-{port}.sandbox.workers.dev - [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 -- [Dev environment tutorial](/sandbox/tutorials/dev-environment/) - Complete example diff --git a/src/content/docs/sandbox/guides/index.mdx b/src/content/docs/sandbox/guides/index.mdx index 66f01bd2fb85ac8..81c700f7ebb6d12 100644 --- a/src/content/docs/sandbox/guides/index.mdx +++ b/src/content/docs/sandbox/guides/index.mdx @@ -21,4 +21,3 @@ These guides show you how to solve specific problems and implement features with - [Tutorials](/sandbox/tutorials/) - Step-by-step learning paths - [API reference](/sandbox/api/) - Complete method documentation -- [Best practices](/sandbox/best-practices/) - Performance, security, and production patterns diff --git a/src/content/docs/sandbox/tutorials/index.mdx b/src/content/docs/sandbox/tutorials/index.mdx index e2359797b80f5b2..7975ec6472c76c3 100644 --- a/src/content/docs/sandbox/tutorials/index.mdx +++ b/src/content/docs/sandbox/tutorials/index.mdx @@ -69,5 +69,3 @@ All tutorials assume you have: - [How-to guides](/sandbox/guides/) - Solve specific problems - [API reference](/sandbox/api/) - Complete SDK reference -- [Examples](/sandbox/examples/) - Code snippets and patterns -- [Best practices](/sandbox/best-practices/) - Production deployment guidance