The companion teardown to this piece — Your connector catalogue is a composition problem you are reviewing one item at a time — makes one argument, and I am not going to re-run it here. It is that a catalogue reviewed connector by connector cannot find the thing the reviewer is actually worried about, because the thing they are worried about is a pair: a read over something confidential and a write to somewhere external, where each connector is defensible alone and the combination is not. Review scales linearly with the catalogue. The compositions do not.

This is the other half. If you accept that, what do you build? The answer in one sentence: stop shipping a wide toolbelt and start minting a narrow grant per task — a set of capability triples intersected against a declared intent, carrying their own caveats, expiring on their own, verifiable by parties who cannot call you, and cleared by a reachability analyser that runs over your own connector graph before the model is handed anything at all.

I will call the reference design @authority/broker throughout, purely so the modules below have a name to hang on. It is not a package you can install — nothing of the sort is published, and the code in this piece is a specification you would implement rather than a dependency you would add. Everything below is specified at the level a competent platform team could build from, which also means it is specified at the level where it can be shown to be wrong. I would rather it be checkable than persuasive.

What the protocol gives you, and what it does not

Start from the text rather than from the folklore, because the folklore on this has drifted a long way from what the specification actually says.

The Model Context Protocol specification is unusually candid about its own limits. Under Security and Trust & Safety it states four key principles — user consent and control, data privacy, tool safety, and controls on model sampling — and then says, plainly, that it cannot enforce them:

While MCP itself cannot enforce these security principles at the protocol level, implementors SHOULD: Build robust consent and authorization flows into their applications…
Model Context Protocol specification, version 2025-06-18, "Security and Trust & Safety"

That is not a gap in the specification. It is a correct statement of what a protocol can do. The principles are stated as implementor obligations, and where an agent runs on a cloud or a model provider, the implementor is that platform. If you are reading this from inside one, that sentence is addressed to you specifically.

The same section is equally direct about what a tool is. Tools represent arbitrary code execution, it says, and must be treated with appropriate caution; descriptions of tool behaviour, including annotations, should be considered untrusted unless obtained from a trusted server. The server-features page restates it normatively — clients MUST consider tool annotations untrusted unless they come from trusted servers. So the self-description of a tool is a claim, not a fact, and any design that derives authority from what a tool says about itself has already lost.

Now the part that motivates everything else. Here is the field list the specification's Data Types section gives for a tool definition: a name, a title, a description, an input schema expressed as JSON Schema, an optional output schema, and optional annotations. That is all of it, bar a protocol-level metadata bag on the wire type. Read the list again with an authorization question in mind.

  • name and title identify the operation. They say nothing about what it may touch.
  • description is prose for the model, and the specification has just told you it is untrusted.
  • inputSchema types the arguments. It constrains the shape of what you pass, not the set of things the call can reach once it is running.
  • outputSchema types what comes back. Same limitation, other direction.
  • annotations are hints, and are normatively untrusted.

There is no field in a tool definition that bounds which rows, tables, threads, endpoints, repositories or accounts an invocation may reach. A schema that accepts a string for `query` will accept any string for `query`. The type system is doing exactly what it was designed to do, and it was not designed to do this.

Authorization is meant to close that gap, and in MCP it does close part of it. The authorization specification is a serious document: OAuth 2.1, PKCE, dynamic client registration, authorization server metadata, protected resource metadata, resource indicators. Clients MUST implement RFC 8707 resource indicators, and MUST send the `resource` parameter — the specification is explicit that they send it regardless of whether the authorization server supports it. Servers MUST validate that tokens presented to them were issued for them as the intended audience. Token passthrough is forbidden outright: a server MUST NOT accept tokens not explicitly issued for it, and where it calls upstream it MUST NOT forward the token it received. The stated risk is a server being used as a proxy for data exfiltration by an actor holding a stolen token.

All of that binds a token to a resource server. None of it binds a token to a task. And the specification defines no scope vocabulary for tools or resources at all; scope appears in the normative text mainly as an error condition — an HTTP 403 carrying "Invalid scopes or insufficient permissions". The vocabulary in which you would say what this particular run is allowed to touch is left to you.

