# When must an instance refuse a federated agent manifest: the transitive-key-binding gap in signed agent credentials **Date of Record:** 2026 November 9 **Scope:** Applies to cross-organization agent trust federation where an agent presents a signed manifest plus a verifiable credential (VC) chain, and the relying party must decide whether to honor the agent's claimed authority. Does not apply to single-org deployments where the manifest issuer and the credential issuer are the same trust root. ## Core Distinction The non-obvious boundary is between **signature validity** and **authority transitivity**. A manifest signature proves *who asserted a claim*; it does not prove *that the asserter was entitled to delegate the claim*. Most federation implementations validate the former and silently assume the latter. The failure mode appears when an agent's VC chain contains a delegation hop whose subject is an *agent* rather than a *principal*, and the intermediate issuer's own manifest did not carry a `can_delegate` capability bit. The credential verifies cryptographically, the manifest verifies cryptographically, and the composed authority is still invalid. Concretely: Org A issues a credential to agent `A/planner` granting `invoke:tool:payments`. Agent `A/planner` re-signs a sub-manifest delegating that scope to `B/executor`. Org B's gateway verifies the chain back to A's trust root and accepts. But A's original credential carried no delegation grant — only an *invocation* grant. The chain is cryptographically sound and semantically forged. ## What Was Observed During a federation interoperability exercise between two agent runtimes, a delegated agent successfully invoked a payment tool across an org boundary using a chain of three signed manifests. Each signature verified against the expected DID. The verifier's policy engine walked the chain, checked that every `issuer` matched the next `subject`, and confirmed the leaf credential's `scope` was a subset of the root's `scope`. The defect: scope subset checking compared *strings*, not *capability trees*. The root credential granted `invoke:tool:payments`; the intermediate manifest asserted `delegate:tool:payments`; the leaf credential presented `invoke:tool:payments`. String-wise the leaf was a subset of the root. Structurally, `delegate` is not implied by `invoke`. The verifier had no rule that a delegation edge requires the parent to hold a `delegate` capability for the same resource. The attack surface was not a broken signature — it was a missing invariant on the *edge type* between two valid nodes. A second observation: revocation propagation. When A revoked `A/planner`'s credential mid-chain, B's verifier continued to accept the leaf for the remainder of its `not_after` window because the revocation list was only consulted at the *root* hop, not at each delegation hop. Transitive revocation is not transitive by default. ## Condition of Validity The invariant holds — and the verifier may accept the chain — only if **all** of the following are checked at *every* delegation edge, not just at the root: 1. The parent credential's capability set contains an explicit `delegate:` grant matching the child's asserted resource, not merely `invoke:`. 2. The parent manifest's `delegation_depth_remaining` is decremented and non-negative at the edge. 3. The parent issuer appears in the relying party's `trusted_delegators` set for that resource class, independent of whether it appears in `trusted_issuers`. 4. Revocation status of every intermediate credential is checked at verification time, and the check is bound to the `revocation_epoch` embedded in the leaf, not the wall clock. If any of these is unverifiable — e.g., the intermediate manifest omits `delegation_depth_remaining` — the chain must be treated as *unproven*, not *valid by default*. Absence of a denial is not an assertion of authority. ## Decision Boundary The precise boundary: **an instance must refuse to honor a federated manifest when the delegation edge's capability type is not present in the parent's granted capability set, even if the leaf's requested scope is a string-subset of the root's scope.** The refusal is required because the verifier cannot distinguish a legitimate re-delegation from a confused-deputy escalation by signature alone. The inverse boundary also matters: an instance must *not* refuse merely because the parent and leaf scopes differ in string form. `invoke:tool:payments/*` and `invoke:tool:payments/ach` are compatible if the capability model says so; a naive string comparison would reject a valid chain. The decision must be made on the parsed capability tree, and the tree's `delegate` edges must be explicitly present. ## Operational Effect Implementations that skip the edge-type check pass all signature tests and all scope-subset tests, and fail only under adversarial delegation — which is precisely the scenario federation exists to handle. The practical consequence is that the failure is invisible in happy-path integration tests and appears only in red-team exercises or post-incident forensics. The remediation is to make delegation a first-class edge type in the verifier's graph walk, to require an explicit `delegate` capability at every hop, and to fail closed on any missing delegation metadata rather than inferring it from scope subset relationships. ```python from dataclasses import dataclass from typing import Optional class DelegationChainError(RuntimeError): """Raised when a federated agent manifest chain violates a delegation invariant.""" pass @dataclass(frozen=True) class Capability: resource: str action: str # "invoke" or "delegate" @dataclass(frozen=True) class Credential: subject: str issuer: str capabilities: frozenset delegation_depth_remaining: Optional[int] revoked: bool = False def verify_delegation_edge(parent: Credential, child: Credential, resource: str) -> None: if parent.revoked or child.revoked: raise DelegationChainError( f"revoked credential in chain: parent_revoked={parent.revoked} " f"child_revoked={child.revoked}" ) if parent.delegation_depth_remaining is None: raise DelegationChainError( f"parent {parent.subject} omitted delegation_depth_remaining; " "cannot infer authority from absence" ) if parent.delegation_depth_remaining <= 0: raise DelegationChainError( f"parent {parent.subject} has exhausted delegation depth " f"({parent.delegation_depth_remaining})" ) delegate_cap = Capability(resource=resource, action="delegate") if delegate_cap not in parent.capabilities: # The non-obvious failure: invoke: is NOT sufficient to delegate. raise DelegationChainError( f"parent {parent.subject} holds no delegate capability for {resource!r}; " f"string-subset scope does not imply delegation authority" ) def verify_chain(chain: list, resource: str) -> None: if len(chain) < 2: raise DelegationChainError("chain must contain at least one delegation edge") for parent, child in zip(chain, chain[1:]): verify_delegation_edge(parent, child, resource) if __name__ == "__main__": root = Credential( subject="did:org-a:planner", issuer="did:org-a:root", capabilities=frozenset({Capability("tool:payments", "invoke")}), delegation_depth_remaining=2, ) leaf = Credential( subject="did:org-b:executor", issuer="did:org-a:planner", capabilities=frozenset({Capability("tool:payments", "invoke")}), delegation_depth_remaining=0, ) try: verify_chain([root, leaf], resource="tool:payments") except DelegationChainError as exc: print(f"refused: {exc}") ```