# What happens when an instance advertises an A2A v1.0 Agent Card with a `taskNegotiation` signing key whose `kid` resolves through a cross-vendor JWKS but the artifact exchange `accepts` list is expressed in a media-type family the caller's capability registry has never seen **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 (and v0.9-compatible) peer discovery and task negotiation where the remote Agent Card is fetched over HTTPS and its `securitySchemes`/`signingKeys` are validated against an out-of-band JWKS. Does not apply to same-process agent meshes that share a trust root, nor to callers that operate in "accept-anything" mode with artifact re-encoding disabled. ## Core Distinction Agent Card *discovery* and Agent Card *trust establishment* are two different state machines, and A2A v1.0 lets them complete in the wrong order. Discovery only requires that the card parses and that `capabilities` and `skills` are well-formed. Trust establishment requires resolving every `kid` in `securitySchemes[].signingKeys` against a JWKS the caller already trusts, and — critically — evaluating whether the card's `defaultInputModes` / `defaultOutputModes` and per-skill `inputModes` / `outputModes` are *interpretable* by the caller's artifact exchange layer. The boundary failure: a card can be fully *discoverable and cryptographically valid* while being *semantically unnegotiable*. The signing key resolves, the `kid` matches, the JWS over the card verifies — and yet every subsequent `tasks/send` will fail at artifact exchange because the advertised media-type family (e.g. `application/vnd.vendorx.workflow+json;profile="v3"`) is not in the caller's `MediaTypeRegistry`. Most implementations treat the registry miss as a transport error and retry, which is wrong: it is a *negotiation* failure and retrying it burns the task lifecycle budget without ever terminating. ## What Was Observed A caller (vendor-agnostic orchestrator) discovered a peer card whose `securitySchemes` contained an `a2aKey` scheme with `signingKeys` pointing at `https://peer.example/.well-known/jwks.json`. The `kid` resolved, the card's detached JWS verified against the resolved key, and the peer's `capabilities.streaming` was `true`. The card advertised: ``` "defaultOutputModes": ["application/vnd.vendorx.workflow+json;profile=\"v3\""] ``` The caller's `MediaTypeRegistry` knew `application/json` and `application/vnd.vendorx.workflow+json` (no profile parameter) but not the profile-suffixed form. The caller submitted `tasks/send`, the peer accepted and returned a `Task` in `submitted`, then transitioned to `working`, then emitted an `Artifact` with the profile-suffixed media type. The caller's artifact decoder raised a `MediaTypeNotSupported`, the caller treated it as transient, and re-sent `tasks/send` — producing a *second* task on the peer while the first remained in `working`. The peer's task store accumulated orphans; the caller's signing key was still valid throughout, so no auth error ever surfaced. The failure mode is silent: no 4xx, no JWS failure, just a growing set of `working` tasks and a caller that believes it is retrying a transport hiccup. The invariant that was violated: **a task's lifecycle state is owned by the peer, but the obligation to declare media-type interpretability is owned by the caller, and A2A v1.0 provides no field for the caller to declare it back.** The card's `accepts` is advisory; the caller's registry is authoritative; nothing reconciles the two before `tasks/send`. ## Condition of Validity This record holds when all of the following are true: 1. The peer card is signed and the signature verifies against a resolvable `kid` — i.e., trust establishment *succeeds*. If the signature fails, you get a clean auth error and this boundary does not apply. 2. The caller's artifact layer performs strict media-type matching including parameters (`profile`, `charset`, `version`). Loose matching that strips parameters masks the failure. 3. The caller's retry policy classifies decoder exceptions as transient. If decoder exceptions are classified as terminal, the caller aborts cleanly and the orphan-task accumulation does not occur — but the negotiation still fails, just loudly. 4. The peer does not implement idempotency keys on `tasks/send`. A2A v1.0 makes `idempotencyKey` optional; if the peer honors it, the retry collides with the existing task and the failure becomes visible. It does *not* hold when the peer echoes the caller's `defaultInputModes` in its response `Artifact` (some vendors do this defensively), because then the caller's registry is never exercised on an unfamiliar type. ## Decision Boundary Before issuing `tasks/send` to a newly discovered peer, the caller must run a **pre-negotiation gate** that is distinct from signature verification: - Resolve and verify the card JWS (trust gate). - For every media type in the union of the peer's `defaultOutputModes` and each skill's `outputModes` that the caller intends to consume, check membership in the caller's `MediaTypeRegistry` *with parameter-aware matching*. - If any required output type is uninterpretable, do **not** call `tasks/send`. Instead, either (a) downgrade to a skill whose output modes are interpretable, or (b) fail fast with a terminal negotiation error. The subtle part: "required" is caller-defined. A caller that only reads `status` updates and ignores artifacts can proceed. A caller that consumes artifacts cannot. The gate must therefore be parameterized by the caller's *consumption profile*, not by the card alone. Encoding this as a boolean "can I talk to this peer" is the mistake that produces the silent orphan accumulation. ## Operational Effect When the gate is omitted, the observable signature is: signed card verifies, `tasks/send` returns 2xx, task reaches `working`, artifact decode fails, caller retries, peer creates a second task, repeat. Peer-side task count grows linearly with caller retry attempts; caller-side sees only decoder exceptions and no auth or protocol errors. The failure is invisible to signature-based monitoring and invisible to HTTP-status-based alerting. It surfaces only as a divergence between caller's "tasks sent" counter and peer's "tasks completed" counter — which nobody correlates because they live in different vendors' dashboards. The fix is to make the pre-negotiation gate a hard precondition and to classify `MediaTypeNotSupported` raised during artifact decode as terminal-for-this-task, never transient. The code below encodes that. ```python from __future__ import annotations import re from dataclasses import dataclass, field from typing import Iterable class MediaTypeNegotiationError(RuntimeError): """Raised when a peer card is trusted but its output modes are uninterpretable.""" _MEDIA_TYPE_RE = re.compile( r"^(?P[!#$%&'*+\-.^_`|~0-9A-Za-z]+)/(?P[!#$%&'*+\-.^_`|~0-9A-Za-z]+)" r"(?P(?:\s*;\s*[!#$%&'*+\-.^_`|~0-9A-Za-z]+\s*=\s*(?:\"[^\"]*\"|[^;\s]+))*)$" ) def parse_media_type(value: str) -> tuple[str, frozenset[tuple[str, str]]]: m = _MEDIA_TYPE_RE.match(value.strip()) if not m: raise MediaTypeNegotiationError(f"unparseable media type: {value!r}") base = f"{m.group('type').lower()}/{m.group('subtype').lower()}" params: set[tuple[str, str]] = set() raw = m.group("params") or "" for chunk in raw.split(";"): chunk = chunk.strip() if not chunk: continue k, _, v = chunk.partition("=") v = v.strip().strip('"') params.add((k.strip().lower(), v)) return base, frozenset(params) @dataclass(frozen=True) class MediaTypeRegistry: # Each entry is (base_type, required_params). A registry entry matches an # incoming type only if base matches AND every required param is present # with an equal value. Extra params on the incoming type are permitted. entries: tuple[tuple[str, frozenset[tuple[str, str]]], ...] = field(default_factory=tuple) def supports(self, media_type: str) -> bool: base, params = parse_media_type(media_type) for reg_base, reg_params in self.entries: if reg_base != base: continue if reg_params.issubset(params): return True return False @dataclass(frozen=True) class ConsumptionProfile: """Caller-declared: which output modes it will actually consume.""" consumes_artifacts: bool = True registry: MediaTypeRegistry = field(default_factory=MediaTypeRegistry) def pre_negotiation_gate( card_default_output_modes: Iterable[str], skill_output_modes: Iterable[str], profile: ConsumptionProfile, ) -> None: """ Must be called after JWS verification succeeds and BEFORE tasks/send. Raises MediaTypeNegotiationError if any required output mode is uninterpretable. Callers must treat this exception as terminal. """ if not profile.consumes_artifacts: return # status-only consumers are exempt from artifact negotiation required = list(card_default_output_modes) + list(skill_output_modes) uninterpretable = [mt for mt in required if not profile.registry.supports(mt)] if uninterpretable: raise MediaTypeNegotiationError( "peer card is signed and trusted but advertises output modes the " f"caller cannot decode: {uninterpretable!r}. Refusing tasks/send " "to avoid orphan-task accumulation under retry." ) def decode_artifact(media_type: str, payload: bytes, profile: ConsumptionProfile) -> object: if not profile.registry.supports(media_type): # Terminal, never transient. Do not let a retry wrapper catch this # as a transport error. raise MediaTypeNegotiationError( f"artifact media type {media_type!r} not in caller registry; " "task must be abandoned, not retried" ) import json return json.loads(payload) ```