MCP's own security guidance names the consequence. That guidance is the Security Best Practices document, which is versioned separately from the specification and which I cite at its 2025-11-25 revision throughout — the scope-minimization section quoted here was added after the June 2025 spec, so it is not in the text a reader will find pinned to that revision. Under scope minimization it describes an attacker who obtains a token carrying broad scopes granted up front — files, database, admin — because, in its words, the server exposed every scope in its supported list and the client requested them all. The result it lists first is expanded blast radius: a stolen broad token enabling unrelated tool and resource access. Its catalogue of common mistakes reads like an inventory of current practice: publishing all possible scopes, using wildcard or omnibus scopes, bundling unrelated privileges to preempt future prompts.

Its prescribed remedy is not a better toolbelt. It is a minimal initial scope set of low-risk discovery and read operations, with incremental elevation driven by targeted challenges when a privileged operation is first attempted, and servers instructed to emit precise challenges rather than return the full catalogue. That is the right instinct, and it is the seed of the design below. What it does not yet give you is an algebra — a way to say what the elevated grant is over, so that the elevation is narrow in the dimension that matters.

The derivation

Two principles from Saltzer and Schroeder's 1975 protection paper do the load-bearing work, and they are worth stating in their own words rather than in the paraphrase they usually get. Least privilege: every program and every user of the system should operate using the least set of privileges necessary to complete the job. Complete mediation: every access to every object must be checked for authority.

Everything that follows is a consequence of taking both of those literally in a setting where the program is a model and the job is a task nobody enumerated in advance.

First: a privilege you cannot express is a privilege you cannot minimise. Least privilege is a statement about a set. To find the least set you need an algebra over sets, and the elements of that algebra have to be at the granularity of the job. "May call search_mail" is not at the granularity of a job; "may read messages in thread T-4471" is. If the finest unit your system can express is the tool, then your least privilege is tool-granular, and tool granularity is set by whoever wrote the connector for their own convenience. You are not minimising. You are rounding up to the nearest thing you can name.

Second: complete mediation needs something to mediate against. "Every access checked for authority" presumes a proposition the checker can evaluate. Given only a tool name, the only proposition available is membership — is this tool in the belt? That is a check against the catalogue, not against authority over the object being touched. It passes for every object the tool can reach, which is the whole reachable set. To get a real check you need the resource set to be a field in the credential, so the enforcement point can ask whether this object is in that set.

Those two together force the shape. A capability is a triple: a verb, a resource set, and a set of constraints. The verb is one operation drawn from a closed vocabulary the platform owns, not a server and not a connector. The resource set is a closed description — an audience, a resource kind, and a conjunction of selectors — where an empty selector list is a refusal rather than a wildcard. The constraints are caveats that narrow: expiry, task binding, tenant binding, call budgets, egress prohibitions, human acknowledgement requirements.

FIGURE 1 · WHAT A GRANT IS MADE OF A tool name is not a capability. A capability is a triple. 01 · THE TOOLBELT Shipped wide, once, up front search_mail name · title · description · inputSchema outputSchema? · annotations? read_file name · title · description · inputSchema outputSchema? · annotations? post_message name · title · description · inputSchema outputSchema? · annotations? No field bounds the reachable set. authority = A ∪ B ∪ C 02 · THE GRANT Minted narrow, per task VERB read one operation, not a server RESOURCE SET mail.message @ one audience where thread = T-4471 CONSTRAINTS expires 00:04:00 · task T-4471 tenant-bound · no egress elsewhere The set is a field, so it is checkable. authority = held ∩ intent A union is a property of the catalogue. An intersection is a property of the job. Only one of the two gets smaller as the platform gets larger.

Third: if authority is a set, union is the wrong operator. A toolbelt is assembled by union. Someone lists what the agent might need, ORs it together, and ships it — which is exactly the bundling of unrelated privileges to preempt future prompts that MCP's own guidance lists as a mistake. But the task does not need the union of plausible needs. It needs a specific, usually tiny, subset. The correct operator is intersection: take what the principal actually holds, intersect it with what this task actually declared, and grant the result. Union grows monotonically with your catalogue. Intersection shrinks with every fact you learn about the job.

Fourth: intersection requires the intent to be written in the same algebra. You cannot intersect a set of capabilities with a paragraph of natural language. The task has to declare what it needs as capabilities — same verbs, same kinds, same selectors — before the model is given anything. This is the demand the design makes on the caller, and it is the demand people resist, so I will be blunt about it: if you will not declare intent, you cannot have narrow grants, because there is nothing to intersect against. The declaration is recorded and the originating text is digested into the audit record, but the text is never parsed to widen a grant. Widening from untrusted prose is the failure the specification already warned you about, arriving through a different door.

Fifth: intent is per task, so the grant is per task — which makes it a mint, not a configuration. A configuration is set once and read many times. An intersection whose right-hand operand changes every task has to be computed every task. That is a minting operation: a broker takes holdings and intent, computes the intersection, appends binding caveats, sets an expiry, and signs. Nothing in that path can widen anything, because the only two operations available to it are intersect and append-caveat, and both are narrowing.

Sixth: the credential has to be checkable by parties who cannot call the issuer. At platform scale the enforcement points are per connector, in many regions, some of them running in the customer's own network. Putting a synchronous call back to a central authority on every invocation gives you a single point of failure with the availability requirements of the whole platform and the latency of a network hop on every tool call. The alternative is a credential that carries its constraints with it and can be verified offline.

That is not a new idea and it should not be reinvented. It is the macaroon construction, which embeds caveats that attenuate and contextually confine when, where, by whom and for what purpose a service should authorize a request, built from nested chained MACs. The paper's own framing includes strengthening existing mechanisms such as OAuth2, which is exactly the position a platform is in: not replacing its authorization server, but adding an attenuation layer above it.

The construction carries a condition, and the condition fixes the arithmetic. Attenuation is only safe if it is monotone — so when two capabilities are intersected, the selectors intersect and the caveats union. That asymmetry is not an implementation detail; it is the property the whole design rests on. Every caveat narrows, so the union of two narrowing predicates is the intersection of two authorities. Because appending a caveat can only ever reduce authority, any holder can attenuate a grant further without asking anyone, and a verifier who has never seen the attenuation can still evaluate it. Delegation stops being a privileged operation.

What remains is making the credential useless anywhere but where it was meant, and here the standards have already settled the question. RFC 8707 states the problem exactly: bearer tokens let any party in possession of a token reach the associated resources, and an access token must only be valid at a specific protected resource and for a specific scope. Its security considerations tell issuers to downscope the scope value to what the resource can process and needs to know. RFC 9700, the OAuth security best current practice, makes it normative — privileges SHOULD be restricted to the minimum required, tokens SHOULD be audience-restricted to a specific resource server, and servers SHOULD use sender-constraining mechanisms such as mutual TLS or DPoP. Audience binding is the `audience` field in the resource set. Sender-constraining is what stops a leaked grant being useful to whoever leaked it.

Seventh, and this is the one the standards do not cover: narrow grants do not bound composition. Every capability in a grant can be individually minimal and the grant as a whole can still be catastrophic, because the agent holds them simultaneously and the connectors behind them are joined by edges nobody drew. A minimal read on support tickets reaches attachments. A minimal read on repository files reaches build configuration. A minimal send on outbound messages reaches an external address. Three minimal capabilities, one exfiltration path, and no single capability review would have flagged any of them. Intersection bounds each element. Nothing so far bounds the closure of the set.

So the last thing the derivation forces is an analyser: a component that takes the proposed grant set, walks the platform's connector graph, computes what the grant transitively reaches, and reports the compositions before execution. And because the party who needs to know is the customer rather than the platform's own reviewers, it has to be a product surface — a pre-flight check the customer calls — not an internal report.

The data model

Below is the core of @authority/broker: the types, the intersection, the mint, and the closure. It is deliberately small. Most of the difficulty in this design is in the connector graph and the selector algebra, and both of those are places where a platform's own domain knowledge does the work — so the code that can be written generically is not much code, and that is a feature.

Configuration

@authority/broker — the parts that are not platform-specific

Three files. The types, because the triple is the argument. The mint, because the claim that nothing on that path can widen a grant should be checkable by reading it. The analyser, because the closure is the part people assume is expensive and it is not.

Note what is absent: there is no tool name anywhere in this file. A tool name is a label on an operation, and this module is about the resources an operation may reach. Note also that an empty selector list is not permitted to mean 'everything' — the mint refuses it. Wildcards are how least privilege dies quietly.

packages/broker/src/capability.ts
/** Verbs are a closed vocabulary the platform owns. Connectors map their
 *  operations onto it; they do not get to invent verbs, because the analyser
 *  reasons over verbs and an open vocabulary is an unanalysable one. */
export type Verb = "read" | "list" | "search" | "create" | "update" | "delete" | "send";

/** A field predicate. Deliberately tiny: every operator here must be
 *  intersectable with every other, and each one you add is a new pair. */
