Skip to content

fix(server): read the A2A profile's input member, and refuse an activated invocation that carries both members - #1889

Merged
bokelley merged 5 commits into
mainfrom
fix/1887-a2a-input-member-9x
Nov 9, 2026
Merged

bokelley merged 5 commits into
mainfrom
fix/1887-a2a-input-member-9x

Conversation

@bokelley

@bokelley bokelley commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Backport of #1888 to the 9.x line (main), so a 3.2-line server interoperates with the 3.2 JS client (@adcp/sdk 16.7.0, the adcp-3.2 dist-tag). Refs #1887. The issue stays open until both PRs merge.

This patch doesn't break anything. 9.x is the stable line, so it doesn't start refusing requests that work today, even non-conforming ones. It reads input, and refuses only the one shape that is genuinely ambiguous: an activated invocation that carries both members. Every other non-conforming activated shape is still dispatched as before, with a deprecation warning. The strict refusal the profile requires ships in 10.x (#1888).

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 on 9.x: which member is read

Request 9.x (this PR) 10.x (#1888)
Not activated, parameters only parameters, as before. Covers A2A 0.x clients and this SDK's own A2AAdapter. same
Not activated, input only input (new) same
Not activated, both members parameters, as before. The members are never merged. same
Activated, input only input: the fix same
Activated, both members refused with INVALID_REQUEST before the handler runs, and no context echoed. It's ambiguous, and no compliant client sends it. refused
Activated, parameters only dispatched from parameters as before, and a deprecation warning is logged refused
Activated, input missing or not an object, or the DataPart count isn't one dispatched as before (pre-fix 9.x dispatched these), and a deprecation warning is logged refused

"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 warning goes to the adcp.server.a2a_server logger and names the reason (parameters_member, input_missing, data_part_count). It quotes the profile rule that "parameters is not an alias for input", so operators can find non-conforming clients before 10.x refuses them.

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.
  • The profile check 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 reads both attributes defensively, so a hand-built context (an adopter's executor test) counts as not activated.
    • It is skipped when the adopter supplies a custom message_parser, because that parser owns its own wire shapes.
  • The both-members 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

  • tests/test_a2a_profile_input_member.py drives the real A2A app over HTTP. It covers:
    • activation by header and by message.extensions;
    • 0.3 and 1.0 parameters;
    • both members, with and without activation;
    • the 9.x warn-and-dispatch shapes, with the warning asserted;
    • a custom parser;
    • the signature gate's cached parse carrying input;
    • the receipt raw-body path.
  • The input cases fail against the pre-fix code (3ede78041).
  • 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): 1026 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.

CI note: Test Python 3.12 fails test_reporting_period_date.py::test_legacy_zone_aliases_resolve_to_the_canonical_date on the new ubuntu-26.04 runners. The cause is not this change: the image's zone database drops the legacy aliases, and 9.x has no tzdata dependency. #1890 pins tzdata and fixes it. This PR will rebase onto it once it merges.

🤖 Generated with Claude Code

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Nov 9, 2026

@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 backport of #1888 to 9.x, no blocking or medium findings.

What I checked:

  • No no-auto-approve team match; gated_paths: false, so the row-2 hard gate does not apply despite review_decision: REVIEW_REQUIRED.
  • high_risk: true is driven solely by src/adcp/server/a2a_server.py (modified). Per the decision table, a modified high-risk file only escalates (row 5) if a medium-or-higher finding exists — none do, so the modification is presumed safe.
  • Wire-shape fidelity: _invocation_arguments reads parameters when present else the profile's input, never merging — unactivated callers dispatch exactly as before, so no backward-compat break. _profile_invocation_violation fails closed on activated requests with bad shape, refusing with INVALID_REQUEST and echoing no context (refusal passes params=None, so no context leak at a2a_server.py:807+).
  • Minor-release pathway (DR-0032/DR-0033): the refused shape is non-conformant per the profile — the fix: prefix is correct, no public-API signature change, and 17 new HTTP-level tests cover every refusal shape.
  • No credentials in ctx_metadata, no type-layering violations, no generated-code hand-edits, no CI-gate weakening, no external I/O/timeouts touched.

Rows 1–8 do not fire; falls through to row 9 → approve.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Nov 9, 2026

@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.

This is a subsequent review (prior outcome: approve). The delta is a single defensive change to _profile_invocation_violation in a2a_server.py: getattr-with-defaults replaces direct attribute access to context.requested_extensions/message.extensions, so a hand-built RequestContext lacking a2a-sdk attributes is treated as "profile not activated" rather than crashing the public execute path. Logic fails in the safe direction and introduces no new None/attribute risk. A matching test was added.

Decision-table walkthrough:

  • Row 1 (critical/high): no such findings.
  • Row 2 (gated_paths + not approved): gated_paths is false — does not fire.
  • Row 3 (deleted high-risk path): no deletions.
  • Row 4 (medium data-loss/schema/infra): no medium findings.
  • Row 5 (high_risk modified + medium): a2a_server.py is modified under a high-risk glob, but there are no medium findings — does not fire.
  • Row 6 (sticky escalate): prior decision was approve, not escalate.
  • Row 7 (no-auto-approve team): no team match.
  • Row 8 (3+ medium): zero medium findings.
  • Row 9: approve.

high_risk is true only because src/adcp/server/** matched on a modified file; with no medium-or-higher findings, the modification is presumed safe per the high-risk reading rules. No credential/ctx_metadata, semver, forward-compat, import-layering, generated-code, or version-pathway concerns in this diff.

@bokelley bokelley changed the title fix(server): read the A2A profile's input member, and refuse parameters when the profile is activated fix(server): read the A2A profile's input member, and refuse an activated invocation that carries both members Nov 9, 2026
bokelley and others added 5 commits November 9, 2026 09:40
…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.

Backport of 3f97994 from next.

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>
…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>
…tion on 9.x, with a deprecation warning

9.x is the stable line, so a patch must not start refusing requests that work
today. An invocation that activates the AdCP A2A profile but carries only
`parameters`, a missing or non-object `input`, or a DataPart count other than
one is still dispatched as before, and a deprecation warning names the profile
rule. Only an activated invocation carrying both `parameters` and `input` is
refused, since it is ambiguous and no compliant client sends it. adcp 10
refuses every non-conforming activated shape.

Refs #1887

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

Refs #1887

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bokelley
bokelley force-pushed the fix/1887-a2a-input-member-9x branch from fde5614 to 9face59 Compare November 9, 2026 09:40

@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 second-pass review with no blocking findings.

This PR backports the AdCP A2A profile's input invocation member while preserving legacy parameters precedence, refusing only the ambiguous activated-both-members shape with INVALID_REQUEST and no context echo. Member selection is consistent across _invocation_arguments, the receipt raw-body path, and the middleware cached parse. The fix: prefix is the correct semver signal (no public export change). Tests drive the real HTTP app and exercise every new branch including the non-struct input cases — satisfying the wire-change pathway + test requirements.

Decision-table walkthrough:

  • Row 1 (critical/high): no such findings.
  • Row 2 (gated paths): gated_paths is false — does not fire.
  • Row 3 (high-risk deleted): the high-risk match on src/adcp/server/a2a_server.py is (modified), not deleted.
  • Row 4 (medium data-loss/schema/infra): no medium findings.
  • Row 5 (high-risk modified + medium): no medium findings, so does not fire.
  • Row 6 (sticky escalate): prior decision was approve, not escalate.
  • Row 7 (no-auto-approve team): no team match.
  • Row 8 (3+ medium): zero medium findings.
  • Row 9: approve.

The high_risk flag is a heuristic LOOK signal; the modified server file has no medium-or-higher concern, so it is presumed safe. No author team gate, no gated-path gate. Falls through to row 9.

@bokelley
bokelley merged commit e1acde8 into main Nov 9, 2026
47 checks passed
@bokelley
bokelley deleted the fix/1887-a2a-input-member-9x branch November 9, 2026 10:27
@aao-ipr-bot aao-ipr-bot Bot mentioned this pull request Nov 9, 2026
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