# What happens when an instance advertises A2A v1.0 over both gRPC and JSON-RPC but its AgentCard's `preferredTransport` disagrees with the binding actually served **Date of Record:** 2026 November 10 **Scope:** Applies to A2A v1.0 AgentCard discovery and cross-runtime task delegation where a single agent endpoint publishes more than one transport binding. Does not apply to single-binding agents, nor to discovery via a registry that rewrites `preferredTransport` before serving the card. ## Core Distinction An AgentCard is a *declaration*, not a *capability probe*. In A2A v1.0 the card carries `preferredTransport` (a single string hint) and, optionally, `additionalInterfaces` (a list of `{url, transport}` entries). The spec treats `preferredTransport` as advisory: a client MAY use it to pick a binding, but the authoritative statement of what is served is the union of the card's declared interfaces. The non-obvious boundary is that **the card's declared set and the endpoint's actually-listening set can diverge without any error surfacing at discovery time**. Discovery succeeds because the card is a static document; delegation fails later, and only for the subset of callers that honored the hint. The failure is not "wrong transport." It is "correct transport per the card, absent at the socket." A client that reads `preferredTransport: "grpc"` and dials the `url` field will attempt gRPC against an endpoint that only terminates JSON-RPC. The gRPC dial may even succeed at the TCP/TLS layer and fail at HTTP/2 preface or at the first `SendMessage` call, producing a delegation error that looks like a protocol bug rather than a discovery inconsistency. ## What Was Observed Three cross-runtime delegations were attempted against an agent whose card declared: - `url: https://agent.internal:8443` (JSON-RPC binding, HTTP/1.1 + HTTP/2) - `preferredTransport: "grpc"` - `additionalInterfaces: [{url: "https://agent.internal:8443", transport: "JSONRPC"}, {url: "https://agent.internal:9443", transport: "GRPC"}]` The gRPC listener on 9443 was in a `SERVING`-adjacent state but had not completed registration of the `A2AService` handler; it accepted connections and returned `UNIMPLEMENTED` for every method. Observed sequence: 1. Card fetched over HTTPS — 200, well-formed, passed schema validation. 2. Client honored `preferredTransport: "grpc"` and dialed the `url` field (8443), not the `additionalInterfaces` gRPC entry (9443). Result: HTTP/2 negotiation against a JSON-RPC-only path, `415 Unsupported Media Type` on the first frame. 3. A second client resolved the gRPC entry from `additionalInterfaces` and dialed 9443. Result: `UNIMPLEMENTED` on `SendMessage`, because the handler was not registered. 4. A third client ignored the hint, used JSON-RPC on 8443. Result: task accepted and completed. The invariant that broke: **`preferredTransport` named a transport that was not reachable at the card's primary `url`.** The card was internally inconsistent — the hint and the primary URL described different bindings — and no layer in the discovery path validated that consistency. ## Condition of Validity This record holds when all of the following are true: - The agent publishes a single AgentCard with a primary `url` and a `preferredTransport` that does not match the binding served at that `url`. - At least one client implementation treats `preferredTransport` as a directive for the primary `url` rather than as a selector over `additionalInterfaces`. - The gRPC binding is either absent, unregistered (`UNIMPLEMENTED`), or behind a different port than the primary `url`. It does **not** hold when the card omits `preferredTransport` (clients fall back to the primary `url`'s implied binding), when a registry normalizes the card, or when the client probes `additionalInterfaces` first. Under those conditions the inconsistency is masked and delegation proceeds. ## Decision Boundary The operational choice is where to enforce card/endpoint consistency: - **Enforce at publish time.** The agent's own startup must refuse to serve a card whose `preferredTransport` is not backed by a listening, handler-registered binding at the primary `url` or a matching `additionalInterfaces` entry. This is the only point where both facts (card content and socket state) are known to the same process. - **Enforce at call time.** A delegating client must treat `preferredTransport` as a *hint over `additionalInterfaces`*, never as a binding for the primary `url`, and must fall back to the next declared interface on `UNIMPLEMENTED` / `415` / connection-refused. This is robust but converts a discovery defect into per-call latency and retry noise. - **Do not enforce at discovery.** Schema validation cannot catch this: the card is valid. Any check that only inspects the document will pass. The boundary is sharp: if you cannot guarantee publish-time consistency, the client MUST implement fallback, and the fallback MUST distinguish `UNIMPLEMENTED` (handler missing — try next interface) from `UNAUTHENTICATED` (do not retry blindly). ## Operational Effect Left unhandled, the inconsistency produces delegation failures that are misattributed. The gRPC-path failure (`415` on the primary URL) reads as a content-type bug; the `UNIMPLEMENTED` on 9443 reads as a version skew. Both are the same defect: a card that advertised a transport it did not serve at the address it named. The blast radius is asymmetric — clients that ignore the hint succeed, clients that honor it fail, so the failure correlates with client implementation rather than with the agent, and reproduces only across runtimes. The fix is a startup invariant, not a retry policy. ```python class AgentCardBindingError(RuntimeError): """Raised when an AgentCard's declared transport set is not backed by a served binding.""" pass def validate_card_against_bindings(card: dict, listening: dict[str, set[str]]) -> None: """ card: parsed AgentCard with 'url', 'preferredTransport', optional 'additionalInterfaces' listening: mapping of url -> set of transports actually served (handler registered) """ primary_url = card.get("url") preferred = card.get("preferredTransport") interfaces = card.get("additionalInterfaces") or [] declared = {(primary_url, preferred)} if preferred else set() declared |= {(i["url"], i["transport"]) for i in interfaces} for url, transport in declared: served = listening.get(url, set()) if transport not in served: raise AgentCardBindingError( f"AgentCard declares transport {transport!r} at {url!r}, " f"but that binding is not served (served: {sorted(served) or 'none'}). " f"preferredTransport={preferred!r}, primary_url={primary_url!r}." ) # The hint must resolve to a binding served at the primary url, or the card is misleading. if preferred and preferred not in listening.get(primary_url, set()): raise AgentCardBindingError( f"preferredTransport={preferred!r} is not served at primary url {primary_url!r}; " f"clients honoring the hint will fail before delegation." ) def delegate(card: dict, listening: dict[str, set[str]], send) -> object: validate_card_against_bindings(card, listening) # A client that reaches here may still treat preferredTransport as a hint and fall back, # but the publishing agent must never emit a card that fails this check. return send(card["url"], card.get("preferredTransport")) ```