# Security

As covered in [Orchestration Architecture](/sandbox-agent/docs/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

```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",
        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);
```

Use [onBeforeConnect](https://rivet.dev/docs/authentication/), [connection params](https://rivet.dev/actors/docs/connections/), and [actor keys](https://rivet.dev/actors/docs/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,
  });
});
```
