ai-sdk-sandbox-cloudflare-bridge
v0.1.2
Published
HarnessV1SandboxProvider implementation backed by a Cloudflare Sandbox container, driven from Node via the Sandbox Bridge HTTP API.
Downloads
415
Maintainers
Readme
AI SDK Cloudflare Sandbox
This package is experimental.
HarnessV1SandboxProvider implementation for Cloudflare Sandbox, driven from Node through the Sandbox Bridge HTTP API. A Cloudflare Sandbox is a Durable Object reachable only from a Worker. Deploy the bridge once and the provider drives it over HTTP with a bearer token.
Setup
npm i ai-sdk-sandbox-cloudflare-bridge @ai-sdk/harnessDeploy the Sandbox Bridge
The bridge exposes the Sandbox SDK over HTTP with a bearer token. Deploy it once to your own Cloudflare account, following the bridge docs:
Scaffold the bridge Worker (
bridge/workerin cloudflare/sandbox-sdk). Itswrangler.jsoncsetsSANDBOX_TRANSPORT: "rpc", which port tunnels require.For
claude-code/codex, give the containerpnpm(the harness installs its bridge with it): in the bridgeDockerfile,FROM docker.io/cloudflare/sandbox:0.12.4thenRUN npm install -g pnpm@9.Set the token and deploy:
wrangler secret put SANDBOX_API_KEY # any strong random string wrangler deploy
You now have a bridge URL (e.g. https://sandbox-bridge.you.workers.dev) and its SANDBOX_API_KEY.
Usage
Construct the provider with the bridge URL and key, then use it like any other sandbox provider.
import { createCloudflareSandbox } from 'ai-sdk-sandbox-cloudflare-bridge';
const cloudflareSandbox = createCloudflareSandbox({
bridgeUrl: process.env.SANDBOX_BRIDGE_URL!,
apiKey: process.env.SANDBOX_API_KEY!,
});
const networkSandboxSession = await cloudflareSandbox.createSession();
const sandboxSession = networkSandboxSession.restricted();
await sandboxSession.writeTextFile({ path: 'hello.txt', content: 'hi' });
const { stdout } = await sandboxSession.run({ command: 'cat hello.txt' });
console.log(stdout); // "hi"
await networkSandboxSession.stop();With a harness agent
import { HarnessAgent } from '@ai-sdk/harness/agent';
import { createClaudeCode } from '@ai-sdk/harness-claude-code';
import { createCloudflareSandbox } from 'ai-sdk-sandbox-cloudflare-bridge';
const agent = new HarnessAgent({
harness: createClaudeCode({ auth: { anthropic: { apiKey } } }),
// IS_SANDBOX lets claude-code run with bypassed permissions as root in the container.
sandbox: createCloudflareSandbox({ bridgeUrl, apiKey: bridgeKey, env: { IS_SANDBOX: '1' } }),
});networkSandboxSession.restricted() is typed as Experimental_SandboxSession, so it is safe to pass to AI SDK tools that accept experimental_sandbox. The network sandbox session carries the infra surface (ports, getPortUrl, setPorts, setNetworkPolicy, stop) that only the harness should reach for.
Configuration
bridgeUrlthe base URL of your deployed bridge Worker.apiKeythe bridge'sSANDBOX_API_KEYbearer token.workdirworking directory inside the container. Defaults to/workspace. Relative paths resolve against it.sandboxIdattach to an existing sandbox id instead of creating one; its lifecycle is left to the caller.session.idreturns the id a created sandbox got, so you can persist it and resume later.envenvironment injected into every command (for example{ IS_SANDBOX: '1' }).portsports to pre-track; the harness bridge port is leased automatically.
Ports
getPortUrl mints a Cloudflare Quick Tunnel for the port and returns its *.trycloudflare.com URL (a ws request returns the same URL with a ws scheme). No custom domain is required. setNetworkPolicy is a no-op; outbound access is governed by the container configuration.
Notes
The bridge's file API is scoped to /workspace, so file operations split by path. Writes under /workspace use the file API, which avoids an argv length limit on large files. Writes elsewhere (the harness bootstraps under /tmp) go through exec and base64. Reads always use exec, since the file API returns an empty 200 for a missing file and so cannot honor the null-on-missing contract.
Cancellation applies to spawn: aborting the signal tears down the exec request, which rejects wait(). The bridge cannot signal the in-container process, so a running command may linger until destroy().
