The companion teardown to this piece — You integrated nine systems for a client and granted an agent all nine at once — makes one argument, which I am not going to re-run. It is that an integration programme is scoped, priced and accepted one system at a time, while the authority an agent acquires is a property of the whole set; that nothing in the statement of work or the acceptance criteria is written at that granularity; and that the review which signed off each connector could not have found the problem, because the problem is not in any connector.

This is the other half: what you build, on an account where you did not choose the identity provider, cannot migrate the data platform, have four sprints left and will not be there in a year. In one sentence — stop handing the agent the toolbelt you delivered and start minting a narrow grant per task: capability triples intersected against a declared intent, carrying their own constraints, expiring on their own, and cleared by a reachability analyser that runs over your own connector graph before the model is handed anything.

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; I know of nothing published under that name, and the code here is a specification you would implement rather than a dependency you would add. It is written at the level a competent delivery team could build from in a sprint, which is also the level at which it can be shown to be wrong.

The half nobody scoped when the deal was signed

Start with the commercial shape, because it is why the gap is structural rather than careless. An integration workstream is decomposed into systems, and that decomposition is not an accident of estimating practice — it is what makes the work sellable. Each system is a discrete unit of effort with its own owner on the client side, its own credentials to obtain, its own test plan, its own acceptance signature. The programme plan, the change record, the invoice and the closure report are all indexed the same way. Nine integrations, nine rows, nine sign-offs.

Authority does not decompose that way. What an agent can do with the reporting database, the ticketing system and the object store at once is not the sum of what it can do with each alone, and no row in the plan holds that difference. The unit of the deal is a system name; the unit of the risk is a reachable set. No amount of diligence applied to the first produces the second.

FIGURE 1 · THE HALF NOBODY SCOPED The engagement recorded nine integrations. It never recorded what they confer. 01 · IN THE SOW Integration 4 Reporting database scoped by system priced by system accepted by system UNIT OF THE DEAL a system name 02 · WHAT WAS DELIVERED connector: postgres role: reporting_owner credential: long-lived scope: whatever the role holds expiry: none UNIT OF THE BUILD a service account 03 · WHAT IT REACHES Every row the role owns including the ones policy hides row security bypassed by the owner no binding to any task no expiry, no receipt UNIT OF THE RISK a closure THE CORRECTION A capability is not a name. It is a triple. VERB read one operation, not a server drawn from a closed vocabulary RESOURCE SET pg.table reporting.invoice where client_id = 88213 no wildcard is accepted CONSTRAINTS expires 00:12:00 · task R-114 no egress beyond the estate caveats only ever accumulate toolbelt authority = union over the estate you inherited — grows with the catalogue grant authority = held ∩ intent — shrinks with every fact known about the task

The instruments the client is governed by already say the grant must be bounded by the contracted purpose. They do not say how, and they name no artefact in which the boundary is recorded. The clearest statement is in the interagency guidance the US federal banking agencies issued in June 2023, which replaced the three agencies' separate third-party frameworks with one text. Its opening premise is the one every services firm's sponsor already knows and every services firm's proposal quietly elides:

the use of third parties does not diminish or remove banking organizations' responsibilities to ensure that activities are performed in a safe and sound manner and in compliance with applicable laws and regulations
Interagency Guidance on Third-Party Relationships: Risk Management, 88 FR 37920, 9 June 2023 (OCC Bulletin 2023-17 / Federal Reserve SR 23-4 / FDIC FIL-29-2023)

Its contracting section states the intersection principle in prose, under confidentiality and integrity: effective contracts typically prohibit the use and disclosure of banking organization and customer information by a third party and its subcontractors, except as necessary to provide the contracted activities or comply with legal requirements. Read that with a connector in mind. Except as necessary to provide the contracted activities is the rule the broker below implements — an intersection against the purpose, not a union over the available systems. What is missing is any mechanism enforcing it at the authority layer rather than in a schedule nobody reads at runtime.

The same guidance names the composition problem under subcontracting: subcontracting can result in risk due to the absence of a direct relationship between the banking organization and the subcontractor, further lessening the organization's direct control of activities. It suggests terms covering notice of intent to subcontract, whether specific subcontractors are prohibited, whether consent is required, and what liability the third party carries for its subcontractors' actions. A tool a services firm wires into a client estate is, functionally, an undeclared subcontractor of capability: it performs part of the contracted activity, it holds credentials, it is not named in the schedule, and nobody consented to it specifically.

