# Which invariants prevent silent authority widening in A2A v1.0 multi-hop signed task delegation **Date of Record:** 2026 November 8 **Scope:** Applies to A2A v1.0 Agent Card negotiation where a task is delegated across two or more hops using signed delegation envelopes (JWS over the task descriptor plus capability grants). Does not apply to single-hop direct invocation, to unsigned best-effort handoffs, or to transports that re-mint Agent Cards per request without preserving the original `issuer` chain. ## Core Distinction There are two ways a delegated task can acquire more authority than its originator intended: *explicit escalation* (a hop requests a broader capability scope and is either granted or denied) and *silent widening* (each hop's Agent Card advertises a capability superset, and the receiving agent resolves the effective scope as the union of cards along the chain rather than the intersection of signed grants). Explicit escalation is safe because it is observable and refusable. Silent widening is the failure mode: the signature chain remains cryptographically valid at every hop, every individual envelope verifies, and yet the task executes with capabilities that no single signer ever approved. The invariant that prevents silent widening is: **effective authority at hop N equals the intersection of signed capability grants from hop 0 through hop N, never the union of advertised Agent Card capabilities.** Agent Cards describe what an agent *can* do; delegation envelopes describe what an agent *may* do for this task. Conflating the two is the entire bug class. ## What Was Observed A three-hop chain: orchestrator O → planner P → executor E. O holds `task:delegate`, `fs:read:/data`, `net:egress:internal`. P's Agent Card advertises `fs:read:*` and `net:egress:*` because P is a general planning agent that legitimately serves other workloads. E's Agent Card advertises `fs:write:/tmp` and `net:egress:internal`. O signed a delegation envelope granting P exactly `fs:read:/data` and `task:delegate`. P, negotiating with E, constructed a new envelope. A defect in P's scope-resolution logic computed the grant for E as `card(P) ∩ card(E) ∪ inherited(O)` — note the operator precedence — instead of `inherited(O) ∩ card(E)`. The result: E received `net:egress:*` (from P's card, never granted by O) plus `fs:read:/data` (correctly inherited). Every JWS verified. O's signature covered its own grant to P, and P's signature covered its grant to E, but no signature covered the *derivation* from one to the other. The chain was valid; the authority was widened. The failure was detected only because E attempted an egress to an external host that tripped a network policy unrelated to A2A. Had E stayed within internal egress — still broader than O's grant of `net:egress:internal`? No: `net:egress:*` includes internal, so the widening was invisible to any check that only verified "is the requested action within the effective scope," because the effective scope had itself been corrupted upstream. A second, subtler observation: P's Agent Card was fetched, not pinned. Between O's negotiation and P's negotiation with E, P's card was re-fetched and had been updated (a routine deploy) to add `fs:read:*`. Even a correct intersection implementation would have produced a different result depending on *when* the card was fetched. Card fetch time is not part of the signed transcript, so the derivation is not reproducible from the envelope alone. ## Condition of Validity The intersection invariant holds only under three conditions, all of which must be enforced: 1. **Grant provenance is explicit.** Each envelope carries the full ordered list of prior grants (`chain` claim), not just the immediate parent's grant. A hop that receives only the parent's grant cannot distinguish "parent was granted X" from "parent advertises X." 2. **Card capabilities are advisory, never authoritative.** Scope resolution reads grants from the chain; it reads cards only to pre-flight *reject* requests that are obviously impossible (an agent that cannot read files at all). A card must never contribute a capability to the effective scope. 3. **The derivation is signed.** The receiving hop's envelope must include a hash of the resolution transcript (ordered grants + card digests + resolution algorithm version). Otherwise the widening is not detectable after the fact, only preventable before. If any condition fails, the chain may still verify cryptographically while carrying widened authority. Verification of individual signatures is necessary but not sufficient. ## Decision Boundary When a hop cannot prove the intersection — because the chain claim is absent, because a card digest is missing, or because the resolution algorithm version differs from the one the originator signed under — the correct action is **reject the delegation and return a `DelegationUnprovable` fault to the parent**, not to proceed with the parent's grant alone. Proceeding with the parent's grant is the tempting fallback and it is wrong: it silently promotes the parent's authority to the child's effective authority, which is exactly the widening being prevented, just with a smaller blast radius. The parent must re-issue with a complete chain claim. Conversely, when the intersection is empty but the task is still nominally delegable (e.g., the child needs no capabilities, only to return a result), the delegation is valid; empty intersection is not an error. Distinguish "no authority needed" from "authority could not be computed." ## Operational Effect Teams that implement intersection-with-explicit-chain observe three things. First, delegation failures become loud and attributable: a rejected hop names the missing chain element, and the parent's re-issue is a one-line fix. Second, Agent Card churn stops being a correctness hazard — cards can be redeployed freely because they no longer feed the authority computation, only the pre-flight impossibility check. Third, audit becomes possible: the signed resolution transcript lets an auditor replay the derivation and confirm that the executed scope equals the intended scope, which is the property that was silently absent in the observed incident. The cost is envelope size (the chain grows linearly with hop count) and a stricter negotiation protocol (parents must propagate the full chain, not just their own grant). Both are cheap relative to the alternative, which is a cryptographically valid chain that authorizes actions no signer approved. ```python from __future__ import annotations from dataclasses import dataclass from typing import Iterable class DelegationUnprovable(RuntimeError): """Raised when a hop cannot prove the intersection of signed grants.""" @dataclass(frozen=True) class Grant: capability: str # e.g. "fs:read:/data", "net:egress:internal" @dataclass(frozen=True) class Envelope: issuer: str chain: tuple[Grant, ...] # ordered grants from originator through this hop card_digests: tuple[str, ...] # digest of each hop's Agent Card at signing time algorithm_version: str REQUIRED_ALGORITHM = "intersect-v1" def resolve_effective_scope( envelope: Envelope, child_card_capabilities: Iterable[str], supported_algorithms: Iterable[str] = (REQUIRED_ALGORITHM,), ) -> frozenset[str]: """Compute the effective authority for a delegated task. Effective scope is the intersection of every signed grant in the chain. Agent Card capabilities are used ONLY to reject impossible requests; they never contribute to the effective scope. """ if not envelope.chain: raise DelegationUnprovable( f"envelope from {envelope.issuer!r} carries no grant chain; " "cannot distinguish advertised capability from signed grant" ) if len(envelope.card_digests) != len(envelope.chain): raise DelegationUnprovable( f"envelope from {envelope.issuer!r} has {len(envelope.chain)} grants " f"but {len(envelope.card_digests)} card digests; derivation is not reproducible" ) if envelope.algorithm_version not in set(supported_algorithms): raise DelegationUnprovable( f"envelope from {envelope.issuer!r} signed under algorithm " f"{envelope.algorithm_version!r}, which this hop does not implement; " "refusing to fall back to parent grant alone" ) effective: set[str] = {g.capability for g in envelope.chain[0]} for grant_set in envelope.chain[1:]: effective &= {g.capability for g in grant_set} # Card capabilities may only veto, never widen. child_card = set(child_card_capabilities) impossible = effective - child_card if impossible: raise DelegationUnprovable( f"child advertises no support for {sorted(impossible)!r}; " "signed grant cannot be honored, refusing delegation" ) return frozenset(effective) def negotiate_handoff( parent_envelope: Envelope, child_card_capabilities: Iterable[str], child_grant: Iterable[Grant], ) -> Envelope: """Build the child envelope, propagating the full chain.""" scope = resolve_effective_scope(parent_envelope, child_card_capabilities) # The child may be granted strictly less than the parent holds, never more. proposed = {g.capability for g in child_grant} if not proposed <= scope: raise DelegationUnprovable( f"proposed child grant {sorted(proposed - scope)!r} exceeds " f"effective parent scope {sorted(scope)!r}" ) return Envelope( issuer="child", chain=parent_envelope.chain + (tuple(child_grant),), card_digests=parent_envelope.card_digests + ("",), algorithm_version=REQUIRED_ALGORITHM, ) ```