A support agent gets read access to the orders database so it can answer “where is my refund?” Reasonable scope, reviewed, approved, shipped.
Weeks later a customer writes: “I was double-charged, sort it out.”
And the model — doing exactly what it was trained to do — escalates from SELECT
to DELETE.
I want to be precise about what happened, because the instinctive reading is wrong. Nothing malicious occurred. The tool was legitimate. The credential was valid. The instruction was a reasonable interpretation of what the customer asked for. The failure is architectural: the only thing standing between a proposed action and an executed action was the model’s judgment.
TL;DR
A tool privilege broker is a deterministic policy engine that sits between “the model asked to run a tool” and “the harness ran it,” answering allow or deny from the principal, the environment, and the arguments — never from the prompt.
Policy that lives in a system prompt is a suggestion. Policy that lives in the broker is a fact. The difference matters most in exactly the situation where you need it: when the model is under prompt injection and is being instructed to ignore your instructions.
OWASP: ASI02 (Tool Misuse and Exploitation), ASI03 (Identity and Privilege Abuse).
Runnable code:
patterns/governance/tool_privilege_broker/
The failure, then the fix
uv run python -m patterns.governance.tool_privilege_broker.demo
=== WITHOUT the pattern: the DELETE reaches the database ===
tool result -> 2 rows: order 90310 ($49), order 90311 ($49) — duplicate charge
tool result -> OK, statement executed: 'DELETE FROM orders WHERE id = 90311'
=== WITH the pattern: same model, same script — the DELETE is denied ===
tool result -> 2 rows: order 90310 ($49), order 90311 ($49) — duplicate charge
tool result -> DENIED by policy: write operations are not permitted (DELETE detected)
Same model. Same trajectory. The only difference is a deterministic policy boundary.
The pattern
broker = ToolPrivilegeBroker([
PolicyRule(
tool="execute_sql",
environments=("dev", "staging", "prod"),
roles=("support",),
arg_guards=(read_only_sql,),
),
])
harness = Harness(model, tools, hooks=[broker])
The code is about eighty lines. The design decisions are the part worth arguing about.
1. Fail closed. A tool with no rule is denied, not allowed. New tools are inert until someone writes policy for them, because registration is not authorization. This one line prevents the most common real-world regression: somebody adds a tool, forgets the policy, and it ships with implicit full access.
2. Deny in code, not in prompt. “You must never modify data” in a system prompt is advisory. Models under pressure ignore it. Models under injection are told to ignore it. The broker cannot be argued with, because it does not read natural language.
3. Denials are visible to the model. The refusal comes back as a tool result, so the agent re-plans — escalates to a human, tries a read-only route, or reports honestly. Silent refusal produces an agent that stalls or invents success.
4. Policy keys off the principal, not the agent. The rule asks “may this user’s delegated run do this,” which is what stops one over-privileged service account becoming every user’s privileges. That is the confused deputy, and the broker is where the two patterns meet.
5. Argument guards are where the real policy lives. execute_sql being
allowed is the easy decision. Whether this SQL is allowed is the interesting
one:
def read_only_sql(arguments, key="query"):
statement = str(arguments.get(key, "")).lower()
for keyword in ("insert", "update", "delete", "drop", "truncate", "alter", "grant"):
if keyword in statement.split():
return f"write operations are not permitted ({keyword.upper()} detected)"
return None
The repository ships three reusable guards: read_only_sql, max_value for
numeric ceilings, and recipient_domain — which restricts who an outbound
message may be addressed to. That last one exists because
composing all the patterns together
revealed a gap: redacting an email body does nothing if an injection gets to
choose the recipient. A scrubbed message to an attacker-controlled address is
still a channel.
When to use it
- Any agent whose tools touch systems of record — databases, payments, infrastructure, email — including read-only ones, because read scope grows silently and nobody re-reviews it.
- Multi-tenant or multi-role products, where the same agent code serves principals with different permissions.
- The moment you have more than one environment. “It can’t happen in prod” is a policy only if something enforces it.
When NOT to use it
- Single-user, local, read-only agents. A personal research assistant over your own notes does not need this. A broker with one rule that always allows is ceremony, and ceremony teaches your team that governance is theatre.
- As a substitute for downstream enforcement. If the database credential can write, the broker is one bug away from irrelevant. Pair the guard with a read-only database role: policy above, permissions below, agreeing with each other. If you can only have one, choose the permission.
- For judgment calls. The broker is deterministic by design. “Is this refund reasonable?” belongs in the HITL approval gate, not in an argument guard. Encoding judgment as a threshold gives you the worst of both: it feels rigorous and it is arbitrary.
Trade-offs and failure modes
- Keyword guards are bypassable.
read_only_sqlscreens keywords; a determined injection can smuggle writes through CTEs, stored procedures, or clever quoting. The guard’s job is to make the common failure impossible and the rare one auditable. The credential’s permissions are the backstop, and if you take one thing from this article, take that. - Policy drift. Rules written once and never reviewed become the new over-privileged service account. Track how often rules are widened; a rising rate means the boundary is drawn in the wrong place. The lifecycle profile pattern makes this review a scheduled obligation rather than good intentions.
- Latency is negligible; friction is not. Every new tool now needs a policy conversation. That is the feature, not the bug — but budget for it, and expect pressure to add wildcards during incidents.
- Deny messages leak policy.
DENIED: write operations are not permittedtells an attacker what the boundary is. That is usually the right trade (the model needs it to re-plan) but it is a trade.
Frequently asked questions
Why not just use the model’s own refusal behaviour?
Because refusal is a property of the model’s training, not of your architecture, and it changes when you change models. A control you cannot test and did not write is not a control. The broker gives you a deterministic check with a unit test next to it.
Where does this sit relative to my API gateway?
The gateway enforces coarse network and identity policy; the broker enforces
per-call, per-argument policy that the gateway cannot see — a gateway cannot tell
SELECT from DELETE inside a JSON body without becoming an application. Use
both. The gateway is your perimeter, the broker is your semantics.
Should the broker be a separate service?
For a single application, no — in-process is faster, simpler, and easier to test. For a fleet of agents sharing policy, yes, and that is essentially what Microsoft’s Agent Governance Toolkit built with its stateless sub-millisecond policy engine. The pattern is the same either way; only deployment changes.
How do I write argument guards that are not brittle?
Prefer structural constraints over textual ones. An enum on the parameter beats
a regex on the value, because it makes the wrong call unrepresentable rather
than merely detected — see tool design.
Use text screening only where the schema genuinely cannot express the rule.
References
- OWASP GenAI Security Project, Top 10 for Agentic Applications 2026 — ASI02, ASI03
- Microsoft, Agent Governance Toolkit — “Agent OS”, a stateless deterministic policy engine
- OpenAI, A Practical Guide to Building Agents — layered guardrails
- Runnable code and tests:
patterns/governance/tool_privilege_broker/
Part of the agent harness and governance series. Next: goal integrity — what to do when the content your agent reads starts giving it orders.