The audit provision goes further than most firms realise: the guidance contemplates periodic independent audits of the third party and its relevant subcontractors, service organisation control reports, payment card industry compliance reports, and reserving the right to audit directly or through an independent party. The right is broad and live. What does not exist is anything for it to inspect — none of those artefacts reports a reachability closure for a granted connector, because no such report has been standardised and, as far as I can establish, none is produced.

That absence is worth stating precisely, because it is easy to inflate. I found no standards body, cloud vendor or regulator publishing a measured ratio between the resource set a tool grant intends and the set it can reach, and no shipping product that reports one. The finding is the gap itself: a contractual duty to bound access to the contracted purpose, and no published instrument for measuring whether you did.

One US control set states the same thing at the granularity of the triple, and states it about this reader specifically. The least-privilege enhancement in NIST's supply-chain publication says that where enterprise users include independent consultants, system integrators and external system service providers, access requirements may need least-privilege mechanisms that precisely define what information and components are accessible, for what duration, at what frequency, using what access methods, and by whom. That is a resource set, an expiry, a rate limit, a method and a principal — five fields, none of which a connector name carries. The same publication makes access control a flow-down obligation into sub-tier contracts.

A revision caution, because this citation is widely got wrong. The version of that publication dated 5 May 2022 was formally withdrawn on 1 November 2024 and superseded by the errata update, whose own cover page carries the changes as of that date and points to Appendix K for the revision history. Cite the update, not the 2022 revision.

In healthcare the flow-down stops being a commercial preference and becomes a regulation. The HIPAA business associate provisions require the contract to provide that the business associate will ensure any subcontractors handling protected health information on its behalf agree to the same restrictions and conditions that apply to it. Read with the requirement not to use or disclose the information other than as the contract permits, a tool grant reaching beyond the contracted purpose is a contract breach before it is ever a security incident.

India's instrument is explicit on both limbs. The Reserve Bank of India's outsourcing of information technology services directions state that outsourcing of any activity shall not diminish the regulated entity's obligations, nor those of its board and senior management, who remain ultimately responsible. On sub-contracting they contemplate terms making the service provider liable for its sub-contractors and requiring prior consent for their use; on access, they require that service provider access to data at the entity's location or data centre be on a need-to-know basis. Need to know is the intersection again, in a supervisory register.

Three regulators, three countries, one sentence: the client stays responsible for what you connect, and access has to be no wider than the job. None of them tells you what to build, and none names a document in which you would show that you did it. That missing document is both the commercial opening and the design brief. Before drawing it, though, the strongest objection deserves stating at full strength, because it is the one a delivery principal will raise in the first five minutes.

The objection is that this is not the firm's problem. The client owns the estate, their security function owns identity and access, the statement of work was signed by people who had every opportunity to specify authority requirements and did not, and a delivery team that unilaterally inserts an authorization layer is manufacturing scope, cost and — if it fails at three in the morning six months after handover — liability nobody priced. There is a version of that which is simply correct: a firm that builds a bespoke security control into a client estate and then leaves has, on a bad day, created an orphan.

That objection is why the design below has the shape it has rather than a better-engineered one. Everything is bent toward two properties: it changes nothing beneath itself, and it hands over as data rather than as a system. If it required a migration, the objection would win. If the deliverable were a service only the delivery team understands, the objection would win. It is a real constraint, and it costs the design real capability — I will name what it costs in the failure analysis.

The derivation

The design follows from four commitments, each of which forces the next. I will take them in order, because the order is the argument.

First: least privilege is a claim about a set, and a connector name denotes no set. To minimise a privilege you need an algebra over privileges, and its elements have to be at the granularity of the job. "May use the reporting connector" is not. "May read rows of reporting.invoice where client_id = 88213" is. If the finest unit your system can express is the connector, your least privilege is connector-granular — and that granularity was set by whoever wrote the adapter for their own convenience, usually by wrapping whatever the service account already had. You are not minimising. You are rounding up to the nearest thing you can name.

So the first move is to model a capability as a triple: a verb, a resource set, a set of constraints. The verb comes from a closed vocabulary the broker owns, not from whatever a connector calls its methods. The resource set names concrete things — a table with a filter, an object-store prefix, a ticket project. The constraints are the caveats riding with the grant: when it dies, which task it belongs to, which tenant, how many calls, where the results may not go.

