Agent permissions
Deny by default, explicit deny wins, and delegation intersects, so an agent cannot borrow authority it was never granted.
An agent holds whatever you forgot to withhold.
An agent authenticates with an opaque bearer token, and any process holding that string is the agent. There is no cryptographic binding to a workload, and the product carries that as a stated residual risk. The permission set is therefore the control that matters, and the difference between a contained incident and a bad week is whether the roles are an allowlist or a list of exceptions.
Conventional role models fail agents twice. A role granting a system rather than an action hands over that whole system, so an agent needing one read against the order database also gets the refund endpoint beside it. And agents call other agents: a model silent about the second hop lets a low-privileged agent ask a higher-privileged one to do what it was just refused.
How it works
Steps 6 and 6b of the request path. The order is encoded in the evaluator, and where each step sits is the reason the guarantee holds.
- 01
Name the resource
The thing authorised is a string: model:<name> for a model call, tool:<server>/<tool> for a tool call. One vocabulary covers both surfaces, so tool:orderdb/* scopes an entire MCP server in one line. - 02
Deny first, then require an allow
Every role and permission is scanned, and the first matching deny returns immediately, naming the role and the pattern; order is irrelevant. With no deny, the first matching allow decides; with no allow either, the refusal reason ends in deny by default. - 03
Intersect every hop, and the named human
Where the request names upstream agents, each role set is a link and every link must allow; the refusal names the earliest failing link and how many there were. Once on-behalf-of enforcement is on, the named person’s mapped roles are appended as the last link of that same chain.
- beat 01Name the caller
- 1Authenticate
- 2Resolve the agent
Nothing is decided for an anonymous caller. The key resolves to one registered agent with a named owner, and its roles, its ceilings and any stop switch in force are read live from the store on every request, not from a cached copy, so a suspension takes effect on the next call instead of after a redeploy.
- beat 02Start the record, then read what is being sent
- 3Open the trace
- 4Sanitise
- 5Scan
The trace is opened before anything can refuse the request, which is why a blocked call is recorded as thoroughly as one that went through. Only then is the text normalised and scanned, in that order, so the detectors see what the model will read rather than what a human reviewer can see.
- beat 03Decide once
- 6Govern
- 6bIntersect with the named human
- 7Enact
One function returns one verdict: allow, block, or hold it for a named person to approve. Every check lives in that one place rather than scattered along the path, which is what makes it possible to read your rules and know what they do.
- beat 04Send it, and only then spend the money
- 8Route
- 9Call upstream
Routing fixes which provider and model will serve the call, so where an agent has a USD ceiling it is tested against the price you will pay, not the one the caller typed. Routing is also the last step that can still refuse locally: after it, the request has left your network.
- beat 05Govern the answer, then count the cost
- 10Govern the response
- 11Meter and record
A tool call the model proposes is judged before it reaches your agent. Metering runs last and on every ending, including the refusals and the streams a client abandoned halfway, because the tokens were spent either way and a partial trace is still evidence.
Step names and their grouping are the product’s own, and the order is encoded in the evaluator rather than drawn for the page.
An explicit deny always wins.
An action is allowed only when a permission on one of the agent’s roles explicitly allows it and none denies it. There is no implicit grant anywhere. The deny path returns before any allow is settled, which makes precedence independent of ordering: a guardrail denying one refund tool wins whether it is listed before or after the broad grant.
- Matching is deliberately dull
- A pattern is a literal string in which * matches any run of characters. Every other metacharacter is escaped, so a full stop in a tool name stays a full stop.
- Namespace wildcards do not leak
- tool:orderdb/* matches every tool on that server and nothing on any other. The bare wildcard allows everything, and is the one grant a reviewer should find in seconds.
- Roles are flat
- A name, a description and a list of permissions. No inheritance, so a superset role restates what it supersets. More typing, less to reason about.
role finance-support
allow tool:payments/* actions: [invoke]
deny tool:payments/issue_refund actions: [invoke]
invoke tool:payments/get_status -> allowed by tool:payments/*
invoke tool:payments/issue_refund -> explicitly deniedDelegation intersects, so a chain cannot escalate.
When agent A delegates to agent B, B’s effective permissions are the intersection of every agent in the chain. The union is never taken: an orders agent and a payments agent delegating to each other can jointly do neither the lookup nor the refund. That is inconvenient by design. Under-privilege surfaces as a ticket somebody investigates; a union produces an escalation with an audit trail that looks legitimate. The chain is a header, asserted rather than proven, which the intersection makes acceptable, because a forged chain can only add links and every link must allow.
The on-behalf-of header names the person an agent acts for and is recorded on the trace in every configuration. Whether it narrows authority is a separate decision in three settings, off by default. Under enforce the person’s mapped roles become the final link of that chain, so a human whose group maps to a wildcard role is a no-op rather than an escalation: the wildcard satisfies its own link and nobody else’s.
off (default) nothing is read. Attribution only.
shadow the intersection is recorded as what it would
have refused. The request proceeds.
enforce the human's roles become the last link of the
chain. Agent may, human may not -> 403.The attack this prevents is the one every naive permission model allows: an agent that has been refused something asks a more privileged agent to do it instead.
- Read: Customer Account
- Update: Shipping Address
- Delete: Account
- Issue: Refund
Deny-by-default: an action the policy does not list is denied, which is why the list of denials is short and the list of grants is the whole policy.
- Read: Customer Account
- Issue: Refund
- Delete: Account
Only what both hold. A refund is not reachable through the chain, because A never had it, and an explicit deny anywhere in the chain wins outright.
Each line closes off an assumption a reader would otherwise carry into a proof of concept.
What this does not doWhat this leans on
Agent registry
One record per agent, and it is the record the gateway enforces against.
Policy engine
One deterministic verdict on every governed request: allow, block, redact, or park it for a human.
MCP gateway
The half of an agent that moves money is a tool call, not a sentence. This is the checkpoint that call goes through.
Where this comes up
Jobs that draw on it
What happens when one role allows an action and another denies it?
The deny wins, regardless of order. The evaluator returns at the first matching deny and names the role and pattern that produced it. That is what makes a subtractive guardrail reliable: grant a whole tool namespace to a team, then remove one action from one agent without rewriting the broad grant.
Can a low-privileged agent gain access by asking a higher-privileged agent?
No. The effective permission set is the intersection of every agent in the chain, so the request is refused at the first link without the grant, and the refusal records which link out of how many. The chain is asserted rather than proven, but because it can only add links and every link must allow, forging it buys an attacker less than sending none.
Does naming a human in the on-behalf-of header restrict what an agent can do?
Only once you switch enforcement on, and the default is off, under which nothing is looked up at all. Under shadow the intersection is computed and recorded as what it would have refused. Under enforce the person’s mapped roles join the delegation chain and can only narrow. Two of the three settings refuse nothing, and an install that has not reached enforce should not call the intersection a control it holds.
Prefer to ask a person? Write to James
Bring us the agent you are least comfortable with.
Create a cloud account and workspace. No card or server installation. Model calls and Google BigQuery scans can incur separate provider charges.