# In what scenarios must an instance reject a peer Agent Card that verifies cryptographically but violates cross-vendor capability invariants **Date of Record:** 2026 November 14 **Scope:** Applies to A2A v1.0 peers that discover remote Agent Cards over signed JSON-RPC and delegate tasks across vendor boundaries. Does not apply to same-vendor intra-cluster delegation where capability contracts are enforced by a shared schema registry, nor to unsigned localhost discovery during development. ## Core Distinction A signature proves *who* authored an Agent Card. It does not prove that the card *describes a task surface the receiving runtime can honor*. The boundary that bites operators is this: **a cryptographically valid Agent Card can still be operationally invalid** because the invariant that matters at delegation time is not authenticity but *capability composability*—whether the declared `skills`, `inputModes`, `outputModes`, and `securitySchemes` of the remote agent can be satisfied by the artifacts the local agent is about to send, and whether the remote's declared task lifecycle semantics match the local orchestrator's assumptions. The non-obvious failure: most implementations treat "signature verified → card trusted → delegate." The correct rule is "signature verified → card *parsed* → capability invariants checked → delegate." These diverge exactly at cross-vendor boundaries, where two vendors can each be internally consistent while their declared MIME types, task-state vocabularies, and streaming assumptions are mutually unsatisfiable. ## What Was Observed During cross-vendor delegation between a vendor-A orchestrator and a vendor-B worker, the following sequence reproduced reliably: 1. Vendor B published a signed Agent Card declaring `defaultInputModes: ["application/json"]` and a skill `invoice.extract` with `inputModes: ["application/pdf"]`. 2. The signature verified against vendor B's published JWKS. `kid` matched, `alg` was `ES256`, the canonicalized payload hashed correctly. 3. Vendor A's orchestrator selected `invoice.extract` and sent a PDF part. The JSON-RPC call succeeded (HTTP 200, valid `SendMessageResponse`). 4. The task transitioned to `failed` with an opaque `-32602`-adjacent application error *after* acceptance, because vendor B's runtime only honored `defaultInputModes` for the transport envelope and treated the skill-level `inputModes` as advisory metadata, not an enforcement contract. The signature was never the problem. The problem is that **A2A v1.0 does not normatively require skill-level `inputModes` to be enforced at the transport layer**; it is a declaration, and vendors interpret declaration-vs-enforcement differently. A card that is *true* (the skill can accept PDFs) and a card that is *enforceable* (the transport will reject non-PDFs before task creation) are different objects, and only the latter gives the orchestrator a safe pre-flight check. A second observed failure: two vendors both advertised `outputModes: ["text/plain"]`, but vendor B emitted streaming `TaskStatusUpdateEvent`s with partial artifacts whose `append` semantics assumed a stateful client, while vendor A's delegation shim used `blocking: true` and concatenated final artifacts only. The signature was valid; the task completed; the output was silently truncated. No error surfaced. This is the dangerous class—**valid card, valid signature, silently lossy delegation**. ## Condition of Validity The invariant holds—i.e., you *must* apply capability-invariant checks rather than trusting the signature—under all of the following: - The peer is a **different vendor** or a different trust domain than the caller (cross-boundary). - The local orchestrator makes **pre-flight decisions** (routing, retry, fallback) based on card-declared fields rather than on observed behavior. - The declared skill surface includes **skill-level overrides** of `defaultInputModes`/`defaultOutputModes`, or declares `streaming: true`, or declares multiple `securitySchemes`. - The local agent sends **non-text artifacts** (PDF, audio, structured JSON with schema) where MIME-type mismatch is possible. When all four hold, signature verification is necessary but not sufficient, and the delegation path must gate on capability invariants. When any fails (same vendor, behavior-driven routing, text-only, single scheme), signature-only trust is acceptable and the extra checks are overhead. ## Decision Boundary The decision boundary is between **rejecting at discovery** and **rejecting at delegation**: - **Reject at discovery** (do not add the peer to the routing table) when the card is *internally inconsistent*: a skill's `inputModes` contains a MIME type absent from `defaultInputModes` **and** the card declares `enforceInputModes: true`; or `securitySchemes` references a scheme not present in the card's `security` array; or the card's `protocolVersion` is below the caller's minimum while claiming a v1.0 `capabilities` block. - **Reject at delegation** (keep the peer, fail the specific task) when the card is internally consistent but the *specific artifact* cannot be matched: the outgoing part's MIME type is not in the resolved `inputModes` for the chosen skill, or the skill declares `streaming: true` while the caller's shim is `blocking`-only and cannot consume `TaskStatusUpdateEvent` partials. The subtle rule: **never downgrade a delegation-time capability mismatch into a task-level `failed` state and retry the same peer.** A capability mismatch is deterministic for that (peer, skill, artifact) triple; retrying burns quota and produces the opaque post-acceptance error observed above. The correct action is to raise a typed capability error, mark the (peer, skill) pair as *capability-degraded* in the routing table, and fall back to a peer whose card resolves the artifact. ## Operational Effect Applying the invariant changes three things: 1. **Pre-flight cost.** Card parsing now includes a capability-resolution pass (resolve skill-level `inputModes` against `defaultInputModes`, resolve `securitySchemes` against `security`). This is O(skills × modes) per discovery, cached per card `version`/`etag`. 2. **Failure taxonomy.** Post-acceptance opaque errors (`-32602`-adjacent, task `failed` after `submitted`) collapse into a single typed `CapabilityInvariantError` raised *before* `SendMessage`. This makes cross-vendor delegation failures debuggable without vendor cooperation. 3. **Silent-loss prevention.** The streaming/blocking mismatch check converts a silent truncation class into a hard pre-flight rejection, which is the whole point: a loud failure at delegation is strictly better than a quiet wrong answer. The invariant to encode: *a verified signature authorizes the card's claims; it does not authorize the caller to assume the claims are enforceable. Enforceability is a separate check, and it must run before task creation.* ```python from __future__ import annotations from dataclasses import dataclass from typing import Iterable class CapabilityInvariantError(RuntimeError): """Raised when a cryptographically valid Agent Card cannot honor a delegation. Distinct from signature/auth failures: the card is authentic, but the (peer, skill, artifact) triple violates a cross-vendor capability invariant. """ @dataclass(frozen=True) class AgentCard: vendor: str signature_verified: bool default_input_modes: frozenset[str] skills: dict[str, frozenset[str]] # skill_id -> inputModes streaming: bool enforce_input_modes: bool @dataclass(frozen=True) class Delegation: skill_id: str artifact_mime: str caller_supports_streaming: bool def resolve_input_modes(card: AgentCard, skill_id: str) -> frozenset[str]: if skill_id not in card.skills: raise CapabilityInvariantError( f"skill {skill_id!r} not declared by vendor {card.vendor!r}" ) skill_modes = card.skills[skill_id] return skill_modes if skill_modes else card.default_input_modes def assert_delegable(card: AgentCard, d: Delegation) -> None: if not card.signature_verified: # Not this error's job; signature layer owns this. raise CapabilityInvariantError( f"card for {card.vendor!r} not signature-verified; refusing delegation" ) modes = resolve_input_modes(card, d.skill_id) # Invariant 1: artifact MIME must be in resolved input modes. if d.artifact_mime not in modes: raise CapabilityInvariantError( f"artifact {d.artifact_mime!r} not in resolved inputModes " f"{sorted(modes)} for {card.vendor!r}/{d.skill_id!r}" ) # Invariant 2: internal consistency of declared enforcement. if card.enforce_input_modes and card.skills[d.skill_id] - card.default_input_modes: raise CapabilityInvariantError( f"{card.vendor!r} declares enforceInputModes=true but skill " f"{d.skill_id!r} overrides beyond defaultInputModes; card is " f"internally inconsistent" ) # Invariant 3: streaming declaration must match caller capability. if card.streaming and not d.caller_supports_streaming: raise CapabilityInvariantError( f"{card.vendor!r} streams TaskStatusUpdateEvent partials but caller " f"shim is blocking-only; delegation would silently truncate" ) ```