TLDR

  1. 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.
  2. 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.
  3. The tool executes only the intersection of institutional policy, represented-user entitlement, task authority, delegate caveats and current resource state.
  4. 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.
  5. 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 scenario

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

Part I

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.

The same identity can carry radically different authority Two paths start with the same attested refund checker identity. The left path receives a forwarded broad token and can reach both case and payments systems. The right path receives a downscoped token and can reach only one case proposal operation. agent:refund-checker:v2 same attested workload identity Forwarded broad token case.read · refund.propose · refund.execute case API + payments API · £500 Exchanged child capability case.read · refund.propose case:7842 · case API · £200 Case API allowed Payments API also reachable Case API proposal only Payments API rejects wrong audience and action
Figure 1. Hold identity constant and vary only the grant. Illustrative thought experiment. The difference in reachable effect comes from authority, not from authentication quality.
Readable key: same identity. The broad token reaches both case and payments services; the exchanged child reaches only the case-proposal operation.

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.

A narrowing capability cone from institution to effect Five progressively narrower trapezoids show institutional ceiling, represented-user entitlement, task grant, sub-agent grant and final tool permission. Each layer removes actions, resources, audiences, time or delegation depth. Institutional ceiling approved services, actions, data zones and maximum consequences Represented-user entitlement portfolio, customer relationship, consent and mandate Task grant one case, purpose, amount, expiry and approved tools Sub-agent capability proposal only, exact case, shorter life, no further delegation Final executable effect resource-local policy still has veto fewer reachable effects at each handoff
Figure 2. The narrowing capability cone. Practitioner synthesis. The cone is multi-dimensional; its width represents reachable effects rather than a measured percentage.
Readable key: institutional ceiling → represented-user entitlement → task grant → sub-agent capability → final executable effect.

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.

Sharp distinction

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.

Part II
The argument in one passChart · flow
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
A structural route through Capabilities, Scoped Identity and Least Authority, from The refund that inherited too much power to Source ledger. Reading use: follow the relation, then test it against the surrounding evidence and limits.

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.

Ak(s,t) = I ∩ U ∩ T ∩ D1 ∩ … ∩ Dk ∩ R(s,t)

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.

Executable authority is the intersection of five independent constraints Five translucent vertical lenses overlap over a narrow central region labelled executable authority. The lenses are institutional ceiling, user entitlement, task grant, delegate caveats and live resource policy. EXECUTABLE AUTHORITY all five must allow Institution User Task Delegate Resource state A permit from one layer cannot cancel a deny from another
Figure 3. The effective-authority meet. Conceptual model. The overlap is qualitative, not a measured area; it shows logical conjunction across independently owned constraints.
Readable key: institution ∩ user entitlement ∩ task grant ∩ delegate caveats ∩ live resource policy.

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.

An attenuable capability token has four different evidence zones A vertical token specimen is divided into identity, grant, lifecycle controls and evidence. An append-only delegation block narrows actions, resources, audience, amount, expiry and depth. IDENTITY issuer · subject · actor/holder · key binding GRANT actions ⊆ parent.actions resources ⊆ parent.resources audiences ⊆ parent.audiences amount ≤ parent.amount · purpose unchanged LIFECYCLE CONTROLS expiry ≤ parent.expiry · uses ≤ parent.uses depth = parent.depth − 1 · revocation epoch EVIDENCE AND BINDING state digest · policy version · approval · nonce append-only chain signature / MAC each block can only subtract The model may request a grant; trusted code mints and verifies it.
Figure 4. Token anatomy as an enforceable contract. Practitioner synthesis. Field names are illustrative; production formats should use reviewed standards and libraries.
Readable key: identity | grant | lifecycle controls | evidence and binding. Each delegation block may only subtract.

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.

Authority should collapse at subgoal closure, not merely at token expiry A time chart compares lingering authority, which remains available until fifteen-minute expiry, with closed authority, which drops to zero when the subgoal completes after twenty seconds. usable authority time after grant subgoal closes at 20 s token expires at 15 min Lingering grant remains callable Epoch-bound grant Same model memory, different executable state closed handle is absent and rejected on replay
Figure 5. Authority lifetime follows the subgoal. Illustrative relationship. The fifteen-minute expiry and twenty-second closure are synthetic values chosen to isolate the mechanism.
Readable key: the lingering grant remains callable until fifteen-minute expiry; the epoch-bound grant becomes unusable when the subgoal closes at twenty seconds.
Part III

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.

