feat(server): negotiate media buy version handling and update actions#871
Merged
Conversation
…rsion # Conflicts: # src/adcp/decisioning/handler.py # src/adcp/decisioning/platform.py # tests/test_decisioning_capabilities_projection.py
There was a problem hiding this comment.
Approving. resolved_adcp_version is added as a top-level RequestContext field outside the echo-prone metadata bucket, the widened MCP outputSchema matches the negotiated 3.0 status envelope, and the new update_media_buy decomposer's 21-key _ACTION_CANDIDATES, 4-value mode set, 7-value status union, and field-coverage sets are byte-for-byte aligned with schemas/cache/3.1.0-beta.3/ (ad-tech-protocol-expert: sound-with-caveats).
Things I checked
- Largest-file rule. Read
src/adcp/decisioning/update_media_buy.py(911 lines) andtests/test_update_media_buy_decompose.py(284 lines) in full._compare_ordered/_ordered_value/_numeric_sumcorrectly fail-closed on mixed-type / non-finite values;_package_budget_reallocationexact-sum detection usesDecimal(str(value))arithmetic so float reallocations stay safe. - Public-API audit. New
adcp.decisioningexports (UpdateMediaBuyMutation,decompose_update_media_buy,disallowed_update_media_buy_mutations,is_update_media_buy_mutation_allowed,normalize_update_media_buy_allowed_actions,requested_update_media_buy_actions,SELF_SERVE_UPDATE_ACTION_MODES,UNKNOWN_UPDATE_ACTION) are purely additive.ToolContext.resolved_adcp_version: str | None = Noneandcreate_tool_caller's newdefault_unnegotiated_adcp_versionkwarg both default to the prior behavior.feat(server):without!is the right conventional-commit prefix. Helper additions are documented indocs/handler-authoring.md:711-770. - Credential-echo contract.
dispatch.py:1175-1178addsresolved_adcp_versionalongsiderequest_id/caller_identity/tenant_id, not insidemetadata, so it bypasses neither_validate_ctx_metadata_credentialsnor theclean_metadatafilter — andinject_context()echoes from buyer-suppliedparams["context"], never from theRequestContextobject. No_CREDENTIAL_SHAPED_KEY_SUFFIXESregression (security-reviewer: no High). - Wire-version inputs are bounded.
detect_wire_version()constrains buyer-supplied versions toSUPPORTED_WIRE_VERSIONS;_normalize_a2a_parameters(a2a_server.py:152-163) correctly un-floatsadcp_major_versionto repair the protobuf Struct float quirk while leaving release-precisionadcp_versionprecedence intact perversion-envelope.json. - Test-plan honesty. Validation block lists
ruff,mypy, and named pytest selectors; all three pre-commit checks are exercised. No unchecked manual verification boxes.
Follow-ups (non-blocking — file as issues)
- Schema-widener pins
anyOf[0]by position._widen_media_buy_output_schema_for_legacy_statuses(src/adcp/server/mcp_tools.py:124-157) assumes the success arm is at index 0. If the Pydantic union ever reorders (codegen renumber, discriminator refactor), the widener no-ops silently and 3.0 buyers start failingoutputSchemavalidation — or worse, clobbers the wrong arm'sstatusliteral. Walk theanyOfand widen the arm whoseproperties.status.const == "completed", or assert at module-load that exactly one matches. UNKNOWN_UPDATE_ACTIONbypass is a footgun for unmapped fields._TOP_LEVEL_UNMAPPED_MUTATION_FIELDS = ("invoice_recipient", "push_notification_config", "reporting_webhook")atupdate_media_buy.py:80-84emitaction="unknown"and are silently skipped bydisallowed_update_media_buy_mutations(line 357-366). An adopter relying solely on this helper to gate buyer updates would acceptreporting_webhook: <attacker URL>even whenavailable_actionsdoesn't list it — SSRF or invoice-redirection vectors if the adopter side-effects on those fields. Either add aninclude_unknown=Truemode that surfaces these, or rewritedocs/handler-authoring.md:751-770to call this out as a footgun rather than a feature.- Bare-string / missing-
modeentries fail open._action_mode_matchesatupdate_media_buy.py:799-803returnsTruewhenevermodeis missing on the allowed-action entry. Intentional for bare strings, but an adopter ingesting wire-shapedavailable_actions[]from an upstream platform that omitsmodewill silently allow non-self-serve actions through a sync handler. Fail-closed for mapping/model inputs, fail-open only for bare strings. - Curated field sets drift silently against the upstream schema.
_TOP_LEVEL_KNOWN_MUTATION_FIELDS/_PACKAGE_KNOWN_MUTATION_FIELDSare hand-curated; new spec fields will fall intoUNKNOWN_UPDATE_ACTIONand bypass the gate. Add a one-shot test diffing these sets againstUpdateMediaBuyRequest.model_fieldsandPackageUpdate.model_fields. creatives: []branch is dead under spec validation.package-update.jsondeclarescreatives: minItems: 1, so theremove_creativeheuristic atupdate_media_buy.py:495only fires when input validation is disabled. Either remove the branch and document that creative removal routes throughcreative_assignments, or document the validation-bypass assumption.- Forward
default_unnegotiated_adcp_versionthroughcreate_mcp_tools. Adopters wiring via the framework helper can't override the"3.0"legacy pin without reaching intocreate_tool_callerdirectly.
Minor nits (non-blocking)
__all__placement.is_update_media_buy_mutation_allowedandrequested_update_media_buy_actionsinsrc/adcp/decisioning/__init__.pyare inserted next to unrelated neighbors —is_*betweenrequire_org_scopeandmixed_registry,requested_*betweenresolve_time_budgetandserve. The wider list isn't strictly alphabetical anyway, but these two stand out.targeting_overlaypayload duplication. When the overlay carriesfrequency_capplus other keys,update_media_buy.py:581-611emits bothupdate_targeting(with the full overlay in.after/.raw) andupdate_frequency_caps— audit consumers readingrawsee the cap twice. Either stripfrequency_capfrom the targeting mutation's payload, or call this out in the dataclass docstring.- Top-level
canceled: Falseis silently dropped. It's in_TOP_LEVEL_KNOWN_MUTATION_FIELDSso it doesn't fall through to_unknown_top_level_fields, but the explicitis Truechecks don't pick it up. Document that uncancel isn't a supported action, or emitUNKNOWN_UPDATE_ACTION.
Safe to merge.
This was referenced May 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses #866, #867, #868.
Validation
PYTHONPATH=src ruff check src/adcp/server src/adcp/decisioning tests/test_tools_list_output_schema.py tests/test_server_dx.py tests/test_dispatcher_version_routing.py tests/test_decisioning_capabilities_projection.py tests/test_a2a_server.py tests/test_update_media_buy_decompose.py tests/test_hello_seller_integration.py examples/hello_seller.pyPYTHONPATH=src mypy src/adcp/server/ src/adcp/decisioning/handler.py src/adcp/decisioning/platform.py src/adcp/decisioning/dispatch.py src/adcp/decisioning/update_media_buy.py examples/hello_seller.pyPYTHONPATH=src pytest tests/test_tools_list_output_schema.py tests/test_server_dx.py::TestPydanticSchemas tests/test_dispatcher_version_routing.py tests/test_decisioning_capabilities_projection.py tests/test_a2a_server.py::test_a2a_omitted_version_keeps_current_media_buy_envelope tests/test_a2a_server.py::test_a2a_explicit_major_version_projects_30_media_buy_shape tests/test_update_media_buy_decompose.py tests/test_hello_seller_integration.py -qPYTHONPATH=src pytest tests/test_mcp_middleware_composition.py tests/test_adcp_version_wire.py tests/test_dispatcher_version_routing.py tests/test_schema_validation_server.py tests/test_a2a_server.py::test_a2a_omitted_version_keeps_current_media_buy_envelope tests/test_a2a_server.py::test_a2a_explicit_major_version_projects_30_media_buy_shape tests/test_update_media_buy_decompose.py -qNotes
available_actions[].