ACTION-LEVEL RBAC

Agent permissions

Deny by default, explicit deny wins, and delegation intersects, so an agent cannot borrow authority it was never granted.

Cloud workspaces use managed portal identity; customer-hosted identity-provider and child identity-management configuration below is not exposed in the self-service cloud portal. Token Observe decides every model call and tool call passing through it against action-level permissions that are deny-by-default: a support agent may hold an allow on tool:orderdb/get_details while tool:payments/issue_refund is absent, and therefore denied. An explicit deny beats every allow wherever it is written. When one agent delegates to another the chain intersects rather than unions: every hop must allow, so a low-privileged agent gains nothing by routing work through a higher-privileged one. The on-behalf-of header does the same once you switch it on; until then it is attribution and nothing else.
Default
Deny. An action no role names is refused
Precedence
An explicit deny beats every allow, in any role
Resources
model:gpt-5o-mini and tool:orderdb/* with wildcards
Delegation
Every hop must allow; the chain intersects
On-behalf-of maskOff by default; only enforce refuses anything
the problem

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.

the mechanism

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.

  1. 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.
  2. 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.
  3. 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.
the order is the guarantee
  1. beat 01
    Name 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.

  2. beat 02
    Start 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.

  3. beat 03
    Decide 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.

  4. beat 04
    Send 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.

  5. beat 05
    Govern 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.
One role, two answers
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 denied

Delegation 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.

The three enforcement settings
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.
delegation intersects

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.

Agent Athe support agent
  • 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.

asks
Agent Bthe billing agent
  • Read: Customer Account
  • Issue: Refund
  • Delete: Account
resolves to
the intersection
Read: Customer 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 do

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.

Ask about this capability
Ask how this one works, where it sits in the request path, or what it will not do. Answers stay inside what this page claims.

Prefer to ask a person? Write to James

Your next step

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.