# Under what conditions must an instance refuse a federated Agent Card whose signature chain is valid but whose delegation depth exceeds the trust anchor's declared hop budget **Date of Record:** 2026 November 12 **Scope:** Applies to cross-organizational agent discovery and invocation where trust is established via signed Agent Cards (e.g., A2A-style cards with JWS signatures) and verifiable credentials (VCs) chained through one or more delegating issuers. Does not apply to single-org deployments where the trust anchor is the instance's own root key, nor to cards presented with a bare self-signed key without a delegation chain. ## Core Distinction A *valid signature chain* and an *authorized delegation chain* are orthogonal properties. Signature verification answers "was each hop signed by the key that the previous hop named as its delegate?" It does not answer "was the previous hop permitted to name a delegate at all, and for how many further hops?" The second question is governed by a *hop budget* — a constraint that must be carried in the credential itself (typically as a `maxDelegationDepth` or `depthRemaining` claim), not inferred from the length of the chain that happens to arrive. The failure mode is subtle because the chain verifies cleanly. Every JWS validates, every VC's `issuer` resolves to the preceding hop's `delegate` key, and the leaf Agent Card's `agentId` matches the subject of the final credential. A naive verifier returns `VALID`. The chain is nonetheless *over-delegated*: some intermediate issuer signed a delegation beyond the depth it was itself granted, or the leaf sits at a depth greater than the trust anchor's declared budget. This is the cross-org analogue of a certificate path-length constraint (`pathLenConstraint` in X.509), and it fails in the same non-obvious way: the cryptographic checks pass while the authorization checks do not. ## What Was Observed During federation between two orgs, Org A's trust anchor (root key `A.root`) issued a delegation credential to `A.broker` with `maxDelegationDepth: 1`. `A.broker` then issued a credential to `B.gateway` — a legitimate single further hop, depth 1, within budget. `B.gateway`, operating under a permissive internal policy, issued a credential to `B.subagent-7` and set `maxDelegationDepth: 3` on it, effectively minting delegation authority it did not possess. `B.subagent-7` presented an Agent Card to an Org A instance. The instance's verifier walked the chain: `A.root → A.broker → B.gateway → B.subagent-7`. All four signatures verified. The leaf credential's `issuer` field resolved correctly to `B.gateway`. The verifier returned `VALID` and the instance accepted the card. The invariant violated: *a delegation credential may not confer a hop budget greater than the remaining budget of its issuer.* `B.gateway` held `maxDelegationDepth: 1` (as received) and had already consumed one hop to reach itself as issuer — its remaining budget to grant downstream was 0. It granted 3. The chain's *effective* depth at the leaf was 3, against an anchor budget of 1. The verifier never computed effective depth against the anchor; it only checked per-link signature validity. A second, quieter observation: the same over-delegation is invisible if you only inspect the leaf. The leaf card was internally consistent — its own `maxDelegationDepth: 3` looked ordinary. The violation only surfaces when you recompute remaining budget backward from the anchor and compare at each hop. ## Condition of Validity A federated Agent Card is acceptable only when all of the following hold simultaneously: 1. Every link in the chain verifies cryptographically (necessary, not sufficient). 2. Each credential carries an explicit `maxDelegationDepth` (or equivalent remaining-budget claim). Absence is treated as depth 0, never as unbounded. 3. For each hop `i` from anchor to leaf, the budget granted by hop `i` to hop `i+1` is `≤ (budget received by hop i) − 1`. 4. The effective depth at the leaf (number of delegation hops from anchor) is `≤` the anchor's declared budget. 5. The leaf's `agentId` and the credential subject agree, and the card's signing key is the one bound in the final credential. Condition 3 is the one that gets skipped. It cannot be checked by validating any single credential in isolation — it requires carrying a running budget through the walk and failing the moment a hop grants more than it holds. A verifier that validates links independently, then checks only the leaf, will pass an over-delegated chain. ## Decision Boundary The boundary is *not* "chain length exceeds anchor budget" alone — that catches the obvious case but misses the case where a middle hop inflates its grant and the leaf lands within the anchor's raw budget anyway by coincidence of short chains. The correct boundary is *per-hop budget monotonicity*: reject as soon as any issuer grants downstream budget `> (its received budget − 1)`, regardless of where the leaf ultimately sits. Two failure directions to keep distinct: - **Over-delegation (reject):** an issuer grants more than it holds. Cryptographic validity is intact; authorization is exceeded. This is the case above. - **Under-delegation (accept with caution):** an issuer grants less than it holds. Legal, but downstream agents may be unexpectedly unable to sub-delegate; surface this as a capability warning, not a rejection. Do not conflate "valid signature" with "authorized depth." The decision to reject is made on the recomputed budget, not on the signature verdict. ## Operational Effect An instance that enforces per-hop budget monotonicity will reject a class of cards that a signature-only verifier accepts. The operational consequence is a hard `TrustFederationError` at card-ingestion time, before any invocation is attempted, with the offending hop identified (`issuer`, `granted`, `held`). This is the desired behavior: the alternative is accepting an agent that has minted authority it was never granted, and discovering the over-reach only after it invokes a downstream capability. Two practical notes for operators. First, budget must be recomputed *backward from the anchor* on every ingestion — caching a leaf's `VALID` verdict across anchor-policy changes is unsafe, because the anchor's budget may be tightened and previously-acceptable chains become over-delegated. Second, log the full recomputed budget vector (`[anchor:1, broker:0, gateway:0, leaf:-3]` in the observed case) alongside the rejection; the negative remaining budget at the leaf is the clearest evidence that a middle hop inflated its grant. ```python from dataclasses import dataclass class TrustFederationError(RuntimeError): """Raised when a federated Agent Card violates delegation-budget invariants.""" pass @dataclass class Hop: issuer: str subject: str max_delegation_depth: int # budget this hop *grants* downstream; absent => 0 signature_valid: bool def verify_federated_card(chain: list[Hop], anchor_budget: int) -> None: """ chain[0] is the credential issued by the trust anchor; chain[-1] is the leaf. Enforces per-hop budget monotonicity, not merely signature validity. """ if not chain: raise TrustFederationError("empty delegation chain") for i, hop in enumerate(chain): if not hop.signature_valid: raise TrustFederationError(f"hop {i} ({hop.issuer}) signature invalid") # Running budget carried from the anchor. Anchor grants anchor_budget. held = anchor_budget for i, hop in enumerate(chain): # This hop consumes one unit of budget to exist as an issuer. held_after_consumption = held - 1 if held_after_consumption < 0: raise TrustFederationError( f"hop {i} ({hop.subject}) exceeds anchor budget: " f"effective depth {i + 1} > {anchor_budget}" ) # The hop may not grant more than it holds. if hop.max_delegation_depth > held_after_consumption: raise TrustFederationError( f"over-delegation at hop {i} ({hop.issuer}): " f"granted {hop.max_delegation_depth}, held {held_after_consumption}" ) held = hop.max_delegation_depth # held is remaining budget at the leaf; negative would already have raised. return None def ingest_card(chain: list[Hop], anchor_budget: int) -> str: verify_federated_card(chain, anchor_budget) return "ACCEPTED" # Observed failing case: A.root(1) -> A.broker(0) -> B.gateway(3) -> B.subagent-7 observed = [ Hop("A.root", "A.broker", max_delegation_depth=0, signature_valid=True), Hop("A.broker", "B.gateway", max_delegation_depth=3, signature_valid=True), Hop("B.gateway", "B.subagent-7", max_delegation_depth=0, signature_valid=True), ] # Raises TrustFederationError: over-delegation at hop 1 (A.broker): granted 3, held 0 ```