> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Keys, scopes and principals

How B3OS authenticates requests, what each scope allows, and how sandboxes, routines and streams get their own narrower identities.

Every request runs as a **principal**: an identity inside one organization and one project, with a set of scopes. Which principal depends on how the request arrived.

| Principal | How it authenticates | Typical use |
|---|---|---|
| API key | `Authorization: Bearer b3os_sk_...` | Your servers, agents and CI |
| CLI token | `Authorization: Bearer b3os_pat_...` from `b3os login` | The CLI, acting for one user |
| Dashboard session | The `__Host-b3os_session` cookie plus an `x-b3os-csrf` header on writes | The dashboard at `beta.b3os.org` |
| Sandbox | Nothing: the node knows which sandbox is calling | Code inside a sandbox calling `http://127.0.0.1:7700` |

To see which principal a credential maps to, call `GET /v1/keys/current`:

```bash
curl -s https://beta-api.b3os.org/v1/keys/current -H "Authorization: Bearer $B3OS_API_KEY"
```

## API keys

A key belongs to one project. It has scopes, an optional spend cap in USD and an optional expiry. The secret is shown once, when the key is created; B3OS stores only its SHA-256 hash. Keys look like `b3os_sk_` followed by 32 base32 characters and a CRC-32 checksum, and the prefix is registered for secret scanning so a leaked key can be found.

