Repository navigation
fix(server): read the A2A profile's input member, and refuse an activated invocation that carries both members - #1889
Conversation
There was a problem hiding this comment.
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 despitereview_decision: REVIEW_REQUIRED. high_risk: trueis driven solely bysrc/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_argumentsreadsparameterswhen present else the profile'sinput, never merging — unactivated callers dispatch exactly as before, so no backward-compat break._profile_invocation_violationfails closed on activated requests with bad shape, refusing with INVALID_REQUEST and echoing nocontext(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.
There was a problem hiding this comment.
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.pyis 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.
…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>
fde5614 to
9face59
Compare
There was a problem hiding this comment.
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.
Backport of #1888 to the 9.x line (
main), so a 3.2-line server interoperates with the 3.2 JS client (@adcp/sdk16.7.0, theadcp-3.2dist-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
parametersmember. A client that follows the AdCP A2A profile sends{"skill": ..., "input": {...}}.@adcp/sdk16.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:contextwas not echoed, and versioned tools failed withVERSION_UNSUPPORTED: adcp_version is required.Rule on 9.x: which member is read
parametersonlyparameters, as before. Covers A2A 0.x clients and this SDK's ownA2AAdapter.inputonlyinput(new)parameters, as before. The members are never merged.inputonlyinput: the fixINVALID_REQUESTbefore the handler runs, and nocontextechoed. It's ambiguous, and no compliant client sends it.parametersonlyparametersas before, and a deprecation warning is loggedinputmissing or not an object, or the DataPart count isn't one"Activated" means that the
A2A-Extensionsheader, or the Message's ownextensionslist, nameshttps://adcontextprotocol.org/extensions/adcp/v3.@adcp/sdksends both.The warning goes to the
adcp.server.a2a_serverlogger and names the reason (parameters_member,input_missing,data_part_count). It quotes the profile rule that "parametersis not an alias forinput", so operators can find non-conforming clients before 10.x refuses them.Design
_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_invocation_violation). The executor seesRequestContext.requested_extensions, which a2a-sdk fills from theA2A-Extensionsheader.Valuedirectly, so a non-finite number can't make it throw.message_parser, because that parser owns its own wire shapes.context. Echoing it from either member would mean choosing between them.a2a_receipt_parameters, used forsync_reporting_receiptsandget_reporting_status) reads the same member. Before this change it also droppedinput.Verification
tests/test_a2a_profile_input_member.pydrives the real A2A app over HTTP. It covers:message.extensions;parameters;input;inputcases fail against the pre-fix code (3ede78041).tests/test_a2a*.py,*auth*,*signed*,*receipt*,test_unified_mcp_a2a.py,test_seller_a2a_client.py): 1026 passed locally.@adcp/sdk@16.7.0 storyboard run <agent> capability_discovery --protocol a2a, served fromexamples/seller_agent.py'sDemoSellerover A2A.failing, with "Response echoes back the context object at /context" on both steps, the issue's exact symptom.media_buy_sellerover A2A with the same runner: 56 passed, 0 failed.CI note:
Test Python 3.12failstest_reporting_period_date.py::test_legacy_zone_aliases_resolve_to_the_canonical_dateon the newubuntu-26.04runners. The cause is not this change: the image's zone database drops the legacy aliases, and 9.x has notzdatadependency. #1890 pinstzdataand fixes it. This PR will rebase onto it once it merges.🤖 Generated with Claude Code