# What happens when an instance delegates a capability across an organizational trust boundary without re-negotiating the delegation chain **Date of Record:** 2026 November 8 **Scope:** Applies to cross-org multi-agent systems where an agent in org A delegates a scoped capability to an agent in org B, and that agent then sub-delegates or invokes the capability against a resource owned by org C. Does not apply to intra-org delegation where a single trust root governs the whole chain, nor to stateless RPC calls where the caller's authority is re-verified on every hop. ## Core Distinction There are two structurally different things called "delegation" in federated agent systems, and conflating them is the root of the failure: 1. **Capability transfer** — an unforgeable token (macaroon, biscuit, UCAN, signed JWT with `delegated_to`) that carries the *authority* to act, attenuated to a subset of the issuer's rights. 2. **Trust negotiation** — the out-of-band establishment that org B's policy engine will *accept* tokens minted by org A's issuer key for the specific resource class being touched, at the current epoch. A capability token can be perfectly valid (signature chain intact, attenuation monotonic, not expired) and still fail because the *trust negotiation* that would let the terminal resource owner accept it was never performed for the (A → B → C) path. The token proves authority; it does not prove that the verifier's policy recognizes the issuer. These are orthogonal. Most federation stacks verify the first and assume the second. ## What Was Observed In a three-org federation (A issues, B brokers, C owns the resource), the following sequence produced a silent authority gap: - A issues capability `cap_A` scoped to `resource:R1` with subject `agent_B`, valid 1 hour. - B's agent receives `cap_A`, and per its local policy sub-delegates a *narrowed* capability `cap_B` to `agent_C_worker` (a worker agent that happens to run on C's infrastructure but is registered under B's control plane). - The worker invokes `resource:R1` presenting the chain `cap_A → cap_B`. - C's policy engine validates the cryptographic chain successfully. It checks: signatures valid, attenuation monotonic (yes — `cap_B` ⊆ `cap_A`), time window (yes), subject binding (yes). It **accepts**. The failure: C's trust negotiation had only ever been performed with B as a *direct issuer*. C's policy had a rule "accept tokens whose root issuer is B's key." It did **not** have a rule that distinguished "root issuer is B" from "root issuer is A, brokered through B." Because B re-signed the narrowed capability with B's own key as the chain root (a common brokering pattern to avoid leaking A's issuer identity downstream), C saw a chain rooted at B and accepted it — even though the *authority* being exercised originated at A, an org C had never negotiated trust with and whose policy constraints (e.g., data-residency, purpose limitation) C had never reviewed. The observable symptom was not an error. It was a **successful authorization that should have required a negotiation event**. Audit logs on C showed a valid, signed, in-policy invocation. The violation only surfaced weeks later during a cross-org data-handling review, when it was discovered that A-originated authority had been exercised against C's resource without C's trust framework ever enumerating A as an authority source. ## Condition of Validity The failure occurs precisely when **all** of the following hold: 1. An intermediate broker (B) re-roots the delegation chain under its own issuer key rather than preserving the original issuer as chain root (the "re-sign on broker" pattern). 2. The terminal verifier (C) authorizes based on **root issuer identity** rather than **full issuer set of the chain**. 3. C's trust negotiation was scoped per-issuer and never covered the *transitive* issuer (A). When any one of these fails, the gap does not appear: - If B preserves A as the chain root (chain-of-custody style, e.g., biscuit with appended blocks rather than re-issuance), C sees A in the chain and its per-issuer policy rejects or flags it. - If C authorizes on the *set* of issuers in the chain (requiring all of them to be in its trust set), the missing A negotiation blocks the call. - If C's negotiation is done per *resource class* rather than per *issuer*, the transitive authority is covered by construction. The invariant that must hold: **the set of principals whose authority is being exercised must be a subset of the set of principals C has negotiated trust with, for the resource class in question.** Re-rooting breaks the mapping between "the principal named in the token" and "the principal whose authority is exercised." ## Decision Boundary The operational question is: *when an intermediate agent re-issues a delegated capability, does it preserve the original issuer as chain root, or re-root under itself?* - **Preserve chain root** when the downstream verifier's trust model is per-issuer (the common case in cross-org federation). Cost: the original issuer's identity and any of its policy constraints travel downstream, which may be undesirable if A does not want to reveal itself to C. This is the correct default for regulated data flows, because it lets C enforce its own negotiation set. - **Re-root under broker** only when the broker has *explicitly assumed* the full authority and liability of the original issuer for the downstream resource — i.e., B has itself negotiated trust with C covering A's authority class, and B's re-issuance is a genuine authority transfer, not a convenience. This must be a deliberate, logged, auditable act, not a side effect of how the broker's library happens to mint tokens. The boundary is crossed silently because both choices produce valid tokens. The difference is invisible to cryptographic verification; it is only visible to a policy engine that inspects the *issuer set*, not just the root. ## Operational Effect When the boundary is crossed undetected: - **Authorization succeeds where negotiation should have gated it.** The system's security posture is weaker than its trust graph claims. Audit trails show valid invocations, so post-hoc detection requires reconstructing the chain's issuer set from token internals rather than from the authorization decision. - **Policy constraints from the original issuer are dropped.** If A's capability carried constraints (purpose limitation, data residency, max-uses), and B re-roots, those constraints are only preserved if B faithfully copies them into the new token. Any broker bug or intentional simplification silently strips them, and C has no way to know they existed. - **Revocation does not propagate.** A revoking `cap_A` at its own issuer does not invalidate `cap_B` if C only checks B's revocation list. The transitive authority remains live until `cap_B`'s own expiry. - **Trust negotiation becomes non-idempotent and order-dependent.** C's trust set grows based on which brokers happen to route traffic through it, not based on any explicit enumeration of authority sources. The trust graph is discovered, not declared. Mitigation: verifiers must authorize on the **full issuer set of the chain**, not the root, and brokers must either preserve chain root or perform an explicit, logged authority-assumption step before re-rooting. The check below enforces this at the verifier. ```python from dataclasses import dataclass class TransitiveAuthorityError(RuntimeError): """Raised when a delegation chain exercises authority from an issuer that the verifier has not negotiated trust with for the resource class.""" pass @dataclass(frozen=True) class Capability: issuer: str # principal that signed this link subject: str # principal receiving authority resource_class: str expires_at: float chain_root: str # issuer of the original capability (chain of custody) issuer_set: frozenset # every issuer appearing in the chain, root included def authorize(chain: list[Capability], resource_class: str, negotiated: dict[str, set[str]], now: float) -> None: """ negotiated: map of resource_class -> set of issuers the verifier has performed trust negotiation with for that class. """ if not chain: raise TransitiveAuthorityError("empty delegation chain") terminal = chain[-1] if terminal.resource_class != resource_class: raise TransitiveAuthorityError( f"resource class mismatch: token={terminal.resource_class!r} " f"request={resource_class!r}" ) if terminal.expires_at <= now: raise TransitiveAuthorityError("capability expired") # Monotonic attenuation: each link's issuer_set must be a subset of the # previous link's issuer_set plus the link's own issuer. Re-rooting that # drops a prior issuer is detected here. for prev, cur in zip(chain, chain[1:]): expected = prev.issuer_set | {cur.issuer} if cur.issuer_set != expected: raise TransitiveAuthorityError( f"issuer set discontinuity between {prev.issuer} and " f"{cur.issuer}: expected {sorted(expected)}, " f"got {sorted(cur.issuer_set)}" ) # The core invariant: every principal whose authority is exercised # (the full issuer set, not just the root) must be in the negotiation set. trusted = negotiated.get(resource_class, set()) unnegotiated = terminal.issuer_set - trusted if unnegotiated: raise TransitiveAuthorityError( f"transitive authority from unnegotiated issuers " f"{sorted(unnegotiated)} for resource class {resource_class!r}; " f"negotiated set is {sorted(trusted)}" ) ```