Architecture
Tool execution
A Tool is something an NPC can do rather than say. This page describes where a Tool call actually runs, what stops it running forever, and what is recorded about it.
The single most important property is that a Tool an NPC was not granted does not exist as far as the model is concerned. Grants are rows, read per NPC at the start of every turn, and the Tool set handed to the Provider is built from them. Nothing filters a call after the fact, because there is nothing to filter: an ungranted Tool is never described to the model at all.
Where a call goes
A Tool call resolves to one of four places.
In process. The built-ins run inside the API process. There are two:
current_time, which returns the current UTC time because a model has no clock,
and fetch_url, which retrieves a public page over HTTPS. Because they run in
the process that holds the Provider keys and the database connection,
fetch_url is the most carefully bounded thing on this page — see below.
In the NPC's Sandbox. bash, browse, install_package, list_files,
read_file and write_file are turned on by a single sandbox grant rather
than individually. The API does not run any of them; it calls sandbox-broker
over the internal network and the broker runs them inside that NPC's container.
Full detail is in
The sandbox boundary.
Against Memory. remember, recall and forget are likewise turned on by
one memory grant. They read and write the same rows a Player sees and can
delete. Memory relevant to the Player's question is also supplied in the prompt
without being asked for, because an NPC that has to decide to call recall
mostly will not.
Over MCP. Every Tool a Player added themselves arrives this way — a registered MCP Server, or a Connector, which is an MCP Server with credentials and a friendly name. The API opens a client per server, namespaces the Tools it offers, and calls through it.
sandbox and memory are grants rather than built-in Tools because each turns
on a family of them, and each needs something the Tool registry cannot supply — a
broker connection, a database. The API binds those at the point of use. One join
table carries all three kinds of grant, because the grant is the same shape in
every case: this NPC may use that thing.
load_skill is always present when the NPC has any Skill in scope. The system
prompt carries only the name and description of each Skill; the instructions
arrive when the NPC asks for them.
fetch_url, specifically
It accepts https: only, and it re-checks the destination on every redirect hop
rather than passing the whole chain to fetch. Following redirects inside
fetch would mean the guard inspected the first address and the runtime then
connected to wherever a 302 pointed, which is the whole attack one hop later.
There is a ceiling on hops, a ten-second deadline across the whole chain, and the
body is truncated rather than refused.
The guard itself refuses anything that resolves onto a private network: loopback,
the RFC1918 ranges, carrier-grade NAT, the IETF protocol assignments, the
benchmarking range, multicast, reserved space, IPv6 unique-local and link-local,
and — the one that motivates the rest — 169.254.0.0/16, where cloud metadata
endpoints live (ADR 0013). A name
that will not resolve is treated as blocked.
Anything more than reading a public page belongs in a Sandbox, which is what the Sandbox exists for.
MCP Servers and Connectors
Clients are pooled per server and reaped after five minutes idle, because connecting is not free: a stdio server spawns a process, an HTTP one performs a handshake, and a Conversation asks for the same server repeatedly. A pooled client is keyed by a fingerprint of the server's configuration, so editing a server's URL or headers replaces the client rather than reusing a stale one.
Credentials are decrypted per call and never returned by any read endpoint. They
are encrypted with AES-256-GCM under a key derived from BETTER_AUTH_SECRET by
HKDF, which means rotating that secret makes every stored credential unreadable —
stated in Rotating secrets so it is not a
surprise.
Three transports are supported: stdio, streamable HTTP, and SSE. HTTP and SSE URLs are re-checked against the private-network guard immediately before the socket opens, not only when the row was written — a server registered before the guard existed is still in the database, and a name that passed validation can point somewhere else by the time anyone connects. Stdio runs a command on the host as the API process, so registering one is Operator-only.
Tool names are namespaced by server, because two servers offering search is
normal and a silent collision is not.
Connector tokens are renewed ahead of expiry at the start of a turn rather than after a call fails, under a short distributed lock so two concurrent turns do not both renew the same one. An NPC discovering mid-answer that its Tools stopped working is worse than half a second at the start.
A server that cannot be reached does not fail the turn. It is dropped from the Tool set and named in the system prompt under "Unavailable tools", so the NPC can say what it could not do rather than quietly giving a worse answer.
What bounds a turn
Nothing in AgentsVille enforces a token quota (ADR 0003), so loops are stopped structurally rather than financially. Four ceilings do that work:
Individual calls are bounded too: a Sandbox command has a timeout the broker
enforces rather than the caller, fetch_url has a deadline and a redirect
ceiling, and the broker's own calls to Docker have their own timeout.
What is recorded
A Tool call and its result are Parts of the Message, the same as text and reasoning. They stream to the browser as they happen, are persisted with the rest of the Message, and render in the transcript in order — including on the abort path, so a turn the Player cut short still shows the calls it had made. A call that failed renders as failed rather than disappearing.
Tool inputs and outputs cross the boundary as JSON and are parsed there rather than cast, so what is stored is JSON by construction.
Tool calls, Sandbox executions and Memory retrievals also emit wide events
through the existing evlog drain rather than a parallel logging mechanism. See
Logs and observability.
The thing to understand about all of this
An NPC's Tools run because a model asked for them, and the text a model reads is
the most hostile input in the system. A prompt can aim any Tool AgentsVille grants an
NPC. That is why fetch_url and the MCP transports both carry an outbound guard
rather than only the form that writes a URL, why stdio MCP is Operator-only, and
why anything that executes model-authored code does so in a container that holds
nothing. The reasoning is in
Security posture.
Practical guidance for granting Tools is in Adding tools and MCP servers.