sandbox-agent/docs/deploy/cloudflare.mdx
2026-03-07 18:54:35 -08:00

158 lines
4.1 KiB
Text

---
title: "Cloudflare"
description: "Deploy Sandbox Agent inside a Cloudflare Sandbox."
---
## Prerequisites
- Cloudflare account with Workers paid plan
- Docker for local `wrangler dev`
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
<Note>
Cloudflare Sandbox SDK is beta. See [Sandbox SDK docs](https://developers.cloudflare.com/sandbox/).
</Note>
## Quick start
```bash
npm create cloudflare@latest -- my-sandbox --template=cloudflare/sandbox-sdk/examples/minimal
cd my-sandbox
```
## Dockerfile
```dockerfile
FROM cloudflare/sandbox:0.7.0
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh
RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex
EXPOSE 8000
```
## TypeScript example
```typescript
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
import { Hono } from "hono";
import { SandboxAgent } from "sandbox-agent";
export { Sandbox } from "@cloudflare/sandbox";
type Bindings = {
Sandbox: DurableObjectNamespace<Sandbox>;
ASSETS: Fetcher;
ANTHROPIC_API_KEY?: string;
OPENAI_API_KEY?: string;
};
const app = new Hono<{ Bindings: Bindings }>();
const PORT = 8000;
async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
try {
const result = await sandbox.exec(`curl -sf http://localhost:${PORT}/v1/health`);
return result.success;
} catch {
return false;
}
}
async function getReadySandbox(name: string, env: Bindings): Promise<Sandbox> {
const sandbox = getSandbox(env.Sandbox, name);
if (!(await isServerRunning(sandbox))) {
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);
await sandbox.startProcess(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`);
}
return sandbox;
}
app.post("/sandbox/:name/prompt", async (c) => {
const sandbox = await getReadySandbox(c.req.param("name"), c.env);
const sdk = await SandboxAgent.connect({
fetch: (input, init) =>
sandbox.containerFetch(
input as Request | string | URL,
{
...(init ?? {}),
// Avoid passing AbortSignal through containerFetch; it can drop streamed session updates.
signal: undefined,
},
PORT,
),
});
const session = await sdk.createSession({ agent: "codex" });
const response = await session.prompt([{ type: "text", text: "Summarize this repository" }]);
await sdk.destroySession(session.id);
await sdk.dispose();
return c.json(response);
});
app.all("/sandbox/:name/proxy/*", async (c) => {
const sandbox = await getReadySandbox(c.req.param("name"), c.env);
const wildcard = c.req.param("*");
const path = wildcard ? `/${wildcard}` : "/";
const query = new URL(c.req.raw.url).search;
return sandbox.containerFetch(new Request(`http://localhost${path}${query}`, c.req.raw), PORT);
});
app.all("*", (c) => c.env.ASSETS.fetch(c.req.raw));
export default app;
```
Create the SDK client inside the Worker using custom `fetch` backed by `sandbox.containerFetch(...)`.
This keeps all Sandbox Agent calls inside the Cloudflare sandbox routing path and does not require a `baseUrl`.
## Troubleshooting streaming updates
If you only receive:
- outbound `session/prompt`
- final `{ stopReason: "end_turn" }`
then the streamed update channel dropped. In Cloudflare sandbox paths, this is typically caused by forwarding `AbortSignal` from SDK fetch init into `containerFetch(...)`.
Fix:
```ts
const sdk = await SandboxAgent.connect({
fetch: (input, init) =>
sandbox.containerFetch(
input as Request | string | URL,
{
...(init ?? {}),
// Avoid passing AbortSignal through containerFetch; it can drop streamed session updates.
signal: undefined,
},
PORT,
),
});
```
This keeps prompt completion behavior the same, but restores streamed text/tool updates.
## Local development
```bash
npm run dev
```
Test health:
```bash
curl http://localhost:8787/sandbox/demo/proxy/v1/health
```
## Production deployment
```bash
wrangler deploy
```