export type Selector =
  | { readonly op: "eq"; readonly field: string; readonly value: string }
  | { readonly op: "prefix"; readonly field: string; readonly value: string }
  | { readonly op: "in"; readonly field: string; readonly values: readonly string[] };

/** A closed description of a set of resources.
 *
 *  `audience` is the canonical URI of the resource server — the same value
 *  RFC 8707 puts in the `resource` parameter — so a grant cannot be replayed
 *  at a different server. `kind` is a connector-declared resource type.
 *  Selectors are ANDed. An empty selector list is rejected at mint time; it is
 *  never read as a wildcard. */
export interface ResourceSet {
  readonly audience: string;
  readonly kind: string;
  readonly selectors: readonly Selector[];
}

/** Caveats only ever narrow. That is what makes offline attenuation safe:
 *  any holder may append one without consulting the issuer, and a verifier
 *  who has never seen the attenuation can still evaluate the result. */
export type Caveat =
  | { readonly k: "expires-at"; readonly epochMs: number }
  | { readonly k: "max-calls"; readonly n: number }
  | { readonly k: "task"; readonly taskId: string }
  | { readonly k: "tenant"; readonly tenantId: string }
  | { readonly k: "no-egress"; readonly exceptAudiences: readonly string[] }
  | { readonly k: "human-ack"; readonly at: "first-write" | "every-write" }
  /** A predicate the platform cannot evaluate, discharged by the customer and
   *  verified offline. This is how customer policy stays outside the platform. */
  | { readonly k: "third-party"; readonly discharger: string; readonly predicateId: string };

/** The triple. Verb, resource set, constraints. */
export interface Capability {
  readonly verb: Verb;
  readonly resources: ResourceSet;
  readonly caveats: readonly Caveat[];
}

/** What the task says it needs, declared before the model is given anything. */
export interface Intent {
  readonly taskId: string;
  readonly tenantId: string;
  readonly principal: string;
  readonly requested: readonly Capability[];
  /** Digest of the originating text, recorded for the audit record.
   *  Never parsed to widen a grant — widening from untrusted prose is the
   *  failure mode the whole design exists to remove. */
  readonly statementDigest: string;
}

export interface Grant {
  readonly grantId: string;
  readonly issuedAtMs: number;
  readonly capabilities: readonly Capability[];
  /** Digest of each tool definition this grant was minted against, keyed by
   *  audience and kind. If a server redefines a tool after approval, the
   *  digest stops matching and the enforcement point refuses. */
  readonly definitionDigests: Readonly<Record<string, string>>;
  /** Detached signature over the canonical encoding: a root chain, then one
   *  MAC per attenuation, so third parties can attenuate offline. */
  readonly signature: string;
}

What is not here: the macaroon MAC chain, the discharge protocol, the canonical encoding, and the per-tenant graph store. Those are the parts where getting it wrong is expensive and where you should use a reviewed implementation rather than the sketch above.

The control path

Five layers, drawn in the figure below, and eight steps that run across them. The ordering rule matters more than any of it: the analyser runs before the mint, the mint runs before the model, and the enforcement point runs on every call regardless of what the other two concluded. Complete mediation is layer four's job alone. Everything above it exists only to make layer four's refusals rare.

