Crack your next interview in 2 weeks — cohort starts Jul 27 — Join free →
ai-agents tool-use system-design python

The Permission Model Hiding Inside Every 'AI Agent'

Say “AI agent” to most people and they picture something autonomous — a system that decides things and does things, on its own. That picture is wrong in a specific, useful way: an LLM never does anything. It only ever asks.

Every tool-calling agent, from a simple chatbot with a calculator plugin to a multi-step coding assistant, is built on the same boundary: the model produces a request — a function name and some arguments — and a separate piece of code decides whether that request gets honored. The model never touches a database, never calls an API, never writes a file. It writes text that describes wanting to. Everything that actually happens is your code’s decision.

This sounds like a technicality. It’s the entire design.

Why the boundary matters more than the loop

Most explanations of AI agents start with the loop — observe, reason, act, repeat — because the loop is the visible, demo-able part. But the loop is just repetition. The part that determines whether an agent is safe to run in production is the boundary at each “act” step: what does your code do with a request before honoring it?

Three things, at minimum. First, it checks whether the requested function actually exists — because a model can and will occasionally request a function that was never defined, a full sentence’s worth of confident-sounding text describing an action that has no implementation behind it. Second, it decides whether the request is safe to run given the current state — a function that deletes something shouldn’t execute on the same trust level as one that reads something. Third, once the function runs, your code decides what the model gets to see about the result, because whatever comes back gets read by the model as if it were as trustworthy as anything else in its context — including any instructions accidentally or maliciously embedded inside it.

None of that is optional if the agent does anything with real consequences. All of it is invisible in a demo where every tool call happens to be well-formed and every tool result happens to be clean data.

The two ways an unbounded loop fails

Take the boundary away — or weaken it — and an agent loop fails in one of two predictable shapes.

The first is a request for something that doesn’t exist. Not maliciously; the model is doing exactly what it always does, producing statistically plausible continuations, and sometimes the plausible continuation is a function name that sounds right but was never registered. Skip the existence check and that request gets treated as real. Depending on how your dispatch code is written, that can range from a harmless no-op to an unhandled exception that crashes the whole loop mid-task.

The second is a request that never stops. Nothing in an LLM’s output guarantees it will eventually say “I’m done.” A model can keep generating tool requests indefinitely — not because it’s stuck in an infinite logical loop the way a buggy while True might be, but simply because each individual request looks locally reasonable to a system that has no global sense of “we’ve already tried this.” Without an explicit ceiling on how many iterations the loop is allowed to run, that’s an unbounded cost — every iteration is a full model call, and nothing internal to the model forces a stop.

Both failure modes have the same fix, structurally: put the check in code, not in the prompt. A system prompt that says “only use the tools I’ve given you” is a strong nudge, not an enforcement mechanism — a sufficiently unusual input can still produce a request outside that list. An allowlist check that runs before dispatch, in code the model has no access to, is the actual enforcement. The same logic applies to the iteration cap: it has to live in the loop’s control structure, not in an instruction the model is merely asked to follow.

What this buys you, concretely

Once the boundary is explicit — every request validated before it runs, every result treated as untrusted content, every loop bounded by a hard cap — an agent stops being a black box you hope behaves well and becomes a system with a small number of well-defined failure points. You can log exactly what was requested, what actually ran, and what came back. You can reason about worst case: at most N iterations, at most N tool calls, each one either a known function or a caught error.

That’s the actual difference between a demo and something you’d put in front of real users. Not a bigger model. Not a longer system prompt. A boundary that doesn’t trust the model’s output any further than you’d trust arbitrary text from the internet — because structurally, that’s exactly what it is.

Find your path →

Want more like this?

Free newsletter. Real projects, delivered fortnightly.

No spam. Unsubscribe anytime. Replies go to a real person.

← Back to all articles