chore: simplify cloudflare compatibility (#191)

This commit is contained in:
NathanFlurry 2026-02-23 19:31:53 +00:00
parent 03e06e956d
commit 4201bd204b
No known key found for this signature in database
GPG key ID: 6A5F43A4F3241BCA
10 changed files with 418 additions and 249 deletions

View file

@ -36,6 +36,16 @@ Test the endpoint:
curl http://localhost:8787
```
Test prompt routing through the SDK with a custom sandbox fetch handler:
```bash
curl -X POST "http://localhost:8787/sandbox/demo/prompt" \
-H "Content-Type: application/json" \
-d '{"agent":"codex","prompt":"Reply with one short sentence."}'
```
The response includes `events`, an array of all recorded session events for that prompt.
## Deploy
```bash

View file

@ -25,7 +25,7 @@ export function App() {
try {
// Connect via proxy endpoint (need full URL for SDK)
const baseUrl = `${window.location.origin}/sandbox/${encodeURIComponent(sandboxName)}`;
const baseUrl = `${window.location.origin}/sandbox/${encodeURIComponent(sandboxName)}/proxy`;
log(`Connecting to sandbox: ${sandboxName}`);
const client = await SandboxAgent.connect({ baseUrl });

View file

@ -10,6 +10,7 @@
},
"dependencies": {
"@cloudflare/sandbox": "latest",
"hono": "^4.12.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"sandbox-agent": "workspace:*"

View file

@ -1,16 +1,20 @@
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
import { runPromptTest, type PromptTestRequest } from "./prompt-test";
export { Sandbox } from "@cloudflare/sandbox";
type Env = {
Bindings: {
Sandbox: DurableObjectNamespace<Sandbox>;
ASSETS: Fetcher;
ANTHROPIC_API_KEY?: string;
OPENAI_API_KEY?: string;
};
type Bindings = {
Sandbox: DurableObjectNamespace<Sandbox>;
ASSETS: Fetcher;
ANTHROPIC_API_KEY?: string;
OPENAI_API_KEY?: string;
CODEX_API_KEY?: string;
};
type AppEnv = { Bindings: Bindings };
const PORT = 8000;
/** Check if sandbox-agent is already running by probing its health endpoint */
@ -23,54 +27,60 @@ async function isServerRunning(sandbox: Sandbox): Promise<boolean> {
}
}
/** 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
async function getReadySandbox(name: string, env: Bindings): Promise<Sandbox> {
const sandbox = getSandbox(env.Sandbox, name);
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;
if (env.CODEX_API_KEY) envVars.CODEX_API_KEY = env.CODEX_API_KEY;
if (!envVars.CODEX_API_KEY && envVars.OPENAI_API_KEY) envVars.CODEX_API_KEY = envVars.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}`);
if (!(await isServerRunning(sandbox))) {
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));
for (let i = 0; i < 30; i++) {
if (await isServerRunning(sandbox)) break;
await new Promise((r) => setTimeout(r, 200));
}
}
return sandbox;
}
export default {
async fetch(request: Request, env: Env["Bindings"]): Promise<Response> {
const url = new URL(request.url);
async function proxyToSandbox(sandbox: Sandbox, request: Request, path: string): Promise<Response> {
const query = new URL(request.url).search;
return sandbox.containerFetch(new Request(`http://localhost${path}${query}`, request), PORT);
}
// 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 app = new Hono<AppEnv>();
const name = match[1];
const path = match[2] || "/";
const sandbox = getSandbox(env.Sandbox, name);
app.onError((error) => {
return new Response(String(error), { status: 500 });
});
await ensureRunning(sandbox, env);
app.post("/sandbox/:name/prompt", async (c) => {
if (!(c.req.header("content-type") ?? "").includes("application/json")) {
throw new HTTPException(400, { message: "Content-Type must be application/json" });
}
// Proxy request to container
return sandbox.containerFetch(
new Request(`http://localhost${path}${url.search}`, request),
PORT
);
}
let payload: PromptTestRequest;
try {
payload = await c.req.json<PromptTestRequest>();
} catch {
throw new HTTPException(400, { message: "Invalid JSON body" });
}
// Serve frontend assets
return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env["Bindings"]>;
const sandbox = await getReadySandbox(c.req.param("name"), c.env);
return c.json(await runPromptTest(sandbox, payload, PORT));
});
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}` : "/";
return proxyToSandbox(sandbox, c.req.raw, path);
});
app.all("*", (c) => c.env.ASSETS.fetch(c.req.raw));
export default app;

View file

@ -0,0 +1,66 @@
import type { Sandbox } from "@cloudflare/sandbox";
import { SandboxAgent } from "sandbox-agent";
export type PromptTestRequest = {
agent?: string;
prompt?: string;
};
export type PromptTestResponse = {
sessionId: string;
agent: string;
prompt: string;
events: unknown[];
};
export async function runPromptTest(
sandbox: Sandbox,
request: PromptTestRequest,
port: number,
): Promise<PromptTestResponse> {
const client = await SandboxAgent.connect({
fetch: (req, init) =>
sandbox.containerFetch(req, init, port),
});
let sessionId: string | null = null;
try {
const session = await client.createSession({
agent: request.agent ?? "codex",
});
sessionId = session.id;
const promptText =
request.prompt?.trim() || "Reply with a short confirmation.";
await session.prompt([{ type: "text", text: promptText }]);
const events: unknown[] = [];
let cursor: string | undefined;
while (true) {
const page = await client.getEvents({
sessionId: session.id,
cursor,
limit: 200,
});
events.push(...page.items);
if (!page.nextCursor) break;
cursor = page.nextCursor;
}
return {
sessionId: session.id,
agent: session.agent,
prompt: promptText,
events,
};
} finally {
if (sessionId) {
try {
await client.destroySession(sessionId);
} catch {
// Ignore cleanup failures; session teardown is best-effort.
}
}
await client.dispose();
}
}