# Cloudflare

## 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/).

## 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.5.x/install.sh | sh
RUN sandbox-agent install-agent claude && sandbox-agent install-agent codex

EXPOSE 8000
```

## TypeScript example (with provider)

For standalone scripts, use the `cloudflare` provider:

```bash
npm install sandbox-agent@0.5.x @cloudflare/sandbox
```

```typescript
import { SandboxAgent } from "sandbox-agent";
import { cloudflare } from "sandbox-agent/cloudflare";

const sdk = await SandboxAgent.start({
  sandbox: cloudflare(),
});

try {
  const session = await sdk.createSession({ agent: "codex" });
  const response = await session.prompt([
    { type: "text", text: "Summarize this repository" },
  ]);
  console.log(response.stopReason);
} finally {
  await sdk.destroySandbox();
}
```

The `cloudflare` provider uses `containerFetch` under the hood, automatically stripping `AbortSignal` to avoid dropped streaming updates.

## TypeScript example (Durable Objects)

For Workers with Durable Objects, use `SandboxAgent.connect(...)` with a custom `fetch` backed by `sandbox.containerFetch(...)`:

```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;
```

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:
- the outbound prompt request
- the final `{ stopReason: "end_turn" }` response

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
```
