Sandboxed Agent
An agent runs in a sandbox with no route into the office: here, a GitHub
codespace. Through the mesh it reaches a model and an MCP server that listen
on loopback in the office, and api.github.com with a credential it never
holds. The admin decides what it may call, down to the HTTP method and path,
watches every decision, and cuts it off when done.
Source: development/examples/sandboxed-agent/.
The scenes, the narration and the recording recipe are in its
SCRIPT.md.
The idea
An agentic application is not deterministic: the code decides at run time which API it calls and with what, so its network behaviour cannot be reviewed in a pull request. The usual way to give it the model, the tools and the internal API it needs is a VPN into the corporate network, which is slow to develop against and turns the sandbox into a door with credentials inside.
The mesh replaces the VPN with one identity and one policy. The agent is an ordinary program written with the Python SDK; the sandbox needs outbound HTTPS to one URL and nothing else, so any sandbox works: a container, a microVM, a codespace. What the agent can reach is decided by name on the admin’s side, and the credential for the external API stays on a node in the office.
The pieces
One policy (policy.json).
The role agent may call inference://office-llm, mcp://tools and
egress://api.github.com, the last one only with GET under
/repos/google/sam/. The destination api.github.com is served by nodes
labelled site=office with the credential named github-ro.
One node in the office (pep.yaml).
A sam-node with the label site=office fronts Ollama and an MCP server on
loopback. The control plane assigns api.github.com to it because of the
label; the node reads the token from <--secrets-dir>/github-ro on every
request and presents it to GitHub. See
egress destinations.
One program in the sandbox (agent.py).
It enrolls once with a single-use token minted for the role agent, then
discovers each service by name and calls it: /v1/models and a chat
completion, an MCP tool, and three requests to GitHub of which the policy
allows one.
Run it yourself
On the office machine, from development/examples/sandboxed-agent/:
make ollama # the office model, in Docker
make mcp # the MCP reference server on :3001, in its own terminal
make mesh # sam-one on a public https URL; copy the API URL from the banner
make tokens URL=$URL # one token for the node, one for the agent
GITHUB_TOKEN_FILE=~/github-ro make pep URL=$URL 2>&1 | python3 audit.py
GITHUB_TOKEN_FILE is a fine-grained token with read access to pull
requests on google/sam, and nothing else; the node is the only process
that reads it.
In the sandbox, with the SDK installed (pip install ./sdk/python) and the
agent token the admin handed you:
export SAM_CONTROL_PLANE_URL=$URL SAM_BOOTSTRAP_TOKEN_PATH=./agent-token
python agent.py # every step, or one of: models, ask, tool, github GET /user
Then, back in the office, make revoke URL=$URL takes the destination off
the mesh and make ban URL=$URL PEER=<peer id> cuts the agent off.
What to notice
- The request GitHub receives carries the office’s token and none of the agent’s headers. The sandbox never had a credential to leak.
POST /repos/google/sam/pullsandGET /userare answered403withProxy-Status: sam-node; error=http_request_deniedby the node, before GitHub hears of them. Each is oneDENYline in the node’s log with the peer, the role, the method and the path.- Removing the
egressentry from the policy withdraws the destination on the node within its sync jitter (the example runs with--control-plane-sync-interval 30s); the same request then finds no service. Banning the peer disconnects it and no router admits it again. - Grants live in a member’s credential until it is refreshed (24 hours by
default,
--control-plane-biscuit-ttl). Removing a service from a role takes effect at that refresh; withdrawing a destination or banning a member takes effect at once, which is why the demo uses those two.