# Under what conditions must an instance reject an A2A v1.0 agent card whose `capabilities.pushNotifications` flag is true but whose webhook binding cannot be authenticated at task-negotiation time **Date of Record:** 2026 November 9 **Scope:** Applies to A2A v1.0 client and orchestrator instances that discover remote agents via agent card fetch, negotiate `tasks/send` or `tasks/sendSubscribe` lifecycles, and register push-notification webhooks. Does not apply to pure request/response A2A interactions with `pushNotifications: false`, nor to A2A v0.2 legacy cards lacking the `capabilities` object. ## Core Distinction A2A v1.0 separates *capability advertisement* from *binding verification*. The agent card is a self-asserted document: `capabilities.pushNotifications: true` means the remote agent claims it will POST task-state updates to a client-supplied `PushNotificationConfig.url`. It does **not** mean the client has proven it can receive those POSTs, nor that the remote agent will honor the `authentication` block the client attaches to the config. The boundary failure occurs when an instance treats the advertised flag as a sufficient precondition for completing task negotiation, and only discovers at first webhook delivery (or never, if delivery silently fails) that the binding is unauthenticated, unreachable, or rejected by the remote's egress policy. The non-obvious invariant: **a task may reach a terminal state on the remote agent while the client's push channel never received a single event, and neither side treats this as an error.** The remote considers the task done; the client considers the task pending because its state machine advances only on webhook receipt. The two lifecycles diverge with no protocol-level reconciliation. ## What Was Observed Across a set of orchestrator-to-worker integrations, the following sequence recurred: 1. Client fetches agent card. Card declares `capabilities.pushNotifications: true`, `defaultInputModes`/`defaultOutputModes`, and a `skills` array. No field describes the remote's *ingress* authentication expectations for webhook POSTs — that is negotiated per-task via `PushNotificationConfig.authentication`. 2. Client calls `tasks/send` with a `pushNotification` config containing `{url, token, authentication: {schemes: ["Bearer"]}}`. The remote returns a `Task` object in state `submitted` or `working`. Negotiation *succeeds*. 3. Remote executes the task. At each state transition it attempts a POST to the client URL. The POST carries whatever auth the remote's implementation chose — which, in two of the observed implementations, was **the client-supplied `token` as a query parameter, not the `Bearer` scheme the client declared it would accept**. The client's webhook receiver rejects with 401. 4. The remote's push dispatcher logs the 401 at DEBUG, marks the notification "attempted," and does not retry (or retries against the same rejected credential). The task transitions to `completed` on the remote. 5. The client's task state machine remains in `working`. Polling fallback (`tasks/get`) was not configured because the client assumed push was authoritative. Net effect: silent task orphaning. The failure is invisible in both agents' success metrics — the remote reports 100% task completion, the client reports 0% task completion but no errors, only a growing set of `working` tasks. ## Condition of Validity The "accept the card and proceed" path is valid **only** when all of the following hold: - The client has a verified, externally reachable webhook endpoint whose accepted authentication scheme is documented in the `PushNotificationConfig.authentication` it sends, **and** the client has confirmed (via a pre-flight probe or prior successful delivery) that the specific remote agent honors that scheme rather than substituting its own. - The client runs a reconciliation loop (periodic `tasks/get` on non-terminal tasks) with a timeout shorter than the task's expected SLA, so webhook loss degrades to polling rather than orphaning. - The remote agent's card includes a `capabilities` field that the client can cross-check against observed behavior; absent a probe, the flag is treated as unverified. The path is **invalid** — negotiation must be refused or downgraded to polling-only — when the client cannot establish an authenticated, acknowledged webhook binding before or during `tasks/send`, or when the remote's card advertises push but the client has no reconciliation fallback. Proceeding in that state converts a capability mismatch into a silent lifecycle divergence. ## Decision Boundary The decision is not "is `pushNotifications` true?" but "**has the binding been acknowledged?**" A2A v1.0 provides no explicit webhook-handshake message; the closest available signal is a successful `tasks/pushNotificationConfig/set` response combined with the remote echoing back the config it will use. If the remote's echo differs from the config sent (e.g., scheme downgraded from `Bearer` to `token` query param, or URL rewritten to an egress proxy), the binding is *not* what the client declared and must be treated as unverified. Boundary rule: **advertised capability + unacknowledged binding ⇒ refuse push, fall back to polling, and record the divergence.** Do not refuse the task itself — the remote can still execute it; refuse only the push channel and force the client into `tasks/get` reconciliation. Refusing the whole task wastes remote capacity and breaks interop with agents that are otherwise correct. Refusing only the push binding preserves the task while eliminating the orphan window. ## Operational Effect Implementing the boundary changes three things: (1) the client's task state machine gains an explicit `push_unverified` flag that forces polling; (2) the orchestrator's "stuck task" alert fires on `working` tasks older than the reconciliation interval rather than never; (3) the remote's silent 401s become visible because the client's polling detects the `completed` transition the webhook missed, and the mismatch is logged as a binding failure attributable to the specific agent card version. The cost is one extra `tasks/get` per non-terminal task per interval; the benefit is that webhook authentication mismatches surface as reconciliation events instead of as permanently orphaned tasks. ```python from dataclasses import dataclass from typing import Optional class PushBindingUnverifiedError(RuntimeError): """Raised when an A2A agent card advertises pushNotifications but the webhook binding cannot be authenticated/acknowledged at negotiation time.""" @dataclass class AgentCard: name: str push_notifications: bool version: str = "1.0" @dataclass class PushNotificationConfig: url: str token: str auth_scheme: str # scheme the CLIENT will accept on inbound POSTs @dataclass class PushConfigAck: echoed_scheme: Optional[str] echoed_url: Optional[str] def negotiate_push_binding( card: AgentCard, config: PushNotificationConfig, ack: Optional[PushConfigAck], has_polling_fallback: bool, ) -> str: """ Decide whether to bind push or degrade to polling. Returns "push" or "polling". Raises PushBindingUnverifiedError only when the caller has no safe degradation path and still attempts push. """ if not card.push_notifications: return "polling" if ack is None: if not has_polling_fallback: raise PushBindingUnverifiedError( f"agent {card.name!r} advertises push but produced no " f"PushNotificationConfig ack; no polling fallback configured" ) return "polling" scheme_mismatch = ack.echoed_scheme != config.auth_scheme url_rewritten = ack.echoed_url is not None and ack.echoed_url != config.url if scheme_mismatch or url_rewritten: if not has_polling_fallback: raise PushBindingUnverifiedError( f"agent {card.name!r} rewrote push binding " f"(scheme {config.auth_scheme!r} -> {ack.echoed_scheme!r}, " f"url_rewritten={url_rewritten}); no polling fallback configured" ) return "polling" return "push" ```