# Which invariants prevent Agent Card discovery from silently succeeding against a stale or partially-replicated registry in A2A v1.0 **Date of Record:** 2026 November 9 **Scope:** Applies to A2A v1.0 cross-runtime delegation where an agent resolves a peer's Agent Card from a discovery registry (well-known URI, registry service, or delegated resolver) before negotiating a task lifecycle. Does not apply to direct, out-of-band card exchange where the caller pins a specific card hash and bypasses the registry entirely. ## Core Distinction Discovery "success" and discovery *validity* are distinct events. A resolver returning HTTP 200 with a well-formed Agent Card proves only that *some* card was served — not that the card is the current authoritative card for the target agent, nor that the registry replica answering the query has converged with the primary. The boundary failure is that A2A v1.0 task negotiation proceeds on the returned card without any protocol-level obligation to confirm the card's freshness or completeness. A stale card yields a *valid-looking* delegation that fails later, during lifecycle negotiation, with an error that points at the wrong layer. ## What Was Observed During cross-runtime delegation between a planner runtime (Rust) and a worker runtime (Python), a task was dispatched against a card advertising capability `task.execute.v2` and endpoint `wss://worker-7.internal/a2a`. The card had been cached at a registry edge replica 40 seconds before the worker completed a rolling deploy that renamed the capability to `task.execute.v3` and moved the endpoint. The worker, receiving a `task.execute.v2` request over the old endpoint, returned a `TaskRejected` with reason `unsupported_capability`. The planner's retry logic — keyed on transport errors, not negotiation errors — did not retry. The task was silently orphaned: accepted into the planner's queue, never executed, never failed. The registry reported the card as `state: active`. The replica had a replication lag watermark of 40s but did not include that watermark in the response, and A2A v1.0 defines no field by which a resolver communicates replica staleness to a caller. ## Condition of Validity The invariant that must hold for discovery to be *operationally* valid: > A resolved Agent Card is valid for delegation only if (a) the card's `version` is strictly greater than or equal to the minimum version the caller requires, AND (b) the resolver's replication watermark is within the caller's staleness tolerance, AND (c) the card's declared capabilities form a superset of the capabilities the caller intends to invoke. Condition (a) alone is insufficient: a stale replica can serve a card whose `version` field was not bumped by the deploy (common when capability names change without a card-version bump, which v1.0 does not forbid). Condition (b) is not expressible in the v1.0 wire format and must be enforced out-of-band by the caller's resolver wrapper. Condition (c) is the only condition the protocol itself checks, and it checks it *after* delegation, at the worker, not at the caller. ## Decision Boundary The boundary between "proceed with delegation" and "re-resolve or fail closed" is crossed when any of the three conditions above is unverifiable. The non-obvious part: **an unverifiable condition must be treated as failed, not as passed.** The default in most resolver implementations is the inverse — absence of a staleness watermark is read as "fresh," absence of a capability-superset check is read as "caller will handle it." That default is the failure mode. Fail-closed on unverifiable freshness means the caller must either pin a card hash out-of-band or refuse to delegate; there is no safe middle ground in v1.0. ## Operational Effect Enforcing the invariant converts a silent orphan into a loud, early, correctly-attributed error at the caller, before the task enters the planner's queue. The cost is a small increase in re-resolution traffic and a hard dependency on a resolver wrapper that exposes the replication watermark the protocol does not carry. The benefit is that cross-runtime delegation failures surface as discovery-layer errors with the stale card attached, rather than as lifecycle-layer errors misattributed to the worker. In the observed incident, enforcing the invariant would have produced a `StaleCardError` at t+0 instead of an orphaned task discovered at t+6h during reconciliation. ```python import hashlib from dataclasses import dataclass from typing import Optional class StaleAgentCardError(RuntimeError): """Raised when a resolved Agent Card cannot be proven fresh and complete.""" pass class CapabilityMismatchError(RuntimeError): """Raised when a card's capabilities do not cover the intended invocation.""" pass @dataclass(frozen=True) class AgentCard: agent_id: str version: int capabilities: frozenset endpoint: str card_hash: str @dataclass(frozen=True) class ResolvedCard: card: AgentCard replication_watermark_s: Optional[float] # None == resolver did not report it def resolve_for_delegation( resolved: ResolvedCard, required_capabilities: frozenset, min_version: int, max_staleness_s: float, pinned_hash: Optional[str] = None, ) -> AgentCard: card = resolved.card # Condition (b): freshness must be verifiable, or pinned out-of-band. if pinned_hash is not None: if hashlib.sha256(card.card_hash.encode()).hexdigest() != pinned_hash: raise StaleAgentCardError( f"card hash {card.card_hash!r} does not match pinned hash" ) else: if resolved.replication_watermark_s is None: raise StaleAgentCardError( f"resolver returned no replication watermark for {card.agent_id}; " "freshness unverifiable and no card hash pinned — failing closed" ) if resolved.replication_watermark_s > max_staleness_s: raise StaleAgentCardError( f"replication watermark {resolved.replication_watermark_s}s exceeds " f"tolerance {max_staleness_s}s for {card.agent_id}" ) # Condition (a): version floor. if card.version < min_version: raise StaleAgentCardError( f"card version {card.version} < required {min_version} for {card.agent_id}" ) # Condition (c): capability superset. missing = required_capabilities - card.capabilities if missing: raise CapabilityMismatchError( f"card for {card.agent_id} missing capabilities {sorted(missing)}" ) return card ```