> ## 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.

# 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:

**curl**

```bash
    curl -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"]}}'
    ```
  **TypeScript**

```ts
    const 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"],
      },
    });
    ```
  **Python**

```python
    sandbox = 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"],
        },
    })
    ```
  **MCP**

```text
    Start 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`](/errors#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/...` |

```python
import os
from openai import OpenAI

client = OpenAI(base_url=os.environ["B3OS_API_URL"] + "/v1/infer", api_key="unused")
```

The [sandbox agent quickstart](/quickstart/sandbox-agent) 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`](/errors#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:

1. **Let the egress proxy inject it.** Grant `vault:run` and the connection in `api.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.
2. **Put a connection's secret in an environment variable.** `secrets` takes a vault connection reference. The value is never returned by the API or logged:

   ```json
   { "template": "base", "secrets": { "GITHUB_TOKEN": { "connection": "conn_01m5t6cbgdjs1ketgcqwa2sne8" } } }
   ```

   `field` picks which secret field to use; it defaults to the one the connection's type injects (`api_key`, `token` or `access_token`), and `basic` connections need `username` or `password`.
3. **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`:

```bash
curl -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.