Second: authority to one resource must not confer authority to another, which makes the operation an intersection rather than a union. The zero trust tenets are the published counter-statement to the ambient toolbelt: access to individual enterprise resources is granted on a per-session basis, trust in the requester is evaluated before access is granted, access should be granted with the least privileges needed to complete the task, and — the sentence that matters — authentication and authorization to one resource will not automatically grant access to a different resource. A toolbelt violates that by construction: it is a union assembled once over everything the task might plausibly need, and once assembled, every element is available at every step.

So authority for a task cannot be assembled by adding up plausible needs. It has to be computed: take what the principal standingly holds, take what this task declares it is about to do, keep only the overlap. Nothing the task declares can add authority — a declaration is a claim, and claims only narrow. Nothing the holding contains is granted unless the task asked for it.

Third: the intersection is different for every task, so it must be minted rather than configured. A configured grant outlives the reason for it. If the intersection is computed per task, the artefact it produces is per task too: it exists when the task starts, carries the task's identifier, and dies when the task ends or the clock runs out. That is a mint, not a configuration entry. And because it is minted at machine rate, nothing on the minting path can require a human — which means the ceiling, the standing holding, is what humans review, once, as code, rather than approving grants they cannot read at a rate they cannot follow.

The fourth falls out of the third, and it is the one most designs miss.

Fourth: narrowness within a grant does not bound the closure of a grant set. You can mint a perfectly narrow read over one table and a perfectly narrow write to one ticket project, and the pair still composes into an exfiltration path, because the systems you connected are already connected to each other. The export job moves the table to a bucket. The ticket workflow attaches from the bucket. The notification rule mails the attachment to whoever raised the ticket. Each of those edges was built by the same engagement and accepted on its own. The path is not a property of any of them; it is a property of the set, and a property of the set can only be found by an analysis over the set.

So the last component is a reachability analyser: given a proposed grant set and a graph of the estate's own data flows, compute what the grant can reach transitively, and refuse or attenuate before execution rather than explain afterwards. This is the genuinely new work, and it is also what becomes the handover artefact, because its output is a document rather than a service.

Four commitments, one design. A triple because least privilege needs an expressible set; an intersection because authority is not transitive; a mint because the intersection is per task; a closure analysis because per-task narrowness does not bound composition. Nothing here is a preference. Drop any one and the next one stops working.

Where the intersection already ships, and where it cannot

The strongest thing that can be said for this design is that its central primitive is not hypothetical. It is a parameter on a production API that a large share of client estates already depend on.

AWS states the rule in almost the words the derivation arrived at: the permissions for a session are the intersection of the identity-based policies for the entity used to create the session and the session policies. And immediately after: session policies limit permissions for a created session, but do not grant permissions. That is the mint, in production, since long before agents existed — a role-assumption call takes an inline policy or up to ten managed session policy references, and what comes back is bounded by both sides at once.

The same page states the conjunctive logic again at identity level: with a permissions boundary set, an entity can perform only the actions allowed by both its identity-based policies and its boundaries — and where all three apply, the session's permissions are the intersection of the session policy, the permissions boundary and the identity-based policy. Three restricting terms, conjoined. That is the caveat model, and it is why the code below unions caveats rather than intersecting them: caveats restrict, so combining them conservatively means keeping all of them. It is also the commercial point — if any of the estate you inherited runs there, the client already pays for this mechanism.

Now the other half, which is why this cannot simply be delegated to the estate's existing authorization. The Kubernetes documentation is plain: a role or cluster role contains rules representing a set of permissions, and permissions are purely additive — there are no deny rules. A cluster role is non-namespaced, so a grant written once applies across all namespaces. Additive with no subtractive form is a union algebra: you can add a permission to a subject, but you cannot hand a subject a copy of itself with less. That is not a criticism; for cluster authorization it is a defensible choice. It is an observation about what you can build on — if the estate's own model has no attenuation operator, attenuation has to live above it.

The database layer supplies the third piece, and it turns an abstract argument about closures into a defect a delivery lead can check this afternoon. PostgreSQL documents both halves of the trap. By default, tables have no policies, so if a user has access privileges to a table according to the SQL privilege system, all rows within it are equally available for querying or updating. And where policies do exist, superusers and roles with the bypass attribute always bypass row security, and table owners normally bypass it as well — though an owner can choose to be subject to it by forcing row-level security on the table.

Read that against how a connector is usually wired. The delivery team needs a role that can create the objects the integration requires, so it gets the owning role — which bypasses row security. The policies the client's data team wrote, the reason they believed the tenant separation held, are not enforced against the connector at all: silently, no error, nothing in a log. The connector works. The tests pass. Acceptance is signed. And the agent behind it reads every row the policies were written to hide.

Above all of this sits the protocol layer, candid about what it does not carry. The Model Context Protocol specification, at its 2025-11-25 revision, requires that a tool's own behavioural annotations be treated as untrusted: clients must consider tool annotations untrusted unless they come from trusted servers. The definition itself carries a name, a title, a description, icons, an input schema, an optional output schema, annotations and execution hints. No field expresses the resource set; none expresses the constraint. Blast radius is not part of the wire format.

The protocol's own security guidance, at the same revision, names the failure mode in its own vocabulary. Its scope-minimisation section lists as risks an expanded blast radius, where a stolen broad token enables unrelated tool and resource access, and privilege chaining, where an attacker can immediately invoke high-risk tools without further elevation prompts. Its catalogue of common mistakes reads like a description of how connectors get wired on a deadline: publishing all possible scopes in the supported list, using wildcard or omnibus scopes, bundling unrelated privileges to preempt future prompts. The same document also names as an anti-pattern the practice firms most often reach for when wiring an agent into an inherited estate — token passthrough, where a server accepts tokens it was not issued and passes them downstream — and the rule is normative: servers must not accept any tokens that were not explicitly issued for them.

One more piece of vocabulary, with its limit attached. OAuth token exchange distinguishes impersonation, where any entity receiving the token is, as far as it can tell, dealing with the original principal, from delegation, where the acting party keeps its own identity and some rights are explicitly understood to have been delegated. Its actor claim records that delegation occurred and identifies who is acting. Delegation, not impersonation, is what an agent inside a client estate does, and the receipt trail depends on the distinction. The limit: the specification does not require an exchanged token to be narrower than the one presented — attenuation is requested by the client and granted at the authorization server's discretion. Anyone who tells you token exchange enforces least privilege has not read it.

OWASP's catalogue closes the loop, and its worked example is the one from three paragraphs ago: under excessive agency it describes an extension intended to read data connecting to a database with an identity holding not only select but update, insert and delete. Its recommendation is to enforce authorization in the downstream systems rather than in the tool layer.

The data model

Three files. The types, because the triple is the argument and should be readable as a type. The broker, because the claim that nothing on the minting path can widen a grant ought to be checkable by reading rather than trusted. The analyser, because the closure is the part people assume is expensive and it is a breadth-first search.

Configuration

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

Nothing here knows what a connector is, deliberately: they were delivered by the engagement and are not being rewritten. This layer takes a standing holding, a declared intent and a graph, and returns a grant or a refusal — plus a receipt either way, because a refusal you cannot show is a control you cannot evidence.

Note what is absent: no connector name appears anywhere. A connector name labels an operation; this module is about the resources an operation may reach. Note also that the caveat type has no removal form — attenuation that can be undone is not attenuation.

packages/broker/src/capability.ts
/** Verbs are a closed vocabulary the broker owns — not a list connectors publish about
 *  themselves. A delivered connector maps its operations onto these; it does not add one. */
export type Verb = "read" | "list" | "write" | "append" | "delete" | "invoke" | "send";

/** Names a concrete thing in the estate you inherited. There is deliberately no wildcard form:
 *  `mint` rejects any selector whose id is empty, "*", or a trailing-star prefix. A wildcard is
 *  how least privilege dies without anyone watching it die. */
export type ResourceSelector = {
  /** Stable id from the estate manifest: "pg.reporting", "s3.exports", "ticketing.cs". */
  readonly system: string;
  /** "pg.table" | "s3.prefix" | "ticket.project" | "mail.thread" — declared by the manifest. */
  readonly kind: string;
  readonly id: string;
  /** Row- or object-level narrowing, pushed down to the connector. Literal values only,
   *  never expressions: an expression is a place for someone else's input to arrive. */
  readonly where?: Readonly<Record<string, string>>;
};

/** Caveats restrict. They are combined by union, never by intersection, because keeping every
 *  restriction from both sides is the conservative operation. */
export type Caveat =
  | { readonly k: "expiresAt"; readonly v: string }          // RFC 3339 instant
  | { readonly k: "boundToTask"; readonly v: string }
  | { readonly k: "boundToTenant"; readonly v: string }
  | { readonly k: "maxCalls"; readonly v: number }
  | { readonly k: "noEgressBeyond"; readonly v: readonly string[] }
  | { readonly k: "humanApproval"; readonly v: "required" };

export type Capability = {
  readonly verb: Verb;
  /** Non-empty by construction — the mint refuses an empty resource list rather than
   *  letting it mean "unconstrained", which is the default every such system drifts into. */
  readonly resources: readonly ResourceSelector[];
  readonly caveats: readonly Caveat[];
};

/** The ceiling. Written once, reviewed like code, and living in the client's repository rather
 *  than in yours — this is the object that has to survive your team leaving. */
export type Holding = {
  readonly principal: string;
  readonly capabilities: readonly Capability[];
};

/** What the runtime says it is about to do. A claim, not an authority: it can only narrow. */
export type TaskIntent = {
  readonly taskId: string;
  readonly tenant: string;
  readonly requested: readonly {
    readonly verb: Verb;
    readonly system: string;
    readonly kind: string;
    readonly id: string;
    readonly where?: Readonly<Record<string, string>>;
  }[];
  readonly ttlSeconds: number;
};

export type Grant = {
  readonly grantId: string;
  readonly taskId: string;
  readonly principal: string;
  readonly capabilities: readonly Capability[];
  readonly issuedAt: string;
  readonly expiresAt: string;
};

export type Refusal =
  | { readonly code: "WILDCARD_SELECTOR"; readonly detail: string }
  | { readonly code: "TTL_EXCEEDS_CEILING"; readonly detail: string }
  | { readonly code: "EMPTY_INTERSECTION"; readonly detail: string }
  | { readonly code: "CLOSURE_EXCEEDS_INTENT"; readonly detail: string };

/** Written on success and on refusal alike. This is the evidence object the audit right in the
 *  third-party guidance has never had anything to inspect. */
export type Receipt = {
  readonly at: string;
  readonly principal: string;
  readonly taskId: string;
  readonly requestedCount: number;
  readonly grantedCount: number;
  readonly closureNodes: number;
  readonly externalSinks: readonly string[];
  readonly outcome: "granted" | "refused";
  readonly reason?: string;
};

export type MintResult =
  | { readonly ok: true; readonly grant: Grant; readonly receipt: Receipt }
  | { readonly ok: false; readonly refusal: Refusal; readonly receipt: Receipt };

Three things this deliberately does not do. It does not sign anything: how the grant reaches a connector — a signed token, a request-scoped context, a row the adapter reads — is estate-specific, and is the one place the design bends to what the client already runs. It does not cache the closure, because a stale closure is worse than a slow one. And it does not infer the graph: every edge is written by a human who knows why it exists.

The control path

The architecture is four layers, three of them marked unchanged. That is not modesty; it is the constraint doing its job.

FIGURE 2 · WHERE THE BROKER SITS One layer added. Nothing beneath it changes. LAYER 4 · UNCHANGED Agent runtime the model, the loop, the planner — whatever the client already chose holds no credential of its own, and never has ↓ declares an intent · holds no credential LAYER 3 · THE ONLY THING YOU ADD The broker intent → intersect → mint → reachability gate → receipt one process, one table, no migration beneath it ↓ presents a minted, expiring, attenuated grant LAYER 2 · UNCHANGED The integrations you already delivered the nine connectors, their adapters, their existing service credentials not renegotiated, not rewritten, not re-accepted ↓ the same calls, against the same systems LAYER 1 · UNCHANGED The client's estate reporting database · object store · ticketing · mail relay · ERP the thing you inherited and do not get to redesign WHAT SURVIVES HANDOVER Grant catalogue in the client's repo, reviewed like code Closure report per release, diffable, produced by the gate Receipt log into the logging system the client already runs Revocation runbook who kills a grant at 03:00, and how None of these needs you in the room to keep working. Three of the four layers are marked unchanged on purpose. That is what lets the broker be added to an engagement already in flight, rather than sold as a migration.

