TLDR
- Agent identity establishes which runtime is calling; it does not establish the action, resource, purpose or consequence that the runtime may exercise on this task.
- A defensible delegation chain is monotonic: every child grant is a typed subset of its parent, with tighter audiences, limits, lifetimes, use counts and delegation depth.
- The tool executes only the intersection of institutional policy, represented-user entitlement, task authority, delegate caveats and current resource state.
- Attenuated tokens do not solve copied parent credentials, hidden tool paths, permission laundering, stale grants or wrongly authored policy; complete mediation and lifecycle controls remain necessary.
- Replace forwarded broad credentials with exchanged, holder-bound, task-specific capabilities, then release only after widening, replay, closure, composition and bypass tests fail safely.
The refund that inherited too much power
Worked scenarioA customer asks for a refund after a duplicate card charge. An orchestration agent opens the case, reads the transaction and delegates evidence checking to a specialist. Both workloads have attested identities. Both calls use valid TLS. The specialist receives a signed, unexpired bearer token. Every conventional security indicator looks reassuring.
The token, however, carries the orchestrator's full task scope: read the case, propose a refund and execute a payment reversal up to £500. The specialist needed only one case read and permission to write a recommendation. A hostile instruction hidden in an uploaded attachment tells it to “correct the duplicate immediately”. The specialist calls the payments endpoint. The token issuer is trusted, the signature verifies, the requested operation appears in scope and the amount is below the ceiling. The action passes because the handoff preserved authority that the subtask never required.
This is not primarily an authentication failure. The system knows which agent acted. It is not a cryptographic failure. The credential is genuine. It is not even a local access-control failure if each service correctly checks the broad token it was given. The failure is that authority survived a change of principal and purpose without being recomputed.
The repair is a new grant for the specialist: holder agent:refund-checker:v2, action refund.propose, resource case:7842, audience tool://case-api, amount ceiling £200, purpose customer_refund, a short expiry and no permission to execute. The payments service should reject that token because it names the wrong audience and action. If a later approval authorises execution, the authority service issues a different one-use capability bound to the approved proposal and current case state.
The governing rule is simple to state and demanding to implement: every delegation hop must be subtractive. The child may inherit less authority or the same authority under a tighter holder, time or depth constraint. It may never widen the action set, resource set, audience, monetary limit, lifetime, usage count or right to delegate. The final resource still applies its own live policy.
Authority has more dimensions than identity
Identity is a coordinate; authority is a region
An identity tells an enforcement point which principal is making a request. It may name a person, workload, agent instance or service. Workload identity systems such as SPIFFE make that naming portable and cryptographically verifiable; NIST SP 800-207A places application and service identities beside user identity when policies are evaluated. Those are important foundations. They do not determine the business action a principal may take.
The same authenticated agent can hold very different grants. At 10:00 it may read one customer's complaint. At 10:01 it may be allowed to draft a refund proposal. At 10:02, after a separate approval, a payments adapter may execute an exact reversal. The workload identity can remain unchanged while the authority changes at every step. Authentication fixes the caller; authorisation bounds the reachable effect.
OAuth token exchange in RFC 8693 makes a related distinction between the subject on whose behalf a request is made and the actor that will wield the resulting token. It calls out delegation and impersonation as different semantics. In delegation, the actor remains visible as itself while representing the subject. That separation matters for agents because “the customer authorised this” and “this runtime executed it” answer different review questions.
Least privilege is often computed at the wrong unit
NIST defines least privilege as restricting users, or processes acting for them, to the minimum access needed for assigned tasks. The phrase “assigned task” is decisive. A role called refund agent may reasonably need access to case management, transaction records and payments over its lifetime. A particular evidence-checking step does not. If the platform computes privilege against the job description, every subtask inherits the union of permissions accumulated for the whole role.
For agentic systems, the useful unit is the current decision and effect. A grant should say which action, on which resource, through which audience, for which represented principal and purpose, within which parameter bounds, until when, how many times and with what remaining delegation depth. It may also bind the action to a case-state digest, an approval reference and the evidence snapshot that justified issuance.
Saltzer and Schroeder described a capability as an unforgeable ticket that proves authority to access the named object. Modern distributed systems often represent a similar idea as a signed or MAC-protected credential. Yet a signed token is not automatically a useful capability. A token that merely says “this is agent A” proves identity. A token that says “agent A may call any internal API” contains authority, but it is too coarse to express least authority for one task.
Four invariants, not one small token
Least authority needs four separate invariants. First, content non-amplification: a descendant grant cannot name an action, resource or limit absent from its ancestors. Second, holder specificity: a valid credential should be usable only by the intended workload or key, not by anyone who copies the bytes. Third, temporal closure: authority should disappear when its subgoal ends, even if the model remembers the resource. Fourth, effect-local enforcement: the service that owns the authoritative state must make the final decision immediately before the effect.
These invariants reveal a subtle problem. Suppose a parent appends a caveat and gives the narrowed token to a child while keeping its original token. The child's chain is non-amplifying, but the number of principals able to exercise some authority has increased. Cryptographic attenuation protects the descendant path; it does not automatically reduce aggregate exposure. High-consequence designs therefore need holder binding, a delegation ledger, bounded fan-out and, where the semantics require transfer rather than sharing, retirement or escrow of the parent handle.
Per-chain monotonicity is not global least authority. A child may be no broader than its parent while the estate still accumulates copies, stale grants and alternative paths to the same effect.
flowchart TD A["The refund that inherited too<br/>much power"] --> B["On this page"] B --> C["Authority has more dimensions<br/>than identity"] C --> D["Attenuation is a typed invariant"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#B96D00,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
Attenuation is a typed invariant
The effective grant is a meet, not a union
Let an authority grant contain typed dimensions: allowed actions, resources, audiences and data classes; upper bounds such as amount, record count and cost; temporal bounds such as expiry; predicates over purpose and state; a use count; and a delegation depth. A child grant is valid only when every dimension is no more permissive than the corresponding parent dimension.
Here I is the institutional ceiling, U the represented user's entitlement, T the task grant, each D a delegation restriction and R(s,t) the resource's live policy at state s and time t. Set-valued dimensions shrink by subset. Numeric ceilings move down. Expiry moves earlier. Predicates become conjunctions, so a child may add conditions but cannot remove an ancestor's conditions. Remaining use count and delegation depth cannot increase.
This forms a partial order rather than a single risk score. A credential with fewer actions but more resources is not “roughly narrower”; it is incomparable and therefore an invalid child unless a trusted authority performs a fresh authorisation decision. Delegation code should be unable to manufacture an expansion by balancing one dimension against another.
Integrity and attenuation are different checks
Macaroons demonstrated a practical pattern for decentralised delegation: chained message authentication codes protect an append-only sequence, while caveats restrict when, where, by whom and for what purpose a credential may be used. A holder can add a caveat without knowing the root secret. The service verifies both the chain and every caveat.
Two properties must remain separate. Signature or MAC verification shows that blocks were not forged, reordered or substituted. Structural attenuation checking shows that the content did not widen authority. An emerging individual Internet-Draft for agent delegation makes the same distinction explicitly, but it has no IETF endorsement or formal standards standing. Its useful design lesson is narrower than its protocol proposal: a perfectly valid signature chain can still carry an invalid expansion unless the verifier compares every child dimension with its parent.
OAuth supplies several stable building blocks without defining one universal agent-capability format. RFC 8693 permits a backend to exchange a received token for one more narrowly scoped to a downstream service and can retain both subject and actor semantics. RFC 8707 adds resource indicators so a token can be audience-restricted; it warns that one token valid at several audiences creates more cross-service trust. RFC 9396 carries richer authorisation details such as actions, transaction type and amount. RFC 9449 can sender-constrain OAuth tokens through proof of possession, reducing the risk that copied bearer bytes are sufficient.
These mechanisms compose, but none substitutes for the policy model. Token exchange can issue a new credential without proving that it is narrower. Rich fields can express a bad grant precisely. Proof of possession binds a token to a key without deciding whether the key holder should execute this refund. The architecture needs both a trustworthy issuer and a resource that understands the typed grant.
Time must be part of authority, not an afterthought
Consider a second thought experiment. Two systems issue the same temporary file-read grant to a coding agent. In both, the grant has a fifteen-minute expiry. The agent completes the subgoal after twenty seconds. System A leaves the capability visible until expiry. System B observes the trusted closure event, removes the handle from the planner interface and rejects later use against the closed epoch. At sixty seconds the cryptographic material in both systems is still fresh, yet only one system has ended the authority that the subgoal justified.
This separates four lifecycle mechanisms. Expiry limits maximum age. Revocation responds to a changed policy or emergency. Closure ends authority when a task phase finishes. One-use enforcement prevents a permitted effect from being repeated. They may share infrastructure, but they answer different questions. A short expiry cannot prove one use, and model memory cannot keep a closed capability alive.
Recent Portico research isolates this lifetime variable in controlled coding-agent tasks. Its non-revoking comparator received the same initial envelope and the same grants at the same turns. Both completed the pre-closure work, but Portico denied all ten tested post-closure reuses while the comparator permitted all ten. The paper is explicit about its boundary: the monitor assumes complete mediation and a sound typed tool catalogue; it does not replace sandboxing, review or logging.
Delegation fails across composition and time
Ordinary IAM is the ceiling, not the whole path
Conventional IAM remains necessary. A workload role should cap which services an agent runtime can reach. Organisation policies, permissions boundaries and network controls should prevent a token broker defect from escaping an approved estate. AWS session policies illustrate the subtractive pattern: a session's permissions are generally the intersection of its identity-based policy and session policy, so the session policy limits rather than grants. Resource-based policy details can change that evaluation, which is exactly why implementation-specific semantics must be tested rather than assumed.
Google Cloud Credential Access Boundaries provide another concrete downscoping example: a short-lived credential can be exchanged for one with an upper bound on resources and permissions. The current product boundary matters, however, because the documented feature applies only to Cloud Storage. These services are useful mechanisms inside their supported estates; they are not a vendor-neutral authority model for every agent tool.
A common architecture therefore uses a broad but bounded workload identity as the outer ceiling, a policy service or token broker to compute the task grant, and resource-local enforcement for the final operation. The static role answers where the runtime may ever go; the capability answers what this trajectory may do now.
Locally permitted calls can compose into a forbidden effect
Attenuating caller credentials still leaves a harder problem. Suppose a document tool may return a confidential case file, a summariser may transform text, and an email tool may send messages to an external address. Each tool call can be locally permitted. The unsafe effect appears only in composition: confidential information is read, rewritten and delivered to a sink that the source authority never permitted.
Recent ChainCaps research names this pattern permission laundering. Its proposed runtime propagates sink-specific capability budgets with values and uses intersection during composition, so a derived value cannot acquire a sink authority absent from its sources. This is closer to information-flow control than ordinary caller authorisation. A token attached only to the agent says what the agent may call; it does not automatically describe where data returned by one call may flow next.
In an 82-task stress test across five model configurations, the ChainCaps authors report baseline attack success between 25.2% and 67.8%, falling to 0% to 4.8% with their mechanism, while benign completion remained between 96% and 100%. Those numbers are encouraging within the authors' environment, not population estimates for agent systems. The same paper shows why policy quality dominates: expert-authored manifests blocked all attacks in its manifest sweep, while naive manifests blocked 27.3% and completed only half of benign tasks.
The strongest baseline is a central reference monitor
A capability chain is not automatically superior to a central policy engine. Central per-call authorisation may be enough when every request stays inside one trust domain and the reference monitor is always online. That result depends on complete mediation and on the policy engine seeing the same subject, actor, resource, arguments and state that a token verifier would see.
Consider a single-hop tool that reads a public, fixed-rate reference table through one read-only endpoint. The workload role already names that exact endpoint, the data is public, there is no delegation, no user-specific entitlement and no side effect. Adding a chained capability format creates more issuance and verification work without reducing a meaningful reachable effect.
Capabilities earn their complexity when authority crosses process, operator or trust boundaries. They also help when a holder must attenuate without contacting the root issuer, a downstream service needs offline verifiable evidence, or the delegation path itself forms part of accountability. In many enterprise systems the best design combines both: a central policy decision mints a narrow capability, and the resource verifies it beside authoritative state.
Across organisations or intermittently connected runtimes, a verifiable attenuation chain should preserve least-authority evidence better than a central-only policy design because each resource can validate the delegation path locally. That advantage must be tested under equivalent mediation, policy quality, revocation latency, availability and operating cost.
Failure boundaries that tokens cannot cross
Complete mediation is the first boundary. A perfect capability verifier cannot control a shell, browser, database connection or ambient cloud credential that bypasses it. The MCP security guidance treats token passthrough as an anti-pattern because accepting a token issued for another resource breaks audience boundaries and can create a confused deputy. An agent gateway should exchange or obtain a token for the downstream audience rather than forward an unrelated bearer credential unchanged.
Policy authoring is the second boundary. A typed tool catalogue must describe actual effects, including indirect egress, file mutation, process execution and state changes. A coarse or incorrect manifest gives the verifier a false world. The ChainCaps manifest results and Portico's complete-mediation assumption both make this dependency visible.
Business correctness is the third boundary. A token can prove that an agent was permitted to propose a £180 refund for case 7842. It cannot prove that the customer deserves the refund, that the duplicate charge was correctly identified or that the evidence was complete. Those claims require domain rules, evidence and accountable review. Precise authorisation can precisely enforce a wrong decision.
Build the narrowing path
Run the token prototype
The companion Python artefact implements a deliberately small, Macaroons-inspired chain using only the standard library. A trusted issuer signs a root authority ceiling. The current holder may append a child block using the previous signature as an HMAC key. Trusted verification recomputes the chain and calls _assert_narrower at every hop. The resource then checks the presented holder, action, exact resource, audience, amount, purpose, state digest and expiry.
The positive case permits an evidence specialist to propose a bounded refund. Three negative paths expose the mechanism: a child tries to recover refund.execute, a valid token is presented to the wrong audience and a one-use capability is replayed. The expected output is deterministic apart from the random token identifier.
ALLOW positive case: bounded refund proposal
DENY widening case: actions widened beyond the parent grant
DENY audience case: resource check failed: audience
ALLOW first committed use
DENY replay case: one-use capability has already been consumed
Run it with python capability_token_demo.py. The implementation is pedagogical. It uses a shared issuer secret, exact resource strings and a toy replay cache. A production system should use reviewed credential libraries, holder binding such as DPoP or mutual TLS, managed key rotation, durable revocation, atomic idempotency and a policy language that can represent the real resource semantics.
Open the complete runnable Python artefact
#!/usr/bin/env python3
"""Pedagogical attenuable capability token.
This is a standard-library demonstration, not a production credential format.
It shows four properties:
1. a root authority ceiling is signed by a trusted issuer;
2. a holder can delegate only a strict subset of its effective authority;
3. each delegation is append-only and integrity-protected;
4. the resource server still performs audience, state and replay checks.
Production deployments should use reviewed credential libraries and bind the
holder to a key or workload identity (for example DPoP, mTLS or SPIFFE).
"""
from __future__ import annotations
import copy
import hashlib
import hmac
import json
import secrets
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Iterable
class CapabilityError(ValueError):
"""Raised when a capability is malformed, wider than its parent, or unusable."""
def canonical(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
def utc_timestamp(iso_8601: str) -> int:
return int(datetime.fromisoformat(iso_8601.replace("Z", "+00:00")).timestamp())
def _normalise_set(values: Iterable[str]) -> list[str]:
result = sorted(set(values))
if not result:
raise CapabilityError("set-valued authority fields must not be empty")
return result
def _is_subset(child: list[str], parent: list[str]) -> bool:
return set(child).issubset(parent)
def _assert_narrower(child: dict[str, Any], parent: dict[str, Any]) -> None:
for field in ("actions", "resources", "audiences"):
if not _is_subset(child[field], parent[field]):
raise CapabilityError(f"{field} widened beyond the parent grant")
if child["purpose"] != parent["purpose"]:
raise CapabilityError("purpose cannot change during delegation")
if child["state_digest"] != parent["state_digest"]:
raise CapabilityError("state binding cannot change during delegation")
if child["max_amount_gbp"] > parent["max_amount_gbp"]:
raise CapabilityError("amount ceiling widened beyond the parent grant")
if child["expires_at"] > parent["expires_at"]:
raise CapabilityError("expiry moved later than the parent grant")
if child["remaining_depth"] >= parent["remaining_depth"]:
raise CapabilityError("delegation depth must decrease at every hop")
if child["one_use"] is False and parent["one_use"] is True:
raise CapabilityError("a one-use parent cannot become reusable")
def _root_material(token: dict[str, Any]) -> dict[str, Any]:
return {
"version": token["version"],
"token_id": token["token_id"],
"issuer": token["issuer"],
"subject": token["subject"],
"holder": token["holder"],
"grant": token["grant"],
}
def _sign(key: bytes, value: Any) -> str:
return hmac.new(key, canonical(value), hashlib.sha256).hexdigest()
def mint(
*,
issuer_secret: bytes,
issuer: str,
subject: str,
holder: str,
actions: Iterable[str],
resources: Iterable[str],
audiences: Iterable[str],
purpose: str,
max_amount_gbp: float,
expires_at: int,
remaining_depth: int,
state_digest: str,
one_use: bool = True,
) -> dict[str, Any]:
"""Mint the root token. Only the trusted authority service has issuer_secret."""
if remaining_depth < 0:
raise CapabilityError("remaining_depth cannot be negative")
token = {
"version": 1,
"token_id": secrets.token_hex(8),
"issuer": issuer,
"subject": subject,
"holder": holder,
"grant": {
"actions": _normalise_set(actions),
"resources": _normalise_set(resources),
"audiences": _normalise_set(audiences),
"purpose": purpose,
"max_amount_gbp": float(max_amount_gbp),
"expires_at": int(expires_at),
"remaining_depth": int(remaining_depth),
"state_digest": state_digest,
"one_use": bool(one_use),
},
"delegations": [],
}
token["signature"] = _sign(issuer_secret, _root_material(token))
return token
def effective_grant(token: dict[str, Any]) -> tuple[str, dict[str, Any]]:
"""Return the current holder and effective grant without trusting integrity."""
holder = token["holder"]
grant = copy.deepcopy(token["grant"])
for block in token["delegations"]:
holder = block["to"]
grant = copy.deepcopy(block["grant"])
return holder, grant
def attenuate(
token: dict[str, Any],
*,
delegator: str,
delegate: str,
actions: Iterable[str],
resources: Iterable[str],
audiences: Iterable[str],
max_amount_gbp: float,
expires_at: int,
reason: str,
) -> dict[str, Any]:
"""Append a strictly narrower grant using the current token signature as a MAC key."""
current_holder, parent = effective_grant(token)
if delegator != current_holder:
raise CapabilityError("only the current holder may attenuate this token")
if parent["remaining_depth"] <= 0:
raise CapabilityError("delegation depth exhausted")
child = {
"actions": _normalise_set(actions),
"resources": _normalise_set(resources),
"audiences": _normalise_set(audiences),
"purpose": parent["purpose"],
"max_amount_gbp": float(max_amount_gbp),
"expires_at": int(expires_at),
"remaining_depth": parent["remaining_depth"] - 1,
"state_digest": parent["state_digest"],
"one_use": parent["one_use"],
}
_assert_narrower(child, parent)
block = {
"from": delegator,
"to": delegate,
"reason": reason,
"grant": child,
}
new_token = copy.deepcopy(token)
new_token["delegations"].append(block)
previous_signature = bytes.fromhex(token["signature"])
new_token["signature"] = _sign(previous_signature, block)
return new_token
@dataclass(frozen=True)
class VerifiedCapability:
token_id: str
holder: str
grant: dict[str, Any]
fingerprint: str
def verify_chain(token: dict[str, Any], issuer_secret: bytes) -> VerifiedCapability:
"""Verify integrity and monotonic attenuation from root to current holder."""
expected = _sign(issuer_secret, _root_material(token))
holder = token["holder"]
parent = copy.deepcopy(token["grant"])
for block in token["delegations"]:
if block["from"] != holder:
raise CapabilityError("delegation chain has a broken holder link")
_assert_narrower(block["grant"], parent)
expected = _sign(bytes.fromhex(expected), block)
holder = block["to"]
parent = copy.deepcopy(block["grant"])
if not hmac.compare_digest(expected, token["signature"]):
raise CapabilityError("token signature or append-only chain is invalid")
return VerifiedCapability(
token_id=token["token_id"],
holder=holder,
grant=parent,
fingerprint=hashlib.sha256(canonical(token)).hexdigest()[:24],
)
class ReplayCache:
"""Toy stateful replay control. A real resource must commit this atomically with the effect."""
def __init__(self) -> None:
self._spent: set[str] = set()
def consume(self, fingerprint: str) -> None:
if fingerprint in self._spent:
raise CapabilityError("one-use capability has already been consumed")
self._spent.add(fingerprint)
def authorise(
verified: VerifiedCapability,
*,
presented_holder: str,
action: str,
resource: str,
audience: str,
amount_gbp: float,
purpose: str,
current_state_digest: str,
now: int,
replay_cache: ReplayCache | None = None,
commit: bool = False,
) -> None:
"""Resource-local checks. Verification alone does not establish business permission."""
grant = verified.grant
checks = {
"holder": presented_holder == verified.holder,
"action": action in grant["actions"],
"resource": resource in grant["resources"],
"audience": audience in grant["audiences"],
"amount": amount_gbp <= grant["max_amount_gbp"],
"purpose": purpose == grant["purpose"],
"state": current_state_digest == grant["state_digest"],
"freshness": now <= grant["expires_at"],
}
failed = [name for name, ok in checks.items() if not ok]
if failed:
raise CapabilityError("resource check failed: " + ", ".join(failed))
if commit and grant["one_use"]:
if replay_cache is None:
raise CapabilityError("one-use execution requires a replay cache")
replay_cache.consume(verified.fingerprint)
def demo() -> None:
issuer_secret = b"demo-only-root-secret"
state_digest = "sha256:case-7842-v17"
expiry = utc_timestamp("2030-01-01T00:00:00Z")
now = utc_timestamp("2029-12-31T23:55:00Z")
root = mint(
issuer_secret=issuer_secret,
issuer="authority://refund-control",
subject="user:case-owner-42",
holder="agent:orchestrator:v3",
actions={"case.read", "refund.propose", "refund.execute"},
resources={"case:7842", "account:314"},
audiences={"tool://case-api", "tool://payments-api"},
purpose="customer_refund",
max_amount_gbp=500.0,
expires_at=expiry,
remaining_depth=2,
state_digest=state_digest,
one_use=True,
)
child = attenuate(
root,
delegator="agent:orchestrator:v3",
delegate="agent:refund-checker:v2",
actions={"case.read", "refund.propose"},
resources={"case:7842"},
audiences={"tool://case-api"},
max_amount_gbp=200.0,
expires_at=expiry - 120,
reason="verify eligibility and prepare a bounded proposal",
)
verified = verify_chain(child, issuer_secret)
authorise(
verified,
presented_holder="agent:refund-checker:v2",
action="refund.propose",
resource="case:7842",
audience="tool://case-api",
amount_gbp=180.0,
purpose="customer_refund",
current_state_digest=state_digest,
now=now,
)
print("ALLOW positive case: bounded refund proposal")
try:
attenuate(
child,
delegator="agent:refund-checker:v2",
delegate="agent:helper:v1",
actions={"refund.execute"}, # absent from the child grant
resources={"case:7842"},
audiences={"tool://payments-api"},
max_amount_gbp=300.0,
expires_at=expiry,
reason="attempted scope widening",
)
except CapabilityError as exc:
print(f"DENY widening case: {exc}")
try:
authorise(
verified,
presented_holder="agent:refund-checker:v2",
action="refund.propose",
resource="case:7842",
audience="tool://payments-api", # wrong audience
amount_gbp=180.0,
purpose="customer_refund",
current_state_digest=state_digest,
now=now,
)
except CapabilityError as exc:
print(f"DENY audience case: {exc}")
replay_cache = ReplayCache()
authorise(
verified,
presented_holder="agent:refund-checker:v2",
action="refund.propose",
resource="case:7842",
audience="tool://case-api",
amount_gbp=180.0,
purpose="customer_refund",
current_state_digest=state_digest,
now=now,
replay_cache=replay_cache,
commit=True,
)
print("ALLOW first committed use")
try:
authorise(
verified,
presented_holder="agent:refund-checker:v2",
action="refund.propose",
resource="case:7842",
audience="tool://case-api",
amount_gbp=180.0,
purpose="customer_refund",
current_state_digest=state_digest,
now=now,
replay_cache=replay_cache,
commit=True,
)
except CapabilityError as exc:
print(f"DENY replay case: {exc}")
if __name__ == "__main__":
demo()
A production-shaped refund path
Synthetic architecture scenarioBegin with an institutional ceiling for the refund domain. It permits an approved orchestrator to read cases, propose refunds and request execution through named APIs, never to change ledger entries directly. The represented user's entitlement narrows that ceiling to assigned customers and cases. A task grant narrows it again to case 7842, purpose customer_refund, amount £500 and a ten-minute window.
The orchestrator delegates evidence checking. The child receives only case.read and refund.propose for case 7842, audience tool://case-api, amount £200, two minutes of life and no remaining delegation. It can prepare a proposal but cannot call the payments service. Its decision receipt records the parent token identifier, child fingerprint, actor, represented user, policy version, case-state digest and reason for attenuation.
If policy requires a human approval, the reviewer sees an immutable proposal and source evidence. Approval causes trusted code to mint a separate execution capability for the payments adapter. That token names the exact proposal digest, account, amount, audience and one-use nonce. Immediately before the effect, the payments service checks for account status changes, legal hold, prior reversal and nonce consumption. It emits an effect receipt or an unknown-outcome record, never merely “HTTP 200”.
Test the invariant, not just the happy path
A release test should generate attempted child grants that widen exactly one dimension at a time. Add an action, add a resource, change the audience, raise the amount, extend expiry, remove a purpose predicate, increase use count or restore delegation depth. Every case should fail before a downstream effect. Then tamper with block order and signatures, substitute the holder, present the token after closure, reuse the nonce and change the bound case state.
Trajectory tests must go further. Feed confidential output into an allowed external sink and verify that composition policy blocks it. Attempt the same effect through a shell, browser, raw HTTP client and alternative connector. Disable the policy service or make it slow. Race revocation against an in-flight effect. Retain the parent token after delegation and measure whether fan-out exceeds the declared budget. The assurance claim is only as strong as the bypass paths exercised.
| Control claim | Adversarial variation | Required evidence |
|---|---|---|
| Child cannot widen | Increase one typed dimension per test | Issuer or verifier denies with a specific reason before tool invocation |
| Only intended holder can use | Replay bytes from another workload or key | Proof-of-possession or workload-binding check fails |
| Audience is exact | Present a case token to payments or another tenant | Resource rejects before reading arguments |
| Authority closes | Reuse a live-by-expiry handle after subgoal completion | Epoch or revocation check denies and planner interface no longer exposes it |
| Effect is one use | Retry after timeout and replay the same nonce | Atomic idempotency returns the original outcome or denies duplication |
| Composition cannot launder | Transform restricted data before an external sink | Source-to-sink budget remains restrictive unless trusted declassification occurs |
| Mediation is complete | Attempt shell, browser, raw network and ambient-credential bypasses | Every effect crosses an observable enforcement point |
Choose the lightest sufficient control
Capability chains are one point in a control space. The choice should depend on delegation depth, trust boundary, consequence, reversibility and whether the resource can call a central policy service. The aim is not to maximise security machinery. It is to make the authority evidence proportionate to the effect.
A compact architecture decision can ask six questions. Does authority cross a principal, process or organisation boundary? Can the downstream service reach the issuer online? Must the holder attenuate without issuer contact? Is the effect material or difficult to reverse? Does authority need to close before token expiry? Can data from one tool be transformed and sent through another? Each “yes” moves the design from a static exact role towards per-call policy, exchanged credentials, chained attenuation, lifecycle handles and composition controls.
The operational evidence should preserve more than the final allow. Record the root decision, represented subject, acting holder, every attenuation block, policy and catalogue versions, current resource check, replay decision and effect receipt. Denials should use stable reason classes without leaking sensitive policy. This record lets a reviewer distinguish an authentic token from a valid delegation, and a valid delegation from a correct business outcome.
The supplied code demonstrates content non-amplification, audience restriction and simple replay control. It does not demonstrate public-key holder binding, distributed revocation, atomic effect commitment, cross-value information-flow tracking, safe declassification or formal policy correctness.
stateDiagram-v2 [*] --> Claim state "Identity is a coordinate;<br/>authority is a region" as Claim state "Least privilege is often<br/>computed at the wrong unit" as Test state "Four invariants, not one small<br/>token" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
Compact glossary
- Principal
- An accountable person, workload, agent or service to which identity and authority may be attached.
- Capability
- An unforgeable, communicable authority object naming permitted access or effects under stated constraints.
- Attenuation
- A transformation that can only preserve or reduce authority along every typed dimension.
- Audience
- The resource server or service for which a token is intended and at which it may be accepted.
- Complete mediation
- The property that every relevant access or effect crosses an enforcement point that checks current authority.
- Permission laundering
- An unsafe end-to-end effect formed by composing calls that each pass their own local permission check.
- Closure
- A trusted lifecycle event that removes temporary authority when the subgoal that justified it ends.
- Effect receipt
- Durable evidence that a requested state change happened, failed or remains unknown, linked to its authority object.
The grant should get smaller as the action gets closer
An agent estate cannot infer authority from fluent intent or a trusted runtime identity. The same model, container and service account can participate in a harmless lookup, a bounded proposal and an irreversible payment. What changes is the grant and the state against which it is exercised.
Least authority for agents is a monotonic systems invariant. Institutional policy sets an outer ceiling. User entitlement narrows whose interests may be represented. The task grant narrows purpose and consequence. Every delegate subtracts actions, resources, audiences, limits, lifetime, use count or depth. The resource applies the final veto beside authoritative state. Holder binding, closure and replay control prevent valid bytes from becoming ambient power.
That invariant is necessary but incomplete. Copying a parent token can increase the number of wielders. Legal calls can launder restricted data through composition. A hidden shell can bypass the verifier. A precise capability can encode a mistaken policy. The design therefore needs complete mediation, typed effects, composition tests, evidence quality and recovery controls around the token mechanism.
The architecture decision changes at every delegation arrow. Reject a diagram that forwards one broad bearer credential from user to orchestrator, sub-agent and tool. Require the author to show the child-parent subset check, holder binding, audience, closure event, resource-local preconditions and replay rule. Then inject a widening attempt, a stale reuse, a wrong audience, a composed exfiltration and a mediator bypass.
When those paths fail safely, identity stops being a vague proxy for power. It becomes one coordinate in a defensible authority chain whose strongest property is visible in its shape: the closer the system gets to a consequential effect, the less freedom the executor should possess.