Authorization
Authorization in Agent Mesh answers one question: may this caller perform this operation on this service on this node right now? Four sources contribute to the answer, and the destination node combines them:
- The caller’s credential (Block 0 authority facts signed by the control plane).
- The standing mesh policy (roles, bindings, HTTP narrowings, and egress destinations distributed by the control plane as Datalog rules).
- Any appended
TaskAuthorizationRuleblocks (tar_block) on the credential, which narrow authority for a specific task or sub-agent hop. - The hosting node’s local
attenuationconfiguration.
Each layer can only narrow what the others allow. If standing policy or any appended task block denies the request, the answer is no.
Services and task operations
A node publishes services, each with a type and a name: mcp://calculator,
inference://vllm-eu, a2a://triage, egress://bigquery.googleapis.com, and
the built-in system://mesh.catalog that answers discovery queries.
Standing mesh roles grant access to services by name (allowed_services) and
can narrow HTTP methods and paths (http). On top of standing roles, a
holder can append TaskAuthorizationRule blocks (tar_block) to scope a
token for a single task or sub-agent hop—narrowing which services, MCP tools
(operation.allowed_tools), HTTP methods and paths
(operation.allowed_methods, operation.allowed_paths), and upstream cloud
IAM permissions and resources (operation.allowed_permissions,
allowed_resources) that token may use.
Mesh policy: roles and bindings
The mesh policy is a document held by the control plane. You edit it through
POST /policies or the console. agentmesh-one can also seed it on first boot
from a file (--policy-file).
Roles name a set of standing permissions:
{
"name": "developer",
"allowed_services": ["mcp://code-reviewer", "mcp://build-runner.*", "inference://*"],
"allowed_targets": ["group:dev-nodes"],
"allowed_labels": ["region=*"],
"http": [
{ "service": "mcp://code-reviewer", "methods": ["POST"], "paths": ["/mcp"] }
],
"custom_datalog": []
}
allowed_services: the services holders may call, astype://name.*can stand for the whole name, for a leading component (mcp://*.internal) or for a trailing one (mcp://build-runner.*). A bare*means every service.allowed_targets: the nodes holders may call, as facts about the destination node’s identity:node:<peer-id>,user:<sub>,email:<addr>,group:<name>,idp_role:<name>, or*. If absent, any node may be called.allowed_labels: the labels a node with this role may declare at enrollment:key=value,key=*or*. If absent, the node may declare no labels.http: optional method and path restrictions for entries inallowed_services.custom_datalog: extra Datalog facts or rules for holders of the role.
Bindings attach roles to identities:
{ "role": "developer", "members": ["group:eng", "email:alice@example.com"] }
A member is one of: user:, email:, group: or idp_role: followed by a
value from the identity’s OIDC claims; node: followed by a peer ID; or the
special value mesh:system:authenticated, which matches every identity that
the identity provider authenticates. Be careful with the last one. On a
public identity provider it means everyone, so bind it only to roles with
few grants.
Roles are never members and never claims. role:x is not a valid member,
and an identity provider cannot give out a mesh role by putting it in a
roles claim. Such a claim becomes an idp_role fact, and a binding can
choose to honour it. The two built-in roles, mesh:role:node and
mesh:role:router, follow the same rule: a binary can only enroll if a
binding gives its identity the role it needs.
The control plane validates a policy when it is posted. It rejects a policy that references an undefined role, uses an unknown member prefix, or would give a single identity more grants than the Biscuit authorizer can evaluate.
From policy to facts
At enrollment, refresh, and token exchange (POST /token/exchange), the
control plane resolves the identity’s roles from the bindings and writes the
result into the credential’s authority block as Datalog facts: one role(...)
fact per role, plus the grants compiled from the lists of every role. For
example, allowed_services: ["mcp://calculator"] becomes
granted_service_exact("mcp", "calculator"), mcp://* becomes
granted_service_all("mcp"), and mcp://*.internal becomes
granted_service_suffix("mcp", ".internal"). Wildcards keep their dot, so
*.acme.example matches svc.acme.example but not evil-acme.example.
The control plane also renders the policy as Datalog rules such as
role("developer") <- group("eng") and
granted_service_exact("mcp", "calculator") <- role("developer"). Nodes
fetch this text every --control-plane-sync-interval (15 minutes by default,
sooner when a policy update event reaches them) and add it to their
authorizer as it arrives. When a node verifies a credential, these rules run
against the identity facts in it. A grant added to the policy therefore
reaches every node within the sync interval, with no need to reissue
credentials. Removing a grant takes effect through the credential instead:
the facts already in a token stay valid until the token is refreshed or
expires.
What the hosting node checks
When a request for service S arrives from peer P, the node validates the
Biscuit structure and runs two enforcement stages:
Stage 1: Standing Datalog policy (Block 0)
The node builds a Biscuit authorizer over Block 0 and adds, in this order:
- The request facts:
service("mcp", "calculator")for the requested service, andconnection_peer_id(P)from the authenticated connection. When the node handles the request as HTTP it addsmethod("GET")andpath("/v1/models"), the path as the backend will see it; for a destination outside the mesh (egress://) it addshost(...)andport(...). - The baseline checks:
client_peer_id($id), connection_peer_id($id)(the token belongs to the peer that presents it), and the expiration check against the current time. - The node’s own identity facts, taken from its own credential, as
target_fact("group", "dev-nodes")and similar. The caller’sallowed_targetsare matched against these facts. - The node’s local rules, from the
attenuationblock of its configuration file: extra facts, extra checks, andallowanddenypolicies. - The baseline policies:
allow if service($t,$n), granted_service_exact($t,$n)and the equivalent policies for sets, prefixes, suffixes, per-type and global wildcards, plus the target checkallow_network_target(...) or target_unrestricted(true), and the rules that turn an HTTP grant into a service grant when the request’s method and path match it. - The synced mesh policy rules described above.
Biscuit evaluates every check and requires all of them to pass. It then
walks the policies in order and applies the first allow or deny that
matches.
Stage 2: Task authorization rules (tar_block 1..k)
If the Biscuit carries appended blocks (1..k, up to MaxAttenuationBlocks = 8),
UnmarshalInbound verifies before building the authorizer that every appended
block has 0 rules, 0 checks, and 1 tar_block("<base64url-proto>") fact.
After Stage 1 succeeds, the verifier evaluates the decoded
TaskAuthorizationRule chain against the request:
- The current time must be strictly before every block’s
expire_time(when set). - In every appended
TaskAuthorizationRuleblock, at least oneTaskRulemust match the request’sservice, HTTPmethodandpath(whenallowed_methodsorallowed_pathsare non-empty), and MCP tool name ontools/call(whenallowed_toolsis non-empty).
Because every block must match, appending a new tar_block at a sub-agent hop
computes the intersection ($\text{Standing Policy} \cap \text{TAR}_1 \cap \dots \cap \text{TAR}_k$)
and can never widen authority.
Why a caller cannot forge a fact or smuggle Datalog
Two kinds of Datalog fact meet in the authorizer. The facts in the
credential’s authority block were written and signed by the control plane:
roles, grants, labels, and client_peer_id. The facts about the request
(service, method, path, host, port, connection_peer_id, time)
are added by the node that received the request, from what arrived on the
wire. A caller writes neither.
When a holder attenuates a token by appending a block, Agent Mesh never evaluates
holder-authored Datalog rules or checks. Appended blocks are restricted to a
single tar_block("<base64url-proto>") fact, which is invisible to Block 0’s
Datalog rules and is evaluated by the verifier’s own TaskAuthorizationRule
matcher. This guarantees that:
- An untrusted holder cannot trigger expensive Datalog backtracking via
crafted
check ifqueries. - The
TaskAuthorizationRuleenforced by the mesh PEP is byte-for-byte identical to the rule intersected byCloudTokenExchangerat egress.
Workloads and agents acting through a node or gateway
When multiple workloads, users, or sandboxed agents share a agentmesh-node (or
call through agentgateway / Istio), they do not share the node’s own
permissions:
- On
/mcpand/v1/*(or via RFC 8693POST /oauth/tokenand Envoyext_authz/ext_proc), the caller presents its own platform JWT (OIDC ID token, Kubernetes projected SA JWT, or SPIFFE JWT-SVID) or a task-attenuated Biscuit. agentmesh-nodeexchanges platform JWTs viaPOST /token/exchangeinto a Delegated Session Biscuit carrying the caller’s ownuser(),email(),group(), androle()facts, bound to the node viaclient_peer_id()andactor_node().- The destination node authorizes the request against the caller’s delegated
Biscuit and its
tar_blockchain, never the origin node’s own roles.
Local rules
The attenuation block in agentmesh-node.yaml gives the hosting node the final
say. Use it for constraints that the operator of that node wants regardless
of what the mesh policy grants:
attenuation:
rules:
- 'maintenance(true) <- time($t), $t > 2026-12-31T00:00:00Z;'
checks:
- 'check if label("jurisdiction", "eu");' # every caller must carry this label
policies:
- 'deny if service("mcp", "db-writer"), group("contractors");'
- 'deny if maintenance(true);'
rules derive new facts, checks must all hold, and policies are
evaluated before the baseline policies. Every predicate in Datalog carries at
least one term (presence-only facts are written name(true)). A syntax error
in any statement stops the node at start, so a broken rule cannot weaken the
node without notice.
Labels
Labels are key=value pairs. A node declares them in its configuration, and
the control plane writes them into the node’s credential as label(k, v)
facts, one per label, if a role the node holds allows them. Labels let
policy describe where a node is or what it is for. Agent Mesh does not define a
fixed set of keys: region, jurisdiction, team, compliance, or
whatever the operator needs.
Labels are used in three places:
- A provider restricting callers adds
check if label("region", "eu")to itsattenuation.checks. Every caller’s credential must then carry that label. - A caller choosing providers sends
X-Mesh-Required-Labels: region=euon the node’s/v1inference endpoints or on a proxied A2A request, or passesrequired_labelstocall_remote_tool. Before it sends any request data, the calling node fetches the provider’s credential through the mutual handshake, verifies it, and confirms that every label named is attested in it. A provider that cannot show them all is skipped. - An operator drawing a boundary sets
egress.require_labelsin the node configuration. Every provider this node talks to must attest all of those labels, whether or not the caller asked for any. The caller can add further requirements but cannot remove the operator’s.
The header and the operator floor follow the same matching rule: a map of
key=value pairs, one value per key, and the provider must attest every
pair. Listing more pairs narrows the set of acceptable providers, as it does
in a Kubernetes label selector or a Prometheus matcher. Labels seen in
discovery results are only used to rank candidates. The only labels that
authorize anything are the signed ones in a credential.
See also
- Policy reference: every field, pattern and fact name.
- Agent architecture:
TaskAuthorizationRule(tar_block),CloudTokenExchanger, and gateway integration. - Node configuration reference: the
attenuation,labelsandegressblocks. - Reaching services outside the mesh: the node as a policy enforcement point and credential broker for outbound API calls.