Create keys in the dashboard or with [`POST /v1/keys`](/api/post-v1-keys). A new key can never have more scopes or a higher spend cap than the key that creates it; asking for more fails with [`forbidden`](/errors#forbidden) and reason `scope_escalation`. Scopes cannot change after creation; create another key instead. [`DELETE /v1/keys/{key_id}`](/api/delete-v1-keys-key-id-) revokes a key at once, and open streams authenticated by it close within a minute.

The first key, created when you redeem your invite, gets these scopes:

`vision:read`, `vision:write`, `vision:run`, `render:read`, `render:write`, `render:run`, `sandboxes:read`, `sandboxes:write`, `sandboxes:run`, `routines:read`, `routines:write`, `routines:run`, `actions:read`, `actions:write`, `actions:run`, `infer:read`, `infer:write`, `infer:run`, `vault:read`, `vault:run`, `platform:read`

It never gets `*`, `vault:write` or `platform:write`. Create a key with those in the dashboard when you need them.

## Scopes

Scopes are `<product>:read`, `<product>:write` and `<product>:run`. They never imply each other: `write` does not include `read`.

- `read` lists and reads objects and their event streams.
- `write` creates, changes and deletes persistent objects.
- `run` does work that costs compute: a scrape, a command, a render, a run, an action call, a model call.

`vault:run` means using a stored connection. A request that names a connection needs `vault:run` on top of the operation's own scope.

| Scope | Allows |
|---|---|
| `vision:read` | List and read browser sessions, profiles, crawls and batch scrapes. |
| `vision:write` | Create and close browser sessions; create, update and delete profiles. |
| `vision:run` | Scrape, map, crawl, batch scrape and extract; drive sessions (act, CDP). |
| `render:read` | List and read screenshots, videos and artifacts. |
| `render:write` | Delete screenshots and videos. |
| `render:run` | Take screenshots; record, restyle and cancel videos. |
| `sandboxes:read` | List and read sandbox, snapshot and template metadata. |
| `sandboxes:write` | Create, delete, pause, resume, snapshot and extend sandboxes; delete snapshots. |
| `sandboxes:run` | Exec, run code, read and write files, open PTYs, read logs and manage processes inside sandboxes. |
| `routines:read` | List and read routines, versions, runs and run events. |
| `routines:write` | Create, publish, roll back and delete routines; validate and import definitions. |
| `routines:run` | Start, signal and cancel routine runs. |
| `actions:read` | Search packs, actions and triggers; read their definitions. |
| `actions:write` | Publish packs to the registry (first-party publishers only in the beta). |
| `actions:run` | Call actions. |
| `infer:read` | List models and voices. |
| `infer:write` | Reserved; no operation requires it yet. |
| `infer:run` | Chat completions, embeddings and speech. |
| `vault:read` | List and read connection metadata (secret values are never returned). |
| `vault:write` | Create, update and delete connections. |
| `vault:run` | Use stored connections in action calls and routine steps. |
| `platform:read` | Read keys, usage, requests, organizations, projects and webhook deliveries. |
| `platform:write` | Create, update and revoke keys; manage projects; read and rotate webhook secrets. |

`<product>:*` grants every scope of one product and `*` grants all of them. Each operation in the [API reference](/api/overview) lists the one scope it needs. A few operations use a pseudo-scope instead:

| Pseudo-scope | Meaning |
|---|---|
| `public` | No credentials (device login, inbound webhooks verified by signature, usercontent routes verified by their node signature). |
| `authenticated` | Any valid principal, whatever its scopes (for example reading its own limits). |
| `session` | A signed-in dashboard session only; API keys and tokens are rejected (`403 forbidden`, reason `session_required`). |
| `node` | A B3OS node's Ed25519 request signature (internal node-to-edge endpoints only; never customers). |

## Organizations and projects

An organization holds projects; a project holds keys and every object you create (sandboxes, routines, profiles, connections). B3OS checks on every request that the object belongs to the caller's organization and project. A key from one organization can never read another's objects, and an object outside your project answers [`404`](/errors#resource_not_found), never `403`, so its existence does not leak.

## Sandboxes get their own principal

Code inside a sandbox calls `http://127.0.0.1:7700` and sends no credential; anything it sends is discarded. Its calls run as a **child principal** created with the sandbox from the key that created it. The child has no B3OS access until you grant it in the `api` field when you create the sandbox:

```json
{
  "template": "agent",
  "api": {
    "scopes": ["infer:run", "vision:run"],
    "spend_cap_usd": 5,
    "connections": ["conn_01m5t6cbgdjs1ketgcqwa2sne8"]
  }
}
```

- `scopes` must be a subset of the creating key's. `*` and `<product>:*` expand to the scopes you hold, except the explicit-only scopes below.
- `spend_cap_usd` stops the sandbox's own API calls at that spend. `null`, the default, means the creating key's cap applies. A cap above the creator's is refused with `403 forbidden`, reason `scope_escalation`, so caps never grow from one sandbox to the next.
- `connections` lists the vault connections the sandbox's action calls may use.

A grant gets these scopes only when it names them exactly, never through `*` or `<product>:*`:

`sandboxes:write`, `vault:read`, `vault:write`, `vault:run`, `platform:read`, `platform:write`

The grant is revoked when the sandbox stops. [Agents in sandboxes](/sandboxes/agents) has the details.

## Routines run as their publisher

A routine version runs as the key that published it, with that key's scopes copied at publish time. If the key is revoked or expires, the routine's triggers pause until a new version is published with a valid key. See [routines](/routines).

## Stream tickets

Browsers cannot set an `Authorization` header on a `WebSocket` or an `EventSource`. For the three streams a browser opens directly (a sandbox terminal, a sandbox's logs and a browser session's CDP socket), get a ticket with your normal credential and open the stream within 30 seconds:

```bash
curl -s -X POST https://beta-api.b3os.org/v1/sandboxes/$SBX/ticket \
  -H "Authorization: Bearer $B3OS_API_KEY"
# {"ticket": "tkt_...", "object": "stream_ticket", "resource": "/v1/sandboxes/sbx_...", "expires_in_s": 30}
```

Pass it as `?ticket=tkt_...` or as the WebSocket subprotocol pair `b3os.v1, b3os.ticket.tkt_...`. A ticket works once, only for streams under its `resource`, and runs the stream with the scopes of whoever requested it. B3OS never accepts a bearer token in a URL.

## How the edge reaches the node

Your key never leaves the edge at `beta-api.b3os.org`. For each request it forwards, the edge signs a single-use node token that lives 60 seconds and names your organization, project, key and scopes. The node checks the signature, the expiry and that the token was not used before, then runs the request as that principal.