Agents in sandboxes
Run an agent inside a sandbox with a scoped grant to B3OS, call the model and tools without a key, inject vault credentials and start nested sandboxes safely.
Code inside a sandbox can call B3OS at http://127.0.0.1:7700 (B3OS_API_URL). The call travels over vsock to the B3OS daemon on the same machine, with no network in between. It needs no key: the node knows which sandbox is calling and runs the call as that sandbox's own principal.
Grant access
A sandbox has no B3OS access until you grant it in api when you create it:
bashcurl -s https://beta-api.b3os.org/v1/sandboxes \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"template": "agent", "api": {"scopes": ["infer:read", "infer:run", "vision:run", "actions:run", "vault:run"], "spend_cap_usd": 5, "connections": ["conn_01m5t6cbgdjs1ketgcqwa2sne8"]}}'
tsconst sandbox = await b3os.sandboxes.createSandbox({ template: "agent", api: { scopes: ["infer:read", "infer:run", "vision:run", "actions:run", "vault:run"], spend_cap_usd: 5, connections: ["conn_01m5t6cbgdjs1ketgcqwa2sne8"], },});
pythonsandbox = b3os.sandboxes.create_sandbox({ "template": "agent", "api": { "scopes": ["infer:read", "infer:run", "vision:run", "actions:run", "vault:run"], "spend_cap_usd": 5, "connections": ["conn_01m5t6cbgdjs1ketgcqwa2sne8"], },})
textStart an agent sandbox that may call the model and scrape pages, and nothing else.
On the remote MCP server, sandbox_create takes the same body as the API, api included, so the agent can ask for this grant; it can never exceed the key's own scopes. The local server's sandbox_create takes only the template, size, timeout and environment, so a sandbox it creates cannot call B3OS. From a terminal: b3os sandbox create --template agent --grant infer:read,infer:run,vision:run.
| Field | Rule |
|---|---|
scopes | A subset of your key's scopes. * and <product>:* expand to the scopes you hold, except sandboxes:write, the vault:* scopes and the platform:* scopes, which a grant gets only when it names them exactly. |
spend_cap_usd | Stops the sandbox's own API calls at this spend. null (the default) means your key's cap applies. A cap above your key's is refused. |
connections | Up to 16 vault connections the sandbox's action calls may use. |
Asking for more than you hold fails with forbidden, reason scope_escalation. The grant ends when the sandbox stops.
Call B3OS from inside
The B3OS SDK and CLI read B3OS_API_URL and use it without a key. Compatible clients only need the base URL; pass any placeholder as the key:
| Client | Base URL inside a sandbox |
|---|---|
| B3OS SDK, CLI | Automatic |
| OpenAI SDK | http://127.0.0.1:7700/v1/infer |
| Firecrawl SDK | http://127.0.0.1:7700/compat/firecrawl |
| Plain HTTP | http://127.0.0.1:7700/v1/... |
pythonimport osfrom openai import OpenAIclient = OpenAI(base_url=os.environ["B3OS_API_URL"] + "/v1/infer", api_key="unused")
The sandbox agent quickstart runs a complete tool-calling loop this way.
Account operations (keys, usage, limits, organizations, projects and the pack registry) are not served on the node. From inside a sandbox they answer 421 edge_only; call https://beta-api.b3os.org with a key for those.
Give the sandbox a credential
Three ways, from safest to least safe:
-
Let the egress proxy inject it. Grant
vault:runand the connection inapi.connections, then call a marketplace action. The proxy adds the credential to requests for the hosts the connection is bound to; the sandbox never sees it. -
Put a connection's secret in an environment variable.
secretstakes a vault connection reference. The value is never returned by the API or logged:json{ "template": "base", "secrets": { "GITHUB_TOKEN": { "connection": "conn_01m5t6cbgdjs1ketgcqwa2sne8" } } }fieldpicks which secret field to use; it defaults to the one the connection's type injects (api_key,tokenoraccess_token), andbasicconnections needusernameorpassword. -
Pass a literal value in
secrets, such as{"NPM_TOKEN": "..."}. It is not returned or logged either, but it lives in your request.
Prefer the first: a credential that never enters the sandbox cannot leak from it.
Sandboxes that create sandboxes
Code in a sandbox can create sandboxes only if its grant names sandboxes:write exactly. The child's grant must be a subset of the parent sandbox's, and a child's spend_cap_usd above the parent's grant cap is refused, so caps never grow down a chain.
Limit where it can reach
egress controls outbound traffic: public (the default) allows any public address, allowlist allows only the hosts in allow (exact names or *. wildcards), and deny blocks everything. Private networks, loopback, the node's own addresses and cloud metadata are always refused. The policy covers traffic bound for the internet; calls to http://127.0.0.1:7700 go to B3OS itself, under the grant.
See what it did
Every call a sandbox makes is in the request log with connection: sandbox and its sandbox_id:
bashcurl -s "https://beta-api.b3os.org/v1/requests?sandbox_id=$SBX" -H "Authorization: Bearer $B3OS_API_KEY"
Usage from those calls is recorded with the sandbox's ID and billed to the key that created it.