# In what scenarios must an instance reject a peer's A2A agent card despite successful schema validation: the capability-declaration/transport-binding decoupling boundary in A2A v1.0 cross-vendor delegation **Date of Record:** 2026 November 14 **Scope:** Applies to A2A v1.0 agent-card discovery where a card passes JSON-Schema and semantic-version validation but the `capabilities` block, `defaultInputModes`/`defaultOutputModes`, and declared `skills[].inputModes` are evaluated against the *actual* transport binding negotiated for delegation. Does not apply to intra-tenant delegation where transport binding is pinned by configuration, nor to cards fetched from a registry that performs binding conformance checks server-side. ## Core Distinction Schema validity of an agent card is a *syntactic* claim. Capability declaration is a *semantic* claim about what the agent can accept over a *specific* transport binding (JSON-RPC over HTTP, gRPC, or the A2A push-notification channel). A2A v1.0 deliberately decouples these: the card's `capabilities` object (e.g., `streaming`, `pushNotifications`, `stateTransitionHistory`) and the MIME-typed `defaultInputModes`/`defaultOutputModes` are declared independently of the `preferredTransport`/`additionalInterfaces` list. This means a card can be 100% schema-conformant, correctly versioned, and yet *internally inconsistent with the transport the delegating agent will actually use*. The boundary is not "is the card valid" but "is the card's declared capability set closed under the binding I will invoke it on." The non-obvious part: the failure is not detected at discovery. It surfaces at first task dispatch — often as a `Task` state of `rejected` or a transport-level `-32601`/`UNIMPLEMENTED`, which the delegating agent's retry logic may misclassify as a transient error. ## What Was Observed Across three cross-vendor integrations (a vendor-A orchestrator delegating to vendor-B and vendor-C workers), the following recurring pattern was recorded: 1. Vendor-B published a card with `capabilities.streaming: true` and `defaultInputModes: ["text/plain"]`, but its `additionalInterfaces` advertised only a gRPC binding whose server implementation returned `UNIMPLEMENTED` for `SendStreamingMessage`. The card was schema-valid; the streaming capability was declared for a binding that did not implement it. 2. Vendor-C declared `skills[].inputModes: ["application/vnd.custom+json"]` at the skill level while `defaultInputModes` omitted that MIME type. Delegation using the skill-level mode succeeded; delegation using the default mode failed with a content-type rejection that surfaced as a generic task failure, not a capability error. 3. A card declared `capabilities.pushNotifications: true` but the `pushNotifications` endpoint returned HTTP 405 on the first webhook registration. The delegating agent had already committed the task to that peer. In all three cases, schema validation passed. The invariant that was violated is: *the declared capability set must be a subset of what is reachable over every binding in `additionalInterfaces` that the delegator may select.* A2A v1.0 does not require this subset relation to be checked at card publication; it is the delegator's obligation at selection time. ## Condition of Validity The rejection rule below is valid only when all of the following hold: - The delegating agent has fetched the card via the A2A well-known URI (`/.well-known/agent-card.json`) or a registry, and has independently validated schema and `protocolVersion` compatibility. - The delegating agent knows, *before* dispatch, which transport binding it will use (i.e., it has selected a binding from `preferredTransport` or `additionalInterfaces`). - The capability in question is one the delegating agent intends to *exercise* (streaming, push, history), not merely observe. - The card is not stale relative to a registry TTL the delegator honors. The rule does **not** hold when the delegator is probing capabilities opportunistically (send-and-see), or when a registry has already performed binding conformance checks and the delegator trusts that registry's attestation. ## Decision Boundary The delegator must reject the card (and not dispatch) when, for the selected binding *b*: - `capabilities.streaming` is true but *b* does not advertise a streaming method, **or** - `capabilities.pushNotifications` is true but *b* exposes no push endpoint, **or** - the MIME type the delegator will send is present in neither `defaultInputModes` nor the target skill's `inputModes`, **or** - the skill-level `inputModes` contradicts `defaultInputModes` in a way that makes the delegator's intended mode ambiguous. The delegator must *not* reject merely because a capability is declared false — that is a legitimate narrowing, not a defect. The boundary is asymmetric: false declarations are safe to honor; true declarations must be verified against the binding. ## Operational Effect Rejecting at selection time converts a class of runtime task failures (which contaminate retry budgets and pollute task-history state) into a deterministic pre-dispatch refusal. In the recorded integrations, this reduced misclassified transient retries by treating binding-capability mismatch as a terminal, typed condition. The cost is one extra binding-resolution step per peer per TTL window; the benefit is that `Task.state` transitions remain meaningful and the delegator's circuit breaker no longer trips on a peer that was never capable of the requested operation on the chosen binding. ```python from __future__ import annotations from dataclasses import dataclass from typing import Iterable class BindingCapabilityMismatch(RuntimeError): """Raised when a schema-valid A2A card's declared capabilities are not closed under the transport binding the delegator will actually invoke.""" @dataclass(frozen=True) class Binding: name: str supports_streaming: bool supports_push: bool @dataclass(frozen=True) class AgentCard: protocol_version: str capabilities: dict[str, bool] default_input_modes: tuple[str, ...] additional_interfaces: tuple[Binding, ...] skills: dict[str, tuple[str, ...]] def select_binding(card: AgentCard, preferred: str) -> Binding: for b in card.additional_interfaces: if b.name == preferred: return b raise BindingCapabilityMismatch( f"preferred binding {preferred!r} not advertised by card" ) def assert_card_usable( card: AgentCard, preferred_binding: str, intended_mime: str, skill_id: str | None = None, ) -> Binding: if card.protocol_version.split(".")[0] != "1": raise BindingCapabilityMismatch( f"unsupported protocolVersion {card.protocol_version!r}" ) binding = select_binding(card, preferred_binding) if card.capabilities.get("streaming") and not binding.supports_streaming: raise BindingCapabilityMismatch( f"card declares streaming=true but binding {binding.name!r} " "does not implement SendStreamingMessage" ) if card.capabilities.get("pushNotifications") and not binding.supports_push: raise BindingCapabilityMismatch( f"card declares pushNotifications=true but binding {binding.name!r} " "exposes no push endpoint" ) allowed = set(card.default_input_modes) if skill_id is not None: if skill_id not in card.skills: raise BindingCapabilityMismatch(f"unknown skill {skill_id!r}") allowed |= set(card.skills[skill_id]) if intended_mime not in allowed: raise BindingCapabilityMismatch( f"intended input mode {intended_mime!r} not declared for " f"skill={skill_id!r} on binding {binding.name!r}" ) return binding ```