Repository navigation
feat(server)!: refuse an undeclared creative start policy on control_media_buy - #1872
Conversation
…media_buy A control_media_buy package that sets start_requires_all_creatives_approved: true now reaches the handler only when the seller declares media_buy.features.creative_start_policy: true. Otherwise the dispatcher answers UNSUPPORTED_FEATURE before the handler runs (recovery correctable, field packages[N].start_requires_all_creatives_approved for the first opted-in package, details.fields listing every one, mutation_outcome not_applied), as AdCP 3.3 requires. false and omission are always allowed and read no capabilities. The declaration is the seller's own: PlatformHandler reads its request-scoped DecisioningCapabilities (get_adcp_capabilities_for_request included), and any other ADCPHandler reads its own get_adcp_capabilities response, through the new ADCPHandler._declared_capabilities_for_request hook. The check applies to handlers on AdCP 3.3 or later, like changed_paths enforcement. create_media_buy and update_media_buy are not checked: AdCP 3.3 withdraws the field from their packages (adcontextprotocol/adcp#8767) and their open schemas ignore it (DR-0009). BREAKING CHANGE: control_media_buy refuses packages[].start_requires_all_creatives_approved: true with UNSUPPORTED_FEATURE before the handler runs when the seller's capabilities do not declare media_buy.features.creative_start_policy: true. A seller that honors the gate declares the feature. Closes #1869 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean PR, no blocking or medium findings.
This PR adds a dispatcher-level gate: control_media_buy requests with packages[].start_requires_all_creatives_approved: true are refused (UNSUPPORTED_FEATURE, recovery: correctable, mutation_outcome: not_applied) before the handler runs unless the seller declares media_buy.features.creative_start_policy, on handlers at 3.3+.
Verified against repo-specific gates:
- Semver signal present:
feat(server)!:prefix plus migration note; no public export removed or type-changed. - Fail-closed behavior: no discovery / no feature /
feature=falseall refuse;_not_supportedreads as not-declared rather than raising. - Version-translation pathway (DR-0032/0033): 3.3 buyer → 3.2 handler refused by conversion (
VERSION_UNSUPPORTED); genuine 3.2 seller ignores per open schema (DR-0009). This is the "Refused" pathway and is covered by tests. mutation_outcome: not_appliedstamped pre-claim-insert by the existingmark_not_applied; test assertion holds.- Capabilities read only when a package opts in (discovery_calls==0 otherwise).
- Rename
_requires_changed_paths→_handler_runs_adcp_33consistent across def/call/tests. - Largest-file (test, 336 new lines) control flow verified: base, decisioning, request-scoped, and roundtrip paths covered.
Decision table: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); no deletions in high-risk reasons (row 3 n/a); no medium findings in data-loss/schema/infra (row 4 n/a); no medium findings at all so rows 5/8 n/a; no prior escalation (row 6 n/a); no no-auto-approve team match (row 7 n/a). high_risk is true only from modified/added files with no medium-or-higher concerns — not an escalation trigger on the flag alone. Falls through to row 9: approve.
… seller known not to declare it (#1878) AdCP 3.3 forbids a buyer from sending packages[].start_requires_all_creatives_approved: true on control_media_buy to a seller without media_buy.features.creative_start_policy: true. On a session negotiated at 3.3 or later, the client now checks its cached get_adcp_capabilities response (the one version negotiation already uses) and, when the seller does not declare the feature, returns a failed TaskResult before anything is sent: dispatched=False, adcp_error.code UNSUPPORTED_FEATURE, adcp_error.field packages[N].start_requires_all_creatives_approved for the first opted-in package, and adcp_error.details with feature and every opted-in field, the vocabulary the seller's own refusal uses (#1872). A client that has not discovered the seller (a pinned server_version) sends the request and lets the seller decide. false and omission always pass, and a session below 3.3 behaves as before: version conversion refuses true there. ADCPFeatureUnsupportedError gains optional field and details, which the pre-send failed result carries. The shared helpers move from adcp.server._creative_start_policy to adcp._creative_start_policy so the client does not import the server package. Closes #1876 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
AdCP 3.3 lets a buyer hold a package's first enablement for serving until every assigned creative is approved:
packages[].start_requires_all_creatives_approvedoncontrol_media_buy. A seller that does not declaremedia_buy.features.creative_start_policy: trueMUST rejecttruewithUNSUPPORTED_FEATUREbefore mutation.falseis the default and always allowed.Before this change the SDK let
truethrough. A seller that didn't know the field ignored it, and the buy started serving before its creatives were approved, which is the opposite of what the buyer asked for. The dispatcher now enforces the rule from the seller's own declared capabilities, the way it enforces the other 3.3 seller-side rules (#1668).Re-scoped per the comment on #1869: AdCP 3.3 withdraws the field from
create_media_buy/update_media_buypackages (adcontextprotocol/adcp#8767), so the check is oncontrol_media_buyonly.Behaviour
create_tool_caller(the dispatch every MCP and A2A tool call goes through) runs the check forcontrol_media_buyafter schema and model validation and before the handler runs, when the handler is on AdCP 3.3 or later (the same version gate aschanged_paths):true: nothing happens, and capabilities are not read.trueand the seller declares the feature: the request reaches the handler unchanged.{ "code": "UNSUPPORTED_FEATURE", "message": "start_requires_all_creatives_approved: true requires media_buy.features.creative_start_policy, which this seller does not declare", "field": "packages[0].start_requires_all_creatives_approved", "suggestion": "Omit start_requires_all_creatives_approved or send false; check media_buy.features.creative_start_policy in get_adcp_capabilities before opting in", "recovery": "correctable", "details": {"feature": "media_buy.features.creative_start_policy", "fields": ["packages[0].start_requires_all_creatives_approved"]}, "mutation_outcome": "not_applied" }fieldnames the first opted-in package anddetails.fieldslists every one, so the rejection is atomic and the buyer can fix them all at once.mutation_outcome: not_appliedcomes from the existing pre-dispatch marking.Where the declaration comes from
A new
ADCPHandler._declared_capabilities_for_request(params, context)hook supplies it:PlatformHandlerreturns its request-scopedDecisioningCapabilities, so a tenant-specific declaration fromget_adcp_capabilities_for_requestcounts. This is the same resolutionrefine_proposalsand push-config admission already use.ADCPHandler(including the decorator builder) returns its ownget_adcp_capabilitiesresponse. A handler that doesn't implement discovery declares nothing, sotrueis refused.Not gated
create_media_buy/update_media_buy: 3.3 removes the field from their packages, and their open schemas ignore it (DR-0009). There's a test pinning this.trueto a 3.2 handler is already refused by request conversion (VERSION_UNSUPPORTED).Repro (#1869, adapted to
control_media_buy)Before:
handler called: True. After:ADCPTaskErrorwith the error above, andhandler called: False.TypeScript SDK parity
@adcp/sdk(adcontextprotocol/adcp-clientmainatb6e5caf8d) doesn't enforce this. Its server code never readsfeatures.creative_start_policy. The only related checks are the boolean-shape and 3.3-version checks on the buyer-sideupdate_creative_start_policyaction assessment (src/lib/media-buy/action-assessment.ts). So there was no TS error shape to copy, and the shape above follows the spec (UNSUPPORTED_FEATURE,correctable, JSONPath-litefield). A matching TS issue should be filed.Other changes
_requires_changed_pathsis renamed_handler_runs_adcp_33and shared by both 3.3 seller rules.DecisioningPlatform.get_adcp_capabilities_for_requestdocuments the new call site.docs/adcp-3.3-sdk-inventory.md(beta.1 delta row),docs/decisioning-capabilities.md(media_buy.features.creative_start_policy) and aMIGRATION_v9_to_v10.mdrow.Test plan
tests/test_control_media_buy_creative_start_policy.py(16 tests):trueis refused with no discovery, with no feature, and with the feature set tofalse;truepasses when the seller declares the feature;falseand omission always pass without reading capabilities;field/details.fieldsare correct across several packages;update_media_buy;ADCPClientover MCP withmutation_outcome: not_applied;PlatformHandlertests cover no feature,false, declared, and tenant-scoped.-n 8;test_import_memory.pyrun serially: 6 passed)make typecheck-allmake linttests/test_import_memory.pyBREAKING CHANGE:
control_media_buyrefusespackages[].start_requires_all_creatives_approved: truewithUNSUPPORTED_FEATUREbefore the handler runs when the seller's capabilities do not declaremedia_buy.features.creative_start_policy: true. A seller that honors the gate declares the feature.Closes #1869
🤖 Generated with Claude Code