A multi-agent run leaves behind two structures. The first is the call graph — which component invoked which, with what arguments, in what order — and modern tracing reconstructs it well enough that most teams have stopped thinking about it. The second is the authority graph: for each of those calls, on whose authority it was made, what that authority permitted, and who narrowed it on the way down. The companion teardown to this piece — Your traces show the call graph. Your customer needs the authority graph. — walks the evidence that the second structure is not recoverable from the first, in any framework or protocol currently shipping. I am going to take that as established and design against it.
The design that follows is the one I would build, and the reference implementation for it is written out below in full rather than described. I am writing it out in the form an implementer needs rather than the form a conference talk needs, which means the second half of this piece is about what it fails to do.
Four premises
Everything below follows from four statements. I want them separated from the design because a reader who rejects one of them should be able to say which, rather than arguing with the consequences.
One: authority must be carried, not ambient. The dominant failure in agent systems today is that a component acts under authority it happens to possess rather than authority it was handed for the purpose. A process holds a credential in its environment; a sub-agent inherits the parent's HTTP client; a tool call runs under the session's token because that is what the session had. In every one of these the authority is a property of where the code is running rather than what it was asked to do. That is ambient authority, and it has the property that nothing in the request identifies the reason the request was permitted.
Two: every delegation must produce a strictly weaker credential that names its parent. If handing work to a sub-agent produces no new credential, there is no edge to record and no boundary to enforce; the sub-agent simply is the parent, from the resource server's point of view. If it produces a credential that does not name its parent, the record has nodes and no edges — a list of grants with no ancestry, which answers who acted and never answers on whose authority. Naming the parent is what turns a set of credentials into a graph.
Three: the graph must be verifiable offline, without replaying the run. This is where a platform's constraints bite hardest, and I will come back to it. The short version: a verifier that must call an issuer to check a credential has coupled every request path to the availability of a central service, and a verifier that must reconstruct the run from telemetry has made a control decision depend on a sampled, best-effort data stream. Both are unacceptable at platform scale. The credential presented at the boundary has to be sufficient on its own.
Four: attenuation must be monotone. A child can never hold more than its parent. Not as a rule enforced by a policy engine, and not as an invariant a reviewer checks — as a property of the only constructor available. This is the premise that does the most work, because it converts over-delegation from a mistake somebody has to avoid into a state the type system cannot produce.
The strongest objection to the whole programme lands here, and it lands hard, so let me put it before the design rather than after it.
You are proposing to carry a credential on every hop of a system whose defining characteristic is that the hops are decided at runtime by a model. You will not know the shape of the delegation until it happens. Any narrowing you perform will therefore be either so generous that it is decorative, or so tight that the agent fails at the first thing it legitimately needed to do — and the operator, at three in the morning, will widen the root grant until the failures stop.
That objection is correct about the failure mode and I have no clean answer to it. What I have is a claim about where the failure becomes visible. In an ambient-authority system, the operator who widens the grant produces no artifact: the system simply stops failing, and the widening is discoverable only by reading the deployment history of a config file. In a carried-authority system, the widening is a root grant with a broader authority set, signed, timestamped, and sitting at the head of every chain issued afterwards. It does not prevent the three-in-the-morning decision. It makes the decision a thing rather than an absence, and my whole argument for this design is that the difference between a thing and an absence is most of what governance is.
What the premises force
Take the four in order and ask what each rules out.
Carried, not ambient rules out the pattern where a sub-agent shares a process, an environment, or an HTTP client with its parent. The credential has to be a value that is passed as an argument, which means it has to be serialisable, which means it has to fit in a header. This is not a small consequence — it sets a size budget that will constrain the data model, and it is the reason chain length turns up later as a real operational cost rather than a footnote.
Names its parent rules out any credential format in which ancestry is optional metadata. It also rules out the obvious shortcut of putting the parent's identifier in a claim that the child could choose not to include, or could set to something else. The link has to be structural — the child's validity has to depend on the parent's, cryptographically, so that a child with a forged or absent parent does not verify.
Verifiable offline rules out every design in which the authoritative answer lives in a central service. No introspection endpoint on the hot path. No policy-decision-point round trip. No lookup of the parent grant to check that it existed. Everything the verifier needs must be in the bundle it was handed, plus one long-lived per-tenant public key it can cache. For a platform this is not a preference. A platform cannot see inside its customers' policy, cannot be on the critical path of their internal authorization decisions, and cannot make a customer's agent latency a function of the platform's control-plane availability. Offline verification matters here more than it does in a single-organisation deployment, because in a single-organisation deployment a central service is at least possible.
Monotone rules out the design almost everyone reaches for first: a request-and-issue flow, where the child says what it wants and something decides whether to grant it. That flow can produce a child holding more than its parent whenever the deciding component is wrong, misconfigured, or persuaded. The alternative is that the child does not request authority at all. It requests a restriction — it names which of the parent's authority to keep — and the constructor computes the intersection.
It is worth being precise about what monotone buys and what it does not, because the word gets used loosely. Monotone here means non-increasing: for every grant g with parent p, the authority of g is a subset of the authority of p. It does not mean strictly decreasing. A hop that narrows nothing produces a perfectly valid child that happens to be identical in authority to its parent, and a chain of such pass-throughs is legal. You can require a strict narrowing at every hop, and this design can carry it as a policy — but it costs a real decision at every pass-through, including the ones that are genuinely just plumbing, and my experience of controls that impose a decision on plumbing is that they get switched off within a quarter. I would rather have a monotone chain that is honestly non-increasing than a strict one that is honestly disabled.
Where the existing standards stop
Almost every piece of this exists somewhere. The reason it has to be assembled is that no single standard carries all four premises, and two of the most relevant ones explicitly decline to.
RFC 8693 has the vocabulary and refuses the invariant. OAuth 2.0 Token Exchange draws exactly the distinction this design needs. Under impersonation, "principal A is given all the rights that B has within some defined rights context and is indistinguishable from B in that context"; under delegation, "principal A still has its own identity separate from B, and it is explicitly understood that while B may have delegated some of its rights to A, any actions taken are being taken by A representing B." That second sentence is premise two, written in 2020. The RFC even defines the chain: nested act claims, where "the outermost 'act' claim represents the current actor while nested 'act' claims represent prior actors," forming what it calls a history trail connecting the initial request through the delegation steps to the current actor.
And then it makes that trail non-load-bearing, in normative language: "For the purpose of applying access control policy, the consumer of a token MUST only consider the token's top-level claims and the party identified as the current actor by the 'act' claim. Prior actors identified by any nested 'act' claims are informational only and are not to be considered in access control decisions." The chain is auditable history. It is not a verifiable constraint.
Nor does the RFC require narrowing. The client gets audience, resource and scope request parameters with which to indicate the token it wants, and the response scope parameter is "OPTIONAL if the scope of the issued security token is identical to the scope requested by the client; otherwise, it is REQUIRED" — which anticipates a different issued scope without mandating a smaller one. Whether a child is weaker than its parent is an authorization-server policy decision. So token exchange gives you the request parameters with which a server may attenuate, and leaves monotonicity to whoever wrote that server. It is also explicit that lifecycles do not couple: "the exchange is a one-time event and does not create a tight linkage between the input and output tokens," so revoking a parent does not, by the protocol, revoke its descendants.
The nearest thing in the spec to a checkable delegation edge is the optional may_act claim, which "makes a statement that one party is authorized to become the actor and act on behalf of another party" and can be used by the authorization server to decide whether the requested delegation is permitted. It is the right shape. It is also optional, evaluated at the server rather than at the resource, and says nothing about how much authority may cross the edge.
Macaroons have the invariant and were never adopted as infrastructure. The 2014 macaroons paper publishes the construction this design uses. A macaroon's chain of HMAC values "is derived by computing, for each message, a keyed HMAC value, nested so that each such value itself is used as the HMAC key for the next message," with the first message required to be "a public, opaque key identifier that maps to a secret root key known only to the target service." The consequence is the one that matters: a holder possesses only the terminal signature, which is enough to extend the chain with further caveats and not enough to recompute an earlier, less-constrained one. The paper describes a derived credential in precisely these terms — one that "has been derived as a strict attenuation of another, more permissive macaroon" by eliding that macaroon's signature and adding caveats. And it does this without a round trip: "by adding caveats, new, more restricted authorization credentials can be directly derived from macaroons, without proxying requests to the target service, or re-minting new credentials from it."
That is premises two, three and four in one construction, published twelve years ago. What the paper reports is a prototype and its measurements; I am not aware of a primary source establishing macaroons as prevailing practice in any named commercial cloud, and I would cite them as a design precedent with a published construction rather than as an industry default. The construction is what I am borrowing, and it is worth understanding why it was available for a decade without being taken up: it solves a problem that did not hurt very much when the delegating parties were programs written by the same team. Runtime-decided delegation between agents is the thing that makes it hurt.
The paper also gives third-party caveats, which turn a delegation into a verifiable obligation on a named external principal: a third-party caveat carries a root key and assertion encrypted for the discharging service, a hint to its location, and the same key encrypted for the target service so it can verify. Such caveats, the authors note, "can ensure that each request authorized by a macaroon is subject to any number of extra steps for authentication, authorization, or audit." That is the mechanism this design uses for the case where a platform must condition a delegation on something only the customer can evaluate.
SPIFFE standardizes the name and stops there, deliberately. A SPIFFE ID is "an RFC 3986 compliant URI comprising a 'trust domain name' and an associated path," and an SVID is "the document with which a workload proves its identity to a resource or caller." That is naming, issuance and verification — authentication, not authorization, and the standard is clear that it defines no privilege or scope semantics. The place it comes closest to delegation is SPIRE's Delegated Identity API, and the project documents that path as impersonation in its own words: it "explicitly and by-design grants the authorized delegate workload the ability to impersonate any of the other workloads it can obtain SVIDs for," and such a delegate "may impersonate and act on behalf of all workload SVIDs it obtains from the SPIRE Agent." Authorization is an allowlist of delegate IDs, and the SVIDs handed back are the full identities of the delegated-for workloads. Nothing there is an attenuated derivative naming a parent, and it would be a factual error to say SPIFFE solves agent delegation. It solves the layer beneath, well, and this design sits on top of it.
The agent protocols draw a boundary rule and a blank. The Model Context Protocol's authorization specification bans ambient token forwarding in normative language: "MCP servers MUST validate that access tokens were issued specifically for them as the intended audience," they "MUST NOT accept or transit any other tokens," and for upstream calls the token "is a separate token, issued by the upstream authorization server. The MCP server MUST NOT pass through the token it received from the MCP client." It grounds this in RFC 8707 resource indicators and RFC 9728 protected-resource metadata, and names the failure it prevents as the confused deputy. That is a real and valuable rule, and it is a boundary rule: it says what must not cross, not what the authority behind a call is. A2A goes the other way and says so plainly — "A2A protocol payloads, such as JSON-RPC messages, don't carry user or client identity information directly. Identity is established at the transport/HTTP layer," credentials are obtained "through processes external to the A2A protocol itself," and "authorization logic is specific to the agent's implementation." Combined with the observation that "agents are typically opaque because they don't share internal memory, tools, or direct resource access with each other," the authority behind a cross-agent call is unrecoverable from the protocol alone.
One more, because it is the constraint this design has to satisfy rather than a gap it fills. The OpenTelemetry GenAI semantic conventions define sixty-three attributes in the gen_ai namespace, every one of them at Development status. They cover agent metadata, conversation and workflow identifiers, prompts, messages, retrieval, memory, evaluation, tool calls and token usage. The span conventions enumerate eighteen operation names, among them create_agent, invoke_agent, execute_tool, chat, plan and invoke_workflow, and that vocabulary is what makes the call graph recoverable. Not one attribute names a credential, a principal, a scope, a permission, an authorization decision, or a delegating parent. Every attribute whose name contains token is a model-token attribute rather than a security-token one: gen_ai.token.type, whose values are input and output, together with gen_ai.request.max_tokens and the gen_ai.usage counters.
I want to be careful about how that last observation is read. The absence is the finding; I have located no primary statement that the project considered and declined such attributes, and it would be wrong to characterise it as a refusal. The conventions are at Development status and moving. What is true today is that a system emitting perfectly conformant GenAI telemetry produces no field in which the authority behind a call could be recorded — and that is a fact about what your traces contain, not a criticism of the people writing the spec.
And on frameworks: in the OpenAI Agents SDK, a handoff transfers conversational context by default. "When a handoff occurs, it's as though the new agent takes over the conversation, and gets to see the entire previous conversation history," and the documented controls over what crosses the boundary are input filters and nested-history settings — "the receiving agent still sees the conversation history unless you change it with an input_filter or nested handoff history settings." The handoff surface exposes no credential, scope or permission parameter. What a given deployment's tools then do about credentials is an implementation property of that deployment, not a documented framework behaviour, and I am not going to assert anything about it.
The data model
A grant is the only thing that travels. It carries an authority set, a set of caveats, a pointer to its parent, and a seal that binds it to the chain above it. The authority set is deliberately thin — a list of action-and-resource pairs — because the platform must not need to understand a customer's resource namespace to compute an attenuation. That is why resource-pattern containment is an injected function rather than a parser in the library. The package compares patterns using a predicate the caller supplies; it never interprets one.
Three properties of the model are load-bearing and easy to miss:
- The constructor computes a meet. attenuate takes a parent grant and a restriction request and returns the intersection. There is no function in the reference implementation that produces a grant with more authority than its input. Widening is not an operation that is refused; it is an operation that does not exist.
- The seal is keyed by the parent's seal. This is the macaroon construction. The holder of a child has the terminal MAC value and cannot invert it to recover the parent's, so it can extend the chain downward and cannot climb it.
- The verifier recomputes the authority rather than reading it. The authority field on a grant is a convenience for the holder and for the record. The verifier does not trust it: it walks the chain from the root and recomputes the running meet itself, so a grant whose stated authority disagrees with what the chain actually permits is evaluated on the chain, not on the claim. Without this, the whole invariant rests on every minting party being honest, and one of the minting parties is running inside a model's tool loop.
The carried credential — the reference implementation
The core of the reference implementation. Four files: the types and the constructor, the offline verifier, the record emitted alongside the trace, and the property test that is the reason to believe any of it. Crypto, identifier generation and resource-pattern containment are all injected, because a platform library must not choose a customer's key management, and must not parse a customer's namespace.
The identifier generator, MAC implementation and root-key resolution are injected throughout. That is not style: a platform library that chooses a customer's key management has made a decision it has no standing to make, and a library that ships its own randomness is a library that has to be trusted about its randomness.
The control path
Six steps, from a human's decision to a resource server's answer. The architecture beneath them is the five bands in the figure that follows.
- Root issuance. A root grant is minted for a trust domain, signed with the tenant's asymmetric key, and bound to a named human or a named scheduled mandate. It is the only grant in the system with a null parent, and it is the only place authority enters. Its authority set should be the smallest thing that makes the workload possible, and everything that goes wrong later goes wrong worse if it is not.
- Delegation. When a component hands work to another — a sub-agent, a tool, a queued job — it calls attenuate with the subset of its own authority the receiver needs and any caveats it wants to add. It gets back a new grant naming it as issuer. It cannot get back anything larger, because the constructor does not produce larger things.
- Emission. The new grant produces one row in the authority record: grant id, parent id, both principals, the effective authority, the caveat kinds, the seal, and — if a span happens to be active — a trace and span id. The row is written whether or not tracing is on.
- Carriage. The chain travels on the request as a header, alongside the access token the resource already requires. It does not replace that token. A resource server that follows MCP's audience rule still gets an audience-bound token issued for it; the chain is the additional thing that says why the call is being made and by whose leave.
- Verification. The resource server verifies offline: recompute the seals from the cached tenant root key, recompute the effective authority as a running meet, evaluate the caveats against the request. No call to an issuer, no policy round trip, no dependence on telemetry having arrived.
- Review. Later — during an incident, an internal audit, or a customer's own assurance exercise — a reviewer starts from a span id in the trace, joins to the authority record, and walks parentGrantId to the root. What comes back is the line of authority behind that one call: who authorised it, what each hop narrowed, and where the chain began.
Step five is where a platform's version of this design diverges most sharply from a single-organisation version, and the divergence is worth stating explicitly. The verifier answers a structural question: is this a well-formed chain, and what does it permit? It does not answer whether the customer's policy allows the call. It cannot — the policy is the customer's, expressed in a language the platform has no business parsing, evaluated against data the platform cannot see. So the split is: the platform guarantees chain integrity and monotonicity; the customer's resource server decides, using the effective authority as an input rather than as an answer.
Binding to the trace without depending on it
The constraint that shaped this design most is that it has to compose with the tracing infrastructure customers already run rather than compete with it. Nobody is going to adopt a second observability system to get an authority graph, and any design that requires one is a design that will lose to doing nothing.
So the authority record carries an OpenTelemetry trace id and span id, and that is the whole integration. A reviewer starting in a trace can pivot to the authority record; a reviewer starting in the authority record can pivot to the trace to see what actually happened. Two indexes over the same run, joined on a key both sides already have.
The temptation — and I have watched teams take it — is to go further and derive the authority graph from the trace, since the spans already have the parent-child structure. That fails for a reason worth being precise about, and it is not the reason most people give. It is not primarily that the GenAI conventions have no credential attribute, though they do not. It is sampling.
Traces are sampled. That is not a defect; it is the design, and it is why tracing is affordable at platform volumes. A control record cannot be. If the authority record is derived from spans, then at any sampling rate below one the record has holes, and the holes are in exactly the places a reviewer will care about: the long-running, high-fan-out sessions that generate the most spans and get sampled the hardest. Worse, the holes are silent — a missing authority edge is indistinguishable from a delegation that never happened, which means the record's failure mode is to under-report exactly the thing it exists to report.
Hence the rule the emitter encodes: the cross-reference is written when a span is active and omitted when one is not, and nothing in verification or in the record's completeness depends on it. This also settles retention. Traces are retained on a cost curve — days, sometimes hours. An authority record is retained on whatever clock the customer's assurance obligations run on, which for a bank in India, a payments platform in the Gulf, or a supervised institution in the United States is measured in years. Sharing a retention policy between the two would be an accident with consequences.
One thing this design deliberately does not do is propose new attributes in the gen_ai namespace for credentials. The conventions are at Development status and are the wrong place for a control record regardless of their maturity, because they inherit the sampling and retention properties of telemetry. If an authority attribute ever lands in the conventions, it will be useful as a cross-reference. It will not be the record.
Where this design fails
Eight failures. The first three are the ones that would keep me up.
One: a narrower authority is not always a smaller blast radius. The lattice orders authority by set inclusion. Consequences are not ordered that way. A grant restricted to write:config/root is strictly weaker than one permitting write across the whole namespace, and may be far more dangerous, because configuration is the thing that governs everything else. Attenuation gives you a provable statement about the extent of a permission and no statement at all about its effect. Anyone who reads a monotone chain as a risk gradient has made an error the design does nothing to prevent, and I would put that warning in the implementation's own documentation rather than in a blog post.
Two: the root grant is the entire ballgame, and this design does not help you write it. Everything below the root is bounded by the root. If the root grant is broad — because someone was debugging, because the workload's needs were not understood at design time, or because narrowing it kept breaking things — then a perfectly monotone chain descends from a bad start and every guarantee below it is a guarantee about the wrong quantity. The design converts the problem into a single, visible, signed artifact, which is genuinely better than the same problem scattered across a dozen environment variables. It does not solve it. Root-grant scoping is a design exercise a human has to do, and it is the exercise most teams will skip.
Three: attenuation bounds damage within authority and does nothing about misuse inside it. The component doing the delegating is, in an agent system, a model in a tool loop. A model that has been successfully injected can still call attenuate — correctly, monotonically, producing a perfectly valid child grant naming an attacker-chosen sub-agent — as long as it stays within the authority it holds. The chain will verify. The record will be complete and accurate. It will accurately record an attack. What the design buys here is a bound and a receipt: the damage is capped at what the compromised node legitimately held, and afterwards you can say precisely what that was and when it started. That is a real improvement over the alternative, in which the answer is a shrug. It is not prevention and should never be sold as prevention.
Four: these are bearer credentials unless you add a possession binding, and the binding costs. Possession of the terminal seal is authority. That is inherited from the macaroon construction and it is the price of offline verification: a verifier who can check a chain necessarily holds enough material to extend it, and could mint a child naming a subject of its choosing. The mitigation is in the model — subjectKey, a public key the caller must prove possession of — and it costs a signature per request and a key-distribution problem you did not have before. The lighter mitigation is the audience caveat, which is why MCP's per-resource audience rule composes with this design rather than competing with it: a child minted by a resource server carrying an audience caveat naming that server can only ever be presented back to it. Neither mitigation is free, and the bearer mode is the one most teams will ship first.
Five: revocation and offline verification pull in opposite directions. RFC 8693 is candid that token exchange creates no lifecycle coupling, and neither does this. A child names its parent, so in principle revoking the parent should invalidate the line — but checking that requires the verifier to consult something, which is the one thing premise three forbids on the hot path. The design's answer is bounded lifetimes plus a per-tenant revocation epoch published with the root key, which the verifier already caches: bump the epoch and every chain rooted before it stops verifying at the next cache refresh. That is a blunt instrument. It revokes everything under a root, not one branch, and its latency is the cache TTL. If you need branch-level revocation in seconds, this design cannot give it to you offline, and you should either accept an online check on the paths that need it or choose a different primitive.
Six: chains grow, and the growth is unbounded in exactly the workloads you care about. Verification is linear in chain depth and the bundle on the wire grows with it, carrying accumulated caveats that never shrink because caveats only accumulate. A shallow workflow is fine. A long-running agent loop that delegates on every iteration is not: depth grows with the loop, and header size grows with depth. The mitigation is re-rooting — periodically issuing a fresh root grant with the current effective authority and starting a new chain — and re-rooting discards ancestry, which is the thing the design exists to preserve. So it is a genuine trade, resolved by writing the compaction point into the authority record as an explicit re-root edge, and accepting that the ancestry before that point lives in the record rather than in the credential.
Seven: some caveats cannot be evaluated offline, and the design pushes them onto the resource server. max-invocations is the honest example, and you can see it in the verifier: it returns false, because counting is state the verifier does not have. Rate limits, budget caps, and anything else with a running total are in the same position. They can be expressed in a caveat, carried, recorded and audited — and they must be enforced by the party holding the counter. That means a caveat can appear in the chain and be silently unenforced if the resource server does not implement it. A caveat registry with mandatory-to-enforce semantics is the right answer and it is a coordination problem, not a code problem.
Eight: multi-tenancy reduces the blast radius of a platform bug without eliminating it. Trust domains are per-tenant and the verifier rejects a chain that crosses domains, so a grant issued in one tenant cannot be extended into another. That is a real containment property. It rests entirely on the platform's key isolation being correct: a bug that lets one tenant's root key be resolved for another tenant's domain defeats the whole scheme in one step. The design narrows the surface to a single function — root-key resolution — which is the right place for it to be. It does not remove it, and anyone deploying this should treat that function the way they treat a crypto primitive rather than the way they treat a lookup.
There is also a whole category this design says nothing about, and it should be named rather than left implied. It does not authenticate the human at the root — that is your identity provider's job. It does not stop an agent reading data it was legitimately granted and putting it somewhere it should not go. It does not evaluate whether an action was wise. And it makes no claim about the model's behaviour at all. It is a mechanism for carrying and constraining authority, which is a small and specific thing to be good at.
What it costs
Three kinds of cost: on the request, on the operators, and on the migration. I will be precise where I can be and explicit where I cannot.
On the request. Verification is one MAC computation per link plus one canonical serialisation per link, so it is linear in chain depth with a small constant, and it is pure computation — no I/O, no network, no lock. That is a property of the algorithm and I am comfortable stating it. What I am not going to state is a microsecond figure, because I have not measured this implementation under a representative workload. The measurement I intend to run is p50 and p99 verification latency against chain depths of one through sixteen, with and without the possession-proof signature, on a single core, and the prediction under test is that verification without possession proof stays under the serialisation cost of the request body it accompanies at every depth in that range — which, if it holds, means the honest answer to "what does this cost per request?" is "less than parsing the JSON you were already parsing." If it does not hold, that is worth knowing and I will publish it either way. The wire cost is more predictable and less comfortable: each link carries two principal identifiers, a permission list and a seal, so a chain of eight with a handful of permissions per link is a header measured in kilobytes, and header size limits on intermediate proxies are the thing that will bite first.
On the operators. This is the cost people underestimate. You are adding a per-tenant key with a rotation policy, a revocation epoch that someone must be able to bump under pressure at two in the morning, an append-only store with a retention policy set by assurance obligations rather than by cost, and a root-grant issuance path that needs a human-facing surface because a root grant is a decision, not a config value. You are also adding a new class of incident — "the chain does not verify" — with its own runbook, its own false-positive modes (clock skew against expires-at caveats is the obvious one), and its own failure-open-versus-failure-closed decision that someone senior has to make on purpose. None of that is exotic. All of it is real headcount.
On the migration. The hard part is not minting grants; it is that a grant is worthless until something verifies it. In a platform you do not control the customer's resource servers, so the enforcement point you actually own is your own boundary — your tool-calling surface, your MCP servers, your agent-to-agent gateway. The order that works is: emit first and enforce nothing, so the authority record fills up and you can look at the shape of the graph your customers' workloads actually produce; then enforce at one boundary you own, in a mode where a verification failure is logged and allowed; then flip that one boundary to failing closed; then widen. Every step in that sequence is reversible except the last, and the value shows up at step one, because a complete authority record with no enforcement is already the artifact that answers the question nobody could answer before.
I would budget the emit-only step in weeks and the first failing-closed boundary in months, and I would expect the argument about failing closed to take longer than the implementation of it.
If you had a week
Build three things and deliberately skip the rest.
- The type, the constructor and the property test. Grant, attenuate, verifyChain, and the monotonicity property test from the fourth tab above. This is a day or two of work and it is the part that has to be right, because everything else is downstream of the invariant actually holding. If the property test is not there, you do not have a guarantee, you have an intention.
- Emission from one boundary, in shadow mode. Pick the boundary where your system hands work from one component to another — for most platforms that is the tool-call dispatcher — and emit an authority edge on every handoff, cross-referenced to the active span. Enforce nothing. Verify nothing. Just write the graph. Within a week you will have something no dashboard in your estate currently shows: the actual shape of delegation in a real workload, including the hops nobody knew were there.
- One resource server that verifies, and logs, and allows. Pick the least frightening one. Have it verify the chain and log the result beside its existing authorization decision. You are looking for the disagreements — the calls your existing authorization permits that the chain would not, and vice versa. Those disagreements are the design review you did not know you needed, and they will tell you more about your root-grant scoping than any amount of whiteboarding.
What not to build in that week: revocation infrastructure, a caveat language, a policy DSL, a UI, third-party caveat discharge, possession binding, and any form of compaction. Every one of those is real work that a real deployment eventually needs, and every one of them is a way to spend a week without ever seeing the graph. See the graph first. It reliably changes what you think you are building.
The last thing I would say to a team weighing this up is that the argument for it is not primarily a security argument. Attenuation caps damage, which is worth something, and the honest accounting in the failure section above should make clear how much and how little. The argument that actually carries is that a system which cannot state the authority behind its own actions cannot be reviewed, cannot be explained to a customer's risk function, and cannot be improved except by guessing. Every other part of the platform got an answer to "what happened?" a decade ago. The authority graph is the one question still answered by asking the person who wrote it.