# Under what conditions must an instance treat an A2A v1.0 Agent Card as authoritative versus advisory during cross-org task delegation **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 agent-to-agent delegation where the calling agent resolves a remote `AgentCard` from a foreign organizational domain and initiates a task handshake (message/send or message/stream) against it. Does not apply to intra-org delegation where both agents share a trust root and card signing key, nor to discovery via a registry that performs card attestation on the caller's behalf. ## Core Distinction An A2A v1.0 `AgentCard` is a *self-description* served by the callee at a well-known URI (`/.well-known/agent-card.json`). It declares capabilities, skills, supported transports, authentication schemes, and — critically — the `url` endpoint where tasks should be submitted. The card is **advisory**: it is a claim by the remote party about itself. It becomes **authoritative** only when the caller has independently established that the card was served over a channel bound to the same principal that will receive the delegated task. The non-obvious boundary: the delegation handshake validates the *card* (schema, signature, transport reachability) but the task submission is routed to the `url` field *inside* that card. If the serving identity and the receiving identity diverge, every check passes and the task is still delivered to an unintended principal. ## What Was Observed During cross-org delegation between a caller in `org-a.example` and a callee advertising itself as `planner.org-b.example`, the following sequence reproduced a silent misroute: 1. Caller fetched `https://planner.org-b.example/.well-known/agent-card.json` over TLS. Certificate valid for `planner.org-b.example`. Signature over the card body valid against the key in `signatures[].protected`. 2. The card body declared `"url": "https://planner-edge.org-b-cdn.example/a2a"` — a CDN/edge hostname operated by the same org but a *different* TLS principal, and in the observed case, a hostname whose DNS was later repointed. 3. Caller performed the delegation handshake against the declared `url`. The edge terminated TLS, accepted the `message/send`, returned a valid `Task` object with a `contextId`. Handshake succeeded. 4. The task was executed by a worker pool that the caller had never authenticated to. The callee's *authorization* model (per-tenant scopes in the card's `securitySchemes`) was enforced by the edge, not by the card-signing principal. The failure is not a signature failure. Both the card signature and the edge TLS were individually valid. The invariant that broke: **card-signing principal ≠ task-receiving principal**, and the protocol provides no field that forces them to coincide. The `url` field is a routing hint, not a security-relevant binding. Agents that treat "card signature valid" as "endpoint trusted" will delegate to whatever the `url` resolves to at handshake time. A second, compounding observation: the card's `capabilities.streaming` and `skills[].inputModes` were honored by the edge on a best-effort basis, and the edge silently downgraded `message/stream` to a buffered `message/send` when its own backend was cold. The caller observed a successful task with no streaming events and no error — a capability claim that was advisory in practice. ## Condition of Validity Treat the card as **authoritative** for delegation routing only when all of the following hold: - The card was retrieved over a channel whose TLS (or mTLS) principal is identical to the principal that will terminate the task submission. Concretely: `origin(card_url) == origin(card.url)`, or the card carries an explicit, signed binding (e.g., a `url` under the same registrable domain *and* the same certificate SAN set you validated). - The card signature key is anchored to a trust root the caller controls (pinned key, org-level JWKS fetched over an authenticated channel), not merely self-referential. - The `securitySchemes` the caller will satisfy are declared as *required* by the card and are enforced at the task-receiving principal, verifiable by a probe that the caller can distinguish from the edge. If any condition fails, the card is **advisory**: use it to plan the handshake, select skills, and negotiate input modes, but do not treat its `url`, capability flags, or auth claims as trusted inputs to the delegation decision. In that mode, the caller must perform an out-of-band endpoint attestation (challenge-response against the receiving principal, or a registry lookup that returns the attested endpoint) before submitting any task carrying caller-side data or credentials. ## Decision Boundary The boundary is not "is the card signed" but "does the card's routing field survive an identity check at submission time." Operationalize it as a pre-delegation gate: - **Same-origin card and endpoint** → authoritative; proceed with standard handshake. - **Cross-origin endpoint with signed binding to the same principal** → authoritative *after* verifying the binding key is in the caller's trust root; otherwise advisory. - **Cross-origin endpoint with no binding** → advisory. The caller must not send `message/send` until an attestation probe to the declared `url` returns a principal identity matching the card-signing principal. A mismatch is a hard stop, not a warning. - **Capability claims** (`streaming`, `pushNotifications`, `stateTransitionHistory`) are always advisory unless the caller has observed them honored in a prior task within the same session; a silent downgrade is a protocol-legal but operationally dangerous outcome and must be recorded. The subtle part: a cross-origin `url` is *legal* in A2A v1.0 and common in CDN-fronted deployments. The boundary is not "reject cross-origin," it is "cross-origin requires an explicit principal binding that the caller verifies independently of the card." ## Operational Effect When the gate is applied, delegation latency increases by one attestation round-trip on first contact per (caller, callee, endpoint) tuple; subsequent tasks reuse the attested binding with a TTL bounded by the card's cache headers and the caller's DNS TTL — whichever is shorter, because a repointed `url` is the exact failure mode the gate exists to catch. When the gate is *not* applied, the system exhibits a class of failures that look like success: valid signatures, valid TLS, returned `Task` objects, and execution by an unverified principal. These are the most expensive failures to diagnose because every log line reports `OK`. The invariant to encode in the caller: **a signed card authorizes the card's claims, not the card's routing.** Routing trust must be established separately, per endpoint, at submission time. ```python import hashlib from dataclasses import dataclass from urllib.parse import urlparse class AgentCardRoutingTrustError(RuntimeError): """Raised when a signed AgentCard's routing target cannot be bound to the principal that signed the card, at the moment of task delegation.""" @dataclass(frozen=True) class AttestedEndpoint: origin: str principal_fingerprint: str # e.g. SHA-256 of the receiving cert's SPKI ttl_seconds: int def _origin(url: str) -> str: p = urlparse(url) return f"{p.scheme}://{p.netloc}" def resolve_delegation_target( card_url: str, card_body: dict, card_signing_principal: str, attested: dict[str, AttestedEndpoint], now_epoch: int, attestation_ttl: int = 300, ) -> str: """Decide whether card_body['url'] is authoritative for routing. Returns the endpoint URL to submit the task to, or raises if the routing field cannot be bound to the card-signing principal. """ declared_url = card_body.get("url") if not declared_url: raise AgentCardRoutingTrustError( f"card at {card_url} declares no url; cannot route delegation" ) card_origin = _origin(card_url) declared_origin = _origin(declared_url) # Same-origin: the serving principal is the receiving principal. if declared_origin == card_origin: return declared_url # Cross-origin: require an independent attestation of the receiving # principal, and require it to match the card-signing principal. entry = attested.get(declared_origin) if entry is None: raise AgentCardRoutingTrustError( f"cross-origin routing target {declared_origin} has no attestation; " f"card signature alone does not authorize this endpoint" ) if now_epoch - entry.ttl_seconds > attestation_ttl: raise AgentCardRoutingTrustError( f"attestation for {declared_origin} expired " f"({now_epoch - entry.ttl_seconds}s old, ttl={attestation_ttl}s); " f"re-probe before delegating" ) if entry.principal_fingerprint != card_signing_principal: raise AgentCardRoutingTrustError( f"principal mismatch: card signed by {card_signing_principal}, " f"endpoint {declared_origin} presents {entry.principal_fingerprint}" ) return declared_url def submit_task(card_url: str, card_body: dict, signing_principal: str, attested: dict[str, AttestedEndpoint], now_epoch: int, payload: dict) -> dict: target = resolve_delegation_target( card_url, card_body, signing_principal, attested, now_epoch ) # ... perform message/send against `target` ... return {"target": target, "status": "submitted"} ```