mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 02:01:35 +00:00
feat(cloudflare): add React frontend and improve deployment docs
- Add React + Vite frontend for Cloudflare example with sandbox-agent SDK - Update ensureRunning to poll health endpoint instead of fixed wait - Fix SDK fetch binding issue (globalThis.fetch.bind) - Update docs with .dev.vars format warning and container caching tip - Use containerFetch proxy pattern for reliable local dev
This commit is contained in:
parent
44382d2c12
commit
3576b7fcca
12 changed files with 619 additions and 425 deletions
|
|
@ -22,10 +22,69 @@ npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/exa
|
|||
cd my-sandbox
|
||||
```
|
||||
|
||||
## Dockerfile
|
||||
|
||||
Create a `Dockerfile` with sandbox-agent and agents pre-installed:
|
||||
|
||||
```dockerfile
|
||||
FROM cloudflare/sandbox:0.7.0
|
||||
|
||||
# Install sandbox-agent
|
||||
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh
|
||||
|
||||
# Pre-install agents
|
||||
RUN sandbox-agent install-agent claude && \
|
||||
sandbox-agent install-agent codex
|
||||
|
||||
# Required for local development with wrangler dev
|
||||
EXPOSE 8000
|
||||
```
|
||||
|
||||
<Note>
|
||||
The `EXPOSE 8000` directive is required for `wrangler dev` to proxy requests to the container. Port 3000 is reserved for the Cloudflare control plane.
|
||||
</Note>
|
||||
|
||||
## Wrangler Configuration
|
||||
|
||||
Update `wrangler.jsonc` to use your Dockerfile:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-sandbox-agent",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "Sandbox",
|
||||
"image": "./Dockerfile",
|
||||
"instance_type": "lite",
|
||||
"max_instances": 1
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"class_name": "Sandbox",
|
||||
"name": "Sandbox"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"new_sqlite_classes": ["Sandbox"],
|
||||
"tag": "v1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Example
|
||||
|
||||
This example proxies requests to sandbox-agent via `containerFetch`, which works reliably in both local development and production:
|
||||
|
||||
```typescript
|
||||
import { getSandbox, proxyToSandbox, type Sandbox } from "@cloudflare/sandbox";
|
||||
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
|
||||
export { Sandbox } from "@cloudflare/sandbox";
|
||||
|
||||
type Env = {
|
||||
|
|
@ -34,62 +93,60 @@ type Env = {
|
|||
OPENAI_API_KEY?: string;
|
||||
};
|
||||
|
||||
/** Check if sandbox-agent is already running by probing its health endpoint */
|
||||
const PORT = 8000;
|
||||
|
||||
/** Check if sandbox-agent is already running */
|
||||
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
|
||||
try {
|
||||
const result = await sandbox.exec("curl -sf http://localhost:8000/v1/health");
|
||||
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
|
||||
return result.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure sandbox-agent is running in the container */
|
||||
async function ensureRunning(sandbox: Sandbox, env: Env): Promise<void> {
|
||||
if (await isServerRunning(sandbox)) return;
|
||||
|
||||
// Set environment variables for agents
|
||||
const envVars: Record<string, string> = {};
|
||||
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
|
||||
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
|
||||
await sandbox.setEnvVars(envVars);
|
||||
|
||||
// Start sandbox-agent server
|
||||
await sandbox.startProcess(
|
||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`
|
||||
);
|
||||
|
||||
// Poll health endpoint until server is ready
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (await isServerRunning(sandbox)) return;
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Proxy requests to exposed ports first
|
||||
const proxyResponse = await proxyToSandbox(request, env);
|
||||
if (proxyResponse) return proxyResponse;
|
||||
const url = new URL(request.url);
|
||||
|
||||
const { hostname } = new URL(request.url);
|
||||
const sandbox = getSandbox(env.Sandbox, "sandbox-agent");
|
||||
// Proxy requests: /sandbox/:name/v1/...
|
||||
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
|
||||
if (match) {
|
||||
const [, name, path = "/"] = match;
|
||||
const sandbox = getSandbox(env.Sandbox, name);
|
||||
|
||||
// Check if server is already running to avoid port conflicts
|
||||
const alreadyRunning = await isServerRunning(sandbox);
|
||||
await ensureRunning(sandbox, env);
|
||||
|
||||
if (!alreadyRunning) {
|
||||
// Install sandbox-agent
|
||||
await sandbox.exec(
|
||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"
|
||||
// Proxy request to container
|
||||
return sandbox.containerFetch(
|
||||
new Request(`http://localhost${path}${url.search}`, request),
|
||||
PORT
|
||||
);
|
||||
|
||||
// Install agents
|
||||
await sandbox.exec("sandbox-agent install-agent claude");
|
||||
await sandbox.exec("sandbox-agent install-agent codex");
|
||||
|
||||
// Set environment variables for agents
|
||||
const envVars: Record<string, string> = {};
|
||||
if (env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
|
||||
if (env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
|
||||
await sandbox.setEnvVars(envVars);
|
||||
|
||||
// Start sandbox-agent server as background process
|
||||
await sandbox.startProcess(
|
||||
"sandbox-agent server --no-token --host 0.0.0.0 --port 8000"
|
||||
);
|
||||
|
||||
// Wait for server to start
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
}
|
||||
|
||||
// Expose the port with a preview URL
|
||||
const exposed = await sandbox.exposePort(8000, { hostname });
|
||||
|
||||
return Response.json({
|
||||
endpoint: exposed.url,
|
||||
message: alreadyRunning
|
||||
? "sandbox-agent server was already running"
|
||||
: "sandbox-agent server started",
|
||||
});
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
|
@ -99,11 +156,10 @@ export default {
|
|||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
// Get the endpoint from the Worker
|
||||
const { endpoint } = await fetch("http://localhost:8787").then((r) => r.json());
|
||||
|
||||
// Connect to sandbox-agent
|
||||
const client = await SandboxAgent.connect({ baseUrl: endpoint });
|
||||
// Connect via the proxy endpoint
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://localhost:8787/sandbox/my-sandbox",
|
||||
});
|
||||
|
||||
// Wait for server to be ready
|
||||
for (let i = 0; i < 30; i++) {
|
||||
|
|
@ -116,42 +172,49 @@ for (let i = 0; i < 30; i++) {
|
|||
}
|
||||
|
||||
// Create a session and start coding
|
||||
await client.createSession("my-session", {
|
||||
agent: "claude",
|
||||
permissionMode: "default",
|
||||
});
|
||||
await client.createSession("my-session", { agent: "claude" });
|
||||
|
||||
await client.postMessage("my-session", {
|
||||
message: "Summarize this repository",
|
||||
});
|
||||
|
||||
for await (const event of client.streamEvents("my-session")) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
```
|
||||
// Auto-approve permissions
|
||||
if (event.type === "permission.requested") {
|
||||
await client.replyPermission("my-session", event.data.permission_id, {
|
||||
reply: "once",
|
||||
});
|
||||
}
|
||||
|
||||
## Configuration
|
||||
|
||||
Update `wrangler.jsonc` to add environment variables:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"vars": {
|
||||
"ANTHROPIC_API_KEY": "your-api-key"
|
||||
// Handle text output
|
||||
if (event.type === "item.delta" && event.data?.delta) {
|
||||
process.stdout.write(event.data.delta);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use `.dev.vars` for local development:
|
||||
## Environment Variables
|
||||
|
||||
Use `.dev.vars` for local development:
|
||||
|
||||
```bash
|
||||
echo "ANTHROPIC_API_KEY=your-api-key" > .dev.vars
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Use plain `KEY=value` format in `.dev.vars`. Do not use `export KEY=value` - wrangler won't parse the bash syntax.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
The `.dev.vars` file is automatically gitignored and only used during local development with `npm run dev`.
|
||||
</Note>
|
||||
|
||||
For production, set secrets via wrangler:
|
||||
|
||||
```bash
|
||||
wrangler secret put ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
Start the development server:
|
||||
|
|
@ -167,40 +230,22 @@ First run builds the Docker container (2-3 minutes). Subsequent runs are much fa
|
|||
Test with curl:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8787
|
||||
curl http://localhost:8787/sandbox/demo/v1/health
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Containers cache environment variables. If you change `.dev.vars`, either use a new sandbox name or clear existing containers:
|
||||
```bash
|
||||
docker ps -a | grep sandbox | awk '{print $1}' | xargs -r docker rm -f
|
||||
```
|
||||
</Tip>
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For production, preview URLs require a custom domain with wildcard DNS routing.
|
||||
Deploy to Cloudflare:
|
||||
|
||||
See [Cloudflare Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) for setup instructions.
|
||||
|
||||
## Faster Cold Starts
|
||||
|
||||
To speed up cold starts, you can:
|
||||
|
||||
1. Create a custom Dockerfile with sandbox-agent pre-installed
|
||||
2. Pre-install agents in the Docker image
|
||||
|
||||
Example `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM docker.io/cloudflare/sandbox:0.7.0
|
||||
|
||||
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh && \
|
||||
sandbox-agent install-agent claude && \
|
||||
sandbox-agent install-agent codex
|
||||
```bash
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
Then update `wrangler.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"containers": {
|
||||
"sandbox": {
|
||||
"image": "./Dockerfile"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
For production with preview URLs (direct container access), you'll need a custom domain with wildcard DNS routing. See [Cloudflare Production Deployment](https://developers.cloudflare.com/sandbox/guides/production-deployment/) for setup instructions.
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@
|
|||
"deploy/index",
|
||||
"deploy/local",
|
||||
"deploy/e2b",
|
||||
"deploy/daytona",
|
||||
"deploy/vercel",
|
||||
"deploy/cloudflare",
|
||||
"deploy/daytona",
|
||||
"deploy/docker"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue