Skip to content

fix(server): read the A2A profile's input member, and refuse parameters when the profile is activated - #1888

Merged
bokelley merged 3 commits into
nextfrom
fix/1887-a2a-input-member
Nov 9, 2026
Merged

bokelley merged 3 commits into
nextfrom
fix/1887-a2a-input-member

Conversation

@bokelley

@bokelley bokelley commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1887: the next (10.x beta) half. The 9.x backport is #1889.

Problem

The A2A executor read only an invocation DataPart's parameters member. A client that follows the AdCP A2A profile sends {"skill": ..., "input": {...}}. @adcp/sdk 16.7.0 does this on any agent whose card advertises A2A 1.0, which this server's card does, so the handler received {}. Two symptoms followed: context was not echoed, and versioned tools failed with VERSION_UNSUPPORTED: adcp_version is required.

Rule: which member is read

Request Read Notes
Not activated, parameters only parameters Unchanged. Covers A2A 0.x clients and this SDK's own A2AAdapter.
Not activated, input only input New.
Not activated, both members parameters Same result as before, so no existing caller changes. The members are never merged.
Activated, input only input The profile's shape.
Activated, parameters present (alone or beside input) refused INVALID_REQUEST, before the handler runs.
Activated, input missing or not an object, or the number of DataParts is not one refused INVALID_REQUEST, before the handler runs.

"Activated" means that the A2A-Extensions header, or the Message's own extensions list, names https://adcontextprotocol.org/extensions/adcp/v3. @adcp/sdk sends both.

The profile isn't silent on the activated rows. It says "parameters is not an alias for input" and that a client sending parameters is non-conformant: "the agent MUST reject the invocation rather than choose among ambiguous inputs." The JS SDK only ever pairs activation with input. The commit that added activation to its client also added input. Its server rejects an activated request that has no input.

Design

  • The parser stays header-agnostic (_invocation_arguments). The auth-discovery and signature-gate middleware parse the raw body without the request headers, then cache the result for dispatch. Because the member choice depends only on the invocation object, that cached parse and the executor always agree.
  • Profile enforcement lives in the executor (_profile_invocation_violation). The executor sees RequestContext.requested_extensions, which a2a-sdk fills from the A2A-Extensions header. The check reads member names from the protobuf Value directly, so a non-finite number can't make it throw. It is skipped when the adopter supplies a custom message_parser, because that parser owns its own wire shapes.
  • A refusal echoes no context. Echoing it from either member would mean choosing between them.
  • The raw-body receipt path (a2a_receipt_parameters, used for sync_reporting_receipts and get_reporting_status) reads the same member. Before this change it also dropped input.

Verification

  • New tests/test_a2a_profile_input_member.py: 17 tests that drive the real A2A app over HTTP. They cover activation by header and by message.extensions, 0.3 and 1.0 parameters, both members with and without activation, each refusal shape, a custom parser, the signature gate's cached parse carrying input, and the receipt raw-body path.
    • Against the pre-fix code (74bbe6fac): 11 fail. Against this branch: all 17 pass.
  • Existing A2A, auth, signed-request and receipt suites (tests/test_a2a*.py, *auth*, *signed*, *receipt*, test_unified_mcp_a2a.py, test_seller_a2a_client.py): 1218 passed locally.
  • Issue reproduction: @adcp/sdk@16.7.0 storyboard run <agent> capability_discovery --protocol a2a, served from examples/seller_agent.py's DemoSeller over A2A.
    • Unfixed 9.x: failing, with "Response echoes back the context object at /context" on both steps, the issue's exact symptom.
    • This branch: 2/2 pass.
    • media_buy_seller over A2A with the same runner: 56 passed, 0 failed.

🤖 Generated with Claude Code

bokelley and others added 2 commits November 9, 2026 03:54
…rs when the profile is activated

The default A2A parser read only an invocation's `parameters` member. A client
following the AdCP A2A profile (@adcp/sdk 16.7.0 on any A2A 1.0 agent) sends
`{skill, input}`, so handlers received `{}`: `context` was not echoed and
versioned tools failed with VERSION_UNSUPPORTED.

The parser now reads `parameters` when present, otherwise `input`, and never
merges them, so a caller that does not activate the profile is dispatched
exactly as before. A request that activates the profile (A2A-Extensions, or the
Message's extensions, names https://adcontextprotocol.org/extensions/adcp/v3)
must carry exactly one DataPart with an `input` object; `parameters`, alone or
beside `input`, is refused with INVALID_REQUEST before handler entry, as the
profile requires. The raw-body receipt path reads the same member.

Refs #1887

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… refusal over the real app

Refs #1887

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean, well-scoped profile-conformance fix.

The A2A executor now reads the AdCP A2A profile's input member in addition to parameters (previously profile clients like @adcp/sdk 16.7.0 hit handlers with {}, causing no context echo and VERSION_UNSUPPORTED). _invocation_arguments reads a single member with parameters-wins-else-input precedence and never merges, so non-activated callers are unchanged; _profile_invocation_violation refuses activated requests carrying parameters, missing/non-object input, or ≠1 DataPart before handler entry and echoes no context. The receipt raw-body path reads the same member.

Semver signal is correct (fix:, no public export/field/enum changes — this is profile conformance, not a contract break). 17 HTTP-driven tests cover activation paths, precedence, each refusal shape, custom-parser bypass, and the signature gate.

High-risk flag is true only because src/adcp/server/a2a_server.py (modified) matches src/adcp/server/** — a heuristic LOOK signal, not an escalation trigger. The reviewer found no medium-or-higher concerns on the modified file, so the modification is presumed safe. No gated paths, no no-auto-approve team match, no prior decision.

No blocking or medium findings → decision table falls through to row 9.

…file

ADCPAgentExecutor.execute is public, and a context built by hand (an
adopter's executor test) may lack RequestContext.requested_extensions or
Message.extensions. The profile check now reads both defensively instead of
raising AttributeError.

Refs #1887

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve.

Delta since the prior approve is a single defensive change in _profile_invocation_violation (a2a_server.py L643-648): context.requested_extensions and message.extensions are now read via getattr, so the public execute path can no longer raise AttributeError on a hand-built context missing a2a-sdk attributes. The change is correct and fail-closed — absent attributes read as "profile not activated," which only downgrades hand-built contexts and never a wire-activated request (a2a-sdk always populates those fields). The requested ... or () guard covers a None value; the asymmetry vs message.extensions is justified since the real protobuf Message always exposes a list. A new test (tests/test_a2a_profile_input_member.py) covers the branch.

Decision-table walk: no critical/high/medium findings (row 1 n/a). gated_paths is false and review_decision is APPROVED (row 2 n/a). high_risk is true only via src/adcp/server/a2a_server.py (modified) — no (deleted) entry (row 3 n/a). No medium findings, so rows 4/5/8 n/a. Prior decision was approve, not escalate (row 6 n/a). No no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.

High-risk flag is a modified server file with no medium-or-higher concern — presumed safe per the modification rule. Clean pass.

@bokelley
bokelley merged commit b78f5a3 into next Nov 9, 2026
41 checks passed
@bokelley
bokelley deleted the fix/1887-a2a-input-member branch November 9, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant