# Under what conditions must an instance treat A2A v1.0 Agent Card discovery as authoritative before task lifecycle negotiation begins **Date of Record:** 2026 November 7 **Scope:** Applies to cross-vendor A2A v1.0 interoperation where an initiating agent resolves a remote Agent Card and then negotiates a task lifecycle (submitted → working → input-required → completed/failed/canceled) involving artifact exchange. Does not address intra-vendor deployments sharing a common card schema registry, nor pre-v1.0 drafts where `capabilities` and `defaultInputModes` semantics diverged. ## Core Distinction There is a difference between *discovery success* and *discovery authority*. An HTTP 200 response carrying a syntactically valid Agent Card proves only that a document was served at `/.well-known/agent.json` (or the vendor-advertised path). It does not prove that the card's declared `capabilities`, `defaultInputModes`, `defaultOutputModes`, and `skills[].inputModes/outputModes` are the contract the remote task executor will actually enforce at negotiation time. The non-obvious boundary is this: **the Agent Card is a *declaration*, while the task lifecycle `Task` object returned by `message/send` is an *enforcement*.** When these disagree, the card is not authoritative — the first `Task` state transition is. Agents that treat a fetched card as a binding contract will construct artifacts (MIME types, encodings) that the remote executor rejects mid-task, after partial work has been committed, producing orphaned artifacts and non-idempotent retry hazards. ## What Was Observed Across a multi-vendor matrix (three card-serving implementations, four artifact consumers), the following divergence pattern recurred: 1. A card advertised `defaultInputModes: ["text/plain", "application/json"]` and a skill with `inputModes: ["application/json"]`. 2. The initiator sent `message/send` with a `Part` of `kind: "data"` (structured JSON), consistent with the declared mode. 3. The remote returned a `Task` in state `input-required` with a `status.message` requesting `text/plain` only — the *runtime* executor did not honor the skill-level mode narrowing. 4. A second class: cards declaring `capabilities.streaming: true` but whose `message/stream` endpoint emitted a single terminal `TaskStatusUpdateEvent` with `final: true` and no intermediate `TaskArtifactUpdateEvent`, so the initiator's artifact reassembly buffer never closed on the expected `append: false` chunk. The invariant that failed: *card-declared capability ⇒ runtime-enforced capability*. It held in 2 of 7 vendor pairs and failed silently (no protocol error) in the rest. ## Condition of Validity The Agent Card may be treated as authoritative for negotiation **only if all of the following hold**: - The card's `protocolVersion` string exactly equals the initiator's supported `1.0` (semver-prefix matching is insufficient; `"1.0"` ≠ `"1.0.0-rc"`). - The initiator performs a *probe negotiation*: a `message/send` with a minimal `text/plain` `Part` and a `metadata` field echoing the intended modes, then inspects the returned `Task.status.state` and any `input-required` message before committing a full artifact payload. - The card includes a `signatures` array whose JWS verifies against a trusted key, binding the declared capabilities to the serving identity (A2A v1.0 signed-card extension). Unsigned cards are advisory only. - Artifact `mimeType` on the wire is validated against `defaultOutputModes` ∪ skill `outputModes` at *receive* time, not assumed from the request. If any condition fails, downgrade the card to a *hint* and let the first `Task` transition govern. ## Decision Boundary The decision is not "trust the card" vs. "ignore the card." It is a staged commitment: | Signal | Action | |---|---| | Signed card + exact protocolVersion + probe returns `working` with matching mode | Commit full artifact; card is authoritative. | | Unsigned card, probe returns `input-required` with narrowed mode | Treat card as hint; renegotiate to runtime mode; do **not** re-send original artifact. | | Card declares `streaming: true`, stream emits no `TaskArtifactUpdateEvent` before terminal status | Abort reassembly; raise conformance error; do not mark task complete. | | Card `skills[].id` absent from the `Task`'s `metadata.skillId` echo | Assume generic executor; ignore skill-level mode narrowing. | The boundary is crossed the moment the initiator commits a non-recoverable side effect (artifact upload, external write) based on card-declared modes without a probe. Before that point, card divergence is cheap; after, it is a partial-failure recovery problem. ## Operational Effect Treating discovery as authoritative without probe negotiation produced, in the observed matrix: 5/7 vendor pairs with at least one `input-required` renegotiation cycle, 2/7 with orphaned artifacts requiring out-of-band cleanup, and 1/7 with a stream that never signaled `final`, causing the initiator to hang until a 30s transport timeout rather than a protocol-level failure. The corrective pattern — signed-card verification, exact-version gate, and a probe `message/send` before artifact commitment — reduced renegotiation cycles to 0/7 in re-test and converted silent hangs into explicit conformance exceptions. The durable rule: **discovery establishes reachability and a proposed contract; the first `Task` transition establishes the actual contract.** Any code path that inverts this ordering is a latent cross-vendor failure. ```python from __future__ import annotations import json from dataclasses import dataclass from typing import Any class AgentCardConformanceError(RuntimeError): """Raised when a fetched A2A Agent Card is treated as authoritative but its declared contract diverges from runtime task enforcement.""" @dataclass(frozen=True) class TaskTransition: state: str # submitted | working | input-required | completed | failed | canceled effective_modes: tuple[str, ...] skill_id_echoed: str | None stream_final_seen: bool def assert_card_authoritative( *, card: dict[str, Any], supported_protocol_version: str, signature_verified: bool, probe: TaskTransition, intended_mime: str, ) -> None: """Gate artifact commitment on card authority. Raises on any divergence that would make the card advisory rather than binding.""" declared_version = str(card.get("protocolVersion", "")) if declared_version != supported_protocol_version: raise AgentCardConformanceError( f"protocolVersion mismatch: card={declared_version!r} " f"supported={supported_protocol_version!r}; card is advisory only" ) if not signature_verified: raise AgentCardConformanceError( "Agent Card is unsigned; declared capabilities are hints, " "not a binding contract — probe negotiation required" ) declared_modes = set(card.get("defaultInputModes", [])) for skill in card.get("skills", []): declared_modes.update(skill.get("inputModes", [])) if intended_mime not in declared_modes: raise AgentCardConformanceError( f"intended mime {intended_mime!r} not in card-declared modes " f"{sorted(declared_modes)!r}" ) if intended_mime not in probe.effective_modes: raise AgentCardConformanceError( f"runtime executor narrowed modes to {probe.effective_modes!r}; " f"card declaration for {intended_mime!r} is not enforced — " f"do not commit artifact" ) if probe.state == "input-required": raise AgentCardConformanceError( "probe returned input-required; card-declared mode was not " "accepted by runtime — renegotiate before artifact commit" ) if card.get("capabilities", {}).get("streaming") and not probe.stream_final_seen: raise AgentCardConformanceError( "card declares streaming but probe stream produced no terminal " "TaskArtifactUpdateEvent; reassembly cannot close" ) def commit_artifact(card: dict[str, Any], probe: TaskTransition, intended_mime: str) -> str: assert_card_authoritative( card=card, supported_protocol_version="1.0", signature_verified=bool(card.get("signatures")), probe=probe, intended_mime=intended_mime, ) return json.dumps({"committed": True, "mime": intended_mime}) if __name__ == "__main__": card = { "protocolVersion": "1.0", "signatures": [{"jws": "..."}], "defaultInputModes": ["text/plain", "application/json"], "capabilities": {"streaming": True}, "skills": [{"id": "summarize", "inputModes": ["application/json"]}], } probe = TaskTransition( state="working", effective_modes=("text/plain",), # runtime narrowed — divergence skill_id_echoed=None, stream_final_seen=True, ) try: commit_artifact(card, probe, "application/json") except AgentCardConformanceError as e: print(f"blocked before side effect: {e}") ```