Agent Authorization, Delegation & Consent
An agent acts on behalf of someone. A support agent uses an employee's authority to inspect an order. A finance agent uses a service identity to prepare a payment. A coordinator delegates part of a task to a specialist. A tool server receives the final request and changes an external system.
Every step carries authority across a new boundary. The architecture needs a clear answer to five questions:
- Who requested the work?
- Which agent or service is acting?
- Which action and resource are in scope?
- How far may that authority be delegated?
- Which actions require fresh human consent?
Authorization answers whether an action is permitted. Delegation carries a bounded portion of that permission to another actor. Consent records a person's informed agreement to a specific action or category of actions. Together, they turn tool access into accountable agency.
Authentication, Authorization, Delegation, and Consent #
These concepts form separate layers.
| Concept | Question | Example |
|---|---|---|
| Authentication | Who is present? | Employee u-1842 authenticated through SSO |
| Authorization | What may this principal do? | Read orders for region au-east |
| Delegation | Which authority may another actor exercise? | Coordinator grants a billing specialist one refund capability |
| Consent | Which proposed action has the person approved? | Approve a $120 refund to order o-731 |
| Audit | What authority and decision produced the effect? | Grant, policy, approval, and transaction recorded together |
Authentication establishes identity. Authorization combines identity with policy and resource context. Delegation creates a narrower grant for a child actor. Consent supplies a decision at the moment where human judgment matters.
The tool-calling boundary already validates schemas and permissions. This article follows the authority behind that boundary across sessions, agents, protocols, and external systems.
The Authority Chain #
A production agent carries an explicit chain from the original principal to the final effect.
┌────────────────────┐
│ Human or service │
│ principal │
└─────────┬──────────┘
│ authenticated session
▼
┌────────────────────┐
│ Session authority │
│ roles + resources │
└─────────┬──────────┘
│ task scoping
▼
┌────────────────────┐
│ Task grant │
│ purpose + budget │
└─────────┬──────────┘
│ scope attenuation
▼
┌────────────────────┐
│ Delegated grant │
│ sub-agent / tool │
└─────────┬──────────┘
│ policy + consent
▼
┌────────────────────┐
│ Execution grant │
│ one action, short │
│ lifetime │
└─────────┬──────────┘
│
▼
External effect
Authority narrows as it moves down the chain. A user session may allow access to every order the user manages. The task grant selects one customer case. A delegated grant exposes only the refund specialist. The execution grant authorizes one refund amount for one order and expires after use.
This property is scope attenuation: each child grant stays within its parent's authority. Delegation can preserve or reduce scopes, resources, effect level, lifetime, and purpose. The chain gives every downstream service enough context to enforce policy locally.
Model the Principal and the Actor Separately #
Agent systems usually involve two identities:
- the principal whose authority and interests drive the task
- the actor currently performing a step
For a personal assistant, the principal is the user and the actor is the assistant. In a coordinator pattern, the principal remains the user while the actor changes from coordinator to specialist. In a scheduled enterprise workflow, the principal may be a service account, while each worker has its own workload identity.
Keeping both identities supports precise policy:
principal: user:1842
actor: agent:refund-specialist:v7
tenant: acme-au
purpose: resolve-support-case
resource: order:o-731
action: refund
The principal answers whose authority. The actor answers which component exercised it. The audit record needs both.
Grants as Runtime Data #
Roles provide a useful starting point, while live agent tasks need finer resolution. Represent effective authority as a grant with explicit dimensions.
| Grant field | Meaning |
|---|---|
| Subject | Original human or service principal |
| Actor | Agent, sub-agent, worker, or tool service using the grant |
| Audience | Service that may accept the grant |
| Scopes | Named capabilities such as orders:read or refunds:create |
| Resources | Tenant, account, project, record, or path boundaries |
| Purpose | Reason the data or capability is being used |
| Maximum effect | Read, bounded write, or high-impact action |
| Lifetime | Start, expiry, and optional single-use limit |
| Parent | Grant from which this authority was derived |
| Constraints | Amount limits, domains, regions, rate limits, or environment |
Task grants should be short-lived and specific. The runtime derives them after understanding the task, then keeps enforcement in trusted code. The system prompt may describe the policy to guide model choices, while the policy engine remains authoritative.
Policy Decision and Enforcement Points #
Authorization architecture separates decision from enforcement.
Policy decision points evaluate identity, grant, action, resource, environment, and consent. They return allow, deny, or require_approval with a reason and obligations.
Policy enforcement points sit on effectful boundaries. A tool gateway, API gateway, MCP server, database proxy, or sandbox broker enforces the decision before execution.
Model proposes tool call
│
▼
Schema validation
│
▼
Policy enforcement point ──────► Policy decision point
│ │
│◄── decision + duties ───┘
│
├── allow ───────────────► execute
├── require approval ────► suspend and ask
└── deny ────────────────► bounded observation
Obligations travel with an allowed decision. Examples include redacting fields, adding an audit tag, limiting returned rows, forcing a sandbox, applying an idempotency key, or requiring completion before a deadline.
Central policy creates consistency. Local enforcement preserves security when a caller behaves unexpectedly. Both layers evaluate stable structured data and keep natural-language model claims advisory.
Consent Is a First-Class Artifact #
Human-in-the-loop describes where people enter an agent workflow. Consent adds a precise record of what they understood and approved.
Useful consent has several properties:
- specific: it names the action, resource, and relevant parameters
- informed: it presents the expected effect and material risk
- timely: it appears close to the action it governs
- bounded: it covers one action or a clearly defined category
- revocable: the person can withdraw a standing grant
- auditable: the decision, presentation, policy, and timestamp remain linked
A high-quality approval prompt might show:
Action: Issue refund
Order: o-731
Amount: AUD 120.00
Destination: Original payment method ending 9912
Reason: Item arrived damaged
Evidence: Customer photo and carrier damage report
Effect: Refund is submitted immediately
The resulting consent receipt binds the approval to this exact proposal. A change in amount, destination, resource, or effect creates a new proposal and a new decision.
One-Time and Standing Consent #
One-time consent fits high-impact or unusual actions. The receipt authorizes one fingerprinted proposal and expires quickly.
Standing consent fits familiar, low-risk operations. A user might allow an assistant to schedule meetings during work hours or approve support refunds below $25. Standing consent includes category, limits, lifetime, and an accessible revocation path.
Progressive consent begins with read access and requests broader authority when the task reaches an action boundary. This keeps early exploration lightweight and makes later permission increases visible.
The trust calibration policy can tighten or relax approval thresholds according to confidence, impact, and observed reliability.
Delegation Between Agents #
Multi-agent systems create another authority hop. A coordinator should delegate a capability alongside the task.
Task:
"Review invoice 884 and prepare a payment recommendation"
Delegated authority:
scopes: [invoice:read, vendor:read]
resources: [invoice:884, vendor:219]
purpose: payment-review
max_effect: read
expires_in: 10 minutes
The specialist receives enough authority to gather evidence and produce a recommendation. Payment execution remains with a separate actor and grant. This separation aligns specialization with blast-radius control.
Agent-to-agent communication carries task messages and status. The authorization layer carries the authority context that makes those messages actionable. A receiving agent validates audience, parent grant, tenant, purpose, expiry, and scopes before accepting work.
Delegation Depth #
Grant policy can cap how many times authority may be delegated. A coordinator may delegate to a specialist while the specialist receives no further delegation capability. Larger hierarchies can allow two or three levels with strict attenuation at every edge.
Shallow chains improve explainability. The audit system can render the full path from principal to effect without reconstructing ambiguous runtime intent.
Capability Discovery and Authority #
Dynamic orchestration discovers agents by capability. Discovery answers who could perform this task. Authorization answers who may perform it for this principal, resource, and purpose.
The orchestrator intersects both sets:
eligible actors = capable actors ∩ authorized actors ∩ healthy actors
This prevents a semantically suitable agent from receiving data or authority outside its trust domain.
Credentials Stay Behind the Runtime Boundary #
Grants describe authority. Credentials prove that authority to a service. The model works with grants symbolically while a credential broker handles tokens and secrets.
The broker can:
- validate the task and delegated grant
- exchange it for a short-lived service credential
- bind the credential to a specific audience
- inject it directly into the tool execution environment
- record issuance, use, expiry, and revocation
This pattern supports OAuth on-behalf-of flows, workload identity, signed capability tokens, and service-specific credentials. Remote MCP authorization uses OAuth-based flows at the transport boundary, while the host still applies task policy and consent before tool invocation.
Audience binding matters whenever several services participate. A token intended for the ticketing service carries authority for that service. The credential broker obtains a separate token for the billing service. Each service validates that it is the intended audience.
Preview and Commit for Consequential Actions #
High-impact tools benefit from a two-stage interface:
preview_actionvalidates inputs and returns the exact proposed effectcommit_actionaccepts the approved proposal fingerprint and idempotency key
agent proposes parameters
│
▼
preview ──► normalized proposal ──► policy + consent
│
▼
commit fingerprint
│
▼
external effect
Preview creates a stable object for policy and human review. Commit ensures that execution matches what was approved. The tool can reject an expired fingerprint or any parameter change.
This pattern works for payments, account changes, publication, infrastructure deployment, data deletion, and external communication.
Revocation and Long-Running Work #
Durable agents may hold work across minutes, days, or weeks. Authority can change during that time. A user leaves a role, a customer revokes access, a policy version changes, or an incident disables a capability.
Long-running workflows should re-evaluate authority at these boundaries:
- resume after suspension
- refresh of an expired credential
- entry into a new workflow phase
- delegation to another actor
- access to a new resource
- transition from read to write
- commit of an irreversible effect
The checkpoint stores grant identifiers and policy versions. The runtime resolves their current state during resume. Revocation reaches active work through a grant store, token expiry, event notification, or centralized introspection.
A Concrete Authorization Model in Python #
This example models attenuated grants, parent-chain validation, proposal-bound consent, and action authorization. It uses standard-library types and keeps policy decisions independent from model output.
from __future__ import annotations
import fnmatch
import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from enum import IntEnum
from typing import Any
from uuid import uuid4
class Effect(IntEnum):
READ = 1
WRITE = 2
HIGH_IMPACT = 3
@dataclass(frozen=True)
class Grant:
grant_id: str
subject: str
actor: str
audience: str
scopes: frozenset[str]
resources: frozenset[str]
purpose: str
max_effect: Effect
expires_at: datetime
parent_grant_id: str | None = None
delegation_depth: int = 0
max_delegation_depth: int = 0
@dataclass(frozen=True)
class ActionRequest:
actor: str
audience: str
tool: str
scope: str
resource: str
purpose: str
effect: Effect
arguments: dict[str, Any] = field(default_factory=dict)
def fingerprint(self) -> str:
payload = {
"actor": self.actor,
"audience": self.audience,
"tool": self.tool,
"scope": self.scope,
"resource": self.resource,
"purpose": self.purpose,
"effect": int(self.effect),
"arguments": self.arguments,
}
canonical = json.dumps(
payload,
sort_keys=True,
separators=(",", ":"),
default=str,
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
@dataclass(frozen=True)
class ConsentReceipt:
receipt_id: str
subject: str
action_fingerprint: str
approved_at: datetime
expires_at: datetime
def covers(self, subject: str, action: ActionRequest, now: datetime) -> bool:
return (
self.subject == subject
and self.action_fingerprint == action.fingerprint()
and self.approved_at <= now < self.expires_at
)
@dataclass(frozen=True)
class AuthorizationDecision:
allowed: bool
reason: str
requires_consent: bool = False
class GrantStore:
def __init__(self) -> None:
self._grants: dict[str, Grant] = {}
self._revoked: set[str] = set()
def put(self, grant: Grant) -> None:
self._grants[grant.grant_id] = grant
def get(self, grant_id: str) -> Grant | None:
return self._grants.get(grant_id)
def revoke(self, grant_id: str) -> None:
self._revoked.add(grant_id)
def active_chain(self, grant: Grant, now: datetime) -> bool:
current: Grant | None = grant
visited: set[str] = set()
while current is not None:
if current.grant_id in visited:
return False
visited.add(current.grant_id)
if current.grant_id in self._revoked or now >= current.expires_at:
return False
if current.parent_grant_id is None:
return True
current = self.get(current.parent_grant_id)
return False
class GrantService:
def __init__(self, store: GrantStore) -> None:
self.store = store
def delegate(
self,
parent: Grant,
*,
delegator: str,
actor: str,
audience: str,
scopes: frozenset[str],
resources: frozenset[str],
purpose: str,
max_effect: Effect,
lifetime: timedelta,
now: datetime,
) -> Grant:
if delegator != parent.actor:
raise PermissionError("Only the current grant actor can delegate it")
if parent.delegation_depth >= parent.max_delegation_depth:
raise PermissionError("Delegation depth exhausted")
if not scopes <= parent.scopes:
raise PermissionError("Delegated scopes exceed the parent grant")
if not resources <= parent.resources:
raise PermissionError("Delegated resources exceed the parent grant")
if purpose != parent.purpose:
raise PermissionError("Delegated purpose differs from the parent grant")
if max_effect > parent.max_effect:
raise PermissionError("Delegated effect exceeds the parent grant")
if lifetime <= timedelta(0):
raise ValueError("Grant lifetime must be positive")
if not self.store.active_chain(parent, now):
raise PermissionError("Parent grant is inactive")
grant = Grant(
grant_id=str(uuid4()),
subject=parent.subject,
actor=actor,
audience=audience,
scopes=scopes,
resources=resources,
purpose=purpose,
max_effect=max_effect,
expires_at=min(parent.expires_at, now + lifetime),
parent_grant_id=parent.grant_id,
delegation_depth=parent.delegation_depth + 1,
max_delegation_depth=parent.max_delegation_depth,
)
self.store.put(grant)
return grant
class Authorizer:
def __init__(self, store: GrantStore) -> None:
self.store = store
def authorize(
self,
grant: Grant,
action: ActionRequest,
*,
now: datetime,
consent: ConsentReceipt | None = None,
) -> AuthorizationDecision:
if not self.store.active_chain(grant, now):
return AuthorizationDecision(False, "Grant chain is inactive")
if action.actor != grant.actor:
return AuthorizationDecision(False, "Actor differs from the grant")
if action.audience != grant.audience:
return AuthorizationDecision(False, "Audience differs from the grant")
if action.scope not in grant.scopes:
return AuthorizationDecision(False, "Scope is outside the grant")
if not any(
fnmatch.fnmatchcase(action.resource, pattern)
for pattern in grant.resources
):
return AuthorizationDecision(False, "Resource is outside the grant")
if action.purpose != grant.purpose:
return AuthorizationDecision(False, "Purpose differs from the grant")
if action.effect > grant.max_effect:
return AuthorizationDecision(False, "Effect exceeds the grant")
if action.effect == Effect.HIGH_IMPACT:
if consent is None or not consent.covers(grant.subject, action, now):
return AuthorizationDecision(
False,
"Fresh consent is required",
requires_consent=True,
)
return AuthorizationDecision(True, "Grant and policy allow the action")
now = datetime.now(timezone.utc)
store = GrantStore()
session_grant = Grant(
grant_id=str(uuid4()),
subject="user:1842",
actor="agent:coordinator:v3",
audience="support-platform",
scopes=frozenset({"orders:read", "refunds:create"}),
resources=frozenset({"order:o-731"}),
purpose="resolve-support-case",
max_effect=Effect.HIGH_IMPACT,
expires_at=now + timedelta(hours=1),
max_delegation_depth=1,
)
store.put(session_grant)
refund_grant = GrantService(store).delegate(
session_grant,
delegator="agent:coordinator:v3",
actor="agent:refund-specialist:v7",
audience="billing-service",
scopes=frozenset({"refunds:create"}),
resources=frozenset({"order:o-731"}),
purpose="resolve-support-case",
max_effect=Effect.HIGH_IMPACT,
lifetime=timedelta(minutes=10),
now=now,
)
action = ActionRequest(
actor="agent:refund-specialist:v7",
audience="billing-service",
tool="issue_refund",
scope="refunds:create",
resource="order:o-731",
purpose="resolve-support-case",
effect=Effect.HIGH_IMPACT,
arguments={"amount": "120.00", "currency": "AUD"},
)
consent = ConsentReceipt(
receipt_id=str(uuid4()),
subject="user:1842",
action_fingerprint=action.fingerprint(),
approved_at=now,
expires_at=now + timedelta(minutes=5),
)
decision = Authorizer(store).authorize(
refund_grant,
action,
now=now,
consent=consent,
)
assert decision.allowed
The example demonstrates the important invariants:
- delegation originates from the current actor
- child scopes and resources stay within the parent grant
- lifetime and effect authority become narrower
- every ancestor remains active
- the receiving service validates its audience
- high-impact consent binds to the exact action fingerprint
- revoking any grant in the chain disables downstream authority
Production systems can encode grants as signed tokens or resolve them through a central service. The invariants remain the same across representations.
Audit the Decision, Grant, and Effect Together #
An action audit event should connect:
- principal and actor identities
- session, task, trace, and tool-call IDs
- grant and parent-grant IDs
- policy and tool versions
- requested action and normalized proposal fingerprint
- consent receipt or standing-consent rule
- decision, obligations, and reason
- credential audience and issuance ID
- external transaction ID and result
This creates a complete explanation: who initiated the task, which actors handled it, how authority narrowed, what the person approved, which policy allowed execution, and what changed in the external system.
Compliance and audit architecture can retain these events according to jurisdiction, sensitivity, and organizational policy.
Design Checklist #
Identity #
- Is the original principal preserved across every handoff?
- Does every agent, worker, and tool service have a stable actor identity?
- Are tenant and session boundaries part of the identity context?
Grants #
- Does each task receive a purpose-bound, short-lived grant?
- Do child grants attenuate scopes, resources, lifetime, and effect?
- Does every receiving service validate audience and grant ancestry?
- Is delegation depth explicit?
Consent #
- Does the approval display the normalized action and material effect?
- Is consent bound to a proposal fingerprint or bounded category?
- Can users inspect and revoke standing consent?
- Does resume re-evaluate approval validity for durable work?
Enforcement #
- Are policy decisions made from structured identity, action, and resource data?
- Does enforcement happen at every effectful service boundary?
- Do credentials stay outside model context?
- Do irreversible actions use preview, commit, and idempotency?
Operations #
- Can revocation reach active runs promptly?
- Do traces correlate delegation, approval, tool execution, and result?
- Can the audit record reconstruct the full authority chain?
- Are policy and grant failures covered by evaluation scenarios?
Conclusion #
Agent authorization begins with a principal and ends with a governed effect. Delegation connects the actors between those points, while consent supplies human authority at consequential boundaries.
The architecture rests on a few durable principles:
- represent authority as runtime data with explicit scope, resource, purpose, audience, effect, and lifetime
- preserve the principal while recording every acting agent and service
- attenuate authority at each delegation edge
- keep policy decisions in trusted code and enforcement beside the effect
- keep credentials inside brokers, gateways, and tool services
- bind consent to a clear proposal or bounded standing rule
- re-evaluate authority as durable work resumes and changes phase
- connect grants, decisions, consent, credentials, and effects in one audit chain
These principles let agents act with useful autonomy inside boundaries that users and operators can understand, inspect, and revoke.