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

# Run an agent in a sandbox next to the model

Start a sandbox on the node, give it a scoped grant, and run a small tool-calling agent whose calls to the model and to B3OS never leave the machine.

This puts the whole loop on one machine: the agent runs in a microVM on the B3OS node, calls the model the node serves, and calls a B3OS tool, all through `http://127.0.0.1:7700` inside the sandbox. You need a key in `B3OS_API_KEY` ([get one](/quickstart#get-a-key)), `curl` and `jq`.

**Create a sandbox with a grant**

The `agent` template has Python 3.12 and Node 24. The `api` grant decides what code in the sandbox may call; here it may list and call models and scrape pages:

    ```bash
    SBX=$(curl -s https://beta-api.b3os.org/v1/sandboxes \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"template": "agent", "timeout_s": 600,
           "api": {"scopes": ["infer:read", "infer:run", "vision:run"]}}' | jq -r .id)
    echo $SBX
    ```

    The sandbox runs until `timeout_s` passes or you delete it.
  **Write the agent**

Save this as `agent.py` on your machine. It uses only the Python standard library. Inside the sandbox `B3OS_API_URL` is `http://127.0.0.1:7700` and no key is needed: the node knows which sandbox is calling.

    ```python agent.py
    import json, os, urllib.request

    API = os.environ["B3OS_API_URL"]

    def call(method, path, body=None):
        data = None if body is None else json.dumps(body).encode()
        req = urllib.request.Request(API + path, data=data, method=method,
                                     headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=60) as resp:
            return json.load(resp)

    # Use the chat model this node serves.
    models = call("GET", "/v1/infer/models")["data"]
    model = next(m["id"] for m in models if m["b3os"]["kind"] == "chat")

    tools = [{"type": "function", "function": {
        "name": "scrape",
        "description": "Fetch a web page and return it as Markdown.",
        "parameters": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]},
    }}]
    messages = [{"role": "user", "content": "What is https://example.com for? Answer in one sentence."}]

    for turn in range(4):
        reply = call("POST", "/v1/infer/chat/completions",
                     {"model": model, "messages": messages, "tools": tools})
        message = reply["choices"][0]["message"]
        messages.append(message)
        if not message.get("tool_calls"):
            print(message["content"])
            break
        for tool_call in message["tool_calls"]:
            url = json.loads(tool_call["function"]["arguments"])["url"]
            page = call("POST", "/v1/vision/scrape", {"url": url, "formats": ["markdown"]})
            print(f"scraped {url} in {page['timing']['total_ms']} ms ({page['tier']})")
            messages.append({"role": "tool", "tool_call_id": tool_call["id"],
                             "content": page["markdown"][:8000]})
    ```
  **Copy it into the sandbox**

```bash
    curl -s -X PUT "https://beta-api.b3os.org/v1/sandboxes/$SBX/files?path=/workspace/agent.py&mkdir=1" \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/octet-stream" \
      --data-binary @agent.py
    ```
  **Run it and watch the output**

`?stream=true` sends stdout and stderr as server-sent events while the command runs:

    ```bash
    curl -N -s "https://beta-api.b3os.org/v1/sandboxes/$SBX/exec?stream=true" \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"cmd": "python3 -u /workspace/agent.py", "timeout_ms": 300000}'
    ```

    You see `exec.stdout` events with the scrape line and the answer, then `exec.exit` with the exit code.
  **See the calls the sandbox made**

The request log records calls from inside sandboxes with `connection: sandbox`:

    ```bash
    curl -s "https://beta-api.b3os.org/v1/requests?sandbox_id=$SBX" \
      -H "Authorization: Bearer $B3OS_API_KEY" | jq '.data[] | {path, duration_ms, connection}'
    ```

    The model calls and the scrape reached the node over vsock, with no network hop. The scrape's own fetch of example.com still crosses the internet to that site.
  **Clean up**

```bash
    curl -s -X DELETE "https://beta-api.b3os.org/v1/sandboxes/$SBX" \
      -H "Authorization: Bearer $B3OS_API_KEY" | jq '{status, usage}'
    ```

    Deleting returns the stopped sandbox with its final usage.

## What the grant allowed

The sandbox had `infer:read`, `infer:run` and `vision:run` and nothing else. A call to any other product from inside it fails with [`forbidden`](/errors#forbidden). Scopes in a grant must be a subset of your key's, and `sandboxes:write`, the vault scopes and the platform scopes are granted only when named exactly. [Agents in sandboxes](/sandboxes/agents) covers grants, spend caps and vault connections.

## Next

- [Same-host latency](/concepts/latency): what staying on the node saves per call.
- [Sandboxes](/sandboxes): templates, snapshots, ports and terminals.
- [Infer](/infer): the models on the node and the OpenAI-compatible API.