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:
bashcurl -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. 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 and reason scope_escalation. Scopes cannot change after creation; create another key instead. 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.
readlists and reads objects and their event streams.writecreates, changes and deletes persistent objects.rundoes 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 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, 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"] }}
scopesmust be a subset of the creating key's.*and<product>:*expand to the scopes you hold, except the explicit-only scopes below.spend_cap_usdstops 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 with403 forbidden, reasonscope_escalation, so caps never grow from one sandbox to the next.connectionslists 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 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.
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:
bashcurl -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.