Step by step, in the order a request actually moves.

  1. *The runtime declares an intent.* Before the model is handed anything, the orchestration layer writes a task intent: a task id, a tenant, a list of verb-plus-resource requests, a time to live. In practice the planner step of whatever framework the client already chose writes it, and it can be wrong or adversarial without breaking anything, because a declaration only narrows.
  2. *The broker checks the shape.* Wildcards are refused outright; a time to live above the account ceiling is refused. Both produce a receipt. This is cheap, and it catches most of the ways a grant goes wide in practice — not through a clever attack but through a hurried default.
  3. *The broker computes the intersection.* For each request, find something in the standing holding that covers it. If nothing does, the request is dropped silently — not defaulted, not escalated, just absent from the grant. If nothing survives at all, the mint is refused.
  4. *The reachability gate runs.* Seed with the systems the surviving capabilities name, walk the estate graph to a bounded depth, and compare the closure's external sinks against what the intent named. If the grant reaches outside the estate along a path nobody asked for, refuse — unless every capability already carries a caveat forbidding that egress.
  5. *The broker mints and binds.* The surviving capabilities get the bindings added: an expiry, the task id, the tenant, the permitted egress set. The grant is returned with an identifier.
  6. *The adapter enforces.* The delivered connector reads the grant, pushes the resource filter into the call it was already making — a where clause, a prefix, a project key — and refuses anything the grant does not name. This is the only place existing integration code is touched: one parameter in, one guard clause.
  7. *A receipt is written either way.* Granted or refused, a row goes to the client's existing logging system with the counts, the closure size and the reason. Nobody reads these day to day; they exist so the audit right the client already holds has something to inspect.

One decision in that list deserves defending, because it looks wrong. Step three drops unheld requests silently rather than raising an error, and an error would be more helpful to a developer. But an error on the mint path is a signal about the shape of the holding, and what receives it is a model that may be following instructions which arrived in retrieved content. Silence turns a probe into a dead end. Put the diagnostics in the receipt, where a human reads them and the model does not.

The reachability gate

The analyser decides whether this design is taken seriously, because it is the only part that finds something a competent reviewer could not have found by reading.

FIGURE 3 · THE CLOSURE OF A ONE-LINE GRANT The grant names one system. The closure names four. CONSTRUCTED ILLUSTRATION GRANTED pg.reporting verb: read nightly export REACHED · 1 s3.exports object store prefix ticket attachment REACHED · 2 ticketing.CS shared project requester notification EXTERNAL SINK mail.relay delivers outside the estate ANALYSER OUTPUT grant.verbs 1 grant.systems 1 closure.nodes 4 external.sinks 1 longest.path 3 verdict: DENY unless a noEgressBeyond caveat is attached Every edge here is a connector the same statement of work paid for. Each was reviewed on its own and accepted on its own. No review of any single connector finds the path, because the path is not a property of any connector. It is a property of the set. The scenario is constructed to be concrete. No grant-to-closure ratio for a real estate was found published anywhere.

The scenario in that figure is constructed. It is not an incident, not drawn from any engagement, and deliberately mundane — an export job, an attachment rule, a notification. That is the point: none of those edges is a security defect, none would fail a review, and each exists because someone asked for it and paid for it. The defect is that the three compose into a path from a governed table to an unbounded external address, and the composition is not visible from any of them.

Building the graph is the work. No API returns it, and the estates this targets have no inventory of their own data flows — itself the finding that usually lands hardest with a sponsor. You build it from four sources: the integration designs the engagement produced, which give most of the deliberate edges; the scheduler, which gives the jobs; the notification and webhook configuration in each system, which gives the edges nobody thinks of as integrations; and two or three people who have been there long enough to know what the previous vendor left behind.

Be honest about what the graph omits, because a closure report that overclaims is worse than none. It contains the edges you wrote down. It does not contain a path through an undocumented shared filesystem, an operator with credentials to two systems, or a report someone emails weekly from a personal export. The analyser's guarantee is bounded to its input, and the report should say so in its first line.

The concern is not runtime cost but staleness. A graph that was accurate at handover and has not been touched since asserts a safety property nobody is maintaining, which is a worse artefact than an absent one. That is why the closure report is versioned per release and diffed, and why the runbook names an owner.

How this design fails

Six ways, in rough order of how likely they are to be what actually goes wrong.

The holding becomes the toolbelt. The standing holding is meant to be a tight ceiling humans review. Under delivery pressure it becomes a wide ceiling nobody reviews, because every refused mint is a ticket, every ticket is friction, and the fastest way to close it is to widen the holding. Six months on, the intersection is a no-op. This is the most likely failure by a wide margin, it is social rather than technical, and the only defence is to keep the holding in the client's repository under the same change process as their application code — so widening it is visible to someone who is not the person in a hurry.

The adapter does not push the filter down. The grant says rows where client_id equals a value. The connector receives that and — because pushing a predicate into an existing query is fiddly and the sprint is ending — filters in application code after fetching everything. The grant is now decorative: every row crossed the boundary, the agent merely did not see them. This is the design's blind spot. The broker can constrain what it is asked for; it cannot verify what the connector did. The only mitigation I trust is a test per connector asserting the narrowing happened at the source.

The graph is wrong in the direction that matters. A missing edge produces a closure that is too small, and a closure that is too small produces a grant passing a gate it should have failed. Missing edges are the common error, because the ones people forget are the ones nobody thinks of as integrations: a notification rule, a backup replication, an extract someone built in a spreadsheet tool. The design fails quietly here — no signal, and a confident pass. Over-inclusiveness in the external-sink list is the cheap counterweight.

Intent declaration collapses into a formality. If the planner learns that declaring a wide intent gets more done, it will declare one every time, and the intersection degenerates to the holding. The structural protection holds — a declaration cannot exceed the ceiling — but the discipline the design was supposed to add is gone. Watch the receipts: a rising ratio of requested to granted counts is the signal, measurable from day one because both numbers are already there.

Time to live becomes a source of incidents. Fifteen minutes is short. A long-running task, a retry storm, a queue backlog, and grants expire mid-flight. The correct behaviour is to re-mint, which requires the runtime to hold the intent rather than the grant — and a runtime built by someone who assumed a grant is a credential you keep will not do that. It presents as intermittent failure under load, the worst possible presentation, and it is a genuine argument for a longer ceiling where orchestration cannot re-mint cleanly.

The design does not cover the human path, and never claimed to. Everything above governs what an agent may reach. It says nothing about the operator with standing credentials to six of the nine systems, the offline export, the service account the previous vendor left, or the analyst who can already do everything the agent was refused. If the threat model is insider misuse, this addresses a slice and leaves the rest untouched. Selling it as broader would not survive contact with their security function.

A seventh item belongs here even though it is not a failure of the mechanism. The design creates a control the client's security function did not build and does not own. Handed over badly — a running service with no documented owner, no runbook, no tests — it becomes, in eighteen months, an unmaintained component on the critical path of every agent action in the estate, installed by a firm that is not there. That is the strongest form of the objection I conceded earlier, and it is why the next section exists.

What it costs

Three costs: latency, operational burden, migration difficulty — and where I have no measurement, I will say so.

Latency. The mint path is a shape check over a handful of selectors, a linear scan of the holding, and a bounded breadth-first search over a graph of tens of nodes — all in-process, no network call. I have not benchmarked it, and I am not going to invent a microsecond figure to make the point land. The measurement is planned: a harness minting against synthetic holdings of ten, a hundred and a thousand capabilities over graphs of twenty, two hundred and two thousand edges, reporting p50 and p99. Until it exists the honest claim is qualitative — this is arithmetic against small in-memory structures, and if it shows up in a trace, the implementation is wrong rather than the design. Latency arrives instead at the wrong ceiling: a time to live forcing re-mints inside one task adds a round trip each time.

Operational burden. Two artefacts need an owner: the holding and the graph. The holding changes when the agent's job changes, which on a live account is often. The graph changes when the estate does, less often but less visibly, and a graph nobody updates is the failure mode above. This is a recurring item on someone's plate, and any proposal presenting it as zero-maintenance is misrepresenting it. The honest framing is that the sponsor is taking on the same burden as a firewall rule set, with the same consequences if they stop.

Migration difficulty. This is where the design earns its shape. Nothing beneath the broker changes: not the identity provider, not the data platform, not the connectors, not the credentials they hold. The integration work that was scoped, delivered and accepted stays that way. What changes is one guard clause per connector and one component above them. That is what makes it insertable into an engagement already in flight, and it is also what makes it weaker than an authorization layer designed into the estate from the start — which is the trade, stated plainly.

