mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 14:03:52 +00:00
* chore: recover hamburg workspace state * chore: drop workspace context files * refactor: generalize permissions example * refactor: parse permissions example flags * docs: clarify why fs and terminal stay native * feat: add interactive permission prompt UI to Inspector Add permission request handling to the Inspector UI so users can Allow, Always Allow, or Reject tool calls that require permissions instead of having them auto-cancelled. Wires up SDK onPermissionRequest/respondPermission through App → ChatPanel → ChatMessages with proper toolCallId-to-pendingId mapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prevent permission reply from silently escalating "once" to "always" Remove allow_always from the fallback chain when the user replies "once", aligning with the ACP spec which says "map by option kind first" with no fallback for allow_once. Also fix Inspector to use rawSend, revert hydration guard to accept empty configOptions, and handle respondPermission errors by rejecting the pending promise. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
4.1 KiB
Text
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:
|
|
- 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
|
|
```
|