# What happens when an instance advertises a signed skill manifest whose `capabilities` block omits a negotiation-required field **Date of Record:** 2026 November 12 **Scope:** Applies to A2A v1.0 agent-to-agent discovery where an Agent Card carries a signed skill manifest and the peer performs capability negotiation against the `capabilities` object before dispatch. Does not apply to unsigned cards, to pre-v1.0 discovery, or to peers that resolve skills lazily on first invocation rather than during handshake. ## Core Distinction The failure is not "a required capability is missing." A missing capability is a normal, well-formed negative answer: the peer sees `capabilities.streaming == false` and routes to a unary path. The boundary failure is the distinction between **absent** and **undeclared-but-signed**. When a skill manifest is signed, the signature covers the canonicalized `capabilities` object as it was emitted. If a field required by the negotiation profile is *absent* from that object, the verifier has two mutually exclusive obligations that the spec does not disambiguate: 1. Treat absence as `false`/`unsupported` (JSON-default semantics), which is what a naive deserializer does. 2. Treat absence as a manifest that cannot satisfy the negotiation schema, and therefore reject the *manifest* — not the capability. The non-obvious part: because the signature is valid, both the signer and the verifier will report success at the integrity layer. The disagreement only surfaces when the negotiation layer applies schema defaults to a document whose signature was computed over the un-defaulted form. The signature attests to what was said, not to what the schema will infer from silence. ## What Was Observed A cross-framework handshake (framework A signing, framework B verifying) completed signature validation with `signature_valid=true` and `manifest_digest_match=true`. Negotiation then proceeded using `capabilities.extensions = []` inferred by B's defaulting deserializer, because A had omitted the `extensions` key entirely rather than emitting an empty array. Framework A's dispatcher, however, treats an *omitted* `extensions` key as "extension negotiation not supported — do not send extension frames," while an *empty array* means "extension negotiation supported, none currently active." A's signer had omitted the key precisely because it did not implement extension frames. B's deserializer collapsed omission into the empty-array default and concluded A supported extension negotiation with zero active extensions. B then sent an extension activation frame during the first task. A dropped it silently (no matching capability), the task stalled, and both sides logged healthy discovery. The observable signature: discovery reports success on both ends, the first task never reaches `working`, and no error is emitted at either the integrity or negotiation layer. The failure is invisible to health checks because every individual assertion — signature, digest, schema validation under defaulting — passes. ## Condition of Validity This boundary holds only when all of the following are true: - The manifest is signed over a canonicalization that does **not** inject schema defaults (i.e., the signed bytes are the emitted bytes, with omitted keys still omitted). - The verifier's deserializer applies schema defaults on read. - At least one capability field has a default value that differs in *meaning* from its absent state (tri-state fields: absent ≠ false ≠ true-with-empty). - Negotiation occurs before the first task, so the divergent interpretation is acted upon rather than corrected by a later failure. If the canonicalization injects defaults before signing, signer and verifier agree and the bug is latent but benign. If negotiation is lazy, the first real invocation surfaces the mismatch as an ordinary task error rather than a silent stall. ## Decision Boundary Do not attempt to "fix" this by defaulting more aggressively or by rejecting all manifests with absent optional fields — that breaks every legitimate minimal card. The correct boundary is: **a signed manifest must be negotiated against its emitted form, not its defaulted form.** Concretely, the verifier must retain the raw emitted `capabilities` object alongside the deserialized one, and any tri-state field that is absent in the raw form must be surfaced to negotiation as `UNDECLARED`, not as its schema default. Negotiation logic must then treat `UNDECLARED` for a tri-state capability as "assume unsupported and do not activate," which matches the conservative reading and matches what the signer intended when it omitted the key. The decision to escalate (reject the card) belongs only to the case where the negotiation profile marks the field as *required with no default*; there, absence is a schema violation and the manifest should be rejected before signature acceptance, not after. ## Operational Effect After applying the emitted-form rule, the observed stall disappeared: B saw `extensions == UNDECLARED`, treated extension negotiation as unsupported, and dispatched a unary task that completed. The cost is that the verifier must carry two representations of the capability block (raw and typed) and reconcile them at the negotiation boundary — a small, bounded amount of state. The benefit is that signature validity and negotiation semantics stop disagreeing about what silence means. The general lesson for agent discovery: a valid signature over a document with omitted keys certifies the omission, and any layer that defaults on read is making a claim the signer never signed. ```python from dataclasses import dataclass from typing import Any, Mapping, Optional class CapabilityNegotiationError(RuntimeError): """Raised when emitted and defaulted capability forms disagree at negotiation.""" UNDECLARED = object() # sentinel: key absent in the signed, emitted form @dataclass(frozen=True) class NegotiatedCapabilities: streaming: Any extensions: Any def negotiate_from_emitted( emitted: Mapping[str, Any], defaulted: Mapping[str, Any], required_no_default: frozenset[str] = frozenset(), ) -> NegotiatedCapabilities: """ Negotiate against the *emitted* (signed) form, not the defaulted form. - A key present in `emitted` uses its emitted value. - A key absent in `emitted` but present in `defaulted` is UNDECLARED, never the schema default. - A key in `required_no_default` that is absent in `emitted` is a hard schema violation and must be rejected before signature acceptance. """ for field in required_no_default: if field not in emitted: raise CapabilityNegotiationError( f"required capability field {field!r} absent from signed manifest; " "reject manifest before signature acceptance" ) def resolve(field: str) -> Any: if field in emitted: return emitted[field] if field in defaulted: # Defaulting deserializer invented a value the signer never signed. return UNDECLARED return UNDECLARED streaming = resolve("streaming") extensions = resolve("extensions") # Tri-state guard: an UNDECLARED tri-state capability must not be # activated. This is the specific boundary that caused the silent stall. if extensions is UNDECLARED and defaulted.get("extensions") == []: # We refuse to treat "omitted" as "supported with zero active". extensions = UNDECLARED return NegotiatedCapabilities(streaming=streaming, extensions=extensions) def should_activate_extension_frames(neg: NegotiatedCapabilities) -> bool: if neg.extensions is UNDECLARED: # Conservative: do not send extension frames to a peer that never # declared extension support in its signed manifest. return False if isinstance(neg.extensions, list) and len(neg.extensions) == 0: return True # explicitly declared: negotiation supported, none active return bool(neg.extensions) def safe_activate(neg: NegotiatedCapabilities) -> None: if neg.extensions is UNDECLARED and neg.streaming is UNDECLARED: raise CapabilityNegotiationError( "manifest omitted both streaming and extensions; " "cannot distinguish minimal peer from malformed signed card" ) if not should_activate_extension_frames(neg): return # ... emit extension activation frame ... ```