Permission laundering across three locally permitted tools A confidential document flows through a permitted read and summarisation into an external send. Local checks are green, but an end-to-end sink budget blocks the final call because the source data did not permit external egress. Case document classification: confidential sink budget: internal only Summariser read is allowed derived text keeps source budget External mail tool send API is allowed external sink is forbidden ✓ ✓ End-to-end capability budget required sink ∉ source budget → deny before egress explicit declassification would need a separate trusted decision Three correct local decisions can still produce one incorrect trajectory.
Figure 6. The composition gap. Synthetic example adapted to the permission-laundering mechanism described by ChainCaps. No deployment claim is implied.
Readable key: confidential source → permitted summariser → external mail. The final sink is denied because the source budget allows internal use only.

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.

Reported attack success before and after ChainCaps in an 82-task stress test For five model configurations, paired horizontal bars show attack success without ChainCaps from 25.2 to 67.8 percent and with ChainCaps from zero to 4.8 percent. Attack success rate (%) 020406080 Sonnet 4 Sonnet 4.6 Opus 4.6 GPT-5.1 Qwen 3.5 67.84.8 25.23.6 31.53.6 53.62.4 53.00.0 without ChainCaps with ChainCaps
Figure 7. Reported ChainCaps stress-test results. Sourced measurements from 82 tasks with three runs per condition in the authors' evaluation. Bars show attack success rate; they should not be generalised beyond that setup. Source: ChainCaps.
Readable key, attack success without/with ChainCaps: 67.8/4.8, 25.2/3.6, 31.5/3.6, 53.6/2.4 and 53.0/0.0 per cent.

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.

Negative control

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.

Open hypothesis

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.

Part IV

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 scenario

Begin 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”.

A production-shaped refund path with separate proposal and execution authority A sequence begins with institutional and user constraints, then a task grant to an orchestrator, a narrower proposal grant to a checker, human approval of an immutable proposal and a separate one-use execution grant to the payments adapter. Denied side paths show scope widening and wrong audience. Institutional ceiling ∩ represented-user entitlement approved refund estate · assigned case · maximum £500 Task grant to orchestrator case 7842 · propose/request execute · 10 min Child grant to refund checker read + propose · case API · £200 · 2 min · depth 0 emits immutable proposal + decision receipt DENY scope widening refund.execute absent in parent Human approval sign exact proposal digest One-use execution capability payments adapter · exact amount/account · live recheck DENY wrong audience case token at payments API Proposal authority and execution authority are separate objects.
Figure 8. A synthetic regulated refund path. Worked scenario, not a disclosed deployment. The shape separates model proposal, human decision, capability issuance and resource-local effect verification.
Readable key: ceiling → task grant → proposal-only child → human approval → one-use execution. Scope-widening and wrong-audience paths are denied.

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.

Least-authority release protocol
Control claimAdversarial variationRequired evidence
Child cannot widenIncrease one typed dimension per testIssuer or verifier denies with a specific reason before tool invocation
Only intended holder can useReplay bytes from another workload or keyProof-of-possession or workload-binding check fails
Audience is exactPresent a case token to payments or another tenantResource rejects before reading arguments
Authority closesReuse a live-by-expiry handle after subgoal completionEpoch or revocation check denies and planner interface no longer exposes it
Effect is one useRetry after timeout and replay the same nonceAtomic idempotency returns the original outcome or denies duplication
Composition cannot launderTransform restricted data before an external sinkSource-to-sink budget remains restrictive unless trusted declassification occurs
Mediation is completeAttempt shell, browser, raw network and ambient-credential bypassesEvery 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.

Decision map for choosing central policy, exchanged tokens or chained capabilities A two-axis map uses delegation and trust-boundary complexity horizontally and consequence or irreversibility vertically. Four regions recommend static exact role, central per-call policy, exchanged holder-bound token, or chained capability with approval, closure and local veto. delegation depth and trust-boundary complexity consequence and irreversibility Exact workload role single hop · public/read only no user delegation or side effect Central per-call policy online reference monitor complete mediation in one domain Exchanged, holder-bound token narrow audience, resource and limits local state and replay check Chained capability lifecycle multi-hop attenuation + holder binding approval, closure, composition and local veto controls accumulate only as the effect demands
Figure 9. Control choice by delegation and consequence. Practitioner decision instrument. The regions are qualitative; process owners should set thresholds from their own harm, reversibility and recovery analysis.
Readable key: exact role for a single low-risk hop; central per-call policy inside one mediated domain; exchanged or chained capabilities as delegation and consequence rise.

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.

Prototype boundary

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.

How the claim changes under pressureChart · state
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 --> [*]
The revision loop begins with Identity is a coordinate, authority is a… and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then test it against the surrounding evidence and limits.

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.