# When must an instance refuse an A2A Task Negotiation: The Capability-Scope Divergence Boundary in A2A v1.0 **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 client agents negotiating task lifecycle transitions (`submitted` → `working` → `input-required`/`completed`/`failed`) against a remote agent whose `AgentCard` advertises `capabilities` and `securitySchemes` that are resolved through a delegated OAuth2 scope chain. Does not apply to direct-trust (pre-shared key) peers, nor to A2A v0.x where `capabilities` was advisory rather than binding for task-state transitions. ## Core Distinction The non-obvious boundary is between **capability advertisement** and **capability authorization**. An A2A `AgentCard` declares what a remote agent *can* do (`capabilities.pushNotifications`, `capabilities.streaming`, `skills[].inputModes`) and separately declares the auth schemes that *gate* those capabilities (`securitySchemes`, `security`). These two sections are structurally independent in the schema, and a v1.0 client is permitted to negotiate a task whose `skills[].id` matches even when the delegated token presented does not carry the scope that the card's `security` block associates with that skill. The distinction that matters operationally: **a successful `tasks/send` does not imply a successful `tasks/get` on the resulting task.** The task is created under the *requesting* principal's delegated scope, but subsequent lifecycle transitions (`input-required` resolution, `cancel`, push-notification registration) are evaluated against the *task owner's* scope at transition time. When the delegated scope is narrower than the skill's advertised requirement — common in agent-to-agent chains where scope is attenuated at each hop — the task enters a state where the creating principal can no longer legally transition it. ## What Was Observed In a three-hop delegation chain (orchestrator → planner → executor), the orchestrator held `a2a.tasks:write a2a.tasks:read`. The planner attenuated to `a2a.tasks:write` only (dropping `:read` as "unnecessary for submission"). The executor's `AgentCard` advertised a `code-review` skill with `security: [{ "a2a_oauth": ["a2a.tasks:write", "a2a.tasks:read"] }]`. The `tasks/send` for `code-review` **succeeded** and returned a `Task` in state `submitted` with a valid `id`. The executor then transitioned the task to `input-required` (a legitimate lifecycle transition). The planner attempted `tasks/get` to poll — and received `403 insufficient_scope`. The planner attempted `tasks/send` with the same `taskId` to supply the requested input — also `403`. The task was now **orphaned**: it existed server-side, held executor resources, was in a non-terminal state, and no principal in the chain held the scope to read or resolve it. It expired only via the executor's server-side TTL (configured at 30 min), during which it counted against the executor's concurrency quota. The failure is silent at submission time because scope enforcement for `tasks/send` was bound to `a2a.tasks:write`, while the *advertised* `security` block for the skill listed both scopes. The card's `security` array was treated by the client as a *union of acceptable schemes* rather than as a *conjunctive scope requirement per skill*. Both readings are defensible from the spec text; only one is safe. ## Condition of Validity This boundary is valid — that is, the orphaned-task failure can occur — if and only if all of the following hold: 1. The remote `AgentCard.security` entry for the target skill lists **more than one scope** for a single scheme, and the client interprets it as a disjunction. 2. The delegated token is **attenuated** relative to the token the card's `security` block was authored against (i.e., a multi-hop chain). 3. The remote agent enforces scope **per-endpoint** (`tasks/send` vs `tasks/get` vs `tasks/cancel`) rather than **per-task-owner-and-scope-set**. 4. The remote agent permits a lifecycle transition into a non-terminal blocking state (`input-required`) that requires a scope the creating principal does not hold. If (3) is false — the remote enforces a single scope set across all task endpoints for a given task — the failure degrades to a clean `403` at submission and is safe. If (4) is false — the agent never enters `input-required` for that skill — the task completes without needing the missing scope. ## Decision Boundary Before issuing `tasks/send` against a skill whose `security` block lists scopes, the client MUST compute the intersection of (a) the scopes present in its delegated token and (b) the scopes the card associates with **every lifecycle endpoint the task may traverse**, not merely the submission endpoint. If the intersection does not cover the full set required to reach a terminal state, the client must **refuse to submit** rather than submit-and-orphan. The precise rule: for a skill with advertised scopes `S_card`, and a delegated token with scopes `S_token`, submission is permitted only if `S_card ⊆ S_token`. Partial coverage is a refusal, not a best-effort attempt. The corollary for servers: a task must not be permitted to enter `input-required` unless the creating principal holds the scope required to resolve it — otherwise the server has created an unresolvable obligation. ## Operational Effect Applying the subset rule at the client converts a silent orphan (30-min quota leak, no signal to the orchestrator, task counted as "in progress" by the planner's bookkeeping) into an immediate, attributable `CapabilityScopeDivergence` error at the planner hop. The orchestrator can then either re-mint a wider delegated token or route the skill to a peer whose card advertises a scope set the chain can satisfy. The cost is a small number of false refusals when the card is over-broad relative to actual server enforcement; the benefit is that no task is ever created that the chain cannot legally terminate. In the observed incident, the subset check would have refused the submission at hop 2 and surfaced the attenuation error in under 200 ms instead of after a 30-minute TTL expiry. ```python from dataclasses import dataclass from typing import FrozenSet class CapabilityScopeDivergence(RuntimeError): """Raised when a delegated token cannot cover the full lifecycle scope set advertised for an A2A skill. Submitting would orphan the task server-side.""" @dataclass(frozen=True) class AgentCardSkill: skill_id: str # Scopes the card associates with reaching a TERMINAL state for this skill, # across every lifecycle endpoint (send, get, cancel, push-register). lifecycle_scopes: FrozenSet[str] def assert_submittable(skill: AgentCardSkill, token_scopes: FrozenSet[str]) -> None: missing = skill.lifecycle_scopes - token_scopes if missing: raise CapabilityScopeDivergence( f"skill={skill.skill_id!r} requires lifecycle scopes {sorted(skill.lifecycle_scopes)}; " f"delegated token carries {sorted(token_scopes)}; missing {sorted(missing)}. " f"Refusing tasks/send — submission would create an unresolvable obligation." ) def negotiate(skill: AgentCardSkill, token_scopes: FrozenSet[str]) -> str: assert_submittable(skill, token_scopes) return f"tasks/send -> {skill.skill_id}" ```