Tool registry and sandbox pattern for AI agents
Keep a registry of every tool an agent can call, with its schema, description, owner, permission level (read, reversible write, irreversible write), rate limit and credentials. Agents can only call registered tools, the orchestrator enforces the permission level (irreversible actions need approval), and tools that run code or touch systems run in an isolated sandbox with the narrowest credentials possible. The registry is your inventory and your policy in one place: if a tool isn't in it, the agent can't use it.
What can go wrong
An agent with a generic "run Python" tool and production credentials can do anything, including things nobody intended. Prompt injection makes this worse: text in a web page or an email can tell the agent to use its tools against you.
Registry
Each entry describes one tool:
| Field | Example |
|---|---|
| Name and version | refund_order v2 |
| Description | What it does and when to use it, written for the model |
| Schema | Arguments and types |
| Permission level | read, reversible write, irreversible write |
| Approval | None, or required above a threshold |
| Rate limit | 10 calls per run |
| Owner | Payments team |
| Credentials | A scoped key for this tool only |
The orchestrator reads the registry, offers only the tools the current agent is allowed to use, and checks every call before running it.
from dataclasses import dataclass
@dataclass(frozen=True)
class Tool:
name: str
level: str
max_calls: int
needs_approval: bool = False
REGISTRY = {
t.name: t
for t in [
Tool("get_order", "read", max_calls=20),
Tool("create_ticket", "reversible_write", max_calls=3),
Tool("refund_order", "irreversible_write", max_calls=1, needs_approval=True),
]
}
def authorise(name: str, calls_so_far: dict[str, int], approved: bool = False) -> str:
tool = REGISTRY.get(name)
if tool is None:
return "deny: not registered"
if calls_so_far.get(name, 0) >= tool.max_calls:
return "deny: call limit reached"
if tool.needs_approval and not approved:
return "pause: waiting for human approval"
return "allow"
calls = {"get_order": 2}
for name in ["get_order", "run_python", "refund_order"]:
print(f"{name:<14}{authorise(name, calls)}")
Sandbox
Tools that execute code, browse the web or touch files run in isolation:
- A container or a micro-VM per run, destroyed afterwards.
- No network access unless the tool needs it, and then only to allowed hosts.
- Time, memory and CPU limits.
- A read-only filesystem except for a scratch directory.
- No secrets inside the sandbox other than the tool's own scoped credentials.
Credentials
- One credential per tool, per environment.
- Short-lived tokens where possible.
- Read-only database users for read tools.
- Never the developer's or the admin's credentials.
Audit
Log every call: the agent, the run, the tool, the arguments, the result, the approver if any, and the time. That log is how you answer "what did the agent do?" after an incident. See audit logs.
MCP and the registry
With the Model Context Protocol, tools come from MCP servers. The registry still applies: decide which servers and which of their tools each agent may use, and don't let an agent connect to arbitrary servers.
Measuring it
- Calls to unregistered or denied tools (should be zero, and every attempt worth reading).
- Approval requests and rejection rate.
- Sandbox escapes in red-team tests.
From the floor
In the production pipeline I teach, the agent zone has a secure tool registry, any kind of tool an agent can call, a calculator, an external API, a Python function, a calendar or a spreadsheet, and the tools execute in an isolated sandbox environment, so they don't interfere with the agent's own execution.
In DevFix, the generated code ran inside Docker, and a safety guard blocked code with calls to remove files or execute arbitrary commands before it reached the sandbox at all.