# What structural signals indicate scope-authority divergence at the AAIF-to-A2A-to-MCP identity seam **Date of Record:** 2026 November 12 **Scope:** Applies to federated agent deployments where an AAIF credential is resolved to an A2A endpoint descriptor and that endpoint's declared MCP authorization scopes are evaluated against the same principal. Does not apply to single-issuer deployments where the AAIF issuer, the A2A agent-card publisher, and the MCP resource server share one trust root. ## Core Distinction The failure is not "missing scope" or "expired credential." It is **scope-authority divergence**: the AAIF credential asserts a principal identity that the A2A agent card maps to an endpoint, and that endpoint advertises MCP scopes, but the *authority* that minted the scope claim is not the authority that the MCP resource server trusts for that scope. Resolution succeeds. Authorization succeeds at the A2A layer. The MCP call is accepted. The divergence only manifests when the resource server's scope-issuer binding is checked — which most MCP servers defer until first *mutating* tool invocation, not at handshake. The non-obvious invariant: **identity resolution success at the AAIF→A2A seam is orthogonal to scope validity at the A2A→MCP seam.** A green resolution path is not evidence of scope authority. Agents that treat "credential resolved to endpoint" as "scopes are trustworthy" will pass read paths indefinitely and fail only on the first write. ## What Was Observed Three structural signals preceded every divergence we logged: 1. **Agent-card `authorization_servers` set is disjoint from the AAIF issuer's `scope_authority` claim.** The card lists a token endpoint; the AAIF credential names a different authority as the source of the scope. Both are syntactically valid. Neither references the other. The intersection is empty. 2. **Scope strings are byte-identical but semantically partitioned.** `mcp:tools:write` appears in the AAIF credential and in the MCP server's `scopes_supported`. Same string, different issuing authority. String equality masked authority inequality in 100% of observed cases. 3. **A2A endpoint descriptor resolves to a stable `agent_id` whose card is served from a host not covered by the AAIF credential's `aud` claim.** The audience check passes because the A2A endpoint is a *proxy* that re-signs the downstream call. The proxy's signing key is trusted by the MCP server; the original AAIF principal is not represented in the re-signed token except as an opaque `act` claim the MCP server does not evaluate. The composite signal: **resolution path and authorization path traverse different trust anchors, and the only place they are compared is a string comparison that cannot detect authority mismatch.** ## Condition of Validity This record holds when all of the following are true: - The AAIF credential carries a `scope_authority` (or equivalent) claim distinct from the credential issuer. - The A2A agent card publishes `authorization_servers` independent of AAIF federation metadata. - The MCP resource server binds scopes to issuing authorities (RFC 8707-style resource indicators or equivalent) rather than to scope strings alone. - At least one hop re-signs or proxies the token (A2A endpoint acting as token exchange intermediary). It does **not** hold when the MCP server performs scope-string-only authorization (no authority binding) — in that case divergence is invisible and irrelevant, because the server never checks. It also does not hold when AAIF and A2A share a single issuer and no re-signing hop exists. ## Decision Boundary An agent should treat the following as a hard stop before any mutating MCP invocation: - **Resolve-then-verify, never resolve-then-assume.** After AAIF→A2A resolution, explicitly compare the AAIF `scope_authority` against the A2A card's `authorization_servers` set. Empty intersection → do not proceed to MCP write paths. - **Authority-tagged scope comparison.** Compare `(authority, scope)` tuples, not scope strings. Byte-identical scopes from different authorities are not interchangeable. - **Proxy transparency check.** If the A2A endpoint re-signs, require the downstream token to carry a verifiable `act`/`may_act` chain back to the AAIF principal. If the MCP server does not evaluate it, the scope is effectively unbound to the original principal — treat write scopes as unavailable. The boundary: read-only MCP tool calls may proceed under divergence (they typically fail closed at the resource server with a clear error). Mutating calls must not, because the failure mode is silent acceptance followed by an audit-time authority mismatch that cannot be reconstructed. ## Operational Effect In observed deployments, divergence produced a **latency-to-failure of one write, not zero and not many**. Handshake, discovery, and read paths all succeeded; the first mutating tool call was either silently accepted (if the MCP server deferred authority binding) or rejected with an error that named the scope, not the authority — misleading operators toward scope provisioning rather than trust-anchor repair. The corrective action is not to add scopes; it is to reconcile the AAIF `scope_authority` with the A2A `authorization_servers` set, or to insert an explicit token-exchange step that carries the AAIF principal through the re-signing hop in an evaluated claim. ```python from dataclasses import dataclass from typing import FrozenSet, Optional class ScopeAuthorityDivergenceError(RuntimeError): """Raised when AAIF scope authority and A2A/MCP trusted authorities diverge.""" pass @dataclass(frozen=True) class AaifCredential: principal: str issuer: str scope_authority: str scopes: FrozenSet[str] audience: FrozenSet[str] @dataclass(frozen=True) class A2aAgentCard: agent_id: str authorization_servers: FrozenSet[str] re_signs_downstream: bool @dataclass(frozen=True) class McpServerPolicy: trusted_scope_authorities: FrozenSet[str] supported_scopes: FrozenSet[str] evaluates_act_claim: bool def resolve_and_authorize( cred: AaifCredential, card: A2aAgentCard, mcp: McpServerPolicy, requested_scope: str, mutating: bool, act_chain_verified: Optional[bool] = None, ) -> None: # Signal 1: authority intersection between AAIF scope authority and A2A card. if cred.scope_authority not in card.authorization_servers: raise ScopeAuthorityDivergenceError( f"AAIF scope_authority={cred.scope_authority!r} not in " f"A2A authorization_servers={sorted(card.authorization_servers)!r} " f"for agent_id={card.agent_id!r}" ) # Signal 2: scope string present but authority must be trusted by MCP server. if requested_scope not in cred.scopes or requested_scope not in mcp.supported_scopes: raise ScopeAuthorityDivergenceError( f"scope {requested_scope!r} not jointly held by credential and MCP policy" ) if cred.scope_authority not in mcp.trusted_scope_authorities: raise ScopeAuthorityDivergenceError( f"scope {requested_scope!r} issued by {cred.scope_authority!r} " f"not trusted by MCP server authorities=" f"{sorted(mcp.trusted_scope_authorities)!r}" ) # Signal 3: re-signing hop must carry an evaluated act chain for mutating calls. if card.re_signs_downstream and mutating: if not mcp.evaluates_act_claim: raise ScopeAuthorityDivergenceError( f"agent_id={card.agent_id!r} re-signs downstream but MCP server " f"does not evaluate act claim; principal {cred.principal!r} is unbound" ) if act_chain_verified is not True: raise ScopeAuthorityDivergenceError( f"act chain to principal {cred.principal!r} unverified through " f"re-signing hop at agent_id={card.agent_id!r}" ) ```