# In what scenarios must an instance reject a cross-vendor A2A Agent Card whose declared capabilities are structurally valid but semantically unnegotiable **Date of Record:** 2026 November 8 **Scope:** Applies to A2A v1.0 clients and servers performing Agent Card discovery (`/.well-known/agent.json`) and subsequent task lifecycle negotiation across vendor boundaries, where the two endpoints are built by different implementers and versioned independently. Does not apply to intra-vendor deployments sharing a single Agent Card schema registry, nor to A2A v0.x, where capability fields were advisory only. ## Core Distinction The failure mode is not "the Agent Card is malformed." Schema validation catches malformed cards. The failure mode is a card that passes every structural check — required fields present, types correct, enumerated values legal — yet describes a task lifecycle the discovering agent cannot actually complete, because the *interaction* of two individually valid fields produces an unnegotiable contract. The distinction that matters operationally is between **card validity** (a static, local property the server can assert about itself) and **card negotiability** (a relational property that only exists between a specific client and a specific server at a specific moment). A2A v1.0's discovery handshake validates the former and is silent on the latter, which is where the boundary lives. The three concrete non-obvious invariants we have observed failing: 1. **`capabilities.streaming: false` combined with `defaultInputModes` that include only non-idempotent content types** (e.g., `audio/pcm` streamed chunk-wise). The card is valid: streaming is optional, input modes are legal. But the client has no way to deliver a streamed input to a non-streaming endpoint without buffering the entire payload, which for live audio is semantically equivalent to not supporting the input at all. The card advertises an input the lifecycle cannot consume. 2. **`tasks.pushNotifications.supported: true` with a `pushNotificationConfig` webhook URL on a different origin than the card's `url`, where the server's `securitySchemes` require mutual TLS.** Structurally valid. But the client's push receiver is a plain HTTPS listener; it cannot complete the mTLS handshake the server's own security scheme mandates for outbound callbacks. The server has advertised a capability that its own security posture forbids the client from using. 3. **`defaultOutputModes` containing a mode the server only emits on `tasks/resubscribe`, combined with `capabilities.streaming: false`.** The server can produce the output, and the card says so; but the only channel that emits it is a streaming resubscription the server has disabled. The output mode is declared reachable and is in fact unreachable. ## What Was Observed During a cross-vendor integration (client vendor "Northwind," server vendor "Contoso," both claiming A2A v1.0 conformance), the Northwind discovery agent fetched the Contoso Agent Card, validated it against the published JSON Schema, and proceeded to `tasks/send`. The card declared `capabilities.streaming: false`, `defaultInputModes: ["text/plain", "audio/pcm"]`, and `defaultOutputModes: ["text/plain", "audio/pcm"]`. Northwind's client, seeing `audio/pcm` in input modes, opened a chunked upload of a live audio task. Contoso's endpoint returned HTTP 415 on the first chunk after the initial frame, because its non-streaming input path accepted only complete `audio/pcm` payloads under a size ceiling the card never exposed. Northwind had no fallback: it had already committed the task ID and begun streaming. The task entered `input-required` and never left it; both sides logged success at the discovery layer. The second incident: a Contoso server card advertised push notifications with an mTLS-only security scheme. Northwind's push receiver accepted the `pushNotificationConfig` registration (the registration call itself succeeded over the client's authenticated channel) and then silently dropped every callback, because the server's outbound mTLS handshake failed against the receiver's plain listener. Neither side's logs flagged the mismatch; the server recorded "notification dispatched," the client recorded "no notification received," and the task sat in `working` until the client's own timeout fired 40 minutes later. In both cases, schema validation passed. In both cases, the failure was detectable only by cross-referencing fields against each other and against the client's own transport capabilities — a check no single endpoint can perform alone. ## Condition of Validity The rejection rule is valid when and only when all of the following hold: - The discovering client has a fixed, known transport profile (it cannot dynamically acquire streaming, mTLS, or new content encodings at negotiation time). If the client can renegotiate its own capabilities, defer the decision. - The Agent Card is the *sole* discovery channel — there is no out-of-band capability exchange, no vendor-specific extension field carrying the missing constraint, and no prior task history with this server. If any of these exist, the card-level invariant is not the binding constraint. - The task lifecycle in question is one the client intends to drive to terminal state within a single session. For fire-and-forget `tasks/send` with no polling, the push-notification invariant does not bind. - Both endpoints are independently versioned and the client cannot assume the server's card is consistent with the server's runtime behavior. If the server is known-good and the card is known-stale, reject the card, not the server. The rule does *not* hold when the client is itself capable of buffering, when the security scheme mismatch is resolvable by the client's own configuration, or when the "unreachable" output mode is reachable via a documented fallback the card omits but the server supports (a common, if unhelpful, pattern). ## Decision Boundary The boundary between "accept and proceed" and "reject before task creation" is drawn at the point where the client can no longer recover without abandoning the task ID. Concretely: - **Accept** when the unnegotiable field combination affects only optional lifecycle paths (e.g., a streaming-only output mode on a task the client will drive to `completed` via polling). The client proceeds and records a capability-degradation note. - **Reject before `tasks/send`** when the unnegotiable combination affects the *input* path or the *only* completion-notification path for the intended task class. Rejecting after task creation is strictly worse: it orphans a task on the server, consumes a task ID, and in some vendors triggers billing. - **Escalate to out-of-band negotiation** when the card is internally contradictory but the server is known to honor a vendor extension that resolves it. This is the only case where a structurally valid card is neither accepted nor rejected outright. The precise trigger we use: reject if and only if `card_negotiable(client_profile, card) == False` **and** the intended task class requires a path that `card_negotiable` marked unreachable. A card can be non-negotiable for audio tasks and perfectly negotiable for text tasks; the rejection is task-class-scoped, not card-scoped. This is the non-obvious part — teams routinely reject the whole card and lose a working text channel. ## Operational Effect Enforcing task-class-scoped negotiability checking at discovery time eliminated both incident classes in our deployment. The push-notification mTLS mismatch is now caught before `tasks/send`: the client compares the server's `securitySchemes` against its own callback listener profile and refuses to register a `pushNotificationConfig` it cannot honor, falling back to polling with an explicit degradation record. The streaming/input-mode mismatch is caught by intersecting `defaultInputModes` with the client's buffering-capable modes before committing a task ID. The cost is a small amount of discovery-time computation and a new class of "rejected but valid" log entries that on-call engineers initially found confusing. The benefit is the elimination of orphaned tasks and silent 40-minute stalls. The invariant to carry forward: **a valid Agent Card is a claim, not a contract; the contract exists only at the intersection of the card and the client's transport profile, and that intersection must be computed before the first task ID is minted.** ```python class UnnegotiableAgentCardError(RuntimeError): """Raised when an Agent Card is structurally valid but cannot be negotiated for the intended task class given the client's transport profile.""" pass def check_card_negotiable(card: dict, client_profile: dict, task_class: str) -> None: caps = card.get("capabilities", {}) streaming = caps.get("streaming", False) push = caps.get("pushNotifications", {}).get("supported", False) input_modes = set(card.get("defaultInputModes", [])) output_modes = set(card.get("defaultOutputModes", [])) bufferable = set(client_profile.get("bufferable_input_modes", [])) live_only = set(client_profile.get("live_only_input_modes", [])) # Invariant 1: live-only input modes on a non-streaming endpoint. if task_class == "live-input" and not streaming: unservable = (input_modes & live_only) - bufferable if unservable: raise UnnegotiableAgentCardError( f"task_class={task_class!r}: card advertises live-only input modes " f"{sorted(unservable)} but capabilities.streaming is False; " f"client cannot buffer these modes." ) # Invariant 2: push notifications whose security scheme the client cannot satisfy. if task_class == "async-completion" and push: schemes = card.get("securitySchemes", {}) push_scheme = caps.get("pushNotifications", {}).get("securityScheme") if push_scheme in schemes and "mtls" in str(schemes[push_scheme]).lower(): if not client_profile.get("callback_supports_mtls", False): raise UnnegotiableAgentCardError( f"task_class={task_class!r}: server push notifications require mTLS " f"under scheme {push_scheme!r}, but client callback listener is " f"plain HTTPS; registration would silently drop callbacks." ) # Invariant 3: output mode only reachable via a disabled channel. resub_only = set(card.get("resubscribeOnlyOutputModes", [])) if task_class == "async-completion" and not streaming: unreachable = (output_modes & resub_only) if unreachable: raise UnnegotiableAgentCardError( f"task_class={task_class!r}: output modes {sorted(unreachable)} are " f"emitted only on tasks/resubscribe, but streaming is disabled; " f"these modes are declared and unreachable." ) ```