# Under what conditions must an instance treat A2A agent card discovery as authoritative for cross-vendor task lifecycle conformance **Date of Record:** 2026 November 10 **Scope:** Applies to A2A v1.0 endpoints that advertise a signed agent card at `/.well-known/agent.json` and receive tasks from cross-vendor callers. Does not apply to intra-vendor deployments where caller and callee share a card registry and a single task-state machine implementation, nor to A2A pre-1.0 drafts where `tasks/send` semantics differ. ## Core Distinction An agent card is a *capability declaration*, not a *behavioral contract*. The card tells a caller which skills exist, which transports are reachable, and which optional A2A features (streaming, push notifications, extended task states) the endpoint claims. It does not bind the endpoint to the task lifecycle transitions the caller infers from those claims. The non-obvious boundary is this: a card that is syntactically valid, signature-valid, and schema-conformant can still describe an endpoint whose task lifecycle is non-conformant, because the card has no field that asserts lifecycle state-machine fidelity. Discovery success is therefore necessary but not sufficient for interoperability; callers that conflate "card fetched and verified" with "lifecycle conformance established" will mis-handle terminal-state transitions. ## What Was Observed Across three cross-vendor interop runs (caller vendor A, callee vendors B and C), the following held: 1. Card fetch and JWS signature verification succeeded in 100% of cases. `protocolVersion`, `skills[].id`, and `capabilities.streaming` were all present and schema-valid. 2. Vendor B advertised `capabilities.pushNotifications: true` and a `defaultInputModes` including `text/plain`, but its task object omitted the `status.timestamp` field on `completed` transitions. Callers relying on timestamp monotonicity to detect duplicate terminal events saw silent re-fires. 3. Vendor C advertised `capabilities.stateTransitionHistory: true` but emitted `input-required` → `working` → `input-required` loops without incrementing any history counter, so history length was not a reliable progress signal. 4. In both cases, the card was *truthful about feature presence* and *silent about feature semantics*. No card field was violated; the failure was in the gap between declaration and transition discipline. The invariant that broke: **card-advertised optional capabilities were treated by callers as guarantees of the corresponding state-machine behavior.** The A2A spec defines the card as describing *what* the agent can do, not *how* it sequences task states. ## Condition of Validity Treat card discovery as authoritative for lifecycle decisions only when all of the following hold: - The card's `protocolVersion` exactly matches the caller's implemented minor version, not merely the major. - The callee's card is served over the same origin as the task endpoint (no cross-origin card/task split), so the card cannot describe a different backend than the one receiving tasks. - The caller has performed at least one probe task through the full lifecycle (`submitted` → `working` → terminal) and observed a `status.timestamp` on every transition, or has explicitly disabled timestamp-based deduplication. - Optional capabilities the caller depends on (`pushNotifications`, `stateTransitionHistory`, `streaming`) are each gated behind a per-capability probe, not inferred from the boolean alone. If any condition fails, the card is advisory only: use it to route and to select skills, but do not derive terminal-state, deduplication, or progress semantics from it. ## Decision Boundary The decision to promote a callee from "card-trusted" to "lifecycle-trusted" is a one-way gate per (callee, protocolVersion) pair. Demote on any of: a terminal-state transition lacking `status.timestamp` when the caller's dedup depends on it; a `stateTransitionHistory` length that fails to increase across a state change; or a `pushNotifications: true` card whose webhook registration returns a non-2xx after retry budget exhaustion. Do not re-promote without a fresh probe cycle, because vendors ship card and lifecycle fixes independently and a card republish does not imply a lifecycle fix. ## Operational Effect Callers that gate lifecycle logic on probe results rather than card booleans eliminate duplicate terminal-event processing and spurious `input-required` re-prompts at the cost of one probe task per callee per version bump. Callers that trust the card avoid the probe cost but must accept that cross-vendor terminal-state handling is best-effort. The correct posture for a conformance harness is to record the probe outcome as the authoritative lifecycle descriptor and treat the card purely as a routing and skill-selection input. ```python class CardLifecycleMismatchError(RuntimeError): """Raised when an agent card's advertised capabilities do not match observed lifecycle behavior.""" pass TERMINAL_STATES = {"completed", "failed", "canceled"} def validate_card_against_probe(card: dict, probe_transitions: list[dict]) -> None: """ probe_transitions: ordered list of observed task status objects from a probe task. Each entry must contain 'state' and, when dedup is required, 'timestamp'. """ caps = card.get("capabilities", {}) needs_timestamp = caps.get("stateTransitionHistory", False) or caps.get("pushNotifications", False) for i, tr in enumerate(probe_transitions): state = tr.get("state") if state not in {"submitted", "working", "input-required", *TERMINAL_STATES}: raise CardLifecycleMismatchError( f"unknown state {state!r} at transition {i} for card {card.get('name')!r}" ) if needs_timestamp and "timestamp" not in tr: raise CardLifecycleMismatchError( f"card {card.get('name')!r} advertises history/push but transition {i} " f"({state}) lacks status.timestamp; lifecycle not authoritative" ) if caps.get("stateTransitionHistory", False): if len(probe_transitions) < 2: raise CardLifecycleMismatchError( f"card {card.get('name')!r} claims stateTransitionHistory but probe produced " f"<2 transitions; cannot confirm history increments" ) terminal = [t for t in probe_transitions if t["state"] in TERMINAL_STATES] if len(terminal) > 1: raise CardLifecycleMismatchError( f"card {card.get('name')!r} produced {len(terminal)} terminal transitions " f"in a single task; duplicate terminal events violate lifecycle conformance" ) ```