Capability scoping for agent tools
Handing an agent one broad tool and a polite note about what not to do is a deny-list, and it inherits every problem deny-lists have.
The fastest way to give an agent capability is to give it one broad tool. Run this SQL. Call this endpoint. Execute this shell command. One tool, unlimited reach, and the constraints expressed as instructions about what not to do.
It is fast because you are not designing anything. You have handed over your entire surface area and written a note asking for restraint.
My position is that agent safety is mostly an API design problem, and that the prompt is the weakest available place to put a constraint. Not useless, but weakest. If a limit matters, it belongs in the shape of the tool, where the model’s cooperation is not required.
Allow-list, do not deny-list
A generic tool with instructions is a deny-list, and it inherits every problem deny-lists have ever had. You are enumerating the bad cases, and the set of bad cases is larger than your imagination.
Consider a tool that runs arbitrary SQL against a read replica, with a prompt saying to only read and never modify. You have to think of DROP, and DELETE, and UPDATE. Then you have to think about whether a read can be harmful on its own, which it can, because SELECT * FROM users is a data exfiltration primitive. Then you have to think about resource exhaustion from an unbounded join. Then about writable functions and extensions.
Now consider seven narrow tools instead: list posts, get one post, create a draft, update a draft, import an image from a URL, set a draft’s featured image, publish. Each one does a single thing, and the set of things the surface can do is exactly the union of those seven. There is nothing to enumerate, because nothing else is expressible.
The trade is real. The narrow surface cannot do things you did not anticipate, which is frustrating the first time you want one of them. That inflexibility is the feature. A surface that can do things you did not anticipate is the definition of the problem.
Four rules that survive contact
No delete. Not a confirmation flag on delete, not a soft delete the agent can trigger. No delete tool at all. Deletion is the one operation where a mistake has no recovery path inside the system, and an agent almost never needs it. If something has to be removed, a human can remove it, and the cost of that inconvenience is far below the cost of the alternative.
Mutation only on unpublished state. An agent that can edit drafts can be wrong in private. An agent that can edit live content is wrong in public, at whatever scale your audience happens to be. Reject the operation at the tool boundary based on the record’s state, not on whether the model believed it was editing a draft.
Destructive or irreversible actions behind an explicit flag. Publishing is the example I use, because it is the transition from private to public and it cannot be quietly undone once something is indexed or in a feed. Make the tool require confirm=true as a separate argument, and make the default refuse. The point is not that a model cannot pass a flag. It is that passing it is a distinct, auditable, deliberate act rather than a side effect of a broader call.
No generic escape hatch. Frameworks in this space often ship something like an execute-any-registered-capability tool, because it is genuinely useful during development. Shipping it defeats every other rule on this list in a single line, since the narrow tools become decoration around a general one. If a capability framework offers a default server exposing everything, turn it off and register only what you meant to expose.
Separate the principal, not just the tools
Narrow tools running as an administrator are narrow by convention only. If the underlying credential can do anything, then any bug in the tool layer, any path traversal in an argument, any unescaped identifier, restores the full surface.
So the agent gets its own role, with permissions that match the tool surface and nothing more. In a content system that means a role which can draft but not publish, cannot install anything, cannot touch users, and is blocked from every route except the one its tools use. Then the tool surface and the credential agree, and neither one alone is the whole defence.
This is also what makes the audit trail worth having. Actions arrive attributed to the agent’s own principal, so “what did the agent change last Tuesday” is a query rather than an investigation.
The test I would run before shipping any of it
Assume the model is fully compromised. Every instruction in its context is adversarial, and it will take the worst action available to it. Now enumerate what it can do.
If the answer is “create and edit drafts, which a human reviews”, that is a system you can leave running. If the answer involves the word “arbitrary”, the design is not finished, and no amount of prompt engineering will finish it.
None of this is an argument for keeping agents away from real systems. I want them wired into real systems, because that is where they are useful. It is an argument that the interesting work is in the tool surface rather than the prompt, and that the tool surface is where I would spend the review time.