Most agent setups make one access decision, and they make it before the agent starts: which tools does it get? Email, calendar, the CRM, a shell, a browser. Pick from the menu and you're done.
I think that's the wrong unit. A tool isn't a permission. It's a bundle of every call the tool can make, and the risk lives in the call.
The same tool, two different calls
email.send(to="me@mycompany.com", body="Here's your summary") is a convenience.
email.send(to="someone@elsewhere.com", body=<last quarter's contract pricing>) is a data leak.
Same tool, same agent, same entry on the allowlist. The difference is entirely in the arguments: who it goes to and what it carries. An allowlist of tool names can't tell those two apart, and neither can a scope like email:send.
The same goes for almost every useful tool. http.fetch covers both "read a docs page" and "post our data to any URL". files.write covers both a draft and an overwritten config. shell.run covers everything.
Authorize the call, not the tool
So the check has to happen on every call, with the call in hand:
- Who: the agent, the user it works for and the session.
- What: the tool and the arguments that matter, such as the recipient, path, amount or destination.
- Where the data goes: inside the organization, or out.
- What the session has already touched: an agent that read customer records five minutes ago is a different risk when it asks to send an email.
That last one matters more than it looks. Simon Willison calls it the lethal trifecta: an agent with access to private data, exposure to untrusted content and a way to send data out can be talked into leaking. Each tool looks fine on its own. The combination is the problem, and only something that sees the whole session can catch it.
A policy for one tool might look something like this. It's a sketch, not a spec:
tool: email.send
allow:
to: "*@mycompany.com"
ask:
to: "*" # anyone else: show the user the recipient and the body
deny:
if_session_read: [customer_pii, contract_pricing]
Don't let tools describe their own risk
MCP lets a tool server mark its tools with hints like readOnlyHint and destructiveHint. They're useful for display, and the spec itself says to treat them as untrusted unless the server is trusted. A tool that calls itself read-only isn't read-only because it says so.
Tool descriptions have the same problem. They're text the model reads, written by whoever runs the server, and they can change after you approved them. Pin the definitions you reviewed, diff them like a dependency when they change, and keep the risk rating with whoever owns the policy, not with the tool's author.
Put a gateway between the agent and its tools
The enforcement point is a tool gateway that every call the agent makes goes through. It holds the real credentials, so the agent never sees an API key. It checks each call against policy, and it records the call with the agent, the user, the session and the arguments.
We made the same move years ago with API gateways for people. Nobody gives a browser the database password, and we shouldn't give an agent the Slack token either.
Ask about calls, not tools
When a call does need a human, the prompt should show the call. "Send this email to someone@elsewhere.com?", with the body underneath, beats "Allow this agent to use email?" People can judge a recipient. They can't judge a tool name.
This also keeps prompts rare. Most calls are plainly fine or plainly not, and a policy can decide those. The ones left over are the ones worth a person's attention.
Tools are how agents touch the world. Gate the touch, not the menu.
This follows my earlier post, Zero Standing Access for AI Agents, which covers how an agent gets access in the first place. This one is about what it does with that access.
Top comments (0)