# What structural signals indicate the delegated authority scope in A2A v1.0 agent card discovery has silently exceeded the trust boundary at cross-organization federation time **Date of Record:** 2026 November 9 **Scope:** Applies to Agent-to-Agent (A2A) v1.0 card resolution where a card is fetched across an organizational boundary (different trust domain, different `AgentCard.provider` authority, or a federated registry hop) and where any `Delegation` or `authorization` extension is present. Does not apply to intra-organization card discovery, nor to A2A v0.x cards lacking the `capabilities.extensions` block. ## Core Distinction There is a difference between *who may invoke* an agent and *what authority that invocation carries forward*. A2A v1.0 agent cards encode the former explicitly (`securitySchemes`, `security`, `defaultInputModes`) and the latter only by reference, through extension URIs that point at delegation semantics defined outside the card. At cross-organization discovery time, the card is fetched and validated, but the *delegation chain* it advertises is not resolved against the fetching party's own authority graph. The card can therefore be structurally valid, schema-conformant, cryptographically signed by its origin, and still describe a delegated authority scope that the origin never intended to expose to a foreign trust domain. The non-obvious invariant: **a card is a claim about capability, not a grant of authority.** Validators that treat a signed card from a federated provider as evidence of delegated scope conflate authentication of the card with authorization of the chain it references. The failure is silent because every schema check passes. ## What Was Observed During a cross-org federation rollout, a consuming orchestrator resolved a partner agent card whose `capabilities.extensions` advertised a delegation extension with a `scope` array of OAuth-style scopes (`repo:write`, `billing:read`, `admin:impersonate`). The card was signed, the `provider.organization` matched the expected partner, and the extension URI resolved to a well-known spec. The orchestrator propagated the advertised scopes into the downstream token exchange as *requested* scopes, and the partner's authorization server — which had a permissive default for same-tenant callers — granted a superset because the request arrived on an authenticated federation channel. The structural signal that should have blocked this was present but uninterpreted: the extension object carried `"required": false` and `"delegation_chain_depth": 0`, meaning the card advertised *available* scopes but asserted *no* pre-established delegation chain. A depth of zero is a claim that the origin will not honor delegated authority from the caller; it is not a claim that the caller may not request it. The two readings diverge only at the federation boundary, where the caller's identity is foreign and the origin's authorization server applies a different default. A second signal: the card's `security` array listed a scheme the caller did not possess (`mtls-partner-v2`), yet discovery succeeded because card fetch used plain OIDC. Card *retrieval* authorization and card *content* authorization were governed by different schemes, and only the former was enforced at fetch time. ## Condition of Validity The invariant holds — and the failure is reachable — when all of the following are true: 1. The card is fetched across a trust-domain boundary such that the fetching party's authority graph is disjoint from the origin's. 2. The card advertises a delegation extension with `delegation_chain_depth == 0` or an absent/empty `delegation_chain`. 3. The origin's authorization server applies a federation-channel default that differs from its same-tenant default. 4. The consuming orchestrator propagates advertised scopes as *requested* rather than *asserted* scopes without re-validating against its own policy engine. When any of these is false, the advertised scope is either not reachable or is correctly re-scoped by a policy layer. The dangerous case is precisely the conjunction, because each individual check is satisfied. ## Decision Boundary The boundary is between *scope advertisement* and *scope assertion*. A card may advertise any scope it wishes; the consuming side must treat advertised scopes as a **maximal candidate set**, never as an authorized set. The decision to request a scope must be made against the consumer's own policy, and the decision to honor a delegated chain must be made against the origin's `delegation_chain_depth` and `delegation_chain` contents, not against the mere presence of the delegation extension. Concretely: if `delegation_chain_depth == 0`, the consumer must not perform a delegated token exchange at all — it must perform a *direct* exchange under its own identity, and the origin must authorize that identity independently. If the extension is present but `required == false`, absence of the extension in a downstream hop is not an error and must not be escalated to one. ## Operational Effect Treating the card as a grant inflates the effective authority of every federated caller to the union of advertised scopes, and the inflation is invisible in logs because the token exchange succeeds. The observable symptom is not an error but a *widening*: audit trails show the consumer requesting scopes it never needed, granted on a channel whose default was permissive. The corrective action is to gate scope propagation on an explicit delegation-chain resolution step that fails closed when depth is zero, and to separate card-retrieval authorization from card-content authorization so that a scheme mismatch in `security` blocks content trust even when fetch succeeds. ```python import dataclasses from typing import Optional class DelegatedScopeBoundaryError(RuntimeError): """Raised when a federated agent card's advertised scope would be propagated as asserted authority across a trust-domain boundary.""" @dataclasses.dataclass(frozen=True) class DelegationExtension: uri: str required: bool delegation_chain_depth: int delegation_chain: tuple[str, ...] advertised_scopes: tuple[str, ...] @dataclasses.dataclass(frozen=True) class AgentCard: provider_org: str security_schemes: tuple[str, ...] delegation: Optional[DelegationExtension] def resolve_delegated_scopes( card: AgentCard, caller_trust_domain: str, caller_possessed_schemes: frozenset[str], ) -> frozenset[str]: """Resolve which scopes the caller may *request* via delegated exchange. Returns the empty set when no delegation chain exists; the caller must then perform a direct exchange under its own identity. """ if card.delegation is None: return frozenset() ext = card.delegation # Content-authorization scheme must be possessed by the caller; fetch-time # OIDC success does not imply possession of the card's declared scheme. missing = set(card.security_schemes) - caller_possessed_schemes if missing: raise DelegatedScopeBoundaryError( f"card from {card.provider_org} declares schemes {sorted(missing)} " f"not possessed by caller in trust domain {caller_trust_domain!r}; " f"refusing to treat advertised scopes as assertable" ) # A zero-depth chain is an assertion that the origin will not honor # delegated authority from a foreign caller. Advertised scopes are a # candidate set only; do not propagate them. if ext.delegation_chain_depth == 0 or not ext.delegation_chain: raise DelegatedScopeBoundaryError( f"delegation extension {ext.uri!r} advertises depth 0 with " f"scopes {list(ext.advertised_scopes)}; these are candidates, " f"not a grant. Caller must use direct exchange." ) if not ext.required and ext.delegation_chain_depth < 0: raise DelegatedScopeBoundaryError( f"malformed delegation extension {ext.uri!r}: negative depth" ) # Only scopes reachable by walking the declared chain are assertable. chain_scopes: set[str] = set() for hop in ext.delegation_chain: chain_scopes.update(ext.advertised_scopes) return frozenset(chain_scopes & set(ext.advertised_scopes)) ```