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), 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:
bashSBX=$(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.
pythonimport json, os, urllib.requestAPI = 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
bashcurl -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:
bashcurl -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:
bashcurl -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
bashcurl -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. 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 covers grants, spend caps and vault connections.
Next
- Same-host latency: what staying on the node saves per call.
- Sandboxes: templates, snapshots, ports and terminals.
- Infer: the models on the node and the OpenAI-compatible API.