# Which invariants prevent silent capability escalation when two federated agent meshes negotiate delegated authority across organizational policy boundaries **Date of Record:** 2026 November 10 **Scope:** Applies to cross-organization agent federation where capability grants are negotiated at runtime via policy intersection between two independently administered authorization domains, and where an agent in domain A invokes a capability hosted by domain B on behalf of a third principal. Does not apply to single-tenant meshes, to static pre-shared credential models, or to delegation that is re-authorized at every hop by a single central policy decision point (PDP). ## Core Distinction The distinction that matters is between **capability attenuation** and **capability intersection**. Most federation designs assume these are the same operation: when domain A delegates `{read, write, delete}` to domain B and B's policy permits `{read, write}`, the naive result is `{read, write}` — an intersection, and intuitively safe because the result is a subset of both inputs. The failure is that intersection is a *set* operation over capability *names*, while authority is a *relation* over (principal, resource, action, context). Two domains can agree perfectly on the capability name `write` and still disagree on what `write` means in terms of scope: A may intend "write within tenant A's namespace," while B's `write` is "write within the object's owning namespace, resolved at B." The intersection of names yields a capability that is a subset of neither domain's actual intent. This is silent capability escalation: the negotiated grant is not a subset of the delegator's authority, and no error is raised because the *names* matched. ## What Was Observed During a federation between a data-processing org (call it the delegator, D) and an analytics org (the delegate, G), D negotiated a delegation of `dataset.transform` to G for a batch pipeline. G's policy engine resolved `dataset.transform` against its own resource hierarchy and returned a grant that included a transitive `dataset.export` — because in G's model, `transform` on a dataset implies read-access to derived artifacts, and `export` was a pre-existing capability G held on its own side that it folded into the delegated session as an "enabling" privilege. D's policy had no `dataset.export` in its vocabulary at all, so D's intersection check saw an unknown capability, applied its default of `permit-unknown-within-session` (a misconfigured but common default), and the session was established. For eleven days, G's agents could export D-origin datasets to G-controlled storage. No audit event fired because every hop was individually authorized under *some* policy; the violation existed only at the composition boundary, which no single PDP evaluated. The invariant that was violated is not "the delegated set must be a subset of the delegator's set." It is stronger: **every capability in the negotiated session must be nameable in the delegator's vocabulary, and its resource-scope resolution must be performed by the delegator's resolver, not the delegate's.** The first clause catches unknown-capability folding; the second catches the scope-semantics mismatch that survives even when names match. ## Condition of Validity The invariant holds only when three conditions are jointly satisfied, and each is independently easy to break: 1. **Vocabulary closure at negotiation time.** The delegator must be able to enumerate the full transitive closure of capabilities the delegate might emit, including "enabling" privileges the delegate attaches to satisfy its own internal consistency rules. If the delegate's policy engine can synthesize capabilities not present in the request, closure requires either a declared synthesis manifest or a dry-run resolution pass before commitment. 2. **Scope resolution authority stays with the delegator.** Resource identifiers in the negotiated grant must be resolved against the delegator's namespace, or the grant must carry explicit scope bindings that the delegator validates. If the delegate resolves `dataset:*` against its own hierarchy, the resulting grant is a different object than the delegator authorized. 3. **No default-permit path for unknown capabilities.** The negotiation must fail closed on any capability token the delegator cannot classify. This sounds trivial and is violated constantly, because "unknown capability" is indistinguishable from "capability from a newer protocol version" and teams add permissive defaults to avoid breaking forward compatibility. When any condition fails, the intersection is computed over incomparable lattices, and subset guarantees evaporate. ## Decision Boundary The operational question is: *when is a negotiated grant safe to commit without a full dry-run resolution?* The boundary is the presence of a **synthesis-capable policy engine** on either side. If both domains use pure attribute-based evaluation with no capability derivation rules (no implications, no enabling-privilege folding, no transitive grants), then name-level intersection is sound and a fast path is acceptable. The moment either side's engine can derive a capability not explicitly requested, the fast path is unsound and the delegator must run a resolution-and-diff pass: resolve the delegate's proposed grant under the delegator's own resolver, diff the result against the delegator's authority set, and reject on any element outside it. The cost is one extra round-trip per session establishment; the alternative is a class of escalation that is invisible to per-hop audit. A secondary boundary: if the delegate's grant is *time-bounded and resource-scoped to a single object*, the scope-resolution mismatch is bounded to that object and the risk is contained. The failure becomes severe only when the grant is scoped by pattern (`dataset:*`) or by role, because then the delegate's resolver — not the delegator's — decides the actual extent. ## Operational Effect Teams that adopt the resolution-and-diff pass observe a one-time burst of rejected negotiations during rollout, almost all of them legitimate-but-undeclared enabling privileges. The correct response is to require delegates to publish a synthesis manifest, not to relax the check. Teams that skip the pass and rely on per-hop audit observe zero rejections and no incidents until the first cross-vocabulary delegation, at which point the escalation is invisible in logs and must be reconstructed from storage egress records — a slow, expensive forensic path. The invariant to encode in the federation handshake is: *a capability that cannot be named by the delegator does not exist, and a capability whose scope is resolved by the delegate is a different capability than the one the delegator granted.* ```python from dataclasses import dataclass, field class FederationEscalationError(RuntimeError): """Raised when a negotiated grant is not a subset of delegator authority.""" @dataclass class Grant: capability: str scope_pattern: str resolved_by: str # "delegator" or "delegate" @dataclass class NegotiationContext: delegator_vocabulary: set[str] delegator_authority: set[tuple[str, str]] delegate_synthesis_capable: bool grants: list[Grant] = field(default_factory=list) def commit_negotiated_grant(ctx: NegotiationContext) -> None: unknown = [g for g in ctx.grants if g.capability not in ctx.delegator_vocabulary] if unknown: raise FederationEscalationError( f"delegate emitted capabilities outside delegator vocabulary: " f"{[g.capability for g in unknown]}" ) foreign_scope = [g for g in ctx.grants if g.resolved_by != "delegator"] if foreign_scope: raise FederationEscalationError( f"scope resolved by delegate, not delegator: " f"{[(g.capability, g.scope_pattern) for g in foreign_scope]}" ) if ctx.delegate_synthesis_capable: # Fast path is unsound; require explicit diff against authority set. outside = [ (g.capability, g.scope_pattern) for g in ctx.grants if (g.capability, g.scope_pattern) not in ctx.delegator_authority ] if outside: raise FederationEscalationError( f"resolution-diff found grants outside delegator authority: {outside}" ) # Invariant holds: commit. return None ```