5.0 KiB
5.0 KiB
Security
Source:
docs/security.mdxCanonical URL: https://sandboxagent.dev/docs/security Description: Backend-first auth and access control patterns.
As covered in Orchestration Architecture, run the Sandbox Agent client on your backend, not in the browser.
This keeps sandbox credentials private and gives you one place for authz, rate limiting, and audit logging.
Auth model
Implement auth however it fits your stack (sessions, JWT, API keys, etc.), but enforce it before any sandbox-bound request.
Minimum checks:
- Authenticate the caller.
- Authorize access to the target workspace/sandbox/session.
- Apply request rate limits and request logging.
Examples
Rivet
import { UserError, actor } from "rivetkit";
import { SandboxAgent } from "sandbox-agent";
type ConnParams = {
accessToken: string;
};
type WorkspaceClaims = {
sub: string;
workspaceId: string;
role: "owner" | "member" | "viewer";
};
async function verifyWorkspaceToken(
token: string,
workspaceId: string,
): Promise<WorkspaceClaims | null> {
// Validate JWT/session token here, then enforce workspace scope.
// Return null when invalid/expired/not a member.
if (!token) return null;
return { sub: "user_123", workspaceId, role: "member" };
}
export const workspace = actor({
state: {
events: [] as Array<{ userId: string; prompt: string; createdAt: number }>,
},
onBeforeConnect: async (c, params: ConnParams) => {
const claims = await verifyWorkspaceToken(params.accessToken, c.key[0]);
if (!claims) {
throw new UserError("Forbidden", { code: "forbidden" });
}
},
createConnState: async (c, params: ConnParams) => {
const claims = await verifyWorkspaceToken(params.accessToken, c.key[0]);
if (!claims) {
throw new UserError("Forbidden", { code: "forbidden" });
}
return {
userId: claims.sub,
role: claims.role,
workspaceId: claims.workspaceId,
};
},
actions: {
submitPrompt: async (c, prompt: string) => {
if (!c.conn) {
throw new UserError("Connection required", { code: "connection_required" });
}
if (c.conn.state.role === "viewer") {
throw new UserError("Insufficient permissions", { code: "forbidden" });
}
// Connect to Sandbox Agent from the actor (server-side only).
// Sandbox credentials never reach the client.
const sdk = await SandboxAgent.connect({
baseUrl: process.env.SANDBOX_URL!,
token: process.env.SANDBOX_TOKEN,
});
const session = await sdk.createSession({
agent: "claude",
cwd: "/workspace",
});
session.onEvent((event) => {
c.broadcast("session.event", {
userId: c.conn!.state.userId,
eventIndex: event.eventIndex,
sender: event.sender,
payload: event.payload,
});
});
const result = await session.prompt([
{ type: "text", text: prompt },
]);
c.state.events.push({
userId: c.conn.state.userId,
prompt,
createdAt: Date.now(),
});
return { stopReason: result.stopReason };
},
},
});
import { createClient } from "rivetkit/client";
import type { registry } from "./actors";
const client = createClient<typeof registry>({
endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!,
});
const handle = client.workspace.getOrCreate(["ws_123"], {
params: { accessToken: userJwt },
});
const conn = handle.connect();
conn.on("session.event", (event) => {
console.log(event.sender, event.payload);
});
const result = await conn.submitPrompt("Plan a refactor for auth middleware.");
console.log(result.stopReason);
Use onBeforeConnect, connection params, and actor keys together so each actor enforces auth per workspace.
Hono
import { Hono } from "hono";
import { bearerAuth } from "hono/bearer-auth";
const app = new Hono();
app.use("/sandbox/*", bearerAuth({ token: process.env.APP_API_TOKEN! }));
app.all("/sandbox/*", async (c) => {
const incoming = new URL(c.req.url);
const upstreamUrl = new URL(process.env.SANDBOX_URL!);
upstreamUrl.pathname = incoming.pathname.replace(/^\/sandbox/, "/v1");
upstreamUrl.search = incoming.search;
const headers = new Headers();
headers.set("authorization", `Bearer ${process.env.SANDBOX_TOKEN ?? ""}`);
const accept = c.req.header("accept");
if (accept) headers.set("accept", accept);
const contentType = c.req.header("content-type");
if (contentType) headers.set("content-type", contentType);
const body =
c.req.method === "POST" || c.req.method === "PUT" || c.req.method === "PATCH"
? await c.req.text()
: undefined;
const upstream = await fetch(upstreamUrl, {
method: c.req.method,
headers,
body,
});
return new Response(upstream.body, {
status: upstream.status,
headers: upstream.headers,
});
});