One cost I will not quantify. The obvious sponsor question is how much narrower the grants get. That number would sell the work, and I could not find it: no standards body, cloud vendor or regulator I searched publishes a measured grant-to-closure ratio, and I found no product that reports one. You can measure it on a specific account once the receipts and the graph exist, and that is a useful line in a closure report. What you cannot do is quote an industry figure, because there is not one.

The handover artefact

For a services firm the running system is not the deliverable. The deliverable is whatever is still true after the team has moved to the next account — and by that test, a set of connectors, a prompt and an institutional memory that walks out of the building is not a handover at all.

Four artefacts, and they are all data rather than services.

  • *The holding, in the client's repository.* A file, in their version control, under their review process, naming what each agent principal is ever allowed to reach. Readable by someone who has never met you, and widening it produces a diff someone has to approve. The most valuable object the engagement leaves behind, and it is a few hundred lines.
  • *The estate graph, with provenance per edge.* Every edge carries what creates it — the job name, the rule id, the webhook. That is what makes it arguable: a reviewer who thinks an edge is wrong can go and look. A graph without provenance is a diagram, and diagrams rot without anyone noticing.
  • *The closure report, per release.* Machine-generated, diffable, short: which grants exist, what each reaches, how many external sinks are in the closure, what changed since last time. This is what the client's audit right has never had to point at, and it is what turns a technical control into something their second line can use.
  • *The revocation runbook.* Who kills a grant, how, at three in the morning, and what breaks when they do. One page. Its existence is the difference between a control the client owns and a component they inherited.

One commercial point, made once. I looked for a published standard for what a professional-services deliverable must hand over regarding capability scope, expiry or reachability at handback — from a standards body, a professional association or a procurement authority — and did not find one. That gap is why the four artefacts are worth proposing as acceptance criteria rather than as good practice — a firm that writes them into a statement of work defines a bar where none exists. It has to be argued from the absence, not asserted from a source that does not exist.

One week

Given a live account, an agent already touching several delivered integrations, and one week of one engineer, this is the build order. It is chosen so each day produces something the client keeps even if the week is cut short on day three.

  1. *Day one — the graph, by hand.* Sit with the integration designs, the scheduler and the notification configuration of each connected system, and write the estate graph as a data file. Do not automate it. The output is a list of nodes, a list of edges each naming the job or rule that creates it, and a pessimistic list of external sinks. This alone produces a conversation the sponsor has never had, and it is the artefact with the longest half-life.
  2. *Day two — the closure, read-only.* Implement the analyser and run it over the authority the agent holds today. Gate nothing. Put the first closure report in front of the delivery lead. If it is boring, you learned something valuable cheaply; if it is not, you have the business case for the rest of the week, written in the client's own systems.
  3. *Day three — the types and the holding.* Write the capability types and the first standing holding for one agent principal, derived from what it actually did last month rather than what someone thinks it needs. Put it in the client's repository from the first commit.
  4. *Day four — the mint, in shadow.* Implement the intersection and mint and run them alongside the live path without enforcing: for every real call, record what the broker would have granted and what the caller used. This is the day that tells you whether the holding is right, and it costs nothing if it is wrong.
  5. *Day five — one connector, enforced.* Pick the connector with the narrowest blast radius and the clearest filter, usually a read against a single table or project, and make it require a grant. One guard clause, one filter push-down, one test asserting the narrowing happened at the source. Ship it behind a flag.
  6. *The rest — receipts, then the second connector.* Wire the receipt into whatever the client already uses for logs: a control with no evidence trail is one the second line cannot see. Then repeat day five per connector in ascending order of blast radius, and leave the runbook behind on the last.

Notice what is not in that week: no identity provider work, no data platform change, no renegotiation of integration scope, no request that the client adopt anything. The first two days produce documents rather than code. That is the shape the constraint forces, and it is why this can be started on a Tuesday of an engagement already half spent.

The argument in one line: you were paid to connect nine systems and you connected them well, and the authority that emerged from connecting them is on nobody's plan, in nobody's estimate and in nobody's acceptance criteria — which makes it the half nobody scoped, and the half that is still there after you leave.