mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 09:01:17 +00:00
chore: fix bad merge
This commit is contained in:
parent
1dd45908a3
commit
94353f7696
205 changed files with 19244 additions and 14866 deletions
191
docs/security.mdx
Normal file
191
docs/security.mdx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
title: "Security"
|
||||
description: "Backend-first auth and access control patterns."
|
||||
icon: "shield"
|
||||
---
|
||||
|
||||
As covered in [Architecture](/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
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts Actor (server)
|
||||
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",
|
||||
sessionInit: { 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 };
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts Client (browser)
|
||||
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);
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Use [onBeforeConnect](https://rivet.dev/docs/actors/authentication), [connection params](https://rivet.dev/docs/actors/connections), and [actor keys](https://rivet.dev/docs/actors/keys) together so each actor enforces auth per workspace.
|
||||
|
||||
### Hono
|
||||
|
||||
```ts
|
||||
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,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue