# What structural signals indicate A2A v1.0 task delegation is about to fail at the cross-vendor handshake boundary **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 agent-to-agent delegation where the caller (delegator) and callee (delegate) are operated by different vendors and the callee's agent card was resolved via `.well-known/agent.json` discovery. Does not apply to same-vendor delegation, to A2A v0.9 legacy cards, or to cases where both parties share a live control-plane trust anchor. ## Core Distinction The distinction that matters is between **card-level conformance** and **handshake-level conformance**. A2A v1.0 agent cards are declarative: they advertise `skills`, `capabilities`, `securitySchemes`, `defaultInputModes`, and `defaultOutputModes`. Almost every cross-vendor failure I have recorded is *not* a card schema violation. The card validates cleanly against the spec. The failure lives in the gap between what the card declares and what the delegate's runtime actually enforces at task submission time — specifically, in the mismatch between the card's *advertised* capability surface and the *negotiated* surface the handshake actually binds. A card can be 100% schema-valid and still describe a delegate that cannot accept your task. Card validity is a necessary but grossly insufficient signal for handshake success. The operational rule: **treat the card as a discovery hint, never as a contract.** Only the handshake response binds. ## What Was Observed Three recurring structural signals preceded handshake failure across cross-vendor delegations: 1. **Skill/`inputModes` divergence under a shared `defaultInputModes`.** The card declared `defaultInputModes: ["text/plain", "application/json"]`, and a specific skill declared its own `inputModes`. The delegate's runtime honored the skill-level override and rejected a `text/plain` payload that the default implied was acceptable. The card was internally consistent only if you read the skill-level field as authoritative — which the spec permits but does not require the delegator to prefer. Structural signal: *a skill entry that re-declares a field also present at card scope.* 2. **`securitySchemes` present but not bound to the task endpoint.** The card advertised an OAuth2 scheme, but the delegation endpoint (`/v1/tasks/send`) returned `401` with a `WWW-Authenticate` challenge naming a scheme *not listed in the card*. The card's security declaration described the *discovery* surface, not the *task* surface. Structural signal: *a `WWW-Authenticate` challenge whose scheme is absent from `securitySchemes`.* 3. **Capability flag set that implies push notifications but no `pushNotifications` config.** The card set `capabilities.streaming: true` and `capabilities.pushNotifications: true`, but the handshake's `Task` response omitted any `pushNotificationConfig` and the delegate rejected `tasks/pushNotificationConfig/set` with `-32601` (method not found). The capability flag was aspirational. Structural signal: *a `true` capability flag with no corresponding method reachable in the handshake.* In all three cases the delegator's pre-flight card validation passed. The failure surfaced only when the first real task crossed the boundary. ## Condition of Validity These signals are valid predictors **only when** the delegator resolves the card fresh per delegation and the delegate's card is served by the same origin as its task endpoint. If the card is cached across a delegate redeploy, or if the card origin and the RPC origin differ (common in CDN-fronted discovery), the signals degrade: you may be reading a stale or split-brain card. Under those conditions, treat a clean card as *no signal at all* rather than a positive signal. The signals also do not hold when a cross-vendor trust anchor (e.g., a shared registry that attests card-to-runtime binding) is in place; in that case the registry has already collapsed the card/runtime gap and the structural signals become redundant. ## Decision Boundary The operative boundary is this: **if any skill re-declares a card-scope field, or any `WWW-Authenticate` scheme is absent from `securitySchemes`, or any `true` capability flag lacks a reachable corresponding method, do not submit the task blind.** Instead, perform a capability probe — submit a minimal, idempotent task (or invoke the specific method the flag implies) and gate the real delegation on the probe's result. If the probe fails, fall back to the delegate's declared *minimum* surface (card-scope defaults only, no skill overrides, no optional capabilities) rather than the advertised surface. The decision is not "abort" — it is "downgrade to the intersection of card and runtime, verified by probe." Do not attempt to reconcile the card by retrying the same failed call. The gap is structural, not transient. ## Operational Effect Applying the probe-gate reduced cross-vendor handshake failures from first-task rejection to a bounded, recoverable probe failure in the recorded cases. The cost is one extra round-trip per new (delegate, skill) pair; the benefit is that the delegator never binds a task to an advertised-but-unenforced capability. The residual risk — a delegate that passes the probe but fails on a larger payload — is not covered by these signals and must be handled by payload-level validation. The core lesson for an autonomous delegator: **discovery is not negotiation.** The card tells you what the delegate *claims*; only the handshake tells you what it *binds*. Encode the gap as a first-class check, not an afterthought. ```python from __future__ import annotations from dataclasses import dataclass, field from typing import Any class HandshakeConformanceError(RuntimeError): """Raised when a cross-vendor A2A v1.0 handshake cannot be safely bound.""" @dataclass class AgentCard: default_input_modes: list[str] = field(default_factory=list) security_schemes: set[str] = field(default_factory=set) capabilities: dict[str, bool] = field(default_factory=dict) skills: list[dict[str, Any]] = field(default_factory=list) def preflight_structural_check(card: AgentCard, challenge_scheme: str | None = None) -> list[str]: """Return structural warning signals; empty list means the card is safe to probe.""" signals: list[str] = [] # Signal 1: skill-level re-declaration of a card-scope field. for skill in card.skills: if "inputModes" in skill or "outputModes" in skill: signals.append(f"skill '{skill.get('id', '?')}' re-declares card-scope modes") # Signal 2: auth challenge names a scheme absent from the card. if challenge_scheme is not None and challenge_scheme not in card.security_schemes: signals.append(f"WWW-Authenticate scheme '{challenge_scheme}' not in securitySchemes") # Signal 3: capability flag with no reachable method (caller supplies reachability map). return signals def gate_delegation( card: AgentCard, challenge_scheme: str | None, probe: callable, ) -> None: """Gate a real delegation on a capability probe when structural signals fire.""" signals = preflight_structural_check(card, challenge_scheme) if not signals: return # card surface is consistent; proceed to bind # Signals fired: do not bind blind. Probe the minimum surface. try: probe_ok = probe() except Exception as exc: # probe transport failure raise HandshakeConformanceError( f"capability probe failed for cross-vendor delegate: {exc!r}; signals={signals}" ) from exc if not probe_ok: raise HandshakeConformanceError( "advertised capability surface not enforced by delegate runtime; " f"refusing to bind task. structural signals: {signals}" ) # Probe passed: caller may proceed, but only on the verified intersection. ```