FIGURE 2 · REFERENCE ARCHITECTURE Five layers inside the boundary. The policy stays outside it. PLATFORM BOUNDARY LAYER 5 Agent runtime — model and tool loop Untrusted for authorization. Tool descriptions reaching it are untrusted input. LAYER 4 Enforcement point — one per connector Every call: audience matches · digest matches · caveats hold · fail closed. LAYER 3 Broker — intersect, attenuate, expire, sign Holdings ∩ intent. Per-client consent registry. Nothing here widens anything. LAYER 2 Reachability analyser — closure before execution Exposed to customers as a pre-flight check, not an after-the-fact explanation. LAYER 1 Connector graph registry Kinds · verbs · typed edges · definition digests. Partitioned per tenant. OUTSIDE Customer policy The platform cannot see inside it, and should not need to. Third-party caveat + offline discharge Analyser before mint. Mint before model. Enforcement on every call regardless. Complete mediation is layer 4's job alone — the others only make its refusals rarer.
  1. Intent declaration. The caller submits a task with its requested capabilities as triples, plus a digest of the originating text. Nothing has been granted; nothing has been shown to a model.
  2. Pre-flight analysis. The analyser computes the closure of the requested set over the tenant's connector graph to a configured depth and returns the reached set, the composition findings, and — crucially — the unmodelled kinds. The caller sees this before committing.
  3. Consent, per client. If a finding requires it, the request goes to a human against a registry of approved client identifiers for that user. This is not decoration: the current revision of MCP's Security Best Practices documents a confused-deputy vulnerability that arises exactly when a proxy with a static upstream client identifier permits dynamic client registration, the upstream sets a consent cookie, and the proxy has no per-client consent of its own — the consent screen is skipped and the authorization code is redirected to an attacker-registered address. The normative fix is per-client consent, and the broker is a proxy in precisely that sense.
  4. Mint. Holdings intersected with intent, binding caveats appended, definition digests recorded, expiry set, signature applied. Refusals are total, not partial.
  5. Execution. The runtime receives the grant and only the tools the grant covers. Tool descriptions reaching the model are still untrusted input; the grant is what constrains the damage they can direct, not what makes them trustworthy.
  6. Enforcement, every call. The connector's enforcement point checks four things: that the grant names this audience, that the presented tool definition digest still matches the one recorded at mint, that every caveat holds, and that the specific object being touched is in the resource set. Any failure refuses. It never consults the broker.
  7. Attenuation on delegation. When a sub-agent is spawned, the parent appends caveats and hands over the narrowed grant. No call to the broker, because appending a caveat cannot widen anything.
  8. Expiry. The grant dies. Long-running tasks re-mint, which means they re-declare, which means the intent record stays honest as the task changes shape.

The digest check in step six is doing specific work. Invariant Labs' April 2025 disclosure on tool poisoning documented post-approval redefinition — a server changing a tool's description after the client has already approved it — alongside hidden instructions in descriptions and tool shadowing, in which a tool from one server injects instructions that alter the agent's behaviour toward a different, trusted server. Binding the grant to a digest of the definition it was minted against does not stop the model being lied to. It does stop the definition being swapped after the human said yes.

The analyser is the product surface

Of everything above, the analyser is the piece most likely to be built as an internal tool and left there. That would be the wrong call, and the reason is arithmetic rather than taste.

FIGURE 3 · THE CLOSURE Review reads the nodes. The risk lives in the edges. GRANTED DEPTH 1 DEPTH 2 read support.ticket read repo.file send message.outbound attachment.blob edge: content build.config edge: reference external.address edge: side-effect · EGRESS confidential content reached at depth 2 by two disjoint paths composition: read ⇒ egress Reviewing N connectors one at a time is N reviews. The pairs that can be composed number N(N−1)/2. Review effort grows linearly. The thing being reviewed grows quadratically. The analyser closes over the proposed grant, not over the catalogue — which is what keeps it cheap enough to sit on the request path rather than in a quarterly review.

A customer reviewing a catalogue of N connectors performs N reviews. The pairs that can be composed number N(N−1)/2, and if you care about chains of three the count is worse again. Review effort grows linearly. The object under review grows quadratically. No staffing model closes that gap, and an approval process that does not acknowledge it will settle, by default, into approving connectors on their own merits and hoping about the combinations.

The analyser closes it by changing what gets analysed. It does not enumerate the catalogue's pairs. It closes over the grant — a handful of capabilities — and walks only the sub-graph they touch. Cost is linear in the vertices and edges of that sub-graph, which is small because the grant is narrow, which is the case precisely because you did the intersection first. The two halves of the design pay for each other.

Here is a constructed illustration — not a real incident, and not drawn from any customer. Suppose a support-triage agent is granted three capabilities: read on support tickets, read on repository files, and send on outbound messages. Each passes a per-connector review comfortably. The graph, however, records that ticket records carry a content edge to attachment blobs, that repository files carry a reference edge to build configuration, and that both of those reach content classified confidential at depth two. It also records that outbound messages reach an external address by a side-effect edge, which is marked egress. The analyser returns a finding: confidential content is reachable, an egress sink is reachable, and the two are held simultaneously. Nobody wrote that path. It is a property of the graph, and it is invisible to anyone reading the three connectors one at a time.

Exposing this as a customer-callable pre-flight changes the conversation from explanation to prevention. The alternative — telling a customer after an incident which composition caused it — is a report they did not want and could not have used.

Multi-tenancy, and the policy you are not allowed to see

Now the constraint that rules out the design most teams reach for first. The instinct is a central policy decision point: one service that knows every rule and answers yes or no. It is a clean design and it does not survive contact with a platform, for three reasons.

