docs: add Cloudflare Sandbox SDK deployment guide (#57)

* docs: add Cloudflare Sandbox SDK deployment guide

- Add docs/deploy/cloudflare.mdx with full deployment guide
- Add examples/cloudflare/ with working Worker code
- Update docs navigation to include Cloudflare option
- Update deploy index page with Cloudflare card

The example shows how to run sandbox-agent inside a Cloudflare Sandbox
with exposed ports for API access.

Co-authored-by: Shelley <shelley@exe.dev>

* fix: guard server startup to avoid port conflicts

Add health check before starting sandbox-agent to prevent 'address already
in use' errors on subsequent requests. The isServerRunning() function probes
the health endpoint to determine if setup should be skipped.

Co-authored-by: Shelley <shelley@exe.dev>

* fix: default cloudflare/sandbox:0.7.0 (latest does not exist)

* 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

---------

Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Nathan Flurry <git@nathanflurry.com>
Co-authored-by: Nathan Flurry <developer@nathanflurry.com>
This commit is contained in:
Jordan Coeyman 2026-02-03 05:15:34 -05:00 committed by GitHub
parent 64582ef299
commit cc5a9e0d73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1459 additions and 220 deletions

View file

@ -0,0 +1,76 @@
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
type Env = {
Bindings: {
Sandbox: DurableObjectNamespace<Sandbox>;
ASSETS: Fetcher;
ANTHROPIC_API_KEY?: string;
OPENAI_API_KEY?: string;
};
};
const PORT = 8000;
/** Check if sandbox-agent is already running by probing its health endpoint */
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;
}
}
/** Ensure sandbox-agent is running in the container */
async function ensureRunning(sandbox: Sandbox, env: Env["Bindings"]): 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 as background process
await sandbox.startProcess(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`);
// Poll health endpoint until server is ready (max ~6 seconds)
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["Bindings"]): Promise<Response> {
const url = new URL(request.url);
// Proxy requests to sandbox-agent: /sandbox/:name/v1/...
const match = url.pathname.match(/^\/sandbox\/([^/]+)(\/.*)?$/);
if (match) {
if (!env.ANTHROPIC_API_KEY && !env.OPENAI_API_KEY) {
return Response.json(
{ error: "ANTHROPIC_API_KEY or OPENAI_API_KEY must be set" },
{ status: 500 }
);
}
const name = match[1];
const path = match[2] || "/";
const sandbox = getSandbox(env.Sandbox, name);
await ensureRunning(sandbox, env);
// Proxy request to container
return sandbox.containerFetch(
new Request(`http://localhost${path}${url.search}`, request),
PORT
);
}
// Serve frontend assets
return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env["Bindings"]>;