Skip to content

Commit fbf13c9

Browse files
bokelleyclaude
andauthored
feat(decisioning)!: AdCP 3.3 seller-side rules (#1720)
Version literals: the capabilities signing gate parses bundle-precision versions (3.3.0-beta.1 read as pre-3.2), PrincipalService accepts 3.3 async payload shapes by default, and account key continuity reduces from the SDK's own release instead of 3.2. sales-fixed-rate rolls up to media_buy in SPECIALISM_TO_PROTOCOLS, so a seller claiming only that specialism advertises supported_protocols. Advertised measurement terms: get_products and list_products refuse a product whose measurement_window names no declared reporting_capabilities.measurement_windows[].window_id, and TermsRejectedError refuses to reject a buyer replaying advertised terms. Property domain identifiers: create/update_property_list reject malformed domain and subdomain identifiers with INVALID_REQUEST, the MCP list resolver rejects a snapshot carrying one instead of canonicalizing it, and normalize_property_list_identifiers converts values before submission. Counterparty-asserted domain lists: bound_counterparty_domains cuts publisher_domains / data_provider_domains to 100 entries, 253 characters and the 8 KiB / 16 KiB byte budgets, and the capabilities projection truncates an oversized list and sets *_complete: false. CreativeMergeRejectedError, RightsChannelViolationError and their details narrowing helpers join the typed errors. BREAKING CHANGE: create_property_list / update_property_list fail with INVALID_REQUEST on mixed-case, trailing-dot or URL-shaped domain identifiers, and MCPPropertyListFetcher fails a resolution carrying one. get_products / list_products fail with INTERNAL_ERROR when a product advertises an undeclared measurement_window. get_adcp_capabilities truncates oversized publisher_domains / data_provider_domains. PrincipalService accepts "3.3" async versions by default. Part of #1668 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 8a7f482 commit fbf13c9

15 files changed

Lines changed: 1514 additions & 21 deletions

‎MIGRATION_v9_to_v10.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ the SDK-relevant inventory is [docs/adcp-3.3-sdk-inventory.md](docs/adcp-3.3-sdk
1919
| `reporting_webhook.operation_id` is a generated field | The field, its constraints and its never-null marker come from the 3.3 schema. The SDK 9 runtime patch is gone; nothing changes on the wire. |
2020
| RFC 9421 signing warns by default | AdCP 3.3 deprecates the `adcp/request-signing/v1` and `adcp/webhook-signing/v1` profiles for Web Bot Auth. Constructing `SigningConfig` or `RequestSignatureVerification` without Web Bot Auth emits a `DeprecationWarning`; both profiles keep working through 3.x. |
2121
| `evaluator_id` is gone from evaluator specs | AdCP 3.3 removes the experimental `build_creative.evaluator.evaluator_id` source form. Use inline `exemplars` or `agent_url`; put the seller's evaluator in `creative_policy.accepted_verifiers[]`. |
22+
| Property-list domain identifiers must be lowercase ASCII hostnames | AdCP 3.3 rejects `Example.com`, `example.com.` and URL-shaped `domain` / `subdomain` identifier values. `PlatformHandler.create_property_list` and `update_property_list` now fail such a request with `INVALID_REQUEST` (`field` names the value) before your platform runs, and `MCPPropertyListFetcher` fails a resolution whose pages carry one (`SERVICE_UNAVAILABLE`, cached snapshot kept) instead of canonicalizing it. Senders call `adcp.property_lists.normalize_property_list_identifiers(request)` before submitting; it lowercases, converts IDNs to A-labels, strips a trailing dot and raises `PropertyListIdentifierError` for anything else. |
23+
| Advertised `measurement_window` must be a declared window | AdCP 3.3 requires `measurement_terms.billing_measurement.measurement_window` on a product to name a `reporting_capabilities.measurement_windows[].window_id`. `get_products` and `list_products` on `PlatformHandler` now fail with `INTERNAL_ERROR` when a returned product advertises an undeclared window; declare the window or drop it. When rejecting buyer terms, raise `TermsRejectedError(proposed_terms=..., advertised_terms=...)`: it refuses (`ValueError`) to reject a buyer replaying the advertised terms. |
24+
| Oversized `publisher_domains` / `data_provider_domains` are truncated | AdCP 3.3 bounds these counterparty-asserted capability lists (100 unique entries, 253 characters each, 8,192 bytes per field, 16,384 combined). The decisioning `get_adcp_capabilities` projection truncates a list that exceeds them, sets `publisher_domains_complete` / `data_provider_domains_complete` to `false` and logs a warning. Declare the publishers you represent rather than mirroring `adagents.json`; `adcp.decisioning.bound_counterparty_domains` bounds a list and reports `complete`. |
25+
| `PrincipalService` accepts `3.3` async payloads | The default `accepted_async_adcp_versions` is `("3.2", "3.3")` (was `("3.2",)`), since the SDK emits 3.3-line payloads. A seller pinned to 3.2 passes `accepted_async_adcp_versions=("3.2",)`. |
2226
| Schema defaults appear in dumps for new 3.3 fields | `GetProductsRequest.include_collections` (`false`) and `proposal_refinement.hold_extension` (`false`) carry schema defaults, so a dumped model declares them. Tests that compare whole dictionaries need the new keys; a 3.3 → 3.2 downgrade drops them with a recorded loss. |
2327
| 3.3 → 3.2 downgrades apply semantic rules | A buyer below 3.3 of a seller answering in 3.3 no longer gets `VERSION_UNSUPPORTED` for these responses: `by_spot` rows without `aired_at` (`scheduled` / `preempted`) and delivery rows without `impressions` are omitted (losses `row_omitted_not_aired`, `row_omitted_impressions_not_measured`), never sent with `impressions: 0`; `media_buy.features.catalog_ingestion` and a `price_breakdown` carrying an `operator_fee` adjustment are dropped; `age_determination_bases` moves to `ext.adcp.age_determination_bases`, and a 3.2 response upgraded to 3.3 reads it back into an absent top-level field. Requests the 3.2 contract cannot express fail with `ADCPVersionConversionError` (`UNSUPPORTED_FEATURE` on the client) before sending: a `reporting_webhook` / `artifact_webhook` without `authentication` (keep sending it while you buy from 3.2 sellers; the SDK never invents one) and a daypart with `start_time` / `end_time` (use `start_hour` / `end_hour`; the SDK never rounds). The downgrade's final check now validates formats as the SDK validators do: earlier betas let such a request through invalid when it carried `start_time: "asap"`. See [docs/cross-version-conversion.md](docs/cross-version-conversion.md#33--32-semantic-rules). |
2428
| Delivery rows may omit `impressions` | Every `by_*` delivery-metrics row makes `impressions` optional. Treat absence as "not measured", never as zero. |

‎docs/adcp-3.3-sdk-inventory.md‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ it adopted the bundle, and what is left to the codegen/wire-fidelity (2) and
2727
| Negotiation | `negotiate_version` matches prereleases exactly. The client's outgoing conversion treats the SDK's served prerelease as its line (`is_convertible_version`), so a 3.3-beta.0 buyer still negotiates down to a 3.1 or 3.2 seller. | done (1) |
2828
| Translation | `convert_payload` steps 3.3-beta.0 ↔ 3.2 ↔ 3.1 ↔ 3.0 through the bundled schemas; the 3.3 step uses the `3.3.0-beta.0` bundle. Downgrades go through `_require_target_valid`, so a converted payload the older schema refuses fails as `ADCPVersionConversionError` (#1621 / #1630). | done (1); per-field rules below are (3) |
2929
| Restricted servers | A server that lists `supported_versions` without `adcp_version` now pins to the newest listed release (`version_policy.default_adcp_version`), instead of refusing because the SDK default (3.3-beta.0) is outside the list. | done (1) |
30-
| Handler gate | `PlatformHandler.get_adcp_capabilities` used `startswith("3.2")` for the proposal-manager projection; it is now "3.2 or later". Audit every other `"3.2"` literal (`accounts.py`, `dispatch.py`, `signing/*`, `server/principal.py accepted_async_adcp_versions`) for 3.3 inclusion. | (3): #1668 |
30+
| Handler gate | `PlatformHandler.get_adcp_capabilities` used `startswith("3.2")` for the proposal-manager projection; it is now "3.2 or later". Audit of every other `"3.2"` literal: `dispatch.py` legacy-format gate, `accounts.py` key continuity, `server/principal.py` relationship gate and the `signing/*` profile map (`signing_profile_for_adcp_version` maps 3.3 to the 3.2 RFC 9421 profile) already include 3.3; the remaining `"3.2"` literals in `signing/*` and `governance.py` name the 3.2 signing / JWS profile, not a release floor. Fixed: the capabilities signing gate (`_is_adcp_32_or_newer`) now parses bundle precision (`3.3.0-beta.1` read as pre-3.2); `PrincipalService` accepts `3.3` async payload shapes by default (`("3.2", "3.3")`); account key continuity reduces from the SDK's own release rather than `3.2`. Open: `media_buy_actions._supports_change_term_id` treats `3.3-beta.N` (N < 9) as pre-3.2-beta.9. | done (3), #1668; `media_buy_actions` follow-up |
3131
| `adcp_version` on `list_products` responses | `ListProductsResponse1/2` gained `adcp_version`. | generated |
3232
| `search_brands` | `SearchBrandsRequest` gained `adcp_major_version`. | generated |
3333

@@ -62,7 +62,7 @@ it adopted the bundle, and what is left to the codegen/wire-fidelity (2) and
6262
| `history[].origin` | `get_media_buys` `HistoryItem.origin` (`seller_operator`, `seller_system`, `connected_platform`, `unknown`) | Seller `revision` advances on every committed change (DR-0031); buyers pass the latest revision and expect `CONFLICT`. | generated |
6363
| `creative_assignments_as_of`, `approval_status_as_of` | `get_media_buys` `Package`, `CreativeApproval`; `list_creatives` `AssignedPackage` | Timestamps. | generated |
6464
| `landing_page_url`, `trackers` on assignments | `core/creative-assignment.json`, `core/assignment-tracker.json`; `sync_creatives` `Assignment` / `AssignmentOperations1`; `list_creatives` `AssignedPackage`; `get_media_buys` `CreativeApproval`; capabilities `creative.supports_assignment_landing_page`, `creative.supports_assignment_trackers` | Per-assignment overrides. | generated |
65-
| `sync_creatives` `merges[]`, `AssignmentOperations4` | `sync_creatives` response `Merge`; error `CREATIVE_MERGE_REJECTED` with `error-details/creative-merge-rejected.json`; enum `creative-merge-rejection-reason`; capability `creative.supports_assignment_merge` | Creative merge. | generated; error-details narrowing: (3), #1668 |
65+
| `sync_creatives` `merges[]`, `AssignmentOperations4` | `sync_creatives` response `Merge`; error `CREATIVE_MERGE_REJECTED` with `error-details/creative-merge-rejected.json`; enum `creative-merge-rejection-reason`; capability `creative.supports_assignment_merge` | Creative merge. | generated; `CreativeMergeRejectedError` mints schema-valid details (field defaults to the first rejected `assignment_operations` entry) and `creative_merge_rejected_details` narrows them: done (3), #1668 |
6666
| `list_creatives` `platform_id`, `provenance`, `seller_revision`, `status_as_of`; request filter `provenance` | `creative/list-creatives-response.json`; `x-entity-types` `creative_seller_revision`; capability `creative.stable_creative_ids` | `platform_id` is native in 3.3 (adcp#8175). There was no forward-compat patch for it in this SDK; nothing to retire. | generated; `stable_creative_ids` seller support and `delete_missing` exclusion: (3), #1638 |
6767
| `validate_input` `Warning.field` | `creative/validate-input-result.json` | `field` is now declared as a string (#8212). | generated |
6868
| `build_creative` `errors` | `BuildCreativeResponse1/3` | Errors array on build responses. | generated |
@@ -101,7 +101,7 @@ it adopted the bundle, and what is left to the codegen/wire-fidelity (2) and
101101
| Field | Where | SDK status |
102102
| --- | --- | --- |
103103
| `signal_status`, `source_audience_id`, `age_determination_bases` | `get_signals` request `Field1` and response `Signal`; `SignalDefinition`, `SignalDefinitionEnrichment`, `SignalListing`; enum `signal-status`; capability `signals.features.signal_lifecycle` | generated; interim `ext.adcp.age_determination_bases` for 3.2 peers (top-level wins): done (3), #1667 |
104-
| `signals.data_provider_domains_complete`, `media_buy.portfolio.publisher_domains_complete` | capabilities; byte budgets 8,192 per field / 16,384 combined, 100 entries, 253 chars each; absence is never a denial | generated; bound enforcement in `property_lists` / portfolio readers: (3), #1668 |
104+
| `signals.data_provider_domains_complete`, `media_buy.portfolio.publisher_domains_complete` | capabilities; byte budgets 8,192 per field / 16,384 combined, 100 entries, 253 chars each; absence is never a denial | `adcp.decisioning.bound_counterparty_domains` cuts a list to the bounds and reports `complete`; `get_adcp_capabilities` projection truncates an oversized declared list and sets `*_complete: false`. The SDK has no buyer-side reader that treats either list as an allowlist: done (3), #1668 |
105105

106106
### Accounts and identity
107107

@@ -119,7 +119,7 @@ it adopted the bundle, and what is left to the codegen/wire-fidelity (2) and
119119
| `error-details/unsupported-targeting-dimension.json` | under `UNSUPPORTED_FEATURE`; adapters minting `UNSUPPORTED_TARGETING_DIMENSION` / `DEPRECATED_TARGETING_DIMENSION` should migrate | | generated; decisioning error minting: (3), #1642 |
120120
| `error-details/agent-permission-denied.json` `Reason` | new nested enum | | generated |
121121
| `brand.json` fetch cap 2 MiB | all fetch paths; `MAX_JWKS_BYTES`, `MAX_CAPABILITIES_BYTES` unchanged | 3.2 verifiers may reject files between 256 KiB and 2 MiB. | done (3), #1654: 2 MiB, depth 32, 10 s total deadline, bounded concurrent fetches; follow-ups on #1635 |
122-
| Property `domain` / `subdomain` lowercase ASCII hostnames | property schemas; invalid values → `INVALID_REQUEST` | | validation and sender normalizer: (3), #1668 |
122+
| Property `domain` / `subdomain` lowercase ASCII hostnames | property schemas; invalid values → `INVALID_REQUEST` | | `PlatformHandler.create_property_list` / `update_property_list` reject invalid `domain` / `subdomain` identifiers with `INVALID_REQUEST` (`validate_property_list_identifiers`); `MCPPropertyListFetcher` rejects a snapshot carrying one instead of canonicalizing it; sender normalizer `adcp.property_lists.normalize_property_list_identifiers`: done (3), #1668 |
123123
| One agent-resolution rule | request signing, webhook signing, governance JWS `iss`, response signing, rights attestations; pinned `signing_keys` published in JWKS; RFC 7638 thumbprints; `key_origins`; `agent_url_match: canonical` | | `adcp.signing` audit: (3) |
124124
| RFC 9421 profile deprecation | `adcp/request-signing/v1`, `adcp/webhook-signing/v1` deprecated (removal no earlier than 4.0) in favour of Web Bot Auth | `rfc9421_deprecation_active()` keys on a 3.3 line in `COMPATIBLE_ADCP_VERSIONS`, so SDK 10 warns by default on legacy `SigningConfig` / `RequestSignatureVerification` construction. | done (1) |
125125

@@ -144,24 +144,24 @@ Every new declaration is optional; 3.2 behaviour stands when undeclared.
144144
| `reporting_capabilities` | `available_installment_statuses[]`; `spot_reporting.available_statuses[]`, `supports_break_position` | generated |
145145
| `supported_indicator_types` / `indicator_type` | `flight_extension_opportunity`, `scale_budget_opportunity` | generated |
146146
| `notification event types` | `collection_card_removed`, `collection_card_updated`, `collection_catalog_bulk_change`, `proposal_created`; wholesale feed `EntityType.collection`, `WholesaleFeedEvent10/11/12`, `Payload26/27/28` | generated |
147-
| specialisms | `sales_fixed_rate` (new specialism for fixed-price, non-guaranteed sellers); `sales-broadcast-tv` 1.1.0 | generated; `adcp.decisioning.specialisms`: (3), #1668 |
147+
| specialisms | `sales_fixed_rate` (new specialism for fixed-price, non-guaranteed sellers); `sales-broadcast-tv` 1.1.0 | generated; `sales-fixed-rate` has `REQUIRED_METHODS` / recommended methods / advertised tools and rolls up to `media_buy` in `SPECIALISM_TO_PROTOCOLS`. The packaged compliance assets carry no specialism storyboards, so `sales_fixed_rate` runs from the protocol's compliance tree: done (3), #1668 |
148148
| `canonical_creatives` | still not declared by 3.3 | the `_forward_compat` capability patch stays |
149149

150150
## 5. Error codes and error details
151151

152152
| Code / detail | Schema | Where it fires | SDK status |
153153
| --- | --- | --- | --- |
154154
| `REFERENCE_DEFINITION_CHANGED` | `error-details/reference-definition-changed.json` | `accept_proposal` when a pinned definition drifted; send only to 3.3 peers | generated; until then the 3.2 downgrade keeps it, since 3.2 `error.code` is open (#1667); minting and 3.2 downgrade mapping: (3), #1641 |
155-
| `CREATIVE_MERGE_REJECTED` | `error-details/creative-merge-rejected.json` (`Rejection`, reason enum) | `sync_creatives` merges | generated; (3), #1668 |
156-
| `RIGHTS_CHANNEL_VIOLATION` | `enums/error-code.json` | rights constraints `channels` / `excluded_channels` | generated; (3), #1668 |
155+
| `CREATIVE_MERGE_REJECTED` | `error-details/creative-merge-rejected.json` (`Rejection`, reason enum) | `sync_creatives` merges | `CreativeMergeRejectedError`, `creative_merge_rejected_details`: done (3), #1668 |
156+
| `RIGHTS_CHANNEL_VIOLATION` | `enums/error-code.json` | rights constraints `channels` / `excluded_channels` | `RightsChannelViolationError` (details `rights_id`, `channel`, `capability_id`; usable as an advisory `errors[]` entry via `to_wire()`), `rights_channel_violation_details`: done (3), #1668 |
157157
| `UNSUPPORTED_FEATURE` + `unsupported-targeting-dimension` | `error-details/unsupported-targeting-dimension.json` (`SupportedDimension`) | replaces adapter-minted `UNSUPPORTED_TARGETING_DIMENSION` / `DEPRECATED_TARGETING_DIMENSION` | generated; (3), #1642 |
158158
| `mutation_outcome` | `core/error.json` | every error (`not_applied`, `unknown`, `applied`) | generated; (3), #1642 |
159159
| `definition_changed` | `enums/proposal-refinement-reason.json` | `refine_proposals` `unable` with `drifted_pins[]` | generated |
160160
| `REVISION_UNREADABLE` with `reporting_revision_id` | 3.2.4 | already in SDK 9 | done |
161161
| `request_body_malformed` | A2A hardening (strict unsigned-body parsing, Part shape rules, task-continuation binding) becomes MUST | | done (3), #1659 |
162162
| `request_target_uri_malformed` | conformance vector `negative/026-non-ascii-host` re-coded | adopted with 3.2.4 (#1479) | done |
163-
| `TERMS_REJECTED` | must not fire for replayed advertised measurement terms; windows reference `reporting_capabilities.measurement_windows[].window_id` | | decisioning: (3), #1668 |
164-
| `COMPLIANCE_UNSATISFIED` | `build_creative` refinement that narrows inherited `required_disclosures` / `prohibited_claims` | | (3), #1668 |
163+
| `TERMS_REJECTED` | must not fire for replayed advertised measurement terms; windows reference `reporting_capabilities.measurement_windows[].window_id` | | `get_products` / `list_products` refuse a product advertising an undeclared window (`INTERNAL_ERROR`, `check_advertised_measurement_terms`); `TermsRejectedError(proposed_terms=, advertised_terms=)` refuses to reject a replay (`is_advertised_terms_replay`): done (3), #1668 |
164+
| `COMPLIANCE_UNSATISFIED` | `build_creative` refinement that narrows inherited `required_disclosures` / `prohibited_claims` | | not applicable: the SDK passes `build_creative` (including `refine_from_build_variant_id`) to the platform and retains no leaf compliance inputs, #1668 |
165165
| `ACCOUNT_NOT_FOUND` | discovery with an unresolved `account` | | done (#1288); storyboard confirmation: (3) |
166166

167167
## 6. Enum changes
@@ -191,7 +191,7 @@ Every new declaration is optional; 3.2 behaviour stands when undeclared.
191191

192192
| Item | Detail | SDK status |
193193
| --- | --- | --- |
194-
| `evaluator_id` source form removed | `core/evaluator-spec.json`: `EvaluatorSpec3` gone, `EvaluatorSpec2` gains `agent_url` and loses `evaluator_id`; `list_evaluators` withdrawn; the seller's evaluator goes in `creative_policy.accepted_verifiers[]`. No alias. | generated; any SDK helper that minted `evaluator_id`: (3), #1668; migration note in `MIGRATION_v9_to_v10.md` |
194+
| `evaluator_id` source form removed | `core/evaluator-spec.json`: `EvaluatorSpec3` gone, `EvaluatorSpec2` gains `agent_url` and loses `evaluator_id`; `list_evaluators` withdrawn; the seller's evaluator goes in `creative_policy.accepted_verifiers[]`. No alias. | generated; no SDK helper mints `evaluator_id` (only the 3.1 / 3.2 versioned views keep it): nothing to remove, #1668; migration note in `MIGRATION_v9_to_v10.md` |
195195
| `seller_id` on event source and audience results | deprecated for `platform_id`; sellers emit both with identical values | generated; seller dual emission and buyer preference: (3), #1639 |
196196
| Natural-key `AccountRef` after provisioning | deprecated; `account_id` with `account.stable_account_id` | (3), #1478 |
197197
| RFC 9421 signing profiles | deprecated for Web Bot Auth; verifiers keep accepting through 3.x | warning: done (1) |

‎docs/decisioning-capabilities.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ alongside the spec; they line up.
9191
- **`media_buy.features`** — `inline_creative_management`,
9292
`property_list_filtering`, `catalog_management` flags that gate
9393
buyer-side flow.
94+
- **`media_buy.portfolio.publisher_domains`** — the publishers you actually
95+
represent, not every domain whose `adagents.json` names you. AdCP 3.3 bounds
96+
this counterparty-asserted list (and `signals.data_provider_domains`): at most
97+
100 unique entries of at most 253 characters, 8,192 bytes per field and
98+
16,384 bytes for both, measured as compact JSON including the member name.
99+
`bound_counterparty_domains(domains)` cuts a list to those bounds in declared
100+
order and reports `complete`; set `publisher_domains_complete=False` (or
101+
`data_provider_domains_complete=False`) when it is not. The framework applies
102+
the same bound to every `get_adcp_capabilities` response it projects: a list
103+
that does not fit is truncated, its `*_complete` flag set to `false`, and a
104+
warning logged. Buyers must never treat either list as an allowlist.
94105

95106
### When you support other protocols
96107

0 commit comments

Comments
 (0)