You cannot see the customer's policy, and you should not want to. A tenant's rule may be that this agent may not touch records belonging to a legal hold, or an account under an internal investigation, or a project the platform has no concept of. Ingesting that policy makes you the custodian of a description of their most sensitive internal state, and it goes stale the moment they change it. The answer is the third-party caveat: the grant carries a predicate the platform cannot evaluate, naming a discharger the customer controls. The customer's own service issues a discharge, the enforcement point verifies the discharge cryptographically and offline, and the platform learns nothing about the predicate beyond whether it was discharged.

You cannot put a central call on every tool invocation. The enforcement points are per connector, in every region you operate, and some run inside customer networks. A synchronous authorization call from each of them is a hard dependency with the availability requirement of the entire platform and a network hop of latency on every single call an agent makes — and agents make many. Offline verification is not an optimisation here. It is the only shape that fits.

The connector graph is itself tenant-sensitive. Which connectors a tenant has registered, and which resource kinds they expose, is competitive information about that tenant's own architecture. A single global graph leaks it through analyser output — a finding that mentions a kind is a statement that the kind exists. The graph is partitioned per tenant, with a shared spine for platform-native connectors, and analyser output is scoped to the requesting tenant. Tenants with contractual residency commitments get their partition where the contract says, which is only tractable because the partition is per tenant in the first place.

One more thing worth pinning, because it is easy to get wrong under load: session identity is not authorization. MCP's guidance is normative on this — servers implementing authorization MUST verify all inbound requests, and MUST NOT use sessions for authentication. A grant is presented and verified on each call. A session identifier is a correlation key and nothing more.

Where this design breaks

A design piece without a failure analysis is a brochure. Here are the six ways I know this one fails, roughly in order of how much they worry me.

1. Intent declaration is a real burden, and the pressure is always toward over-declaring. This is the deepest problem. Tasks whose scope is genuinely discovered mid-run — exploratory analysis, incident investigation, anything where the second step depends on the first result — will hit refusals and re-mint repeatedly. Each re-mint is a round trip and, if consent is required, a human. The rational response for a caller under deadline is to declare wide up front, and a wide declaration intersected with wide holdings gives you back the toolbelt with extra steps. Total refusal rather than partial helps, because it makes over-declaration visible rather than free. It does not remove the incentive. If this design fails in the field, this is how.

2. The graph is only as good as its edges, and third-party edges are claims. The analyser reasons over declared edges. A connector that has an undocumented side effect has an edge you do not have, and the closure will look clean while being wrong. Worse, the specification has already told you that a third-party server's self-description is untrusted — so a connector declaring its own kinds and edges is asserting, not attesting. The mitigations are partial: mark inferred edges as inferred, report unmodelled kinds instead of treating them as leaves, and hold third-party connectors to a lower trust tier where their declared edge set is treated as a lower bound rather than a description. None of that turns an assertion into a fact.

3. Narrow grants do not stop the model being deceived. Tool poisoning still works inside a grant. If an agent legitimately holds read on a thread and a poisoned description directs it to summarise that thread into an outbound message it also legitimately holds, the grant permits it and the enforcement point approves it, correctly. The grant bounds the blast radius to what was minted. It does not adjudicate intent within that radius. Anyone selling capability narrowing as an answer to prompt injection is overselling it, and the analyser's egress findings are the honest surface of this limitation, not a fix for it.

4. The selector algebra will produce false refusals, and false refusals are how a control gets disabled. Two selectors that are semantically disjoint but syntactically incomparable — a regular-expression predicate against a prefix, say, or two different date encodings — return `undefined` from the intersection, which the mint must treat as a refusal. Fail-closed is correct and it is also the behaviour that gets a control switched off in production at 2 a.m. Every operator you add to the algebra to reduce this adds a pair of cases to maintain, and the pairs are quadratic in the operator count. The design deliberately ships three operators and expects to be argued with about the fourth.

5. Attenuated bearer credentials cannot be recalled. The property that makes offline attenuation work — no issuer involvement — also means there is no issuer to call when you want a grant back. Short expiry is the real answer, and short expiry fights directly with long-running tasks. A revocation list reintroduces the central dependency you removed, though only on a slow path, which is a defensible compromise if you scope it to grant identifiers and accept that it is eventually consistent. Be clear-eyed that you have traded revocation latency for availability, and that some tenants will consider that trade the wrong way round.

6. The broker is now the highest-value target on the platform. A component whose whole job is issuing authority is a mint, and a compromised mint issues credentials that are genuine. Nothing in a credential lifecycle answers that; the answers are structural — the signing material lives where the broker's own workloads cannot reach it, mint operations are logged to an append-only record the broker cannot rewrite, and the enforcement points hold a policy ceiling that no grant can exceed regardless of what it says. The ceiling is the important one, because it is the only control here that survives the broker being wrong.

What it costs

Three costs, and I will separate what I can reason about from what would have to be measured.

Latency, and where I will not give you a number. The mint is one round trip per task, not per call — which is the correct comparison, because a toolbelt design that puts an authorization check on the call path pays per call. Intersection is O(H×R) over holdings and requested capabilities with a map lookup per selector; both counts are small. Closure is linear in the vertices and edges of the grant's sub-graph, bounded by the depth limit. The structural claim is that this is a single control-plane round trip added ahead of a model invocation that will take orders of magnitude longer. I have not measured it on a production connector graph, and I am not going to invent a figure: the prediction under test is that a closure to depth three over a grant of ten capabilities completes well inside the model's own time to first token, and that the mint round trip therefore does not appear in a user-visible latency budget. That is a hypothesis with an obvious experiment attached, and it should be run against your own graph before anyone quotes it.

Operational burden, which is the real bill. The broker moves onto the control plane's availability budget, not a batch service's — if it is down, no new task starts. It needs signing material with rotation, an append-only mint log, and per-tenant graph partitions with a registration path. The connector graph needs an owner, because an unmaintained graph decays into a reassurance. Refusal telemetry needs a triage path, because a refusal that nobody looks at is a control on the way to being disabled. And the pre-flight becomes a documented, versioned, customer-facing API, with everything that implies about deprecation.

Migration, which is where the schedule actually goes. You will not switch a live platform from wide tokens to narrow grants in one change. The path that works is a shadow: mint grants and record what they would have refused, while continuing to honour the existing wide token. Run it long enough to see the refusal distribution across your real workload mix — the tail is where the false-refusal problem lives, and a week of traffic will not show it. Then enforce per connector, starting with the ones whose resource sets are easiest to express, and hold the connectors with the messiest selectors until last. Expect the mapping from existing scope strings to triples to be lossy, and treat the loss as the finding rather than as a conversion problem: every scope you cannot express as verb-plus-resource-set-plus-constraint is a place where you never actually knew what you had granted.

If you had one week

Not the architecture. The first thing that produces a finding, because a design that has not surprised anyone yet has not earned its budget.

  1. Days one and two — the triple, and the lossy conversion. Write `capability.ts` and a converter from your existing scope strings to triples. Run it over your real grants. The output you care about is not the conversions; it is the list of scopes that will not convert, because each one is an authority you are granting today without being able to say what it is over.
  2. Day three — the graph, at whatever fidelity you can get by Wednesday. Nodes for your platform-native connector kinds, classification and egress flags, and edges only where a connector explicitly declares one. It will be incomplete. Mark the gaps in the `unmodelled` list rather than smoothing them, because the gaps are the first thing anyone senior will ask about and the honest answer is more useful than a full-looking graph.
  3. Day four — the closure, as a command-line tool. Run `analyse` over the grants your ten busiest agent workloads hold today. This is the day the week justifies itself or does not. If it returns no findings, your graph is too thin, not your platform too safe.
  4. Day five — the mint, in log-only mode. Wire the broker into one workload's task creation. Enforce nothing. Record every capability that would have been refused, alongside whether the task then succeeded. You now have the two numbers that decide whether this is deployable: how often narrow grants would have blocked real work, and how much of what you grant today nobody used.

What not to build in that week: the MAC chain, the discharge protocol, the customer-facing pre-flight API, and any user interface. All four are necessary and none of them will tell you anything you do not already know. The closure and the refusal log will.

The claim this design rests on is narrow enough to state and check. A tool definition types its arguments and does not bound its reach; least privilege over a set you cannot express is not least privilege; and a union over a growing catalogue is the wrong operator for a job whose actual needs are small and knowable in advance. Everything else — the caveats, the offline verification, the analyser, the per-client consent — follows from taking those three seriously at a scale where you cannot see inside your customers' policy and cannot call anyone on the hot path.

Composition bounded before the run, rather than explained after it. That is the whole of what this buys, and it is worth the bill.