# Changelog ## 3.2.0-rc.7 ### Minor Changes - 2785fcb: Add `viewable_rate` as a `kind: "metric"` optimization goal, closing a schema omission: the optimization docs already described viewability as a standard metric goal, but neither goal schema accepted it. A `viewable_rate` goal requires a viewability `standard` (`mrc` or `groupm`), takes an optional measurement `vendor`, and bounds `threshold_rate.value` to at most 1. Products and seller capabilities can advertise `viewable_rate` in their supported optimization metrics, and products can declare `metric_optimization.supported_viewability_standards`. When a package carries both a `viewable_rate` goal and a viewability performance standard, a lower goal never relaxes the standard. ### Patch Changes - 26432e6: Declare the deprecated `account` field in `media-buy/list-creative-formats-request.json` so the universal `pagination_integrity_creative_formats` storyboard keeps its account scoping against media-buy agents. Previously the runner stripped `account` and emitted `input_schema_field_stripped`, so sellers that scope seeded formats by account could return a different result set during the pagination walk. - 0547bde: Seed fixture accounts in package_correlation_legacy_fallback storyboard so account-scoped requests resolve before attribution is checked. - 7b5472f: Stop the universal `read_tool_idempotency` storyboard from sending the creative-agent-only `type` filter to `list_creative_formats`, avoiding spurious `input_schema_field_stripped` notices when the target is a media-buy agent. - e82f055: Tighten the unreleased `viewable_rate` optimization goal before 3.2.0-rc.7. The legacy goal shape now accepts only `threshold_rate` targets for `viewable_rate`, so a meaningless `cost_per` target is rejected rather than silently capped at 1. Viewability `standard` and `vendor` are now allowed on `viewed_seconds` goals too, which were already governed by the viewability standard, and both goal shapes reject those fields on other metrics. The migration guide documents the `BrandRef`-to-`BrandKey` vendor mapping when converting legacy goals to the canonical shape. ## 3.1.24 ### Patch Changes - 802211e: Gate the 3.1 governance approval and conditions storyboards on `media_buy.governance_aware` so sellers that do not claim governance support are not graded against multi-agent governance scenarios. - 150139c: Keep the 3.1 guaranteed and non-guaranteed sales baselines ungoverned, and isolate their account natural keys so governance bindings from other compliance scenarios cannot leak into their media-buy requests. Fixes #7628. - f2abd96: Canonicalize `format_ids[].agent_url` in the `get_products_pagination_integrity` storyboard: `https://compliance.adcontextprotocol.org` → `https://compliance.adcontextprotocol.org/`. `core/format-id.json` requires callers to canonicalize `agent_url` before treating two `format-id` values as the same, and `docs/reference/url-canonicalization.mdx` step 5 substitutes `/` for an empty path when an authority is present — so a schema-conformant seller emits the trailing slash and failed the storyboard's raw string comparison. Corrects all six occurrences (both seeded fixtures, both request filters, and the `field_contains` values on `wholesale_first_page` and `wholesale_terminal_page`). Partially addresses #7367; the runner-side canonicalization in `adcp-client` remains the general fix. - 56f93da: Correct the 3.1 creative-generative storyboard to require `creative_manifest.format_kind` instead of the mutually exclusive legacy `format_id`, matching the canonical-formats storyboard and response schema. - 5f52da3: Fix `VERSION_UNSUPPORTED` recovery value in error-compliance storyboards: `fatal` → `correctable`, matching `core/error.json` `enumMetadata`. Also corrects the general error-shape narrative enum list from `correctable, transient, or fatal` to `transient, correctable, or terminal`. Affects `error-compliance.yaml` and `error-compliance-signals.yaml`. Backport of #7376 to the 3.1.x line. - 023d726: Route the 3.1 governance approval and conditions storyboards across their sales and governance agents, capture the synchronized governance plan ID, and include it in the governed media-buy request. - cf424a5: Provenance storyboards now mark every creative-library and test-controller step with its actual tool prerequisite, so hosted grading does not fail sellers for optional surfaces they do not advertise. Fixes #7586. - b23175a: Keep the universal media-buy lifecycle and non-governance seller specialisms ungoverned, and isolate their account natural keys so governance bindings from other compliance scenarios cannot leak into their runs. Refs #7585. ## 3.2.0-rc.6 ### Patch Changes - a601dfb: Correct three `enums/error-code.json` prose recovery tags that named a value outside the closed `recovery` enum. `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE`, `PIXEL_TRACKER_LOSSY_DOWNGRADE` and `PIXEL_TRACKER_UPGRADE_INFERRED` ended their `enumDescriptions` with `Recovery: warning`, but `core/error.json` closes `recovery` to `transient` / `correctable` / `terminal`, and all three carry `correctable` in `enumMetadata` — which the block's own `$comment` makes the normative authority the prose MUST match. Prose now reads `Recovery: correctable — non-fatal advisory, do not auto-retry`, preserving the non-fatal semantics without inventing an enum member. Extends `tests/error-recovery-vectors.test.cjs` to hold `error-code.json` to the same prose/metadata agreement already asserted for `request-signing-error-code.json`. No schema shape or wire-behaviour change. - b01c18b: Reconcile the experimental Reliable Reporting waiver lifecycle with its health and issue projections. An exact bilateral waiver now retires the caller-scoped `CONSUMER_STATUS_MISMATCH`, restores the underlying seller health in summary and period views, leaves the immutable consumer statement auditable, and uses the existing `reporting.status_changed` recovery transition when health changes. This repairs an unrepresentable state without changing the JSON Schema wire shape. - 5300e38: Keep the unpublishable 3.2.0-rc.5 bundle out of release aliases and advance its two post-cut release blockers through a freshly reviewed release candidate. ## 3.2.0-rc.5 ### Minor Changes - 6947106: Add the shared MediaBuy `name` to `buy_products`, `accept_proposal`, and the compact commitment response, keeping it outside accepted commercial terms and defining buyer-supplied precedence over a proposal-derived default. - 7dfeef8: Re-add `sales-exchange` specialism with storyboard covering programmatic exchange product discovery with floor prices and bid guidance, auction-priced media buys, in-flight bid adjustment, and delivery with auction transparency metrics. Addresses the exchange portion of #2511. - 7dfeef8: Re-add `sales-retail-media` specialism with storyboard covering catalog sync, keyword-targeted sponsored search, item availability management, and product discovery for retail media networks. Addresses the retail-media portion of #2511. - 7dfeef8: Re-add `sales-streaming-tv` specialism with storyboard covering CTV product discovery, VAST creative sync, household frequency-capped media buys, and delivery with household reach metrics. Addresses the streaming-tv portion of #2511. - 7082ff1: Add an optional seller-policy decline reason to `REQUOTE_REQUIRED` and buy-specific `ACTION_NOT_ALLOWED` errors, with typed details for the existing envelope-field and change-term conventions. The coarse vocabulary includes inventory, share-of-voice, minimum-commitment, notice-period, contract-term, frequency-cap, and other policy dimensions while preserving seller control over sensitive policy disclosure. - 6947106: Require verifiers to treat unsigned MCP transport sessions as non-authoritative: identity, authorization, account and resource selection, and task ownership must derive from each request rather than `Mcp-Session-Id`. - 03d00cf: Publish the complete request-signing transport error vocabulary with normative, machine-readable recovery classifications and remediation hints. Keep these lowercase `WWW-Authenticate` codes separate from task error codes, and bind the security profile to the new schema so SDKs no longer infer recovery from names or independently transcribe prose tables. - 6947106: Require idempotency key ledgers to outlive created resources for the declared replay window, and add the terminal `COMMITTED_RESOURCE_PURGED` outcome for a write that commits before its resource is independently deleted. Ambiguous handler or downstream timeouts now retain a fail-closed reconciliation claim instead of freeing the key for duplicate execution, with sandbox purge/replay coverage in the compliance controller. - 2639ace: Add optional AdCP 3.x migration fields that make `get_creative_features` retry-safe and reconcilable without breaking existing implementations. Clients SHOULD send `idempotency_key`; providers that advertise replay support MUST honor supplied keys for at least 24 hours. Providers SHOULD emit a stable `evaluation_id`, which remains stable across replays and async completion when present. Both fields are planned to become required in AdCP 4.0. Add deterministic conformance coverage for the recommended keyed and identified profile, including synchronous, asynchronous, conflicting, and concurrent retries plus terminal pricing and consumption reconciliation. ### Patch Changes - a3657f9: Fix get-reporting-status-response schema compilation under AJV strictTypes and strictRequired. Adds `properties` stubs alongside bare `required` arrays in `not`/`then` subschemas (17 locations: Summary view not.anyOf×10, Periods view then.not and not, Revision view not.anyOf×5) and adds `"type": "object"` to the `pagination` property in the Periods and Revision view discriminator arms. Runtime validation behavior with strictTypes/strictRequired disabled is unchanged. - 0f83623: Fix webhook-emission compliance setup to discover and bind a real product and pricing option before create_media_buy, waiting for asynchronous discovery to complete when needed. Replace unresolved test-kit schema references with explicit request and response schema paths and complete the trigger samples so conformance validation cannot silently skip these requests. ## 3.2.0-rc.4 ### Minor Changes - 121d8b3: Add owner-selectable Legacy and Strict Spec grading profiles for verified agents, with Sandbox preview support. Persist immutable exact-role/version assessments and audited selections, project public badges through durable retries, and expose the selected profile through registry APIs, feeds, dashboard controls, badge metadata, and verification tokens. - dc69e0b: Publish canonical supply-path verification golden vectors and an explicit evaluator semantics version. Clarify fail-closed owner-sold carriage verification for affirmative collection constraints, dangling property references, unknown or unevaluated authorization constraints, revocation precedence, agent URL identity and independent domain-level paths. Retain authority-scoped revocations and authoritative-location pins in persistent registry state. Refuse stale or non-authoritative cache provenance and require explicit publisher attribution in cross-origin shared catalogs. Expose cache observation times and resolved URLs. Add publisher_domain attribution to shared collection declarations and require it for cross-origin owner evidence. Preserve successful-fetch provenance through failed crawls and provide an operator command for independently confirmed authority migrations. - 3cea56c: Add protocol-specific `anonymous_discovery` declarations for media-buy product discovery and signal discovery. The optional booleans let callers plan anonymous catalog reads without making catalog-completeness claims or weakening authentication requirements for mutations and private state. - cfc33f6: Require recovery on AdCP 3.2 producer errors while preserving legacy 3.1 decoding and adding compatibility vectors for retry classification and scheduling. - 33edf91: Add an optional `account` field to `sync_accounts` response rows, echoed for settings-update-mode entries. Previously the response schema required `brand` + `operator` on every row, which made settings-update mode unimplementable for account-id-namespace sellers with no buyer-declared natural key, and made `action: "failed"` rows for such accounts unrepresentable entirely. The new discriminator mirrors the pattern `sync_governance` already uses. Closes #7517. ### Patch Changes - 70a91fe: Add compliance scenario for frequency-cap negotiation through proposal refinement (refine_frequency_cap_negotiation). Exercises replace-cap, clear-cap, and unable outcomes against the fields introduced in #7449. - 336546b: Align the 3.2 docs, announcement banner, and SDK guidance with the published 3.2.0-rc.3 checkpoint and @adcp/sdk@14.0.0-rc.36. - eb3cbd6: `canonical_format_validate_input` no longer lists `comply_test_controller` in `required_tools`. The storyboard runner's per-storyboard gate admits a storyboard when any listed tool is present, so any agent exposing the (universal) test controller was selected and then failed all 17 steps on the missing `validate_input`. Agents without `validate_input` now receive a coverage-gap skip; agents implementing it run unchanged. Same shape as #6774 (fixes #7404, bug 1). - 8b9868a: Allow complete reporting summaries to carry the nearest future period start from active committed configuration schedules while preserving both scope guards, obligation boundaries, and evaluated coverage. Clarify the seller's population rule and add portable compliance vectors, a lifecycle storyboard, and source, generated, and runtime validation regressions. - 17f0921: Provenance storyboards now mark every creative-library and test-controller step with its actual tool prerequisite, so hosted grading does not fail sellers for optional surfaces they do not advertise. Fixes #7586. - 516c9d2: Skip billing capability discovery when an agent does not advertise the account capability block, preventing unrelated agents from receiving failing billing grades. - af1ea26: Gate the webhook-emission storyboard's wholesale `get_products` branch set and its aggregate assertion on the advertised wholesale buying mode, so non-media-buy agents grade the branch family as not applicable. - 7509a53: Keep the universal media-buy lifecycle and non-governance seller specialisms ungoverned, and isolate their account natural keys so governance bindings from other compliance scenarios cannot leak into their runs. Refs #7585. - 4d0b482: Test kits declare the account their storyboards address. `acme-outdoor` and `nova-motors` now carry an `account:` block naming the operator (and both sandbox spellings) that account-bearing steps send, so a seller implementing a kit can seed the accounts those steps reference instead of inferring them from the storyboards' sample requests. Closes the seeding half of #7588, where one unseeded account produced 19 check failures attributed to the tools the steps name rather than to the account they could not resolve. - 8a4aac7: Preserve canonical document identities across bundled schemas and embedded MCP profile resources so relocated schema packages resolve nested references fully offline. - 1fdb7fc: Give 35 request-signing conformance vectors bodies that are schema-valid for the operation their URL names, so a seller that validates the request payload before authenticating the caller still reaches the RFC 9421 verifier checklist. Only `request.body` changes; headers, URLs, `verifier_capability` and `expected_outcome` are untouched, and each vector's intended fault is preserved (#7567). - 70e8363: Controller-seed canonical test-kit account prerequisites before account-scoped storyboard steps run, using deterministic per-storyboard operator units to keep account state isolated. This prevents missing or leaked setup from being misgraded as failures of the tools those steps exercise. The reference training agent now accepts those account fixtures on every applicable tenant and lets framework task settlement emit terminal webhooks before controller completion returns, with a bounded wait that releases the tenant lock if settlement stalls. Refs #7588. - 98fa207: Add the `media_buy_frequency_cap_updates` compliance scenario covering root-cap replace and clear through `update_media_buy_frequency_cap`, atomic rejection of `new_packages` whose product cannot join the shared counter, the resulting-state rule for clearing a cap while adding packages, and the `ACTION_NOT_ALLOWED` path when the package mix can no longer change the root cap. The reference training seller now implements package and shared MediaBuy frequency caps so the four frequency-cap scenarios execute against it, and the specification states the `ACTION_NOT_ALLOWED` rule for root-cap changes explicitly. - 1467e46: Training agent implements the rc.3 Reliable Reporting consumer-status hardening and the conformance storyboard grades it. The public training agent now advertises `consumer_status_task`, serves `sync_reporting_status`, and projects the full RC.3 contract: `content_mismatch` with its closed `mismatch_code`, `obligation_counts.consumer_status_pending`, issue `opened_at` / `issue_state` / `external_ref`, and the `operations_contact` / `consumer_mismatch_escalation_seconds` capability fields driving `recommended_action` escalation — with the escalation boundary taking precedence over the stale-`received` grace deadline. `comply_test_controller`'s `reporting_core_lifecycle_probe` gains two additive operations, `advance_past_status_deadline` and `advance_past_escalation`, and the capability-gated `reporting_consumer_status` storyboard uses them to grade counted silence, each `mismatch_code`, `opened_at` stability across the severity change, escalation to a `contact_*` action, and `operations_contact` presence. ## 3.1.23 ### Patch Changes - 0bcfe47: `canonical_format_validate_input` no longer lists `comply_test_controller` in `required_tools`. The storyboard runner's per-storyboard gate admits a storyboard when any listed tool is present, so any agent exposing the (universal) test controller was selected and then failed all 17 steps on the missing `validate_input`. Agents without `validate_input` now receive a coverage-gap skip; agents implementing it run unchanged. Same shape as #6774 (Refs #7404, bug 1 only). - 8a933df: Correct the v3.1.22 release record to include the request-aware Context Match cache partitioning from #7397 alongside the single-user privacy protections from #7396, document the emergency privacy/security notice exception and immutable artifacts, and direct operators with unsafe caches to bypass caching until both request and trusted provider-evaluation contexts are isolated, while keeping the additional `cache_namespace` conformance contract in 3.2. - 0bcfe47: Fix the four compliance storyboards present on the 3.1 maintenance line that incorrectly included `get_adcp_capabilities` in `required_tools` alongside capability-specific tools: `billing_gate_dispatch`, `billing_out_of_band`, `canonical_supported_formats`, and `evaluator_auth`. Removing the universal capability-discovery tool prevents agents without the storyboard's capability-specific tools from entering through the per-storyboard OR gate. This backports the applicable subset of #6774; the other 13 storyboards changed on main do not exist on this line. ## 3.1.22 ### Patch Changes - 5739f1b: Align legacy format asset declarations with the canonical asset union and guard both individual and repeatable-group variants against future drift. - 8244f16: Restrict Context Match embeddings derived from non-public single-user content and require privacy reduction for free-form context signals. ## 3.2.0-rc.3 ### Minor Changes - f1848ee: Add a shared MediaBuy-level `frequency_cap` (one counter across every package) with `media_buy.aggregate_frequency_capping` seller capability, `Product.media_buy_support` participation, discovery via `required_media_buy_support` and `media_buy_frequency_cap`, the `update_media_buy_frequency_cap` action, and proposal-refinement removal. Scope is determined by field location; package caps are unchanged. `ACTION_NOT_ALLOWED.attempted_action` now references the structured action-id schema so it can name `update_media_buy_frequency_cap`; 3.1 SDKs that validate that field against the flat `media-buy-valid-action` enum should update. - d44756a: Add structured product frequency-cap constraints (`overlay_support.frequency_cap_support`) and constraint-aware discovery without changing the legacy `frequency_cap: true` boolean, plus package-qualified `update_frequency_caps` availability. - db1ee5f: Add request-only targeting and product-purchase input schemas for established and compact create/update surfaces. Each targeting dimension now distinguishes omission (inherit or preserve), a non-null replacement, and `null` (clear), while discovery, capability, accepted-commercial-term, and readback schemas remain strict and non-null. ### Patch Changes - 8644cdf: Update the TypeScript SDK dependency and current 3.2 RC guidance, certification prompts, conformance metadata, and provenance assertions to `@adcp/sdk@14.0.0-rc.35`. - acab756: Harden the experimental Reliable Reporting consumer-status loop. Reserve `authoritative_party` on `reporting-delivery-config.json` (sellers MUST reject `consumer` with `UNSUPPORTED_FEATURE` until a later minor defines the buyer-deposited revision task) and relax the seller-authoritative billing-feed constraint accordingly. Give a `received` status made stale only by a seller restatement a bounded `delayed` grace window before it escalates. Make the buyer's posting deadline `expected_at + automated_recovery_window_seconds` and surface unmet deadlines through `obligation_counts.consumer_status_pending`, a count that never changes seller health. Add issue lifecycle fields (`opened_at`, `issue_state`, `external_ref`) with optional `consumer_mismatch_escalation_seconds` and `operations_contact` capability advertisement. Add the `content_mismatch` consumer status with a closed `mismatch_code` for contract-fact disagreements, which are explicitly not measurement disputes. Extend the `reporting_core_lifecycle_probe` controller with `restate_after_received` so the grace projection is graded live, and make the reference seller reject the reserved `authoritative_party` value so the normative MUST has working code behind it. - decd95f: Retire superseded 3.x beta prerelease artifacts from `dist/schemas`, `dist/compliance`, and `dist/protocol`, keeping only the `3.2.0-beta.11` schema bundle (frozen `3.2-beta` documentation selector), `3.2.0-beta.6` (the training agent's retained checkpoint), and `3.1.0-beta.7` (the TypeScript SDK side bundle). This removes about 2 GB from the repository and the runtime image, whose size gate the rc.3 release branch had started to exceed. Previously published beta URLs continue to be served from the artifact CDN on a best-effort basis; documentation now links to the `3.2.0-rc.2` bundle. The immutable-release-artifact guard now permits whole-tree deletion of tagged beta checkpoints while still rejecting in-place edits and any deletion of release-candidate or stable artifacts. - 8d50c0e: `scripts/run-storyboards-matrix.sh` snapshots the freshly built `dist/schemas/latest` into a temporary directory and runs every tenant against that copy, so a concurrent `npm run build:schemas` in the same tree can no longer abort a tenant with the SDK's schema-root version mismatch. The pre-push hook treats the local matrix as advisory: it is skipped when the working tree has uncommitted changes or when `ADCP_SKIP_STORYBOARD_MATRIX=1`, with the `training-agent-storyboards.yml` workflow as the authoritative gate. - 322e39c: Update the pinned Changesets release action to v2.1.2 and refresh its vendored contract fixture and release-workflow verification. ## 3.2.0-rc.2 ### Minor Changes - 609a3b0: Add buyer and orchestrator agent storyboards: 6 specialisms (buyer-discovery, buyer-activation, buyer-negotiation, buyer-monitoring, buyer-recovery, orchestrator-multi-agent), fixture publisher contract and reference implementation, buyer-orchestrator compliance track with three certification levels, and 10 buyer-specific check kinds. - ea5c643: Restore the normative constraints `canonical-forecast-point` dropped from its source twin: the `maximum: 1` bounds on `viewable_rate` and `metrics.coverage_rate` ranges, and the `anyOf` requiring `standard` whenever any viewability value is present. A shared forecast-rate range keeps generated SDK types unambiguous, while a parity contract test compares the twins' resolved viewability schemas and exceptional metric constraints so canonical-pair drift fails CI instead of shipping silently. - b8ab4fe: Add cursor pagination for truncated `get_media_buy_delivery` breakdowns on the bounded-enum dimensions: `device_type`, `device_platform`, `audience`, and `placement`. Previously `by__truncated: true` was a retrieval dead end — there was no protocol-defined way to fetch the dropped rows. Requests can now set `reporting_dimensions..cursor` (reusing the response's new `by__pagination` field, itself the existing `pagination-response.json` shape already used by `get_products`) to page through the rest of a truncated breakdown. `geo` is deliberately excluded — at `postal_area` granularity it can reach tens of thousands of rows, closer to a bulk-export shape than per-package cursor pagination, and is deferred to the bulk-export/security work tracked in #5669/#5666. Closes #5671. - 1f898d3: Add sub-country product coverage filters and define request-preserving behavior for retained get_products facades. Correct migration guidance that converted coverage and signal-option eligibility into delivery targeting, and add regression coverage for native and legacy discovery. ### Patch Changes - afe8b67: Update the TypeScript SDK checkpoint and current RC.1 guidance to `@adcp/sdk@14.0.0-rc.33`. - 62d6a64: Clarify that A2A conversation continuity uses the transport-native contextId, while schema-declared MCP request-body context_id fields are compatibility-only and ignored. - 4eed58a: Align the AdCP 3.2 release story, SDK compatibility guidance, and Reliable Reporting reference docs with the published RC.1 checkpoint. - 388e78e: Add the opt-in experimental Reliable Reporting `sync_reporting_status` loop: buyers report whether each expected period was received, omitted from the seller ledger, missing its revision, or unreadable. Preserve immutable buyer-attributed status history beside seller obligations in `get_reporting_status`, detect when a previously received revision becomes stale after a seller restatement, surface caller-scoped mismatches as typed issues, and publish notice that the task becomes required Core only in the next eligible minor after October 24, 2026. - 58eddf8: Fix `VERSION_UNSUPPORTED` recovery value in `error-compliance.yaml` storyboard prose. Two occurrences of `fatal` (which is not in the `recovery` enum) have been corrected: - `unsupported_major_version` step `expected:` block: `recovery: fatal` → `recovery: correctable`, matching `enumMetadata.VERSION_UNSUPPORTED.recovery` across all 3.x bundles. - General error-shape narrative: `correctable, transient, or fatal` → `transient, correctable, or terminal`, matching the enum declaration order in `core/error.json`. The storyboard validations do not assert `recovery`, so no existing conformance test is affected. This corrects misleading prose that could cause hand-implementers to emit schema-invalid error envelopes. - fc52d81: Align legacy format asset declarations with the canonical asset union and guard both individual and repeatable-group variants against future drift. - e36e319: Update the 3.2 SDK guidance after the Go RC.1 regeneration merged and its package release entered review. - 830e306: Serve AdCP 3.2 RC.1 from the public training surface, including Reliable Reporting tools and revision digests, and align the release notes, migration guidance, training, and TypeScript, Python, and Go SDK instructions with the published RC.1 packages. - b5580e1: Prohibit Context Match embeddings derived from non-public content attributable to a single user or session, require privacy reduction for free-form keywords and summaries, and align Trusted Match guidance and examples with the publisher privacy boundary. - 0005361: Revert the unreleased dotted-domain restriction from the 3.2 release line. The breaking BrandRef and BrandKey constraint remains deferred to 4.0. - 245108f: Scope guaranteed idempotency replay to state-mutating requests while requiring read wrappers to tolerate optional keys. ## 3.2.0-rc.1 ### Minor Changes - 4cc68e1: Deprecate cross-buy delivery aggregates and response-wide currency, add media-buy-level currency with a package-grain fallback for mixed-currency external buys, and preserve qualified standard delivery values on each package. - 6ce1133: Disambiguate cross-buy reach as deduplicated or summed constituent reach, forbid aggregate frequency for summed reach, and preserve legacy payload validity when the new discriminator is absent. - dd6ab0b: Make Reliable Reporting a proper-name AdCP 3.2 capability with explicit version discovery and Core, Managed Delivery, and Reconciled Billing conformance stories. Strengthen it with upstream source-timezone schedules, immutable billing-purpose reporting evidence and control-total adjustments, a health-independent `reporting.ledger_changed` invalidation, checkpointed incremental ledger repair, and evidence-scoped offering reliability statistics. - 6cf4e18: Make first-party measurement legible on the vendor-metric surface (#7150). Add the closed `vendor_relationship` enum (`first_party` / `affiliated` / `third_party`, defined on structural ownership/control facts, a relationship disposition rather than a trust ranking) to `reporting_capabilities.vendor_metrics[]` so a seller declares its relationship to the measurement vendor — required whenever the vendor is the seller itself or under common ownership with it, with absence meaning undeclared rather than third-party — and MAY echo it on `vendor_metric_values[]` rows; methodology stays at the vendor catalog's `methodology_url` / `methodology_version` rather than as free text on the row. Add `measurable_plays` and `measurable_play_seconds` coverage denominators to `vendor-metric-value` and `forecast-vendor-metric-value` for play-based channels (DOOH, cinema, place-based) where `impressions` is itself modelled. Add `methodology_version` to vendor-scope `committed_metrics` entries (request and response) so the version pin the measurement catalog already promises exists on the contract. Document that a seller MAY be its own measurement vendor when it publishes the metric catalog like any other vendor and declares `first_party`. ### Patch Changes - 73a2957: Align the public training agent, reporting compliance examples, Addie certification guidance, and version documentation with the AdCP 3.2 RC.0 wire bundle, `@adcp/sdk@14.0.0-beta.31`, and Python `adcp==8.0.0b13`. - 7a470ca: Allow partial reporting coverage to be represented when a media buy has mixed support across reporting slices, and clarify that an explicitly empty denominator is fully covered. - 4eaee12: Gate the deterministic SI session follow-up steps on `si_initiate_session` so agents outside the sponsored intelligence domain cascade-skip the phase instead of receiving a false `si_send_message` failure. Reported in adcp-client#2827. - 7483a04: Add separate provider-native immutable version references to reporting file entries, and standardize reporting object and version reference bounds for S3-compatible managed delivery. - 4461fdb: Share the package delivery metric value schema across pull and webhook reporting so generated SDK types remain unambiguous. - 832ebb0: Fix the build-time vendor-metric uniqueness lint so `vendor_metric_values` rows key on `(vendor.domain, vendor.brand_id, metric_id, qualifier)` — the rule `delivery-metrics.json` already states — instead of collapsing qualifier-distinct rows (7-day vs 30-day attribution windows) into false duplicates. Qualifier canonicalization is key-sorted deep equality, matching the structured-qualifier join rule on `committed-metric.json`. `reporting_capabilities.vendor_metrics` declarations keep the 3-tuple. No wire change. ## 3.2.0-rc.0 ### Minor Changes - efa8dbc: Add explicit inventory-local and IANA timezone semantics to daypart targeting, with product-scoped timezone-mode capability declarations. - 17162cf: Add experimental product-scoped identity-absence and reach-methodology declarations, require a reach unit on frequency-only delivery for identity-absent products, and add sales-DOOH conformance for frequency-cap rejection and identity-safe delivery reporting. - e037258: Define DOOH share of voice as duration-weighted time share, align contracted and delivered units, and add seller-packaged slot contiguity parameters. ### Patch Changes - f9f0a6b: Align the public training agent, Addie certification guidance, and version documentation with the AdCP 3.2 beta.11 wire bundle and `@adcp/sdk@14.0.0-beta.29`. - deedffb: Define period-boundary reporting obligation availability and expand two-sided implementer guidance and training. - 91cbf0e: Grade current-source storyboard jobs for pull requests targeting main and pushes to main with the explicit candidate-version resolver, while keeping immutable 3.0 compatibility runs strict. - c12dfd3: Document Python SDK 8.0.0b12 as the exact schema and type pairing for AdCP 3.2.0-beta.11. - cd63082: Advance stable schema discovery aliases to the published 3.1.20 bundle and refresh the 3.2 beta documentation for the beta.10 TypeScript and Python SDK wave. ## 3.2.0-beta.11 ### Patch Changes - f1b32c0: Align the training agent and compliance guidance with the published AdCP `3.2.0-beta.10` bundle in `@adcp/sdk@14.0.0-beta.27`. Keep legacy creative synchronization and listing in the same trusted account partition as current media-buy creation, restore the sales compliance account seeder, and keep the DOOH baseline ungoverned while its separate governance scenario exercises registration. - dd8edda: Include `get_principal` alongside `sync_principal` in the MCP media-buy role profile. ## 3.2.0-beta.10 ### Minor Changes - 51ffa95: Add the `audio_vast` canonical format for VAST-tagged audio delivery while retaining `audio_daast` for existing DAAST integrations. VAST 4.1+ is the standards-conformant audio profile; sellers may explicitly advertise older versions for legacy audio interoperability. - e206c35: Add the `sales-dooh` specialism and a digital out-of-home, non-guaranteed compliance storyboard using the existing channel, product, placement, canonical-format, play, and DOOH-metric contracts. Exercise typed DOOH placement identifiers, loop/slot timing, screen resolution, and motion facts while keeping canonical `format_options` authoritative for creative acceptance. Extend deterministic delivery simulation to prove `plays` and `dooh_metrics`, document optional vendor-defined attention without making it part of the core DOOH claim, require every 3.2 product-list path to expose resolved canonical formats without a separate source-mode capability, set a 128 KiB MCP interoperability target with `tools/list` pagination guidance, prevent success-payload duplication across MCP text and `structuredContent`, and clarify that sandbox behavior is selected by the resolved account rather than switched on per media-buy request. - b7b6ba9: Add the experimental managed reporting delivery surface: caller-owned account configuration with protocol-managed destination provisioning, `get_reporting_status` summary/period/revision views, independently scheduled feed purposes, consistent ledger reconciliation, durable delivery capabilities, immutable obligation/revision/materialization records, a normative file manifest, authenticated consumer reconciliation through `sync_reporting_receipts`, and the `reporting.delivery_ready` notification. Advertise snapshot/official schedules as atomic offerings, bind their applicability to products, and preserve explicit full/partial/none/unknown package coverage through configuration, revisions, and status aggregation. Publish portable, byte-exact reconciliation scenarios for SDKs, machine-identify required canonicalization vectors, and enforce control-value and physical-checksum discriminants in the source schemas. Stage the surface into three conformance tiers over one data model: `reporting.core` (obligations, revisions, five health states, `get_reporting_status` over existing transports — the only required tier, implementable by a polling-only seller with no destination, materialization, manifest, canonicalization, or receipt code), `managed_delivery` (file/dataset-share/warehouse offerings with materializations plus retention and revocation bounds), and `reconciled_billing` (`sync_reporting_receipts` plus the canonical-digest contract). Push is optional in every tier: `reporting.status_changed` is available to Core, while the materialization-specific `reporting.delivery_ready` doorbell is managed-delivery-only. Webhook signing is required only when a reporting notification is declared. Complete the tier boundary one layer down: obligation destination and materialization fields, receipt counts, configuration destination readiness, and the healthy/complete conditions are now conditional on their tiers — Core healthy/complete is defined around an authoritative readable revision over existing API transports. `reporting.delivery_ready` is managed-delivery-only, and a new tier-independent `reporting.status_changed` invalidation announces health transitions in either direction (including clock-driven waiting-to-delayed and delayed-to-action_required) with stable `issue_id`s for durable work-item projection. Ships with the reporting.core implementation guide. - 877f6a2: Add the experimental principal layer: the `sync_principal` task for a party's (buyer agent or operator identity) standing configuration with a seller and the side-effect-free `get_principal` read task. Includes reusable reporting destinations with per-pattern normative proof (file write-probe, warehouse commit verification, dataset-share recipient acceptance), destination suspension and revocation semantics with an advertised halt interval, retained superseded and revoked destination generations in readback, two-sided negotiation through `reporting_destination_offerings` capability advertising, capability-change webhook compatibility, optimistic concurrency with cross-task version coherence, principal-scoped idempotency, principal isolation, and credential-free destination setup state. Interactive clients such as Claude do not have to register as buyer agents, but must propagate a stable delegated user identity. - 253f984: Add optional creative identity to spot-level as-run delivery records so buyers can reconcile each airing with multi-creative package rotations. - 9753caa: Extend the experimental principal layer per RFC #7015: caller-level webhook subscribers may carry account-anchored event types with fire-time authorization scoping and an explicit `include_future_event_types` opt-in; a new `declarations` section carries buyer-declared consumption facts (async payload versions, verifiable webhook signing algorithms, experimental opt-ins) with a seller-computed accepted intersection governing asynchronous interactions; and a new caller-anchored `principal.changed` invalidation webhook fires on seller-driven connection-state transitions with repair through `get_principal`. Account-anchored subscription requires the explicit `all_authorized_accounts` acknowledgment with per-delivery-attempt authorization (revocation suppresses queued retries); `include_future_event_types` extends only to invalidation-only classified types; declarations readback persists the accepted intersection, the seller's `selected_async_adcp_version`, and per-value exclusion reasons. Capabilities advertise `caller_event_types` so buyers select from the offering instead of probing by rejection, and every readback carries the seller-resolved `principal_kind` with an `expected_principal_kind` assertion fence. - e3cb7e7: Add buyer-safe structured error reasons using the comprehensive error-code vocabulary, including creative validation codes, and expose public and members-only registration counts on registry and directory lookups without revealing private registrations. - c0366ba: Define the verified canonical `brand.json` Agent URL as the stable identity input for RFC 9421 signed callers, preserving principal continuity across key rotation while keeping distinct Agent URLs isolated. - 267860b: Clarify bounded discovery redirects: adagents.json follows its existing same-registrable-domain policy, while identity-sensitive brand.json follows only the original hostname and its exact www counterpart. Explicit authoritative URLs use zero HTTP redirects. Define JSON-number equivalence, safe-integer rejection, and exact-decimal string guidance for AdCP payloads carried through A2A protobuf DataParts, with portable vectors for both changes. - 93d39c0: Add `targeting_overlay.collection_selection`, mirroring `placement_selection`: buyers name a complete committed collection set as domain-qualified selectors (partial selection gated by `collection_targeting_allowed`), and sellers MUST echo the committed selection on package readback — materializing concrete selectors even when the selection was produced through `collection_list` references — making the package readback collection-echo obligation satisfiable. Resolved collection-list rows can now carry the domain-qualified identity (`publisher_domain` + `collection_id`) so rows remain matchable without a platform-independent distribution identifier. - 5a0aff8: Add DOOH structured selling-unit fields to placements: `dooh_placement_attributes` (slot_duration_seconds, loop_duration_seconds, screen_resolution, motion) and `identifiers[]` on both placement.json and placement-definition.json. Define deterministic publisher/product inheritance, post-merge slot-to-loop validation, versioned OpenOOH identifiers, and canonical-format authority. Add the `dooh-motion-type` enum and supersede pricing-layer loop_duration_seconds in flat-rate-option.json. - d3a6daf: Require universal-macro translation diagnostics to flag native ad-server token syntax embedded inside concrete `value` mappings, while preserving ordinary bracketed text as non-suspect. - bdfadfc: Model programmed audio and video streams, including FAST and virtual linear services, as first-class `channel` collections. Collection distributions can identify exact host properties and use a publisher-scoped `platform_channel_id`, while host `adagents.json` declarations provide collection-scoped authorization for owner-sold avails without requiring host-defined placement catalogs. Collection selectors gain a bulk-grant form (`collection_ids` omitted = all collections at that publisher domain) so hosts do not sync owner-assigned IDs; `platform_channel_id` is forbidden in bare `{type, value}` contexts because its identity is `(publisher_domain, value)`. Registry projection now materializes the collection narrowing onto authorization rows, snapshots, and change-feed events (fail-closed when a declared constraint is unparseable), collection-constrained validate queries without collection scope fail closed, and a collection selector naming an external publisher no longer satisfies the ads.txt `managerdomain` explicit-scope gate. ### Patch Changes - d312e3c: Add conformance storyboards for owner-sold channel carriage and committed collection selection, and pin the normative anchors they grade against: a schema-valid selector referencing an unknown collection_id or publisher_domain is rejected with REFERENCE_NOT_FOUND per the error-code registry (INVALID_REQUEST remains the code for schema-invalid shapes), and a mode-default purchase reads back as mode selected naming the product's complete bundle because the default branch carries no selectors and cannot satisfy the readback echo. create_media_buy's fixed-restatement exception now names the collection_selection full-bundle case alongside placements. - 6217bae: Clarify that the `dooh` property type covers venue-operated digital endpoints, including audio-only players, while the `dooh` channel remains buyer-facing allocation vocabulary. Document multi-channel property declaration plus the canonical audio, publisher authorization, placement, pricing, and proof-of-play pattern without adding a delivery-environment value to the audio feed taxonomy. - 5cac963: Add end-to-end conformance coverage for external audience dataset binding, isolate sourced audiences by seller account, reject credential-bearing source references, preserve frozen-version response shapes, complete the reference seller's async product-discovery task lifecycle, add executable capability gates to optional creative scenarios, and clarify how storyboard applicability is resolved before dispatch. - cebb3c6: Decouple community format revisions from immutable reference-renderer package pins while retaining the legacy renderer revision annotation for compatibility. - 0629a1a: Document the experimental authenticated-principal connection layer across the AdCP 3.2 release guide, release notes, introduction, roadmap, and cross-surface release instrumentation. - fb1d7af: Fix invalid channel value "video" in compliance fixture products. The Channel enum has never included "video"; the correct value is "olv" (online video). Affects 19 occurrences across 17 compliance source files. - 03ebb90: Allow release-only storyboard validation to resolve a generated candidate bundle against the training agent's current stable-line capability, while preserving the agent's advertised versions and wire version. Synchronize TypeScript SDK AdCP metadata and Python SDK package manifests before validating a schema candidate. - cc51898: Declare the concrete AdCP release represented by development `latest` schema roots so public SDK schema-root validation can bind them without cache overlays. - 66e2064: Prepare the AdCP 3.2 candidate experience: center the adopter narrative on the compact proposal lifecycle, expose a public training-agent demo through Addie, add a durable authenticated registry refresh/status lifecycle for long-running compliance suites, document the hosted verifier's candidate-profile boundaries, preserve stable 3.2 documentation aliases across the beta-to-RC transition, add a guarded `3.2.0-rc.0` promotion path, and let candidate-bundle storyboard discovery use the generated external schema root. - 8b0980d: Let side-effect-free `get_principal` return a distinct `recognized` result for an existing durable principal identity before standing configuration exists, without read-time materialization. - c4c8aff: Run long release verification before minting the short-lived GitHub App token used to update version-package pull requests and publish releases. - 0863e4d: Add isolated regression coverage for vendor metric semantic uniqueness validation. - 2edb737: Build deterministic, retained commit-addressed schema PR bundles and validate them against both official SDK generators before protocol changes merge. ## 3.2.0-beta.9 ### Minor Changes - 2fecc3e: Add the AdCP 3.2 draft account change feed from RFC #6810: source-neutral authoritative reads, `list_account_changes`, `account.change_recorded`, account-specific connected-source coverage, 90-day retention, explicit cursor-expiry recovery, capability-gated conformance, and a shared-account training lab. The RFC remains open for beta integration feedback. - 5f1199f: Add symmetric, product-scoped application receipts for property and collection lists in product discovery and proposal-refinement responses. - fe6c585: Add authority-discriminated placement identity and property, collection, installment, collection-by-property, installment-by-property, and placement-by-property delivery reporting. The new intersections provide affirmative delivery evidence instead of relying on independent marginals, with explicit per-response suppression flags. Released-compatible whole-placement echo shapes remain accepted in placement selection. Also add optional `creative_name` convenience metadata while keeping `creative_id` as stable identity. - a1672f9: Add registry-backed seller acceptance-policy discovery with reusable version-pinned platform profiles, proposal-bound media-buy change terms, structured seller disposition evidence on failed governance outcomes, and seller acceptance criteria for buyer-selected governance agents. Add typed change constraints and status scope, project accepted terms into current `available_actions` through `change_term_id`, and deprecate the released 3.1 `terms_ref` action-link overload for removal in 4.0 while retaining explicit 3.1 compatibility. Add compliance storyboards for product promises, proposal materialization, accepted-term persistence, service-mode routing, state transitions, latent rights, and cross-version action parsing. Harden all four surfaces against stale or ambiguous policy, authorization confusion, untrusted audit evidence, unsafe remote resolution, and rejected-binding persistence. ### Patch Changes - 3981795: Make proposal-finalization cardinality conditional on observed proposal identities, restore audience-impairment coherence grading through the compliance controller, and re-enable async signals task-scope grading across account, owner, and tenant boundaries. - a6d1e49: Expand and harden the 3.2 reference sales agent and conformance corpus with targeting-aware discovery, advanced delivery reporting, audience-activation discovery, governed seller delegation, and strict current-source sales grading; restore and regression-gate the packaged wholesale-product scope check; and document the remaining runtime and coverage boundaries precisely. ## 3.2.0-beta.8 ### Patch Changes - b6e0240: Add a compliance storyboard (`media_buy_seller/metric_container_subsumption`) pinning independent implementations to the container-subsumption rule in `enums/available-metric.json`: a container token (e.g. `viewability`) satisfies `required_metrics` filtering and `requested_metrics` selection for its leaf identities (e.g. `viewable_rate`), leaf selection resolves to the canonical carrier object rather than a flat duplicate, and a leaf never implies a sibling leaf. The training agent's reference seller gains the missing `required_metrics` filter on `get_products`, evaluated through the same subsumption-aware availability check its `requested_metrics` narrowing already uses, closing the divergence hazard flagged in #6785. - f85a040: Repair MDX syntax in the generated tool reference and 3.2 creative documentation so versioned release snapshots build successfully, and advance the public beta guidance to the beta.8 checkpoint. ## 3.2.0-beta.7 ### Minor Changes - 675a2f0: Add buyer-authored immutable creative revision identity across sync, review, library readback, and delivery attribution, plus agent-unique served variant identity for unambiguous post-flight preview replay. - 294cb5b: Add `nielsen_audio` to `demographic-system` (same P/M/W notation as Nielsen TV, measured on the radio panel) and broaden the enum's scope to audio channels. RAJAR remains a `measurement_source`, not a notation system. Documents radio delivery reconciliation in the channel guide: demographic notation, provider identity, and `measurement_windows` maturation as the three declarations, with weekly panel cadence as an optimization-eligibility gate. Implements the WG-ratified #6139 decisions. - b2ae44a: Add the static OOH channel contract (experimental in 3.2): an `ooh_metrics` delivery block — panels with multi-scheme identifiers, posting periods, share of voice, illuminated hours, modeled `estimated_impressions` with a declared methodology tier (`estimation_basis`), and posting records whose evidence artifacts use the new channel-neutral `placement-evidence` core schema (shared with print tearsheets) — plus an out-of-home channel guide. Also mirrors `measurement_source` from delivery-forecast into delivery-metrics (WG-ratified) so measured-channel rows are self-describing about whose data produced them. Static units have no play events; delivery is a period-level modeled audience estimate and settlement rests on proof-of-posting, per OAAA conventions. - ad899c2: Add format-scoped production tracker execution contracts for first-class pixel, VAST, and DAAST tracker assets. Pin effective commitments, execution versions, placement scope, and digests in immutable package format snapshots so buyers can distinguish supported, unsupported, and undeclared tracker behavior before spend without coupling the production promise to preview observation. - ba81c91: Add optional, addressable percentile and histogram distributions alongside `viewability.viewed_seconds` for comparable duration reporting. - dc349b0: Add audience activation method declarations (#4324), WG-approved as **experimental** for 3.2 (`media_buy.audience_activation` in `experimental_features`; schemas carry `x-status: experimental`). Products declare how buyer audience data can reach them via `audience_activation.methods` — `sync_audiences`, `tmp_identity_match`, `file_transfer`, `dataset_query`, `clean_room`, or `platform_distribution` — with vendor identity as a BrandRef domain. The seller-level union surfaces as `media_buy.audience_targeting.supported_activation_methods` in `get_adcp_capabilities` for fast-fail discovery, and buyers filter products with `filters.audience_activation_methods` (OR across entries, AND within an entry, omitted fields as wildcards). Dataset entries may publish `consumer_identities[]` with an opaque principal and optional paired cloud/region deployment metadata. Platform destinations are optional account-scoped coordinates. Clean-room declarations cover only collaborations that produce targetable audiences and compose with the declared dataset or distribution rail; analytics-only rooms do not imply audience activation. Grant-based paths are in-protocol only when the vendor flow is grantee-identified. - 172dae7: Add external audience source references on `sync_audiences` (#6540), the runtime leg of the audience-activation surface (experimental, `media_buy.audience_activation`). An audience carries either inline member deltas or a `source` reference (`core/audience-source.json`): `dataset` (the seller reads a grantee-identified share — Snowflake, Databricks Delta Sharing with D2D or OIDC token federation, BigQuery authorized views) or `platform_segment` (binds a vendor-distributed segment to an `audience_id`). Data never transits AdCP. Responses echo the source with `access_status` and `columns_read`; counts anchor to `last_synced_at` (required once counts populate). Normative lifecycle rules: transport fixed at creation (cross-transport upserts → `CONFLICT`), loss of source access never changes audience status (frozen membership stays targetable; `suspended` reserved for consent/policy causes), access expiry is not deletion. New error code `SOURCE_ACCESS_FAILED` with `error.field`-keyed recovery. - 8c0c982: Promote the reporting-cadence optimization-eligibility rule from guidance to normative: buy-side agents MUST treat a product's declared `available_reporting_frequencies` as an optimization-eligibility gate and MUST NOT make mid-flight optimization decisions against metrics whose declared cadence is `quarterly` or `post_campaign`. Enforcement routes through buyer-artifact grading (sellers have nothing to attest); an anti-drift test pins the normative language to the enum and the governing doc. Ratified with the radio and OOH WG packets (#6138/#6139/#6140). - 675a2f0: Add explicit creative delivery contracts for inline display tags, atomic paired redirects, and revision-bound creative representation sets. A complete representation set is one immutable buyer revision; deterministic selection identifies one `representation_id` without creating a build or served variant and carries the complete revision digest into the seller-bound manifest. Define exact VAST asset versions versus product and seller acceptance sets, VAST MediaFile delivery/MIME/container/codec/dimension/bitrate/byte requirements, decimal file-size units, and declaration-level technical completeness. Add declared macro dialect, resolver ownership, encoding depth, capability matching, per-token validation results, structured rejection errors, documentation, and conformance vectors for issues #6761–#6764. ### Patch Changes - dd518c1: Complete the reference test-vector index with every published compliance and unversioned set, accurate development-snapshot guidance, and direct links to the media-buy vector files. - 53bd11c: Correct capability gating and fixture inputs in the current conformance storyboards, and make training-agent matrix coverage capability-resolved with explicit not-applicable and quarantine accounting. - 752adad: Guard empty RUNNER_ARGS array expansion in sharded runner scripts for bash 3.2 compatibility - 64f99dd: Define capability-driven RFC 9421 signing for sandbox functional storyboard dispatch. Runners reuse the existing published compliance key, sign operations advertised in `request_signing.required_for` or `supported_for`, preserve bearer authentication, and never bypass seller verification or retry unsigned after a signer failure. - bd2ef39: Align the AdCP 3.2 beta compliance surface, training agent, and current TypeScript guidance with `@adcp/sdk@14.0.0-beta.8`. The SDK embeds the exact `3.2.0-beta.6` schema and compliance bundle, so active wire pins move from `3.2-beta.5` to `3.2-beta.6`. ## 3.2.0-beta.6 ### Minor Changes - 4439b18: Add AdCP 3.2 CTV experience profiles implementing the IAB CTV Ad Portfolio without new channel-named canonicals. A shared `ctv_ad_experience` vocabulary (menu, pause, screensaver, overlay, squeezeback, in_scene) pairs each experience with the canonical contracts sellers ingest in practice: `native_in_feed` gains a menu profile with a Native 1.2 video slot, `menu_placement` (tile or headline banner), and `focus_behavior`; `video_vast` gains an explicit `creative_type` (linear, nonlinear, either) superseding `linear_required`, with nonlinear required for the five video-backed experiences and per-experience constraints (overlay and squeezeback need 10s minimum duration, in_scene needs 3s and forbids interactivity, pause is unfloored). SIMID remains available to ordinary Linear VAST but is rejected on every nonlinear CTV profile because VAST serializes `InteractiveCreativeFile` only under Linear `MediaFiles`. `image` accepts pause and screensaver (the image-plus-copy contract major pause-ad sellers ingest); `video_hosted` accepts screensaver; `sponsored_placement` accepts catalog-derived menu tiles, squeezeback, and in_scene. Shared `motion_level` (AdCOM attributes 21–23) and `activation_method` (QR, deep link, push, email, tune-in, SMS) vocabularies carry the interaction layer; activations are engagement events, never impressions. Pairings outside the experience matrix fail validation. The docs distinguish buyer-selected experience commitments from player-resolved runtime eligibility and define multi-canonical cells as sibling format options, not an implicit fallback field. The IAB OpenRTB/AdCOM signaling mapping (plcmt 5–9, playbackmethod 8–11, motion attributes, Native plcmttype) is documented as a bridge annex. Includes worked menu/pause/overlay examples and compliance vectors. - 56f9224: Add capability-gated delivery breakdowns by canonical creative `format_kind` to `get_media_buy_delivery`, including explicit GET-only scope, `custom` aggregation, truncation disclosure, independent reconciliation from creative-level rows, and the full sort contract (`sort_direction` plus the `by_format_sorted_by`/`by_format_sort_direction` applied-sort echo with row-grain fallback and nulls-last semantics). - 9a7cf31: Add the experimental AdCP 3.2 `seller_rendered_stateful_display` and `coordinated_placements` canonical creative formats. The stateful display declaration is an executable template contract — slots with limits, safe zones, breakpoints, and a disclosed state/transition graph — with `supply_mode` selecting who renders: `components` (buyer supplies focal-pointed imagery and copy; seller assembles every state × breakpoint deterministically), `rendered_canvases` (buyer authors externally against the published contract), or transitional `layered_source`. Sellers offering the canonical must support deterministic `preview_creative` rendering of every state × breakpoint. Sizing supports fixed, range, aspect-ratio, and fluid (`full_bleed`, `gutter_residual`, viewport-percent) breakpoints; single-state reveal units declare a `reveal` mechanic and `underlay` anchoring instead of fabricated states; transitions add `in_view_timer`, `media_event`, scroll `direction`, and `hover` inputs with per-state click URLs and a `clickthrough` policy. Validation enforces a dismissibility floor for overlay anchoring, an anti-strobe floor on timer cycles, per-canonical slot asset-type whitelists, and non-blocking LEAN policy warnings for IAB-prohibited trigger/anchoring combinations. `coordinated_placements` binds multiple product placements atomically with shared slots, optional `sequence` ordering, and per-component `serving_policy`, with placement-reference normalization and schema validation of referenced sibling options. Includes compliance scenarios, worked product declarations, and adopter documentation. - a35dfdb: Add `time_based_views` to delivery reporting: an array of time-threshold video view counts, each entry keyed by (threshold_seconds, basis). The new `view-threshold-basis` enum distinguishes play-time counting (platform 2s/6s video views) from in-view counting (IAB/MRC viewable video), which are materially different numbers at the same threshold and must not be conflated or summed. Capability-gated via the `time_based_views` token in available-metric. Implements RFC #6430 with the basis discriminator the RFC's open questions pointed toward. - 465cb85: Add `requested_metrics` to `get_media_buy_delivery`, giving the GET path the same metric narrowing the reporting webhook already has. Omitted means unchanged full payloads; impressions and spend are always included; requesting a leaf metric identity returns its canonical nested carrier; and `missing_metrics` MUST NOT flag absences caused solely by request narrowing. Implements RFC #6624. - 6a5ceb9: Add leaf metric identities so nested delivery values are individually declarable, committable, aggregatable, and sortable: `quartile_25`–`quartile_100` (resolving to `quartile_data.q1_views`–`q4_views`) and `viewable_rate`, `viewable_impressions`, `measurable_impressions`, `viewed_seconds` (resolving to the same-named `viewability` fields) join `available-metric` and `sort-metric`. This closes an existing contradiction: `committed-metric` qualifier rules and `delivery-metric-aggregate` conditionals already referenced these metric_ids, and the shipped `committed_metrics` / `metric_aggregates` examples were invalid against their own schemas. Also adds the missing flat transactional scalars (`commissionable_value`, `plays`, `cost_per_completed_view`, `cpm`, `downloads`, `units_sold`, `new_to_brand_units`) to `sort-metric`, with survey/model-based lift scalars documented as intentionally sort-excluded. Leaf identities resolve to the nested canonical values — no duplicate flat response fields are introduced. A metric-identity coherence contract test now enforces enum/schema/example agreement. **Seller conformance note:** the `viewability` description now requires (MUST) that sellers populate `standard` on reported viewability objects when `committed_metrics` carry a `viewability_standard` qualifier — upgraded from a SHOULD. The trigger is any `committed_metrics` entry with `qualifier.viewability_standard` set, including the container `metric_id: "viewability"` (already in the enum). Sellers with an existing `{metric_id: "viewability", qualifier: {viewability_standard: "mrc"}}` commitment who were sometimes omitting `standard` become non-conformant. Sellers who never use the `viewability_standard` qualifier are unaffected. - a793fc8: Fix the vendor-scope qualifier on `delivery-metric-aggregate` (previously a closed object with no properties, so only `{}` could validate) and add the optional 5-key qualifier to the vendor branches of `committed-metric`, `missing-metric`, `package-request` committed_metrics, the performance-feedback surfaces, and — critically — the `vendor-metric-value` delivery carrier, whose row uniqueness re-keys from `(vendor, metric_id)` to `(vendor, metric_id, qualifier)` so a vendor metric committed under two attribution windows is representable in the delivery report. Container tokens (`viewability`, `quartile_data`, `dooh_metrics`) are barred as value-bearing aggregate `metric_id`s — leaf identities exist for that. Matches what `canonical-reporting-commitment` already allows; a qualifier parity contract test now enforces an identical closed key set across every hand-maintained copy. - 6b4525e: Make the automatic delivery breakdowns (creative, keyword, catalog_item) optionally negotiable: including their keys in `reporting_dimensions` adds `limit`/`sort_by`/`sort_direction` control and makes the new `by_X_truncated` and applied-sort echo fields binding, so "top creatives by quartile_100" is answerable with a completeness contract. Omitting the keys preserves today's automatic behavior exactly. Implements RFC #6623. - be68a66: Add `sort_direction` (asc/desc, default desc) to the six sortable delivery breakdown dimensions and a per-breakdown applied-sort echo (`by_X_sorted_by` / `by_X_sort_direction`, MUST whenever the breakdown is present) so the existing silent fallback-to-spend becomes visible to buyers. Ascending sort enables bottom-N optimization queries (worst placements by viewable_rate) that cannot be recovered from a truncated descending pull. ### Patch Changes - 350a22c: Map the standard unsuffixed `display_160x600` legacy format to a canonical 160×600 image declaration. - 3c8fbfd: Correct the OpenRTB/AdCOM `cattax` mappings documented for IAB Content Taxonomy 3.0 and 2.2. - a1e72ec: Fix 17 compliance storyboards that incorrectly included `get_adcp_capabilities` in `required_tools` alongside capability-specific tools. Because `required_tools` uses OR semantics, listing a universal tool made the storyboard-level gate trivially satisfied for every conformant agent — agents lacking the actual capability tool (e.g. `sync_accounts`, `build_creative`, `get_products`) would enter the storyboard and fail at the first capability-specific step instead of receiving a clean coverage-gap skip. Affected storyboards: `billing_gate_dispatch`, `agent_notification_configs`, and 15 scenarios across the `media-buy` and `creative` protocol families. - b407de6: Require a human approval on the final release PR head before publishing committed protocol artifacts, and leave generated documentation snapshots open for human review. - dea443d: Allow request-signing capability declarations to name both A2A 0.3 slash-path methods, including the nested push-notification-config family, and A2A 1.0 PascalCase methods. Matching remains exact and case-sensitive, so dual-stack agents advertise each supported wire name independently. ## 3.2.0-beta.5 ### Minor Changes - 496920a: Define cross-transport async identity and convergence rules for direct responses, AdCP polling, A2A tasks, continuations, and webhooks. The contract separates identifier namespaces, makes terminal settlement and publication single-winner, binds webhook delivery keys immutably to payloads, requires recoverable continuation-generation handoffs, preserves legacy composite atomicity, and advertises webhook retry horizons. - 81c409b: Add conformance surface for outcome_target reverse forecasting. The new `media_buy_seller/outcome_target` scenario (required by the sales-proposal-mode specialism, gated on `media_buy.outcome_target`) grades the answer contract: `total_budget_guidance` on every returned proposal and a forecast whose points carry the goal's metric or event key, with `forecast_range_unit` `clicks`/`conversions` structuring the curves. Fixes the canonical-proposal gap the contract exposed (#6745): `core/canonical-proposal.json` gains optional `total_budget_guidance` and `forecast`, restoring parity with the legacy proposal's planning outputs so the compact `request_proposals` lifecycle can actually express the answer. The training agent implements a deterministic reverse-forecast reference model. - 4097b73: Add creative rejection conformance for the one-policy-per-error invariant. The new controller-gated `creative/policy_backed_rejections` storyboard first discovers an isolated product that declares exactly the automatic-redirect and HTTPS-only registry policies, then submits a canonical hosted display tag that deterministically violates both. Conformance requires exactly two `CREATIVE_REJECTED` entries with one `details.policy_id` each. The runner contract also publishes the reusable `array_length` assertion used to grade exact error cardinality. - 1607036: Scope optional media-buy compliance paths to the capabilities sellers actually advertise. Creative-library storyboards now require an explicit library claim, including compound gates where another applicability predicate already exists. Measurement-term acceptance is split from the universal rejection contract behind a new optional capability, and product refinement requires the `refine` buying mode. Sellers outside these optional surfaces will see the affected scenarios move from runnable badge/completeness denominators to not applicable. ### Patch Changes - 8074975: Align the AdCP 3.2 beta compliance surface and training agent with `@adcp/sdk@14.0.0-beta.5`. The beta.5 validators carry the flexible-window availability vocabulary, so the `availability_windows` `list_with_horizon` step is no longer registered known-failing (closes the adcp-client#2637 exclusion) and the scenario grades all eight steps. Wire pins move from `3.2-beta.3` to `3.2-beta.4` across the training agent, compliance scenarios, and versioned reference docs. ## 3.2.0-beta.4 ### Minor Changes - 18f5f8e: Add conformance surface for flexible-window availability discovery. New `media_buy.availability_horizon` capability declaration gates the new `media_buy_seller/availability_windows` scenario (required by the sales-guaranteed specialism): horizon partitioning into coalesced half-open time windows, eligibility-aware `availability_status` (a gap shorter than the product's minimum bookable duration is `unavailable` even with no competing hold), forecast excluded from `list_products` conditional reads, and `PRODUCT_UNAVAILABLE` on buys against closed or too-short windows. Product fixtures accept an optional seller-internal `availability` calendar (`min_bookable_days`, `booked_windows`) consumed by seeding. Adds schema test vectors for the `availability_horizon` mutual-exclusion and time-dimension shapes. - c6b0513: Allow products to disclose per-package cardinality limits for country inclusion, country exclusion, and proximity targeting. - 8d0a0c8: Add flexible-window availability discovery. `offer_filters.availability_horizon` lets a buyer ask "which dates can I run?" instead of filtering to one exact flight; sellers answer by partitioning the horizon into `time`-dimensioned forecast points (new `forecast-dimension-time` variant) carrying the new `availability_status` field (`available` | `unavailable`). Availability data is a snapshot bounded by the forecast's `valid_until`, never a hold — proposal finalization remains the firm-avails and commitment boundary. Forecast data is excluded from `list_products` feed-version scoping, and a `list_products` request whose `fields` includes `forecast` must not be answered with `outcome: "unchanged"`. - e93e3e4: Add structured reverse-forecast planning input. `criteria.outcome_target` lets a buyer state the outcome they need — a compact goal (a `forecastable-metric` delivery metric or an `event-type` conversion event) plus a desired volume, e.g. "10,000 clicks" — and ask the seller to solve for budget, answering with `total_budget_guidance` on proposals and forecasts whose points carry the goal's key in `metrics`. The goal vocabulary is shared with forecast reporting and package-level optimization goals, so every permitted goal has a defined answer and buyers carry the same metric or event name from plan to buy. Gated by the new `media_buy.outcome_target` capability declaration; sellers that do not declare it reject the field with `UNSUPPORTED_FEATURE` rather than silently ignoring it. - e72ff10: Add the projection-only `products_available` outcome for valid AdCP 2.5, 3.0, and 3.1 products-only brief results. The response now provides either a real seller-fenced `listed_purchase` continuation or an explicitly lossy, fail-closed `legacy_create` continuation without fabricating proposals, terms digests, or feed versions. AdCP 2.5 continuations additionally disclose the absence of a mutation replay guarantee. Document transaction-boundary differences between the established and compact media-buy lifecycles, and route supported MediaBuy name actions through compact control. ### Patch Changes - 71706fb: Correct the OpenRTB source and complete the PAIR profile mapping, including matcher, match method, publisher scope, key rotation, and TMP's lossy scope boundary. - 49c81e0: Align the AdCP 3.2 beta compliance surface and training agent with `@adcp/sdk@14.0.0-beta.4`. ## 3.2.0-beta.3 ### Minor Changes - 9577ad2: Generate MCP tool discovery input schemas with plain object roots for strict hosts, while retaining canonical request schemas as the call-time validation authority. - 68d3b93: Define trailing DNS root-dot stripping and empty-label rejection for request-signing canonicalization. - acc022a: Publish schema-validated language-neutral universal-macro translation vectors for ratified behavior, including control-character rejection, frozen-consent diagnostics, and bare-query normalization. ### Patch Changes - cae33f5: Update the public training agent for the SDK 14 media-buy lifecycle while preserving the frozen AdCP 3.0 compatibility surface. - 170fc1a: Require the compact direct-buy compliance storyboard to chain the listed pricing version and verify the accepted proposal identity and terms digest during MediaBuy readback. - 4e603eb: Gate property-list compliance scenarios on declared execution support and require an actionable no-inventory rejection for empty intersections. ## 3.2.0-beta.2 ### Patch Changes - 5c7b835: Add a deterministic AdCP 3.2 compliance storyboard for the compact direct-purchase lifecycle from versioned product discovery through operational control and authoritative MediaBuy readback. - 5c7b835: Extend the compact proposal lifecycle storyboard through revision-checked MediaBuy control and readback, including lifecycle, revision-history, and accepted-proposal linkage assertions. ## 3.2.0-beta.1 ### Minor Changes - 71f0046: Add per-route creative preview origin discovery, publisher-authorized preview delegation, isolated community reference-renderer declarations, and versioned placement-presentation composition. - 2b312e0: Add a deterministic controller probe and compact media-buy lifecycle storyboards for product-to-proposal acceptance, declined-proposal terminality, and proposal expiry, and restore testable task documentation for the four compact planning operations. - 09764b1: Define deterministic PackageRequest format-selector normalization, reject conflicting canonical and legacy projections, require fixed image dimensions to travel together, and add compliance coverage for the 3.x compatibility paths. ### Patch Changes - d9879ad: Accept the deprecated `adcp_major_version` compatibility field on every AdCP 3.2 compact media-buy lifecycle request. - 23491e3: Make the compact 3.2 media-buy lifecycles primary in navigation, concepts, and walkthroughs, and document the TypeScript SDK beta. Keep the established facades at an explicit compatibility boundary. - 39290c4: Restore Changesets v2 compatibility while retaining git-backed pushes for large generated release commits. ## 3.2.0-beta.0 ### Minor Changes - 28aeec1: Add experimental structured accessibility-violation details to the existing `CREATIVE_REJECTED` error so producers can return machine-readable criteria, pointers, failure kinds, and remediation without introducing a backward-incompatible error code. - 5638ce3: Add basis-aware age targeting without weakening the canonical age predicate. Buyers can constrain `demographics.age.accepted_bases`, products declare user-level `supported_bases`, and package readback records effective applied bases and verification methods. Age compliance continues to override demographic permissions, and World ID threshold claims must entail the requested age predicate. This new capability should be reflected in future media-buy certification coverage. - 84ff78e: Define atomic media-buy total-budget updates, proportional fixed-package redistribution, and conformance coverage. - 48ccce9: Add immutable product audience evidence, buyer-authored admissibility and ranking requirements, seller capability discovery, digest-pinned package readback, portable attestation evaluation, and conformance vectors for AdCP 3.2. - 1c8d320: Add deterministic brand.json locale fallback and whole-array localization for tone and asset metadata. - 02e477f: Add portable browser-family inclusion and exclusion to targeting-aware product discovery and package execution. Products declare `browser` and `browser_exclude` support independently, either without restriction or with explicit supported-family subsets. The canonical taxonomy distinguishes recognized-but-unlisted `other` browsers from unclassifiable `unknown` browsers and intentionally excludes browser versions and seller-native targeting IDs. - b07891e: Add pixel-density and rendition-set support for 3.2 canonical images. Canonical image declarations now separate logical render dimensions from accepted intrinsic `pixel_ratios`; image assets may declare `pixel_ratio`; and image slots may use `required_pixel_ratios` to require coverage such as 1x plus 2x while leaving 1.5x optional. Top-level and slot density sets combine by intersection for both singular assets and rendition arrays. SDK inference, ambiguity, intersection, rendition coverage, and v2-narrows-v1 comparison are pinned by shared positive and negative vectors. The unversioned legacy reference catalog gains distinct 2x-only and paired 1x-plus-2x compatibility IDs that become discoverable across 3.x when deployed; their density and coverage annotations are forward-projection metadata for 3.2-aware SDKs, not pre-3.2 canonical semantics. New integrations use the 3.2 parameterized `display_image` template with `format_id.pixel_ratio`, and the v1-to-v2 registry exposes machine-readable rules that preserve its dimensions and density during SDK projection. - 6d21174: Define deterministic capability-selected runtime tool projections, publish concise manifest summaries for every active Media Buy and Creative role tool, and keep response schemas available for lazy SDK validation outside model context. - 5ef5592: Add capability-gated buyer-pushed catalog item suppression, restoration, and current-state readback to `sync_catalogs`, with immutable catalog generations, optimistic concurrency, bounded batches, optional expiry, deterministic privacy-preserving failures, exact per-item correlation, atomic mixed requests, and enforcement against selection, dynamic rendering, and lineage-known cached creatives. - ee66e24: Add optional `delivery_date` support to `simulate_delivery` so compliance storyboards can seed deterministic delivery rows and verify half-open `get_media_buy_delivery` date filters. The training agent now aggregates dated simulations within `[start_date, end_date)` while preserving cumulative behavior for legacy undated simulations. Date-bounded training-agent responses now follow the task's documented half-open range semantics, including an exclusive midnight `reporting_period.end` and rejection of empty ranges where the dates are equal. - 381ede4: Add opt-in demographic delivery breakdowns to `get_media_buy_delivery`, with product-scoped reportable age ranges, measurement systems, and privacy-suppression disclosure kept distinct from targeting execution capability. - 59bd8b6: Define AdCP 3.2 storyboard fixture resolution. Fixture IDs remain literal seed IDs for compatibility and become run-scoped handles when a storyboard opts into explicit `seed`, `discover`, or future entity-specific `construct` strategies. Add deterministic matching and binding rules, schema-aware ID substitution, resolution evidence, `fixture_unsatisfied` coverage grading, controller ID `x-entity` annotations, authoring documentation, and a source lint. Pilot the discovery contract on the `sales_non_guaranteed` specialism without changing legacy runner behavior. - cac7e69: Add a structured `GetProductsRejected` business-outcome arm for sellers that understand a well-formed brief or refinement but deliberately decline it. Define transport-success and mutual-exclusion semantics, expose deterministic conformance coverage through `force_get_products_arm`, and keep no-match, incomplete, async, and technical-failure outcomes distinct. - 77d17df: Add attributed delivery reconciliation and a two-party, append-only campaign adjustment lifecycle for AdCP 3.2. Disagreement handling splits by evidence source: a forwarded seller-statement copy whose ID or digest mismatches the canonical statement is `disputed` and blocks adjustment acceptance, while buyer measurement variance is a recorded, non-blocking `measurement_variance` — the higher of seller-stated and buyer-observed spend bounds both conservative exposure and the verified-decommitment ceiling, so manufactured variance can only shrink what either side can extract. Sellers report canonical delivery statements and evidence-bound adjustments; buyers submit separate observations, close operational governance periods without asserting final billing truth, and accept or dispute adjustments. Audit logs expose discrepancies, period state, conservative exposure, gross commitment, verified economic reductions, and accounting-mode-specific headroom without weakening sticky trailing-window fragmentation defense. ## Migration This change breaks three experimental surfaces (`x-status: experimental`). All three are changed for the first time in 3.2 beta, making the beta publication itself the required 6-week-notice vehicle per `docs/reference/experimental-status.mdx`. **`report_plan_outcome` request `delivery` object** (`report-plan-outcome-request.json`): The deprecated unbound delivery snapshot is superseded by a required buyer-attributed observation. Before: `deprecated: true`, `additionalProperties: true`, no required fields. After: `additionalProperties: false` with six required fields (`observation_id`, `source`, `observed_at`, `reporting_period`, `cumulative_spend`, `currency`), plus `seller_statement_id`/`seller_statement_digest` required when `source` is `seller_statement_copy`. ```json // Before (any shape accepted) { "delivery": { "media_buy_id": "mb_123", "impressions": 4200000, "spend": 137500 } } // After (minimum required) { "delivery": { "observation_id": "obs_001", "source": "buyer_measurement", "observed_at": "2026-03-22T01:05:00Z", "reporting_period": { "start": "2026-03-15T00:00:00Z", "end": "2026-03-22T00:00:00Z" }, "cumulative_spend": 12500, "currency": "USD" } } ``` **`check_governance` request `delivery_metrics`** (`check-governance-request.json`): Required fields expand from 1 (`reporting_period`) to 7: `statement_id`, `statement_digest`, `sequence`, `issued_at`, `reporting_period`, `cumulative_spend`, `currency`. (`seller_reference` and `canonical_payload` are not `delivery_metrics` request fields — they exist only on the response's `delivery_statement`.) ```json // Before { "delivery_metrics": { "reporting_period": { "start": "2026-03-15T00:00:00Z", "end": "2026-03-22T00:00:00Z" } } } // After { "delivery_metrics": { "statement_id": "stmt_001", "statement_digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "sequence": 1, "issued_at": "2026-03-22T00:05:00Z", "reporting_period": { "start": "2026-03-15T00:00:00Z", "end": "2026-03-22T00:00:00Z" }, "cumulative_spend": 12500, "currency": "USD" } } ``` **`report_plan_outcome` with `outcome: "delivery"`** (`report-plan-outcome-request.json`): `check_id` and `governance_context` are now unconditionally required for this outcome. Previously both could be omitted, which allowed a delivery observation with no bound plan-owner check — the both-or-neither plan-owner path. ```json // Before (accepted without check_id/governance_context) { "plan_id": "plan_1", "idempotency_key": "outcome-delivery-001", "outcome": "delivery", "delivery": { /* ... */ } } // After (both required) { "plan_id": "plan_1", "idempotency_key": "outcome-delivery-001", "check_id": "chk_seller_delivery_001", "governance_context": "gc_mb_seller_456", "outcome": "delivery", "delivery": { /* ... */ } } ``` - fa8c262: Add compact, self-contained JSON Schema 2020-12 input and output artifacts for MCP 2026-07-28 tool discovery. - 6468688: Add the `INVALID_PRICING_OPTION` and `INVALID_USAGE_DATA` standard error codes used by `report_usage`, with canonical descriptions and recovery metadata. Align the shared pricing-option example and clarify that idempotent replays return the original response without emitting a duplicate-request advisory. - eebd81b: Add an optional, persisted `name` to media-buy create, update, success, and read surfaces so buyers and sellers can share a human-readable trafficking label without overloading financial references or opaque context. - 71953f8: Add a materialized creative-localization contract with source-only or source-plus-target variants, a shared cross-protocol BCP 47 language-tag primitive with an explicit AdCP wire profile, contextual text/markdown language conformance, strict RFC 4647 Lookup over externally supplied delivery preferences, explicit language-family fallback rules, default and unmatched-locale behavior, creative-wide review, transactional replacement, exact request-to-sync-to-list identity, per-item fail-closed readback, localized delivery attribution, and seller-enforced product-format locale policies using RFC 4647 Basic Filtering. Document the separate architecture patterns for authoritative agent input declarations, observable response-language selection, whole-value published content maps, and executable creative so later surfaces reuse language semantics without copying creative topology. Translation and generation remain separate creative-production operations. - b2fc579: Add the AdCP 3.2 OAuth capability declaration and capability-gated universal compliance storyboard for RFC 9728/RFC 8414 discovery consistency, backed by a credential-free, SSRF-bounded runner contract and deterministic metadata-graph vectors. - 48ccce9: Add reference-first portable attestation schemas, evaluator capability discovery, normative resolution and verification rules, and conformance vectors for AdCP 3.2. - 3934dda: Add buyer-opt-in asynchronous rendering to preview_creative while retaining synchronous default behavior; build_creative's existing async contract is unchanged. - 6307ff1: Add optional audio loudness constraints and document canonical file-based radio :15, :30, and :60 creative contracts, including industry identifiers and the absence of VAST or tracker semantics. - 487df50: Add issuer-bound rights-grant attestations for AdCP 3.2. Rights constraints now carry digest-pinned portable attestation references, sellers advertise and return verifier-of-record evaluations, and the legacy buyer-controlled `verification_url` is explicitly non-authoritative and never fetched for authorization. - 48ccce9: Add portable runtime signal-quality attestations to check_governance, including action-specific capability policy, normalized evaluation results, signed-context binding, audit readback, and conformance vectors. - 19fd2d5: Add seller-optimized shared budgets and explicit bidding policy placement across media-buy packages. Media buys and proposals can delegate cross-package allocation to the seller under an aggregate budget, optimization goals, pacing, package caps, and soft minimum-spend targets, while preserving fixed package budgets as the default. A new media-buy/package `bidding` block separates objective functions from automatic bidding, manual bids, auction ceilings, average-cost controls, and ROAS controls, with complete-block inheritance and authored-scope-preserving readback. Media-buy outcome controls bind to allocation goals in seller-optimized mode and compatible package goals in fixed mode; package controls bind to package goals. All canonical monetary fields use one media-buy currency, ROAS event sources declare supported value currencies, and structured capabilities advertise support by scope, allocation context, mode, strength, and combination. - 6572ef6: Add consumer conformance coverage for designated-task signed brand responses. The new sandbox runner contract dynamically signs `verify_brand_claim` and `verify_brand_claims` fixtures and grades fresh acceptance plus expiry, request-replay, and tenant-mismatch rejection against the normative verifier checklist. Closes #6147 - 64a26e7: Add opt-in spot-level as-run delivery reporting for scheduled inventory, with stable spot identity, actual airing time, and explicit product capability for metrics available at spot grain. - aee4f79: Add an assessed synthetic-depiction provenance declaration, seller policy requirement, and canonical missing-declaration error for AdCP 3.2. - 1656819: Add optional storyboard coverage for search_brands, get_creative_features, and get_media_buy_artifacts without gating agents that do not advertise the experimental tasks. - 1eb51fe: Add the optional `voice_synthesis.rights_offering_id` brand-side provenance pointer and clarify how it joins a configured voice to `get_rights`, `acquire_rights`, and transformer provenance without creating a build-time authorization gate. - 49825ba: Allow `null` for video-only delivery metrics (`quartile_data`, `completion_rate`). Sellers running non-video inventory (display, audio-only, DOOH-without-video) legitimately have no value for these metrics, and returning `null` is the correct "not applicable" signal. The schema previously required `type: "number"` / `type: "object"` and rejected `null`, causing receivers to fail validation on every valid display-inventory delivery report. `delivery-metrics.json` (`totals` / `by_package[]`) now accepts `["number", "null"]` for `completion_rate` and `["object", "null"]` for `quartile_data`; `get-media-buy-delivery-response.json` `aggregated_totals.completion_rate` gets the same loosening so the aggregate path can't re-trigger the failure. The `minimum`/`maximum` constraints on `completion_rate` still apply to non-null values, and the type stays narrowed to null (no strings/arrays). Every other delivery metric continues to signal "not applicable" by omission, not `null` — this exception is scoped to the two video-only fields. Spec-loosening for the receiver contract: producers already sending numbers/objects remain valid. The separate inline `completion_rate` in `report-plan-outcome-request.json` (a governance self-report block, not on the `get_media_buy_delivery` path) is intentionally left unchanged. - 7213f46: Define the snapshot/log contract as authoritative current-state convergence for state notifications and capability-scoped data parity for delivery notifications. Add optional, migration-safe `notification_id` correlation to webhook activity, register `window_update` consistently, and document exact repair keys and recovery procedures across current push/read pairs. - aa9f21f: Define media-buy portfolio channels and countries as exhaustive routing allowlists when present, treat omission as unknown rather than global coverage, validate assigned country codes, and add advisory checks for missing or empty scope plus required checks for contradictory returned product channels. - fb744bd: Add the 3.2 advisory brand-authorization binding for `verify_brand_claim` and `verify_brand_claims`. A verifier can now report a cryptographically valid but unbound response as `untrusted` through the new `brand-response-authorization-result` schema instead of treating the brand-agent's assertion or rejection as authoritative. The security and task docs define the advisory lookup, canonical agent-URL matching, authorized-JWKS scoping, batch deduplication, and safe failure behavior. Conformance vectors plus an executable reference evaluator cover authorized, unavailable, ambiguous, wrong-agent, wrong-key, and forged rejection cases. Mandatory cryptographic brand authorization and hard rejection remain deferred to 4.0. - f36f565: Make canonical creative formats the AdCP 3.2 authoring and discovery path. Creative agents now advertise stable `creative.supported_formats[].capability_id` entries and explicit `operations` through `get_adcp_capabilities`; `build_creative` and `list_transformers` select those capabilities with string IDs; and the registry supports reverse discovery by canonical format, publisher format option, and creative operation. Consumers retain 3.x compatibility with catalog entries that omit either new routing field, treating absent `operations` as `build`. Deprecate `list_creative_formats`, compound named format IDs, and format-attached transformer I/O throughout the schemas and current documentation while retaining explicit 3.x compatibility branches. Sales agents declare deliverability on `Product.format_options[]`, publishers declare acceptance in `adagents.json.formats[]`, and portable creative manifests continue to carry `format_kind` plus an optional `format_option_ref` rather than agent-local capability identity. Add normative multi-placement eligibility guidance and reusable canonical classification vectors. Make publisher catalog freshness observable: `publisher.adagents_changed` now covers semantic changes in every top-level `adagents.json` field, including formats-only and placements-only revisions, and new events populate `changed_fields` plus format and placement counts. Clarify that the registry publisher lookup—not the origin-only `validateAdAgents()` path—provides the community-catalog fallback and its provenance. - 316d55d: Add `sample_render_url` to canonical format declarations and registry summaries so publisher catalogs retain a human-facing preview path without a creative agent owning the format. Define declaration-scoped authority and safe consumer behavior without implying buyer-asset rendering, validation, creative approval, publisher acceptance, or live-delivery fidelity. - 5bf55d3: Add agent-level `capabilities.changed` notifications for cached `get_adcp_capabilities` responses, plus account-anchored `account.status_changed` notifications for durable account lifecycle changes. Includes freshness metadata, registration schemas, webhook payload documentation, and read-side account webhook activity. - 1d5f020: Complete cross-role governance conformance coverage for AdCP 3.2 with exact task-and-mode capability gates, positive and negative rights and paid-creative proofs, a universal discoverability index, and matching reference-agent enforcement. - 2ec0882: Add a compact, backwards-compatible performance-feedback assertion and make measurement agents discoverable producers of optimizer-ready feedback through buyer-controlled orchestrator gateways. The request now names its baseline, reuses standard/vendor metric identity, attributes a producer and provider-scoped methodology, carries a small evidence summary or provider-hosted evidence reference, and supports immutable maturation through final/as-of/supersession fields. The first gateway tier deliberately reuses two fixed tasks: providers pull buyer-approved delivery with `get_media_buy_delivery` and return compact assertions with `provide_performance_feedback`. Orchestrators authenticate returned assertions and fan normalized feedback out to sellers under the buyer's identity; sellers can return a receipt and honest optimizer application disposition. Existing per-account `allowed_tasks` grants authorize these gateway reads and writes, so measurement providers never need seller credentials and no new RBAC primitive is required. Webhook and offline interchange are deferred until complete registration, credential, payload, and receipt contracts exist. Also clarifies Accounts Protocol conformance: buyer-declared sellers (`require_operator_auth: false`) MAY omit `sync_accounts` when they lazily provision from the natural key and expose `list_accounts` as the cold-start recovery read. - 3f1bfeb: Define governance enforcement as a cross-role core capability. Consequential request schemas now declare `x-governed-commitment`, while `adcp.governance_enforcement` advertises the enforcement modes and covered tasks. Tighten the experimental authorization boundary: buyer intent checks use `plan_id`, approved decisions alone issue `governance_context`, downstream services treat that context as the opaque plan binding, and media-buy online execution enforcement follows prepare → check → commit. `conditions` is now an intent-only counterproposal with a separate non-authorizing consultation handle. Harden governance identity and accounting at the same boundary. Authenticated buyer identity controls delegations across the full lifecycle, while the intent's target audience controls seller authorization. Purchase checks may run before a durable media-buy ID exists, must authenticate as that audience, and may narrow but never widen or change the currency of the intent authorization. For media-buy updates, buyers propose a positive-delta ceiling and sellers independently compute and enforce the actual delta from authoritative state. Indirectly priced tasks require an explicit commitment, including amount zero for verified no-cost work. Critical JWS extensions bind the monetary ceiling, exact task, and canonical payload hash so services can verify authorization without reading governance-private state. Outcome reports authenticate as the original buyer, preserve purchase type, settle an opaque action binding once across all lifecycle check IDs, cache identical retries before mutable plan lookup, validate all monetary inputs, and reserve the governance-owned approved budget rather than trusting a buyer-reported amount. Publish one cross-language fixture set with JCS payload hashes, decision tables, and 27 byte-exact Ed25519 compact-JWS cases. The cases cover critical markers, audience/caller/task/payload bindings, commitments, time bounds, replay identifiers, signature tampering, and zero-cost authorization using an explicitly test-only public keypair. This intentionally changes validation on the experimental campaign-governance schemas in the next minor release; integrations on that experimental surface must update together when adopting 3.2. - 98b9bda: Add structured discovery for exact supported language-targeting ranges while preserving legacy boolean capabilities, and widen targeting language values from ISO 639-1 to canonical BCP 47 ranges with explicit RFC 4647 Basic Filtering semantics. - 0eee990: Add hard daily budget caps to the shared media-buy budget hierarchy (RFC #5983). - Media-buy `daily_budget_cap` bounds aggregate daily spend without creating package allocations; optional package caps are subordinate ceilings, not reservations. - One media-buy `budget_cap_timezone` defines the calendar-day boundary for every cap. Without an override, the cap capability selects the account timezone or an advertised feature-specific fixed timezone. - Legacy create/update and compact buy/control/accept lifecycle schemas expose the aggregate cap and timezone; package request/update/control and readback schemas expose only the subordinate cap. - Numeric updates apply immediately with current-day spend counted; `null` removes a cap. Timezone changes begin at the next existing cap-day boundary. - Account capabilities now declare seller-fixed versus account-fixed operational timezones. Buyer-selected account timezones are established through `sync_accounts`, round-trip through account reads and natural references, and remain distinct from explicit reporting and billing clocks. - `media_buy.budget_capping` declares `supported_scopes`, `supported_periods`, `timezone_basis`, optional `fixed_timezone`, and optional buyer override support. A `daily_budget_cap` is always hard; sellers must reject unsupported scopes instead of silently dropping or softening them. - 3e52da3: Define Trusted Match publisher authentication as a deployment obligation, require HTTP 401 with a WWW-Authenticate challenge for missing or failed authentication, bind authenticated publishers to allowed Context Match properties in multi-publisher deployments, and add gated raw-HTTP conformance coverage for absent and invalid authentication on Context Match and Identity Match. - 9ceac99: Deprecate inline provider credentials in secured artifact access. Signed URLs are now the recommended default for one-off delivery, while credential-free workload identity remains available for established relationships and bounded bearer tokens remain available for origins that require them. Document the AdCP 3.2 migration path and require secret-safe handling of legacy credentials, bearer tokens, and complete signed URLs during the compatibility window. The deprecated `service_account.credentials` field remains schema-valid throughout 3.x and is eligible for removal in 4.0 or later once the six-month notice and full-release-cycle policy gates are satisfied. - f3e3e0c: Add `quality_used` to successful `preview_creative` responses so buyers can detect render-quality downgrades before approval. - cc917a4: Require delivery, media-buy, and creative-list responses to honor deterministic date, identity, and status filters through observable membership and boundary semantics. - 133f1f8: Extend `reporting-frequency` with `weekly`, `quarterly`, and `post_campaign` to match audience-currency publication cadences in measured channels (OOH, radio, print), with guidance that a product's declared cadence gates mid-flight optimization eligibility. Surface the print channel guide in docs navigation. - a66cce5: Define a normative VAST validation contract for `vast` creative assets. Today the entire format-layer contract for a VAST asset is the `vast_version` string in `vast-asset-requirements.json`: nothing in the spec requires parsing the document, checking for an `` or ``, verifying the version attribute, or bounding wrapper chains, and `error-code.json` has no VAST codes, so a structurally valid manifest can carry an unplayable tag that fails silently at serve time. This change adds: - `creative_specs.vast_validation` on `get_adcp_capabilities` (`structural` | `document` | `wrapper`, default `structural`), following the capability-gating pattern of `media_buy.governance_aware`: sellers that do not inspect VAST documents keep the default and are unaffected. - A "VAST Validation" section in the video channel docs specifying the checks at each level: document parse, root element and `` agreement with the declared `vast_version` / format requirement / seller `vast_versions`, `` and `` presence, HTTPS URLs, wrapper resolution bounded by the format's existing `max_wrapper_depth`, loop detection, per-hop timeout, and terminal-document checks. Validation runs at `sync_creatives` (including `dry_run`); `validate_input` stays manifest-structure-only. A passing preflight is explicitly not approval of future responses from a decisioning endpoint. - Three error codes with `enumDescriptions` and `enumMetadata`: `VAST_PARSE_FAILED`, `VAST_VERSION_MISMATCH`, `VAST_WRAPPER_DEPTH_EXCEEDED` (all correctable, with `error.details.reason` discriminators). Macro correctness and substitution verification are explicitly out of scope (in-flight WG work on click-tracker insertion and decisioning-time substitution). Additive; no change for sellers that do not declare the capability. - 9c66556: Add `CreativeFilters.asset_types` to `list_creatives`, add `zip` to `AssetContentType`, define exact OR-within-field and AND-across-fields matching semantics for top-level creative assets, and add focused published-post and HTML5-bundle conformance coverage. Generated SDK types expose these additions only after the matching SDK release is published; pinned consumers must upgrade explicitly. - b41666f: Align brand property and verification schemas with the canonical property-type enum, including `linear_tv` and `ai_assistant`. - afd1e98: Add identifier-based named-place geographic targeting: - `targeting_overlay.geo_places` and `geo_places_exclude` carry stable identifiers with country, system, place type, optional catalog version, and diagnostic labels. - `get_adcp_capabilities` declares exact country/type pairs, accepted catalog versions, and a standard resolver for every collision-safe identifier system. - `get_products.targeting_overlay` carries known place IDs so configured products, pricing, and forecasts reflect them; `required_overlay_support` and Product `overlay_support` declare collision-safe permission for place values selected later. - Package status MUST echo persisted place overlays with the applied catalog version through the existing `targeting_overlay` contract. - Resolver responses echo their normalized query, carry machine-verifiable disambiguation and lifecycle metadata, and support existing-ID refresh after catalog rollover. - `PLACE_TARGET_UNAVAILABLE` provides a nonfatal, correctable read-path signal when a pinned target can no longer execute without silently changing geography. - Create-time place overlays use deterministic `UNSUPPORTED_FEATURE`, `INVALID_REQUEST`, and `PRODUCT_UNAVAILABLE` dispositions while preserving the configured product's binding pricing contract. - Place forecast and delivery breakdowns remain deferred; package echo is the interim configuration-audit path. Refs #5588. - 39ef6da: Require AdCP 3.2 producers to emit integer `error.retry_after` seconds while preserving the released numeric wire type for 3.x compatibility, and define ceiling-before-clamp behavior for clients that encounter legacy fractional values. - eb9ce94: Add portable age-targeting intent, product-scoped continuous/bucket/signal execution capabilities, authoritative demographic signal predicates, and lossless exact package readback for AdCP 3.2. - 8f47ee4: Preserve Trusted Match Context targeting key-values in router-authored, provider-attributed buckets. **Migration for experimental TMP adopters:** provider-to-router responses continue to use `signals.targeting_kvs`. Router-to-publisher responses that previously used `signals.targeting_kvs: [{ "key": "category", "value": "sports" }]` must instead preserve attribution as `signals_by_provider[provider_id].targeting_kvs: [{ "key": "category", "value": "sports" }]` and validate that hop against `context-match-response.json`. The flattened router field is removed rather than accepted as an alias because it loses the provider identity required for safe publisher mapping. This breaking change to the experimental `trusted_match.core` surface was [announced in #6252](https://github.com/adcontextprotocol/adcp/issues/6252) on August 6, 2026. Under the [experimental-surface notice contract](https://adcontextprotocol.org/docs/reference/experimental-status), it must not appear in a release before September 17, 2026. - 0096e30: Add `reference_assets` array to `product_card_detailed` for typed seller collateral (coverage maps, sample renders, environment photos, media kits). New schema: `core/product-card-reference-asset.json`. - 245d11c: Promote the released 20-value media channel taxonomy from draft documentation status to a stable normative surface by lazy consensus. - e75f12f: Add `property_list_exclude` to the targeting overlay: a reference to a property list whose properties must not carry the buyer's ads, for brand-safety do-not-run lists (apps and sites). Mirrors `collection_list_exclude` and reuses `property-list-ref.json`. Exclude wins on overlap with `property_list` and applies regardless of the product's `property_targeting_allowed` flag. Sellers declare support via the property/collection list entries in the `get_adcp_capabilities` targeting table. - 9288de4: Standardize property-list change notifications on the RFC 9421 webhook profile. Keep the undefined legacy body-level `signature` field as a required, deprecated compatibility marker through 3.x; remove it in 4.0. - 1760c31: Gate signal-activation governance conformance on the task-scoped `adcp.governance_enforcement` claim. The denied activation scenario now exercises an explicit paid activation, grades missing signed authorization, and verifies that rejection caused no platform-primary deployment call instead of treating a response echo as governance evidence. - f81c406: Allow buyers to reconcile an existing advertiser account's operator identity through revision-checked `sync_accounts` settings updates. Sellers advertise supported operator and operator-unit changes, return machine-readable dry-run impacts, preserve account continuity during atomic rekeying, route operator-domain handoffs through explicit approval with billing and grant revalidation, reject target-key collisions without merging, expose pending approval state through account reads, and tombstone former natural keys with an `ACCOUNT_MOVED` repair reference instead of provisioning duplicate accounts. - 0bb862e: Add a joined 3.2 warnings and indicators contract aligned to the compact media-buy lifecycle. Completed `buy_products`, `accept_proposal`, and `control_media_buy` operations may carry structured non-blocking warnings, mirrored by the `create_media_buy` and `update_media_buy` 3.x facades; continuing conditions appear as compact current indicators on media buys, packages, or package–creative assignments. Seven standard indicator types cover creative, audience, inventory, pacing, and budget risks or optimization opportunities that warrant buyer attention. Mixed publisher approvals use scoped outcomes. Polling through `get_media_buys` is the baseline; sellers may additionally support signed `indicators.changed` invalidations. Assignment notifications remain independent, and creative-library sellers may expose bounded reverse assignment state through `list_creatives`. No indicator IDs, sub-versions, history API, automatic action dispatcher, or separate `get_indicators` task is introduced. - bb58a59: Remove the deprecated top-level media-buy lifecycle `status` property from synchronous `create_media_buy` and `update_media_buy` success payloads for AdCP 3.2. Lifecycle state now uses only `media_buy_status`; top-level `status` remains reserved for the protocol task envelope. This intentionally uses a minor changeset under the ratified DR-0011 exception so the approved removal in #4906 is released as 3.2 rather than being mechanically retargeted to 4.0. - 3785751: Rename the experimental Trusted Match Protocol Offer `macros` field to `creative_data` before stabilization, without retaining a deprecated alias, and keep tracker URLs in the creative manifest where they belong. - 0b772e7: Require every AdCP 3.2 request signature on a body-bearing request to cover `content-digest`, retain legacy digest modes only for 3.0/3.1 compatibility, migrate 3.2 request `Signature` and `Content-Digest` binary fields from the legacy Base64URL override to RFC 8941 padded Base64 while keeping webhook v1 on its explicitly routed legacy encoding throughout 3.x, add versioned body-substitution conformance coverage, and reject non-canonical release versions with leading zeros or malformed prerelease suffixes across AdCP version-negotiation surfaces. The external signed-request storyboard remains explicitly 3.1-compatible until profile-aware vector selection lands; repository CI provides the 3.2 body-integrity coverage in the interim. - 3bdadda: Add contingent revenue-share pricing for affiliate and other outcome-priced media. The new `revenue_share` pricing option applies a decimal `commission_rate` to settled `commissionable_value`. Product discovery can filter fixed, auction, and contingent pricing independently; delivery and `report_usage` expose the commission basis for formula-checked reconciliation. Revenue-share packages bind billing to an event source and measurement window, do not use `bid_price`, and treat package budget as the maximum payable commission. - 0b4a2e2: Add package-scoped weighted, even, sequential, and random creative rotation with package-local groups and validated sequence positions. - 0bb862e: Add the compact AdCP 3.2 product and MediaBuy lifecycle: `list_products`, `request_proposals`, `refine_proposals`, `decline_proposals`, `buy_products`, `accept_proposal`, and `control_media_buy`. The task-specific contracts separate offer discovery, immutable draft proposal creation, explicit finalization with inventory reservation, terminal decline, direct purchase, proposal acceptance, and operational delivery control while retaining `get_products`, `create_media_buy`, and `update_media_buy` as compatibility facades throughout 3.x. New purchase and control inputs never accept inline creatives; commercial amendments and negotiated cancellations fork an accepted proposal, while operational controls remain revision-checked. Compact purchase snapshots preserve resolved package flight, billing-measurement, performance, and reporting terms. A shared opportunity reference connects planning-cycle context through purchase without duplicating proposal version identity. Canonical inputs use stable brand keys, catalog references, and compact account and optimization types so brand assets, legacy named formats, creative provenance, and compliance payloads do not transitively enter the clean tools. Publish machine-readable SDK fallback grades and task-result schema resolution, plus MCP production, media-buy, and creative catalogs that remove presentation annotations without changing validation semantics. The role catalogs select active 3.2 seller-hosted operations and publish client-side input-only prompt views while retaining output and terminal task-result schemas in their parent validation catalogs. Preserve the AdCP 3.1 inventory-reservation contract in the compact lifecycle: requested and revised proposals remain immutable drafts until `refine_proposals` finalizes them, and a finalized `committed` snapshot guarantees inventory is held until `expires_at`. Add `countries` and `property_list` product-attribute filters, and publish compact task-specific async envelopes for consultative proposal planning and re-underwriting. Let compact BrandKeys qualify commercial advertiser identity with canonical `countries[]` without turning identity into delivery targeting. Extend buyer-declared natural accounts with an operator-owned unit (`id` plus mutable display `name`), optional immutable account currency, and sandbox identity. These fields round-trip through `sync_accounts` and `list_accounts`; the operator unit remains explicitly distinct from the seller/storefront `account_id`. Require sellers implementing 3.2 advertiser-account provisioning to advertise fixed versus per-media-buy account currency support while keeping the additive field optional on the shared 3.x response schema for 3.1 compatibility, and define only the BrandKey projection of a compatibility BrandRef as account identity. - ec02269: Settle TMP router merge behavior for 3.2: Context Match duplicate offers use provider priority with arrival-order tie-breaking, while Identity Match uses an explicit responder-scoped union that preserves silent-ignore privacy semantics. - a26d30a: Add signed-response verifier error codes and response-signing verifier checklist. - Add `SIGNED_RESPONSE_ENVELOPE_EXPIRED`, `SIGNED_RESPONSE_REQUEST_HASH_MISMATCH`, and `SIGNED_RESPONSE_TENANT_MISMATCH` to `enums/error-code.json` with normative descriptions and recovery classifications. - Add a 10-step verifier checklist for designated-task response signing in `docs/building/by-layer/L1/security.mdx`, formalizing the MUST-level verification steps for `verify_brand_claim` / `verify_brand_claims` signed responses — expiry, request-hash binding, tenant binding, and payload consistency. Refs #6147 - 60f368f: Use stable structural schema titles for generated type names, consistently annotate deprecated fields with `deprecated: true`, and deprecate exact `format_ids` fields in AdCP 3.2 ahead of their removal in AdCP 4.0. - 5c3ba24: Define the versioned AdCP A2A 1.0 profile extension, including structured { skill, input } invocation, completed-Task mapping for submitted AdCP work, get_task_status polling, and binding vectors. - fd610b5: Add deterministic proposal-refinement constraints (total budget, CPM ceiling, impression floor, flight window), product changes, alternatives, outcome reasons with hold and batch-abort codes, negotiation lineage via required `parent_proposal_id`, buyer-verifiable `terms_digest` semantics, and capability discovery; rename the unreleased revision `instructions` field to `ask`. - 146f106: Unify targeting across `list_products`, `request_proposals`, `refine_proposals`, the `get_products` compatibility facade, configured product selection, and media-buy execution. Buyers can now provide concrete `targeting_overlay` values during discovery and require product-scoped future targeting through `required_overlay_support`. Products disclose selectable `overlay_support`, sparse buyer-reviewable `targeting_resolution` changes, and opaque buyable `product_id` values: non-custom wholesale IDs remain stable for the same logical offer within seller and cache scope, while custom IDs remain lineage-bound. Product pricing and forecasts are bound to concrete effective targeting; future support alone guarantees selectability, not value-specific availability or forecasting. Buyers should prefer structured fields over equivalent brief prose, while sellers continue to apply explicit hard brief requirements. A material structured interpretation is confirmed at response-root `targeting_resolution.brief_targeting` for `request_proposals` and `get_products`, or on the affected `refine_proposals.results[]` entry for refinement instructions. Move purchased placement selection into `targeting_overlay` alongside property and collection selection, while preserving creative placement references as routing-only. Fixed placement sets may be restated exactly across discovery, create, and update without advertising selectable support. Add typed device-platform exclusion with independently declared product support, and keep arbitrary buyer-supplied ad-server key/value targeting outside the protocol trust boundary. Put discovery and package resolution behind lifecycle-specific schemas, move demographic package execution readback to `targeting_resolution.demographics` before its 3.2 release, deprecate targeting-like product filters, add migration guidance and conformance coverage, and retain exact-only booked package execution. Clarify that deterministic product filters exclude non-matching products in `brief`, `wholesale`, and `refine` modes, and add seeded behavioral conformance coverage that detects full and partial filter no-ops. Update the buyer skill, Addie knowledge, and buyer learning modules to teach structured-first request decomposition and targeting-resolution review. Live training-agent support follows the generated 3.2 beta SDKs under issue #6199. - 8448085: Restructure `Format.assets[]` oneOf from a flat 16-variant union to a two-tier discriminated union. Outer discriminator on `item_type` ("individual" | "repeatable_group"); inner discriminator on `asset_type` for the 15 individual-asset variants. Adds `discriminator.propertyName` hints at both tiers and direct `required` constraints on each variant so codegen tools (openapi-generator, quicktype) produce proper discriminated-union types. Wire-payload acceptance set is unchanged. - 636d461: Add typed proposal negotiation compliance storyboard. - Add `typed_proposal_negotiation.yaml` exercising the AdCP 3.2 typed negotiation lifecycle through `refine_proposals`: capability-gated constraint satisfaction (total_budget, product_changes, alternatives), partial invariant, unsupported dimension rejection, finalize atomicity, idempotent replay, immutable lineage, digest-verified acceptance, amendment, cancellation, double-finalize rejection, and multi-source batch. - Register the scenario in the media-buy seller `index.yaml`. Refs #6559 - 0b5a576: Extract `enums/delivery-status.json` and `$ref` it from `get-media-buys-response.json`, `get-media-buy-delivery-response.json`, and `media-buy-delivery-webhook-result.json`. These three schemas previously inlined `delivery_status` independently: the `get_media_buys` snapshot path had 6 values (including `not_delivering`), while the delivery report and webhook paths only had 5. A buyer polling `get_media_buy_delivery` or receiving a delivery webhook had no way to observe `not_delivering` even though that is precisely the "zero delivery during flight" signal those paths exist to surface. All three now resolve to the same 6-value enum. Fixes #6103. - be8acd6: Add country- and value-aware ISO subdivision support declarations for `geo_regions` and `geo_regions_exclude` across seller capabilities, targeting-aware product discovery, and product-scoped future overlay support. Define exact configured discovery/refinement as region preflight and document independent include/exclude matching, atomic execution, and deterministic error dispositions. - 56c03e9: Add VAST 4.3 to the VAST version enum. IAB Tech Lab released VAST 4.3 in December 2022, but `vast-version.json` stopped at 4.2, so a buyer trafficking a 4.3 tag had to declare a version the document does not carry. `vast_version` mirrors the `version` attribute on a VAST document's root element, and the enum already carries 2.0 and 3.0, so the roster is the published-version list rather than a curated feature set. The two schemas that restated the list inline (`core/requirements/vast-asset-requirements.json` and `formats/canonical/video_vast.json`) now `$ref` the shared enum, per the Enum Consolidation rule in `docs/spec-guidelines.md`. VAST 4.4 is deliberately excluded: its XSD is annotated "DRAFT for working group discussion" and is not a published specification. - 5997d58: signals + media-buy: enforce cache-scope isolation for wholesale-feed conditional fetch The schemas already state the wholesale-feed token is keyed by `(cache_scope, wholesale_feed_version)`, but nothing exercised that a token minted under one `cache_scope` cannot short-circuit (`unchanged: true`) a request the agent resolves to another. The reference training agent advertises `wholesale_feed_versioning.cache_scope_account: true` yet keys conditional fetch on a scope-independent token, so it would silently answer `unchanged` across scopes — exactly the gap reported in #5739. - **Schemas** — `signals/get-signals-response.json` and `media-buy/get-products-response.json`: add a normative cross-scope MUST-NOT to the `unchanged` description, scoped to the conditional-fetch comparator (it MUST key on `(cache_scope, wholesale_feed_version)`, not the token alone). - **Storyboards** — new universal `wholesale-feed-signals-scope-isolation` and `wholesale-feed-products-scope-isolation`, gated on `wholesale_feed_versioning.cache_scope_account: true`; agents without per-account overlays grade `not_applicable`. - **Reference agent** — scope-key the wholesale feed/pricing tokens so the comparator rejects cross-scope tokens (and the existing same-scope `unchanged` path still matches). Closes #5739. - 81cf467: Withdraw the incorrectly specified `publisher_domain` filter from `get_products` before the next minor release. The filter was not patch-eligible for the stable 3.1.x line, and its implementation incorrectly accepted the plural `publisher_domains[]` form that product schemas reject. ### Patch Changes - 30f10f1: Add audience dependency-impairment conformance coverage and clarify `list_accounts` as the recommended cold-start recovery read for buyer-declared accounts. - c7209a7: Add capability-gated compliance storyboards for seller-fixed, buyer-selected, and seller-assigned account timezones; account-based and feature-fixed daily budget-cap boundaries; buyer cap-timezone overrides; and independent account, reporting, and billing clocks. Cover buyer-preference mismatches against a seller-fixed UTC clock, portable natural-key reconnects, and selecting a second advertised timezone as a separate immutable account identity. Reject unadvertised buyer timezone overrides with the canonical unsupported-feature error. - 362ed68: Add literal v1 canonical mappings for observed duration-, dimension-, and VAST-suffixed legacy format ids, with reference vectors that preserve encoded constraints and fail closed for durationless placements. - a72fbd7: Clarify that future brand relationship declarations do not extend trust before their effective time and that omitted declaration timestamps age from a durable first observation. - d54e7d8: Autolink machine-resolved error codes and task names in compliance reference docs, with build-failing unknown-symbol checks and reviewed exceptions. - 4deb946: compliance(media-buy): the `available_actions` scenario uses a non-guaranteed product fixture so `sales-non-guaranteed`-only sellers can run it. `available_actions.yaml` seeded a guaranteed-only product, so its `create_buy_from_product` step (and the whole available-actions enforcement flow that follows) failed with a terminal `DELIVERY_MODE_NOT_SUPPORTED` for sellers that declare only `specialisms: ["sales-non-guaranteed"]`. The `allowed_actions` behavior the scenario actually grades is delivery-type-agnostic, so the fixture is switched to `non_guaranteed` (floor-priced) — the same fix applied to the base `media_buy_seller` flow. The packaged `dist/compliance/` cache is generated from this source. - 4deb946: Runner output contract: document the branch-set `any_of` peer cascade exemption. `cascade_rules` now names a `branch_set_cascade_exemption` (parallel to `sole_stateful_step_exemption`) stating that a stateful peer's genuine failure or `peer_branch_taken` skip MUST NOT cascade `prerequisite_failed` onto a sibling phase sharing the same `branch_set.id` under `any_of` semantics — the peers are mutually-exclusive alternatives, not a dependency chain. The exemption is scoped to `any_of`, is N-ary-safe (any number of peers), leaves cross-set and within-phase cascade unchanged, and is explicitly `depends_on`-agnostic (it fires whether the sibling's dependency is the implicit default or an explicit `depends_on` naming the peer). `storyboard-schema.yaml`'s `depends_on` section gains a cross-reference. Documents-only; codifies the runner behavior shipped in adcp-client#2306 (closing adcp-client#2305), root-caused in adcp#5337. No schema or wire change. - 4deb946: Enforce `cancellation_fee.rate` / `.amount` by fee `type` in `cancellation-policy.json`. Both fields are documented as conditionally required — `rate` "Required when type is 'percent_remaining'", `amount` "Required when type is 'fixed_fee'" — and the requirement is restated in the pricing-models reference, but `cancellation_fee` listed only `["type"]` in `required[]`. A validator therefore accepted `{ "type": "percent_remaining" }` (or `{ "type": "fixed_fee" }`) with no fee value at all, leaving a money-path term that declares nothing computable for a buyer accepting the product's cancellation terms. Adds `if/then` conditionals: `percent_remaining` requires `rate`, `fixed_fee` requires `amount`; `full_commitment` and `none` are unaffected. No prose change — this aligns the schema with the already-documented contract, and no existing example regresses (both doc examples already carry `rate`). Regression coverage added to `tests/composed-schema-validation.test.cjs`. - eac2f7a: Emit canonical version-qualified HTTPS `$id` and external `$ref` values in published schema artifacts so locally loaded schemas resolve references consistently. - f672367: Add the `capabilities_response_schema_invalid` canonical notice code and a `capability_pointer` (RFC 6901) optional notice field to the runner output contract, so a schema-invalid `get_adcp_capabilities` response is reported once as a root cause ahead of the track results instead of fanning out into unrelated-looking track failures. - badabd9: Clarify that `brand.json` supplies master brand identity while catalogs supply product and item payload, including item-level property or franchise logos that do not override the master brand kit. - 4deb946: Clarify the boundary between `validate_input` manifest preflight and `sync_creatives` dry-run trafficking rehearsal. - ee58aac: Clarify that 3.1 SDKs preserve pixel-ratio registry metadata without interpreting it, while typed validation, precedence, and error semantics begin in 3.2. - 11d8ad0: Clarify that exhausting the rate-limit trip runner without observing a `RATE_LIMITED` response is a coverage gap reported with `skip_result.reason: not_applicable` and `skip_result.detail: rate_limit_not_triggered`, not a seller conformance failure. - bc3cfc9: Add deterministic compliance coverage for reconciling submitted `create_media_buy` tasks through `get_task_status` and `list_tasks`, including controller-driven terminal completion. - 54afa6a: Complete the sponsored-intelligence training tenant with the SDK's native SI platform surface, accountable sponsored-context fixtures, and required storyboard coverage. - 6f2172e: Document the 3.2 creative-format discovery deprecation posture and relax conformance so agents that declare equivalent canonical-format discovery are not required to expose `list_creative_formats`. Buyers MUST NOT assume the v1 discovery tool when canonical discovery is declared. `list_creatives` remains the creative-library query task; v1 `format_id` / `format_ids[]` remain supported through 4.x. Compliance storyboard steps that call `list_creative_formats` are gated with `requires_tool`, producing an explicit non-failing `missing_tool` skip when the canonical discovery branch passes. - da3954f: Deprecate the legacy `list_creative_formats` pricing request fields through 3.x and direct transformation and generation pricing discovery to `list_transformers`. - 266264c: Fail closed during release signing when the current protocol tarball is missing. - 420d1e8: Move stale active-window dates in compliance fixtures and 3.0 compatibility bundles forward so storyboard runs continue to exercise protocol behavior instead of calendar drift. - 4deb946: Replace phantom error codes in creative and campaign-governance task docs with canonical enum members. `sync_creatives`, `build_creative`, the Creative Protocol specification, `check_governance`, and `sync_plans` documented 13 `errors[].code` values that do not exist in `enums/error-code.json` (`INVALID_FORMAT`, `ASSET_PROCESSING_FAILED`, `BRAND_SAFETY_VIOLATION`, `FORMAT_MISMATCH`, `CREATIVE_IN_ACTIVE_DELIVERY`, `ASSET_MISSING`, `ASSET_INVALID`, `GENERATION_FAILED`, `INVALID_MANIFEST`, `AMBIGUOUS_CHECK_TYPE`, `SELLER_NOT_RECOGNIZED`, `INVALID_PLAN`, `BUDGET_BELOW_COMMITTED`). SDKs that validate `errors[].code` against the published enum reject responses built from the docs literally, the same failure mode as #4852 and #5307. Each phantom is remapped to the existing code with matching semantics (`UNSUPPORTED_FEATURE`, `VALIDATION_ERROR`, `CREATIVE_REJECTED`, `INVALID_STATE`, `INVALID_REQUEST`, `PERMISSION_DENIED`); `GENERATION_FAILED` is replaced with guidance that generation-pipeline failures surface as task-level failure with the most specific applicable canonical code, per the open-vocabulary rule on `error-code.json`. Also fixes the one live `INVALID_FORMAT` emission in the training-agent reference implementation. Docs and reference implementation only; no wire change. - 4deb946: Fix false failures in creative compliance storyboards (canonical_supported_formats, evaluator_auth). `canonical_supported_formats`: removes the hardcoded `capability_id: "training_image_generation"` assertion (capability_id is agent-local; any valid value must pass) and the `field_absent` check on `supported_formats[1]` (agents may advertise multiple canonical formats). Fixes `context_outputs` field name from `key:` to `name:`. `evaluator_auth`: adds `requires_capability` guards to all five optional phases so agents that correctly declare `creative.supports_evaluator: false` receive `not_applicable` instead of failing the evaluator track. Guards evaluate against the raw capabilities response, bypassing a runner-side boolean-false accumulator bug. Fixes `context_outputs` field name from `key:` to `name:`. - 4deb946: Fix a misleading `get_media_buy_delivery` example that implied buyers can look up delivery by their own reference. `media_buy_ids` are seller-assigned; the top-level `buyer_ref` field was removed in 3.0.0. The example is retitled "Correlating Your Own Reference", uses seller-assigned `mb_...` IDs, and adds a note pointing buyers to reconcile their own reference via `context` echoed on `create_media_buy` / `get_media_buys`. - 1f7f4c2: Fix the creative-fate compliance storyboard to build creative assets from the required slots declared by the seller's selected product format. Clarify that runners must resolve context substitutions recursively before sending requests. - 2c1d02d: Fix three inline enum drift bugs where canonical enum files gained values that were missed in inline copies, and add a CI lint to prevent the class of bug. - Replace inline property-type enum in get-adcp-capabilities-response.json trusted_match.surfaces with $ref (missing linear_tv) - Replace inline adcp-protocol enum in registry-event.json badgeRole with $ref (missing measurement) - Replace inline catalog-type enum in sponsored_placement.json supported_catalog_types with $ref (missing promotion) - Add scripts/lint-schema-enum-drift.cjs: detects inline enums that are strict subsets of canonical enums, preventing future drift - 7970889: Correct the `input_schema_field_stripped` compliance notice so it identifies request payload drift without blaming agents for omitting fields that are not part of the canonical task schema, and consume the SDK release that keeps broad `list_accounts` discovery requests unscoped. - 66736ae: Allow `list_creatives` response rows to use either the legacy `format_id` identity or the AdCP 3.1 canonical `format_kind` and optional `format_option_ref` identity. - 55296ef: Remove hard-coded request-signing conformance vector counts from the compliance runner header and request-signing documentation. The examples now cover both vector directories without totals and explicitly supply matching local vector and key inputs instead of relying on package-internal cache paths. This changes only protocol comments and documentation, not schemas or runner behavior; documentation drift linting and its regression tests are also hardened to preserve the corrected CLI guidance. - 4deb946: Align idempotency and rate-limit guidance with the canonical top-level `error.retry_after` field across schemas, documentation, and compliance storyboards. - 04fb691: Remove a literal tab character from a comment line in `static/compliance/source/universal/runner-output-contract.yaml` (line 303). Tabs cannot start a token in YAML, so strict parsers fail to load the file entirely — platform engines consuming the packaged 3.1.4/3.1.5 compliance caches could not parse the runner output contract. Comment text unchanged; no semantic content changes. - da636b4: Clarify and enforce governed signal activation: `activate_signal` now documents `governance_context`, signal agents fail closed on governed accounts without a valid approval context, and signal governance compliance checks no longer require the signals tenant to own `sync_plans`. - 810d9fa: Correct the `video_16x9_30s` legacy canonical mapping to retain `16:9` as an aspect ratio, guard ratio tokens from pixel misclassification, and publish observed AAO-namespace static-display literals with their declared size constraints. - 582b724: Gate the `create_media_buy` submitted-arm compliance storyboard on the seller advertising `force_create_media_buy_arm` under `compliance_testing.scenarios`. Sellers without that sandbox-only forcing capability now skip the whole storyboard before its independent downstream phase can incorrectly fail their otherwise conformant synchronous or provisional media-buy flow. - 19599f3: Generate compliance error-code and storyboard documentation from the canonical machine sources, with build-failing drift checks. - 420d1e8: Allow governance checks to accept human approval from `ext.human_approval` and use that approval to clear reallocation-threshold human review. - aa99113: Require governance-aware media-buy storyboards to prove approved execution through durable seller readback and governance audit evidence, without requiring a response echo. - 298d99c: Grade creative-asset fixture coverage gaps as `not_applicable` with the new canonical `fixture_unavailable` skip reason instead of failing the seller, preflight future directives before intervening side effects, preserve the skip as non-failing untested coverage in registry aggregation, require explicit text-slot mappings, and add common image fixtures for 970x250, 300x600, 336x280, and 1080x1920 formats. Storyboards that previously stopped on a missing common-size fixture can now execute and produce real behavioral signal; valid formats the runner still cannot synthesize no longer count against the agent under test. - 032c624: Grade the `account.supported_billing` contract with data-driven out-of-set probes and applied-value membership checks. - a48e844: Grade synchronous wholesale product discovery over a five-second observation window and fail when the seller emits a matching task webhook after returning an inline terminal response. - a9e6ba9: Add `update_rights` to the brand-rights conformance lifecycle now that its schemas and completion-webhook task type are published. - 0afad76: Ground media-buy, creative, and governance read/list storyboard assertions in state created by prior steps. - efe8a4b: Add the AdCP 3.2 beta program overview, release notes, migration guidance, and release runbook needed for the beta.0 → SDK → beta.1 readiness cycle. - 4deb946: Preserve withdrawn and unpublished release status when generating file-based schema discovery so exact artifacts remain available without becoming stable alias targets. - 1046d10: Add optional conformance coverage and cross-linked guidance that keeps buyer-managed synced audiences distinct from seller- and source-native signals through discovery, media-buy selection, rejection, and exact readback. - 4deb946: Remove an incidental video-only constraint from the inventory list targeting storyboards so single-channel sellers can exercise the channel-agnostic scenarios. - 9770326: Clarify that AdCP-defined property names remain reserved for their existing semantics at the same wire location after deprecation, rename, or removal. - b438eb4: Rewrite the governance-conditions compliance flow around the cross-role authorization contract: conditions now yield only a consultation handle, the adjusted intent must receive approval, seller execution is binary, and committed state plus the plan outcome replace response-echo assertions. - b03bb8d: Restore frozen AdCP 3.0 storyboard compatibility with the fail-closed training-agent comply controller sandbox gate. - 502b22d: Mark all AdCP v2 schema releases as deprecated in discovery metadata after their end-of-life date while preserving the historical v2 and v2.5 aliases and pinned schema URLs. - d6b16c6: compliance: SHOULD-level session-lifecycle guidance for conformance runners Adds a `session_lifecycle` block to `runner-output-contract.yaml` (per the #6204 triage): runners SHOULD establish one MCP session per storyboard and reuse it across that storyboard's steps, SHOULD close sessions gracefully at storyboard end, and SHOULD treat an HTTP 404 on a known-terminated session id as terminal rather than retryable. A fresh streamable-HTTP session per call spends 4-5 protocol round trips on handshake alone (~5x per-step wall time, attributed to the agent under test rather than the runner), and orphaned GET-stream reconnects against expired sessions generate silent 4xx volume that is easily misread as rate limiting. Guidance only — no wire or schema change; the session-per-storyboard implementation lands in `adcp-client`. - 6fad41c: Surface relationship trust state in SearchBrandResult and the AgenticAdvertising.org registry API brand list endpoints. `SearchBrandResult` (the stub returned by `search_brands`) gains three optional fields — `relationship_trust`, `relationship_verified_at`, and `claimed_house_domain` — using the canonical semantics already defined on `ResolvedBrand`. The `house` field becomes optional (brands with no verified or claimed house are no longer required to supply it). Trust values follow the existing enum: `inline | mutual | leaf_only | house_only | standalone | unverifiable`. An absent `relationship_trust` means trust has not yet been computed and MUST NOT be interpreted as `standalone`. The AgenticAdvertising.org registry API (`/api/brands/registry` and `/api/brands/find`) and registry MCP list tools now return the same three fields on each brand row. Trust is persisted to the `brands` index table by the crawler after each brand.json resolution cycle, including house-side-only declarations discovered through `brand_refs[]`, so list endpoints return it without a per-row `resolveBrand()` call at query time. The `BrandRegistryItemSchema` and `CompanySearchResultSchema` Zod schemas are updated to match. The training agent now implements the experimental `search_brands` task end to end, including deterministic filtering, pagination, context echoing, and canonical trust vocabulary. - 1df9f18: Add deterministic compliance coverage for seeded `update_rights` lifecycle updates and unknown grant references. - ba9d7ad: Skip the per-agent billing permission phases when a seller does not advertise agent billing, preventing capability-level `BILLING_NOT_SUPPORTED` responses from being graded as failures against the narrower `BILLING_NOT_PERMITTED_FOR_AGENT` contract. - 530b859: Add an experimental publisher configuration schema for mapping provider-attributed Trusted Match targeting KVs to local ad-server destinations. - bd4dc10: TMP: publisher-owned TMPX macro mapping with provider-declared slot IDs and split provider→router / router→publisher response schemas. **Provider slot contract.** Providers declare a stable, provider-local slot list on their registration (`tmpx_slots: [string]` in provider-registration.json), e.g. `["primary","secondary"]`. Slot IDs are opaque provider-namespaced tokens, NOT ad-server macro names — distinct providers MAY reuse the same slot_id because publisher lookup is keyed on `(provider_id, slot_id)`. Ordering carries the ordered-prefix invariant: shorter responses emit an ordered prefix of the registered slots and are never shifted or sparse. **Response schemas.** Provider-to-router responses (`provider-identity-match-response.json`) carry `tmpx_chunks: [{slot_id, value}]`. Router-to-publisher responses (`identity-match-response.json`) reshape `tmpx_providers[provider_id]` to `{ chunks: [{slot_id, value}] }`, preserving the emitting provider's slot IDs and order. Chunks share a single definition — the new `trusted-match/tmpx-chunk.json` schema — `$ref`d from both hops. Both hop schemas add explicit negative constraints (`not: {anyOf: […]}`) that reject the other hop's fields (`tmpx_providers`/`tmpx` on the provider hop; `tmpx_chunks` on the publisher hop) and envelope-extension fields (`context`, `ext`) that would leak across the identity privacy boundary. Both maps carry `propertyNames` constraints matching the `provider_id` charset (and `slot_id` charset on inner maps), so the wire cannot carry map keys outside the registered form. **Publisher-owned config.** `publisher-tmpx-config.json` captures the publisher-owned deployment configuration as `tmpx_macro_mapping: { provider_id: { slot_id: destination } }`. The publisher's adapter reads `tmpx_providers[provider_id].chunks[]` from the response and, for each chunk, substitutes `chunk.value` into `tmpx_macro_mapping[provider_id][chunk.slot_id]`. Publishers use registered `tmpx_slots` to validate the mapping at startup and to detect provider slot-contract drift before serve time. When a response carries a `slot_id` (or whole `provider_id`) the mapping does not cover, the adapter MUST fail closed for that provider on that impression — none of that provider's chunks are fired into the ad-serving path and the adapter logs a configuration error; other providers on the same response are unaffected. **Security-motivated reshape.** Restores the direction described in #2203 by removing publisher-local names from the untrusted-provider boundary, and closes the cross-provider name-hijack surface tracked as #5945 by construction — the router never accepts destination names from providers. **Why 3.1.x, not 3.2 — bounded pre-production correction.** TMP has no production use yet, and this corrected shape will be in place before any 3.1 TMP production deployment ships. That fact — not the general experimental-surface rule — is the primary justification: the surface exists but has no live consumers to migrate. `x-status: experimental` (see `docs/reference/experimental-status.mdx`) is what makes the reshape technically permissible inside 3.x; the pre-production posture is what makes it practically safe. #5729 shipped the surface being reshaped 26 days ago into 3.1.1–3.1.4, but this changeset does not lean on that as a general precedent — the reshape is bounded to the pre-production window regardless of what came before. The change lives on the single-source schema tree, so it rolls forward into 3.2 automatically with no 3.1.x fork to maintain. **Migration.** Providers declare `tmpx_slots` on their registration (opaque provider-local IDs; drop the previous `tmpx_macros` list if adopted). Providers emit `tmpx_chunks: [{slot_id, value}]` on their identity-match response (previously `tmpx_macros[{name,value}]` or `tmpx_values[value]`). Routers produce `tmpx_providers[provider_id].chunks[{slot_id, value}]` (previously `.macros[{name,value}]` or `.values[value]`). Publishers configure `tmpx_macro_mapping[provider_id][slot_id] = destination` (a slot-keyed map, previously an ordered array). The legacy singular `tmpx` field remains supported through 3.x (removed in 4.0). Blast radius: the shape being replaced shipped only in 3.1.1–3.1.4 on an experimental field with no production adopters; the earlier working-tree ordinal variant introduced in this PR was never released. **Test coverage.** `tests/example-validation-simple.test.cjs` adds twenty new fixtures (positive + negative) proving each schema-encoded invariant actually rejects the wrong shape: wrong-hop fields on both hop schemas, legacy carrier fields (`tmpx_values`, `tmpx_macros`) on both hops, envelope-extension bleed-through, `provider_id`/`slot_id` charset violations on both maps, and duplicate slot IDs on `tmpx_slots`. **Router slot-contract enforcement.** The router MUST validate each provider's registered slot contract before forwarding: if the provider has no `tmpx_slots` registration, or if the returned `slot_id` sequence is not an exact non-empty ordered prefix of the registered list, the router MUST drop that provider's chunks atomically. This covers duplicate, reordered, sparse, and unregistered slot IDs before they reach publisher mappings. - 32b0128: Close the metric-accountability loop in the reference seller and conformance suite. Package commitments are seller-stamped and read back, vendor values and deferrals are reconciled per package, and unified `missing_metrics` reports overdue standard or vendor commitments. Packages without a snapshot fall back to current product reporting capabilities, while metrics not yet measurable in the current window stay out of the gap list. - ee66e24: Add behavioral conformance scenarios for media-buy status and ID filters, half-open delivery date ranges, and sales-agent creative status and assignment filters. Update the training agent to honor `list_creatives` media-buy filters. ## Unreleased experimental-surface notices - Cross-role governance enforcement: the experimental `governance.campaign` surface will add typed `target_agent`, task-scoped `adcp.governance_enforcement`, critical task/payload/commitment JWS bindings, intent-only conditions negotiation, and governance-authoritative settlement. The implementation may merge during 3.2 development; the beta-to-GA period provides the experimental-surface notice window. See the 3.2 release notes and migration guide. ## 3.1.21 ### Patch Changes - 2d4b210: Scope guaranteed idempotency replay to state-mutating requests while requiring read wrappers to tolerate optional keys. ## 3.1.20 ### Patch Changes - 301573a: Use the media-buy request schema when validating the universal creative-format pagination storyboard, avoiding spurious `input_schema_field_stripped` notices when the target is a media-buy agent. - 6dae516: Correct the OpenRTB/AdCOM `cattax` mappings documented for IAB Content Taxonomy 3.0 and 2.2. ## 3.1.19 ### Patch Changes - 76710cd: Gate creative-library and product-refinement compliance paths on advertised 3.1 capabilities, and keep measurement-term acceptance outside the universal rejection scenario. ## 3.1.18 ### Patch Changes - 71706fb: Complete the PAIR OpenRTB profile mapping, including matcher, match method, publisher scope, key rotation, and TMP's lossy scope boundary. ## 3.1.17 ### Patch Changes - 4f73f65: Correct the OpenRTB source for PAIR identifiers and clarify that PAIR wire values are rotating, publisher-scoped identifiers rather than universal IDs. ## 3.1.16 ### Patch Changes - 9289fea: Gate inventory-list compliance scenarios on the stable property-list capability and accept either documented no-match outcome. ## 3.1.15 ### Patch Changes - 39fff21: Clarify that exhausting the rate-limit trip runner without observing a `RATE_LIMITED` response is a coverage gap reported with `skip_result.reason: not_applicable` and `skip_result.detail: rate_limit_not_triggered`, not a seller conformance failure. ## 3.1.14 ### Patch Changes - bcaf175: Fix three schema fields whose inline enums omitted values already present in their canonical enum definitions. ## 3.1.13 ### Patch Changes - b09c757: Gate the `create_media_buy` submitted-arm compliance storyboard on the seller advertising `force_create_media_buy_arm` under `compliance_testing.scenarios`. Sellers without that sandbox-only forcing capability now skip the whole storyboard before its independent downstream phase can incorrectly fail their otherwise conformant synchronous or provisional media-buy flow. - 2bc2054: Skip the per-agent billing permission phases when a seller does not advertise agent billing, preventing capability-level `BILLING_NOT_SUPPORTED` responses from being graded as failures against the narrower `BILLING_NOT_PERMITTED_FOR_AGENT` contract. ## 3.1.12 ### Patch Changes - d156fae: Remove hard-coded request-signing conformance vector counts from the compliance runner header and request-signing documentation. The guidance now describes the vector directories without totals, so future fixture additions cannot make the prose stale. Comment and documentation text only — no schema, runner logic, or generated artifact is affected. ## 3.1.11 ### Patch Changes - ee58aac: Clarify that 3.1 SDKs preserve pixel-ratio registry metadata without interpreting it, while typed validation, precedence, and error semantics begin in 3.2. - 1f7f4c2: Fix the creative-fate compliance storyboard to build creative assets from the required slots declared by the seller's selected product format. Clarify that runners must resolve context substitutions recursively before sending requests. ## 3.1.10 ### Patch Changes - 30f10f1: Add audience dependency-impairment conformance coverage and clarify `list_accounts` as the recommended cold-start recovery read for buyer-declared accounts. - 648b820: Publish additive legacy Retina creative formats for seven standard display sizes. Each size now has a 2x-only format and a paired 1x-plus-2x rendition format, with registry mappings and shared vectors so current SDKs can discover and project the new catalog IDs while the canonical pixel-density protocol remains targeted at 3.2. ## 3.1.9 ### Patch Changes - 04fb691: Remove a literal tab character from a comment line in `static/compliance/source/universal/runner-output-contract.yaml` (line 303). Tabs cannot start a token in YAML, so strict parsers fail to load the file entirely — platform engines consuming the packaged 3.1.4/3.1.5 compliance caches could not parse the runner output contract. Comment text unchanged; no semantic content changes. - bd4dc10: TMP: publisher-owned TMPX macro mapping with provider-declared slot IDs and split provider→router / router→publisher response schemas. **Provider slot contract.** Providers declare a stable, provider-local slot list on their registration (`tmpx_slots: [string]` in provider-registration.json), e.g. `["primary","secondary"]`. Slot IDs are opaque provider-namespaced tokens, NOT ad-server macro names — distinct providers MAY reuse the same slot_id because publisher lookup is keyed on `(provider_id, slot_id)`. Ordering carries the ordered-prefix invariant: shorter responses emit an ordered prefix of the registered slots and are never shifted or sparse. **Response schemas.** Provider-to-router responses (`provider-identity-match-response.json`) carry `tmpx_chunks: [{slot_id, value}]`. Router-to-publisher responses (`identity-match-response.json`) reshape `tmpx_providers[provider_id]` to `{ chunks: [{slot_id, value}] }`, preserving the emitting provider's slot IDs and order. Chunks share a single definition — the new `trusted-match/tmpx-chunk.json` schema — `$ref`d from both hops. Both hop schemas add explicit negative constraints (`not: {anyOf: […]}`) that reject the other hop's fields (`tmpx_providers`/`tmpx` on the provider hop; `tmpx_chunks` on the publisher hop) and envelope-extension fields (`context`, `ext`) that would leak across the identity privacy boundary. Both maps carry `propertyNames` constraints matching the `provider_id` charset (and `slot_id` charset on inner maps), so the wire cannot carry map keys outside the registered form. **Publisher-owned config.** `publisher-tmpx-config.json` captures the publisher-owned deployment configuration as `tmpx_macro_mapping: { provider_id: { slot_id: destination } }`. The publisher's adapter reads `tmpx_providers[provider_id].chunks[]` from the response and, for each chunk, substitutes `chunk.value` into `tmpx_macro_mapping[provider_id][chunk.slot_id]`. Publishers use registered `tmpx_slots` to validate the mapping at startup and to detect provider slot-contract drift before serve time. When a response carries a `slot_id` (or whole `provider_id`) the mapping does not cover, the adapter MUST fail closed for that provider on that impression — none of that provider's chunks are fired into the ad-serving path and the adapter logs a configuration error; other providers on the same response are unaffected. **Security-motivated reshape.** Restores the direction described in #2203 by removing publisher-local names from the untrusted-provider boundary, and closes the cross-provider name-hijack surface tracked as #5945 by construction — the router never accepts destination names from providers. **Why 3.1.x, not 3.2 — bounded pre-production correction.** TMP has no production use yet, and this corrected shape will be in place before any 3.1 TMP production deployment ships. That fact — not the general experimental-surface rule — is the primary justification: the surface exists but has no live consumers to migrate. `x-status: experimental` (see `docs/reference/experimental-status.mdx`) is what makes the reshape technically permissible inside 3.x; the pre-production posture is what makes it practically safe. #5729 shipped the surface being reshaped 26 days ago into 3.1.1–3.1.4, but this changeset does not lean on that as a general precedent — the reshape is bounded to the pre-production window regardless of what came before. The change lives on the single-source schema tree, so it rolls forward into 3.2 automatically with no 3.1.x fork to maintain. **Migration.** Providers declare `tmpx_slots` on their registration (opaque provider-local IDs; drop the previous `tmpx_macros` list if adopted). Providers emit `tmpx_chunks: [{slot_id, value}]` on their identity-match response (previously `tmpx_macros[{name,value}]` or `tmpx_values[value]`). Routers produce `tmpx_providers[provider_id].chunks[{slot_id, value}]` (previously `.macros[{name,value}]` or `.values[value]`). Publishers configure `tmpx_macro_mapping[provider_id][slot_id] = destination` (a slot-keyed map, previously an ordered array). The legacy singular `tmpx` field remains supported through 3.x (removed in 4.0). Blast radius: the shape being replaced shipped only in 3.1.1–3.1.4 on an experimental field with no production adopters; the earlier working-tree ordinal variant introduced in this PR was never released. **Test coverage.** `tests/example-validation-simple.test.cjs` adds twenty new fixtures (positive + negative) proving each schema-encoded invariant actually rejects the wrong shape: wrong-hop fields on both hop schemas, legacy carrier fields (`tmpx_values`, `tmpx_macros`) on both hops, envelope-extension bleed-through, `provider_id`/`slot_id` charset violations on both maps, and duplicate slot IDs on `tmpx_slots`. **Router slot-contract enforcement.** The router MUST validate each provider's registered slot contract before forwarding: if the provider has no `tmpx_slots` registration, or if the returned `slot_id` sequence is not an exact non-empty ordered prefix of the registered list, the router MUST drop that provider's chunks atomically. This covers duplicate, reordered, sparse, and unregistered slot IDs before they reach publisher mappings. ## 3.1.8 ### Patch Changes - 810d9fa: Correct the `video_16x9_30s` legacy canonical mapping to retain `16:9` as an aspect ratio, guard ratio tokens from pixel misclassification, and publish observed AAO-namespace static-display literals with their declared size constraints. ## 3.1.7 ### Patch Changes - 66736ae: Allow `list_creatives` response rows to use either the legacy `format_id` identity or the AdCP 3.1 canonical `format_kind` and optional `format_option_ref` identity. ## 3.1.6 ### Patch Changes - 362ed68: Add literal v1 canonical mappings for observed duration-, dimension-, and VAST-suffixed legacy format ids, with reference vectors that preserve encoded constraints and fail closed for durationless placements. ## 3.1.5 ### Patch Changes - 4deb946: compliance(media-buy): the `available_actions` scenario uses a non-guaranteed product fixture so `sales-non-guaranteed`-only sellers can run it. `available_actions.yaml` seeded a guaranteed-only product, so its `create_buy_from_product` step (and the whole available-actions enforcement flow that follows) failed with a terminal `DELIVERY_MODE_NOT_SUPPORTED` for sellers that declare only `specialisms: ["sales-non-guaranteed"]`. The `allowed_actions` behavior the scenario actually grades is delivery-type-agnostic, so the fixture is switched to `non_guaranteed` (floor-priced) — the same fix applied to the base `media_buy_seller` flow. The packaged `dist/compliance/` cache is generated from this source. - 4deb946: Runner output contract: document the branch-set `any_of` peer cascade exemption. `cascade_rules` now names a `branch_set_cascade_exemption` (parallel to `sole_stateful_step_exemption`) stating that a stateful peer's genuine failure or `peer_branch_taken` skip MUST NOT cascade `prerequisite_failed` onto a sibling phase sharing the same `branch_set.id` under `any_of` semantics — the peers are mutually-exclusive alternatives, not a dependency chain. The exemption is scoped to `any_of`, is N-ary-safe (any number of peers), leaves cross-set and within-phase cascade unchanged, and is explicitly `depends_on`-agnostic (it fires whether the sibling's dependency is the implicit default or an explicit `depends_on` naming the peer). `storyboard-schema.yaml`'s `depends_on` section gains a cross-reference. Documents-only; codifies the runner behavior shipped in adcp-client#2306 (closing adcp-client#2305), root-caused in adcp#5337. No schema or wire change. - 4deb946: Enforce `cancellation_fee.rate` / `.amount` by fee `type` in `cancellation-policy.json`. Both fields are documented as conditionally required — `rate` "Required when type is 'percent_remaining'", `amount` "Required when type is 'fixed_fee'" — and the requirement is restated in the pricing-models reference, but `cancellation_fee` listed only `["type"]` in `required[]`. A validator therefore accepted `{ "type": "percent_remaining" }` (or `{ "type": "fixed_fee" }`) with no fee value at all, leaving a money-path term that declares nothing computable for a buyer accepting the product's cancellation terms. Adds `if/then` conditionals: `percent_remaining` requires `rate`, `fixed_fee` requires `amount`; `full_commitment` and `none` are unaffected. No prose change — this aligns the schema with the already-documented contract, and no existing example regresses (both doc examples already carry `rate`). Regression coverage added to `tests/composed-schema-validation.test.cjs`. - 4deb946: Clarify the boundary between `validate_input` manifest preflight and `sync_creatives` dry-run trafficking rehearsal. - 4deb946: Replace phantom error codes in creative and campaign-governance task docs with canonical enum members. `sync_creatives`, `build_creative`, the Creative Protocol specification, `check_governance`, and `sync_plans` documented 13 `errors[].code` values that do not exist in `enums/error-code.json` (`INVALID_FORMAT`, `ASSET_PROCESSING_FAILED`, `BRAND_SAFETY_VIOLATION`, `FORMAT_MISMATCH`, `CREATIVE_IN_ACTIVE_DELIVERY`, `ASSET_MISSING`, `ASSET_INVALID`, `GENERATION_FAILED`, `INVALID_MANIFEST`, `AMBIGUOUS_CHECK_TYPE`, `SELLER_NOT_RECOGNIZED`, `INVALID_PLAN`, `BUDGET_BELOW_COMMITTED`). SDKs that validate `errors[].code` against the published enum reject responses built from the docs literally, the same failure mode as #4852 and #5307. Each phantom is remapped to the existing code with matching semantics (`UNSUPPORTED_FEATURE`, `VALIDATION_ERROR`, `CREATIVE_REJECTED`, `INVALID_STATE`, `INVALID_REQUEST`, `PERMISSION_DENIED`); `GENERATION_FAILED` is replaced with guidance that generation-pipeline failures surface as task-level failure with the most specific applicable canonical code, per the open-vocabulary rule on `error-code.json`. Also fixes the one live `INVALID_FORMAT` emission in the training-agent reference implementation. Docs and reference implementation only; no wire change. - 4deb946: Fix false failures in creative compliance storyboards (canonical_supported_formats, evaluator_auth). `canonical_supported_formats`: removes the hardcoded `capability_id: "training_image_generation"` assertion (capability_id is agent-local; any valid value must pass) and the `field_absent` check on `supported_formats[1]` (agents may advertise multiple canonical formats). Fixes `context_outputs` field name from `key:` to `name:`. `evaluator_auth`: adds `requires_capability` guards to all five optional phases so agents that correctly declare `creative.supports_evaluator: false` receive `not_applicable` instead of failing the evaluator track. Guards evaluate against the raw capabilities response, bypassing a runner-side boolean-false accumulator bug. Fixes `context_outputs` field name from `key:` to `name:`. - 4deb946: Fix a misleading `get_media_buy_delivery` example that implied buyers can look up delivery by their own reference. `media_buy_ids` are seller-assigned; the top-level `buyer_ref` field was removed in 3.0.0. The example is retitled "Correlating Your Own Reference", uses seller-assigned `mb_...` IDs, and adds a note pointing buyers to reconcile their own reference via `context` echoed on `create_media_buy` / `get_media_buys`. - 4deb946: Align idempotency and rate-limit guidance with the canonical top-level `error.retry_after` field across schemas, documentation, and compliance storyboards. - 4deb946: Preserve withdrawn and unpublished release status when generating file-based schema discovery so exact artifacts remain available without becoming stable alias targets. - 4deb946: Remove an incidental video-only constraint from the inventory list targeting storyboards so single-channel sellers can exercise the channel-agnostic scenarios. ## 3.1.4 ### Patch Changes - ce93f1f: Publish 3.1.4 as the corrective successor to withdrawn 3.1.3. This restores the supported 3.1 schema and training-agent behavior to the 3.1.2 contract by removing the patch-ineligible `get_products.filters.publisher_domain` field and its incorrect plural-selector implementation. Exact 3.1.3 artifacts remain available as an immutable withdrawn release record. ## 3.1.3 ### Patch Changes - 0bfeb6a: Add `publisher_domain` filter to `get_products`: buyers can now filter products by publisher domain, returning only products whose `publisher_properties` include an exact match for the specified domain. The training agent enforces this filter at runtime, and the schema documents the expected matching semantics (exact match, no subdomain expansion). ## 3.1.2 ### Patch Changes - 49825ba: Allow `null` for video-only delivery metrics (`quartile_data`, `completion_rate`). Sellers running non-video inventory (display, audio-only, DOOH-without-video) legitimately have no value for these metrics, and returning `null` is the correct "not applicable" signal. The schema previously required `type: "number"` / `type: "object"` and rejected `null`, causing receivers to fail validation on every valid display-inventory delivery report. `delivery-metrics.json` (`totals` / `by_package[]`) now accepts `["number", "null"]` for `completion_rate` and `["object", "null"]` for `quartile_data`; `get-media-buy-delivery-response.json` `aggregated_totals.completion_rate` gets the same loosening so the aggregate path can't re-trigger the failure. The `minimum`/`maximum` constraints on `completion_rate` still apply to non-null values, and the type stays narrowed to null (no strings/arrays). Every other delivery metric continues to signal "not applicable" by omission, not `null` — this exception is scoped to the two video-only fields. Spec-loosening for the receiver contract: producers already sending numbers/objects remain valid. The separate inline `completion_rate` in `report-plan-outcome-request.json` (a governance self-report block, not on the `get_media_buy_delivery` path) is intentionally left unchanged. - 420d1e8: Move stale active-window dates in compliance fixtures and 3.0 compatibility bundles forward so storyboard runs continue to exercise protocol behavior instead of calendar drift. - da636b4: Clarify and enforce governed signal activation: `activate_signal` now documents `governance_context`, signal agents fail closed on governed accounts without a valid approval context, and signal governance compliance checks no longer require the signals tenant to own `sync_plans`. - 420d1e8: Allow governance checks to accept human approval from `ext.human_approval` and use that approval to clear reallocation-threshold human review. ## 3.1.1 ### Patch Changes - 1a18bbe: Add `media_buy.governance_aware` capability to `get-adcp-capabilities-response` and gate the `governance_denied` / `governance_denied_recovery` storyboards on it, so sellers without outbound governance consultation grade `not_applicable` instead of false-failing on a `GOVERNANCE_DENIED` they cannot produce. Addresses #5665 (Option A). - 1a18bbe: Clarify broadcast product/reporting ownership, correct the broadcast compliance channel fixture to `linear_tv`, and document that scheduled broadcast buys should not be modeled as `non_guaranteed` solely because third-party audience measurement settles later. - 1a18bbe: Fix hosted compliance auth defaults so static fixture API keys are only inferred for fixture-backed runs, align the async media-buy submitted-state fixture account with the create request, and mark governance-denial storyboards as multi-agent scenarios routed through seller and governance agents. - 1a18bbe: Clarify that `get_products` pagination is valid in all buying modes, with `brief` and `refine` pagination bounding returned `products[]` in curated results while `wholesale` pagination walks the product feed. Add conformance coverage for the deterministic wholesale cursor walk without treating brief/refine as exhaustive catalog enumeration. - 1a18bbe: Add stable schema discovery pointers at `/schemas/index.json` and `/schemas/latest.json`, mark prerelease schema directories in root discovery metadata, and keep major/minor schema aliases pointed at stable releases. - 1a18bbe: Apply residual prose cleanups to the `sponsored_context_accountability` storyboard: the `prerequisites.description` second paragraph and the `si_send_message_presentation_accepted` step narrative both still implied dynamic host-echo / different-identity substitution, contradicting the fixed Acme literal fixture the storyboard actually uses. Reword both spots to scope the prose to the static Acme fixture per @bokelley's #5551 review (2026-06-17 13:15 UTC). - 1a18bbe: TMP: provider-scoped TMPX macro trafficking with declared macro names + multi-chunk support. Builds on the `tmpx_providers` map shipped in #5689; reframes the contract to communicate exact macro/value pairs (not provider_id→token strings) and supports values that exceed a single ad-server macro slot. Provider registrations declare a stable, provider-namespaced list of ad-server macro names in `tmpx_macros` (e.g. `["PIN_TMPX_1", "PIN_TMPX_2"]`) — the names the publisher actually trafficks in GAM / VAST URLs / DOOH play logs. The identity-agent response emits `tmpx_macros[]` as ordered `{name, value}` pairs filling those slots. The router merges per-provider into `tmpx_providers: { provider_id: { macros: [{name, value}] } }` on the response so the publisher walks each provider's pairs and substitutes each `value` verbatim into the slot named by `name`. Multi-chunk values are capped at 2 per provider in v1; the cap MAY rise without a shape change. Macro names MUST NOT be derived from `provider_id` at runtime — trafficking is configured against the registered names ahead of time. **Breaking change to an experimental surface (sanctioned by `x-status: experimental`)**: `tmpx_providers` was introduced in #5689 as `Map` (opaque token per provider). It is reshaped here to `Map`. Consumers that adopted the v1 shape between #5689 and this change MUST migrate to read each provider's `macros[*].value` rather than a single string. The legacy singular `tmpx` field remains supported through 3.x (removed in 4.0). **Experimental notice window.** `docs/reference/experimental-status.mdx` recommends ~6 weeks of published notice before a breaking change to an experimental surface. `tmpx_providers` shipped in #5689 days ago, so the literal 6-week window can't apply here; in practice nobody could have adopted a field that didn't exist 6 weeks ago, so the risk is bounded. Adopters of the v1 shape (if any) should consult this changeset's migration sketch and pin to the latest schema release before deploying. **Dual-shape alias waiver.** The experimental policy asks for an alias accepting both the old (`Map`) and new (`Map`) forms "where feasible" — typically via a `oneOf` on `additionalProperties` during transition. Skipped here because the v1 shape is hours old and the surface is `x-status: experimental`; the dual-shape carrying cost (validator complexity, ambiguous consumer code paths, perpetual deprecation tail) outweighs the migration cost for a window where the field had no realistic adopters. Routers that want belt-and-suspenders compatibility for any caller that did read the v1 shape MAY mirror one provider's first-slot `value` into the deprecated singular `tmpx` field; that path is already specified. Schema updates: `tmp/provider-registration.json` adds `tmpx_macros`; `tmp/identity-match-response.json` adds `tmpx_macros` (provider-side) and reshapes `tmpx_providers` (router-merged), plus a shared `TmpxMacro` definition. Spec narrative: IdentityMatchResponse and Provider Registration field tables surface the new fields and the reshape; `§Inventory-specific behavior` walks the per-macro substitution flow; `§Identity Match fan-out` in router-architecture.mdx gets a rewritten normative TMPX-collection paragraph. - 1a18bbe: Rename the canonical Trusted Match schema source directory from `tmp` to `trusted-match`, update registry references and examples to the self-describing path, and add schema discovery metadata for protocol layers plus prerelease supersession. Hosted schema routing keeps legacy `/schemas/{version}/tmp/...` URLs working by falling back to the canonical `trusted-match` files when a historical `tmp` artifact is not present. ## 3.1.0 ### Minor Changes - e6cd62c: Creative retention contract (#2260): creatives outlast campaigns, with mandatory state-change signalling. Resolves the 3.0 ambiguity in `docs/creative/creative-libraries.mdx` ("Retention of unassigned creatives is seller-defined") without mandating a numeric retention floor that no industry platform publishes uniformly (GAM is indefinite; Meta ~37 months; FreeWheel 25 months; most others publish nothing). The protocol surface buyers actually need is observability of state changes, not a fixed number. **Library lifecycle is independent of buy lifecycle.** A creative MUST persist in the library regardless of the status of the buys that referenced it. Buy rejection, cancellation, or completion releases assignments only. This holds for `sync_creatives`, inline creatives on `create_media_buy`, and platform-native uploads — no carve-out by submission path, and no carve-out by creative composition (assets, brief, brand+catalog pointers, or any combination). **State changes are observable.** When a seller archives an unassigned creative, expires it for inactivity, or revokes a previously-approved creative, the seller MUST make the new state observable. For creatives with active assignments the signal is an `impairment` on the buy (existing mechanism from the dependency-impact cluster). For creatives with no active assignments the conformant signal today is the `status` value visible on the next `list_creatives` read — consistent with the [snapshot-and-log contract](docs/protocol/snapshot-and-log.mdx) which already names `list_creatives` as the reliable signal for resource-state changes outside an active buy. A push channel for account-scoped creative state changes is being defined under #2261; once that channel ships, sellers SHOULD additionally fire on it. **Library can be a view, not a separate store.** Sellers whose underlying ad server has no library object distinct from per-buy attachment (some CTV/podcast stacks) satisfy "creatives outlast campaigns" by exposing the buyer-synced creative through `list_creatives` for the buy's lifetime and continuing to expose its terminal state after teardown. **`creative/specification.mdx` state machine** updated to add an `approved → archived` (seller-initiated) edge, scoped to creatives without active package assignments. Sellers MUST NOT seller-archive a creative with active assignments — the existing `approved → rejected` (revocation) path with an `impairment` on the affected buy is the only conformant route when active serving is involved. The state-machine diagram and rule list both reflect the new edge. **`creative-status.json` `archived` enumDescription** widened to acknowledge that archive can be buyer- or seller-initiated, to constrain seller-initiated archive to creatives without active assignments, and to pin the conformant signal to `list_creatives` until the push channel ships. No new enum values; no new fields. Additive description-only change. Variant addressability — whether a format's rendered outputs (PMax-style fan-out, `responsive_creative`, `agent_placement`) carry per-variant IDs — is a format-level concern, handled in RFC #3305 / #3307, not a library-retention concern. Closes #2260. Refs #2261 (webhook mechanics), #2254 (parent media-buy lifecycle issue, already closed), #3305 / #3307 (format-level variant addressability). - c54c0d5: Creative-lifecycle webhooks (#2261) lands **#4582 track 3** (per-account subscription model) by making `sync_accounts` the universal account-state write surface — no new tool. Governance and notifications are separate concerns: `sync_governance` remains governance-only; `sync_accounts` carries `notification_configs[]` for webhook subscribers. **Events** - `creative.status_changed` — fires on every seller- or system-initiated status transition: `processing → rejected`, `pending_review → approved`/`rejected`, `approved → pending_review` (re-review), `approved → rejected` (post-approval revocation), `approved → archived` (seller-initiated). Buyer-initiated transitions do NOT fire — acknowledged on the `sync_creatives` response path. Payload: `creative/creative-status-changed-webhook.json`. `transition.from` is narrowed to `{processing, pending_review, approved}` — post-terminal states never appear there. - `creative.purged` — fires when a creative is destroyed (retention, takedown, advertiser request, legal erasure, account closure). Soft purges retain a tombstone on `list_creatives` for 30 days; hard purges leave no record (sanctioned Rule 4 carve-out for legal erasure). No coalescence permitted. Payload: `creative/creative-purged-webhook.json`. **Subscription model — `sync_accounts` polymorphic key** `sync_accounts` now supports two modes via `oneOf` on each per-account entry: - **Provisioning mode** — flat `brand` + `operator` + `billing` (today's shape, unchanged). Implicit-account sellers provision/upsert. - **Settings-update mode** — `account` (AccountRef) keyed by `account_id` (explicit) or natural key (implicit). No provisioning side effects. Sellers that don't implement either mode reject with `UNSUPPORTED_PROVISIONING`. Explicit-account sellers (DV360-class) gain a single focused write surface for account-level settings. Both modes accept `notification_configs[]` (replace semantics — omit to leave unchanged, `[]` to remove all). Each entry has: - `subscriber_id` (required, unique per account — no conditional required-when-multiple rules) - `url`, `event_types[]`, optional legacy `authentication`, `active` - Sellers MUST reject `event_types[]` containing media-buy-anchored types (forward rule: "any type whose contract anchors at a media buy or below") `list_accounts` echoes `notification_configs[]` per account with credentials redacted (write-only). **Governance unchanged** `sync_governance` keeps its original surface and scope. Governance agents are **not** implicitly subscribed to webhooks. A governance team that wants creative-lifecycle fires registers its URL as a separate `notification_configs[]` entry on `sync_accounts` — explicit, auditable, filterable via `event_types[]`. No foot-gun where governance endpoints get force-fed signals they aren't built to ingest. **Self-serve buyers** A buyer who wants webhooks but no governance: just `sync_accounts`. Never touches `sync_governance`. The two surfaces are independent. **New schemas** - `core/notification-config.json` — per-subscriber config. `subscriber_id` always required, `additionalProperties: false`, `ext` for extensions. - `core/account.notification_configs[]` — read-side echo on the account record. - `enums/creative-event-reason-code.json` — 13 categorical values distinct from `impairment-reason-code`: `review_passed`, `review_failure`, `processing_failure`, `seller_rereview`, `policy_revocation`, `content_drift`, `takedown_request`, `advertiser_request`, `seller_archive` (folds prior `inactivity_archive` + `storage_policy`), `account_closed`, `account_suspended`, `retention_expired` (soft only), `legal_erasure` (hard only). Each value's enumDescription documents buyer remediation; `policy_revocation` vs `content_drift` carries an explicit "when in doubt" rule. - `notification-type.json` extended with `creative.status_changed` and `creative.purged`, plus a "media-buy-anchored vs account-anchored" framing for future additions. **Read surface (#4701 track 4 adoption)** `list_creatives` adopts the 7-item `webhook_activity[]` checklist: - `include_purged: true` returns soft-purged tombstones as a wrapped `purge: { kind, at, reason_code }` block — co-presence enforced by schema, not prose. `status` is frozen pre-purge. - `include_webhook_activity: true` + `webhook_activity_limit` (1–200) return per-creative `webhook_activity[]` records. Items `$ref` the canonical `webhook-activity-record` shape from #4701; `notification_type` discriminates status changes vs purges. **Pairing with #4588 impairments** When a creative transition breaks active serving (`approved → rejected`), the seller already MUST surface a corresponding `impairment` on every media buy referencing the creative (per #4588's `impairment.coherence`). The two signals are paired but distinct — buyers correlate by `creative_id`. **No ordering guarantee between the two fires** — explicitly documented; reconcile via the snapshot. **Retroactive contract** When a seller declares support via `get_adcp_capabilities`, the obligation covers all creatives in the library — no grace period. **Docs** - `docs/accounts/tasks/sync_accounts.mdx` — new § "Two modes: provisioning vs. settings-update" and § "Account-level webhook subscriptions" - `docs/accounts/tasks/list_accounts.mdx` — `notification_configs` response field documented - `docs/creative/specification.mdx § Lifecycle webhooks` — state machine + events; supersedes previous SHOULD language; no-ordering note - `docs/creative/task-reference/list_creatives.mdx` — § Purged tombstones (with `purge` wrapper), § Webhook activity, § Buyer handler (end-to-end example: verify → dedupe → re-read) - `docs/protocol/snapshot-and-log.mdx` — account-level adopters section + Rule 4 carve-out for hard purges Closes #2261. Lands #4582 track 3. - a8aa0ab: spec(media-buy): billing authority + finality flags on both reporting surfaces (closes #2391 for 3.1; dispute task deferred to 3.2). Closes part 1 of #2391 — the prerequisite to a structured dispute task. A buyer reading the 3.1 spec can now answer "where do I look for the billing-grade number, and has it stopped moving?" without any new tasks: existing `measurement_terms.billing_measurement` already names the authoritative party; new finality flags on both reporting surfaces mark when numbers are closed for invoicing. Changes: - `static/schemas/source/media-buy/get-media-buy-delivery-response.json` — add row-level `is_final` and `finalized_at` on `media_buy_deliveries[*]` (alongside existing per-package `is_final`); add `finalized_at` on each `by_package[*]` entry next to existing `is_final`. Row-level finality is equivalent to all packages being final for the same `measurement_window`. - `static/schemas/source/account/report-usage-request.json` — add `final` (default true on absence), `finalized_at` (present iff `final: true`), and `measurement_window` to each usage record. Symmetric with seller-side delivery rows. Description updated to acknowledge sales-agent receivers for buyer-attested / vendor-attested reconciliation. - `static/schemas/source/core/measurement-terms.json` — add optional `finalization_deadline_hours` on `billing_measurement`. When the authoritative party misses the deadline, the seller MAY fall back to seller-attested numbers and the breach is handled under `makegood_policy`. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx` — replace the "AdCP 3.0 does not specify a structured dispute task" paragraph with normative "Final vs provisional" + "Who is authoritative for billing" sections; point at the new advanced-topics page; flag dispute task for 3.2. - `docs/media-buy/advanced-topics/billing-authority.mdx` — new normative page tying the pieces together with worked examples (seller-attested, buyer-3PAS, vendor-attested Nielsen). - `docs.json` — register the new page under media-buy → Concepts. Strictly additive — no existing fields change shape, no required-field additions. Agents that don't emit `is_final`/`final`/`finalized_at` remain spec-valid; the absent semantics match the 3.0 baseline. A 3.2 issue tracks the structured dispute task that builds on this foundation. - f8b51e4: spec(media-buy): extend three-shape submitted envelope to `sync_audiences`. PR #2434 established the three-shape (`success | error | submitted`) response pattern on `sync_creatives` for operations whose ingestion may be queued before per-item results can be returned. `sync_audiences` is the next natural fit — audience matching is classically asynchronous (`capabilities.audience_targeting.matching_latency_hours` already declares it), and sellers whose pipeline batches ingestion, gates the upload behind governance review, or routes through an upstream clean-room cannot return the per-audience `audiences` array before the response is emitted. `SyncAudiencesSubmitted` mirrors `SyncCreativesSubmitted` exactly: top-level `status: "submitted"` + `task_id`, optional `message`, optional advisory `errors[]`, no `audiences` array on the envelope. The synchronous success branch is tightened with the same triple-`not` guard (`errors`, `task_id`, `status: submitted`) so the three shapes are unambiguously mutually exclusive — preserving the structural parser invariant from adcp-client#649 across all three-shape `sync_*` responses. This is purely additive on the success/error arms — per-audience asynchronous matching (an audience reported with `status: "processing"` while the rest of the sync resolves synchronously) continues to belong on the synchronous success branch via the existing `audience-status` enum; the submitted envelope is the less-common operation-level async case. Files: - `static/schemas/source/media-buy/sync-audiences-response.json` — third `SyncAudiencesSubmitted` arm; success/error arms tightened to forbid `task_id` / `status: submitted` so the discriminated union is mutually exclusive. - `docs/media-buy/task-reference/sync_audiences.mdx` — `## Response shapes` documents the three branches; quick-start examples updated to discriminate `submitted` before reading `audiences`; new `## Async patterns` section names the per-audience-async vs operation-level-async distinction. - `scripts/oneof-discriminators.baseline.json` — variant count bumped to 3. `sync_accounts` and `sync_event_sources` were considered for the same treatment and deliberately left synchronous: - `sync_accounts` — per-item `action` + `status` already cover the realistic async-of-records cases; no operation-level async pattern needed. - `sync_event_sources` — deferred pending implementer input on whether seller-side validation of stream endpoints is a real latency source (filed as a follow-up RFC). Closes #2435. - cb21c9d: feat(training-agent): emit compact JWS governance_context with required plan_hash The training agent now signs the `governance_context` it returns from `check_governance` per the [AdCP JWS profile](/docs/building/by-layer/L1/security#adcp-jws-profile), replacing the opaque UUID it previously emitted. Closes #2475. **What's signed** - Compact JWS with `alg: EdDSA`, `typ: adcp-gov+jws`, and a `kid` published on the aggregated `/.well-known/brand.json` alongside per-tenant transport keys (distinct `kid`, `adcp_use: "governance-signing"`, `use: "sig"`, `key_ops: ["verify"]`). - All 13 spec claims emitted: `iss`, `sub`, `aud`, `iat`, `exp`, `jti` (UUID v7), `phase`, `caller`, `check_id`, `media_buy_id` (conditional), `policy_decisions`, `audit_log_pointer`, and the required audit-layer `plan_hash`. - Intent tokens expire in 15 minutes; execution-phase (`purchase`/`modification`/`delivery`) in 30 days. Fresh signature on every check — no caching across plan revisions. **`plan_hash` canonicalization** - `base64url_no_pad(SHA-256(JCS(plan_payload)))` with the closed bookkeeping exclusion list applied in code. - Validated bit-exactly against all 11 reference test vectors under `static/compliance/source/test-vectors/plan-hash/`. - Per-revision `planAsSupplied` is retained in `revisionHistory` so historical tokens remain auditable after a subsequent `sync_plans` mutates state. **Discovery surfaces** - `/.well-known/brand.json` now includes the governance-signing JWK. - New `/.well-known/governance-revocations.json` — signed (`typ: adcp-gov-revocation+jws`) flattened-JSON, empty by design, memoized on a 60-second cadence to prevent unbounded sign work under DoS. **Sandbox-only behavior the spec calls out** - `aud` defaults to the training agent's own sales tenant URL when `payload.target_seller` is omitted — every storyboard's downstream `create_media_buy` targets that URL, so the binding is honest for the test loop. Production governance agents MUST require buyer-supplied `target_seller` and refuse to issue without one. - When the buyer requests a non-intent phase but omits `media_buy_id`, the token is issued at `phase: intent` rather than emit a structurally-valid-but-step-12-rejected token. - Ephemeral Ed25519 keypair per process (same model as webhook-signing). KMS provisioning is the production answer; sandbox cert-track work is unblocked by the ephemeral pair. Cert-track learners can now decode the JWS header, inspect the 13 claims, and verify the signature against the published JWKS — the training agent is a usable test-vector source for the JWS profile. - c382ec0: Dependency-impact cluster (3.1): media-buy `health` + `impairments[]` surface, resource-level offline states across audience/creative/catalog-item/event-source, `impairment` notification_type, and the foundational snapshot/log protocol contract + persistent webhook contract that ties it together. Two expert review cycles incorporated. **Media buy health surface** (#2853, #2855, #2856) - New `enums/media-buy-health.json` (`ok` | `impaired`, `default: "ok"`) — orthogonal to `media-buy-status`. A paused/pending/active buy can each be impaired without affecting `status`. - New `core/impairment.json` — package-scoped dependency state change. Materiality: `package_ids` minItems: 1; MUST-strength for audience/event_source/property (cheap 1:N joins), SHOULD for creative/catalog_item (expensive pool joins). Sellers MAY report conservatively when uncertain; MUST NOT report when serving is provably unaffected. - New `enums/impairment-offline-state.json` — canonical offline values (`suspended | rejected | withdrawn | insufficient | depublished`) referenced by `impairment.transition.to`. The `resource_type` ↔ `offline_state` pairing is enforced by `impairment.coherence` (#2859), not at field validation. - New `enums/impairment-reason-code.json` — flat shared enum with per-resource-type valid subset documented in enumDescriptions. - `core/media-buy.json` adds `health` (with `default: "ok"`) and `impairments[]`. Sellers MUST add/remove entries on next sync after the underlying resource transitions, and the snapshot MUST reflect transitions within 5 minutes of `observed_at` regardless of poll cadence. - `enums/notification-type.json` adds `impairment` plus minimal factual enumDescriptions for the four pre-existing values. Webhook payload reuses the `impairment` shape plus the buy's updated `health`. **Resource-level offline states** (#2838, #2857, #2858) - `enums/audience-status.json` adds `suspended` for seller-initiated offline transitions. - `enums/creative-status.json` enumDescriptions clarify `approved → rejected` is a valid post-approval transition. - `enums/catalog-item-status.json` adds `withdrawn` for seller-initiated removal — distinct from `rejected` (no buyer-side resubmit path). - `core/event-source-health.json` clarifies `insufficient` covers source-offline; disambiguate via `events_received_24h: 0`. - Property depublication verified via brand.json / adagents.json; no per-property status field. **Webhook foundation** (#4582 tracks 1–2) - New `docs/protocol/snapshot-and-log.mdx` — five-rule contract: - **Two distinct ids** (idempotency_key per-fire; notification_id per-state-event). Same notification_id under different idempotency_keys = re-emission signal. - **Snapshot delta** per push event; no webhook-only state. - **At-least-once delivery**; snapshot is authoritative. - **Either path is complete** — buyers using webhooks reliably and buyers using only GET get the same data. Holds today for state events; partial for data events (#4590 closes the gap for delivery reporting). - **Shared id space** between push and log. - `docs/building/by-layer/L3/webhooks.mdx` "Persistent channel contract" — at-least-once, no-ordering, per-event-type coalescence (5min for general impairment, sub-minute for latency-sensitive fraud/brand-safety subclasses), replay-via-snapshot, mutability, auth renewal, termination. - `docs/media-buy/media-buys/lifecycle.mdx` documents the `health` surface, materiality coverage, reverse-direction rule, `impairment.coherence` invariant, the operational-vs-commercial non-goal, and a remediation-by-reason_code table. Additive across the board: new fields, new enum values, new docs. No breaking changes; safe in a minor release. Buyers that exhaustively switch on `media-buy-status` see no change (no new status value); buyers that read `media-buy.health` see the new dependency-health signal alongside their existing `status` handling. Refs #2838, #2853, #2855, #2856, #2857, #2858, #4582. Spin-outs: #4586 (defect signals), #4587 (advisory signals). Follow-ups: #4590 (windowed reporting pulls), #4594 (type notification_id on webhook envelope), #2859 (coherence assertion tooling), #2860 (storyboard). - c9ca76d: `impairment.coherence` — cross-resource invariant tying `media_buy.impairments[]` to the underlying resource state. **Rule (lifecycle.mdx § Compliance — expanded)** - **Forward.** Every entry in a buy's `impairments[]` MUST reference a resource whose current status is an offline state (`audience: suspended`, `creative: rejected`, `catalog_item: withdrawn`, `event_source: insufficient`, depublished property). Stale impairments on the buy fail the check. - **Inverse.** Any resource in an offline state referenced by a non-terminal buy MUST appear in that buy's `impairments[]`, and the buy's `health` MUST be `impaired` whenever `impairments[]` is non-empty (and `ok` when empty). Stale resources off the buy fail the check. - **Out of scope.** Terminal-status buys (`completed`, `canceled`, `rejected`) MAY remain unreported; materiality is schema-enforced via `impairment.json#/properties/package_ids` `minItems: 1` (#2855) and is not re-checked here. **Wiring** (`static/compliance/source/specialisms/*/index.yaml`) - Added `impairment.coherence` alongside the existing `status.monotonic` invariant on the five specialisms whose storyboards exercise resource transitions that can drive impairments: - `audience-sync` — audience `suspended` - `sales-catalog-driven` — catalog_item `withdrawn` - `creative-ad-server`, `creative-template`, `creative-generative` — creative `rejected` - Each specialism's invariants block now carries an inline comment describing the cross-resource rule and the not-applicable grading path until #2860 lands the storyboard exercise. **Docs** - `docs/media-buy/media-buys/lifecycle.mdx § Compliance` — replaces the two-bullet sketch with the precise forward/inverse rules, out-of-scope carve-outs, and the relationship to `status.monotonic`. - `docs/building/verification/compliance-catalog.mdx` — new **Cross-resource invariants** section catalogs `status.monotonic` and `impairment.coherence` with scope and per-specialism applicability. Complements `status.monotonic` (single-resource lifecycle observation). Grades `not_applicable` until [#2860](https://github.com/adcontextprotocol/adcp/issues/2860) wires the cross-resource exercise into the relevant specialism storyboards. Additive — new invariant on existing specialisms, no breaking changes. Runner support for the `impairment.coherence` invariant ID is the adcp-client follow-up (mirrors the `status.monotonic` rollout pattern from #2664). Closes #2859. - f2364d9: Dependency-impact end-to-end storyboard (`media_buy_seller/dependency_impairment`) — the cross-resource exercise that drives non-NA grading of the `impairment.coherence` invariant ([adcp#2859](https://github.com/adcontextprotocol/adcp/issues/2859)). Five phases against the compliance test controller's sandbox: 1. **setup** — discover a product, create an active media buy, sync a creative with an inline assignment, and force the creative to `approved` for a clean baseline. 2. **baseline_healthy** — `get_media_buys` MUST report `health: ok` with empty/absent `impairments[]`. 3. **transition_offline** — `comply_test_controller force_creative_status` flips the creative to `rejected` with a rejection reason. 4. **verify_impaired** — `get_media_buys` MUST report `health: impaired` with an `impairments[]` entry whose `resource_type: creative`, `resource_id` matches, `package_ids` includes the buy's package, and `transition.to: rejected`. Closes the forward + inverse rules for this transition. 5. **recover_and_verify** — flips the creative back to `approved` and reads the buy again; `health` MUST return to `ok` and `impairments[]` MUST be empty. Exercises the biconditional both directions — a seller that leaves stale impairments behind fails this phase and the runner invariant. Wired into `protocols/media-buy/index.yaml#requires_scenarios` so every media-buy seller storyboard run grades it. Sellers that don't expose `comply_test_controller force_creative_status` grade `not_applicable` rather than fail. Creative-track only today. Audience-track and catalog-track variants are follow-ups pending `force_audience_status` / `force_catalog_item_status` support in the compliance test controller. Closes #2860. - 55bf2fa: spec(creative): add `bills_through_adcp` capability + `BILLING_OUT_OF_BAND` error. PR #2879 softened the creative-ad-server conformance so ad servers that bill out of band (flat license, SaaS contract, bundled enterprise — CM360 is the canonical case) stay spec-valid without returning `pricing_options`. Two follow-ups close that loop on the wire: - `capabilities.creative.bills_through_adcp` (boolean, default false/absent) on the `get_adcp_capabilities` response — a pre-call discriminator so buyer agents can pre-filter creative agents across a portfolio before establishing an account just to probe pricing. When `true`, buyers can expect `pricing_options` on `list_creatives`, `pricing_option_id`/`vendor_cost` on `build_creative`, and `report_usage` that accepts records against the rate card. - `BILLING_OUT_OF_BAND` (recovery: terminal) on the error-code enum — the standard code for a per-record `report_usage` rejection where the record is well-formed but the account bills via a non-AdCP channel. Distinct from `BILLING_NOT_SUPPORTED` (media-buy `billing`-value rejection) and `BILLING_NOT_PERMITTED_FOR_AGENT` (per-buyer-agent commercial gate) — signals that the entire billing surface is offline for this account, not that a specific value or caller is rejected. The code itself is the discriminator; no `error.details` shape is defined (mirroring `CONFIGURATION_ERROR`). Strictly additive — no existing agents break. Agents that don't declare `bills_through_adcp` remain in the probe-to-discover mode buyers already tolerate. Both follow `held-for-next-minor` / 3.1 on the drift registry. Closes #2881, #2882. Builds on #2879. Files: - `static/schemas/source/protocol/get-adcp-capabilities-response.json` — `bills_through_adcp` added to the `creative` block alongside `has_creative_library` / `supports_generation` / `supports_transformation` / `supports_compliance`. - `static/schemas/source/enums/error-code.json` — `BILLING_OUT_OF_BAND` in enum, `enumDescriptions`, and `enumMetadata`. - `scripts/error-code-drift-dispositions.json` — `held-for-next-minor` / `3.1` entry. - `specs/creative-agent-pricing.md` — pre-account-discovery and capabilities-change sections updated. - `static/compliance/source/specialisms/creative-ad-server/index.yaml` — `report_usage` narrative references the standard code (replaces "vendor codes are fine today" placeholder). - `docs/protocol/get_adcp_capabilities.mdx` — capability table row + example. - 4c12454: spec(envelope): normalize MCP envelope serialization (flat root, drop `payload.required`, `context` joins envelope). `core/protocol-envelope.json` declared `required: [status, payload]` with `payload` as a nested object, but every shipping SDK (`@adcp/client`) emits the flat MCP shape — envelope fields and body fields as siblings at the root, no nested `payload:` key. Task response schemas like `media-buy/get-products-response.json` declared body fields at the root, not under `payload`. The schema's literal reading contradicted the deployed reality. Two prior triage rounds (2026-04-23, two separate sessions) converged on the same call: ratify the flat-on-the-wire behavior, add `context` as a first-class envelope field distinct from `context_id`, and drop the `payload.required` constraint. Going with that resolution: - **`payload.required` dropped.** `payload` becomes a documentary grouping construct, NOT a required wire key. The schema's `required:` is now empty (the `not` block rejecting legacy `task_status` / `response_status` stays). Per-transport serialization is normative in `notes`: - **MCP**: envelope fields and body fields are siblings at the root of the tool response. No nested `payload:` key. Matches MCP's `structuredContent` convention. - **A2A**: envelope fields map to transport-native task metadata (`task.status.state`, `task.contextId`, `task.id`); body fields appear inside `task.artifacts[0].parts[].DataPart` (final) or `task.status.message.parts[].DataPart` (interim). - **REST**: envelope fields MAY ride headers or body siblings; body fields appear at the JSON body root. - **`context` joins the envelope as a first-class field**, `$ref` to `core/context.json`. Semantically orthogonal to `context_id`: - `context_id` — server-managed session identifier. - `context` — caller-supplied opaque echo, preserved byte-for-byte by the agent. - Both MAY appear on the same response; they are NOT aliases. - **`description` rewritten** to lead with the canonical-field-set framing rather than the "wraps the payload" mental model the old text used (which encouraged the nested-`payload` misreading). Producer and receiver rules added to `docs/building/by-layer/L0/mcp-guide.mdx` so the wire shape is normative from both ends: - MCP tool implementations MUST emit envelope and body fields as flat siblings at root. - MCP tool consumers MUST parse from the flat root; receivers MUST NOT require a nested `payload:` key. - `context_id` vs `context` distinction surfaced with one-line definitions and the "both may appear" clause. Why this resolution over "make nested canonical and migrate `@adcp/client`": - The flat shape is what every shipping integrator has parsed against since 3.0 GA. Declaring it non-conformant before any peer SDK ships inverts the codify-deployed-behavior precedent the ecosystem already follows (OpenRTB, prebid, GAM). - MCP's native conventions favor flat — `structuredContent` is itself a flat field; nesting `payload:` inside it is ceremonial boilerplate. - A2A's transport-native task metadata already carries the envelope fields; nesting `payload:` would force redundant double-wrapping. Why `context` joins as a peer of `context_id` rather than a convention: - `get-products-response.json:147` already `$ref`s `core/context.json` for per-request echo. The convention is in use; it just never made it into the envelope doc. - Splitting on `_id` (session identifier) vs `context` (per-request echo) is the same split A2A makes between `task.contextId` and `task.metadata`; not codifying it leaves the spec less expressive than the transports it runs over. Files: - `static/schemas/source/core/protocol-envelope.json` — description rewritten; `context` added; `payload` description clarified as documentary grouping; `required: [status, payload]` removed; `notes` array rewritten with normative per-transport serialization. - `docs/building/by-layer/L0/mcp-guide.mdx` — `## MCP Response Format` section rewritten with normative producer + receiver rules and the `context_id` / `context` distinction. Validation: `composed-schema-validation.test.cjs` (43 tests) passes against the changed envelope. Existing SDKs (`@adcp/client`) remain conformant. Closes #2911. Unblocks adcp-client#832 (per-field envelope validation). - 037e21b: spec(3.1): pre-GA clarifications batch #2 — five spec/docs items. Five issues from the 3.1.0 milestone Cluster B + C work, plus four closed as already-shipped on inspection. **Shipped in this batch:** - **#4453 — `expires_at` optional on `preview-creative-response.json`.** Removed from `required` on all three branches (top-level + nested batch entries + variant branch); description updated to document the non-expiring case. Buyers MUST treat URLs as invalid after `expires_at` when present, MAY assume valid until out-of-band revocation when omitted. Note: AdCP 3.x has no protocol-level revocation signal — buyers requiring expiry guarantees SHOULD require sellers that publish `expires_at`. - **#4567 — `account.account_financials` description sharpened as pre-call discriminator.** The field already existed at `protocol/get-adcp-capabilities-response.json:166`; description rewritten to make the buyer's pre-call-discriminator purpose explicit and to surface the companion-pattern relationship with `creative.bills_through_adcp`. No schema change; closing the issue with the rewrite as the answer. - **#4578 — Version inference when `get_adcp_capabilities` is absent.** New paragraph in `versioning.mdx` § Bidirectional negotiation: buyers SHOULD infer v2 when the tool itself isn't on the seller's tool list, route through the v2 wire-shape adapter, emit a one-time advisory warning that retry-safety guarantees are unknown. Fail-open by design — failing closed blocks the most common adoption path (sellers that shipped v2 and never implemented v3 discovery). Buyers MUST NOT use absence as a positive v2 conformance signal; idempotency / signed-requests / other v3 trust primitives MUST be treated as unknown and gated at the application layer. - **#4584 — `get_creative_delivery` pagination field-name normalization.** Added `total_count` (canonical, matches `PaginationResponse.total_count`) to the inline pagination block; marked `total` as deprecated alias with `deprecated: true`, removed in AdCP 4.0. Sellers populate both identically through 3.x; buyers SHOULD prefer `total_count`. Page-based pagination shape (`limit`/`offset`) retained — full migration to cursor-based `PaginationResponse` is a 4.0 candidate, not a 3.1 minor change. Description on the `pagination` block calls out the divergence and the migration timeline. - **#3049 — Canonical rejection-set shape on `errors[].details`.** New SHOULD-level guidance under `core/error.json` `details` description: when reporting a rejected value against a closed accepted set, sellers SHOULD use `details.accepted_values` (array) + optional `details.rejected_value` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root). `details` remains `additionalProperties: true` — pre-3.1 sellers using legacy keys remain conformant. Safety carve-out: sellers MUST NOT enumerate ecosystem-wide accepted sets on a per-caller rejection (turns the error into an enumeration oracle). SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what 3.1+ adopters should emit. - **#4592 — Sponsored Placement adapter-contract docs.** New doc page at `docs/creative/sponsored-placement-adapter-contracts.mdx` documenting the four runtime contract families that ship under the single `sponsored_placement` canonical (Amazon SP buyer-uploaded, Criteo/CitrusAd network-composed, Pinterest/Snap Collection layout-per-impression, generative-per-SKU). Documents the catalog-asset contract, tracking vocabulary, adopter quirks, and experimental-readiness per family. Linked from `canonical-formats.mdx` experimental-canonicals table. Not a spec extension; documents the variability buyers and sellers encounter against the canonical so the evidence-based promotion gate is informed. **Closed as already-shipped (no commit needed, will be closed via PR comments):** - **#4400** — `start_time`/`end_time` asymmetry. The asymmetry is intentional at the spec level (you can `start` asap; you can't `end` asap — "end asap" means "cancel"). The structured-object form (`{type: "asap"}`) the issue's seller adopted is a non-spec extension; the spec is and remains string-only at `core/start-timing.json`. If WG wants to canonicalize the structured form for forward-extensibility, file an RFC; not a clarification. - **#3555** — `pushNotificationConfig.url` port semantics. Already shipped: `core/push-notification-config.json:9-11` description plus `security.mdx:113-119` "Destination port: permissive by default" both exist with the unconstrained-by-default guidance. - **#4466** — adagents.json `authorization_type` doc. Already shipped: `docs/governance/property/adagents.mdx:166` reads `*(required)*`. - **#4574** — `list_authorized_properties` cleanup (and comment-expanded `list_audiences` / `list_targeting_categories`). Already shipped in main: `get_adcp_capabilities.mdx:957` has the migration section; `whats-new-in-v3.mdx` and `release-notes.mdx` carry the migration tables. The expanded-scope cleanup is implicit — `list_audiences` and `list_targeting_categories` have zero upstream references in `static/schemas/source/` or `static/compliance/source/`. - **#4713** — 3.1 version negotiation docs surface. Already shipped in main: `whats-new-in-v3.mdx:346-348` covers version negotiation; `a2a-guide.mdx:912` and `mcp-guide.mdx:825` both updated for the release-precision contract. Files: - `static/schemas/source/creative/preview-creative-response.json` — `expires_at` optional on three branches, description updated - `static/schemas/source/protocol/get-adcp-capabilities-response.json` — `account.account_financials` description sharpened - `static/schemas/source/creative/get-creative-delivery-response.json` — `total_count` canonical + `total` deprecated alias - `static/schemas/source/core/error.json` — `details` description gains canonical rejection-set shape SHOULD-guidance - `docs/reference/versioning.mdx` — new paragraph on absence-of-`get_adcp_capabilities` v2 inference - `docs/creative/sponsored-placement-adapter-contracts.mdx` — new doc page (four contract families) - `docs/creative/canonical-formats.mdx` — link to the new adapter-contracts page Closes #4453, #4567, #4578, #4584, #3049, #4592. Closes #4400, #3555, #4466, #4574, #4713 (no code change; see PR comments). - b1a45e6: spec(3.1): pre-GA clarifications batch #3 — per-format error attribution on `build_creative` + sales-guaranteed submitted-vs-sync contract. Two real spec clarifications surfaced during the 3.1 cluster work. **#4556 — Per-format error attribution on `BuildCreativeError`.** The multi-format `build_creative` contract is **atomic** (already documented on `BuildCreativeMultiSuccess`: "all formats must succeed or the entire request fails") — so the issue's framing of "partial success with some manifests + some errors" is non-conformant. What the spec was missing is the per-format attribution convention on the error response, so buyers can identify _which_ format(s) caused the batch to fail and retry only the failing subset. Added normative guidance on `BuildCreativeError.errors[]`: - `error.field` carries `target_format_ids[N]` (zero-based index) — required when the error is format-scoped, mirrors the JSONPath-lite convention used elsewhere - `error.details.format_id` carries the resolved `format_id` value — required when the error is format-scoped, lets buyers dispatch on format identity without re-parsing `field` - Whole-batch errors (auth, governance denial, transport-level) MAY omit both - Sellers SHOULD emit one error per failing format rather than collapsing — keeps per-format recovery routing unambiguous - Per-format `correctable` errors are scoped to the named format only; buyers may retry just that format with corrected input This is the spec-level diagnostic surface for the agentic self-correction loop the issue identifies — the atomicity rule stays, but buyers no longer have to retry the whole batch to figure out which format failed. **#3822 — Sales-guaranteed submitted-vs-sync contract.** The skill ↔ storyboard contradiction surfaced during matrix-blind fixture runs: an SDK skill in adcp-client (`build-seller-agent`) instructed sales-guaranteed agents to return a task envelope for every `create_media_buy`. The `sales_guaranteed` compliance storyboard runs **multiple** create_media_buy paths and only one expects `submitted` — four shared scenarios (measurement_terms_rejected, pending_creatives_to_start, inventory_list_targeting, invalid_transitions) expect synchronous `media_buy_id` returns against the non-guaranteed fixture products listed first in the storyboard. A blind agent following the skill fails 5 of 5 grader steps. Resolution at the spec layer: added a `### When to return Submitted vs synchronous Success (normative)` section to `docs/media-buy/task-reference/create_media_buy.mdx` documenting that the choice is **per-call**, driven by per-product `delivery_type` + the seller's `requires_io_approval` capability — not a uniform per-seller rule. Conformant SDK skills MUST NOT instruct agents to return `submitted` for every `create_media_buy` regardless of input. Cross-references the `sales-guaranteed` specialism storyboard fixture pattern (non-guaranteed products listed first so open-brief `get_products` calls resolve to synchronous-create paths). Names the issue explicitly so future readers / future SDK skill audits land on the correct contract. The SDK skill itself lives in adcp-client and will need a follow-up fix there; this PR closes the spec-side ambiguity that allowed the bad skill to ship. Files: - `static/schemas/source/media-buy/build-creative-response.json` — `BuildCreativeError` description + `errors` field gain per-format attribution convention - `docs/media-buy/task-reference/create_media_buy.mdx` — new "When to return Submitted vs synchronous Success" section after the Submitted Response shape Closes #4556. Refs #3822 (spec-side resolution; SDK-side skill fix tracked in adcp-client). - c2a8855: Grader: webhook-emission universal now fails agents that haven't published a 9421 webhook-signing JWKS at their `brand.json` `agents[].jwks_uri`. The `signature_validity` phase is required (no longer `optional` / `skip_if hmac_legacy`), and a new `signing_keys_published` precheck phase asserts the JWKS contains a key with `adcp_use: "webhook-signing"` before the signature phase runs. Closes the on-ramp loophole that previously let agents self-declare themselves out of webhook signing via `webhook_auth_mode == 'hmac_legacy'`. Operationalizes the "no new HMAC implementers after date X" enforcement from the RFC 9421 migration plan (#4205). New error codes on `signing_keys_published`: `webhook_signing_keys_unpublished` (no JWKS or empty), `webhook_signing_keys_wrong_purpose` (JWKS present but no key with `adcp_use: "webhook-signing"`), `webhook_signing_keys_all_revoked` (all webhook-signing keys revoked). Refs #3360, #4205. - 64ca807: MCP webhook `operation_id` is now the canonical, normative correlation identifier; URL-path parsing is forbidden ([adcp#3554](https://github.com/adcontextprotocol/adcp/issues/3554)). Two ambiguities in 3.0 made cross-implementation interop fragile: 1. The `mcp-webhook-payload.json` description told publishers to "echo" `operation_id` back from the URL but never specified the URL-extraction convention (path segment? query parameter? template?), and the field was not in `required` — so a conformant publisher could legally omit it. 2. `docs/building/by-layer/L0/mcp-guide.mdx` marked `task_type` and `operation_id` as **deprecated** in favor of URL-path routing, directly contradicting `webhooks.mdx` (which correctly told receivers not to parse the URL) and the actual server implementation. Resolution — every comparable async-notification protocol in ad tech (OpenRTB `nurl`/`burl`, VAST tracking pixels, A2A `PushNotificationConfig`) makes the URL opaque to the entity firing the HTTP call; AdCP now matches that precedent. **Normative wire contract:** - `operation_id` is now **required** in `mcp-webhook-payload.json`. - `push-notification-config.json` gains an optional `operation_id` field as the canonical buyer→seller registration channel. Sellers MAY reject registrations without it via `INVALID_REQUEST`. - Buyers SHOULD supply `operation_id` via `push_notification_config.operation_id` and SHOULD generate a unique value per task invocation. Buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server. - Sellers MUST echo the buyer-supplied `operation_id` verbatim into every webhook payload. Sellers MUST NOT derive `operation_id` by parsing the URL; the URL structure is implementation-defined and opaque to the seller. - Receivers MUST correlate webhooks using the payload field, never URL-path inspection. Buyer-side URL conventions (path templates, query parameters, opaque tokens) are routing aids for the buyer's HTTP server only. Updated alongside: - `docs/building/by-layer/L3/webhooks.mdx#operation-ids-and-url-templates` carries the full normative wire contract. - `docs/building/by-layer/L0/mcp-guide.mdx` field-listing updated; broken `#best-practice-url-based-routing` anchor removed; deprecated-fields framing replaced with the canonical position. - `docs/building/by-layer/L0/a2a-guide.mdx` "URL-Based Routing" best-practice section rewritten — A2A receivers correlate the same way as MCP receivers (payload field, never URL parsing). Closes the cross-protocol consistency gap a contributor would otherwise hit when reading the two L0 guides side-by-side. - Training-agent webhook emitter (`server/src/training-agent/webhooks.ts`) extracts the buyer-supplied `operation_id` from `push_notification_config.operation_id` and echoes it on the wire, with `task_id` as a fallback when the buyer didn't supply one. The seller-side principal-scoped string (used to key the idempotency-key store) is renamed `deriveWebhookIdempotencyScope` and is never placed on the wire. - Test vectors at `static/test-vectors/webhook-payload-extraction.json` updated to satisfy the tightened payload schema. Closes #3554. - 64ca807: `pushNotificationConfig.url` port semantics: declare unconstrained by default ([adcp#3555](https://github.com/adcontextprotocol/adcp/issues/3555)). The 3.0 spec was silent on whether publishers may restrict destination ports on buyer-supplied webhook URLs, leaving SDK authors to choose between two bad defaults: lock to `{443, 8443}` (silently rejects buyers on Tomcat `:9443`, Spring Boot `:4443`, path-routed multi-tenant gateways) or accept any port (weakens defense-in-depth). Resolution — the SSRF guard the protocol relies on is the **IP-range check + DNS-rebinding-resistant connect pin** already defined in `security.mdx#webhook-url-validation-ssrf`, not port filtering. Reserved-range checks cover the realistic threat (smuggling traffic to internal services on `10.0.0.0/8`, `127.0.0.0/8`, `169.254.169.254`); port filtering on top of a routable public IP is a marginal defense whose cost (rejecting conformant buyers) typically exceeds its benefit. **Normative position** (now stated in `docs/building/by-layer/L1/security.mdx#destination-port-permissive-by-default`): - Publishers SHOULD NOT enforce a destination-port allowlist on counterparty-supplied URLs by default. The URL contract is `format: "uri"` only; the protocol does not constrain ports. - Operators who want a hardened destination-port allowlist as defense-in-depth (locked-down enterprise egress) opt in explicitly via SDK or deployment configuration, with `{443, 8443}` as a reasonable hardened-mode starting point. - SDKs that ship a `DEFAULT_ALLOWED_PORTS` constant MUST default it to "no restriction" and surface `{443, 8443}` as an opt-in profile, never as a default. - Sellers that activate hardened mode MUST document the allowed-port set in their operator-facing documentation. Schema description in `push-notification-config.json` updated to point at the security-doc section; normative SHOULD NOT lives in `security.mdx` (the right home for SSRF-class guidance) rather than in the schema description field. Surfaced by Python SDK foundation audit on `adcp-client-python#297`, which exports `adcp.signing.DEFAULT_ALLOWED_PORTS = {443, 8443}` as opt-in hardening aligned with this recommendation. Closes #3555. - 4c12454: spec(errors): add `PROPOSAL_NOT_FOUND` to the canonical error catalog. Counterpart to existing `PROPOSAL_EXPIRED` (known proposal whose `expires_at` window has passed) and `PROPOSAL_NOT_COMMITTED` (known proposal still in `draft`). `PROPOSAL_NOT_FOUND` covers the third proposal-lifecycle failure mode: the seller doesn't recognize the `proposal_id` at all — never finalized, belongs to a different tenant, or evicted from session cache before consumption. Without this code, sellers had to reuse `INVALID_REQUEST` (loses semantics, wrong recovery class) or invent local codes (no cross-SDK consistency). The Python SDK's v1.5 ProposalManager (adcp-client-python#538) was shipping `PROPOSAL_NOT_FOUND` via its `KNOWN_NON_SPEC_CODES` allowlist as a stopgap, same pattern as `CONFIGURATION_ERROR` from #3995. Recovery: `correctable` — buyer should re-issue `get_products` with `buying_mode: 'refine'` + `action: 'finalize'` to obtain a current `proposal_id`, then retry `create_media_buy`. Files: - `static/schemas/source/enums/error-code.json` — code added to `enum`, `enumDescriptions`, and `enumMetadata` (recovery + suggestion) per the three-parallel-structures convention. - `scripts/error-code-drift-dispositions.json` — `held-for-next-minor` for target_version `3.1` (PROPOSAL_EXPIRED / PROPOSAL_NOT_COMMITTED are already on 3.0.x; PROPOSAL_NOT_FOUND is the new AHEAD code). - `docs/media-buy/task-reference/get_products.mdx`, `docs/media-buy/product-discovery/refinement.mdx`, `docs/building/by-layer/L3/error-handling.mdx`, `docs/building/operating/transport-errors.mdx` — error-table rows alongside `PROPOSAL_EXPIRED`. Closes #4043. - 4c12454: spec(media-buy): clarify finalize-exclusivity and multi-finalize atomicity in `get_products` `refine[]`. The 3.0.6 spec allows multiple `refine[]` entries and matches `refinement_applied[]` by position, but was silent on what a seller does when one entry has `action: 'finalize'` and others don't. Two adopting SDKs (`adcp-client`, `adcp-client-python`) settled on "process the first finalize; silently drop the rest" — undocumented, divergent across wrappers, and inconsistent with the existing `proposal_finalize` compliance scenario which keeps refine and finalize on separate steps. The conformance harness couldn't enforce a contract because the spec hadn't picked one. Picked **option (a) — finalize is exclusive within `refine[]`** with explicit multi-finalize atomicity: - If any entry has `action: 'finalize'`, **all** entries in the array MUST be proposal-scoped finalize entries. Mixing finalize with `include` / `omit` or with request- / product-scoped entries MUST be rejected with `INVALID_REQUEST`. - Multi-finalize against different `proposal_id`s in one call is allowed and MUST be **atomic** — all proposals commit or none do; partial commits are non-conformant. Sellers that cannot guarantee atomic multi-proposal commit MUST reject multi-finalize arrays with `INVALID_REQUEST` and name the constraint in `error.message`. - No capability flag for multi-finalize — the failure response is the discovery surface, so buyers MUST NOT assume support without a successful first attempt. Why (a) over (b) "finalize-with-ordered-refinement" or (c) "implementation-defined": - (a) matches the existing `proposal_finalize.yaml` compliance scenario, which already separates refine and finalize into distinct phases (`refine_proposal` has no finalize; `finalize_proposal` has only finalize). - (b) introduces ordering + partial-failure semantics across mixed entries, expanding the seller state machine for no buyer-side win (the buyer who wants both can sequence the calls trivially). - (c) leaves divergent SDK behavior in the field and is exactly the gap this issue asks to close. Files: - `static/schemas/source/media-buy/get-products-request.json` — `refine` field description gains the finalize-exclusivity and multi-finalize atomicity contract. `action.finalize` enum description cross-references the array-level rule. - `docs/media-buy/product-discovery/refinement.mdx` — new `## Finalize is exclusive within refine[]` section before `## Proposals in refine mode` with ✅/❌ examples and the multi-finalize atomicity contract. SDK alignment: `adcp-client`'s `detectFinalizeAction` and `adcp-client-python`'s `detect_finalize_action` should reject mixed arrays at the SDK layer rather than silently dropping non-finalize siblings; tracked separately in those repos. Closes #4107. - 4c12454: spec(media-buy): disambiguate `pending_creatives` status description. Sharpens the enum description for `media-buy-status.pending_creatives` to remove the ambiguity raised in #4196 (readers interpreting the name as "waiting for publisher/governance approval" rather than the intended "buyer-side creative submission missing"). Document, don't rename. The wire churn of renaming the enum value isn't worth the marginal clarity gain — the existing description already named the buyer action, and `pending_X` is a consistent naming convention across the enum (`pending_start` follows the same shape: "phase X is next required", not "X is pending approval"). Renaming would force every downstream SDK, dashboard, storyboard fixture, and seller-side state machine to migrate for what is fundamentally a documentation gap. The new description leads with **"Buyer-side action required"**, explicitly contrasts with publisher/governance approval flows ("the seller has already accepted the buy"), and names the convention so readers can apply the same parse to `pending_start` without filing a follow-up issue. Closes #4196. - 4c12454: spec(errors): make `error.code` forward-compatible decoding normative. The drift lint shipped in #4221 enforces a strict policy: adding a new code to `error-code.json` is a wire change held to the next minor, because a 3.0.x receiver decoding a 3.1 sender's `error.code` has no contract that says it must accept the unknown value. The closed-enum hazard was prose-level in `error-code.json` ("agents MUST handle unknown codes gracefully by falling back to the recovery classification") but not surfaced as a normative receiver rule in the spec body — strict validators reading `core/error.json` would have rejected unknown codes anyway. `core/error.json` already types `error.code` as `string` (not as a closed enum reference), so the wire was already open at the envelope level; what was missing was the receiver contract that says so explicitly, and the sender contract that says `error.recovery` is the normative carrier across version skew. This change makes that explicit: - **Receivers MUST decode unknown codes**, recover the recovery class from `error.recovery`, and default to `transient` when `recovery` is absent (matches the manifest's `error_code_policy.default_unknown_recovery`). - **Senders MAY emit codes outside the receiver's pinned vocabulary** — newer codes, platform-specific codes — and MUST populate `error.recovery` on every error from 3.1 onward so receivers across version skew can classify reliably. - **`error.recovery` is the normative wire carrier**; `enumMetadata.recovery` in `error-code.json` is the documentary mirror for known codes. 3.0.x policy unchanged — 3.0.x receivers predate this rule, so 3.0.x stays wire-stable for the rest of its support window. From 3.1 onward, future maintenance lines can ship new codes additively (3.1.5 adds a code; 3.1.0 receivers handle it via `error.recovery`) instead of every code being held to the next minor. Files: - `static/schemas/source/core/error.json` — `error.code` description elevated from "agents MUST handle unknown codes" prose to a wire-level rule pointing at `error-handling.mdx#forward-compatible-decoding-normative`. `error.recovery` description states it as the normative carrier across version skew. - `docs/building/by-layer/L3/error-handling.mdx` — new `### Forward-compatible decoding (normative)` section under `## Standard Error Codes` with the full receiver / sender / `error.recovery` contract and the "why this matters" / "3.0.x policy unchanged" carve-outs. Best-practice list item updated to point at the new section. Refs #4227. Pairs with #3725 / #3738 (`enumMetadata.recovery`) and #4221 (the drift lint). - 9d056d3: Buyer-side webhook delivery visibility for AdCP 3.1, landing #4278 alongside #4582 track 4 (standardized log surface). Two new request fields, one new response field, two new shared core schemas, and the canonical pattern documentation that future resources will follow. ### Request additions (`get-media-buys-request.json`) - `include_webhook_activity` (boolean, default `false`) — when true, each returned media buy MAY include a `webhook_activity` array describing recent reporting and health webhook fires for the calling principal. - `webhook_activity_limit` (integer, 1–200, default 50) — per-buy cap on returned records, most-recent first. The two request-field names are now the **canonical opt-in convention** for any AdCP resource exposing `webhook_activity[]` (see snapshot-and-log.mdx § Webhook activity log pattern). ### Response addition (`get-media-buys-response.json#/properties/media_buys/items`) - `webhook_activity[]` — `$ref`s the new canonical record at `/schemas/core/webhook-activity-record.json`. ### New shared core schemas (#4582 track 4) - **`/schemas/core/webhook-activity-record.json`** — canonical record shape for a single webhook delivery attempt, intended to be `$ref`'d from any resource read that surfaces a `webhook_activity[]` log. Fields: `idempotency_key` (equals the payload's dedup key — no parallel `delivery_id`), `subscriber_id` (reserved for multi-subscriber configurations; precedent #3009), `fired_at`, `completed_at`, `notification_type` (refs the shared notification-type enum; adopters MUST add their types to that registry rather than minting a parallel enum), `sequence_number`, `attempt` (one record per attempt), `status` (`success` / `failed` / `timeout` / `connection_error` / `pending`), `url` (query+fragment stripped, secret-shaped path segments SHOULD be redacted), `http_status_code`, `response_time_ms`, `payload_size_bytes`, `error_message` (server-side classification only — never bodies or headers), and `ext` (resource-specific extension envelope per the standard AdCP pattern). Nullable fields use the draft-07 union-type idiom (`"type": ["string", "null"]` etc.); the spec's `nullable: true` OpenAPI shorthand is not part of draft-07 and is not used. Top-level `additionalProperties: false` — resource-specific extensions go on `ext`, not as ad-hoc top-level fields. This is a **deliberate departure** from the surrounding convention (every other core schema with an `ext` slot uses `additionalProperties: true`) and is the structural enforcement of the "uniform across resources" promise that justifies the hoist; future schema reviewers should not "fix" it back to `true`. - **`/schemas/core/truncation-sentinel.json`** — universal AdCP sentinel for fields whose content has been truncated due to a size cap. Shape: `{ "_truncation": { "original_size_bytes": N, "preview": "...", "preview_format": "" } }`. The leading-underscore `_truncation` key is the discriminator — receivers detect a sentinel by testing `'_truncation' in value`, no redundant boolean. `_truncation.additionalProperties: true` so future revisions can add classification fields without a forward-compat break. `preview_format` is an open string with `text` / `json` / `base64` / `xml` / `html` listed as common values; receivers SHOULD treat unknown values as `text`. The description carries the canonical `oneOf` usage example so the first real consumer doesn't reinvent the discriminator convention. Lands now so future RFCs (notably the `include_webhook_payloads` extension) plug into a shared shape; no field uses it today. ### Normative rules (#4582 track 4) - **Retention is MUST, not SHOULD.** Sellers that surface `webhook_activity[]` MUST retain records for at least 30 days from each record's `completed_at`. For records still in `pending` status the clock runs from `fired_at` until the attempt terminates and then resets to 30 days from `completed_at` — so retry trails do not age out mid-flight. Sellers that cannot honor the floor MUST omit the field entirely rather than return a shorter window. This gives buyers a single retention guarantee they can build debug tooling against, and gives sellers with thin storage a clean opt-out via the three-state presence semantics rather than per-seller-negotiated floors. Resolves #4278 open question. - **Scoping** MUST be calling-principal only even when multiple principals share visibility into the same resource via account-level access. - **One record per attempt.** Single-attempt successes appear as a single record with `attempt: 1`; retry trails appear as multiple records sharing `idempotency_key`. - **Three-state presence.** Field omitted = seller does not surface (no persistence, OR capability surface excludes the relevant webhook channel, OR no registered endpoint for the principal); `[]` = persists but no recent fires; non-empty = actual records. Sellers MUST NOT collapse states. - **URL privacy.** Query string and fragment MUST be stripped. Sellers SHOULD redact path segments matching obvious secret patterns (high-entropy random material, UUID / token shapes). - **`error_message` privacy.** Server-side classification string only — never request headers, response bodies, or buyer-endpoint stack traces. ### Documentation - New normative section **`docs/protocol/snapshot-and-log.mdx` § Webhook activity log pattern** — names the canonical record, the two request-field conventions, scoping, retention floor, three-state presence semantics, record cardinality, and privacy rules. Includes an explicit **8-item adoption checklist** so future resources have unambiguous MUST hooks. Item 1 is the **notification-channel prerequisite**: adoption requires a registered notification channel for the relevant fire types — per-buy `push_notification_config` (existing) for buy-scoped resources, or the per-account subscription model from #4582 track 3 for resources that outlive a buy. The two are different primitives that fulfill the same prerequisite. Without a channel there are no fires to log, so the rest of the checklist is gated on this item. The earlier media-buy-specific mention now cross-references the pattern. Buyers diagnosing an unexpected omission have two observable signals (`push_notification_config` registration state, seller capability declaration) to discriminate the cause without filing a ticket. - New "Diagnosing missing fires" subsection in `docs/building/by-layer/L3/webhooks.mdx` so buyers triaging missing fires from the transport contract page can find the debug surface. - `docs/media-buy/task-reference/get_media_buys.mdx` documents `include_webhook_activity` / `webhook_activity_limit` / `webhook_activity[]` with field table, status semantics, three-state presence, retention MUST, and a JS+Python "diagnose a webhook delivery problem" example that groups attempts by `idempotency_key` and selects the latest attempt by `attempt` number (robust against iteration order). ### Scope of this PR within #4582 - **Track 1** (snapshot/log duality doc) — already shipped at `docs/protocol/snapshot-and-log.mdx`; this PR extends it with the Webhook activity log pattern section. - **Track 2** (persistent webhook contract) — already shipped at `docs/building/by-layer/L3/webhooks.mdx`; this PR adds the cross-link from the contract page back into the debug surface. - **Track 3** (per-account subscription model) — explicitly **not** in this PR; targeted for 3.2.0 because it introduces a new account-level surface that needs to compose carefully with #3009 (multi-subscriber, 4.0). - **Track 4** (standardized log surface) — **shipped here**: hoisted record schema, universal truncation sentinel, retention MUST resolution, canonical pattern documentation. - **Tracks 5–7** (auth/transport hygiene, dedup edge cases, conformance rendezvous) — separate cadence per the epic. ### Dependency chain (informational) Track 4's adoption checklist names a notification-channel prerequisite as item 1. The implication: media buys adopt today because their channel (per-buy `push_notification_config`) already exists. Resources that outlive a media buy — creative-lifecycle (#2261), audiences, properties, account-level governance (#1711) — are blocked on track 3 (3.2.0) for the per-account channel. Once track 3 ships, those consumers plug into this pattern's record shape, request fields, scoping, retention floor, and three-state presence — inheriting transport, subscription, and observability from #4582 rather than re-deriving any of them. The #2261 RFC itself scopes to creative-specific event payloads + state-machine transitions; everything else is inherited. ### Backwards compatibility Both request fields are optional with default `false` / `50`; the response field is optional and absent unless `include_webhook_activity: true` is set AND the seller surfaces fire history for the buy with the required retention floor. Old clients see no change. ### Out of scope (future work) - **`include_webhook_payloads`** — sensitive opt-in to surface request and response bodies. Carved out as a separate extension because request/response bodies warrant stricter access controls and would consume the new truncation sentinel for size-bounding. - **Operator-facing aggregate views** across principals. - **Cross-subscriber visibility** under #3009 — `subscriber_id` is reserved on the record shape now so #3009 can populate it without a schema break. - **Real-time push** of webhook-activity events. - **Replay tool** (re-fire a past delivery). Closes #4278. Lands #4582 track 4. - a48d619: Add `allowed_values` to `text-asset-requirements.json` and the matching `CREATIVE_VALUE_NOT_ALLOWED` error code. Creative agents can now declare a closed set of permitted string values for a text input slot (e.g., legal- or brand-approved CTAs); conformant implementations MUST reject submissions outside the list with `CREATIVE_VALUE_NOT_ALLOWED`, echoing the offending field path in `error.field` and the allowed list in `error.details.allowed_values` so buyer agents can re-prompt deterministically. The field is optional and additive — existing producers and consumers are unaffected. Refs #4331. - 95bc69c: spec: webhook token round-trip + storyboard `required_any_of_tools` (closes #4339, #4325) Two additive 3.1.0-beta.2 blockers bundled. Both are non-breaking — existing senders and receivers continue to interoperate. **#4339 — webhook authentication `token` round-trip (`static/schemas/source/core/`)** - `mcp-webhook-payload.json` — promote the echoed authentication `token` to a typed optional property (`minLength: 16`, `maxLength: 4096`). The field previously traveled on the wire under `additionalProperties: true`; this is purely a typed surface on an existing implicit contract. Schema-driven SDK clients can now access `payload.token` without falling through an extras path. Receivers that configured a token MUST compare it to this value to validate request authenticity, and SHOULD use a constant-time equality check to mitigate timing attacks. The length-check fast-path is forbidden — receivers MAY range-check token length only after subscription lookup and never as a short-circuit on equal-length inputs. - `push-notification-config.json` — add `maxLength: 4096` to the existing `token` field (was previously only `minLength: 16`); this is a constraint addition on the upper bound, not a tightening of the lower bound that would reject existing-conformant configs. Cross-reference the payload-side validation obligation. Add downgrade-defense sentence: receivers that registered both an RFC 9421 signing key and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification. Clarify that `token` is NOT on the 4.0 removal track (only the legacy `authentication` block is being removed in favor of RFC 9421). **#4325 — storyboard `required_any_of_tools` declarative one-of-N gate (`static/compliance/source/universal/`)** - `storyboard-schema.yaml` — add `required_any_of_tools` as a top-level optional storyboard field. Each entry is an OR-family `{ tools: string[] (minItems: 2), rationale?: string }`. Multiple entries AND-combine. Distinct from `required_tools` (lenient any-of coverage skip) and `provides_state_for` (step-scope state substitution). - `runner-output-contract.yaml` — extend `requirement_unmet` with the canonical `detail` sub-reason prefix `missing_required_tool_family:` plus the literal wire shape for separators (`" or "` between family members, `"; "` between multi-gate aggregations). No new top-level `skip_result.reason` enum value — the contract version stays at 2.2.0. Aggregator guidance is human-display only; automated consumers SHOULD parse only the first sub-reason from aggregated `detail` and surface multi-gate state separately. - `scripts/build-compliance.cjs` — validate the field on specialism `index.yaml` files (filter+trim `tools[]` before `minItems:2` enforcement; reject non-string `rationale`; drop empty `rationale` after trim) and hoist into `compliance//index.json` for downstream SDK consumption. **Downstream pickups (tracked separately):** - `adcontextprotocol/adcp-client-python#638` — drops the `extra='allow'` token round-trip path once types regenerate against this schema. - `adcontextprotocol/adcp-client#1481` — drops `examples/hello_si_adapter_brand.ts` top-level `offering_id` mirror once the 3.1.0-beta.2 dist publishes (the SI capture-path fix shipped in #3937 / dist 3.1.0-beta.1). - `adcontextprotocol/adcp-client#1642` — migrates the runner-level account-discovery conformance gate (#1624) to per-storyboard `required_any_of_tools` consumption. **Known follow-ups (filed as issues, non-blocking on this beta):** - `minLength: 16` on both `token` fields permits ~96-bit base64url credentials, below the 128-bit entropy SHOULD in the description. Raising the floor to 22 would tighten an existing field; the gap is intentional for backward compatibility and re-evaluated in 4.0. - `docs/building/by-layer/L3/webhooks.mdx` token-echo subsection and `whats-new-in-3-1.mdx` / `migration/prerelease-upgrades.mdx` entries are pending. Schema descriptions carry normative weight; the docs page catches up in a follow-up PR. - 4c12454: spec(security): clarify idempotency-replay semantics for state-tracking fields on stateful resources. The existing idempotency contract (security.mdx §Idempotency rule 2) made the immutable-cache invariant explicit for async (`submitted`) responses — even if the underlying task transitions to a terminal state, replay returns the originally-cached `submitted` payload, not the current state — but was silent on synchronous-success responses that carry state-tracking fields inline (`status` on `create_media_buy`, per-record arrays on `sync_*`, resource snapshots on `acquire_rights` / `activate_signal`). The gap surfaced in real storyboard runs: a media buy created with `status: pending_creatives`, then mutated to `canceled`, then replayed via the same `idempotency_key` returned the cached `pending_creatives` bytes. A buyer that trusted the response as current state hit `NOT_CANCELLABLE` on the next mutation and a state-machine bug. Three options surfaced: 1. **Replay returns cached bytes verbatim** — what sellers do today; preserves byte-stable replay; buyers must re-read for current state. 2. **Replay returns current state** — what buyers reading the bytes expected; breaks byte-stable replay and forces sellers to refresh the cache on every resource mutation. 3. **Capability-declared** — sellers advertise their replay policy. Picked (1) and made it normative across both branches: - Seller rule 2 extended explicitly to synchronous-success responses. State-tracking fields in the cached payload MUST NOT refresh on replay. Partial refresh ("some fields current, others snapshot") is non-conformant — it would multiply the number of valid cache contents for a given key and break the canonical-replay invariant the rest of the rules build on. - New buyer-obligation paragraph: **Replay responses are historical snapshots.** Buyers requiring current state MUST consult the resource's read endpoint (`get_media_buys`, `list_accounts`, `list_creatives`, etc.). `replayed: true` is the explicit signal that a fresh read is required before any state-dependent decision. Agentic buyers MUST treat `replayed: true` as a stop signal for any planning step whose next action depends on resource state. - `Response-level replay indicator` gains a `State-machine routing` bullet pointing back at the seller rule and buyer obligation so the contract reads consistently from either entry point. Why (1) over (2) or (3): (2) forces every seller to thread the resource state machine through the idempotency cache (multiplying valid cache contents and breaking byte-stable replay). (3) adds capability surface for a question the spec should answer uniformly — heterogeneous replay semantics across sellers is exactly the kind of cross-seller inconsistency the idempotency contract exists to prevent. (1) is what existing sellers do; the gap was the contract being silent on sync-success, not divergent behavior. Files: - `docs/building/by-layer/L1/security.mdx` — seller rule 2 expanded (async + synchronous-success branches); new "Replay responses are historical snapshots" paragraph under "Buyer obligations"; `Response-level replay indicator` list gains the state-machine-routing bullet. Closes #4371. - d3bdc28: `comply_test_controller`: `account.sandbox: true` is now **required** on every controller request. The follow-up to #4382 / #3755 — sample_request blocks across all 25 controller call-sites in the storyboard suite have been swept to include the field, and the request schema's `required` array now lists `account` alongside `scenario`. Schema examples updated to match. Lint coverage is automatic: the existing `lint-storyboard-sample-request-schema.cjs` runs ajv against every storyboard sample_request, so any new `comply_test_controller` step that omits `account.sandbox: true` fails CI with `required@/:account` and is blocked without an allowlist entry. No new lint code needed — the schema tightening is the gate. This operationalizes the (Sandbox) verdict's defense-in-depth: the seller-side persisted-record check is the load-bearing gate, and now the wire format enforces it too. Closes #4383. - 4c12454: spec(mcp,security): require MCP tool wrappers to tolerate envelope-level fields. Buyer SDKs send envelope-level fields (`idempotency_key`, `context_id`, `context`, `governance_context`, `push_notification_config`) uniformly across all AdCP tool calls — including read-only tools that don't consume them. Buyers cannot know per-tool which envelope fields the seller's wrapper happens to declare, and the wire-level contract via `additionalProperties: true` on every published request schema permits them. Some MCP server implementations apply stricter validation than the schema declares — FastMCP / Pydantic with declared signatures raises `unexpected_keyword_argument`, Zod `.strict()` rejects unknown keys, OpenAPI codegen sometimes injects `additionalProperties: false` into input models. The result: read tools like `get_products` reject calls when `idempotency_key` arrives in params, breaking cross-seller portability the protocol promises. This is the server-side counterpart to the `additionalProperties: true` default — generalizing the principle already established for response validators in [`runner-output-contract.yaml` > `response_schema_validator_semantics`](https://github.com/adcontextprotocol/adcp/blob/main/static/compliance/source/universal/runner-output-contract.yaml) ("validator configuration MUST NOT contradict the schema's own `additionalProperties` declaration") to the request side. Files: - `docs/building/by-layer/L1/security.mdx` — new `#### Server-side tool wrapper conformance` subsection under §Idempotency (the most-affected envelope field). Concrete traps and fixes named for FastMCP/Pydantic, Zod/valibot, and OpenAPI codegen. - `docs/building/by-layer/L0/mcp-guide.mdx` — new `### Server-side tool wrappers MUST tolerate envelope fields` subsection under §MCP-Specific Considerations, cross-linking to the security.mdx normative rule. Concrete traps and one-line fixes for the three common stacks. Confirmed pre-existing in the wild — issue filer (#4399) hit it in production against a real seller, fixed in the seller's Wave 23.20 by adding `idempotency_key: str | None = None` to read-tool wrapper signatures. Closes #4399. - 4c12454: spec(security): require `idempotency_key` on every AdCP task request — read and mutating alike. Follow-up to #4399 (MCP tool wrapper envelope tolerance) — the deeper question that surfaced once that fix landed: why does this category of bug exist at all? Sellers reject `idempotency_key` on `get_products` because the contract framed it as a "mutating-only" envelope field, but `get_products` is polymorphic: - `buying_mode: 'brief'` / `'wholesale'` resolves as a pure read most of the time. - The same tool MAY return a `Submitted` envelope when curation requires upstream queries or HITL — that's async-task creation, which is mutation territory. - `buying_mode: 'refine'` with `action: 'finalize'` is a commit that transitions a proposal to committed with an `expires_at` hold window (see #4107). Buyers cannot predict at call time which mode the seller will resolve. So the rule "send `idempotency_key` on mutating requests only" required classification the buyer can't do, and the rule "sellers reject mutating requests that omit it" left sellers tripping over reads that turned into mutations or carried the field uniformly. The simpler rule: `idempotency_key` is required on every AdCP task request, period. Read and mutating alike. The buyer no longer classifies; the seller no longer rejects on the read/write distinction; the polymorphism on `get_products` (and any future tool that gains hybrid read/write modes) stops being a wire-contract footgun. For calls that resolve as pure reads, the cache provides byte-stable replay-on-retry within the TTL — harmless and gives buyers a uniform retry-safe contract. For calls that resolve as async-task creation or commit, the cache provides the same at-most-once guarantees as on mutating tasks. The rate-limit ceiling in rule 8 already accounts for high-volume traffic; read traffic adds to insert rate but the ceiling is tunable per operator. Files (`docs/building/by-layer/L1/security.mdx`): - §Idempotency rule 1 lead — "required on every AdCP task request — read and mutating alike". Drops the long list of mutating task names (the list was always going to drift as new tools shipped). - New `**Why universal — including read tools.**` paragraph naming `get_products`'s polymorphism as the canonical case. - §Response-level replay indicator — "responses to any request that resolved via the idempotency cache" (was "responses to mutating requests"). - §Buyer obligations / "When the seller's capability declaration is missing" — fail-closed now applies to every AdCP task request, with explicit reasoning about why pure-read calls aren't exempt under polymorphism. - §Server-side tool wrapper conformance (added in #4399) — `idempotency_key` line tightened from "MUST accept and ignore on read tools" to "MUST accept it; the idempotency layer routes it per rules 2-9". Why this over keeping the mutating-only rule and just fixing #4399's wrapper bug: - The wrapper bug was a symptom of the binary contract being wrong-shaped. Patching the symptom (sellers must accept envelope fields) without fixing the binary leaves future polymorphic tools (anything that can return Submitted) hitting the same class of failure. - "Cleaner and simpler" beats "send-on-mutating-only" once the polymorphism exists — the buyer's SDK doesn't need a read-vs-write classifier and the seller's wrapper doesn't need to know which mode a call resolved into before it sees the key. - Cache-growth concern bounded by rule 8 (per-agent insert ceiling); the recommended numbers were sized for realistic high-volume launch patterns and remain tunable. Refs #4399. Supersedes the "MUST tolerate on read tools" carve-out — `idempotency_key` is now required, not tolerated. - 4c12454: spec(compliance): standardize `notices` advisory channel on runner-output-contract. `universal/signed-requests.yaml` already mandates an "informational notice (not a failure)" for agents that still advertise the deprecated `signed-requests` specialism — but the contract had no field for it. Runners had two bad options: bake the advisory into prose `skip.detail` strings (unparseable by dashboards), or stay silent and let sellers hit a wall at the 4.0 cut where `request_signing` becomes required and `legacy_hmac_fallback` is removed. Adds a structured advisory channel: - **`step_result.notices`** — per-step advisory array. - **`run_summary.notices`** — run-scoped advisories (e.g., one `request_signing_required_in_4_0` notice per run, not per storyboard). - Notices MUST NOT contribute to `steps_failed`, `validations_failed`, or change `step_result.passed`. They fill the gap between validation failures (agent did something wrong), skips (runner couldn't apply the storyboard), and advisory-severity validations (storyboard author marked a check non-blocking) — none of which fit "passing observation, but here's a forward-looking advisory." - Three severities: `info` (advisory context only), `deprecation` (allowed today, spec recommends migration), `future_required` (optional today, required at a named future version with `effective_version`). - Forward-compat: receivers MUST treat unknown `code` or `severity` values as well-formed and surface them verbatim — additive extensions ship without breaking older consumers, matching the same forward-compat rule the contract already applies to authored check kinds. Canonical first-day codes documented under `notice.canonical_codes`: - `signed_requests_specialism_deprecated` (deprecation, motivated by the existing SHOULD in `signed-requests.yaml:34`). - `request_signing_required_in_4_0` (future_required, `effective_version: 4.0`). - `legacy_hmac_fallback_removed_in_4_0` (deprecation, `effective_version: 4.0`). `signed-requests.yaml` updated to reference the canonical code instead of the prose-only SHOULD. Files: - `static/compliance/source/universal/runner-output-contract.yaml` — version bumped 2.1.0 → 2.2.0 (additive). New top-level `notice:` block defines required/optional fields and canonical codes. `step_result.optional_fields` and `run_summary.optional_fields` gain `notices`. - `static/compliance/source/universal/signed-requests.yaml` — points the existing SHOULD at the new canonical `signed_requests_specialism_deprecated` code. SDK side (`@adcp/sdk`, `@adcp/client`) implements emission; tracked separately at adcp-client#1704. Refs #4418. - f23cefc: Add `get_creative_features.audit_observations[]` for non-blocking creative governance audit observations. The first standardized observation is `OVERSIGHT_DISCLOSURE_CARVEOUT_CLAIMED`, emitted when provenance declares `human_oversight: edited` or `directed` while also declaring `disclosure.required: false`. This surfaces the editorial-responsibility carve-out claim for audit routing without treating it as `PROVENANCE_CLAIM_CONTRADICTED` or a rejection reason by itself. Docs now define the seller and governance-agent handling pattern, and a media-buy conformance storyboard exercises the observable flow where a seller calls an on-list verifier and accepts the creative instead of treating the audit observation as a rejection. Closes #4438. - b5d64ea: feat(media-buy): allowed_actions on products, available_actions on buys, structured ACTION_NOT_ALLOWED rejection Adds a structured action vocabulary for `update_media_buy` capability discovery. Buyers can pre-flight which mutations are valid on a given buy in its current state instead of learning by mid-flight rejection. Composes with #4425's `requires` predicate grammar for caller-side requirement expression. **Schema additions** - `media-buy-valid-action` enum extended with finer-grained values: `extend_flight`, `shorten_flight`, `update_flight_dates`, `increase_budget`, `decrease_budget`, `reallocate_budget`, `update_targeting`, `update_pacing`, `update_frequency_caps`, `replace_creative`, `update_creative_assignments`, `remove_creative`, `remove_packages`. The coarse legacy values (`update_budget`, `update_dates`, `update_packages`, `sync_creatives`) are retained for 3.x backwards compatibility and removed in 4.0. - `media-buy-action-mode` enum (new): `self_serve`, `conditional_self_serve`, `requires_proposal`, `requires_approval`. - `action-not-allowed-reason` enum (new): `wrong_status`, `not_supported_on_product`, `not_supported_on_buy`, `mode_mismatch`. - `sla-window` core object (new): optional `response_max` + `completion_max` ISO 8601 durations. - `product-allowed-action` core object (new): `action` + `modes[]` + optional `allowed_statuses[]` + optional `sla` + optional `terms_ref`. Advisory template. - `media-buy-available-action` core object (new): `action` + singular `mode` + optional `sla` + optional `terms_ref`. Authoritative per-buy resolution. - `allowed_actions[]` on `product`: array of `product-allowed-action`. - `available_actions[]` on `get_media_buys`, `create_media_buy`, and `update_media_buy` responses: array of `media-buy-available-action`. The existing `valid_actions[]` field is deprecated in favor of `available_actions[]`; sellers SHOULD populate both during the 3.x deprecation window, consumers MUST prefer `available_actions[]` when both are present, and `valid_actions[]` is removed in 4.0. - `ACTION_NOT_ALLOWED` error code: populated with `attempted_action`, `reason`, and `currently_available_actions` in `error.details` so buyer SDKs can offer recovery without a separate `get_media_buys` round-trip. Typed details schema at `error-details/action-not-allowed.json`. - `enumMetadata` on `media-buy-valid-action`: each entry carries `update_fields` (dotted paths into `update_media_buy` body) so SDKs and codegen can dispatch from schema metadata rather than parsing the field-mapping table. Legacy coarse values additionally carry `deprecated: true` and `rollup` (the finer-grained values that supersede them) so SDKs can hide deprecated values when rollup targets are present in the same payload. - `allowed_actions[]` and `available_actions[]` arrays are uniquely keyed by `action`; sellers MUST NOT emit two entries with the same `action` value. Predicate evaluators consuming dotted paths like `available_actions.extend_flight.sla.response_max` MUST index by `action`. **Documentation** `docs/media-buy/task-reference/update_media_buy.mdx` adds the normative action → field mapping table (each action's exact `update_media_buy` fields), the mode table, and the relationship between flat `valid_actions[]` and structured `available_actions[]`. **Composition with #4425** The `requires` predicate grammar in #4425 queries `available_actions[]` as a first-class field. Field-level constraint metadata (bounds, max deltas) is out of scope for v1 and the natural home is `requires` rather than a parallel grammar. Duration predicates (e.g. `lte` on SLA `response_max`) extend the predicate vocabulary; tier-based SLA expression (`fast` / `standard` / `slow`) remains a possible alternative if the WG prefers to stay inside `equals`/`in`. Refs #4480, #4425. - 057ddf6: compliance: require baseline `sync_governance` registration in money-moving sales specialisms Adds a `sync_governance` registration step to the 3.1 beta compliance flows that move or monitor spend: `sales-social`, `sales-catalog-driven`, `sales-guaranteed`, `sales-non-guaranteed`, `sales-broadcast-tv`, and the generative seller flow under `creative-generative`. The step stops at account-level governance-agent registration and does not add `check_governance` enforcement to these parent tracks. This remains a minor beta compliance fix under the conformance-suite policy in `docs/reference/versioning.mdx`: the wire contract and `sync_governance` task already exist, and this PR aligns the beta grader with that existing baseline rather than adding a new protocol surface. Existing beta sellers claiming these money-moving specialisms must now implement `sync_governance` registration and the one-governance-agent rejection rule to remain conformant in 3.1 grading. The `governance-aware-seller` specialism remains the opt-in claim for the full governance-check loop (`check_governance`, denial propagation, conditions, and recovery) after baseline registration. - 0d7452e: feat(compliance): add `media_buy_seller/performance_buy_flow` capability-gated scenario (closes #4569) A non-guaranteed seller that advertises `media_buy.conversion_tracking` now has its performance-buy path certified end-to-end. The new scenario gates on the conversion_tracking capability via `requires_capability: present: true` (runner support landed in `@adcp/client` 7.6.0) — sellers without the capability grade `not_applicable`. The scenario verifies the dots actually connect when a seller claims conversion tracking: - `sync_event_sources` returns a usable `event_source_id`. - `create_media_buy` with an event-kind `optimization_goal` (CPA target) referencing the registered source is accepted. - `create_media_buy` with a goal referencing an unregistered `event_source_id` is rejected with `INVALID_REQUEST` and `error.field` set to the offending path — silent acceptance is a façade. - `log_event` against the bound source is forwarded upstream (anti-façade `upstream_traffic` assertion). - `get_media_buy_delivery` returns first-class conversion metrics: `conversions` and `cost_per_acquisition` at the buy level. Per-creative attribution is intentionally deferred to a follow-up scenario because real adopters report at differing granularities (per-line for retail-media, per-campaign for MMP-mediated mobile, per-placement for CTV); requiring per-creative here would fail honest implementations. ROAS (`target.kind: per_ad_spend`) and value-max (`target.kind: maximize_value`) are deliberately out of scope here — many honest conversion-tracking sellers (broadcast TV, upper-funnel video, signal-only) don't compute return-on-ad-spend. ROAS gets its own scenario gated on a separate `supported_target_kinds` capability bit ([#4639](https://github.com/adcontextprotocol/adcp/issues/4639)). This is the first scenario in a broader capability-claim contract pattern tracked under [#4637](https://github.com/adcontextprotocol/adcp/issues/4637): every non-trivial capability a seller declares should have a `requires_capability`-gated scenario proving the claim is honest end-to-end. **Added to `sales-non-guaranteed.requires_scenarios`.** - c9ca76d: Windowed pull recovery on `get_media_buy_delivery` — closes [snapshot-and-log](docs/protocol/snapshot-and-log.mdx) Rule 4 for data-bearing events. **Capability** (`core/reporting-capabilities.json`) - New `windowed_pull_granularities` (array of `reporting-frequency` enum values). Capability-scoped MUST: sellers MUST honor `time_granularity` pulls at any granularity declared here. Sellers MAY emit higher-frequency webhooks than they pull (e.g., stream-tap webhook with warehouse pulls only at daily); buyers see the gap up front via the capability. **Request** (`media-buy/get-media-buy-delivery-request.json`) - New `time_granularity` (reporting-frequency enum: `hourly` | `daily` | `monthly`) and `include_window_breakdown` (boolean). When both are set, the response returns per-window delivery slices shape-aligned with `reporting_webhook` payloads at the same granularity. **Response** (`media-buy/get-media-buy-delivery-response.json`) - New `media_buy_deliveries[].windows[]` array. Each slice carries `window_start`, `window_end`, `totals` (delivery-metrics), optional `by_package`, `is_final`, and `measurement_window`. Slices are ordered ascending and contiguous over the requested date range. Buyers reconcile missed webhooks by joining on `(media_buy_id, window_start)`. **Error code** (`enums/error-code.json`) - New `UNSUPPORTED_GRANULARITY` for pulls outside the declared `windowed_pull_granularities`. Sellers SHOULD echo the supported set in `error.details.supported_granularities`. Recovery: correctable. **Spec** (`docs/protocol/snapshot-and-log.mdx`) - Rule 4 promoted from SHOULD to MUST for capability-declared granularities. The contract holds within the seller's declared parity set; honest declaration of asymmetric webhook-vs-pull frequencies is in scope. Additive across the board: new request fields are optional, new response array is opt-in via `include_window_breakdown`, new capability defaults to empty (preserves current behavior — cumulative date-range pulls only). No breaking changes; safe in a minor release. Closes #4590. Anchors snapshot-and-log Rule 4 alongside the existing transport-layer log surface ([#4278](https://github.com/adcontextprotocol/adcp/issues/4278)). - 4af7213: Add creative-agent canonical `supported_formats` storyboard coverage for 3.1. The training agent now advertises implemented canonical creative build capabilities with agent-local `capability_id` values, accepts those IDs as `build_creative` targets for implemented canonical outputs, rejects unsupported targets with `FORMAT_NOT_SUPPORTED`, and keeps 3.0 compatibility mode from accepting 3.1-only capability selectors. - fba3451: Add the canonical-format `validate_input` conformance storyboard for 3.1. The training agent now exposes `validate_input` on sales, creative, and creative-builder tenants, returns the three 3.1 result discriminators for canonical/product targets, and the storyboard matrix requires the new coverage to stay clean on each surface that advertises the tool. - c9ca76d: Type `notification_id` as a first-class envelope field — closes a Rule 1 ambiguity on the webhook envelope contract. **Schema** (`core/mcp-webhook-payload.json`) - New optional top-level `notification_id` (string, 1–255 chars). Description anchored on snapshot-and-log Rule 1: stable across re-emissions, distinct from the per-fire `idempotency_key`. Population is event-shape-dependent — present on state-shaped events (equals the resource's stable id, e.g., `impairment_id`); absent on point-in-time data events (e.g., delivery report fires) per Rule 1. **Cross-references** (`enums/notification-type.json`) - Each enumDescription now declares its per-type `notification_id` population: - `impairment` → `impairment.impairment_id` (stable across re-emissions and the closing fire) - `scheduled` / `final` / `delayed` / `adjusted` → absent (point-in-time data events; dedupe by `idempotency_key` only) - Future notification types declare per-type population the same way. **Spec** - `docs/building/by-layer/L3/webhooks.mdx` — removes the "or the equivalent event-scoped id surfaced in the payload" hedge in the persistent-channel delivery-semantics block; receivers MUST track `notification_id` for state-shaped events. - `docs/protocol/snapshot-and-log.mdx` — Rule 1 forward-reference replaced with a direct anchor to the envelope schema and the per-type enumDescriptions. Additive — new field is optional and existing senders/receivers continue to validate. Receivers consuming the envelope from a strictly-typed SDK gain `notification_id` at the type level instead of having to read prose. Closes #4594. Follow-up to #4588 (snapshot-and-log Rule 1 prose) and the impairment cluster. - 67aaaac: feat(compliance): audience_buy_flow + event_dedup_flow capability-gated scenarios; training-agent audience_id validation Two new scenarios in the capability-claim contract pattern (#4637), both added to `sales-non-guaranteed.requires_scenarios`: - `media_buy_seller/audience_buy_flow` — gated on `media_buy.audience_targeting` presence. Certifies `sync_audiences` → bound `audience_id` in targeting → unbound id rejected → delivery against an audience-targeted buy. Sibling to `performance_buy_flow` on the audience side; the unbound-id rejection is the discriminating assertion. - `media_buy_seller/event_dedup_flow` — gated on `media_buy.conversion_tracking.multi_source_event_dedup` equals true. Certifies that the same `event_id` from two registered event sources attributes to one conversion, not two. Sellers without `multi_source_event_dedup` grade `not_applicable` — the bit gates the scenario; the cumulative-count check is the assertion. Training-agent fix: `create_media_buy` now rejects `targeting_overlay.audience_include` / `audience_exclude` entries whose `audience_id` was never registered via `sync_audiences`, with `INVALID_REQUEST` and `error.field` set to the literal JSONPath-lite path of the offending entry. Mirrors the `event_source_id` validation pattern from #4654. `sync_audiences` itself is now wired through the training agent (legacy `/mcp` and v6 `/sales/mcp` via `AudiencePlatform`) so adopters can run the audience scenario against the reference implementation. Three sibling product-level scenarios (reach, clicks, completed_views) remain blocked on #4651 product-level capability gating RFC. - fff0e2a: feat(compliance): metric-mode (reach/clicks/completed_views) + ROAS capability-gated scenarios using contains: matcher Four new scenarios in the capability-claim contract pattern (#4637), all gated via the `contains:` matcher (shipped in @adcp/client 7.70 — adcp-client#1817), all added to `sales-non-guaranteed.requires_scenarios`: - `media_buy_seller/performance_buy_flow_roas` — gated on `media_buy.conversion_tracking.supported_targets` containing `per_ad_spend` (#4639). Certifies that sellers advertising ROAS optimization accept event-kind goals with `target.kind: per_ad_spend` and `value_field` populated, reject ROAS goals that omit `value_field` on every event source entry, and report `conversion_value` and `roas` on delivery alongside `conversions` and `cost_per_acquisition`. Sibling to `performance_buy_flow` on the value side. - `media_buy_seller/reach_buy_flow` — gated on `media_buy.supported_optimization_metrics` containing `reach` (#4669). Certifies that sellers advertising reach optimization accept metric-kind goals with `metric: reach`, a `reach_unit` from the product's `metric_optimization.supported_reach_units`, and an optional `target_frequency` band; reject unsupported `reach_unit` values; and report `reach` and `frequency` on delivery. - `media_buy_seller/clicks_buy_flow` — gated on `media_buy.supported_optimization_metrics` containing `clicks` (#4669). Certifies that sellers advertising click optimization accept metric-kind goals with `metric: clicks` and a `cost_per` target, and report `clicks` and `cost_per_click` on delivery. No rejection arm — clicks is universal in semantics with no obvious unbound-id surface. - `media_buy_seller/completed_views_buy_flow` — gated on `media_buy.supported_optimization_metrics` containing `completed_views` (#4669). Certifies that sellers advertising completion optimization accept metric-kind goals with `metric: completed_views` and a `view_duration_seconds` in the product's `metric_optimization.supported_view_durations`; reject unsupported `view_duration_seconds` values (per `optimization-goal.json:50-53`, silent rounding creates measurement discrepancies); and report `completed_views` and `completion_rate` on delivery. All four scenarios grade `not_applicable` against the embedded training agent today — the training agent doesn't declare `supported_targets` or `supported_optimization_metrics` and therefore cannot claim these optimization kinds. This is the correct anti-façade hygiene per the `event_dedup_flow` precedent (#4664): an agent that doesn't claim a capability is not held to its scenario. The training agent stays honest by NOT claiming what it can't do; production adopters opt in by declaring the capability bits. Refs: #4637 (meta), #4639 (`supported_targets`), #4669 (`supported_optimization_metrics`), #4642 (CPA scenario precedent), #4664 (`event_dedup_flow` precedent), #4651 (product-level capability gating), adcp-client#1817 (`contains:` matcher). - e4587be: feat(schemas): add `supported_targets` to `conversion_tracking` capability. The seller-level `conversion_tracking` capability object on `get_adcp_capabilities` has no way to declare which event-goal `target.kind` values it can compute against. Today the spec requires sellers to reject `target.kind: per_ad_spend` event goals when no `event_sources[]` entry carries `value_field` (`static/schemas/source/core/optimization-goal.json`), but buyers have no pre-submission signal — they discover the constraint only at `create_media_buy` rejection time. `supported_targets` is an optional array on the existing `conversion_tracking` object, enum-constrained to `cost_per | per_ad_spend | maximize_value`. Named to parallel the product-level `metric_optimization.supported_targets` — same concept (which target kinds are supported), one at seller-capability granularity and one at product granularity. Buyers filter their event-goal shape against this list before submission; sellers MUST reject goals whose `target.kind` is not listed. When omitted, only target-less event goals (maximize conversion count within budget) are guaranteed. Purely additive and backward-compatible — no existing field changes, no requireds. Unblocks a future `performance_buy_flow_roas` storyboard scenario (capability-gated) without coupling that scenario to this schema PR. Files: - `static/schemas/source/protocol/get-adcp-capabilities-response.json` — new optional `supported_targets` property on the `conversion_tracking` object. Refs #4569, #4637. Closes #4639. - 93e570b: feat(compliance): frequency_cap_enforcement capability-gated scenario New scenario in the capability-claim contract pattern (#4637), added to `sales-non-guaranteed.requires_scenarios`: - `media_buy_seller/frequency_cap_enforcement` — gated on `media_buy.frequency_capping` presence (#4640 / #4670). Certifies that a seller advertising frequency_capping accepts a package-level `frequency_cap` (cap-form: `max_impressions` + `per` + `window`) on `create_media_buy` and, after simulated delivery, reports `totals.reach` + `totals.frequency` on `get_media_buy_delivery` with the observed frequency at-or-below the requested cap. Cap-form is the assertion target because it declares the numeric ceiling whose enforcement this scenario verifies; cooldown-form `suppress` is a separate semantic and not exercised here. Runtime-enforcement scenario — structurally simpler than the goal-mode scenarios (audience_buy_flow, performance_buy_flow). No rejection arm: `frequency_cap` is a numeric constraint, not a pointer to a registered resource, so there is no unbound-id analogue to reject against. The discriminating assertion is the observed frequency in delivery totals — a seller that silently drops the cap would deliver to its natural frequency distribution and overshoot. The observed-frequency-within-cap assertion uses `field_less_than` with a literal `value: 3.01` against a `max_impressions: 3` cap. The storyboard-schema check enum exposes `field_less_than` (strict less-than) as the only single-step numeric-comparison matcher today; a native `<=` / `field_at_most` matcher does not exist. The 0.01 epsilon lets the assertion target the cap literal without rejecting honest sellers that report frequency at exactly 3.0. A runner extension adding `field_at_most` (storyboard schema + runner update) would let this drop to `value: 3` without the epsilon — captured here as a soft follow-up; the cap-enforcement signal is already discriminating without it. No training-agent changes — the training agent does not declare `frequency_capping` today, so the scenario grades `not_applicable` against the reference implementation and CI passes. Same anti-façade pattern as the other capability-gated scenarios: the bit gates the scenario, the assertion targets the runtime behavior that the bit commits to. Refs: #4637 (capability-claim meta), #4640 (capability bit), #4670 (frequency_capping shipping PR). - 72dc776: feat(schemas): add media_buy.frequency_capping capability declaration (closes #4640) Sellers can now declare frequency-capping support in get_adcp_capabilities. Presence of the object means the seller honors `targeting.frequency_cap` and MUST reject caps they cannot enforce rather than silently dropping them. Two optional sub-fields let buyers pre-flight validate before submitting: - `supported_per_units` — entity granularities (devices, individuals, etc.) from reach-unit.json - `supported_window_units` — duration units (hours, days, campaign) from duration.json `enforces_within` from the original RFC was dropped — no SSP can back that attestation cleanly. Per-product overrides for mixed addressable/non-addressable inventory are a likely follow-up. A capability-gated `frequency_cap_enforcement` storyboard scenario lands separately under the capability-claim contract pattern (#4637). - 7b5734e: feat(schemas): add media_buy.supported_optimization_metrics seller-level summary (closes #4651) Sellers can now declare which optimization metrics they support at the seller level, mirroring the product-level `metric_optimization.supported_metrics` enum. Buyer agents get a single discoverable rollup for pre-flight metric filtering; storyboard scenarios get a gate path they can use with `requires_capability` to skip sellers that don't support a metric (e.g., reach_buy_flow, clicks_buy_flow, completed_views_buy_flow). Sellers MUST keep this in sync with their product catalog — values appear here only if at least one product supports them. Per-product inspection via `metric_optimization.supported_metrics` remains the source of truth for buy-time targeting; this is a seller-level discoverability convenience. Unblocks the metric-buy-mode storyboards under the capability-claim contract pattern (#4637). Those scenarios additionally require a `contains:` matcher on `requires_capability` (filed against adcp-client). - 8650fb0: New `media_buy_seller/dependency_impairment_cardinality` scenario — pressure-tests the `impairment.coherence` inverse rule under cardinality. The base scenario tests forward + inverse + health-iff with one creative on one package, which a buggy seller can pass by emitting any impairment entry whose `resource_id` matches a known-rejected creative. This scenario asserts the seller emits the **right number** of entries, each pointing at the **right resource**. Five phases, two creatives (A, B) on two packages (package_a, package_b): 1. **setup** — create buy with two packages, sync both creatives, assign each to its own package, baseline both at `approved`. Cardinality 0. 2. **reject_first_cardinality_one** — force A to rejected. Assert exactly one impairment, `resource_id: A`, `package_ids` contains `package_a` only. Catches sellers that emit `package_ids: [package_a, package_b]` (over-scoping) or duplicate entries. 3. **reject_second_cardinality_two** — force B to rejected. Assert two impairment entries. Catches sellers that merge entries. 4. **recover_first_via_swap** — swap `package_a` binding from A to fresh creative C. Cardinality back to 1. Catches sellers that don't decrement on swap recovery. 5. **recover_second_via_swap** — swap `package_b` binding from B to fresh creative D. Cardinality back to 0. Failure modes caught beyond the base scenario: - Wrong resource_id on an impairment (right cardinality, wrong target). - Single impairment with `package_ids` inflated to both packages when only one creative is rejected. - Failure to decrement `impairments[]` when an impairment clears partially via swap recovery. Wired into `protocols/media-buy/index.yaml#requires_scenarios`. Sellers without `comply_test_controller force_creative_status` or without multi-package support grade `not_applicable`. Same capability gating as the base scenario (`capabilities.media_buy.impairment_propagation: "snapshot"` required for grading). Closes #4681. - 8650fb0: Rewrite `media_buy_seller/dependency_impairment` phase 5 to use **swap-assignment** as the canonical recovery vector instead of same-ID re-approval. In production, buyers rarely re-approve a rejected creative on the same ID — they ship a corrected asset under a new ID and update the package's `creative_assignments`. The previous scenario modeled an uncommon flow and would have failed sellers whose review pipeline treats `rejected` as a hard wall (a legitimate design). New phase 5 sequence: 1. Sync a second creative (B, approved) into the library — not yet assigned. 2. Force B to `approved` baseline via `comply_test_controller`. 3. Call `update_media_buy` with `packages[].creative_assignments` (replacement semantics per `package-update.json`) to swap the package's binding from A (rejected) to B (approved). 4. Read the buy — `health: ok`, `impairments[] empty`. Creative A's library status stays `rejected` but A is no longer a dependency of any package on this buy, so the impairment clears. Scenario `version` bumped 1.0.0 → 2.0.0 to mark the recovery semantics change. `required_tools` adds `update_media_buy`. Narrative explicitly notes that same-ID re-approval is covered by a future opt-in sibling scenario (`media_buy_seller/dependency_impairment_reapprove_recovery`) for sellers whose review flow supports the reinstatement path. Closes #4682. - 8650fb0: New capability `capabilities.media_buy.impairment_propagation` on `get_adcp_capabilities` — sellers declare how they propagate dependency-resource impairments (creative rejection, audience suspension, catalog withdrawal, event source insufficient, property depublication) to buyers. Three postures, each a real-world pattern: - **`snapshot`** (default) — seller populates `media_buy.health` and `media_buy.impairments[]` on `get_media_buys` reads. The `impairment.coherence` compliance assertion grades the propagation. Premium guaranteed sellers tend toward this. - **`webhook_only`** — seller fires `notification-type: impairment` webhooks but does NOT mirror the impairments on the buy snapshot. Buyers reconcile state from the push channel alone. High-throughput SSPs / DSPs tend toward this when state lives in the event stream. - **`out_of_band`** — seller propagates outside the AdCP protocol surface entirely (email to trafficker, dashboard, partner-specific notification feed). Long-tail and enterprise-bundled platforms tend toward this. Sellers declaring `webhook_only` or `out_of_band` are not graded by the `impairment.coherence` storyboard scenarios (`dependency_impairment`, `dependency_impairment_cardinality`) — those grade `not_applicable` for those postures. Their compliance bar is the webhook contract or the offline agreement, not snapshot coherence. Docs: `lifecycle.mdx § Compliance` extended with a paragraph describing the capability and how it gates the snapshot-coherence rules. Each posture documented as a legitimate operational pattern, not a workaround. Runtime gating in the compliance runner is the adcp-client follow-up — once the runner reads the capability and grades `not_applicable` accordingly, the storyboard scenarios will skip cleanly on `webhook_only` / `out_of_band` sellers. Spec-side declaration ships in this PR; runner-side `not_applicable` enforcement tracked in the adcp-client follow-up. Closes #4683. - 4d4c9a0: Restructure `capabilities.media_buy.impairment_propagation` from a single-value enum to `capabilities.media_buy.propagation_surfaces` (non-exclusive array). The enum couldn't express the common case where a seller propagates impairments on both the buy snapshot AND fires webhooks (GAM, FreeWheel, CM360) — the previous shape forced a choice between `snapshot` and `webhook_only`, which created an incentive to declare `webhook_only` and dodge the snapshot-coherence bar even when the seller actually does both. The array shape lets sellers declare `["snapshot", "webhook"]` honestly. Surface values: - **`snapshot`** — `media_buy.health` + `media_buy.impairments[]` mirror impairments on `get_media_buys` reads. Graded by `impairment.coherence` storyboards when declared. - **`webhook`** — `notification-type: impairment` webhooks fire via `push_notification_config`. Graded by the persistent-channel webhook contract. - **`out_of_band`** — propagation via channels outside the AdCP protocol surface (email, dashboard, partner-specific feeds). Compliance bar is the offline agreement, not a protocol assertion. Sellers with a non-AdCP-field mapping gap (e.g., `media_buy.delivery_status_detail` instead of `media_buy.health`) SHOULD document the mapping rather than declare `out_of_band` — the spec's gap is what this value legitimately covers. Default when absent: `["snapshot"]` (preserves current snapshot-coherence contract for sellers that don't declare). Storyboard gating: `impairment.coherence` scenarios (`dependency_impairment`, `dependency_impairment_cardinality`) grade `not_applicable` when `propagation_surfaces` does not include `"snapshot"`. Sellers declaring `["snapshot", "webhook"]` are graded on the snapshot surface here and on the webhook contract separately. Pre-release breaking change to the freshly-shipped `impairment_propagation` enum (landed in PR #4685 but unreleased — 3.1.0 GA is 2026-05-29). No deprecation cycle needed; sellers migrating from a pre-release adoption translate single values to one-element arrays. Closes #4686. - 0988b54: Add `field_pattern` / `envelope_field_pattern` compliance check kinds and use the envelope-scoped form to validate `adcp_version` shape in the version-negotiation storyboard. Tighten media-buy storyboards that reuse a discovered `pricing_option_id` so auction-priced flows send `bid_price` and fixed-price flows validate the captured option before downstream package creation. - 56b9b63: feat(compliance): per_creative_attribution capability bit + scenario New capability bit and scenario in the capability-claim contract pattern (#4637), landing the deferred per-creative conversion attribution work from #4642 / #4725. - `media_buy.conversion_tracking.per_creative_attribution` (boolean, defaults to false) — `static/schemas/source/protocol/get-adcp-capabilities-response.json`. Declares whether the seller can attribute conversions to specific creatives within a package and surface that breakdown via `media_buy_deliveries[].by_package[].by_creative[].conversions` in `get_media_buy_delivery`. Optional; omission means `false` and is backward-compatible. - `media_buy_seller/per_creative_conversion_attribution` — new scenario gated on `media_buy.conversion_tracking.per_creative_attribution: true`, added to `sales-non-guaranteed.requires_scenarios`. Registers two distinct display creatives via `sync_creatives`, creates a media buy whose single package's `creative_assignments` references both, logs two purchase events against the bound event source, simulates delivery, and asserts `by_package[0].by_creative[0..1].{creative_id,conversions}` are populated. The second-row assertion is the asymmetry check that separates honest per-creative attribution from a single-row façade collapsing attribution to whichever creative the seller tracked first. Closes the gap deliberately left by `performance_buy_flow` (#4642), whose narrative explicitly defers per-creative attribution: honest adopters report at differing granularities — social platforms per-ad, retail-media networks (Criteo, Amazon Ads) per-line, MMP-mediated mobile (post-iOS-14) per-campaign / per-ad-set, broadcast and CTV performance products per-placement. Requiring per-creative in the base CPA scenario would have failed those honest implementations. The bit gates the scenario; sellers that don't advertise it grade `not_applicable`. `log_event`'s payload (`core/event.json`) does NOT carry `creative_id` — attributing each event back to a specific creative is the seller's internal click / view-through correlation, not the buyer's. The scenario logs two events with distinct `event_ids` and relies on the seller's correlation to spread `simulate_delivery`'s `conversions` count across the two assigned creatives in the `by_creative[]` breakdown. No training-agent changes — the training agent does not declare `per_creative_attribution`, so the scenario grades `not_applicable` against the reference implementation and CI passes. Same anti-façade pattern as `event_dedup_flow` (#4664) and `frequency_cap_enforcement` (#4640): the bit gates the scenario, the assertion targets the runtime behavior the bit commits to. Refs: #4725 (capability bit + scenario), #4637 (capability-claim meta), #4642 (performance_buy_flow that deferred this), #4639 (supported_targets bit for the sibling ROAS gate). - 9357289: Catalog sync cluster (3.1): three companion proposals for catalog mirroring between AdCP agents and consumers (storefronts, federated marketplaces, registries). Independent and complementary — agents MAY adopt any subset. **#4762 — `get_signals` wholesale discovery mode** - `signals/get-signals-request.json` adds `discovery_mode` enum (`brief` default, `wholesale`). Wholesale mode bans `signal_spec` / `signal_ids` and returns the agent's full priced catalog, paginated. Symmetric with `get_products buying_mode: "wholesale"`. - `signals/get-signals-response.json` adds `incomplete[]` (scopes: `signals`, `pricing`, `catalog`) so partial completion is signalled inline rather than via async/Submitted handoff. `signals` becomes conditionally required (omitted when `unchanged: true`). - `protocol/get-adcp-capabilities-response.json` adds `signals.discovery_modes`. Agents not declaring `"wholesale"` MAY return `INVALID_REQUEST` for wholesale calls. - `docs/signals/tasks/get_signals.mdx` documents wholesale enumeration, authorization/provenance preservation for marketplace signals, pricing scope, and capability probing. **#4761 — `catalog_version` conditional fetch (ETag-style)** - `media-buy/get-products-request.json` and `signals/get-signals-request.json` add `if_catalog_version` and `if_pricing_version` opaque tokens. - `media-buy/get-products-response.json` and `signals/get-signals-response.json` add `catalog_version`, `pricing_version`, and `unchanged`. When `unchanged: true`, `products` / `signals` MUST be omitted and `catalog_version` MUST be echoed — encoded as an explicit `oneOf` so the unchanged response is schema-valid without breaking the standard required-payload contract. - Tokens are opaque and scoped to the request-parameter tuple that produced them. Pre-v3.1 agents that ignore the conditional fields simply return the full payload — semantically correct, just inefficient. - Pagination interaction: if the catalog mutates mid-pagination, sellers SHOULD return the new `catalog_version` on each page; consumers SHOULD restart from `cursor: null` on a mid-pagination version change. **#4763 — Per-agent catalog change feed** - New `specs/catalog-change-feed.md` modeled on `specs/registry-change-feed.md`. UUID-v7 cursor-based event log, one feed per agent, denormalized payloads, optional webhook subscriptions. - Event types: `product.{created,updated,priced,removed}`, `signal.{created,updated,priced,removed}`, `catalog.bulk_change` (fast-forward for rate-card sweeps). - `protocol/get-adcp-capabilities-response.json` adds top-level `catalog_change_feed` declaration (`supported`, `retention_window_days` ≥7, `webhooks_supported`, `event_types[]`). - Endpoints (`GET /catalog/events`, `POST /catalog/subscriptions`) live on the agent itself, not the registry. Authorization scope mirrors wholesale enumeration. Additive across the board for 3.0-conformant agents: new optional fields, new conditional schemas, new capability stanzas, new spec doc. Agents MAY implement any combination: conditional-fetch alone for cheap probes against stable catalogs, the full feed for high-frequency mirroring, wholesale-only as a transitional step. Reference implementations land in the prebid salesagent as part of v3.1 conformance prep. **Validator obligation for 3.1 SDKs (read carefully):** the 3.1 `get_products` / `get_signals` response schema makes `cache_scope` required to enforce the two-layer cache safety invariant — a seller that silently omits `cache_scope` on an account-scoped response would cause buyers to mis-key the cache and serve account-overlay payloads to other accounts. Pre-3.1 sellers correctly omit `cache_scope` and remain conformant to their declared version. SDKs that validate strictly against the 3.1 schema MUST select the validator based on the server-declared `adcp_version` (release-precision version negotiation, 3.1): for responses with `adcp_version` starting `3.0`, the 3.1 cache_scope-required constraint MUST be relaxed. This is a tightening within 3.1, not a 3.0 break — but adopter SDKs that hardcode the 3.1 schema without version-pinned validation will reject correct 3.0 traffic, so the obligation is normative. Refs #4761, #4762, #4763. - 8fcf7f9: Add published registry change-feed schemas for `/api/registry/feed`. The new `core/registry-feed-response.json` wrapper references `core/registry-event.json`, which now validates the current registry event vocabulary across property, agent, publisher, and authorization changes. Registry docs and specs now cite the schemas and align examples with the implemented cursor and filter contract. - 4c12454: spec: PR #4796 review follow-ups — close 7 footguns surfaced by detailed review. Consolidated fixes from a careful review of the 10-commit WG-review batch. None change the underlying decisions — they close gaps in the contract surface that careful adopters would have hit in production. **Polling / state re-read MUST mint a fresh `idempotency_key`.** The original §Idempotency guidance covered network-retry (reuse key) and agent-replan (new key) but was silent on polling reads and state re-reads. Under universal idempotency from 3.1, reusing a prior poll's key returns the cached snapshot for up to `replay_ttl_seconds` — the dashboard polling `get_products(brief)` or buyer agent reading `get_media_buys` after a mutation gets stale data, silently. Added a third case to the retry-vs-replan classification: **polling / state re-read** intent is "give me current state at time T," and MUST mint a fresh key per call. The same rule now governs the re-read step in the [Replay responses are historical snapshots] pattern — the re-read key MUST be fresh, never the mutation's key (which would return `IDEMPOTENCY_CONFLICT` or, worse, the cached mutation response). **Bootstrap carve-out for `get_adcp_capabilities`.** The fail-closed-on-missing-TTL rule deadlocked the bootstrap — the discovery call is _how_ the buyer learns whether the seller declares `replay_ttl_seconds`. Made explicit: `get_adcp_capabilities` is exempt from rules 1–9; buyers MAY omit `idempotency_key` on the discovery call; sellers MUST accept the call without it. Fail-closed applies to every subsequent task request after the capability fetch. **Rate-limit ceilings flipped from "operators SHOULD revisit" to concrete read/write split.** A 3.1 agentic dashboard polling `get_products(brief)` + `list_creatives` + `list_accounts` across 5 accounts at 1Hz is ~15 inserts/sec on reads alone, before any write traffic. The original 60/sec sustained ceiling would silently rate-limit legitimate read polls. New recommended ceilings: **Reads 300/sec sustained / 1,500/sec burst, Writes 60/sec sustained / 300/sec burst, Combined cap 350/sec sustained / 1,700/sec burst**. The split-budget shape (separate counters) MUST be implemented from 3.1 onward even when operators tighten the magnitudes — a shared single-budget cap is the failure mode this rule prevents (a buyer's dashboard polling can't starve write capacity that protects `create_media_buy` / `sync_creatives` / `activate_signal`). **Multi-finalize atomicity contract clarified — observation point, not rollback.** "Atomic" was ambiguous: was the seller obligated to roll back if proposal A finalized but proposal B failed mid-commit? There's no `unfinalize` operation, so rollback was an unspecified obligation. Made explicit: atomicity runs on the pre-commit validation gate — sellers MUST NOT return success unless every named proposal has both completed and persisted; if any proposal fails validation, the seller MUST reject the entire call without committing any. Mid-commit failure (post-validation, pre-persist) MUST return `INTERNAL_ERROR` with `refinement_applied[]` per-position outcomes; recovery is undefined at the protocol level and buyers SHOULD re-read state before retrying. Buyer-intent caveat added: buyers whose intent specifically required atomic commit (budget-shared proposals) MUST be prepared to abandon the intent if the seller returns `MULTI_FINALIZE_UNSUPPORTED` — there is no recovery for that loss of intent beyond accepting the looser sequential-commit guarantee. **`context` envelope/body relationship documented.** 147 task request/response schemas already declare body-level `context` `$ref`'ing `core/context.json`. With #2911 adding `context` to the envelope, the field exists in two places. Under flat MCP serialization the two declarations occupy the same wire key — they're the same field, not a collision. Made explicit: envelope declaration is **authoritative**; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation in isolation). Future versions MAY drop the body-level declarations; conformance does not require either to be present, only that the wire value `$ref`s `core/context.json`. **Forward-compat decoding ↔ Retry Logic symmetric cross-link.** The original cross-link was Forward-compat → Retry Logic (one direction). A reader landing on Retry Logic didn't see that the `transient` default for unknown codes was bounded there. Added a back-link paragraph at the top of `## Retry Logic`: "The rules in this section bound every `transient`-classified error, including the `transient` default applied to unknown error codes under § Forward-compatible decoding." **`pending_creatives` sharpening landed in `media-buys/index.mdx`.** The original #4196 fix landed only in the enum description on `media-buy-status.json`. Readers landing on the lifecycle doc at `docs/media-buy/media-buys/index.mdx` saw only the old "Approved but no creatives assigned" framing. Mirrored the buyer-side-action-required + `pending_X` naming convention into the lifecycle doc. **Strict-validator adopter-action row added.** The 7-row adopter-action table in 3.1.0 release notes didn't call out adopters with strict-validator test fixtures or codegen against `core/protocol-envelope.json`. Dropping `required: [status, payload]` is a JSON-Schema-level relaxation — strict validators that asserted "envelope MUST reject responses missing payload" will start accepting envelopes they used to reject. Added an 8th row noting the fixture refresh and codegen audit (OpenAPI / quicktype / Pydantic consumers will see `status` and `payload` flip from required to optional in generated types). Files: - `docs/building/by-layer/L1/security.mdx` — polling/re-read paragraph (renamed "network retry vs. agent re-plan vs. polling / state re-read"), bootstrap carve-out for `get_adcp_capabilities`, split read/write ceilings in rule 8, fresh-key requirement in Replay responses section - `docs/building/by-layer/L3/error-handling.mdx` — back-link paragraph at top of `## Retry Logic` - `docs/media-buy/product-discovery/refinement.mdx` — atomicity-at-observation-point clarification, mid-commit failure paragraph, buyer-intent caveat - `docs/media-buy/media-buys/index.mdx` — `pending_creatives` description mirrors the buyer-side-action sharpening - `docs/reference/release-notes.mdx` — strict-validator adopter-action row (8th) - `static/schemas/source/core/protocol-envelope.json` — `context` envelope/body relationship explained on the envelope declaration - `static/schemas/source/media-buy/get-products-request.json` — refine[] description gains observation-point atomicity + mid-commit failure + buyer-intent caveat Refs PR #4796 review comments. No new behavior; closes gaps in the existing contract. - 41fce13: spec(envelope): `status` is REQUIRED on every task response envelope. The protocol envelope (`core/protocol-envelope.json`) now declares `status` in its `required` array, formalizing the wire contract the docs and conformance storyboards already assume. Every task response — including synchronous read-only metadata calls like `get_adcp_capabilities` — MUST carry a top-level `status` field. Synchronous calls emit `status: "completed"`; async calls emit `submitted`, `working`, `input-required`, etc. per the task-status enum. **Why this is a wire-shape clarification, not a new requirement.** The docs (`sdk-stack.mdx`, `mcp-response-extraction.mdx`, `webhooks.mdx`, `error-handling.mdx`) already treat envelope `status` as a canonical protocol-layer field. The `v3_envelope_integrity` conformance storyboard already asserts presence via `envelope_field_present`. The schema design just left `status` declared but not required on the envelope, which let SDKs ship without emitting it on some sync responses. This change closes that ambiguity. **Resolves #4832** — adopter (`@adcp/sdk@7.7.0`, production seller) hit `v3_envelope_integrity/no_legacy_status_fields` failure because the SDK's auto-registered `get_adcp_capabilities` handler builds the response payload without setting `status`. The storyboard was correct; the envelope contract just wasn't formalized in schema. **Adopter impact.** Agents shipping responses without top-level envelope `status` are now non-conformant per the schema. The single broadly-distributed gap is `@adcp/client`'s auto-registered `get_adcp_capabilities` (tracked separately); other tools that go through the v6 handler pipeline already carry `status` because the SDK threads the envelope around typed platform returns. Adopters using raw-handler patterns (deprecated v5) should audit their responses and add `status: "completed"` to any sync response missing it. **Phased follow-ups (not in this PR):** - SDK companion in `adcp-client`: emit `status: "completed"` on the auto-registered `get_adcp_capabilities` handler (and audit any other sync helper that builds responses without the v6 pipeline). - Per-task schema fold: extend each of the 64+ task response schemas (`create-media-buy-response.json`, `sync-creatives-response.json`, etc.) to `$ref` `protocol-envelope.json` in addition to `version-envelope.json`. Mechanical cleanup that lets per-task `response_schema` validators catch envelope omissions directly, without relying on the separate `envelope_field_present` storyboard check. Targeted for the 3.1 cycle ahead of GA. - ca60b16: spec/chore(envelope-fold): close 3 brand-schema body-`status` collisions surfaced by #4878, normalize schema-source UTF-8, harden pre-push hook. Follow-up bundle to PR #4896 (envelope-fold). Three brand response schemas had body-level `status` collisions with the envelope `status` (TaskStatus) that the fold didn't carve out; left unfixed they were jointly unsatisfiable on the per-task validator. Two non-spec improvements (UTF-8 normalization, pre-push hook trap) landed alongside since they were touching the same surface. ## Brand-schema body-`status` renames Same pattern as #4895 (media-buy) and #4897 (governance), applied to three brand-protocol response schemas: - **`brand/verify-brand-claim-response.json`** — `status` → `verification_status` ($ref unchanged: `brand/verification-status.json`). Updated `required[]` and the error branch's `not.anyOf` discriminator clause. Schema is NOT `x-status: experimental` but is pre-3.1-GA, so beta-cycle rename is acceptable. - **`brand/creative-approval-response.json`** — `status` → `approval_status` (const discriminator: `approved` | `rejected` | `pending_review`). Renamed across all four oneOf branches (3 success + 1 error), all `required[]` lists, and the error branch's `not.anyOf` clause. Not experimental. - **`brand/acquire-rights-response.json`** — `status` → `rights_status` (const discriminator: `acquired` | `pending_approval` | `rejected`). Renamed across all four oneOf branches, all `required[]` lists, and the error branch's `not.anyOf` clause. Schema is `x-status: experimental` so hard rename is sanctioned. Docs swept: - `docs/brand-protocol/tasks/verify_brand_claim.mdx` — 10 example bodies renamed `status` → `verification_status`. - `docs/brand-protocol/tasks/acquire_rights.mdx` — 4 example bodies renamed `status` → `rights_status`. - `docs/brand-protocol/walkthrough-rights-licensing.mdx` — 4 example bodies renamed `status` → `rights_status`. Why now (vs deferring to a separate PR): the doc-injector in #4878 correctly skipped these three files because the schema-level collision was detectable in advance. Closing them in the same PR keeps the envelope-fold contract whole — every per-task response schema admits at least one valid response with envelope `status: "completed"` post-merge. ## Training-agent envelope-status fixes (server, not spec) `server/src/training-agent/task-handlers.ts`: - **Idempotency replay path** (L4547-4561) now stamps `status: 'completed'` if the cached inner response lacks one. Older cache entries written pre-envelope-fold are auto-upgraded on replay. Without this, every cache hit on a folded schema fails its own per-task validator. - **`handleCreateMediaBuy` / `handleUpdateMediaBuy` cancel branch / `handleUpdateMediaBuy` non-cancel branch** now emit `media_buy_status: MediaBuyStatus` instead of body `status: MediaBuyStatus` (canonical 3.1 form per #4895). The envelope-stamp guard at L4622-4623 then sets envelope `status: 'completed'` cleanly. Without this, MediaBuyStatus values like `pending_creatives` / `active` would survive the guard and fail TaskStatus validation. Nested `media_buys[].status` and `media_buy_deliveries[].status` (get_media_buys and get_media_buy_delivery handlers) are intentionally left as `status` — the cascade is deferred to 4.0 (#4905) per #4895's Option-E-pure scope. ## Doc fix (signals/activate_signal) `docs/signals/tasks/activate_signal.mdx:466` — "Error Response (Failed)" example was mis-injected with `status: "completed"`. Corrected to `status: "failed"`. Aligns with the `error-handling.mdx` two-layer model: envelope `status: "failed"` + `errors[]` + optional `adcp_error`. ## Schema-source UTF-8 normalization (chore) 48 schema source files re-encoded by some prior tooling using `\uXXXX` escape sequences for printable non-ASCII characters (em-dashes, en-dashes, smart quotes). Same character semantically, but inflates diffs and obscures real changes — was the dominant source of noise in #4896's review. - `scripts/normalize-schema-utf8.mjs` — targeted normalizer that only rewrites `\uXXXX` escapes for printable non-ASCII BMP characters. Does NOT touch JSON-required escapes, surrogates, control characters, whitespace, property order, or anything else. Round-trip sanity check via `JSON.parse`. - `npm run fix:schema-utf8` — apply normalization. - `npm run test:schema-utf8` — CI guard. Added to the master `test` chain so regressions are caught at PR time. ## Pre-push hook hardening (chore) `.husky/pre-push` — `dist/docs` / `dist/addie/rules` / `.addie-repos` / `.context` are moved to `/tmp/.prepush--` before the Mintlify broken-links check, then restored. If interrupted, the temp dir was orphaning into `dist/docs/.prepush--/`. Now: - Trap `EXIT / INT / TERM` to restore on any exit path. - Idempotent restore (only moves if source exists AND dest doesn't). - `.gitignore` entry `.prepush-*/` and `dist/docs/.prepush-*/` as belt-and-suspenders. ## Test verification - `npm run build:schemas` — clean - `npm run test:schemas` — 8/8 - `npm run test:examples` — 36/36 - `npm run test:composed` — 43/43 - `npm run test:json-schema` — 270/270 - `npm run test:schema-utf8` — passes - `npx vitest run server/tests/unit` — 3760/3760 pass (233 test files) - ca60b16: spec: fold `protocol-envelope.json` into per-task response schemas Closes #4878. Companion to #4876 (envelope `status` REQUIRED) — that PR locked the contract on the envelope schema; this PR cascades it to every per-task response schema so per-task `response_schema` validators catch envelope omissions directly, without relying on the separate `envelope_field_present` storyboard check. **What changed.** 64 task response schemas now `$ref` `core/protocol-envelope.json` in their `allOf` chain alongside the existing `core/version-envelope.json` ref. Two schemas without an existing `allOf` (`brand/search-brands-response.json`, `creative/validate-input-response.json`) had `allOf` added with both envelope refs for consistency. **Carve-outs.** - `core/pagination-response.json`, `core/catalog-events-response.json` — nested helpers, not task responses. Excluded. - `governance/check-governance-response.json`, `governance/report-plan-outcome-response.json` — body-level `status` enum (`approved`/`denied`/`conditions` and `accepted`/`findings` respectively) collides with envelope `status` (task-status enum) on MCP flat serialization. Excluded; tracked as a separate spec issue. **What this catches in adopter shape.** Pre-3.1-GA, any response shape lacking top-level envelope `status` now fails its own per-task `response_schema` validator, not just the universal `envelope_field_present` storyboard step. Validators integrated against the per-task schema (typed-SDK codegen, request-replay tooling, schema-aware test fixtures) gain envelope coverage for free. **Cleanup also applied.** 25 schema examples in the affected response schemas were updated to include `status: "completed"`. 62 JSON blocks in the docs (across 27 `.mdx` files) were updated likewise. Test fixtures in `tests/composed-schema-validation.test.cjs` and `tests/example-validation-simple.test.cjs` were updated to include `status` on the relevant cases — surface-aligned with the schema fold so the test suite continues to assert what conformant adopters MUST send. **SDK companion (filed separately as #4877).** `@adcp/client`'s auto-registered `get_adcp_capabilities` handler needs to emit `status: "completed"` for adopter responses to remain conformant; that's the going-forward fix in the SDK repo. **Body-status conflict tracked as follow-up.** The two carve-outs (`check_governance`, `report_plan_outcome`) need their body discriminator field renamed (e.g. `verdict` / `decision`) ahead of 3.1 GA. Filing as a separate spec issue. - 06abeab: spec(media-buy): add `media_buy_status` field on create_media_buy and update_media_buy success responses; deprecate top-level `status` (#4895). Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, drawn from `task-status.json`) and the body-level `MediaBuyStatus` (`status`, drawn from `media-buy-status.json`) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. The two enums overlap on `completed | canceled | rejected` and diverge elsewhere — a `MediaBuyStatus: 'active'` is silently destroyed when the envelope stamps a TaskStatus at the same path, and no validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 3.2 removal of legacy `status` (#4906) → 4.0 nested cascade (#4905)) per the issue triage. **Strictly additive in 3.1 — no schema is renamed and no `required[]` constraint changes.** - **`media-buy/create-media-buy-response.json`** (`CreateMediaBuySuccess` branch) — adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status` field. The legacy `status` is marked `deprecated: true` (description) and slated for removal in 3.2 (#4906). Both fields are optional in 3.1; neither was in `required[]` before and neither becomes required now. The `CreateMediaBuySubmitted` branch is unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator, not a MediaBuyStatus. - **`media-buy/update-media-buy-response.json`** (`UpdateMediaBuySuccess` branch) — symmetric: adds `media_buy_status`, marks legacy `status` as deprecated. Both optional. **Not in scope** (deliberate — see below): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status`. These fields live nested inside arrays at depth ≥ 1, so the envelope `status` at the response root does not collide with them on the wire. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is mildly annoying but the price of keeping the change strictly additive — renaming a nested field that 3.0 sellers already emit would require either a `required[]` swap (breaking) or a double-fielded transition (schema churn for no wire-collision payoff). Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. The synthetic `cancel_media_buy` response (issue body called this out as a separate scope question) is performed via `update_media_buy` with cancel intent — there is no dedicated `cancel_media_buy` tool. Inherits the rename from `UpdateMediaBuySuccess` for free. No separate schema change. Storyboards swept: - `protocols/media-buy/state-machine.yaml` — three `field_present path: "status"` assertions against `update-media-buy-response.json` updated to `path: "media_buy_status"`. Under additive-deprecate, 3.1-conformant sellers SHOULD emit `media_buy_status`; the assertion documents the canonical-field expectation. - `protocols/media-buy/scenarios/pending_creatives_to_start.yaml` — two `field_value` assertions checking MediaBuyStatus values against `create-media-buy-response.json` and `update-media-buy-response.json` updated to `path: "media_buy_status"`. - `protocols/media-buy/scenarios/create_media_buy_async.yaml` — left as `path: "status"`: this checks the `submitted`-arm TaskStatus discriminator, not a MediaBuyStatus. Docs: - `docs/media-buy/task-reference/update_media_buy.mdx` — the cancellation success-response example shows the canonical `media_buy_status` form. - `docs/reference/whats-new-in-3-1.mdx` — migration note in Final-spec clarifications batch. Adopter impact: - **Sellers (3.1+):** SHOULD emit `media_buy_status` on `create_media_buy` and `update_media_buy` success responses. MAY continue to emit the legacy top-level `status` during the deprecation window — both fields are valid in 3.1. - **Buyers (3.1+):** MUST prefer `media_buy_status` when present. MAY fall back to the legacy `status` during the deprecation window for compatibility with sellers still on the legacy form. - **3.0 sellers and buyers:** continue to work unchanged. The schema remains backward-compatible — no required-field swap, no rename, no breakage. The `get-media-buys-response`, `get-media-buy-delivery-response`, and `core/media-buy.json` surfaces are untouched, so the nested `status` field 3.0 emitters already produce continues to validate. - **3.2:** the deprecated top-level `status` on the success branches of `create-media-buy-response.json` and `update-media-buy-response.json` is removed (#4906). The deprecation window is intentionally short — storyboard certification already forces 3.1-conformant sellers off the legacy field, so carrying it longer would just mean SDK consumers hold two fields in generated types for no operational benefit. After 3.2, top-level `status` on these responses unambiguously carries envelope TaskStatus only. - **4.0:** the nested `status` cascade lands (#4905) — `media_buys[].status` on `get-media-buys-response`, `media_buy_deliveries[].status` on `get-media-buy-delivery-response`, and `status` on `core/media-buy.json` rename to `media_buy_status`. Genuinely breaking (a `required[]` swap), held to the major. - SDK regen required for `@adcp/client`, `adcp-go`, and the Python client. The `@adcp/client` transport precedence fix (adcontextprotocol/adcp-client#1898) already drafts the consumer-side logic. Related: - #4876 — envelope `status` REQUIRED (beta.2). - #4897 — companion governance schema rename (separate PR). - adcontextprotocol/adcp-client#1898 — SDK-side audit and transport precedence fix. - 989da51: spec(governance): rename body-level `status` on `check_governance` and `report_plan_outcome` responses to free the envelope `status` key (#4897). Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, drawn from `task-status.json`) and the body-level governance field share the same root key. The two enums overlap on `completed | canceled | rejected` and diverge elsewhere; whichever side wins on the wire, the other is silently destroyed and no validator catches it. Resolution (WG-recommended Option A per the issue triage): - **`governance/check-governance-response.json`** — `status` → `verdict`. Enum unchanged (`approved | denied | conditions`); `if/then` discriminator blocks now key on `verdict`. Renamed in `required[]`. Description threads (`findings`, `conditions`, `expires_at`) updated to reference the new name. - **`governance/report-plan-outcome-response.json`** — `status` → `outcome_state`. Enum unchanged (`accepted | findings`); renamed in `required[]`. Description thread on `findings` updated. - **`governance/get-plan-audit-logs-response.json`** — `entries[].status` → `entries[].verdict` (cascade for vocabulary consistency with check-governance-response). Other `status` fields (`plans[].status`, `governed_actions[].status`) are lifecycle states, not verdicts, and are left unchanged. Docs swept (~25 example bodies + table descriptions): - `docs/governance/overview.mdx` - `docs/governance/campaign/tasks/check_governance.mdx` (7 examples + response table + prose) - `docs/governance/campaign/tasks/report_plan_outcome.mdx` (5 examples + response table) - `docs/governance/campaign/tasks/get_plan_audit_logs.mdx` (2 nested check entries) - `docs/governance/campaign/audit-trail.mdx` (7 example bodies + field-tagging table) - `docs/governance/campaign/specification.mdx` (3 examples) Storyboards swept (the issue triage initially scoped this as "no yaml renames needed"; corrected during implementation): - `static/compliance/source/specialisms/governance-spend-authority/index.yaml` — `field_present path: "status"` → `path: "verdict"` - `static/compliance/source/specialisms/governance-spend-authority/denied.yaml` — both `field_present` and `field_value` assertions - `static/compliance/source/specialisms/governance-delivery-monitor/index.yaml` — two `field_present` assertions - `static/compliance/source/protocols/governance/index.yaml` — two `field_present` assertions plus a stale `outcome.expected` block referencing `status: recorded` (not in the enum) → corrected to `outcome_state: accepted` Adopter impact: - Wire-shape change on three experimental governance schemas (`x-status: experimental`). - Buyers and sellers rename one property name per emitter / consumer; enum values are unchanged. - SDK regen required for `@adcp/client`, `adcp-go`, and the Python client. Per the experimental-surface contract, this is a sanctioned 3.1 pre-GA adjustment. Related: - #4876 — envelope `status` REQUIRED (beta.2). - #4895 — companion media-buy collision (separate PR). - #4896 — per-task envelope fold. Once this PR lands, the carve-outs for `check-governance-response.json` and `report-plan-outcome-response.json` in #4896 can be removed; both schemas pick up the standard envelope fold cleanly. - 4adb65a: spec(errors): register `STALE_RESPONSE` for cache-fallback served when an upstream is unreachable (#4899) The existing error vocabulary covered the binary "upstream unreachable, no response" case (via `SERVICE_UNAVAILABLE`) but had no registered code for the **degraded-but-functional** case: an upstream or sub-agent is unreachable now, but the seller has a cached prior response and serves that cache instead of returning empty. Without a standard code, every seller either invents a discriminator (`STALE_CACHE` / `CACHED_FALLBACK` / `DEGRADED_RESPONSE` / ...) or returns `SERVICE_UNAVAILABLE` with a populated payload — internally contradictory, since the call did succeed from the caller's POV. This change: - Adds `STALE_RESPONSE` to `static/schemas/source/enums/error-code.json`. Recovery: `transient`. Emitted **alongside** a populated success payload as a non-fatal advisory in `errors[]`; transport-level success markers stay flipped to success (HTTP 200, MCP `isError: false`, A2A `succeeded`). Sibling to the existing per-asset advisory family (`PIXEL_TRACKER_LOSSY_DOWNGRADE`, `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE`). - Adds `error-details/stale-response.json` — required `served_from_cache: true` + `cache_age_seconds`, optional `freshness_target_seconds`, `upstream: {url, name}`, and `original_error: {code, message}`. Multi-upstream cases emit one `STALE_RESPONSE` entry per stale upstream (mirroring the per-asset advisory precedent), not one aggregated entry. - Adds the System-errors-table row in `docs/building/by-layer/L3/error-handling.mdx` with the distinction from `SERVICE_UNAVAILABLE` (empty payload + fatal). - Adds the disposition entry in `scripts/error-code-drift-dispositions.json` (`held-for-next-minor`, `target_version: 3.1`). **Normative wire rules.** Sellers MUST emit `STALE_RESPONSE` only when the response payload is non-empty AND derived from a cache entry past the surface's freshness target. When no cached entry exists or the cache hit is within freshness target, sellers MUST NOT emit this code. Buyers MUST treat as non-fatal and SHOULD surface staleness to operators or end users where relevant; `cache_age_seconds` is the informational knob for the buyer's retry policy. Closes #4899. - d08dcea: Clarify durable `sync_accounts.accounts[].notification_configs[]` semantics: omitted means unchanged, `[]` clears the account's subscribers, and a non-empty array replaces the account-scoped set keyed by `subscriber_id`. The account-level subscription surface remains limited to account-anchored resource events already defined in `notification-type.json`; it does not define `account.*` lifecycle events. Account status changes remain observable through `list_accounts` polling or the one-shot `sync_accounts.push_notification_config` async-result channel. Standardize endpoint proof-of-control for active durable webhook configs, including the challenge payload and response schemas, auth-mode binding, paused-config behavior, retry guidance, and failure semantics. - 2c5196b: spec(3.1): clarify publisher-scoped placements and product format-option selectors. Adds public placement catalog support in `adagents.json`, keeps seller-private routing fields out of public placement schemas, and introduces structured publisher-scoped `placement_refs` for creative assignment. Product placement IDs remain publisher-scoped; omitted `publisher_domain` is only a legacy single-publisher fallback. Renames the beta buy-side canonical-format selector from `capability_*` to `format_option_*`. `FormatOptionRef` now selects publisher-catalog-backed options by `{scope: "publisher", publisher_domain, format_option_id}` and product-local options by `{scope: "product", format_option_id}` in the package's target product context. Pre-GA `capability_ids` / `capability_id` request fields are rejected instead of silently accepted. - 17a648c: Add account-level `notification_configs[]` lifecycle and semantic rejection storyboards for 3.1, plus an exact `list_accounts.account` filter so buyers can re-read one account by seller account ID or natural key. The training agent now supports the new account-level notification configuration contract and the release gates exercise both current 3.1 storyboards and 3.0 compatibility. - 752d586: Add dimensional and measurement-aware fields to delivery forecast points. `ForecastPoint` now supports dimensional rows for geography, placement, device, platform, audience, and intersections such as placement x country via `dimensions`, letting sellers expose country and placement availability without splitting one sellable product into product-per-dimension variants. Forecast points also support `viewability` and `vendor_metric_values` using `ForecastRange` values so pre-buy forecasts can mirror delivery reporting while remaining independent of product `pricing_options`. Geo forecast dimensions reuse the existing metro/postal system enums, forecast viewability requires `standard` whenever forecast values are present, and proposal-level rows can carry `product_id` when a dimensional row maps back to an executable product allocation. - 2a5f0f5: feat(compliance): add typed JCS non-finite controller error Adds `JCS_NON_FINITE_NUMBER` to the comply-test-controller `ControllerError.error` enum for digest-mode `query_upstream_traffic` responses that cannot be RFC 8785/JCS-canonicalized because the parsed JSON-like value tree contains a non-finite numeric value (`NaN`, `+Infinity`, or `-Infinity`). Runner-output and storyboard contracts now state that this case grades the affected upstream_traffic digest validation as `not_applicable` and contributes to `validations_not_applicable`, not `steps_failed`. Closes #5069. - dc806c3: Clarify media-buy and creative contract edge cases for the 3.1 beta. Adds normative guidance for canonical-format matching: legacy named formats are normalized before comparison, product capability checks are directional, under-specified requests do not satisfy fixed product constraints, and range constraints require containment rather than overlap. Documents the stored-creative adapter handoff boundary: buyers send only `creative_id` on the AdCP wire, while any generic `id` alias is seller-side adapter compatibility data copied from `creative_id`. Tightens media-buy lifecycle semantics by requiring `revision` on create/get/update success responses and requiring `confirmed_at` on created/read media buys while allowing `null` only for provisional buys that already have a `media_buy_id` and are retrievable before seller commitment. This is a 3.1 beta schema tightening that catches the schemas up to existing normative `MUST` text for `revision` and commitment timestamps, rather than a new post-GA contract. The nullable `confirmed_at` shape is buyer-observable (`string | null` instead of only `string`) so buyers can distinguish committed synchronous creates from provisional buys that exist but are not yet seller-committed. - 47001d6: spec(brand): add machine-readable brand guideline constraints Adds optional `logos[].id`, `logos[].slots[]`, canonical format `logo_slots[]` and `required_logo_slots[]` hints, plus `visual_guidelines.color_constraints[]`, `logo_usage_rules[]`, and `mark_lockups[]` to make guideline rules enforceable: color pairing matrices, deterministic logo slot selection, logo usage contexts, and co-brand/secondary-mark lockups. Includes two schema-valid fictional fixtures that exercise the new surface without adding real-brand public examples. - 4d632f7: Add optional `ext` fields to discovery filters for vendor-namespaced, seller-specific criteria. This closes the schema gap surfaced by adcp-go#277 and tracked for follow-up in adcp-go#279: `product-filters.json` already allowed extension keys via `additionalProperties: true`, but did not expose the protocol-standard `ext` slot. The same request-side filter pattern applied to creative and signal discovery filters. Existing wire payloads remain compatible, while generated SDKs can now surface discoverable extension objects. - e5c2694: spec(creative): signal-driven creative fan-out (`signal_conditions[]`) + item-selection strategy (`selection_strategy`), folding #5262. Implements RFC #5240 (accepted 2026-06-03; ships `x-status: experimental`). Adds a keep-all PRODUCTION axis for signals to `build_creative`, sibling to the catalog fan-out axis (`max_creatives`, #5219) and distinct from the choose-among `variant_axis`. Rides #5280's advisory-pointer contract: signal pointers inform production but MUST NOT hard-block at the build layer; trafficking-compatibility is enforced reject-at-trafficking on the sales side. Strictly additive — no existing agents break. All new fields optional and gated by new capability flags; agents that don't advertise `supports_signal_fanout` behave exactly as today. **Experimental.** The whole signal-fanout surface ships `x-status: experimental` under feature id `creative.signal_fanout` (sellers implementing it MUST list it in `experimental_features`), mirroring `creative.evaluator` (#5305). It introduces a new, not-yet-field-tested cross-agent reject-at-trafficking MUST (`SIGNAL_TARGETING_INCOMPATIBLE`), and the numeric condition-compatibility comparison (range-overlap vs exact-match) plus the `proximity` geo-input binding stay WG-open — experimental status keeps both revisable per [experimental-status](/docs/reference/experimental-status). - `media-buy/build-creative-request.json` — optional `signal_conditions: SignalTargeting[]` (reuses `core/signal-targeting.json` via `allOf`, NOT a new minted signal-ref) plus an optional `signal_agent_segment_id` on each condition — the RESOLVED-segment identity (vs `signal_ref`'s definition identity) the buyer echoes verbatim from `get_signals` / product `signal_targeting_options`; it is the primary trafficking-compatibility key, with categorical `signal_ref`+value the weaker fallback. Also optional `selection_strategy` (new enum). - `enums/creative-selection-strategy.json` — NEW closed string enum `[audience_relevance, contextual_fit, performance, proximity, inventory_priority, random]` (folds #5262; mirrors the closed shape of `creative-quality.json`). - `protocol/get-adcp-capabilities-response.json` — `creative.multiplicity`: `supports_signal_fanout`, `max_signal_conditions_limit` (clamp like `max_creatives_limit`), `selection_strategies[]`. - `media-buy/build-creative-response.json` — `BuildCreativeVariantSuccess.creatives[].signal_condition` + top-level `selection_strategy_applied`; `BuildCreativeEstimate.estimate.conditions_total`. - `enums/error-code.json` — `SIGNAL_TARGETING_INCOMPATIBLE` (recovery: correctable) in enum + `enumDescriptions` + `enumMetadata`, with a drift disposition. The normative cross-agent trafficking-compat MUST that warrants the RFC. The compatibility algorithm is spelled out: exact `signal_agent_segment_id` match when both sides carry it; categorical `signal_ref`+value-set comparison otherwise; equal categorical labels from DIFFERENT providers are never compatible absent an explicit equivalence mechanism; mixed segment-handle/categorical only matches when the seller resolves both to the same provider-issued segment. - `core/package-signal-targeting.json`, `core/product-signal-targeting-option.json`, `signals/get-signals-response.json`, `docs/media-buy/advanced-topics/targeting.mdx` — clarify that `signal_agent_segment_id` is the opaque, provider-scoped RESOLVED-segment handle buyers echo verbatim (preferred over reconstructing identity from categorical values); providers MAY namespace handles so cross-provider identity stays legible without a shared taxonomy registry. - `docs/creative/buyer-attached-inputs.mdx`, `docs/creative/task-reference/build_creative.mdx`, `docs/signals/specification.mdx`, `docs/media-buy/task-reference/create_media_buy.mdx` — request/response field docs, the trafficking-compatibility contract narrative, and the reject-at-trafficking note. Consolidates the parallel exploration in #5315 (segment-handle identity, namespaced provider IDs, trafficking-compat rules) into this single RFC-impl PR rather than a second RFC for #5240. Closes #5240, #5262. Refs #5219, #5280, #5315. - 5fc5283: feat(creative): advisory evaluator with gate-then-rank pipeline for build_creative (#5241, #5305) Adds an optional, advisory `evaluator` input to `build_creative` (a buyer-attached pointer, #5280) and a per-leaf `eval` block on `BuildCreativeVariantSuccess` variants that explains the `recommended`/`rank` the agent already sets on the `best_of_n` axis. The evaluator is the rank-side of the `get_creative_features` feature oracle and drives a **gate-then-rank pipeline** over the producing agent's best-of-N exploration, per leaf: evaluate (chosen source form) → optional hard **gate** (`feature_requirement[]`, drop fails) → **rank** the survivors (`rank_by`). - **Gate (#5305 Q1):** `evaluator.feature_requirement[]` reuses the `feature-requirement` predicate (its schema already names creative gates as an intended reuse) — a leaf that fails is dropped from the agent's recommended survivors. This is internal best-of-N pruning, not an AdCP-layer block of an already-produced billable leaf: what is produced and billed stays governed by `max_variants`/`max_creatives`/`max_spend`, preserving the advisory invariant. The buyer may attach a get_creative_features-capable agent (`evaluator.feature_agent`, or the `agent_url` source form) the producing agent calls to obtain the gate's feature values; that agent is subject to the seller's `creative_policy.accepted_verifiers[]` allowlist — the same buyer-represents → seller-calls mechanism #5280 established for provenance `verify_agent`, no new allowlist. An off-list agent is rejected with a new `EVALUATOR_AGENT_NOT_ACCEPTED` error (mirrors `PROVENANCE_VERIFIER_NOT_ACCEPTED`; added to the enum, enumDescriptions, and enumMetadata). - **Rank (#5305 Q2):** `rank_by` is an explicit ordered `[{feature_id, direction: maximize|minimize}]` (not the predicate shape, which has no sort direction) over the gate survivors. - **Exemplars (#5305 Q3):** the exemplars form calibrates a single agent-defined `predicted_performance` feature (value in [0,1]) the evaluator computes and returns in `eval.features[]`; `rank_by` orders on it. All three forms thus resolve to "produce a feature value, gate/rank on it." - **One contract (#5305 Q4):** the `agent_url`/`feature_agent` evaluator agent uses the same `get_creative_features` contract (returns `creative-feature-result[]`) used for gate, rank, and provenance. - **Verdict (Q6):** a pass/warn/fail check is a categorical string feature value gated via `feature_requirement.allowed_values`; the verdict is derived, never stored on `creative-feature-result` (which stays closed: value `oneOf bool|number|string`). - **Telemetry (Q7) / type (Q8):** `eval.calls_used`/`seconds_used` live on the open `eval` wrapper; `eval.features[]` is `creative-feature-result[]` (wrapper open, items closed). The `build_creative` Request parameters table gains an `evaluator` row. New schema: `core/evaluator-spec.json` (3-form oneOf: exemplars / evaluator_id / agent_url, an optional hard `feature_requirement[]` gate, an explicit `rank_by` ordering, an allowlisted `feature_agent` pointer, plus a soft `eval_budget`). Gated by a new `creative.supports_evaluator` capability flag. Targets 3.1 (the line where `get_creative_features` finalizes). Non-breaking, fully additive / optional. **Experimental.** The whole evaluator surface — the `evaluator` input, the `eval` response block, `creative.supports_evaluator`, and `core/evaluator-spec.json` — ships `x-status: experimental` under the feature id `creative.evaluator` (sellers that implement it MUST list it in `experimental_features`). It is a new, not-yet-field-tested gate-then-rank surface, and the `evaluator_id` form's discovery surface (`list_evaluators`) is a committed 3.x follow-on rather than shipping now — so per [experimental-status](/docs/reference/experimental-status) the surface MAY change between 3.x releases with notice, rather than being frozen under full 3.x stability guarantees before cross-party integration. Reserved follow-ons that may reshape these fields: `list_evaluators` discovery, a separate `supports_evaluator_gate` capability, and a hard MUST-enforce-gate semantic. - af3e682: Add optional `last_updated` (date-time) to `signal-definition.json`, `signal-definition-enrichment.json`, and the `get_signals.fields` projection enum. Closes the signal-record freshness gap raised in #5248. `refresh_cadence` and `lookback_window` describe methodology freshness; `last_updated` tells buyer agents when the seller last published or updated this specific definition record — the one verifiable freshness signal that agents can compare across providers without trusting self-declared methodology claims. Description follows `signal-listing.json` precedent: "When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time." Adding to `signal-definition-enrichment.json` means the field is also projectable through `get_signals.fields` for buyers that want it inline during discovery without fetching the full definition. - f8c389d: spec(media-buy): add optional `publisher_domain` to `get_media_buy_delivery` `by_placement` rows (closes #5299). `by_placement` rows carried only `placement_id` and `placement_name`, so a buyer running across multiple publishers through one sales agent could not attribute delivered impressions to a publisher namespace without re-fetching `get_products` and cross-referencing the product's `placements[]` — a round-trip that requires retaining the buy-time catalog and breaks for inline placements. Changes: - `static/schemas/source/media-buy/get-media-buy-delivery-response.json` — add an optional `publisher_domain` (with the same domain regex as `core/placement.json`) to `by_placement` row items. It is a flat sibling of the existing `placement_id`/`placement_name` (not a nested PlacementRef — the row already ships those fields flat, so nesting would break consumers). Sellers SHOULD emit it whenever the resolving product placement carries a `publisher_domain` (always true for `kind: publisher_ref`); MAY omit only for `seller_inline` placements in a legacy single-publisher context. Single-valued because a placement resolves within exactly one publisher namespace. While in the block, add the missing `x-entity: "placement"` annotation to `placement_id` for parity with `core/placement.json` and `core/placement-ref.json`. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx` — note the optional `publisher_domain` field under "Available dimensions". Strictly additive — no existing field changes shape, no new required fields. `by_placement` rows are already `additionalProperties: true`, and the obligation is SHOULD-when-known (not a retroactive MUST), so pre-existing single- and multi-publisher reports remain spec-valid. Package-level publisher attribution on `get_media_buys` (the PackageStatus proposal in #5299's comments) is intentionally out of scope: an ad-network product can span multiple publishers, so a scalar there has an unresolved cardinality question (scalar-absent-when-multi vs. plural). This change covers only the placement grain, where the scalar is sound. - 68039f7: schema: allow hosted audio/video duration ranges to omit one endpoint. Hosted `duration_ms_range` now supports one-sided ranges such as `[null, 60000]` for "up to 60 seconds" and `[15000, null]` for "at least 15 seconds", while rejecting `[null, null]`. This keeps duration constraints to two mechanisms: `duration_ms_exact` for fixed durations and `duration_ms_range` for bounded or one-sided ranges. - 085fa58: Clarify async discovery webhook registration for `get_products` and `get_signals`. Adds optional `push_notification_config` to the `get_products` and `get_signals` request schemas for curated/semantic discovery modes, adds the `get_signals` working/submitted async envelopes to the webhook result union, allows failed discovery completions to omit success payload arrays, documents that `submitted` tasks remain pollable via `get_task_status` (legacy `tasks/get`) even when webhook notifications are configured, requires accepted webhook configs to receive at least terminal completion/failure notifications, and preserves the synchronous wholesale feed rule (`get_products` `buying_mode: "wholesale"` and `get_signals` `discovery_mode: "wholesale"` MUST NOT use the Submitted arm). - 630599e: Clarify that `inline_creative_management` covers inline package creatives on `create_media_buy` and `update_media_buy` independently of Creative Protocol support, and add compliance coverage for sellers that accept inline creatives without `sync_creatives`. - 3281278: Document the live community-mirror lifecycle endpoints in the registry OpenAPI spec so SDKs can generate typed request and response models instead of hand-rolling DTOs. - 69cb5ca: schema(brand): raise brand.json `agents[]` maxItems 20 → 200 for multi-tenant operators, and reconcile the JWKS size budget The per-tenant JWKS pattern blessed in #5458 is one `agents[]` entry per tenant, but the `maxItems: 20` cap made a >20-tenant `brand.json` schema-invalid — below the scale the multi-tenant case is actually about. Raises the cap to 200 (additive and non-breaking — loosening a `maxItems` never invalidates an existing valid document). Also reconciles the two JWKS size figures in L1 security so a conservative verifier can't reject a conformant shard: the 64 KiB `MAX_JWKS_BYTES` is the JWKS-specific budget (deliberately tighter than the generic 5 MB SSRF body ceiling), and per-tenant `jwks_uri` sharding is the conformant path above it — for size as well as key isolation. Closes #5445. - cb3b658: Define adagents.json discovery redirect policy and reconcile the reference implementation with it. The initial `/.well-known/adagents.json` fetch now follows **same-registrable-domain** redirects (apex↔www, HTTPS-preserving, ≤3 hops, SSRF re-validated per hop, anchored on the originally-requested domain) so that standard apex→www managed hosting resolves instead of being silently reported unauthorized. **Cross-registrable-domain** redirects are refused — declare delegation with `authoritative_location` instead — and the `authoritative_location` dereference continues to refuse all redirects. Docs: managed-networks "Why not HTTP redirects?" and L1 security SSRF/TLS-hardening sections; new conformance vectors in `static/test-vectors/adagents-discovery-redirects.json`. - 85411b1: Add optional `status_as_of` freshness timestamp to `get_media_buys` media-buy objects. The field lets sellers identify when a returned media-buy-level `status` was last refreshed from the source of truth, covering cached or rolled-up list reads from curator/storefront aggregators. Sellers omit it or return `null` when status is live or freshness is unknown. - e815fc8: Add brand-side sponsored_context_accountability storyboard under `compliance/source/protocols/sponsored-intelligence/sponsored-context-accountability.yaml`. Refs #5541 (bragent conformance testbed offer) and #5486 (RFC: sponsored context influence modes and disclosure obligations for SI). Exercises the PR #5501 surfaces against a brand-side SI agent in four phases inside a single yaml so the review surface stays small and the contract is visible together: - `presentation_only_happy_path` — agent emits a `sponsored_context` envelope with `paying_principal.brand.domain`, `context_use=presentation_only`, `disclosure_obligation`, and `declared_by.role=brand_agent`; host returns an accepted receipt with matching `accepted_context_use`; second brand turn lands cleanly. - `required_disclosure_commitment` — literal `sponsored_context` carries `disclosure_obligation.required=true`; host's receipt carries `disclosure_commitment.status=accepted`; agent accepts the well-formed receipt without error. - `rejected_receipt` — host returns `host_receipt.status=rejected` with a `rejection_reason`; agent accepts the rejection as a valid wire response (the audit trail records the decline). - `silent_downgrade_rejected` — host returns an accepted receipt whose `accepted_context_use` does not match the declared `context_use`; the agent MUST reject. Regression anchor is `error_code ∈ {VALIDATION_ERROR, INVALID_REQUEST}` (the canonical AdCP enum); the recommended "silent downgrade forbidden" message wording stays in the step's `expected:` text as a manual-review pointer, not a hard check (promoting it would require a new `error_message_contains` matcher in the runner). Uses only the existing storyboard matchers (`response_schema`, `field_present`, `field_value`, `error_code`). LLM-generated `response.message` is asserted as present/non-empty only, so language and provider are implementation choices. bragent (kapoost/bragent, v0.2.0+) serves as the empirical reference surface from which the assertions were derived; the storyboard itself is decoupled from any live service. - 63e58c3: spec(conformance): AAO Verified — one brand mark, two qualifiers (Spec) and (Live) Adds **AAO Verified** as the public trust mark for AdCP agents, with two composable qualifiers in parens — **(Spec)** and **(Live)** — that an agent can hold either or both: - **AAO Verified (Spec)** — your AdCP wire format matches the spec. Storyboards run against your test-mode endpoint on AAO's compliance heartbeat. Issued automatically when storyboards pass for the agent's declared specialisms + active AAO membership. - **AAO Verified (Live)** — AAO has observed real production traffic flowing through your agent. The compliance engine continuously watches delivery against your live ad-server integration over a 7–14 day rolling window. Lights up in 3.1 once the canonical-campaign runner is operational; the eight-check observability machinery already ships. **(Spec) and (Live) are independent.** Each axis demonstrates conformance through different evidence — (Spec) via simulated interactions against a test endpoint, (Live) via observed real traffic that exercises wire format, filters, lifecycle, and scope through the eight checks. Sellers without a test-mode endpoint (SDK-built agents, production-only platforms) can earn (Live) directly. The two qualifiers share one brand mark — buyers learn one name, the qualifier in parens names which axis was earned. Earlier drafts used "AdCP Conformant" + "AAO Verified" as two distinct mark names (and earlier still, "Tier 1 / Tier 2"). The single-brand-with-qualifiers framing is cleaner: a test agent earning **Verified (Spec)** is a complete claim, not a "junior" tier. Seller obligation for (Live): designate a compliance account with real live campaigns (PSA / remnant / house / genuine revenue all qualify) and grant the `attestation_verifier` scope (#2964) to the AAO compliance engine. Eight observable checks run over the rolling window. Path B (brownfield) has two first-class forms — B1 polling-only, B2 webhook-attached. Mark lifecycle: continuous observation, auto-expiring on signal degradation, no one-shot pass. Closes #2965. Depends on #2964 (`attestation_verifier` scope + RBAC error codes) and the merged #2963 account-ownership tightening. Multi-subscriber webhooks (which relax the dedicated-tenant requirement on Path B2) tracked for 4.0 in #3009. - 63e58c3: spec(accounts): caller-scope introspection via per-account `authorization` on sync/list + RBAC error codes Caller-scope authorization model for AdCP. Vendor agents (media-buy, signals, governance, creative, brand) attach an optional `authorization` object to each per-account entry in `sync_accounts` and `list_accounts` responses — describing `allowed_tasks`, per-task `field_scopes`, an optional standard `scope_name`, and an optional `read_only` flag. Absence means the vendor agent does not advertise introspectable scope; callers MUST NOT infer access from absence. Conceptually analogous to RFC 7662 OAuth 2.0 Token Introspection, specialized for AdCP's task-and-field authorization model and folded into existing account discovery rather than split into a new task. Standard named scope `attestation_verifier` is spec-mandated (binds to the AAO Verified (Live) qualifier; Media Buy Protocol). Other scope names are vendor-specific and MUST use the `custom:` prefix so a typo of the standard value fails schema validation. Three new error codes surface RBAC decisions that previously had no standard code: `SCOPE_INSUFFICIENT`, `READ_ONLY_SCOPE`, `FIELD_NOT_PERMITTED`. `FIELD_NOT_PERMITTED` MUST populate `error.field`; `SCOPE_INSUFFICIENT` SHOULD carry an `introspection_hint` pointing at where to re-read scope. All four authz codes classify as `correctable` but are NOT agent-autonomous (scope broadening requires operator intervention) — agents SHOULD surface rather than auto-retry. Identity binding, refresh cadence, and consistency are normative: the authorization object is scoped to `(caller identity, account_id)` at read time; vendor agents MUST resolve identity from the authenticated request (not client-supplied fields) and reflect operator-initiated scope changes within 300 seconds. Sequential reads within the refresh window MUST return identical authorization objects (modulo operator-initiated changes) — flicker from load-balanced or eventually-consistent backends is non-conformant. Closes #2964. - 1e76c74: spec(brand): `account` on AcquireRights/UpdateRights + governance-bound CPM projection rule Coupled spec gaps surfaced while validating a multi-tenant + multi-specialism hello adapter (per #3918): 1. **`acquire_rights` and `update_rights` accept `account: AccountReference`.** Governance-aware brand agents need brand+operator (or `account_id`) to look up any governance agent previously bound via `sync_governance`. The brand-rights compliance storyboard already sends `account: { brand, operator }` on the wire for `acquire_rights`, but the schema didn't define the field — adapters were falling back to `req.buyer.domain` (the brand, not the operator) for account resolution. `update_rights` had the same shape gap and is also a modification-phase governance trigger per the campaign-governance spec. Both fields are optional, follow the same shape `create_media_buy` uses. 2. **CPM-projection MUST broadened to cover the bound path on `acquire_rights`.** `acquire-rights-request.json` previously required `campaign.estimated_impressions` only when the request carried an intent-phase `governance_context` token AND the pricing option was CPM. Brand agents that resolve their governance binding via `sync_governance` (no inline token) still project CPM commitment — and "implementer-chosen defaults are non-conformant" applies equally there. The MUST now covers both paths: the request is governance-aware whenever an inline `governance_context` is present OR `account` resolves to an account with a bound governance agent. Non-CPM pricing options remain unaffected. The equivalent commit-delta projection rule for `update_rights` is left for a follow-up — it requires designing the delta semantics (impression_cap delta vs. pricing_option-switch delta) and is not yet normative. 3. **Inline-token-wins precedence.** When both an inline `governance_context` token and a bound governance agent are present on the same request, the inline token wins. The token is per-request, JWS-bound to a specific plan, and is the primary correlation key; the bound agent is the resolver fallback. Stated in the `account` field descriptions and in the `acquire_rights` task reference. 4. **`sync_governance` doc-comment clarifies account-scoped binding.** Adopters were reading the existing description as ambiguous on whether the binding could vary per plan inside the same account. The wire offers no field for per-plan governance agents (and `maxItems: 1` plus the singular `governance_context` envelope foreclose it). Description now states explicitly: binding is account-scoped, not plan-scoped; a single bound agent owns the lifecycle for every plan on the account; `plan_id` is threaded through `check_governance` for per-plan routing inside the bound agent, not at the registration layer. Also fixes a stale anchor in the `acquire_rights` validation prose (`#buyer-side-governance-invocation` → `#spend-commit-invocation`). Closes the wire-schema items on #3918 (`account` on acquire_rights/update_rights, broadened MUST, `plan_id` ambiguity). The two items deliberately not included: `plan_id` as a sync_governance field (conflicts with the documented account-wide binding), and loosened HTTPS pattern (better solved in the storyboard runner than by relaxing the wire spec). - 6d9646e: Activate public AAO Verified badge issuance for AdCP 3.1 while keeping AdCP 3.0 compatibility badges active. The badge-eligible default compliance target is now the 3.1 line, with explicit non-default targets such as `3.0`, exact 3.1 beta targets, or future exact 3.1 RC targets remaining diagnostic-only for public compliance state. Registry and Addie outputs now surface whether a compliance run can update public badges and which badge versions it can issue. Closes #5108. - cf0857e: adagents.json: allow catalog-only community mirrors (empty `authorized_agents`). The inline `adagents.json` variant required `authorized_agents` with `minItems: 1`, which made the community-mirror use case the spec itself describes — catalog-only files (e.g. at `creative.adcontextprotocol.org/translated//adagents.json`) for platforms that haven't adopted AdCP — impossible to express, since such a mirror has no sales agent to authorize. It is also the exact `authorized_agents: []` shape the SDK's `buildCommunityMirrorAdagents()` emits, which `POST /api/adagents/create` rejected with a 400. - **Schema:** `authorized_agents` may now be empty (`[]`); `minItems: 1` is dropped. A new content guard requires a file to carry either sales authorization or a non-empty catalog array (`formats`/`properties`/`placements`/`collections`/`signals`), so a file with neither is still invalid. `catalog_etag` remains recommended-not-required at the schema layer (the mirror contract is enforced by the producer/SDK, consistent with "SDK is canon for wire contracts"); the schema only widens what was previously rejected, so every file valid today stays valid. - **Registry:** `POST /api/adagents/create` and the proposed-file validator accept an empty `authorized_agents` when catalog content is present. - **Consumer semantics:** an empty `authorized_agents` asserts _no sales authorization_ — validators MUST NOT read it as deny-all, authorize-all, or a revocation, MUST NOT treat it as an error, and MUST still consume the catalog arrays. - The Meta community-mirror example now uses `authorized_agents: []` instead of a fabricated advisory agent. - 556edf3: Extend `check:platform-agnostic` lint to cover enum and const values; fix `brand.json` platform-agnosticism violation. **Lint extension (`tests/check-platform-agnostic.cjs`):** adds enum/const-value scanning alongside the existing property-name check. Uses a path-qualified `ENUM_VALUE_ALLOWLIST` so the same vendor token can be legitimate in one enum (e.g., `roku` in `enums/genre-taxonomy.json`) but a violation in another. Pre-compiles vendor-token regexes. Skips `examples` arrays (user-data samples, not normative definitions). Title/description text intentionally excluded — vendor names in prose are permitted per spec-guidelines. **Schema fix (`static/schemas/source/brand.json`):** removes the single-value enum `["openai_agentic_checkout_v1"]` from `product_catalog.agentic_checkout.spec` and replaces it with a free-form `string`. The enum encoded a specific vendor's checkout API version as a normative discriminator, violating the platform-agnosticism rule in `docs/spec-guidelines.md`. Non-breaking: existing data using `"openai_agentic_checkout_v1"` remains valid. **Note:** `openai_product_feed` in `brand.json`'s `feed_format` enum is contested (see #2439): one expert treats it as a violation; another treats it as a canonical feed-schema identifier parallel to `google_merchant_center`. It is allowlisted pending @bokelley's decision. Closes #2439. - 806b7cf: feat(registry): add optional `tracks_silent` to `ComplianceRun` schema Adds an optional `tracks_silent: integer` field to `ComplianceRun` in `openapi/registry.yaml`, alongside the existing `tracks_passed`, `tracks_failed`, `tracks_skipped`, and `tracks_partial` fields. `tracks_silent` counts tracks where every observation-based invariant ran but received no lifecycle resource events during the run — configured but not exercised. Counting these separately from `tracks_passed` lets dashboards avoid over-crediting silent tracks as real protection. The field is **optional** (not in `required:`) for back-compat with runs persisted before SDK 6.4.0 (`adcp-client#1163`), which widened `TrackStatus` with `'silent'` and started emitting `tracks_silent` in `ComplianceSummary`. Without this schema addition, downstream services deserialize pre-existing runs with `tracks_silent: undefined` and cannot render silent rows distinctly. Non-breaking: adds an optional field; existing consumers unaffected. Closes #3752. - 2a2e5c4: spec(errors): register `AGENT_SUSPENDED` / `AGENT_BLOCKED` codes + consolidate the 3.0.5 `details.status` placeholder. Two new error codes for the per-buyer-agent commercial-status axis (sibling to `ACCOUNT_SUSPENDED` / `CAMPAIGN_SUSPENDED`, scoped to the agent-relationship), both `recovery: terminal`. The code itself is the discriminator — no `error.details.scope` field, no `error.details` payload — mirroring `BILLING_NOT_PERMITTED_FOR_AGENT`'s discriminator-by-code precedent. 3.0.5 shipped `error-details/agent-permission-denied.json` with a `details.status: ["suspended", "blocked"]` axis as a placeholder while the dedicated codes were being designed. 3.1 consolidates the placeholder: the `status` field is removed from the schema; sellers MUST emit `AGENT_SUSPENDED` / `AGENT_BLOCKED` directly. The schema's `agent-permission-denied.json` now carries only `scope: "agent"` + `reason: "sandbox_only"` for non-status per-agent provisioning gates. `oneOf` exclusivity drops out (single payload axis), `reason` becomes required. Migration: sellers that integrated against the 3.0.5 placeholder shape MUST switch to the dedicated codes. The known adopter (JS SDK BuyerAgentRegistry, [adcp-client#1269](https://github.com/adcontextprotocol/adcp-client/issues/1269)) is in Phase 1 placeholder mode, not production — the consolidation is intentional and is the reason 3.1 is the right release for it. The DX-expert "wire-level recovery field ambiguity" gap from #3887 review closes for the suspended/blocked paths — those paths now carry `recovery: terminal` directly at the wire level. Same cross-tenant onboarding oracle clamp + channel-coverage rules established in #3887 apply uniformly to the new codes. Closes #3871. Builds on #3887. Files: - `static/schemas/source/enums/error-code.json` — `AGENT_SUSPENDED` / `AGENT_BLOCKED` enum + descriptions + `enumMetadata.recovery: "terminal"`. `PERMISSION_DENIED` description points at the new codes for suspended/blocked. - `static/schemas/source/error-details/agent-permission-denied.json` — `status` field removed, `oneOf` removed, `reason` required. - `docs/building/implementation/error-handling.mdx` — Authorization (RBAC) table adds `AGENT_SUSPENDED` / `AGENT_BLOCKED` rows. Per-Agent Authorization Gate subsection rewritten to cover all three paths (`AGENT_SUSPENDED`, `AGENT_BLOCKED`, `PERMISSION_DENIED + scope:"agent" + reason:"sandbox_only"`) under a single uniform clamp + composition-pattern guidance + 3.0.5 → 3.1 migration note. - d597efe: spec(compliance): pin endpoint_pattern wildcard grammar + downgrade non-JSON match modes to not_applicable (closes #3845) Two implementation-surfaced ambiguities from runner-side adoption of #3816 (the anti-façade + cascade-attribution contract). Both are minor-but-load-bearing pins that affect cross-runner determinism on the same storyboard. **1. `endpoint_pattern` wildcard grammar.** `comply-test-controller-request.json` previously described `endpoint_pattern` as a "glob-style pattern" with no normative grammar. The `@adcp/sdk` runner picks the most permissive interpretation (`*` matches `/`-crossing, all other regex metacharacters escaped literally). A different runner could legitimately read "glob-style" and ship POSIX glob semantics where `*` doesn't cross `/` and `?` is single-char-any — same storyboard, different verdict. Pinned: `*` matches zero or more characters of any kind including `/`. No other characters have wildcard semantics — `?` is a literal question mark, `[`/`]` are literal brackets. Implementations MUST anchor the pattern (full-string match). Renamed "glob-style" → "wildcard" in the description so the grammar's intentional narrowness is obvious from the noun. **2. Non-JSON `payload_must_contain` match modes downgrade to `not_applicable`.** The earlier comment in `storyboard-schema.yaml` said the runner "falls back to substring matching for `match: present`" against non-JSON payloads (form-urlencoded, multipart, plain text). The `@adcp/sdk` runner implemented this as a terminal-key heuristic (extract `hashed_email` from `users[*].hashed_email`, substring-search the raw payload string). That creates false positives: a payload mentioning `hashed_email` anywhere — URL fragment, comment, unrelated metadata field — would pass the assertion. For an anti-façade contract specifically, false positives are exactly what lets façades pass. Per the option-(b) decision in #3845: ALL `payload_must_contain` match modes (`present` / `equals` / `contains_any`) now grade `not_applicable` against non-JSON `content_type`. Storyboards that need a "the upstream call carried this value" signal against non-JSON payloads use `identifier_paths` instead — that surface substring-searches storyboard-supplied VALUES (not path-derived strings), which is encoding-agnostic and doesn't suffer the false-positive surface. **Why both belong in spec, not runner docs.** #3816 explicitly framed itself as the load-bearing anti-façade contract that distinguishes a real adapter from a façade. Two compliant runners grading the same storyboard differently against the same agent (because of unspecified wildcard / substring semantics) means adopters can game whichever runner is more permissive. Pinning these is small but the divergence cost is high. **Cross-link:** SDK PR `adcontextprotocol/adcp-client#1289` is the runner-side adoption that surfaced both ambiguities; runner needs a follow-up alignment to drop the terminal-key fallback now that the spec downgrades non-JSON matches to `not_applicable`. - 5a0a792: Add compliance storyboards for async `get_products` and `get_signals` discovery. The new optional cases force submitted discovery envelopes, verify task visibility through `list_tasks`, force deterministic completion, poll `get_task_status` with terminal results, and assert terminal webhook delivery. Also adds `get_products` to the task-type enum, documents the new controller directives `force_get_products_arm` and `force_get_signals_arm`, and aligns account scoping across legacy and alias task polling schemas. - d80ee8e: Add `audio_distribution_types` discovery metadata to products, placements, and `get_products.filters`, using IAB Tech Lab/OpenRTB 2.6 `audio.feed` definitions with AdCP-native field names. - da8b053: spec(errors): split `AUTH_REQUIRED` into `AUTH_MISSING` (correctable) + `AUTH_INVALID` (terminal) `AUTH_REQUIRED` conflated two operationally distinct cases: missing credentials (genuinely correctable — agent provides creds and retries) and rejected credentials (terminal — expired/revoked tokens require human rotation, not auto-retry). A buyer agent honoring `correctable` on revoked keys will retry-loop, hammering seller SSO endpoints in a pattern indistinguishable from a brute-force probe. **New codes:** - `AUTH_MISSING` — `Recovery: correctable`. No credentials were presented; agent re-handshakes and retries. - `AUTH_INVALID` — `Recovery: terminal`. Credentials were presented and rejected (expired / revoked / malformed signature). Requires human-driven credential rotation; auto-retry is counterproductive. **Backward compat:** `AUTH_REQUIRED` is retained in the enum as a deprecated alias (recovery: correctable) during the 3.x deprecation window. Sellers MUST migrate to the split codes; agents MUST handle all three. The `error-code-aliases.json` linter registry now maps `AUTH_REQUIRED → AUTH_MISSING` so storyboard references emit warnings. **Related:** adcp-client#1135 (TS SDK error-code drift fix that surfaced this spec gap), adcp-client#1147 (typed-error recovery alignment). Closes #3730. - a1067d0: Add optional `scopes`, `valid_from`, and `valid_until` fields to `brand.json` `authorized_operators[]` so houses can time-box and activity-scope agency-of-record or delegated-operator relationships. Existing entries remain valid when these fields are omitted. - 21fd8f3: spec(accounts): billing-gate conformance storyboard + BrandAuthorizationResolver naming guidance Tier-3 follow-up to #3828 / #3831 (BuyerAgentRegistry spec backing). **Validated end-to-end against the training-agent reference implementation in #3851** — running the storyboard against a real agent surfaced three bugs that lint couldn't catch, all corrected before this PR went ready: 1. `check: error_code` doesn't accept a `path` parameter for per-account error extraction → switched to `check: field_value` with explicit path on both gate phases. 2. `expect_error: true` requires transport-level error markers (MCP `isError` / A2A `failed`) — sync_accounts produces transport-level success with per-account errors in the success envelope, not transport-layer failures → removed the flag from both gate phases with explanatory comment. 3. Idempotency-key reuse across reject/recover phases produced `IDEMPOTENCY_CONFLICT` (same key + different payload per error-handling.mdx) → recover phase now uses a fresh idempotency_key with a distinct stability tag, and both the narrative and recover-phase docs corrected to reflect that the recover phase is a new request rather than a replay. Plus one runner-side gap documented in the test kit: today's storyboard runner does not auto-extract `auth.api_key` from the test kit; callers pass it explicitly via `--auth`. The kit's `auth.api_key` declares the bearer the seller's harness expects to be authenticated under; the CLI carries it onto the wire. Storyboard now passes 3/3 strict assertions against the training-agent's per-agent-gate flow (capability_discovery + per_agent_gate_reject + per_agent_gate_recover); capability_gate phase grades `not_applicable` when the seller advertises all three billing values, which is the correct outcome against the training-agent. **Conformance.** New universal storyboard `billing-gate-dispatch` under `static/compliance/source/universal/` exercises the two-gate dispatch contract on `sync_accounts.billing` rejection: - Capability gate (`BILLING_NOT_SUPPORTED` with `error.details.scope: "capability"` and `error.details.supported_billing` echo). Skipped when the seller supports all three `billing` values. - Per-buyer-agent gate (`BILLING_NOT_PERMITTED_FOR_AGENT` with the clamped `error.details.rejected_billing` + optional `error.details.suggested_billing`). Skipped when the test kit does not declare `commercial_relationship: "passthrough_only"`. Recovery phase chains off the rejection and validates that retrying with the seller's `suggested_billing` produces a successful provisioning. The storyboard also asserts the negative-shape security clamp on the per-agent gate: `error.details` MUST NOT carry `permitted_billing` (full subset), `rate_card`, `payment_terms`, `credit_limit`, or `billing_entity` — these are the per-agent commercial-state oracles that `error-details/billing-not-permitted-for-agent.json` (`additionalProperties: false`) closes off. Conformance catalogs (`docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx`) updated; doc-parity lint clean. The storyboard documents two follow-ups it does not yet land: 1. `comply_test_controller` `seed_buyer_agent` extension to toggle the test caller's `commercial_relationship` programmatically — would let any seller exercise both per-agent branches without a manually-curated test kit. 2. Test-kit field schema for `commercial_relationship` (currently referenced in `skip_if` expressions; needs a normative test-kit schema entry). **SDK naming.** Adds normative guidance to `accounts-and-agents.mdx` Buyer-agent identity section: SDKs surfacing a typed Protocol for the brand-operator authorization check MUST name it after the file consulted — `BrandAuthorizationResolver` (or idiomatic equivalent), NOT `AdagentsResolver`. `adagents.json` is publisher-side and models a different relationship; naming the buyer-side resolver after it confuses surfaces and locks adopters into the wrong mental model. Cross-coordination filed as adcp-client-python#346 ahead of either SDK shipping the Protocol. - 5134f45: Define the designated-task response payload JWS envelope for Brand Protocol verification responses. `verify_brand_claim` and `verify_brand_claims` success schemas now require `signed_response`, binding the signed task body to the designated task, resolved brand tenant, responding agent URL, request hash, and `iat`/`exp` freshness window. The security and brand-agent docs specify ordinary JWS signing input over JCS-canonicalized payloads, response-signing JWK verification requirements, per-brand response-signing key separation, and bulk audit retention requirements. - 45089c6: Integrate Brandfetch Brand Context API as authenticated ephemeral enrichment context. - 0627c47: Add `idcrea` as a supported creative identifier type for French ARPP.PUB workflows. This also clarifies that `ad_id` is common for US television and accepted by some radio/audio workflows, rather than requiring all broadcast or audio workflows to use Ad-ID. - a4a51bc: Add operation-scoped `push_notification_config` to `build_creative` requests and include `build_creative` in task-type enum values so async build webhooks and task polling can name the task. - fa64db9: Add optional `recipe_hash` fields to `build_creative` success responses so creative agents can expose an opaque, agent-scoped identity for build-determining inputs without standardizing a cross-agent hash algorithm. - af1d287: spec(creative): add build_creative spend controls — `max_spend` cap + `mode: "estimate"` dry-run. Follow-on from the persona/scenario review: fan-out (`max_creatives` × `max_variants`) and refinement produce many independently-billed leaves, and `per_unit` pricing gives a rate but not the unit count in advance — so an autonomous buyer had no protocol brake on spend. Both additions are optional and gated by a new `creative.supports_spend_controls` capability. - **`mode: "estimate"`** (request) → new `BuildCreativeEstimate` response shape (6th `oneOf` member): a dry run that produces and bills nothing and returns a `cost_low`/`cost_high` band computed against the request's actual inputs, with `basis` (`fixed` exact / `estimated_units` / `cpm_deferred`) and an optional per-leaf breakdown. Advisory/non-binding in this revision. - **`max_spend: { amount, currency }`** (request) → a hard per-call ceiling: the agent stops before the next leaf would exceed it and returns the partial `BuildCreativeVariantSuccess` with new `budget_status: "capped"` and an advisory `BUDGET_CAP_REACHED` in `errors[]` (every returned leaf real and billed; `items_returned` < `items_total`). First-leaf-over-cap → terminal `BUDGET_CAP_REACHED`; currency mismatch → `INVALID_REQUEST`. - New error code **`BUDGET_CAP_REACHED`** (distinct from `BUDGET_EXCEEDED`/`BUDGET_EXHAUSTED`), in both `enumDescriptions` and `enumMetadata`. - New capability **`creative.supports_spend_controls`** (default false). Deferred to the working group (flagged, not omitted): whether an estimate can be **binding**, and whether a refinement-**loop** bound is a protocol-level session budget vs. a buyer responsibility (documented as buyer-side for now). - d024eb8: spec(accounts): buyer-agent identity model + billing error-code coverage for sync_accounts Adds the spec/doc backing that adcp-client #1269 (BuyerAgentRegistry) needs to land without inventing wire behavior. **Error codes (additive, non-breaking).** Registers four codes referenced by `sync_accounts` but missing from the canonical enum, plus one new code for the per-buyer-agent commercial gate: - `BILLING_NOT_SUPPORTED` — seller-wide capability gate (`supported_billing` does not include the value), or per-account-relationship gate. Carries `error.details.scope` ∈ `{"capability", "account"}` so callers can dispatch without parsing prose. Default reject for billing-value mismatches. - `BILLING_NOT_PERMITTED_FOR_AGENT` — _new_. Seller-wide capability accepts the value, but the calling buyer agent's commercial relationship does not (e.g., onboarded as passthrough-only — no payments relationship — so `agent` and `advertiser` reject). Distinct from `BILLING_NOT_SUPPORTED` so agents can dispatch on autonomous-retry vs surface-to-human. `error.details` MUST conform to the new `error-details/billing-not-permitted-for-agent.json` schema: `rejected_billing` plus an optional single `suggested_billing`. The shape is deliberately clamped — it MUST NOT carry the agent's full permitted-billing subset, rate cards, payment terms, credit limit, billing entity, or any other per-agent commercial state (those are commercial-state oracles; full-subset disclosure in a single probe is exactly what the clamp prevents). - `PAYMENT_TERMS_NOT_SUPPORTED` — seller declines the requested `payment_terms` value. - `BRAND_REQUIRED` — billable operation attempted without a brand reference. All four registered in `enum`, `enumDescriptions`, and `enumMetadata` per the dual-surface requirement (#3738). **Uniform-response rule for unauthenticated callers.** Sellers MUST NOT emit `BILLING_NOT_PERMITTED_FOR_AGENT` to unauthenticated, unverified, or weakly-authenticated callers — emitting the per-agent code without an established agent identity is a cross-tenant onboarding oracle (same shape as `*_NOT_FOUND`). Unauthenticated callers receive `BILLING_NOT_SUPPORTED` (the broader code) regardless of which gate would have fired with identity established. Documented in `error-handling.mdx` Billing and Account Setup section. **`sync_accounts` task doc** adds the normative line that sellers MAY reject `billing` at the per-buyer-agent commercial gate distinct from the seller-wide capability gate; error rows cross-link to the new error-handling and accounts-and-agents sections. Also fixes a pre-existing doc bug: the error table referenced `PAYMENT_REQUIRED` (never registered in the enum) where the registered code is `ACCOUNT_PAYMENT_REQUIRED` — corrected to use the registered identifier. **Buyer-agent identity narrative.** New "Buyer-agent identity" section in `accounts-and-agents.mdx` framing the two-layer model the spec already implies but doesn't name: agent identity (signed-request `agent_url` derivation OR seller's credential-to-agent mapping) and brand-operator authorization (`brand.json/authorized_operators`). Both layers MUST pass; the checks compose. The brand-operator check runs against cached `brand.json` per existing revocation/cache semantics (eventual revocation, 24h TTL), and high-value or first-time-on-brand provisioning SHOULD bypass the cache to close the TOCTOU window. Per-buyer-agent commercial state — onboarding records, payment-relationship status, default account terms — is offline (out of scope) but surfaces on the wire through (a) the new `BILLING_NOT_PERMITTED_FOR_AGENT` runtime gate and (b) defaults sellers MAY apply during `sync_accounts` upsert (per-account values on the request always take precedence). Defines "passthrough-only" inline on first use. **`agent_url` derivation.** `security.mdx` "Agent identity" section now names the derivation explicitly: `agent_url` is the `url` field of the `agents[]` entry whose `jwks_uri` resolved the `keyid` at step 7 of the verifier checklist — not a JWK claim, JWS claim, or signed envelope field. The publication coordinate the verifier already used to fetch the JWKS _is_ the canonical identity. Closes a loophole where an SDK could surface a buyer-asserted `agent_url` from the envelope and treat it as cryptographically established. The bearer / API-key / OAuth transport is also clarified: agent identity MUST come from the seller's credential-to-agent mapping; sellers MUST NOT introduce an envelope-side `buyer_agent_url` as an alternate input. Existing buyer-asserted _verifier_ references (`creative.verify_agent.agent_url`, `governance.accepted_verifiers[].agent_url`) are explicitly outside this prohibition — they name agents the seller invokes under a published allowlist, not the signer. **Two new `error-details/` schemas** lock the recovery shapes so SDKs and conformance fixtures don't diverge: `billing-not-permitted-for-agent.json` (`additionalProperties: false`, `rejected_billing` + optional `suggested_billing`) and `billing-not-supported.json` (`scope` + optional `supported_billing` echo). The per-agent schema's clamp prevents full-subset commercial-state disclosure; the per-supported schema's `scope` field MUST be omitted on the unauthenticated path so it cannot itself become a per-account-relationship oracle. **Tier 3 (conformance fixtures + cross-language naming alignment with Python `BrandAuthorizationResolver`)** tracked as #3828. - 8dc46bc: Add `FORMAT_NOT_SUPPORTED` to the canonical error-code enum for creative-agent canonical build routing. The 3.1 `creative.supported_formats` storyboard and `build_creative` docs already require creative agents to fail closed with this code when `target_format_id.id` is not an advertised canonical capability or supported legacy named format. Publishing the enum entry, including the `supported_capability_ids` details hint, keeps schema validation, docs, and conformance aligned. - da42f43: test(compliance): add canonical format satisfaction create-time coverage. Defines the direct `PackageRequest.format_kind`/`params` canonical selector used by the negative under-specification case and publishes the runner-output contract for `canonical_format_satisfaction`. Read surfaces now echo supplied format selectors losslessly, and update payloads treat all format selector fields as immutable. - ee1a0b3: **Canonical formats 3.1 follow-ups — fixture, vocab, Pinterest disambiguation.** Closes three of the GA-blocking follow-ups identified in PR #3307 expert review, plus a latent slot-enum bug surfaced by the new fixture: - **Latent slot `asset_type` enum gap fixed** in `_base.json`. The canonical-formats slot enum was missing `pixel_tracker`, `vast_tracker`, and `daast_tracker` — meaning any product carrying explicit tracker slots (including the `native_in_feed` default slots) failed validation. Added all three to the enum and to the size-mutex if/then "no size semantics" branch. Discovered by the new native_in_feed fixture; would have hit any 3.1 adopter shipping explicit tracker slots. - **`native_in_feed` reference Product fixture** at `static/examples/products/canonical/taboola_content_recommendation.json`. Realistic Taboola US Content Recommendation product covering all 12 native_in_feed default slots — title, body_text, main_image (1200×627 / 1080×1080), cta with closed enum, advertiser_name, sponsored_label, landing_page_url, display_url, rating, plus impression / viewability / click `pixel_tracker`. CPC pricing, hourly+daily reporting, v1_format_ref points at `native_content`. Brings the canonical fixture suite to 13 (one per canonical, plus generative Veo on video_hosted). - **Pinterest disambiguation worked example** in `docs/creative/canonical-formats.mdx`. Spells out which Pinterest product routes to which canonical: Promoted Pin → `native_in_feed`, Pinterest Collection → `sponsored_placement` (catalog-keyed), Idea Pin → `image_carousel`, Shopping Pin → `sponsored_placement` (fanout_mode: single_item). The cleave is asset-bundle vs catalog-row composition; same logic applies to Snap Story / Snap Collection, TikTok TopView / TikTok Collection, etc. Closes the routing ambiguity flagged by Pia + Nastassia at GA review. - **10 new IAB OpenRTB Native 1.2 vocab entries** in `asset-group-vocabulary.json`. - Five Data Asset additions: `likes` (type 4), `downloads` (type 5), `saleprice` (type 7), `address` (type 9), `secondary_body_text` aliased to `desc2` (type 10). - Five core-native vocab additions surfaced by product-expert review — the `native_in_feed` canonical's default slots referenced these but the vocab didn't have entries, leaving the flagship fixture authoring against non-canonical IDs: `title` (Title Asset type 1; `headline` is the alias for the singular case, distinct from `headlines` pool used by responsive_creative), `main_image` (Image Asset type 3 main, with `image_main`/`hero_image` aliases), `icon` (Image Asset type 1), `advertiser_name` (the IAB `sponsoredBy` field), `sponsored_label` (renderer disclosure string). - `phone_number` description annotated with IAB type 8; `body_text` annotated with IAB type 2. `price` description updated to call out the price ↔ saleprice discount-rendering convention. **Migration doc** updated: 14 reference Product fixtures, dropped the "native_in_feed fixture follows in a subsequent PR" placeholder. Remaining 3.1 follow-ups tracked separately: - **SDK codegen (TypeScript + Python)** — multi-week build, the gating dependency for adopter consumption. Schemas shippable today; typed-tagged-union ergonomics arrive with codegen. - **`native_in_feed` conformance storyboard** — multi-phase YAML to extend `static/compliance/source/protocols/creative/index.yaml` with native sync_creatives + preview coverage. - 9c087a2: canonical-formats: five adopter-flagged additions before lock. Each surfaces guidance that was implicit in the spec but not findable; one resolves a normative silence. **1. "What `format_kind` is NOT for" decision rule** (canonical-formats.mdx). Adopters seeing the broadcast / DOOH / generative annotations were tempted to propose `format_kind: dooh_image` or `format_kind: broadcast_video` in 3.2. New section enumerates the six axes (creative type / production model / slot shape / channel / measurement / targeting) and the rule of thumb: new `format_kind` ONLY when the creative ASSET is structurally different. All 50 ad formats in the catalog ship via this rule; zero new canonicals added for broadcast / DOOH / native / generative. **2. `slots_override` authoring decision rule** (canonical-formats.mdx). When to use it vs leave it off was implicit across the four annotation patterns. New section + table makes it explicit: would a buyer composing the manifest list **different assets** vs the canonical's defaults? Yes → `slots_override`. No → omit. Worked cases: IAB MREC (default), native standard (override), DOOH (default), broadcast (default), generative (override), host-read podcast (override). **3. End-to-end `adagents.json` fetch flow worked example** (canonical-formats.mdx). The pieces were documented separately (publisher catalog, property scoping, capability_id resolution, community-mirror fallback, supersession). New section walks the full buyer journey in order: `Product` with `publisher_properties` → fetch `/.well-known/adagents.json` → fall back to AAO mirror on 404 → check `superseded_by` → scope `formats[]` by `applies_to_property_ids` → resolve `capability_id` against same-file `formats[]`. Concrete payload sequence included. **4. Multi-size fan-out normative decision** (canonical-formats.mdx + error code prose). The spec was silent on whether SDKs MAY fan out a multi-size v2 declaration to N v1 format_ids via catalog lookup. Resolved as MAY-do non-normative: SDKs without catalog access emit only seller-asserted refs (the conservative wire shape, default normative behavior); SDKs with catalog access MAY synthesize the missing per-size refs. Either way, `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` MUST fire as a transparency advisory; `error.details.synthesized_refs` lists catalog-resolved entries when fan-out is in play. Two SDKs processing the same input may produce different `format_ids[]` lengths, but the advisory keeps consumers in sync. **5. `format_schema` fetch-contract test fixtures** (static/examples/format-schemas/). 14 paired positive + negative test vectors covering all 7 failure-mode categories per the normative contract: digest verification, transport (https-only / redirect / oversize), SSRF (RFC 1918 / metadata endpoint), `$ref` sandboxing (cross-origin / depth-exceeded), schema-compile budget (catastrophic regex), schema validity (body is JSON but not a valid schema), graceful degradation (404 with/without cache). Each fixture documents the `setup` to simulate, the `expected_outcome`, the `expected_error_code`, and the rationale linking back to the contract clause. README at `static/examples/format-schemas/README.md` documents shape and usage. Cross-SDK conformance harness tracked as follow-up #4699. Validation: schema build clean, 14 canonical fixtures + 28 negative fixtures (was 19) + 50-entry catalog convention lint all green. - 9c087a2: **Canonical formats (AdCP 3.1).** v2 introduces a structured creative-format vocabulary that buyers and sellers can validate against without per-seller integration code. 13 canonical `format_kind` values (image, html5, display_tag, image_carousel, video_hosted, video_vast, audio_hosted, audio_daast, sponsored_placement, native_in_feed, responsive_creative, agent_placement, custom) with a two-axis model: `format_kind` names the creative TYPE; `asset_source` names the production model (buyer_uploaded / publisher_host_recorded / seller_pre_rendered_from_brief / seller_human_designed / agent_synthesized). Products carry `format_options[]` declarations narrowing a canonical with `params`, `slots`, `applies_to_channels`, optional `capability_id` for multi-format routing, and optional `experimental` flag. The full reference is at `docs/creative/canonical-formats.mdx`. **Wire-shape details adopters care about:** - `v1_format_ref` is ALWAYS an array of `{agent_url, id}` entries — single-ref is `[{...}]`. Multi-size declarations carry one ref per size in `params.sizes[]`. The `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` error code surfaces when ref count < sizes[] count. - v1 catalog's `canonical:` annotation is ALWAYS an object — minimal `{ "kind": "image" }`, rich `{ "kind", "asset_source", "slots_override" }`. The object form is what lets the 8 generative catalog entries (`display_*_generative`) project losslessly to v2: buyer ships a text prompt, not image bytes. - Display canonicals (`image`, `html5`, `display_tag`) support three size modes (mutex-enforced at schema layer): fixed `width+height`, multi-size `sizes: [{w,h}]` (mirrors OpenRTB `banner.format[]`), responsive `min_width/max_width/min_height/max_height`. The same product can carry N format_options across the three modes. - `ProductFormatDeclaration.canonical_formats_only: true` is the v2-only marker (mutex with `v1_format_ref`). - `format_kind: "custom"` requires `format_shape` (vocabulary entry) + `format_schema` (URI+digest) and either `canonical_formats_only: true` OR `v1_format_ref`. **Publisher catalog (`adagents.json formats[]`).** Publishers declare their format support once via top-level `formats[]` (with optional `applies_to_property_ids` / `applies_to_property_tags` scoping). Placements reference declarations by `capability_id`. For platforms that haven't adopted AdCP (Meta, TikTok, etc.), AAO publishes community-maintained adagents.json at `creative.adcontextprotocol.org/translated//adagents.json`; `superseded_by` field signals platform-adoption cutover. New media-buy filters `list_creative_formats(publisher_domain, property_id)` answer "what formats does this publisher accept?" with a normative resolution chain (publisher hosted → AAO mirror → agent-derived from products) and a response `source` field labeling which tier produced the list. **Where each piece of metadata lives (the "no new canonical" pattern).** Before reaching for a new canonical, the spec checks: production model → `asset_source`; slot shape → `slots_override`; channel → `applies_to_channels`; tracking / measurement → `sync_event_sources` / `event_log`. New canonical only when the CREATIVE ASSET is structurally different. Applied: generative, broadcast TV, DOOH, native all stay on existing canonicals via sibling refinement. Conversion pixels (Meta Pixel, GA4) explicitly belong on event_log, NOT on `platform_extensions` of a creative format. **Coverage at GA.** 50/50 ad formats in the AAO catalog annotated with the projection-ref object form. 7 UI scaffolding entries (`product_card_*`, `format_card_*`, `proposal_card_*`, `native_product_card`) split into `ui-element-formats.json` — they're agent-interface widgets, not ad formats; `list_creative_formats` returns them so consumers can resolve by `format_id`, but they never project to ad canonicals. **Error codes added** (all surfaced via response `errors[]` augmentation; non-fatal advisories): `FORMAT_PROJECTION_FAILED`, `FORMAT_DECLARATION_DIVERGENT`, `FORMAT_DECLARATION_V1_AMBIGUOUS`, `FORMAT_CAPABILITY_UNRESOLVED`, `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE`, `PIXEL_TRACKER_LOSSY_DOWNGRADE`, `PIXEL_TRACKER_UPGRADE_INFERRED`. **Cross-version pixel_tracker contract (normative for SDK auto-negotiation)**: when a 3.1 buyer SDK talks to a 3.0.x seller that doesn't know `pixel_tracker`, the SDK MUST downgrade to v1 `{asset_type: url, url_type: tracker_pixel}` shape and emit `PIXEL_TRACKER_LOSSY_DOWNGRADE` with per-field details. Conversely a 3.1 SDK reading v1 trackers MUST upgrade by inferring event/method from `asset_id` conventions and emit `PIXEL_TRACKER_UPGRADE_INFERRED`. Both directions are lossy-with-advisory — no REFUSE branch, even for `method: js` (v1 sellers fire the URL as a GET; counter-based measurement increments, but JS-execution-dependent measurement like OMID-style verification won't run). Buyer-side decision per asset: accept the loss or route to a 3.1-capable seller. Full bidirectional mapping table documented in `pixel-tracker-asset.json` description. **Renderer-fired pixel tracker asset type (#4706)**: new `pixel_tracker` asset type at `static/schemas/source/core/assets/pixel-tracker-asset.json` — the generic web-pixel tracker primitive, applies to any web-rendered canonical (image, html5, image*carousel, responsive_creative, sponsored_placement, native*\*, plus non-VAST/DAAST events on video_hosted/audio_hosted). Discriminated union with `event` (impression, viewable_mrc_50, viewable_mrc_100, viewable_video_50, click, custom), `method` (img, js), `url` (uri-template with universal-macros support), and `custom_event_name` (required when event is `custom`). Discriminator shape and event/method enums formalized in IAB OpenRTB Native 1.2 (`imptrackers[]` / `jstracker` / `eventtrackers[]` / `link.clicktrackers[]`). Scope is RENDERER-FIRED trackers — conversion pixels (Meta Pixel, GA4, server-side postbacks) stay on `sync_event_sources` / `event_log` per the format-vs-event_log boundary documented in canonical-formats.mdx. Formats with format-specific tracker structures keep their own primitives: `vast_tracker` (VAST ``), `daast_tracker` (DAAST parity). **Catalog tracker upgrade**: 45 ad-format payload entries in `server/src/creative-agent/reference-formats.json` previously carrying `impression_tracker` as `asset_type: "url"` upgraded to `asset_type: "pixel_tracker"` with `event: "impression"` + `method: "img"` — display / native / dooh / video_hosted / audio_hosted families all benefit. Plus 4 `slots_override` declarations on `native_standard` / `native_content` reshaped to use `pixel_tracker` for impression and click trackers. To match real measurement plans (impression + viewability + click trackers are commonly attached together), 90 additional optional slots added across the 45 web-rendered entries: each now carries `viewability_tracker` (`event: viewable_mrc_50`) and `click_tracker` (`event: click`) as optional `pixel_tracker` slots. Buyer populates only the trackers their measurement plan declares; absent slots are skipped. New vocabulary entries: `impression_tracker`, `click_tracker`, `viewability_tracker` in asset-group-vocabulary, all mapping to `asset_type: pixel_tracker`. **`pixel_tracker.event` enum mirrors IAB Native event-type registry**: 5 standardized event values plus `click` and `custom`. Maps 1:1 to IAB OpenRTB Native 1.2: `impression` (1), `viewable_mrc_50` (2), `viewable_mrc_100` (3), `viewable_video_50` (4), `audible_video_complete` (500). `audible_video_complete` is distinct from `viewable_video_50` — the former is 100% completion with audio on; the latter is 50% pixels for ≥2 seconds with audio on. Meaningful on non-VAST video (Meta Reels, YouTube Shorts, TikTok Spark) where audible-complete is measured but VAST `` isn't the wire format. **Negative-fixture coverage**: 10 new test vectors for `pixel_tracker` in `tests/canonical-negative-fixtures.test.cjs` covering valid shapes (impression img / impression js / viewable_mrc_50 / click / audible_video_complete / custom-with-name) and the rejection paths (custom without `custom_event_name`, non-custom with `custom_event_name`, invalid event enum, invalid method enum). Total negative-fixture count goes from 28 → 38. **Other adopter-facing additions:** - `ProductFormatDeclaration.seller_preference: "preferred" | "accepted" | "discouraged"` — soft routing hint on multi-format products. - `placement-definition.json format_options[]` (capability_id reference OR inline) with same-file resolution scope. - Convention lint at `tests/canonical-format-conventions.test.cjs` enforces: object-form `canonical:`, array-form `v1_format_ref[]`, size-mode mutex, AAO-mirror URL convention, slot/param consistency. **Round-2 adopter feedback (Pia Malovrh / Nastassia Fulconis, 2026-05-18 Slack):** - **`native_in_feed` canonical added.** 13th `format_kind` covering IAB OpenRTB Native 1.2 in-feed native ads, content-recommendation widgets (Taboola, Outbrain, Yahoo Native, AdMob Native), and publisher in-feed sponsored placements without catalog dependency. Slots map 1:1 to IAB Native asset types (`title`, `body_text`, `main_image`, `icon`, `cta`, `advertiser_name`, `sponsored_label`, `landing_page_url`, plus renderer-fired `pixel_tracker` trackers). Routing answer: buyer agents reading `native_in_feed` know to assemble title+image+body+CTA; reading `sponsored_placement` know to attach a catalog feed. Defaults to non-experimental (IAB Native 1.2 contract is well-established). - **`sponsored_placement` narrowed.** Canonical is now normatively catalog-keyed retail-media ONLY: REQUIRES `source_catalog` slot. Schema description explicitly excludes IAB in-feed native, content-recommendation, PMax-style algorithmic surfaces, and single-image/video creative — those route elsewhere. Earlier broader framing failed buyer-agent routing (buyer reading `sponsored_placement` couldn't disambiguate Amazon SP from Taboola). - **`scenes` → `video_brief` rename.** Renamed `static/schemas/source/creative/scenes.json` → `creative/video-brief.json` (`$id: /schemas/creative/video-brief.json`), wrapper field `scenes[]` → `segments[]`, per-segment `description` → `prompt`. The shape was always a structured generation brief (no camera direction, shot type, mood, or reference attachments) — the rename calls it that. asset_group_id `scenes` → `video_brief` (with `scenes`, `storyboard`, `shot_brief` as aliases). Distinct from `creative_brief` (free-form text); `video_brief` is the structured timed-prompt form. Buyers wanting visual-direction surface still attach `reference-asset.json` with `purpose: storyboard`. - **`seller_preference` semantics clarified.** Schema description and `ProductFormatDeclaration` description now state normatively that `format_options[]` is the closed set of accepted formats for a product — sellers MUST reject `create_media_buy` requests targeting any `format_kind` outside that set. `seller_preference` is a soft ranking hint WITHIN the accepted set, NOT an enforcement axis. There is intentionally no `required` enum value; the closed-set rule already handles "this is the only format that works" (list one entry → that's the set). Pia's "won't work vs please don't" distinction is resolved structurally rather than via enum proliferation. - **Registry two-tier boundary documented; platform-specific IDs removed.** Dropped `youtube_video_id` and `pin_id` from `asset-group-vocabulary.json`. Added normative paragraph: the canonical vocabulary is the IAB-aligned portable tier; platform-specific asset identifiers (TikTok video IDs, Snap attachment IDs, Meta Advantage+ creative IDs, etc.) belong on the canonical's `platform_extensions[]` (URI+digest reference to the platform's extension schema). Earlier draft set precedent for every platform's identifier vocabulary leaking into the canonical registry — explicitly reversed before GA. **Resolves** #4148 (canonical-formats vocabulary), #4620 (publisher-scoped catalogs), #4652 (.adcp placeholder cleanup), #4689 (catalog generative deannotation). Coordinated with adcp-client #1815 (SDK v1↔v2 projection) and adcp-go (catalog consumer). - 2938456: feat(registry): add catalog collections and YouTube channel aliases Adds first-class registry catalog collections, collection change-feed events, YouTube channel distribution identifier types, collection sync/distribution lookup APIs, and an admin community collection upsert path. This supports publisher-owned collections distributed through third-party platforms such as YouTube while keeping publisher authorization anchored on the publisher's own domain. - 4deed71: Add catalog content macros (`{ITEM_NAME}`, `{ITEM_DESCRIPTION}`, `{ITEM_TAGLINE}`, `{ITEM_PRICE}`, `{ITEM_PRICE_CURRENCY}`) for catalog-driven creative rendering Extends the catalog-item macro family from ID values (`{SKU}`, `{GTIN}`, `{OFFERING_ID}`, …) to scalar content values, so catalog-driven creatives (sponsored_placement / DPA: Meta DPA, Snap Collection, TikTok Shopping) can substitute a rendered item's `name`, `description`, `tagline`, `price.amount`, and `price.currency` into a template. Each token maps 1:1 to a real, documented catalog field via the existing `catalog_field` dot-notation vocabulary (catalog-field-binding.json ScalarBinding) — no parallel field vocabulary is introduced. All five are scalar TEXT values and fall under the existing catalog-item substitution-safety rules unchanged (NFC normalization → RFC 3986 percent-encoding to the unreserved set → one-pass nested-expansion prohibition → URL-context scope). No new escaping context is added; conformance vectors for content values are added to `catalog-macro-substitution.json`. Single-brace `{MACRO}` only. `{{double-brace}}` stays reserved and is NOT adopted — it is one of the downstream ad-server macro syntaxes (`%%...%%`, `${...}`, `[...]`, `{{...}}`) that sales agents MUST neutralize/percent-encode; adopting it would relax a documented substitution-safety guarantee. Which catalog items render stays seller-declared via the already-shipped `fanout_mode` enum on `sponsored_placement.json` (`single_item` / `per_item` / `multi_item_in_creative`); no buyer-side selection field is added. On ML-optimized DPA surfaces (Meta Advantage+, TikTok Shopping) the platform may override buyer-authored overlay text, so content macros are a buyer-declared hint the seller MAY honor. `format.supported_macros.items` auto-extends via its `anyOf` universal-enum branch (#5099); no schema edit is needed there. Closes #5277. - 42f3557: Add `committed_metrics_supported` capability flag to `media-buy-features.json`. Closes the buyer-side detection gap from #3510 where absence of `committed_metrics` was indistinguishable between 'seller didn't snapshot' and 'seller doesn't have snapshot infrastructure.' Closes #3517. **Why one flag (not two).** Per the unified metric-accountability design (#3576), `committed_metrics` is a single array carrying both standard and vendor-defined entries. The flag inherits that unification — one flag declares the seller's snapshot capability across the whole contract surface. **MUST timing — atomic.** Sellers declaring this flag `true` MUST populate `committed_metrics` on every `create_media_buy` response AND MUST honor append-only mid-flight metric additions via `update_media_buy`. The MUST ships with the flag, not as a future tightening — advisory-only flags leave the audit gap exploitable, defeating the purpose. **Placement choice — Option A (extend `media-buy-features.json`).** Matches the existing `property_list_filtering` / `catalog_management` precedent. Buyers can pass it as a `required_features` filter on `get_products` to narrow the catalog to snapshot-supporting sellers — that side effect is the design intent, not a bug. **Backwards compatibility.** Optional and additive. Sellers without the flag are unchanged; buyers ignore the flag if they don't filter on snapshot support. Closes #3517. - 59f1c37: Add `package.committed_metrics` and `package.committed_vendor_metrics` — frozen snapshots of the product's `reporting_capabilities.available_metrics` and `vendor_metrics` stamped at `create_media_buy` response time. Closes #3481. **The audit gap.** PR #3472 established that the product's `available_metrics` becomes the binding reporting contract carried into the resulting media buy. That holds **only if** the product is immutable AND the seller stores a snapshot at buy creation. Neither is guaranteed: - Products mutate (sellers add/remove metrics from `available_metrics` as their reporting infrastructure evolves) - Without a per-package snapshot, `missing_metrics` on `get_media_buy_delivery` is computed against "what the product _currently_ advertises" — a 90-day-old buy is incorrectly judged as "clean" because the seller quietly dropped a metric they originally committed to - An ops team auditing a 90-day-old buy will not trust an implicit contract reference This was flagged on PR #3472 by the product expert as the primary sell-side audit gap. **Changes.** - `core/package.json`: new `committed_metrics: AvailableMetric[]` field and new `committed_vendor_metrics: { vendor, metric_id }[]` field. Both optional in v1; sellers without per-package snapshot infrastructure fall back to the product's live state (absence is conformant). Both MUST NOT change post-creation — `update_media_buy` cannot modify them. Renegotiating the metric contract requires a new buy. - `media-buy/get-media-buy-delivery-response.json`: `missing_metrics` description updated to declare the reconciliation source — when `committed_metrics` is present, that is the contract; when absent, fall back to the product's current `available_metrics`. - `docs/media-buy/task-reference/create_media_buy.mdx`: new "Reporting contract on confirmed packages" subsection documenting the snapshot semantics, immutability, and v1-optional posture. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: bullet updated to point at the reconciliation source. **Design choices spelled out (resolves the three open questions on #3481).** 1. **Optional or required?** Optional. Forcing the snapshot at v1 would break existing implementations on first deployment. Optional with a doc note that "buyers SHOULD reconcile against `committed_metrics` when present and fall back to the product's live state when absent" lets sellers adopt incrementally. Expected to become required at the next major. 2. **What snapshots into `committed_metrics`?** The product's full `reporting_capabilities.available_metrics` at the moment of `create_media_buy`, NOT the intersection with the buyer's `required_metrics` filter. The product committed to reporting all those metrics; reducing to the intersection would silently drop reporting on metrics the buyer didn't explicitly list but the seller still has. `requested_metrics` (on `reporting_webhook`) remains the buyer's payload-optimization filter — a separate concept. 3. **Mutation policy?** Frozen at creation, MUST NOT change post-creation. `update_media_buy` cannot modify `committed_metrics` or `committed_vendor_metrics`. If the buyer/seller need to renegotiate, that's a new buy. This is the cleanest contract; mutability with audit trail can be added later if real demand emerges. **Backwards compatibility.** Optional and additive. Sellers without snapshot infrastructure fall back to the implicit contract (product's current state) — this matches the v1 behavior of #3472. Buyers can incrementally upgrade to consume `committed_metrics` when present. Closes #3481. - a88d106: Registry: community-mirror catalog lifecycle (#2176). Makes AAO catalog-only adagents.json mirrors first-class registry resources. A community mirror is the catalog-only adagents.json (`authorized_agents: []` + formats/properties/placements) AAO publishes on behalf of a platform that hasn't adopted AdCP, served at `creative.adcontextprotocol.org/translated//adagents.json`. Builds on #5352/#5353, which made `authorized_agents: []` valid. - **Store:** new `community_mirrors` table (migration 506) keyed by `platform`, with the adagents.json body, `catalog_etag`, `superseded_by`, and provenance. - **Endpoints** (`/api/registry/mirrors`): - `GET /api/registry/mirrors` — list mirrors with their `catalog_etag` (public). - `GET /api/registry/mirrors/:platform` — read one mirror (public). - `PUT /api/registry/mirrors/:platform` — idempotent publish/upsert (registry moderators or admins). Forces `authorized_agents: []`, requires catalog content, validates the proposal. - **Serving:** `GET /translated/:platform/adagents.json` on the creative agent serves the stored mirror with an `ETag` (from `catalog_etag`, falling back to a content hash), `If-None-Match` → `304`, `Cache-Control`, and a `superseded_by` → `Link: rel="successor-version"` header. Read-back by platform and listing close the gap where published mirrors could not be retrieved; the idempotent upsert lets audit fixes update in place instead of duplicating. - d6e94f4: Registry: add `DELETE /api/registry/mirrors/:platform` to retire a community mirror. Completes the #2176 community-mirror lifecycle with a moderator/admin-gated retire endpoint, closing the post-supersession deprecation window. Because buyers cache the mirror URL and fall back to it until the platform self-adopts, deletion refuses a mirror that has not published a `superseded_by` migration signal unless `?force=true` is passed — so live fallback traffic isn't yanked out from under buyers. After deletion the serving route returns 404, the documented "no mirror" state. The publish/delete authorization check is factored into a shared helper. - 15cbd99: Add `completion_source` qualifier key to disambiguate seller-attested vs vendor-attested `completion_rate`. Closes #3861 with Option C from the issue. **The hybrid problem.** `completion_rate` is dual-natured: the seller witnesses completion via player events (the seller's player fired the completion beacon), and third-party measurement vendors can independently attest to completion via SDK callbacks, panel methodology, or server-side beacon validation. The two paths can yield materially different rates — particularly in SSAI environments where the player's view of completion may differ from a vendor's. Same `metric_id`, two semantics — exactly the case the [taxonomy doc](https://docs.adcontextprotocol.org/docs/measurement/taxonomy)'s working rule of thumb addresses ("if two layers seem to claim the same field, the field is probably two fields wearing one name — split it"). **The qualifier slot is the right home.** Instead of splitting the metric_id (`seller_completion_rate` vs `verified_completion_rate`), surface the dual nature at the qualifier layer that #3576 already established for viewability. Viewability is now joined by completion_rate as a Tier 1 graduated metric using the qualifier slot — proves the pattern is generalizable, not viewability-specific. **Schemas added.** - `enums/completion-source.json`: closed enum `["seller_attested", "vendor_attested"]` with descriptions. **Schemas updated.** - `core/package.json` `committed_metrics.qualifier`: adds `completion_source` alongside `viewability_standard`. MUST be set when `metric_id` is `completion_rate` and the seller commits to a specific source. - `media-buy/package-request.json` `committed_metrics.qualifier`: same shape on the buyer-side request surface. - `media-buy/get-media-buy-delivery-response.json` `aggregated_totals.metric_aggregates.qualifier`: adds `completion_source` for partitioned delivery rollups by source. - `media-buy/get-media-buy-delivery-response.json` `by_package[].missing_metrics.qualifier`: adds `completion_source` for accountability — a buyer expecting vendor-attested completion flags a seller-attested-only delivery report as missing the vendor commitment. **Vendor identity** is anchored on the matching `performance_standard.vendor` BrandRef in the buy contract, not duplicated on the metric row. Same pattern as MRC viewability anchored on `performance_standard.vendor` for the DV/IAS/etc. case. **Reconciliation.** The atomic-unit join `(scope, metric_id, qualifier)` from #3576 + #3848 (just-merged `metric_aggregates`) extends naturally — completion_rate rows now carry a `completion_source` qualifier, joined like viewability_standard rows. No reconciliation logic changes; new keys plug into the existing slot. **Doc updates.** - `docs/media-buy/task-reference/create_media_buy.mdx` — `committed_metrics` reporting contract section now lists both qualifier keys (viewability_standard and completion_source) with their conditional-required semantics. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx` — qualifier vocabulary section names both keys; missing_metrics description shows the completion_source flagging example. **Backwards compatibility.** Additive. Existing `committed_metrics` / `missing_metrics` / `metric_aggregates` consumers without qualifier-aware reconciliation continue to work; the closed-vocabulary nature of qualifier means new keys appear only in subsequent minors with explicit migration paths. Closes #3861. - c6fb0dd: spec(errors): add `CONFIGURATION_ERROR` to canonical error catalog Adds a standard error code for **adopter-side server misconfiguration** — a deployment that the seller has stood up incorrectly, that the buyer cannot fix, that is not transient, and that is not an opaque crash. The canonical catalog previously had no code that fit this slot: `INVALID_REQUEST` is buyer-fixable, `SERVICE_UNAVAILABLE` is transient, `UNSUPPORTED_FEATURE` is a capability mismatch, `ACCOUNT_SETUP_REQUIRED` is buyer-side onboarding, and `GOVERNANCE_UNAVAILABLE` is scoped to a registered governance agent. Concrete failure modes the new code fits: an account is declared with `mode: 'mock'` but no `mock_upstream_url` is populated; a platform is declared with `mode: 'live'` or `mode: 'sandbox'` but no `upstream_url` is declared; a required environment variable is unset on the seller process. Recovery is `terminal` — the buyer MUST surface to the seller's operator and MUST NOT auto-retry, since retries cannot resolve a misconfigured deployment until the operator intervenes. Wire shape is unchanged — the code itself is the discriminator, no `error-details/configuration-error.json` is registered (mirroring the minimal-disclosure precedent of `AGENT_SUSPENDED` / `AGENT_BLOCKED`); `error.message` carries the operator-readable diagnostic. Sellers SHOULD calibrate that message to a level useful to a seller-side operator without leaking deployment internals to the buyer. The new code is additive — existing catalog entries are unchanged, and SDKs that fall back to the `recovery` classification on unknown codes will already treat unknown sightings as terminal per the forward-compatibility rule in `error-handling.mdx`. Closes #3995. - f3705ae: Add contextual signal coverage forecasts for signal discovery and product-relative availability planning. Signals can now include optional `coverage_forecast` data with an explicit denominator, bucket overlap semantics, bucket completeness, and forecast points keyed by canonical signal dimensions. Forecast points gain a `signal` dimension kind and `coverage_rate` becomes a standard forecastable metric for availability breakdowns. The feature is additive on the wire. Existing `coverage_percentage` remains available for compatibility, but richer planning should use `coverage_forecast` when sellers can disclose the denominator and value-level distribution. - 313e3a9: Add top-level `paused` to `create_media_buy` so buyers can create campaigns with delivery held from the outset. A start-paused buy returns `media_buy_status: "paused"` once activation prerequisites are satisfied; missing creatives and future start dates still surface as `pending_creatives` and `pending_start`. - a091c67: Add `media_buy.creative_approval_mode` to `get_adcp_capabilities` so sellers can declare whether human review can block serving eligibility after creatives are assigned and automated validation passes. Sellers with any reachable manual-review workflow declare `require_human`, which lets compliance runners skip auto-approval-dependent storyboards instead of reporting false failures. Omission is legacy-unspecified rather than an affirmative `auto_approve` claim; the `pending_creatives_to_start` storyboard now runs only when sellers explicitly declare `auto_approve`. - af1d287: spec(creative): add `list_transformers` task + account-scoped creative transformers, and extend `build_creative` for transformer selection and variant/catalog multiplicity. A **transformer** is the creative analog of a media-buy product: an agent-offered, account-scoped, selectable unit of build capability (a voice, model, style, or director) with a typed configuration surface and per-account pricing. This makes account-specific render configuration — including custom values like cloned voices that exist only for one credential — discoverable from the agent rather than guessed, hung on a global format, or smuggled through `ext`. Strictly additive. Existing `build_creative` callers are unaffected (all new request fields are optional; the shipped `BuildCreativeSuccess`/`BuildCreativeMultiSuccess` response shapes are unchanged — a new fifth member is added alongside them). New: - `list_transformers` task (creative protocol): account-scoped, brief-filterable, paginated discovery. An `expand_params` mode returns account-scoped enumerable option **values** (e.g. your configured voices) on the same tool — no separate options endpoint. - Core schemas `transformer.json` and `transformer-param.json`. - `get_adcp_capabilities` → `creative.supports_transformers` discriminator. `build_creative` extensions: - Request: `transformer_id` (select one transformer; target format(s) must be a subset of its `output_format_ids`), `config` (typed bag keyed to the transformer's params — agents MUST reject unknown/out-of-range values), `max_creatives` (catalog/item fan-out: N distinct creatives, one per item, with sampling), `max_variants` + `variant_axis` + `keep_mode` (alternatives per creative). - Response: a new `BuildCreativeVariantSuccess` member — `creatives[]` each carrying `variants[]`, with a `build_variant_id` namespace (distinct from preview `preview_id` and served `variant_id`), per-leaf pricing receipt, and `items_total`/`items_returned`. Best-of-N is variants + `recommended`/`rank`. You pay for all produced variants (`per_unit` × N); a kept variant lazily earns a `creative_id` on trafficking, which flows to `report_usage`. Per-format atomic; per-item non-atomic. `build_variant` lineage + refinement: - `build_variant_id` is now the leaf-level lineage anchor (`x-entity: build_variant`): minted per produced variant, distinct from the call-level `build_creative_id`, lazily earning a durable `creative_id` only on trafficking. Untrafficked leaves are billed via the inline per-leaf `vendor_cost` only; `report_usage` reconciliation applies once a leaf earns a `creative_id`. - Conversational refinement: `build_creative` gains `refine_from_build_variant_id` — re-build a prior leaf with a natural-language instruction in `message` plus an optional `config` delta, returning new lineage-linked variants (each with `parent_build_variant_id`); never a mutation. Composes with `max_variants`/`variant_axis`, mutually exclusive with `max_creatives`. Gated by the new `get_adcp_capabilities` → `creative.supports_refinement` discriminator (`UNSUPPORTED_FEATURE` when unsupported; `REFERENCE_NOT_FOUND` for an unknown/expired ref). Pricing rides the existing `per_unit` model + inline receipt + `report_usage` unchanged — transformers carry `pricing_options` (reusing `vendor-pricing-option.json`). Deprecations (deprecated in 3.1, removed at 4.0; SDKs MUST keep honoring them through 3.1–3.x): `Format.input_format_ids`, `Format.output_format_ids`, `Format.pricing_options`, and the `input_format_ids`/`output_format_ids` discovery filters on `list_creative_formats` — all superseded by `list_transformers`, which carries each transformer's own I/O signature and pricing. - 431cc86: Reconcile creator and engagement conversion events with the existing metric vocabulary by adding `follow`, `content_view`, and `watch_milestone` event types, clarifying `subscribe` as paid, adding structured event surfaces and progress fields, and allowing per-source `ext` metadata on `sync_event_sources` results. - de60c64: spec(auth): require buyer-principal credentials on transport channel; add `CREDENTIAL_IN_ARGS` error code The AdCP spec was previously silent on credential placement. Buyer-principal credentials arrive over the transport's authentication channel — Bearer per RFC 6750 §2, RFC 9421 signature headers, MCP/A2A authentication framing per RFC 9728 §3, or mTLS — but nothing in the spec said credentials MUST arrive there and MUST NOT arrive embedded in the task payload. In practice the gap produced a recurring bug class: storefront-shaped adopters independently rediscovered top-level `_access_token`, then nested `request.context._access_token`, then `request.ext._access_token` — three rounds of expert review on a single PR each surfacing a different smuggling vector. Without spec-level clarity, every adopter reaches the same conclusion independently and ships its own ad-hoc allowlist. This release adds a normative **Credential placement** section to `authentication.mdx` after the existing tenant-resolution paragraph: buyer-principal credentials MUST arrive on the transport's authentication channel and MUST NOT be placed in the task payload — top-level, in `context`, in `ext`, or any other nested location. The rule is transport-agnostic; it applies under every supported authentication mechanism. Two carve-outs are explicit: `push_notification_config.authentication.credentials` (the legacy seller-to-buyer webhook authentication, orthogonal to the buyer principal) and onboarding-time secrets exchanged out-of-band. Relay topologies (#2324) authenticate under the relay's own principal — pass-through preserves the brand agent's RFC 9421 signature, re-signing carries brand-agent identity in the request body as identity context — neither model permits forwarding the brand's transport credential as a relay-side payload field. A new error code, `CREDENTIAL_IN_ARGS`, joins `error-code.json`. Sellers SHOULD reject credential-in-args under AdCP 3.1; the requirement upgrades to MUST 90 days after the 3.1 publication date. The code's recovery classification is `terminal` — auto-retry against this code re-logs the credential on each attempt, exactly the prompt-injection exfiltration surface the rule closes (`security-model.mdx#threats-specific-to-agentic-advertising`). `error.field` identifies the path at which the credential was detected (e.g., `request.context.access_token`) and MUST NOT echo the credential value or any prefix of it; sellers MUST drop the smuggled credential from logs, audit rows, and observability spans before persisting the rejection. `CREDENTIAL_IN_ARGS` is distinct from `AUTH_REQUIRED` (no credentials presented or transport-channel credentials rejected) and `PERMISSION_DENIED` (authenticated caller not authorized). The new code is additive — existing catalog entries are unchanged, and SDKs that fall back to the `recovery` classification on unknown codes already treat unknown sightings as terminal per the forward-compatibility rule in `error-handling.mdx`. The 90-day SHOULD-to-MUST window gives implementations time to land detection without leaving credentials sitting in LLM-visible payloads during the migration. Closes #4046. - 68b86a5: Restructure `product.delivery_measurement.provider` as a `vendors: BrandRef[]` array, deprecating the legacy free-form string. Closes the BrandRef-migration half of #3860; the merger-with-`performance_standards` question is deferred to a follow-up RFC since it requires more design (`delivery_measurement` describes the _overall_ measurement story while `performance_standards` carries _committed_ metrics with thresholds — they're different concerns). **The BrandRef migration.** Before this minor, `delivery_measurement.provider` was a string like `"Google Ad Manager with IAS viewability"` — buyer agents had to string-parse to find the verification vendor. The string also conflated two jobs: vendor identity AND methodology description. With this minor: - New `vendors: BrandRef[]` field — structured measurement-vendor identity, anchored on `brand.json` `agents[type='measurement']`. Array because a single product often has multiple vendors playing different roles (ad server + viewability vendor; retail-media seller + third-party retail measurement). Each entry's measurement-agent capabilities catalog is queryable via `get_adcp_capabilities.measurement.metrics[]`. - Legacy `provider: string` — marked deprecated. Dropped from the schema's `required` array (was previously the lone required field on `delivery_measurement`); retained for one-minor backwards compatibility. When both fields present, consumers MUST use `vendors` for identity and treat `provider` as informational text. - `notes: string` — clarified as free-form methodology prose only, not vendor identification. **Distinct from `performance_standards.vendor`.** `delivery_measurement.vendors` carries vendor identity for the overall measurement story (including non-committed-but-reported metrics); `performance_standards[].vendor` carries vendor identity for _committed_ metrics with thresholds. The two fields cover different scopes — the merger question raised in #3860 is deferred. **Migration.** ```json // before "delivery_measurement": { "provider": "Google Ad Manager with IAS viewability", "notes": "MRC-accredited viewability. 50% in-view for 1s display / 2s video." } // after "delivery_measurement": { "vendors": [ { "domain": "googleadmanager.com" }, { "domain": "integralads.com" } ], "notes": "MRC-accredited viewability. 50% in-view for 1s display / 2s video." } ``` **Backwards compatibility.** Additive (new field, deprecated field retained, required dropped). Existing implementations populating `provider` continue to work for one minor; removed at the next major. **Doc updates.** `media-products.mdx` field description reflects the structured shape. Closes #3860 (BrandRef migration). The merger-with-`performance_standards` question stays open as a follow-up. - 9ce754e: Close two reporting gaps on `core/delivery-metrics.json`: a duration metric that had no reporting-side counterpart, and ambiguous reach/frequency measurement windows. Attention metrics (#4579) are intentionally **not** added as flat scalars — see below. **`viewability.viewed_seconds` (#4579, partial).** Extend the existing `viewability` block to include `viewed_seconds` — average in-view duration per measurable impression. Buyers can already set `viewed_seconds` as an optimization goal in `optimization-goal.json`; this gives them a place to receive the reported value back. Nested into `viewability` rather than added as a top-level scalar because the viewability `standard` governs the in-view threshold for both `viewable_rate` and `viewed_seconds`, and they share the same `measurable_impressions` denominator. The vendor identity is already carried on the parent block. **Attention metrics (#4579, remainder).** `attention_seconds` and `attention_score` are intentionally **not** added as graduated delivery-metrics fields. Per `docs/measurement/taxonomy.mdx`, vendor-specific metrics with no industry-graduated standard flow through `vendor_metric_values` — every attention vendor (DoubleVerify, IAS, Adelaide, TVision, Lumen, …) defines them differently with no MRC-or-equivalent accreditation. The reporting path is `vendor_metric_values[]` with `metric_id: "attention_seconds"` or `"attention_score"` and the vendor identified on the row. The `optimization-goal.json` metric enum description is updated to point reporters at this path so the optimization-side and reporting-side stay aligned without schema-graduating these vendor-specific metrics. **`reach_window` for reach/frequency disambiguation (#4580).** Add `reach_window` to declare the measurement window for reported `reach` and `frequency`. Before this minor, a buyer summing `reach` across daily delivery rows could silently double-count audiences — a seller could legitimately report daily uniques, cumulative-to-date uniques, or a custom window, with no way for the buyer to tell. With this minor: - `reach_window: { kind: "cumulative" | "period" | "rolling", period?: Duration }`. `cumulative` = uniques since campaign start (do not sum across rows; each later row supersedes). `period` = uniques within a single non-overlapping reporting period — e.g., a daily snapshot. `rolling` = uniques within a trailing window — e.g., trailing-7-day reach reported by Nielsen, iSpot, GAM, DV360. The `period: Duration` field is required when `kind` is `period` or `rolling` (enforced via `if/then` so a `kind: "period"` row without a `period` field is rejected at validation). - `reach` and `frequency` descriptions updated to reference `reach_window`. When `reach_window` is omitted, the window is unspecified — buyers MUST NOT sum reach across rows or compare/average frequency across rows. - Sellers SHOULD populate `reach_window` whenever `reach` is present. Not made hard-required for backwards compatibility, but the description language is prescriptive. **Backwards compatibility.** Additive. `viewability.viewed_seconds` and `reach_window` are both optional. Existing sellers continue to validate without changes; existing buyers ignoring the new fields keep working. Buyers SHOULD upgrade their reach summation logic to gate on `reach_window` semantics. **Doc updates.** Metrics tables in `docs/media-buy/task-reference/get_media_buy_delivery.mdx` and `docs/creative/task-reference/get_creative_delivery.mdx` reflect the changes. Closes #4580. Addresses #4579 partially (viewed_seconds added; attention metrics routed via vendor_metric_values). - f138c04: Extract remaining delivery-report inline row schemas into named core schemas for SDK generation, and reserve `x-adcp-open-payload: false` until a canonical generator contract exists. - 3a33e82: spec(specialisms): deprecate sales-proposal-mode (refs #3823 item 4, #3844) Proposal mode is how guaranteed deals get sold in practice — RFP → proposal → review → finalize → IO signing → live. Auction-based sales don't have proposals; they're bid-by-bid. Today `sales-proposal-mode` (proposals + briefs) and `sales-guaranteed` (IO + guaranteed) are halves of the same flow that force sellers to declare both or pick the wrong one. Following the established `signed-requests` precedent (deprecated in 3.1, retained until 4.0): - Adds `sales-proposal-mode` to `x-deprecated-enum-values` in `static/schemas/source/enums/specialism.json` - Updates `enumDescriptions[sales-proposal-mode]` with the deprecation note + migration path - Adds a deprecation banner to the storyboard at `static/compliance/source/specialisms/sales-proposal-mode/index.yaml` - Updates `sales-guaranteed`'s narrative to explain how proposal flows relate to guaranteed selling and why proposal_finalize is not yet folded into its `requires_scenarios` The clean folding of `proposal_finalize` into `sales-guaranteed.requires_scenarios` (so both flavors of guaranteed selling grade against the proposal lifecycle) needs a wire-level capability flag the storyboard runner can use to skip the scenario as `not_applicable` for direct-buy guaranteed sellers (auction PG, retail SKU; no RFP). The runner gates only on `requires_capability` predicates against `get_adcp_capabilities`, not on scenario-level metadata. Tracked as a follow-up in #3844 (`add supports_proposals capability flag`). **Migration through 3.x**: sellers that do proposals continue to declare BOTH `sales-guaranteed` AND `sales-proposal-mode` so the proposal flow grades under the proposal-mode specialism's existing storyboard bundle. Pure-direct-buy guaranteed sellers (auction PG, retail SKU) declare only `sales-guaranteed`. The wire shape is unchanged — both enum values remain valid through 3.x. **At 4.0**: with the `supports_proposals` capability flag in place (#3844), `proposal_finalize` joins `sales-guaranteed.requires_scenarios` with capability-gated skip semantics, the `sales-proposal-mode` enum value is removed, and the storyboard bundle is retired. - 971ffe4: `brand.json` gains a fifth variant and distributed publishing model. Additive — existing publishers unchanged. **New variant — Brand Canonical Document.** A self-published per-brand document carrying the brand's identity attributes plus optional `house_domain` (string, the domain of the brand's parent house). Standalone brands (no parent house — Patagonia, Liquid Death) omit `house_domain`. Excludes top-level house-only fields (`house`, `brands`, `brand_refs`, `authorized_operators`) and redirect-variant fields (`authoritative_location`, `region`, `note`, `redirect_reason`, `redirect_effective_at`) to disambiguate from the other four variants. **House Portfolio additions.** Gains `brand_refs[]` — portfolio entries for brands whose canonical documents live elsewhere (child-owned data). Each entry has shape `{ domain, brand_id, managed_by?, effective_at? }`. The entry shape is defined as `#/definitions/portfolio_entry` (the name is distinct from `core/brand-ref.json`, which is the buyer-side schema for identifying brands in media-buy plans). `managed_by` (optional) is house-declared and explicitly non-trust-bearing — it's a directory field for aggregation across houses. `effective_at` (optional) is the publisher-declared timestamp consumers use to age mutual-assertion edges. Required widened from `["house", "brands"]` to `["house"]` with `anyOf` requiring at least one of `brands[]` or `brand_refs[]`. **Trust model.** A child Brand Canonical Document declares `house_domain: ""`; the house's `brand_refs[]` must reciprocate for mutual-assertion trust. Trust resolves at two layers: brand identity (logos/colors/tone/tagline — authoritative on the leaf's TLS alone) and brand relationships (governance, billable inclusion — gated on mutual assertion). A leaf-only edge keeps identity trust and surfaces a self-healing notification SHOULD to the house's `contact.email`. Standalone (no `house_domain`) trumps any third-party portfolio claim. Compliance fields resolve strictest-of (union); `policy_categories` and brand-level `disclaimers[]` enumerated alongside `data_subject_contestation` and `compliance_policies`. **Typed brand-level trademarks.** New `#/definitions/trademark` extracts the inline house-portfolio shape (`{registry, number, mark}`) as a named definition with optional `status`, `license_type`, `licensor_domain` (when `license_type=licensed_in`), `countries`, and `nice_classes` (Nice Classification for cross-industry disambiguation — Delta-airline vs Delta-faucet). The existing `brand` definition now accepts typed `trademarks: Trademark[]`, enabling both inline `brands[]` entries and self-publishing Brand Canonical Documents to carry their brand-specific marks. House-level `trademarks[]` remains for corporate-level marks; resolution is union. **Conformance invariants** (validator + lint, not JSON Schema expressible): - `brand_id` MUST NOT appear in both `brands[]` and `brand_refs[]`; `brand_id` and `domain` MUST each be unique within `brand_refs[]`. - `house_domain` MUST NOT appear inside `brands[]` entries. - Mutual-assertion verification MUST follow House Redirects on the house side before comparing membership. - `managed_by` is a directory field — consumers MUST NOT use it for trust or authorization. Aggregation by `managed_by` is the intended use. - Standalone trumps third-party claim. - Compliance strictest-of for `data_subject_contestation`, `compliance_policies`, `policy_categories`, audience exclusions, regulated-category flags, and brand-level `disclaimers[]`. - Edge aging via `brand_refs[].effective_at` (or consumer's first observation); AAO's reference crawler ages at 180 days. - Self-healing: leaf-only edges SHOULD trigger consumer-side notification to the house's `contact.email`, rate-limited per `{leaf, house}` pair. **Publisher migration.** Free-text values for the existing inline `trademarks[].status` or `trademarks[].countries` properties now must conform to the typed enum (`active|pending|abandoned|cancelled|expired`) and ISO 3166-1 alpha-2 respectively. Publishers using non-conforming values will surface validation errors and need to update; the field shape was previously open via `additionalProperties: true` so this is the only behaviour change visible to existing data. `brand-json.mdx` is the normative spec — Motivation, the five variants, the trust model with self-healing notification, Adopting `brand_refs[]`, Out-of-scope cases (JVs, PE-opacity, jurisdictional governance), the resolution algorithm, Trademarks, Conformance, and prior art (ads.txt / app-ads.txt / sellers.json, WebFinger / host-meta) all live there. - 013ff96: spec(envelope): add `adcp_error` to `protocol-envelope.json` + envelope-aware lint resolution The `protocol-envelope.json` schema already declared `replayed`, `status`, `task_id`, `context_id`, `governance_context`, etc. — and explicitly states (line 5): "Task response schemas should NOT include these fields - they are protocol-level concerns." Storyboards correctly assert on envelope-level fields (`path: "replayed"`, `path: "adcp_error"`), but the validations-path lint walked only the per-task `response_schema_ref` and never the envelope, so those assertions were stuck behind allowlist entries. Two changes here: 1. **Schema:** add `adcp_error: $ref core/error.json` to `protocol-envelope.json`, mirroring the field's normative description in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. The envelope already had `replayed` for the parallel transport-level idempotency-replay indicator; `adcp_error` is the corresponding transport-level error signal that fatal task failures populate alongside the payload's `errors[]`. The envelope schema previously omitted it — a documentation/schema drift this closes. 2. **Lint:** `lint-storyboard-validations-paths.cjs` now falls back to `protocol-envelope.json` when a path's first segment isn't found in the response schema. Replaces the storyboard-by-storyboard allowlist for envelope-level paths with structural resolution. Both `replayed` (3 entries) and `adcp_error` (1 entry) now resolve cleanly; allowlist drops to zero. ### What this PR is NOT doing The protocol-expert review pushed back on the original direction (adding `replayed` to `create-media-buy-response.json` for "consistency" with 8 mutating-task payload schemas that already define it). Those 8 schemas are themselves violating the envelope contract — they redundantly declare envelope fields at the payload level, contradicting `protocol-envelope.json:5`. Removing `replayed` from those 8 schemas is a separate spec cleanup PR (deprecation-window question for any SDK currently reading off the payload). ### Test plan - [x] `npm run test:schemas` (clean — `adcp_error` field validates as a valid `$ref`) - [x] `npm run test:storyboard-validations-paths` (13 tests pass; 3 new cases lock in envelope-aware resolution and the "first segment must match an envelope property for fallback to fire" rule) - [x] `npm run test:examples` - [x] Lint runs clean across all 82 storyboard files with an empty allowlist - bd3a18c: spec(error): standardize VALIDATION_ERROR `issues[]` as a normative field on `core/error.json` Closes #3059. Adds an optional top-level `issues` array to the standard error envelope, normalizing what `@adcp/client` (and prospectively `adcp-go` / `adcp-client-python` / hand-rolled sellers) already need for multi-field validation rejections. **Why minor**: new optional field on a published schema (`core/error.json`). Existing senders/receivers stay conformant — the field is additive. Receivers that ignore unknown fields keep working; receivers that look for it gain a richer pointer map without parsing `message` text. **Shape**: each entry is `{ pointer (RFC 6901), message, keyword, schemaPath? }`. `schemaPath` MAY be omitted in production to avoid fingerprinting `oneOf` branch selection on adversarial payloads. **Backward compatibility with `field` (singular)**: when both are present, sellers SHOULD set `field` to `issues[0].pointer`. Pre-3.1 consumers reading only `field` get the first failure; 3.1+ consumers prefer the top-level `issues`. **`details.issues` mirror**: sellers MAY mirror `issues[]` into `details.issues` for backward compat with consumers reading from `details`. New consumers should prefer top-level. Updates: - `static/schemas/source/core/error.json` — adds `issues` property with item shape - `docs/building/implementation/error-handling.mdx` — adds `issues` to the error-envelope field table; clarifies `field`/`issues` interaction - 6da3000: spec(error): canonicalize `schema_id` + `discriminator` on `core/error.json#issues[]`; unify the validator-internals production-emit stance with carve-outs Closes #3867. Adds two optional fields to every `issues[]` item on the standard error envelope and harmonizes production-emit guidance across the three validator-internals fields (`schemaPath`, `schema_id`, `discriminator`) — including normative carve-outs for cases where the public-spec replay rationale doesn't apply. **Why minor**: pure additive optional fields on a published schema. Existing senders/receivers stay conformant — both fields ride the wire today through `additionalProperties: true` via `@adcp/sdk`'s TypeScript client (adcp-client#1307), which is what motivated canonicalization. Cross-SDK consumers (Python, Go) couldn't rely on the field names without a spec entry. **`schema_id`** — the `$id` of the rejecting (sub-)schema. For tools served from the flat tree (modular, with `$ref`s preserved), this lands on the deepest published sub-schema (e.g. `/schemas/3.1.0/core/activation-key.json`) so the adopter can navigate directly to the failing variant. For tools served from the bundled tree, `$id` preservation during bundling (companion change in `scripts/build-schemas.cjs`, also closing #3868) lets `schema_id` reach the same deep sub-schema; consumers reading bundles produced before that fix see the response-root `$id` instead, which still names a valid published schema. Snake_case to match the rest of the error envelope (`retry_after`, etc.); the older `schemaPath` (camelCase) is retained for 3.0.x backward compatibility and renamed to `schema_path` in a future major. **`discriminator`** — array of `{property_name, value}` pairs identifying the const-discriminated variant the validator selected from values present in the payload. The inner field is named `property_name` (not `field`) to avoid collision with the top-level `error.field` (JSONPath-lite pointer to the offending payload location), and to align directly with OpenAPI 3.x `discriminator.propertyName`. Compound discriminators (e.g. `audience-selector`'s `(type, value_type)`) produce multiple entries; entry order MUST follow declaration order in the rejecting schema's `properties` block. The discriminator semantics are tightened to avoid leaking validator implementation details: - Sellers MUST populate only when the rejecting schema is a const-discriminated `oneOf` / `anyOf` AND the discriminator property is present in the payload — emission on partial-match inference would fingerprint the seller's validator (Ajv vs Python `jsonschema` vs `gojsonschema` diverge on tie-breaking). - Sellers MUST omit `discriminator` when zero variants survive validation; omission is the agent's signal that the validator could not localize a target variant. - The wire field reports the value the caller sent — not a validator inference — so it is deterministic across implementations. **Validator-internals production-emit stance.** The earlier prose on `schemaPath` (`SHOULD NOT emit on production-facing endpoints — leaks which oneOf branch the validator selected, a probe oracle for adversarial callers`) is incompatible with shipping `discriminator` and `schema_id`, both of which expose the same "validator's chosen variant" surface. The resolution: the public-spec rationale wins **with explicit carve-outs**, replacing the blanket SHOULD-NOT. The base rationale: schemas are published at adcontextprotocol.org and bundled with every SDK, so when the rejecting element is in the public spec, an adversary can replay the same validator locally against the same payload and derive branch selection from the payload alone — the wire field carries no information the adversary can't compute. The carve-outs (normatively documented in `error-handling.mdx`): - **Private extensions.** Sellers running schemas with custom `oneOf` branches, server-only sub-schemas, or enum subsets layered via `additionalProperties: true` MUST NOT emit `schema_id`, `schemaPath`, or `discriminator` when the rejecting element is not in the published spec. Replay-locally is structurally inapplicable. - **Version skew.** Sellers validating against a pre-release or post-release schema MUST NOT emit a `schema_id` whose `$id` is not in the published bundle for the version named in `get_adcp_capabilities`. - **Custom keywords.** `keyword` MUST be drawn from the JSON Schema Draft 7 / 2020-12 vocabulary; validator-specific custom keywords MUST NOT be emitted on the wire. - **Probe terseness.** Sellers MAY scope all three fields to dev/sandbox responses on rate-limited production endpoints to keep envelopes terse, even when the carve-outs above don't apply. Field omission is always conformant. Updates: - `static/schemas/source/core/error.json` — adds `schema_id` (string) and `discriminator` (array of `{property_name, value}`) properties under `issues.items.properties`; rewrites the `schemaPath` description to drop the SHOULD-NOT framing and point at the unified production-emit stance. - `docs/building/implementation/error-handling.mdx` — adds a `Validator-internals fields on issues` subsection covering field semantics, `schema_id` resolution path (HTTPS canonical / SDK-bundled / bundled-tree caveat / validator strict-mode requirement), discriminator semantics, and the four carve-outs. **Open question carried in the PR description, not blocked on this changeset**: should `discriminator` be an object map (`{type: "audience", value_type: "ids"}`) instead of an array of pairs? The array shape matches what `@adcp/sdk` already emits and what #3867 proposes; the object map is more ergonomic for compound-discriminator consumers (`if (d.type === "audience")` vs `.find(d => d.property_name === "type")`). Resolved as array for v3.1; revisit before v4. - 4c12454: spec: expert-review follow-ups on the 3.1 WG-review batch (#4399 / #4399b / #4107 / #4227 / #4371 / #2911). Consolidated fixes from four-expert review (ad-tech-protocol-expert, adtech-product-expert, security-reviewer, docs-expert) of the 9-commit WG-review batch on this branch: **Staged enforcement on universal idempotency_key.** Product expert flagged that a hard "MUST reject reads without idempotency_key" cliff at the 3.1 cut breaks hand-rolled integrators built via curl / thin MCP clients / OpenAPI codegen that doesn't include the field uniformly. Switched to staged: **3.1.0** sellers MUST accept reads carrying `idempotency_key` and SHOULD reject reads that omit it (MAY accept the omission during the 3.1.x maintenance window); **3.2.0** sellers MUST reject. SDK-using integrators (`@adcp/client`, `adcp-py`) are unaffected since both already send uniformly. **Cache-at-rest encryption (security reviewer M2).** Universal `idempotency_key` from 3.1 means the cache holds account-scoped read responses (`get_products`, `list_accounts`, `list_creatives`, `get_signals`), not just write receipts. Added: sellers MUST encrypt the cache tier at rest with the same controls used for the underlying resource store, MUST NOT treat the cache as a transient retry-receipt store exempt from data-at-rest controls, and MUST scope reads by `(authenticated_agent, account_id)` at the storage layer (not just application layer). **Forward-compatible decoding bounded by retry budget (security reviewer M1).** A receiver that literal-reads the new "default `transient` for unknown codes" rule and writes a retry loop without `maxRetries` could be DOS'd by a hostile sender emitting `code=GO_FOREVER, recovery=transient`. Added: the `transient` default is bounded by §Retry Logic (`maxRetries` + jittered exponential backoff); receivers MUST NOT loop indefinitely. Cross-link added from §Idempotency Buyer obligations to Forward-compatible decoding (the asymmetric link gap docs reviewer flagged). **Stale `replayed.description` (flagged by both protocol and docs reviewers).** `core/protocol-envelope.json` still said "Only present on responses to mutating requests that carry idempotency_key" — contradicts both the universal-idempotency change and the replay-snapshot rule. Updated to: "MAY appear on responses to any request that resolved via the idempotency cache, including read tools". **A2A serialization framing (protocol reviewer).** Envelope `notes` array described `task.artifacts[0].parts[].DataPart` and `task.status.message.parts[].DataPart` as symmetric, but `a2a-response-extraction.mdx` treats artifacts as canonical and `status.message.parts[]` as the fallback container only for interim states. Tightened to match the canonical/fallback framing and pinned the A2A version (0.3.0+). **Cache-growth ceiling acknowledgment (protocol reviewer optional).** Rule 8's recommended 60/sec sustained ceiling was sized against a write-heavy launch pattern. Added a note that read traffic now contributes under universal idempotency and operators with read-heavy mixes SHOULD revisit the deployed ceiling at the 3.1 cut rather than accept silent `RATE_LIMITED` of legitimate reads. The numeric recommendations remain the right starting _shape_, not the right starting _magnitude_, when reads dominate. **Mint `MULTI_FINALIZE_UNSUPPORTED` (protocol reviewer optional).** Protocol reviewer flagged that `INVALID_REQUEST` for a seller-side capability gap on multi-finalize ($refine[]$ atomicity) blurs "I can't support this combination" with "your request is malformed." Added `MULTI_FINALIZE_UNSUPPORTED` as the preferred code (`recovery: correctable`); `INVALID_REQUEST` remains acceptable for sellers on pre-3.1 error catalogs. **3.1.0 release-notes — `Wire conformance` section + adopter-action table (docs reviewer).** The reach_window section was the only 3.1.0 entry; the spec changes from the WG-review batch were invisible to a 3.0→3.1 migrator reading release-notes. Added a new `### Wire conformance — idempotency & envelope tolerance` section covering all 8 spec changes plus an adopter-action table for the seven distinct integrator categories (SDK-using buyers, hand-rolled MCP clients, FastMCP/Pydantic/Zod sellers, sellers with synchronous-success state-tracking responses, agentic buyers reading `status` from mutations, sellers emitting unsigned webhooks or deprecated specialism claims, sellers emitting unknown error codes). **`get_adcp_capabilities.mdx` idempotency block (docs reviewer consistency).** Stale "for mutating requests" framing on the capability description updated to reference the staged universalization and link to security.mdx. **3.0→3.1 sender audit note (protocol reviewer optional).** `error.recovery` MUST-populate-from-3.1 rule is safe for buyers (they default to `transient` when absent) but sellers ratcheting `adcp_version` to 3.1 with un-audited error-emit code paths are non-conformant. Adopter-action table calls this out explicitly. Files: - `docs/building/by-layer/L1/security.mdx` — enforcement-curve paragraph, cache-at-rest paragraph, retry-bounded cross-link, rule-8 read-traffic acknowledgment - `docs/building/by-layer/L3/error-handling.mdx` — `transient`-default bounded-by-retry sentence, `MULTI_FINALIZE_UNSUPPORTED` table row - `docs/building/operating/transport-errors.mdx` — `MULTI_FINALIZE_UNSUPPORTED` recovery row - `docs/protocol/get_adcp_capabilities.mdx` — idempotency block updated for staged universalization - `docs/reference/release-notes.mdx` — new `### Wire conformance` section under 3.1.0 with adopter-action table - `docs/media-buy/product-discovery/refinement.mdx` — `MULTI_FINALIZE_UNSUPPORTED` referenced, error table row added - `docs/media-buy/task-reference/get_products.mdx` — `MULTI_FINALIZE_UNSUPPORTED` error table row - `static/schemas/source/core/protocol-envelope.json` — `replayed.description` rewritten, A2A serialization framing tightened - `static/schemas/source/enums/error-code.json` — `MULTI_FINALIZE_UNSUPPORTED` added to enum / enumDescriptions / enumMetadata (recovery: correctable) - `scripts/error-code-drift-dispositions.json` — `MULTI_FINALIZE_UNSUPPORTED` held-for-next-minor / 3.1 - `static/schemas/source/media-buy/get-products-request.json` — multi-finalize description references the preferred code Refs the eight prior commits in this WG-review batch. - 9633927: docs(spec-guidelines): enum-membership criterion + reconcile sync_catalogs phantom error codes (#3456) Records the **enum-membership criterion** as a durable spec-authoring guideline in `docs/spec-guidelines.md` (under Enum Design), generalizing the decision recorded on #3456: a value earns membership when it is **published**, **natively supported** (handled without bespoke per-value mapping), and has **shared demand** (relevant across >1 producer AND >1 consumer); a material dialect earns its own value only when the parent's consumer would mis-handle it. `feed_format` (#3456) is the worked example, with a note distinguishing this from platform-agnosticism (a `feed_format` value legitimately names a vendor's _published spec_). Reconciles four error codes documented in the `sync_catalogs` error table but absent from `enums/error-code.json` — `FEED_FETCH_FAILED`, `INVALID_FEED_FORMAT`, `ITEM_VALIDATION_FAILED`, `CATALOG_LIMIT_EXCEEDED` — adding them to the canonical enum with `enumDescriptions` + `enumMetadata` (all `recovery: correctable`) and `held-for-next-minor` (3.1) drift dispositions. The `INVALID_FEED_FORMAT` phantom flagged on #5271 turned out to be one of four siblings in the same table. Refs #3456 (resolution shipped in #5298; this is the durable docs formalization + the error-code reconciliation). - 7129dbc: Add `tiktok_shop`, `pinterest_catalog`, and `openai_product_feed` to the `feed_format` enum, and reconcile `brand.json` to reference the canonical enum. All three are externally-documented, Google-Merchant-Center-derived product-feed dialects that real sellers (TikTok Shop, Pinterest, OpenAI/ChatGPT commerce) parse natively — so buyers declaring them no longer have to fall back to `custom` + `feed_field_mappings` to re-describe a standardized feed. Each carries material deltas a strict GMC parser would mis-handle (TikTok `sku_id`/`video_link`; Pinterest composite price/shipping + mandatory `google_product_category`; OpenAI `is_eligible_*` flags), which is the bar for a dialect to earn its own value under the #3456 enum-membership criterion. `feed_format` values are vendor spec names (proper nouns), not semantic categories — a feed format _is_ the vendor's published spec, so there is no vendor-neutral name (the deliberate inverse of the semantic `video_placement_types`/`social_placement_surfaces` axes). The enum now carries `enumDescriptions` documenting each format and citing its spec. `brand.json` previously inlined a drifted feed_format enum (it had `openai_product_feed` but was missing `shopify`/`linkedin_jobs`); it now `$ref`s `/schemas/enums/feed-format.json` (matching `core/catalog.json`), so the two surfaces can no longer diverge. `feed_format` is a seller-side parsing label only — AdCP ships no per-format mapping table and SDKs do not parse feeds, so first-class membership is a label + SDK enum-widening, not a parser obligation. Closes #5271. Implements the #3456 enum-membership criterion. - 0276746: Add optional `filter_diagnostics` block to `get_products` response — non-fatal observability for the filter-not-fail empty-result UX gap. Closes #3482. **The gap.** Every `required_*` filter in `product-filters.json` is silent-exclude semantics (the established AdCP convention; matches OpenRTB / SSP capability discovery patterns). When the result list is empty or unexpectedly small, the buyer can't distinguish: - "No inventory matches the brief" - "`required_metrics` excluded everything" - "`required_geo_targeting` excluded everything" - "`budget_range` had no overlap with available products" Today the buyer must blindly relax filters one at a time to discover which one was unsatisfiable. Both pre-build expert reviewers (protocol and product) independently flagged this as the buyer-side observability gap on PR #3472 (`required_metrics`). **Shape.** Optional, additive, observability — not error reporting: ```json { "products": [], "filter_diagnostics": { "total_candidates": 47, "excluded_by": { "required_metrics": { "count": 31, "values": ["completed_views"] }, "required_geo_targeting": { "count": 9 }, "budget_range": { "count": 7 } } } } ``` - `total_candidates`: integer baseline before filters applied. May be sampled or capped at large catalogs. - `excluded_by`: keyed by filter property name as it appears in the request's `filters` object. Each value carries `count` (required), optional `values` (the specific filter values that contributed to exclusions), and optional `notes` (human-readable narrative). **Counts only — never product names.** Listing excluded products would leak competitive intelligence about adjacent campaigns or seller inventory. Counts plus `values` (the filter inputs that did the excluding, not the products that got excluded) is enough for triage without that leakage. **Counting semantics intentionally loose.** Sellers vary on whether to count products excluded by ANY filter or ONLY by this filter. The spec documents the field as approximate — buyers SHOULD treat counts as triage signal, not exact accounting. Tightening this would force every seller to implement the same ordering of filter evaluations, which is an internal-architecture imposition AdCP shouldn't make. **Wired in.** - `media-buy/get-products-response.json`: new optional `filter_diagnostics` object with the shape above. `additionalProperties: true` on each per-filter detail object so filter-specific extensions (e.g., per-metric breakdown) can land later without spec churn. - `docs/media-buy/task-reference/get_products.mdx`: new Response Metadata row + dedicated `filter_diagnostics` section with field table and example response. **Backwards compatibility.** Optional and additive. Sellers that don't populate the field, and buyers that don't consume it, see no change. **Sell-side adoption.** Zero cost for sellers who don't populate it. Sellers that already track per-filter exclusion counts internally surface them with a single new field on their response builder. Sellers without that instrumentation can adopt incrementally — the field's absence is conformant. Closes #3482. - 19813bd: Align `get_creative_features` documentation with its already-Final lifecycle stage, and close a phantom error code. Per [specification-lifecycle](docs/reference/specification-lifecycle.mdx) (a surface with no `x-status` marker that has shipped in a GA release is at the **Final** stage), `get_creative_features` is already Final: none of its schemas (`get-creative-features-request.json`, `get-creative-features-response.json`, `creative-feature-result.json`) carry `x-status: experimental`, it shipped in 3.0 GA, and it is absent from the canonical `experimental_features` list in [experimental-status](docs/reference/experimental-status.mdx). It is listed as a **Required** creative-governance task in `docs/protocol/required-tasks.mdx`, and its capability is advertised via `get_adcp_capabilities.creative_features[]`. The task carried a stale "AdCP 3.0 Proposal — under development" prose banner that contradicted that Final state. This is not a Proposed→Final transition — the lifecycle stage is unchanged — so no decision record is required; it removes a contradictory documentation artifact. **Changes** - Removed the proposal `` banner from `docs/governance/creative/get_creative_features.mdx` and the creative-governance section landing page `docs/governance/creative/index.mdx`. The section's only banner-marked page was `get_creative_features`; `provenance-verification` carries no proposal banner. - Added `CREATIVE_INACCESSIBLE` to the canonical error-code enum (with `enumDescriptions` and `enumMetadata`, recovery `correctable`). The `get_creative_features` error example documented this code but it was absent from the enum — a documented task surface must not emit a phantom code (#3456 enum-membership criterion). It fires when a creative governance agent cannot retrieve the submitted `creative_manifest` assets at all — distinct from `CREATIVE_NOT_FOUND` (a `creative_id` absent from the agent's library), `CREATIVE_REJECTED` (assets retrieved but failed policy), and `GOVERNANCE_UNAVAILABLE` (agent unreachable; transient). No schema field changes; no behavior change to the task. The `creative/specification.mdx` (v1 creative model) and `media-buy/specification.mdx` proposal banners are unrelated surfaces and unchanged. The frozen `dist/docs//` release snapshots still carry the banner by design — they refresh at the next snapshot cut, not on content PRs. This unblocks the 3.1 creative-feature-oracle gate/rank pipeline (#5311 / #5305), which uses `get_creative_features` as the gate's feature source. Refs #5311, #5305, #3456. - 377ef99: Add optional `currency` and `total_budget` fields to `CreateMediaBuySuccess` and `UpdateMediaBuySuccess` response schemas to match the entity shape already required by `get_media_buys`. Sellers using a shared mapper across create/list will now have these fields declared in the create and update schemas, eliminating silent Zod validation failures on `get_media_buys` poll steps. - 1154e9d: feat(schema): add `Submitted` arm to per-tool response `oneOf` for `update_media_buy`, `build_creative`, and `sync_catalogs` (#3392) AdCP 3.0 shipped `*-async-response-submitted.json` schemas for 6 HITL tools but only 2 of 6 per-tool `xxx-response.json` schemas included the `Submitted` arm in their top-level `oneOf`. This left SDK codegen unable to generate typed `*Task` HITL methods for the 4 missing tools. This changeset fixes 3 of the 4 gaps (the `get_products` case is flagged for human review — see #3392): - `update-media-buy-response.json` — adds `UpdateMediaBuySubmitted` arm (`status: "submitted"` + `task_id`); updates `UpdateMediaBuyError.not` to exclude the submitted state - `build-creative-response.json` — adds `BuildCreativeSubmitted` arm; updates `BuildCreativeError.not` to exclude the submitted state - `sync-catalogs-response.json` — adds `SyncCatalogsSubmitted` arm; updates `SyncCatalogsError.not` to exclude the submitted state Non-breaking: existing `Success | Error` consumers are unaffected. Buyers gain a new permitted response shape and SDK codegen can produce typed HITL methods for these three tools. Note: the fix uses the same inline arm pattern as `create-media-buy-response.json` and `sync-creatives-response.json` — not `$ref` to the `*-async-response-submitted.json` schemas (those are task-completion artifact payloads for the webhook path, not the initial-response discriminated arm). Closes partial scope of #3392. - af1d287: spec(creative): generative-encoding safe additions — `free_text` params + per-output transformer pricing. The additive half of the generative-agent (Veo/Imagen) encodings follow-on. The two _normative_ rules it pairs with — generation count is owned by `max_variants`/`max_creatives` (never a config param), and `aspect_ratio` rides the format axis — are intentionally left to the working group; only the safe schema bits land here. - `transformer-param.json` `value_source` gains **`free_text`** (an open buyer-authored string with no closed set — e.g. a `negative_prompt` or style note; `type` MUST be `string`, the closed-set fields MUST be absent) plus an optional **`max_length`**. The description also states that count/quantity knobs MUST NOT be params (count rides `max_variants`/`max_creatives`). - `vendor-pricing-option.json` gains optional **`applies_to_output_format_ids`** so one creative transformer can price different outputs differently (e.g. a multi-publisher template charging per publisher format); an unscoped option is the default. Additive and inert for non-creative vendors (signals/governance) — **flagged for shared-schema owner ack**. - 271f669: Add `filters.pricing_currencies` to `get_products` so buyers can restrict discovery to media products priced in currencies they can transact in. The filter matches products with at least one product-level `pricing_options` entry in a requested ISO 4217 currency, requires mandatory product-scoped signal charges to be satisfiable in those currencies or have no incremental price, and requires sellers to prune returned product-level `pricing_options` to matching currencies. - 7b2de61: Single governance agent per account — reconcile 3.x governance schemas with a coherent semantic model (closes #3010). **The inconsistency.** 3.x registration (`sync_governance`) allowed up to 10 governance agents per account with per-agent `categories`, and the campaign-governance spec documented fan-out-and-unanimous-approval. But the protocol envelope and `check_governance` carried a single `governance_context` string, and the four-value `scope` enum on brand.json (`spend_authority | delivery_monitor | brand_safety | regulatory_compliance`) didn't carve the governance responsibility at its joints — those aren't independent specialisms held by different authorities, they're phases and facets of one evaluation over one plan. **Decision.** Commit to single-agent: an account binds to one governance agent that owns the full lifecycle. Multi-agent registration was aspirational and produced schema inconsistencies without a coherent semantic story. A plan is unitary (budget, policies, restricted attributes all live on the plan); `check_governance` already separates authorization / fidelity / drift on the `phase` axis (`purchase` / `modification` / `delivery`); internal specialist review (legal, brand safety, category) belongs inside the configured agent, not at the registration layer. **Changes.** - `account/sync-governance-request`: `governance_agents` constrained to `maxItems: 1`. `categories` field removed. Description makes the one-agent-per-account invariant explicit and explains why (phases, not specialisms; plan is unitary; specialist review composes inside the agent). - `core/protocol-envelope`: `governance_context` stays a singular string. Description updated to state the single-agent invariant and why phased lifecycle (not split authority) means one token covers the full governed action. - `brand.json`: remove the governance-agent `scope` enum (`spend_authority | delivery_monitor | brand_safety | regulatory_compliance`) — no longer meaningful under single-agent registration. P&G example updated to drop the stray `scope` array. - `docs/governance/campaign/specification.mdx`: replace "Multi-agent composition" with "One governance agent per account" explaining the rationale (authorization/fidelity/drift are phases, regulatory rules are encoded in the plan, specialist review composes inside the agent, one lifecycle/one token/one audit trail). Fix the remaining `governance_agent(s)` plural residue. - `governance/check-governance-request` / `response` / `report-plan-outcome-request`: revert any language implying per-agent fan-out; all three are single-agent calls as originally designed. - `docs/governance/campaign/tasks/check_governance.mdx`, `report_plan_outcome.mdx`: revert to the single-agent prose. **Backwards compatibility.** Buyers with one agent registered (practically every 3.0 deployment per maintainer's reading of the ecosystem) are unaffected. Buyers that registered more than one agent per account against the previous `maxItems: 10` — if any exist — MUST collapse to a single agent; the protocol does not support routing or aggregating across multiple. Sellers that validated the `categories` field MUST treat registrations without it as valid (the field is removed, not deprecated). **What this is not.** This PR does not address specialist governance surfaces adjacent to campaign governance — brand-safety pre-screen of creatives, property-list policy, content-standards evaluation — those are separate governance domains with their own agents and their own lifecycle. Campaign governance speaks only for the plan. - 2578146: spec(idempotency): declare `capabilities.idempotency.in_flight_max_seconds` so buyers can compute retry budgets Closes #4406. Follow-up to #4402 (rules 9 + 10 + IDEMPOTENCY_IN_FLIGHT). Rule 9 requires sellers to bound the lifetime of an in-flight idempotency row to their declared per-task handler timeout. That bound exists in every conformant seller's deployment but is not buyer-observable — `capabilities.idempotency` currently declares `replay_ttl_seconds` (1h–7d) only, which is far wider than a realistic handler timeout. A buyer that retries on `IDEMPOTENCY_IN_FLIGHT` must either pick an arbitrary retry budget or be told to wait up to the full `replay_ttl_seconds` ceiling. This change adds an optional `in_flight_max_seconds` field to the `IdempotencySupported` branch of `adcp.idempotency`: - **Optional in 3.1.** SDKs that don't see the field fall back to rule 9's order-of-magnitude SHOULD heuristic. Additive change; no existing seller is non-compliant for omitting it. - **Required when `supported: true` in 4.0** — same migration path `replay_ttl_seconds` followed across the 2.x → 3.x boundary. Buyers get a guaranteed bound at the next major. - **Bounded** `integer ≥ 1, ≤ 604800` at the schema layer; cross-field bound `≤ replay_ttl_seconds` is enforced by the composed-schema validation suite (JSON Schema cannot express field-relative bounds). - **Forbidden on the `IdempotencyUnsupported` branch.** No replay window means no in-flight bound — mirrors the existing `replay_ttl_seconds` treatment. Buyer SDKs use the declared value to: - Cap individual retry waits on `IDEMPOTENCY_IN_FLIGHT` at this value rather than the much-wider `replay_ttl_seconds` ceiling. - Surface meaningful "your retry will succeed or fail within N seconds" hints to operators. - Treat any `error.details.retry_after` exceeding this value as a seller bug — the in-flight row cannot legitimately outlive the declared bound. Rule 9 in `security.mdx` is updated to point at the new capability field as the primary retry-budget bound when declared; the order-of-magnitude heuristic remains the fallback for sellers that haven't yet adopted the field. - 231bc2e: spec(idempotency): add normative rules for concurrent retries and downstream reconciliation; introduce `IDEMPOTENCY_IN_FLIGHT` Two new normative rules in `L1/security.mdx#idempotency`: **Rule 9 — Concurrent retries / first-insert-wins.** A second request carrying the same `(authenticated_agent, account_id, idempotency_key)` MAY arrive while the first is still executing. Sellers MUST resolve the race deterministically (`INSERT … ON CONFLICT DO NOTHING` on the scope tuple) and MAY pick one of two policies, behaving consistently: **wait-and-replay** (block the second request until the first completes, return cached response with `replayed: true`), or **reject-and-redirect** (return new `IDEMPOTENCY_IN_FLIGHT` code with `error.details.retry_after`). Same key with a _different_ canonical payload during the in-flight window still returns `IDEMPOTENCY_CONFLICT` (rule 5). Verified against the canonical Python sales-agent (Wonderstruck) — its wait-and-replay implementation passes the new rule out of the box. **Rule 10 — Crossing service boundaries / downstream reconciliation.** When a seller invokes a downstream system (SSP, ad server, payment provider) during request handling, "errors don't cache" (rule 3) is necessary but not sufficient — a crash between downstream-accepts and local-persist leaves the seller in a "downstream unknown" state. Sellers MUST adopt one of two patterns for every downstream call whose duplicate-invocation has business consequences: **write-claim-before-invoke** (persist a claim row with `downstream_request_id` before invoking; reconcile on retry by querying the downstream by that id) or **thread-buyer-key** (pass the buyer's `idempotency_key` or a deterministic seller-side derivative as the downstream's own idempotency key). The pattern "best-effort dedup on downstream response inspection" is explicitly forbidden. **New error code: `IDEMPOTENCY_IN_FLIGHT`** (held for 3.1 per the wire-stability policy). Recovery: transient. Buyers MUST retry with the **same** `idempotency_key` after `error.details.retry_after` — minting a fresh key on this code turns a safe retry into a double-execution race. **Transitional note on `SERVICE_UNAVAILABLE + retry_after`.** Both reference implementations today (the Python sales-agent at `wonderstruck.sales-agent.scope3.com` and the `@adcp/sdk` middleware) implement wait-and-replay (rule 9's other policy) and never need to emit `IDEMPOTENCY_IN_FLIGHT`. SDKs that previously emitted `SERVICE_UNAVAILABLE + retry_after: 1` on the in-flight branch are NOT out of compliance with rule 9 as long as they adopt wait-and-replay end-to-end — `IDEMPOTENCY_IN_FLIGHT` is only required when a seller picks reject-and-redirect. The `@adcp/sdk` middleware swap from `SERVICE_UNAVAILABLE` to `IDEMPOTENCY_IN_FLIGHT` is tracked separately (adcp-client follow-up); it's a wire-code tightening, not a behavioral change. **Storyboard coverage.** `static/compliance/source/universal/idempotency.yaml` gains a `concurrent_retry` phase using two new cross-response check kinds (`cross_response_count_distinct`, `cross_response_field_equal`) that operate on the resolved response set across N parallel dispatches. The runner contract is documented in the new `test-kits/parallel-dispatch-runner.yaml`; runners without parallel-dispatch support skip the phase with a stable not_applicable marker. SDK/runner implementation tracked separately (adcp-client follow-up). Author skill (`skills/call-adcp-agent/SKILL.md`) and the buyer-facing `docs/protocol/calling-an-agent.mdx` updated so buyers know to wait-and-retry on `IDEMPOTENCY_IN_FLIGHT` rather than mint a fresh key. - 5015802: Add `IMPRESSION_ID` universal macro for impression-level deduplication A general-purpose per-impression identifier macro that buyers, measurement vendors, verification services, and TMP can use for per-impression dedup, cross-vendor reconciliation, pixel-retry detection, and (in TMP) cross-identity exposure dedup. Closes the gap where TMP context-only impressions had no impression_id available (no `{TMPX}` → no buyer-side decode-time mint). Format is implementation choice — UUID, ULID, snowflake, or any collision-resistant scheme. Three-layer minting hierarchy: (1) publisher first-party code, (2) ad-decision layer (Prebid TMP module, ad server, SSP), (3) buyer impression tracker at `{TMPX}` decode (TMP-specific fallback). Each lower layer MUST defer to whatever an upstream layer already minted. Documents the Prebid TMP module pattern using the `tmp_impression_id` GAM targeting key and the optional reuse of `adUnit.transactionId` when Prebid's `enableTIDs` config is on. No router changes; preserves TMP's identity↔context structural separation by keeping minting at the publisher/decision-layer join. - 1a2b9e3: Add non-colliding AdCP task-lifecycle aliases in the protocol namespace: `get_task_status` and `list_tasks`. These are aliases for AdCP's application-layer lifecycle tools, not aliases for transport-native MCP/A2A `tasks/*` APIs. The existing `core/tasks-get-*` and `core/tasks-list-*` schemas remain valid through 3.x for compatibility; the new aliases avoid transport-name collisions without changing AdCP async task polling or reconciliation semantics. - f44fba3: Three small cleanups from the measurement schema audit (closes audit findings §3.8 and §3.10; finishes the prose-side work for #3863). **§3.8 — `attribution-window` dedup.** `optimization-goal.json` previously inlined a partial `attribution_window` shape with `post_click` and `post_view` but no `model`, with `post_click` required. The canonical `core/attribution-window.json` has `post_click`, `post_view`, and `model` with `model` required. Two surfaces describing the same concept with conflicting constraints. Fix: - `optimization-goal.json` `attribution_window` collapses to `$ref attribution-window.json` so there's one canonical shape. - `attribution-window.json` `model` becomes optional (was required). Absence means the seller's default attribution model applies (typically `last_touch` per industry convention). Sellers SHOULD populate `model` when committing to a specific methodology. Buyers reading delivery reports get the seller's choice when set; fall back to default when not. **§3.10 — `dooh_metrics.calculation_notes` description tightening.** Previously a one-liner ("Explanation of how DOOH impressions were calculated") that read like a primary methodology surface. Tightened to clarify it's for **row-specific supplementary context** (a particular daypart's calculation, a venue-mix exception) — the canonical methodology declaration belongs on the measurement vendor's `get_adcp_capabilities.measurement.metrics[]` block where it's discoverable once and inherited across delivery rows. Doesn't deprecate the field — DOOH methodology genuinely has row-level exceptions worth carrying inline. **#3863 — `forecastable-metric.json` description drift fix.** The description previously claimed `audience_size`, `measured_impressions`, `grps`, `reach`, `frequency` were forecast-only deltas. **Wrong:** `grps`, `reach`, `frequency` are also in `available-metric.json` (have been since their introduction). The actual forecast-only deltas are `audience_size` and `measured_impressions`. Description corrected. Closes the prose-cross-reference half of #3863; the schema-level enforcement of overlap (build-script work, not schema work) is deferred. **Backwards compatibility.** All three changes are additive or relax existing constraints (the `attribution-window.model` requirement relaxation makes previously-failing payloads valid; previously-valid payloads remain valid). No breaking changes. Closes audit findings §3.8 and §3.10. Substantially closes #3863 (prose cross-references); build-script overlap enforcement deferred to a follow-up. - 12bfb06: Add `measurement` capability block to `get_adcp_capabilities`. Closes #3612 (the protocol surface piece of the per-metric catalog discovery design from #3586). Unblocks #3613 (AAO crawler + index implementation). **Adds `measurement` to `supported_protocols` and `enums/adcp-protocol.json`.** Measurement is a protocol-in-development. The capability block ships now so measurement vendors can publish their catalogs and AAO can crawl them; additional measurement tasks (reporting, attribution, panel queries) and a baseline compliance storyboard land in subsequent minors. Same as every other protocol — `creative` is in `supported_protocols` AND has a capability block; same for `governance`. Measurement follows the same model. **Self-describing, parallels other agents.** Every AdCP agent type publishes capabilities at the agent itself (sales / creative / governance / brand / buying / signals / rights). Measurement now follows the same pattern with a new `measurement` block whose `metrics[]` array carries the per-metric catalog. The shape mirrors `governance.property_features[]` (typed feature objects in an array) including the `methodology_url` and `methodology_version` fields. **Scope.** An agent claiming `measurement` computes one or more quantitative metrics about ad delivery, exposure, or effect (impression verification, viewability, IVT, attention, brand lift, incrementality, outcomes, emissions — vendors define the surface in `metrics[]`). Returns metric definitions (this block), not pricing or coverage (negotiated per buy via `measurement_terms`) and not live values (returned per buy via `vendor_metric_values`). Same mechanical model as `compliance_testing` and `webhook_signing`. **No closed category enum.** An earlier draft included a closed 12-value `measurement-category.json` enum and a required `category` field on each metric. WG review pushed back on two grounds: (1) categories overlap (e.g., `brand_safety` measurement vs. governance's `content_standards`), making the boundary fuzzy; (2) without a buyer-side discovery primitive consuming the field, the enum was adding schema surface and drift risk without earning its keep. Dropped: `category` field, `measurement-category.json` enum file, `metric_categories[]` on brand.json (already removed in this PR's prior commit). AAO and buyer agents normalize across catalogs from `metric_id`, `description`, `standard_reference`, and `accreditations[]` — all already structured. If a category facet proves useful once #3613's discovery primitive lands, it can be added back as an open vendor-asserted string with real query patterns shaping the taxonomy. **Schema additions.** - `protocol/get-adcp-capabilities-response.json`: new `measurement` block with `metrics[]`. Each metric carries `metric_id` (required), plus optional `standard_reference`, `accreditations[]` (third-party certification list, distinct from `standard_reference` — accrediting body, optional cert ID, validity date, evidence URL), `unit`, `description`, `methodology_url`, and `methodology_version`. `additionalProperties: false` with explicit `ext` slot, matching the governance pattern. `uniqueItems: true` on `metrics[]` — duplicate `metric_id` within one agent's catalog is a conformance bug. **Why `accreditations[]` is separate from `standard_reference`.** A metric can implement a published standard (URL points at the spec) without holding independent third-party accreditation. Buyers asking "is this MRC-accredited?" need a structured answer that survives URL parsing — every vendor pasting the same MRC URL whether accredited or not gives a false signal of comparability. The split surfaces the distinction at the schema layer. **Doc updates.** - `docs/protocol/get_adcp_capabilities.mdx`: new `measurement` section with field table, response example showing `accreditations[]` and `methodology_version`, the discovery-vs-settlement framing, an explicit Scope subsection ("what does claiming `measurement` mean?"), and an explicit "this is a discovery surface, not a rate card" callout (pricing/SLAs/coverage are negotiated per buy via `measurement_terms`). - `docs/registry/index.mdx`: refines the measurement-vendor discovery section to reference the now-defined `measurement` capability block and forward-references the AAO index endpoint (#3613) and the buyer-agent direct-call docs (#3614). - `core/reporting-capabilities.json`: updated `vendor_metrics[]` prose to point at `get_adcp_capabilities.measurement.metrics[]` as the canonical metric-definition source (was previously brand.json). **Backwards compatibility.** All additions are optional and additive. Sellers without measurement capability are unchanged; sellers with measurement capability gain a structured catalog surface. **WG review.** This is the protocol surface for measurement-vendor capability declaration. Three independent expert reviews plus WG pushback shaped this version: kept `measurement` in `supported_protocols` per the protocol-in-development framing, added `methodology_version`, added structured `accreditations[]` to separate "implements a standard" from "third-party certified," dropped the brand.json coarse-filter field, and dropped the closed category enum in favor of letting real catalogs shape the taxonomy. Closes #3612. - f6f90d8: Add optional `vendor: BrandRef` to two vendor-attested rows that lacked structured vendor identity, bringing them into the same identity discipline as `vendor_metric_values`, `performance-standard.vendor`, and `committed_metrics` (vendor-scope entries). **`core/delivery-metrics.json` `viewability`** (closes #3862). Optional but RECOMMENDED — makes the viewability row self-describing so buyer agents reading delivery in isolation can attribute the numbers to a measurement vendor without joining back to `package.committed_metrics` or `package.performance_standards`. Same shape as `vendor_metric_value.vendor` for symmetry. **`core/performance-feedback.json`** (closes #3859). SHOULD be populated when `feedback_source` is `third_party_measurement` or `verification_partner` AND a single attesting vendor exists. OMITTED for blended outputs (MMM mixes from Nielsen MMM / Analytic Partners / in-house models, multi-touch attribution that joins across vendors, clean-room outputs from LiveRamp / Habu / AWS Clean Rooms where the clean room is not itself the measurement source) — exactly the high-value third-party signals that don't have a single attesting vendor. Optional for `buyer_attribution` and `platform_analytics` (those sources are implicit from context). Described in the field; not enforced via JSON Schema `if/then`, matching the precedent set by `performance-standard.standard`. Without the BrandRef on single-vendor feedback, the row is unattributed — consumers can't verify authorization, resolve metric definitions via the vendor's `get_adcp_capabilities.measurement.metrics[]`, or route disputes. Both fields are additive and backwards-compatible. Origin: schema audit run during PR #3843, findings §3.4 and §3.9. Aligns with the [measurement taxonomy](https://docs.adcontextprotocol.org/docs/measurement/taxonomy) doctrinal framing that vendor-attested measurement is anchored on `BrandRef → brand.json agents[type='measurement']` discoverable identities. Doc updates: `docs/media-buy/task-reference/provide_performance_feedback.mdx` (vendor field row, example payload), `docs/media-buy/media-buys/optimization-reporting.mdx` (viewability field list). - c2e3edf: Add a per-agent REST surface at `/api/me/agents` so members can register, list, update, and remove individual agents from CI or scripts via WorkOS API key (Bearer `sk_…`) — no full-profile round-trip and no Addie/UI dependency. Reuses the same visibility gate and server-side type resolution as `PUT /api/me/member-profile`; type-resolution flips (the smuggle-protection events) are audit-logged. Writes serialize through `SELECT … FOR UPDATE` on `member_profiles` so concurrent register/update/delete calls cannot race the JSONB read-modify-write. Multi-org callers may pass `?org=…` to target a non-primary org; verification goes through `resolveUserOrgMembership`. `DELETE /api/me/agents/{url}` returns `409 unpublish_first` when the agent is currently `public` so the registry catalog and the published `brand.json` cannot silently disagree. `PATCH /api/me/agents/{url}` with a body `url` that disagrees with the path returns `400 url_immutable` rather than dropping the rename silently. - 0b2cf2b: Add `metric_aggregates` partition to `aggregated_totals` on `get_media_buy_delivery` — qualifier-aware delivery rollups symmetric to `committed_metrics`. Closes #3848. Supersedes #3631 and #3833 (both already closed). **The atomic unit is now identical across contract, diff, and delivery.** Each surface carries `(scope, metric_id, qualifier, …)` rows; reconciliation collapses to a row-level join on the tuple. `committed_metrics` adds `committed_at`; `missing_metrics` strips it; `metric_aggregates` swaps it for `value` plus per-metric component fields. **Provides the structural primitive for solving apples-to-oranges sums.** MRC and GroupM viewability define materially different thresholds and must never be combined into a single cross-buy rate. The partition shape (one row per `(metric_id, full-qualifier-set)`) makes the partition expressible; future qualifier-aware metrics (`completion_rate` × completion threshold; attention scoring × methodology if it standardizes) plug into the same shape with no schema break. Note: this PR ships the _structure_ — sellers actually emitting partitioned rows requires a forcing function from the contract surface (buyers committing to specific qualifiers via `committed_metrics`) plus seller adoption. Expect adoption to lag the structure until a real contract demand exists. **Schema additions.** - `media-buy/get-media-buy-delivery-response.json` `aggregated_totals.metric_aggregates`: array of discriminated rows. Two oneOf branches (`scope: standard` / `scope: vendor`), `additionalProperties: false` on both (matching `committed_metrics` symmetry), reusing the qualifier shape from `core/package.json` `committed_metrics` and the BrandRef pattern from `core/vendor-metric-value.json`. Per-metric component fields (`measurable_impressions`, `viewable_impressions`, `impressions`, `completed_views`, `spend`, `conversions`, `conversion_value`, `clicks`) inlined as siblings of `value` rather than nested in a `components` sub-object — flatter, matches the per-buy `viewability` block's existing flat shape. Per-metric required components enforced via `if/then` for the four highest-traffic metrics (`viewable_rate`, `completion_rate`, `cost_per_acquisition`, `roas`); other metrics rely on prose-described components today (full `oneOf` discriminated on `metric_id` would be 31+ branches; deferred to a future minor if conformance testing demands). - `core/package.json` `committed_metrics` description updated to cross-link `aggregated_totals.metric_aggregates` and articulate the row-symmetric model across contract / diff / delivery. **Granularity rule.** One row per `(metric_id, full-qualifier-set)`, reported at the finest available granularity. Buyers re-aggregate up if they want a coarser view. Eliminates rollup ambiguity and prevents accidental double-counting. **Closed today, expected to diverge.** `committed_metrics.qualifier` and `metric_aggregates.qualifier` are both `additionalProperties: false` today with identical content (`viewability_standard` only). The delivery vocabulary is **expected to diverge from contract** in future minors as transparency disclosures buyers don't commit to ship delivery-only (e.g., `tracker_firing` pending #3832). New keys ship explicitly in subsequent minors on either surface. **Unqualified metrics stay top-level; mutual exclusion MUST.** `impressions`, `spend`, `media_buy_count`, etc. remain at the top of `aggregated_totals`. `metric_aggregates` is only used for metrics with non-empty qualifier sets. **For any `metric_id` appearing in `metric_aggregates`, the corresponding top-level scalar in `aggregated_totals` MUST be omitted (not zeroed)** — sellers MUST NOT emit both. Avoids duplicate sources of truth. **Qualifier-set drift across reports.** When a campaign gains a new qualifier mid-flight (e.g., adds `tracker_firing` partitioning in week 2 after only client-side firing in week 1), prior periods' rows remain valid at their original granularity. Buyers SHOULD NOT retroactively repartition. **Per-buy shape stays flat.** Each individual buy is single-qualifier by definition; only the cross-buy aggregate spans qualifiers. Per-buy `totals.viewability` continues to be a flat object with its own `standard` field. **Value typing.** Heterogeneous by `metric_id` (rate vs count vs ratio). Buyer agents MUST inspect `metric_id` before doing arithmetic — same dispatch convention as `committed_metrics`. Documented in the description and in `docs/media-buy/task-reference/get_media_buy_delivery.mdx`. **Backwards compatibility.** Additive. The field is optional in v1 (`additionalProperties: true` on `aggregated_totals` already permitted ad-hoc partition fields like the original Vox `viewability` insertion); existing clients are unchanged. Doc updates: `docs/media-buy/task-reference/get_media_buy_delivery.mdx` adds an "Aggregated metric partitions" section documenting the reconciliation join, granularity rule, qualifier-vocabulary asymmetry, per-buy / aggregate divergence, and value-typing dispatch. Closes #3848. - 53e7920: Reconcile the metric vocabulary across the protocol. Closes #3858 (deprecate `metric-type` enum on `performance-feedback`); substantially addresses #3863 (four-parallel-enums cleanup) — full sub-enum restructuring deferred to a follow-up minor. **Problem.** Four parallel metric enums grew independently with overlapping but inconsistent vocabularies: - `available-metric.json` (30 values) — closed delivery enum used by `committed_metrics`, `required_metrics`, `reporting_capabilities.available_metrics` - `forecastable-metric.json` (15 values) — forecast-time enum, mostly mirrors `available-metric` plus deltas (`audience_size`, `measured_impressions`, `grps`, `reach`, `frequency`) - `performance-standard-metric.json` (5 values) — verification subset (`viewability`, `ivt`, `completion_rate`, `brand_safety`, `attention_score`) - `metric-type.json` (8 values) — legacy `performance-feedback` enum mixing metrics, verification, and attribution into one list (`overall_performance`, `conversion_rate`, `brand_lift`, `click_through_rate`, `completion_rate`, `viewability`, `brand_safety`, `cost_efficiency`) **Changes.** ### `performance-feedback.json` (#3858) - Adds `metric: { scope, metric_id, qualifier? }` field — the discriminated row shape symmetric with `committed_metrics` and `metric_aggregates`. Preferred over the legacy `metric_type` field for new implementations. - Marks `metric_type` as **deprecated** in description and **drops it from `required`** at the schema level — the previous "still required while deprecated" pattern was internally inconsistent. Existing implementations populating `metric_type` continue to work; new implementations populate `metric` instead. Removed at the next major when `metric` becomes the canonical dispatch path. - When both `metric` and `metric_type` are present, consumers MUST use `metric` for dispatch. - **`metric` is also optional** — for holistic feedback (a trader flagging a campaign as underperforming without a specific metric), senders can omit `metric` entirely; `performance_index` plus the response narrative carry the signal. This preserves the workflow that legacy `metric_type: "overall_performance"` and `cost_efficiency` served. - Standard-scope `metric` entries support `qualifier.viewability_standard` (MRC vs GroupM) and `qualifier.completion_source` (seller vs vendor attested). Vendor-scope entries carry the BrandRef pattern. - For `brand_safety` migration: buyers who don't know the vendor's specific `metric_id` MAY populate the top-level `vendor` field and OMIT `metric` — the row stays attributable via `feedback_source` + `vendor` without forcing buyers to learn vendor-specific metric vocabularies. ### `metric-type.json` (#3858) - Marked deprecated in title and description. - Description carries a migration table mapping each legacy value to its replacement on the new `metric` field. Meta-bucket values (`overall_performance`, `cost_efficiency`) migrate to **omitting `metric` entirely** — the previously-meaningless meta-buckets are now expressible as "no specific metric" rather than "a meta-string with no defined dispatch semantics." `conversion_rate` has no clean direct target (the protocol distinguishes ratio from count); migration suggests either feeding back `conversions` or a vendor-scope MMM/MTA conversion-rate variant. `brand_safety` migration accommodates buyers who don't know vendor-specific metric IDs (top-level `vendor` field carries source identity even when `metric` is omitted). ### `forecastable-metric.json` (#3863, partial) - Description clarifies which values mirror `available-metric.json` (the canonical delivery vocabulary) and which are forecast-only deltas. Forecast-only values graduate into `available-metric.json` if and when the industry converges on adding them to delivery reporting. - No schema shape change in this minor; the cross-reference is documented in prose. ### `performance-standard-metric.json` (#3863, partial) - Description clarifies the verification-subset role and the relationship to `available-metric.json` (shared values mirror; verification-only values like `ivt`, `brand_safety`, `attention_score` flow through `vendor_metric_values` or vendor-scope `committed_metrics` entries). - No schema shape change. ### `provide_performance_feedback.mdx` - Request parameters table updated with the new `metric` field row and the `metric_type` deprecation marker. - Disambiguates the top-level `vendor` field (source of the feedback) from the nested `metric.vendor` field (vendor that defines the metric). Often the same; can differ. **Migration.** Implementations using `performance-feedback.metric_type` continue to work unchanged for one minor. New implementations SHOULD populate both fields during the transition window: `metric_type` for backwards-compat with consumers reading the legacy field, `metric` as the preferred dispatch surface. At the next major (4.0), `metric_type` is removed and `metric` becomes required. **Backwards compatibility.** Additive (new field on performance-feedback). Existing consumers that ignore the new field continue to work. Deprecated `metric_type` is still required at the schema level for one minor. **What's deferred** (#3863 follow-up). Forecast-only sub-enum extraction (split `forecastable-metric` into `delivery-metrics-shared` + `forecast-only`) and `performance-standard-metric` cross-reference enforcement at the schema level. Both are mechanical follow-ups; the prose description updates ship the conceptual reconciliation now and unblock the deprecation path on `metric-type`. Closes #3858. Substantially addresses #3863. - 4f08ba1: Add five missing scalar metrics that production reporting carries today but had no enum entry: `cost_per_completed_view`, `cpm`, `downloads`, `units_sold`, `new_to_brand_units`. Closes the missing-scalars sub-item of #3460. **The scalars and where they fit.** - `cost_per_completed_view` — CTV CPCV pricing scalar. Parallels existing `cost_per_click` and `cost_per_acquisition`; the package's `pricing_model` is `cpcv` when this field is the billing basis. - `cpm` — Cost per thousand impressions. Universal pricing scalar across CTV, display, mobile/web video, native, audio, and DOOH inventory. Conspicuous absence next to `cost_per_click` before this PR; the package's `pricing_model` is `cpm` when this field is the billing basis. Field name aligns with the canonical `cpm` token in `pricing-model.json` and `pricing-options/cpm-option.json` so buyers cross-walk pricing model → reported scalar without a translation. - `downloads` — IAB-standard scalar for audio/podcast inventory (IAB Podcast Measurement Technical Guidelines 2.x methodology). Distinct from `views`. - `units_sold` — Retail-media commerce scalar. Distinct from `conversions` (a single transaction may carry multiple units). Attribution windows are platform-specific; sellers SHOULD declare the window via `reporting_capabilities.measurement_windows` or `measurement_terms` rather than encoding it in this scalar. - `new_to_brand_units` — Retail-media count of units sold to first-time brand buyers. Unit-volume parallel to existing `new_to_brand_rate` (which carries the fraction-of-conversions metric); this is the absolute unit count. **Wired in.** - `enums/available-metric.json`: five new enum values appended. - `core/delivery-metrics.json`: five new properties (`type: number, minimum: 0`) added next to `cost_per_click`. Existing `new_to_brand_rate` description tightened to clarify it is the fraction of `conversions` (transactions), distinguishing it from the new units count. - `docs/media-buy/media-buys/optimization-reporting.mdx`: metric list updated. **Sub-items already resolved on #3460.** - **Closed-vs-open enum** — resolved by #3492 (vendor-metric extensions). Closed enum stays closed; vendor-defined metrics live in the parallel structured `vendor_metrics` surface anchored on the vendor's brand.json. - **`completion_rate` derived ratio** — resolved by the drop-carve-out call in #3472's refactor. `missing_metrics` is the symmetric mirror of `available_metrics` with no carve-outs. **Sub-item that remains as a follow-up.** - **DBCFM cross-check with David Porzelt** on whether `engagements`/`follows`/`saves`/`profile_visits` (added in #3453) collide with DBCFM `Reporting`/`Performance` KPI codes. Human contact; not a code change. **Backwards compatibility.** All additions are optional. Existing reports without these scalars stay conformant; sellers that adopt them populate the new fields when applicable. Closes #3460. - 6776ce4: Unify outcome measurement into the same primitives as the rest of the measurement surface — outcome metrics live in `available-metric.json`, attribution methodology and window live in the qualifier slot, and `outcome_measurement` as a dedicated field is deprecated. Closes #3857. **The conceptual collapse.** Before this minor, the protocol had two surfaces describing overlapping subject matter: - `delivery-metrics.json` carried outcome scalars (`conversions`, `conversion_value`, `roas`, `cost_per_acquisition`, `units_sold`, etc.) as part of seller-reported delivery — already the audit-flagged "attribution-derived but seller-reported" hybrid. - `core/outcome-measurement.json` (a separate field on `product`) carried business outcome capabilities (`incremental_sales_lift`, `brand_lift`, `foot_traffic`) as free-form strings with implicit vendor identity. These were always the same conceptual category — seller-as-measurement-vendor outcome metrics — split across two surfaces because the protocol predated the unified row-shape vocabulary established by #3576 / #3848. With the qualifier slot proven generalizable (#3877's `completion_source` joining `viewability_standard`), the two surfaces collapse cleanly. **Schemas added.** - `enums/attribution-methodology.json`: closed enum `["deterministic_purchase", "probabilistic", "panel_based", "modeled"]` covering the methodology axis. `deterministic_purchase` is the retail-media closed-loop default (Walmart Connect / Kroger Precision / Amazon DSP); `modeled` covers MMM and clean-room outputs; `panel_based` covers Nielsen / comScore / Edison; `probabilistic` covers statistical match without a 1:1 identifier. - `enums/lift-dimension.json`: closed enum `["awareness", "consideration", "favorability", "purchase_intent", "ad_recall"]` for brand-lift dimension disambiguation. Brand lift is multidimensional in production — Kantar, Upwave, Cint, DV all report each dimension separately with its own sample size and confidence interval; the qualifier ensures rows aren't combined into a single number. **Schemas updated.** - `enums/available-metric.json`: adds `incremental_sales_lift`, `brand_lift`, `foot_traffic`, `conversion_lift`, `brand_search_lift` to the closed delivery vocabulary. Existing outcome scalars (`conversions`, `conversion_value`, `roas`, etc.) cover the rest. **Note: no separate `attributed_sales` entry** — that's `conversion_value` with `qualifier.attribution_methodology: "deterministic_purchase"`. The unified pattern handles the deterministic/probabilistic/modeled split via qualifier rather than parallel metric IDs. - `core/delivery-metrics.json`: adds scalar properties for the five new outcome metrics, with descriptions clarifying which methodologies typically apply. - **Qualifier slot expanded with three new keys** at all five sites (`core/package.json` `committed_metrics`, `media-buy/package-request.json` buyer-side `committed_metrics`, `media-buy/get-media-buy-delivery-response.json` `metric_aggregates` and `missing_metrics`, `core/performance-feedback.json` `metric`): - `attribution_methodology` — closed string enum (`$ref attribution-methodology.json`) - `attribution_window` — structured duration (`$ref duration.json`). **First object-valued qualifier key** — the slot was previously string-enum-only; this PR establishes that qualifier values can be structured. Schema description explicitly calls out object-valued shape and forbids shorthand strings (`"14d"`); consumers MUST dispatch on key name to know value shape, and structured-value qualifiers join on canonical (key-sorted) deep equality. Window isn't disambiguating "which version of the metric" the way `viewability_standard` does — it's parameterizing — but the join-on-`(metric_id, qualifier)` pattern handles the same-metric-different-window case correctly so the placement works. - `lift_dimension` — closed string enum (`$ref lift-dimension.json`). Disambiguates `brand_lift` rows by surveyed dimension. Production reality (Kantar, Upwave, Cint, DV) reports awareness/consideration/favorability/purchase_intent/ad_recall as separate measurements; a single scalar would force vendors to either pick one or composite. Same qualifier-pattern solution as the other multi-flavored metrics. - `core/outcome-measurement.json`: title and description marked **deprecated**. Description carries a migration table mapping legacy field semantics to the unified pattern. Schema retained as-is for one-minor backwards compatibility. - `core/product.json` `outcome_measurement` field description marked deprecated, points at the new pattern. **Doc updates.** - `docs/media-buy/commerce-media.mdx`: "How products declare it" section rewritten to show the new pattern (`reporting_capabilities.available_metrics` + qualifier on commit) alongside the legacy `outcome_measurement` field for the transition window. Existing example payloads continue to use the legacy field — they validate during the deprecation window. - `docs/media-buy/product-discovery/media-products.mdx`: `outcome_measurement` field description updated with deprecation note. - `docs/media-buy/task-reference/create_media_buy.mdx`: qualifier section adds `attribution_methodology` and `attribution_window` with their conditional-required semantics. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: qualifier vocabulary section names all four keys. **Migration.** Retail-media sellers using `outcome_measurement` continue to work for one minor. New implementations declare outcome capabilities via `reporting_capabilities.available_metrics` (the same surface used for impressions, conversions, ROAS today) and pin attribution methodology + window via `qualifier` on `committed_metrics` / `metric_aggregates`. Seller-as-measurement-vendor remains the dominant retail-media topology — vendor identity is implicit (the seller) when no separate `performance_standards.vendor` BrandRef is set. **What's deferred.** `reporting_frequency` and `reporting_format` (the `outcome_measurement.reporting` field's dimensions) move to a follow-up extension on `reporting_capabilities` — they're a property of the seller's reporting infrastructure (daily API, weekly dashboard) rather than a per-metric concern, so they don't belong entangled with the metric definition. Existing `outcome_measurement.reporting` payloads continue to work for one minor. **Backwards compatibility.** Additive (new metrics, new qualifier keys, new enum). Deprecated `outcome_measurement` field continues to validate. Removed at the next major when the unified pattern is canonical. Closes #3857. - 72c9be4: feat(media-buy): clarify package correlation across mixed seller versions. Sellers now have explicit normative guidance to echo `product_id` on package responses created from explicit `create_media_buy` package requests. Buyers targeting mixed seller populations should use package-level `context`, commonly `context.buyer_ref`, as the legacy-safe fallback for sellers that do not echo `product_id`; read surfaces now document persisted media-buy and package context so that fallback is recoverable, and deprecated top-level `buyer_ref` is removed from not-found recovery guidance. - aa58c94: Add `capability_ids[]` to `PackageRequest` (the `packages[]` item shape on `create_media_buy`) as a V2 path equivalent to `format_ids[]`. Lets buyers reading the V2 mental model (`Product.format_options[]`) author a `create_media_buy` call without translating back through `v1_format_ref[]`. Symmetric with the V2 path that `creative-manifest` already exposes (manifest carries a single `capability_id`; package-side carries an array since one package may activate multiple `format_options` entries). Additive optional field. When both `capability_ids` and `format_ids` are sent, `capability_ids` wins and the seller routes by it; the resolving seller ignores `format_ids` (V2-native buyer SDKs SHOULD still emit it as a v1-compat hint for v1-only sellers further down the wire). When neither is sent, the package defaults to all formats supported by the product (unchanged from v1 behavior). Sellers MUST reject with `UNSUPPORTED_FEATURE` when an entry doesn't match a `format_options[]` entry, when the product is v1-only (no `format_options[]` at all), or when the product's `format_options[]` entries don't publish `capability_id` values. Closes #4842. - add4715: Add schema-level `not` constraints to `package-update.json` that explicitly forbid the fully-immutable fields (`product_id`, `format_ids`, `pricing_option_id`) from appearing in update payloads. Mirrors existing MUST NOT prose with machine-checkable validation so permissive sellers can no longer silently override frozen values. `committed_metrics` is intentionally NOT in the not-list. Per the unified metric-accountability design (#3576), `committed_metrics` is **append-only** on update — sellers accept new entries (mid-flight metric additions) but MUST reject modify/remove of existing entries via runtime validation (`validation_error` with code `IMMUTABLE_FIELD`). The "you can append but not modify" semantics are not expressible in JSON Schema's `not` clause, so this is enforced at the seller's runtime layer rather than the schema layer. The append-only contract is documented on `committed_metrics` itself. Closes #3520. - a8ba75c: Add `sponsored_placement_types` (retail media) and `social_placement_surfaces` (social) declarations to products and placements, plus matching `get_products.filters` discovery filters, mirroring the `video_placement_types` pattern. Both are seller-declared discovery metadata, not buyer gates. Retail values: `sponsored_search`, `sponsored_display`, `sponsored_native` (`sponsored_offsite` excluded — not catalog-keyed). Social values: `feed`, `stories`, `short_video`, `explore`, `search` (semantic surfaces, not platform brand names). - 3f7c461: Add `plays` scalar to `delivery-metrics.json` and `available-metric.json` — closes a forecast↔delivery asymmetry where `plays` was declared as a forecastable metric (`forecastable-metric.json:23`, `forecast-point.json:38`) but absent from delivery reporting. Closes #3516. **The shape.** Top-level `type: number, minimum: 0`. Description cross-references the forecast-side definition and explicitly distinguishes from `dooh_metrics.loop_plays` (per-screen rotation count) and `impressions` (multiplied audience figure). Used for DOOH and broadcast inventory where buyers reconcile against forecast `plays`. Why top-level (Option A) over nesting in `dooh_metrics` (Option B): - Forecast side declares `plays` at the same level as `impressions` / `views` (top-level on `forecast-point`); reconciliation pairs cleanly when the delivery-side field mirrors that placement - Used for broadcast inventory too (not DOOH-only), so confining to `dooh_metrics` would force a separate field for non-DOOH plays - Matches the type convention of other top-level count scalars (`type: number`, not the `integer` used inside `dooh_metrics`) **Test plan** — `build:schemas`, `test:schemas`, `test:examples`, `typecheck` all green. Closes #3516. - 72b79ac: Replace country-fused postal targeting as the preferred shape with country-local postal systems: - `postal-system` now adds country-local system names such as `zip`, `zip_plus_four`, `outward`, `plz`, and the fallback `postal_code`; the published enum retains existing country-fused values for 3.x compatibility. - New postal area objects use `{ country, system, values }`. - Country/system pairs are validated so known countries only accept their registered local systems; unknown countries use `postal_code` or `custom`. - `get_adcp_capabilities.media_buy.execution.targeting.geo_postal_areas` now prefers an ISO 3166-1 alpha-2 country-keyed map such as `{ "US": ["zip"], "ZA": ["postal_code"] }`. - During the 3.x migration, sellers SHOULD emit equivalent deprecated aliases such as `us_zip` alongside native country keys where an alias exists. Buyers and SDKs SHOULD normalize both forms. - Deprecated country-fused aliases remain accepted through legacy branches for SDK backfill and existing integrations. - Delivery geo rows now require native postal rows to include `country`. Refs #5383. - 8da6974: Clarify proposal lifecycle semantics and mark measurement catalog discovery experimental for 3.1. Proposal updates: - `proposal_status` is the per-proposal source of truth for whether finalization is required before `create_media_buy`. - `finalize` is seller commitment to firm pricing/terms/hold, not buyer acceptance. - `create_media_buy(proposal_id)` is buyer acceptance/execution of a committed proposal. - `supports_proposals` is a conformance grading declaration, not buyer routing logic for an individual returned proposal. - `allowed_actions[]` / `available_actions[]` remain scoped to media-buy mutations; proposal lifecycle is not modeled as a proposal-level action list. - `requires_proposal` is removed from media-buy action modes before 3.1 GA, replacing the rc-shipped enum with `REQUOTE_REQUIRED` recovery when an update exceeds the current quoted envelope. 3.1 does not define an amendment-quote artifact for `update_media_buy`. Measurement updates: - `measurement` capability block is marked `x-status: experimental`. - Agents implementing the measurement catalog declare `measurement.core` in `experimental_features`. - Docs describe measurement vendor catalog discovery as experimental while the task surface and compliance baseline remain unfrozen. - 75793d5: feat(provenance): embedded_provenance, watermarks, accepted_verifiers, and structured rejection codes Two new optional arrays on `provenance.json` distinguish between provenance metadata carried within the content stream (`embedded_provenance`) and content watermarks that encode an identifier or fingerprint (`watermarks`). The separation aligns with C2PA's normative taxonomy: embedded provenance maps to binding assertions and manifest embedding (Section A.7), while watermarks map to the `c2pa.watermarked.*` action family. The verifier contract follows seller-publishes / buyer-represents / seller-confirms: - **Seller publishes** `creative_policy.accepted_verifiers[]` — the governance agents it operates or has allowlisted, each with `agent_url`, optional `feature_id`, and optional `providers[]`. Returned on `get_products`. - **Buyer represents** on each `embedded_provenance[]` and `watermarks[]` entry by attaching `verify_agent: { agent_url, feature_id? }` whose `agent_url` matches a published `accepted_verifiers[]` entry (canonicalized). - **Seller confirms** by cross-checking the URL against its allowlist before any outbound call, then invoking `get_creative_features` against the matching on-list agent. Sellers MUST NOT call buyer-asserted endpoints outside their allowlist. This closes the SSRF / exfil / phishing surface a buyer-controlled URL would otherwise create, and matches how publishers actually pick verifiers (they run their own pipeline; buyer-attached evidence is supplementary, not authoritative). A new `provenance_requirements` object on `creative-policy.json` gives sellers structured, field-level provenance requirements: `require_digital_source_type`, `require_disclosure_metadata`, `require_embedded_provenance`. Sellers that publish a requirement MUST enforce it on `sync_creatives` with the matching error code from the new `PROVENANCE_*` family on `error-code.json`: - `PROVENANCE_REQUIRED` — no provenance object on the creative - `PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING` — required `digital_source_type` absent - `PROVENANCE_DISCLOSURE_MISSING` — required `disclosure` block absent - `PROVENANCE_EMBEDDED_MISSING` — required `embedded_provenance` entry absent - `PROVENANCE_VERIFIER_NOT_ACCEPTED` — `verify_agent.agent_url` is off the seller's `accepted_verifiers` list (cross-checked before any outbound call) - `PROVENANCE_CLAIM_CONTRADICTED` — on-list verifier (called via `get_creative_features`) refutes the buyer's claim These codes are correctable: a buyer's orchestrator reads them, fixes the creative, and resubmits without negotiating with the seller. `PROVENANCE_CLAIM_CONTRADICTED.error.details` is constrained to the audit-safe allowlist `{ agent_url, feature_id, claimed_value, observed_value, confidence, substituted_for }` so verifier responses cannot leak cross-tenant or PII data. The `c2pa` field description on `provenance.json` is updated to note that sidecar manifest bindings break during ad-server transcoding, with a reference to `embedded_provenance` as the alternative for intermediary pipelines. New enum files: `embedded-provenance-method.json`, `watermark-media-type.json`, `c2pa-watermark-action.json`. New compliance scenario: `protocols/media-buy/scenarios/provenance_enforcement.yaml` walks the structural-rejection contract end to end (discover requirement → reject off-list verifier → reject missing disclosure → accept corrected resubmission). All wire additions are optional and additive; existing agents that do not read the new fields are unaffected. Closes #2854 (Option A: must-carry baseline expansion + Track 1: embedded provenance field shape). - 8f03600: Add published-post reference creatives as a canonical-format refinement, not a new task surface or format family. - Adds `published_post` as an asset payload type, canonical slot asset type, and `asset_types` filter value for `list_creative_formats`. - Adds `publisher_owned_reference` to canonical `asset_source` where a product resolves an existing post instead of accepting uploaded bytes. - Adds `required_connections` for downstream platform grants, plus `AUTHORIZATION_REQUIRED` details for missing advertiser account, publisher identity, or post-scoped authorizations. - Adds `CreativeStatus: "suspended"` plus authorization/source reason codes so recoverable published-post dependency loss is distinct from policy rejection, with documented escalation from `suspended` to `rejected` when the dependency becomes terminal. - Adds `AUTHORIZATION_REQUIRED` for authenticated calls that need additional creator, identity, or post authorization before serving. - Documents the canonical `video_hosted` published-post pattern and keeps catalog-driven retail media on `sponsored_placement`. - 1f158e8: Fix release validation for compliance bundle closure and align signals conformance with the owned-signal manifest fix tracked in #5186. - Package webhook receiver envelope vectors under the versioned compliance tree and update storyboard references to bundle-relative paths. - Fail compliance and protocol tarball builds when authored vector/test-kit references do not resolve inside the packaged compliance tree. - Narrow baseline and `signal_owned` conformance back to discovery-only so SDK manifests do not require owned-signal agents to implement marketplace activation. - Require `activate_signal` on the `signal_marketplace` specialism and update the Signals Protocol docs to state the two-tier obligation explicitly. - 6ff3f9d: Reconcile `available-metric` enum with `delivery-metrics.json` so every declarable metric has a corresponding property in the delivery payload. **Why.** A buyer that says "I can only use products that report `completed_views`" only has accountability if the enum used at the discovery layer is a 1:1 mirror of what reporting can actually return. The enum had drifted from the property set: - `video_completions` was listed in the enum but had no corresponding property in `delivery-metrics.json` — the property was renamed to `completed_views` in a prior release (per `docs/reference/release-notes.mdx` §7) and the enum alias was never cleaned up. A seller declaring it in `available_metrics` was advertising a metric they could not report. - Four scalar properties on `delivery-metrics.json` (`engagements`, `follows`, `saves`, `profile_visits`) had no enum entries, so a product that reports social/social-platform engagements had no way to declare so at discovery. **Changes.** - `enums/available-metric.json`: remove `video_completions`; add `engagements`, `follows`, `saves`, `profile_visits`. Object/namespace entries (`viewability`, `quartile_data`, `dooh_metrics`) remain — they map to namespace properties in `delivery-metrics.json`. - `core/reporting-capabilities.json`: example updated to use `completed_views`. - `docs/media-buy/media-buys/optimization-reporting.mdx`: metric list rewritten to match the reconciled enum (drops the stale `video_completions` entry, adds `engagements` / `follows` / `saves` / `profile_visits` / `new_to_brand_rate`). Notes platform variance for `saves` (Pinterest "repins", TikTok "video_saves"). - `docs/media-buy/task-reference/create_media_buy.mdx`: `requested_metrics` examples updated to `completed_views`. - `server/src/training-agent/publishers.ts`: training-agent fixture `reportingMetrics` arrays use `completed_views`. **Vocabulary provenance.** `completed_views` and `engagements` follow IAB/MRC and VAST 4 conventions. `follows`, `saves`, and `profile_visits` are platform-native names (Meta/TikTok/Pinterest); AdCP is setting these as the canonical aliases for cross-platform reporting since IAB does not define social-platform engagement scalars. **Backwards compatibility.** Removing `video_completions` from the enum is a validation-constraint change — minor-bumped per the schema-publication-at-merge policy. Any seller that had populated `available_metrics: ["video_completions"]` was already non-functional (no `video_completions` field in delivery responses to populate, only `completed_views`). Buyers that filtered against `video_completions` on the discovery side should switch to `completed_views`. This unblocks a follow-up that adds `required_metrics` to `get_products` and `missing_metrics` to `get_media_buy_delivery` for end-to-end metric accountability through the media buy lifecycle. **DBCFM KPI cross-reference.** The DBCFM `Reporting`/`Performance` KPI vocabulary has not been mapped into AdCP (PRs #1594, #1605, #1664 covered price/business-entities/proposal-lifecycle; measurement block is out of scope). No string-level or semantic collision exists at merge time. When the DBCFM measurement mapping is eventually added, note that `engagements` corresponds to DBCFM `Interaktionen`, `follows` to `Follower-Gewinn`, `saves` to `Gespeichert`, and `profile_visits` to `Profilbesuche`. No aliasing is required — the AdCP names are unambiguous — but a cross-reference note will be needed in the DBCFM mapping doc (tracked in #3460). **`completion_rate` is a derived ratio.** `completion_rate = completed_views / impressions` — it is derivable, not independently reportable. The planned `missing_metrics` check in `get_media_buy_delivery` must treat ratio metrics as derivable to avoid false `metric_accountability_breach` hints. This is a design signal for the `required_metrics`/`missing_metrics` follow-up; it does not affect this PR. - 16147ac: Add `redirect_reason` and `redirect_effective_at` to both redirect variants in `brand.json` (Authoritative Location Redirect and House Redirect). Today, when a brand.json transitions from a portfolio document to a redirect (e.g., during M&A — Dentsu becomes a House Redirect to WPP), DSPs / crawlers / prebid configs sit on stale cached state for whatever their TTL is. Free-text `note` is human-readable but not machine-parseable. `redirect_reason` is an enum (`acquisition`, `divestiture`, `rebrand`, `regional`, `legacy`, `consolidation`, `other`) that consumers SHOULD use to inform cache TTL: in-transition reasons (`acquisition`, `divestiture`, `rebrand`, `consolidation`) suggest the resolved target is moving and consumers SHOULD shorten cache TTL until stable; stable reasons (`regional`, `legacy`) keep standard caching. `redirect_effective_at` is an ISO 8601 timestamp. Caches **MUST** treat any entry cached before this timestamp as stale and re-fetch through the redirect — this is the hard invariant that closes the cache-poisoning gap during transitions, regardless of TTL. Both fields are optional and additive. Existing redirect publishers continue to work unchanged. Motivated by review of the distributed brand.json RFC ([#3533](https://github.com/adcontextprotocol/adcp/pull/3533)) — the M&A migration story uses existing redirect variants, and this PR makes that ergonomic. - f7f6600: spec(request-signing): add `protocol_methods_*` namespace to `request_signing` capability; widen test-agent strict route to enforce it (closes #4318, #4314) `request_signing.supported_for` / `required_for` carry **AdCP protocol operation names** (`create_media_buy`, `update_media_buy`, …). They have always been silent on **JSON-RPC protocol methods** like `tasks/cancel` and `tasks/get` — methods that traverse the same authenticated channel as `tools/call` (auto-registered by MCP and defined by A2A 0.3.0 §7.x), but are not AdCP operations and MUST NOT be conflated with the AdCP-tool namespace per the existing normative rule at `security.mdx:927`. Buyers signing `tasks/cancel` on abort had no spec-grounded way to know whether the seller's verifier covered it; the only defensible default was to over-sign on best-effort. This change adds three sibling fields to `request_signing` for sellers to declare verifier coverage of protocol methods: ```jsonc { "request_signing": { "supported": true, "supported_for": ["create_media_buy", "update_media_buy"], "required_for": ["create_media_buy"], "protocol_methods_supported_for": ["tasks/cancel", "tasks/get"], "protocol_methods_required_for": ["tasks/cancel"] } } ``` Schema enforces the namespace split via `pattern: "/"` on items — JSON-RPC method strings (containing `/`) MUST appear here; AdCP tool names (no `/`) MUST appear in `supported_for` / `required_for`. `protocol_methods_required_for` is `subset_of` `protocol_methods_supported_for`; `protocol_methods_warn_for` is `disjoint_with` `protocol_methods_required_for` and `subset_of` `protocol_methods_supported_for` (mirrors AdCP-namespace rules). `identity.brand_json_url` is now `required_when` any of the new fields is non-empty. Normative text added to `docs/building/by-layer/L1/security.mdx`: - The `protocol_methods_*` arrays are matched against the JSON-RPC envelope's `method` field, not the `tools/call` `params.name`. - The same RFC 9421 covered components apply to JSON-RPC method calls (`@target-uri`, `@method`, `content-digest` per the seller's `covers_content_digest` policy, `authorization` when present). - Buyers MUST NOT infer protocol-method coverage from `supported_for` / `required_for`. `test-agent.adcontextprotocol.org` strict route (`//mcp-strict`) is widened to enforce the new bucket: `STRICT_REQUIRED_FOR` adds `update_media_buy` and `sync_creatives` (so a buyer that signs the initial create but forgets follow-on mutations gets a 401 instead of a silent green light), and a new `STRICT_PROTOCOL_METHODS_REQUIRED_FOR = ['tasks/cancel']` constant feeds the SDK verifier through a new namespace-aware `mcpOperationResolver`. The wire response from `get_adcp_capabilities` splits the bundle so AdCP tool names emit on `required_for` and JSON-RPC methods emit on `protocol_methods_required_for`. Closes the original `tasks/cancel`-on-abort regression-test ask in adcp-client#1617 Phase 2. The earlier #4314 proposal of an `X-Test-Require-Signing` per-session header is **not** adopted: per the triage, header-driven per-session enforcement contradicts `security.mdx:927` (declaration-enforcement coherence) and the SDK's verifier architecture (singleton capability objects, eagerly-built authenticators). Strict-route enforcement on `/mcp-strict` is the spec-coherent path. No `VerifierCapability` (SDK type) shape change — the SDK's flat `required_for` array remains; namespace separation lives on the wire and in storyboard runners, not in the verifier match step. - dececcd: Add end-to-end metric accountability through the media buy lifecycle: buyers can now require specific reporting metrics at discovery time, and delivery reports surface any gaps in the contract. **Why.** Without this, a buyer asking for `completed_views` on a CTV CPCV buy discovers metric availability through `reporting_capabilities.available_metrics` on each product, then has to manually filter — and at delivery time there is no field that flags when an advertised metric was not produced. The closest existing primitive (`required_performance_standards`) is for guarantee thresholds (e.g., "70% MRC viewability") with vendor selection, not for capability-level metric discovery. **Changes.** - `core/product-filters.json`: new `required_metrics` field on `get_products` filters. Sellers MUST silently exclude products whose `reporting_capabilities.available_metrics` is not a superset (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same vocabulary computes `missing_metrics` on `get_media_buy_delivery`. - `media-buy/get-media-buy-delivery-response.json`: new `missing_metrics` field on each `by_package[]` entry. Lists metrics from the product's `available_metrics` that are NOT populated in this report. Empty array (or absent) indicates clean delivery; non-empty signals an accountability breach. Sellers MUST exclude metrics not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. - `docs/media-buy/task-reference/get_products.mdx`: documents the new filter, filter-not-fail semantics, and the derived-ratio carve-out. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: documents the `missing_metrics` field as the accountability signal. - `static/compliance/source/protocols/media-buy/scenarios/measurement_accountability.yaml`: new conformance storyboard exercising the full lifecycle — discovery with `required_metrics`, create, simulated delivery, and delivery-report shape validation. Storyboard validates schema-level contract; semantic enforcement (verifying the seller honestly populates `missing_metrics`) is left to a follow-up that extends the test controller with metric-omission scenarios. **No additional field on `create_media_buy`.** The product's declared `available_metrics` carries forward as the reporting contract — adding a new field on the buy would duplicate that, and `measurement_terms` / `performance_standards` already cover guarantee-level commitments at the package level. **Backwards compatibility.** Both fields are optional and additive. Existing sellers that do not populate `missing_metrics` are interpreted as "no breach" (field absent = clean delivery), so existing reports remain conformant. Buyers that omit `required_metrics` see the same behavior as today. **Hint kind follow-up.** A dedicated `metric_accountability_breach` storyboard hint kind (with Diagnose/Locate/Fix/Verify formatter) is deferred to a follow-up @adcp/client PR — for now, breach is detectable via standard schema validation on the delivery response and the storyboard runner's `field_present` check on populated metrics. Refs #3460. - e52f78e: Add normative `response_schema_validator_semantics` clause to `runner-output-contract.yaml`. Runners MUST apply the referenced JSON schema with a draft-07 compliant validator that honours the schema's own `additionalProperties` declaration without process-level override. Configuring AJV `removeAdditional: 'all'` or Zod `.strict()` on derived schema objects in a way that contradicts the schema's `additionalProperties: true` declaration is a conformance violation. Addresses issue #4419, where the comply runner produced false-negative verdicts for spec-valid seller responses that included optional or newly-added fields (`authorization`, `sandbox`). - dbc5b56: Name 3.1 schema component shapes used by SDK code generators: account-with-authorization response items, forecast dimension variants, signal selection-group rules, canonical projection slot overrides, committed metrics, delivery metric aggregates, and vendor-metric optimization rows. Document nullable scalar representation and mark intentionally open payload fields with `x-adcp-open-payload`. - f23c966: Add `search_brands` task to the brand protocol. Provides a natural-language brand discovery verb for IP desks that need to find brands on an agent's roster before they have a known `brand_id`. Returns lightweight brand stubs (public identity tier) that feed directly into `get_brand_identity` or `get_rights` without an extra identity-resolution round-trip. New schemas (experimental): `search-brands-request.json`, `search-brands-response.json`. New task type `search_brands` added to stable `task-type.json` enum. Closes #3480. - 1d1c562: Add Sponsored Intelligence sponsored-context accountability primitives. New SI schemas define `context_use` (`presentation_only`, `comparison_set`, `reasoning_context`), `sponsored_context` declarations, and host `sponsored_context_receipt` records. `si_get_offering`, `si_initiate_session`, and `si_send_message` now have optional fields for carrying those declarations and receipts across the host boundary. The model separates `paying_principal` (who economically sponsored the context) from `host_receipt` (what use mode and disclosure commitment the receiving host accepted). Accepted receipts must include the accepted use mode and disclosure commitment; hosts that cannot honor the declaration reject the context rather than down-scoping it. - 1584e44: spec(signals): make deprecated `coverage_percentage` optional on signal responses. `get_signals.signals[]` and wholesale feed signal payloads now keep `coverage_percentage` as an optional deprecated legacy scalar instead of a required deprecated field. `coverage_forecast` is the source of truth for detailed signal coverage planning; the scalar remains a backward-compatible fallback for clients that still consume it. Adds validation coverage for `coverage_rate.low` and `coverage_rate.high` upper bounds, and pins the intended valid signal forecast shape where `presence: "present"` omits `signal_value`. Closes #5089. - e5d2bbc: Extend `core/signal-definition.json` with definition-side signal enrichment for taxonomy metadata, DTS-aligned source/methodology disclosures, modeling metadata, jurisdiction applicability, consent basis, and per-signal data-subject-rights routing. Taxonomy is modeled as signal-definition metadata rather than a new `signal-value-type`, so package targeting continues to use the existing binary, categorical, and numeric expression grammar. Categorical signals can map `allowed_values[]` strings to stable taxonomy nodes with `taxonomy.value_mappings`. Parent taxonomy node expansion is declared as seller behavior through `taxonomy.parent_match_behavior` instead of being implied by the schema. Adds a signal-specific `core/signal-modeling-disclosure.json` instead of reusing creative `provenance.disclosure`, because data-signal modeling disclosure has different semantics from content provenance and render guidance. Modeled signals now require non-empty training-data jurisdictions, and required modeling disclosures must name the jurisdictions where the disclosure applies. - 1b6831e: Add product-scoped `included_signals`, `signal_targeting_options`, `signal_targeting_rules`, and package-level `targeting_overlay.signal_targeting_groups` for explicit buy-time selection of seller-offered signals. Signals are referenced with `signal_ref` using `scope: "product"` for product-local signal options, `scope: "data_provider"` with `data_provider_domain` for signals from published adagents.json `signals[]`, or `scope: "signal_source"` for source-native signals that are not published in adagents.json `signals[]`. `included_signals` describes non-selectable signals already bundled into or planned into the product. Sellers can expose signals through `get_signals`, omit inline options when a wholesale product uses that feed, declare product-specific options or overrides through `Product.signal_targeting_options`, and buyers can apply selected signals on `create_media_buy` with the selected signal `pricing_option_id` and optional seller execution handle without overloading first-party audience fields. `signal_targeting_groups` provides the portable Boolean baseline for all signal selection: top-level `operator: "all"` with child groups using `operator: "any"` for include groups and `operator: "none"` for exclusion groups. Product-scoped signal pricing is authoritative for product composition, and free or bundled signals may omit `pricing_options`. Updates media-buy and signals docs plus targeting-overlay echo vectors. Product signal listings share one signal-ref-plus-definition shape. Product-local signal refs require inline `name` and `value_type`; data-provider and signal-source refs can be reference-only because the authoritative definition lives at the referenced provider-published signal definition or source. `signal_targeting_rules.resolution_model` distinguishes direct targeting from seller-planned resolution. Use `direct_targeting` when selected signals are applied as package targeting predicates, and `seller_planned` when selected signals are inputs to seller-managed planning against product-specific inventory, timing, availability, reach, or pacing constraints. This also relaxes `get_signals.signals[].data_provider` and `pricing_options` so source-native, free, bundled, or caller-hidden pricing cases can omit those fields; buyers should not assume every discovered signal has a data-provider display name or standalone price. The legacy `SignalId` / `signal_id.source` shape is deprecated in favor of `SignalRef`. New payloads should use `signal_ref` for response identity and `signal_refs` for exact lookup/refinement; `signal_id` and `signal_ids` remain as deprecated compatibility fields for older clients. During this minor-version migration window, legacy `signal_id` remains accepted on signal listings, audience selectors, legacy flat signal targeting, and wholesale signal events. Legacy `targeting_overlay.signal_targeting` also remains schema-valid but deprecated; new package-level selection should use `targeting_overlay.signal_targeting_groups`. The legacy `signals.features.catalog_signals` capability flag is also deprecated. New agents should rely on `supported_protocols: ["signals"]`, `signals.data_provider_domains`, `signals.discovery_modes`, and `get_signals` behavior instead of emitting a separate provider-published-signals feature flag. - 6ddfea9: Lift sole-stateful-step cascade exemption into `runner-output-contract.yaml` as normative MUST language. The spec was previously silent on what happens when the sole stateful step in a phase grades `not_applicable`, `missing_tool`, or `missing_test_controller` — causing runner divergence (the TS SDK exempts the cascade; other runners may not). Adds a top-level `cascade_rules` section with `default_cascade` and `sole_stateful_step_exemption` rules. Also bumps the contract's own `version` field from `2.0.0` → `2.1.0`. - 7525019: Add `identity.brand_json_url` to `get_adcp_capabilities` response — capabilities-level pointer to the operator's brand.json so verifiers can bootstrap from an agent URL to that agent's signing keys without out-of-band knowledge of the operator domain. Closes the discovery gap in the request-signing chain (capabilities → `identity.brand_json_url` → brand.json → `agents[]` → `jwks_uri` → JWKS). **What's new in `static/schemas/source/protocol/get-adcp-capabilities-response.json`:** - New `brand_json_url` field inside the existing `identity` block (HTTPS URI). Co-located with `identity.key_origins`, `per_principal_key_isolation`, `compromise_notification` — all the trust-posture fields that depend on it. Naming intentionally distinguishes from `sponsored_intelligence.brand_url`: `brand_url` is reserved for "the brand being advertised" contexts; `brand_json_url` names the file artifact (the operator's brand.json), independent of whether the operator is a single brand, a house, an agency, or a pure operator record. - Schema-optional in 3.x; storyboard-enforced when the agent declares any signing posture (`request_signing.supported_for`/`required_for` non-empty, `webhook_signing.supported === true`, or any `identity.key_origins` subfield). Becomes schema-required in 4.0 for responses declaring `supported_versions` containing any 4.x release. - Structured constraints (required-when rules, verifier constraints, distinct-from relationships) lifted into a new `x-adcp-validation` extension keyword on the field. Codegen consumers get a tight 2-sentence JSDoc; the storyboard runner and SDK validators consume the structured rules programmatically. See `docs/reference/schema-extensions.mdx` for the convention. **What's new in `docs/building/implementation/security.mdx`:** - §"Discovering an agent's signing keys via `brand_json_url`" — 8-step verifier algorithm with eTLD+1 origin binding (pinned PSL snapshot required), `authorized_operators[]` opt-in for SaaS-platform-as-operator deployments, mandatory `identity.key_origins` consistency check (purpose-AND-role, with sell-side webhook publisher-pin carve-out), no-redirect rule on brand.json fetch, body cap and timeout budgets, negative-cache 60s floor. - Eight new `request_signature_*` rejection codes with detail fields and remediation column: `brand_json_url_missing`, `capabilities_unreachable`, `brand_json_unreachable`, `brand_origin_mismatch`, `agent_not_in_brand_json`, `brand_json_ambiguous`, `key_origin_mismatch`, `key_origin_missing`. - Trust-root distinction: brand.json operator-attested; adagents.json publisher-attested; agent never self-attests. - Quickstart subsection mirroring §796 — 6 numbered steps + 15-line pseudocode for implementing a `brand_json_url`-based verifier. - Reference-implementation paragraph naming `@adcp/client` (TypeScript), `adcp` (Python), `adcp-go` (Go) with their `resolveAgent` / `getAgentJwks` / `verify_request_signature` signatures and the `npx @adcp/client resolve ` CLI. **Backwards compatibility:** Strictly additive. Verifiers that ignore `identity.brand_json_url` continue to work. The full design (with reviewer history, multi-tenant operator handling, SDK + CLI integration, naming-convention discussion, and rejected hosted-AAO-resolver alternative) is in `specs/capabilities-brand-url.md`. **Adopting from 3.0 (no version bump required).** The wire shape is forward-compatible — 3.0-conformant agents can populate and read the field today without waiting for the 3.x bump. A 3.0 seller MAY emit `identity.brand_json_url` on its capabilities response and a 3.x verifier picks it up automatically; a 3.0 verifier MAY read it opportunistically and run the 8-step chain when present, falling back to existing out-of-band agent → operator mapping when absent. The chain itself is plain HTTPS fetches and JSON parsing — no 3.x SDK required. AdCP doesn't backport new schema fields to patch releases (3.0.x), but 3.0-pinned implementers building signature verification today (e.g., Scope3) can ship the field now and let the 3.x rollout happen passively. See [security.mdx §Discovering an agent's signing keys](https://adcontextprotocol.org/docs/building/implementation/security#discovering-an-agents-signing-keys-via-brand_json_url) for the verifier algorithm. - 1323f39: spec(specialisms): add `sponsored-intelligence` to `AdCPSpecialism` (preview) Adds `sponsored-intelligence` to the `AdCPSpecialism` enum so SI agents have a wire-level specialism ID to claim, with the same dispatch parity as `signal-marketplace`, `creative-template`, `governance-spend-authority`, and the other agent shapes. SDKs (e.g. `@adcp/sdk` v6) can now key SI dispatch off the specialism ID instead of routing through escape-hatch handler bags. Shipped as `status: preview` while the four SI lifecycle tools (`si_get_offering`, `si_initiate_session`, `si_send_message`, `si_terminate_session`) remain `x-status: experimental`. Per the preview-status contract, claims of this specialism are graded as `{ status: "preview", passed: null, reason: "storyboard not yet defined" }`; conformance for SI agents continues to be exercised by the `sponsored-intelligence` protocol baseline at `/compliance/{version}/protocols/sponsored-intelligence/`. Promotes to `stable` (with `required_tools` and a graded storyboard) when the SI tools graduate. Closes #3961. - 4e96782: Add optional `requires` field to the storyboard schema for whole-storyboard runtime requirement gating. Third-party runners can now declare per-storyboard requirements (`controller`, `seeded_state`, `real_wire`) that the runner evaluates at load time before executing any steps. Storyboards without the field run unchanged. The `requirement_unmet` skip reason is added to runner-output-contract.yaml to match the skip reason already emitted by `@adcp/sdk@^6.16.0` (adcp-client#1635). - b7068f0: Tighten three universal storyboard false-failure paths: webhook-emission now explicitly requires a configured webhook receiver so unresolved runner URL templates must grade not_applicable instead of reaching the agent; security_baseline positive static-credential probes now document initialized-session dispatch rather than raw direct Bearer-only `tools/call`; and schema-validation now requires the concrete INVALID_REQUEST past-start rejection instead of a trailing branch-set contribution assertion. Migration note: agents that currently accept a past concrete `start_time` and adjust it to a current/future flight must instead return `INVALID_REQUEST`; use `start_time: "asap"` when the buyer wants immediate activation. - cf889f2: feat(media-buy): `supports_proposals` capability flag — closes #3844 Adds a wire-level capability flag at `media_buy.supports_proposals` (boolean) so the storyboard runner can gate `proposal_finalize` cleanly, and folds the scenario into `sales-guaranteed.requires_scenarios`. `get-adcp-capabilities-response.json`: - New `media_buy.supports_proposals` boolean. A declaration of `true` is a commitment the seller will be graded against (return at least one entry in `proposals[]` for `buying_mode: 'brief'`; honor `action: 'finalize'` to transition draft → committed), not just a feature flag. Full-service guaranteed sellers (premium pubs, broadcast, CTV) declare `true`; auction-based PG, retail SKU, and quoted-rate direct-buy flows declare `false`. `media-buy/scenarios/proposal_finalize.yaml`: - Adds `requires_capability: { path: media_buy.supports_proposals, equals: true }`. Sellers that explicitly declare `false` skip the scenario as `capability_unsupported`; sellers that declare `true` (or omit the field per the runner's absence semantics) are graded against it. `specialisms/sales-guaranteed/index.yaml`: - Adds `media_buy_seller/proposal_finalize` to `requires_scenarios`. Now safe — capability-gated. Narrative updated to remove the "tracked at #3844" caveat. `specialisms/sales-proposal-mode/index.yaml` and `enums/specialism.json`: - Deprecation note for `sales-proposal-mode` updated to point sellers at the migration path: drop the specialism, declare `sales-guaranteed` plus `media_buy.supports_proposals: true`. Storyboard retained through 3.x for backward compat; removed at 4.0. Refs: #3823 (taxonomy consolidation), #3840 (sales-proposal-mode deprecation), #3844 (this). - 81ad6f5: Require creatives accepted by a synchronous `sync_creatives` success response to be immediately visible through `list_creatives` for the same account and authorized caller, while preserving the submitted task envelope for whole-operation async ingestion. - 48e140f: feat(training-agent): impairment tracking on media buys — creative-status transitions propagate to media_buy.impairments[]. Closes #4719. Two storyboards added in #4677/#4685 (`media_buy_seller/dependency_impairment` and `dependency_impairment_cardinality`) needed full impairment-tracking machinery: when a creative referenced by a media buy's package transitions to `rejected`, the buy MUST surface `health: "impaired"` and an `impairments[]` entry; when the buyer recovers via assignment swap, the impairment MUST clear. **Model.** Adds `impairments?: Impairment[]` to `MediaBuyState` (`server/src/training-agent/types.ts`). Impairment shape mirrors `static/schemas/source/core/impairment.json` — `impairment_id`, `resource_type`, `resource_id`, `package_ids`, `transition`, `reason_code`, `observed_at`. **Propagation.** `comply-test-controller.ts:forceCreativeStatus` now calls `propagateCreativeImpairment` after mutating creative status. Walks `session.mediaBuys`, finds buys whose packages reference the creative, and appends/removes an impairment entry per direction (`approved → rejected` appends; `rejected → approved` removes). Idempotent on re-emission. **Recovery.** `handleUpdateMediaBuy`'s `creative_assignments` replacement path recomputes the buy's open impairments: any creative-impairment whose `resource_id` is no longer referenced by any package on the buy is dropped. This is the canonical recovery vector — the buyer swaps the offline creative for an approved sibling. **Response surface.** `handleGetMediaBuys` now emits `health` (`'impaired'` when `impairments.length > 0`, else `'ok'`) and `impairments[]` per the spec. **Comply config.** `force_creative_status` adapter wired into the `/sales` tenant's `buildSalesComplyConfig` (was missing — the storyboards reported `force_scenario_unsupported`). **Storyboard scenario adjustments.** The v6 SDK's `SalesPlatform.syncCreatives(creatives, ctx)` signature drops the request-level `assignments[]` field — the platform method has no surface for inline assignments. Both dependency_impairment scenarios are restructured to do the binding via `update_media_buy.packages[].creative_assignments` after `sync_creatives`, which is the spec's canonical surface for the binding anyway. Filed upstream at `adcontextprotocol/adcp-client#1842` to thread assignments to the platform. `dependency_impairment_cardinality` also needed an explicit `bid_price` on its `create_media_buy` request — the product returned for its slightly-different brief picks an auction-pricing option as `pricing_options[0]`, and the seller correctly requires `bid_price` for auction. The parent `dependency_impairment` scenario happens to land on a fixed-price option and didn't need it. Sales floor lifts from 72:340 to 74:380 (+1-clean buffer below observed 75:398). Files: - `server/src/training-agent/types.ts` — `MediaBuyState.impairments`, `Impairment` interface. - `server/src/training-agent/comply-test-controller.ts` — `propagateCreativeImpairment`, called from `forceCreativeStatus`. - `server/src/training-agent/task-handlers.ts` — `health`/`impairments` in `handleGetMediaBuys`; assignment-swap impairment clearing in `handleUpdateMediaBuy`. - `server/src/training-agent/tenants/comply.ts` — `force_creative_status` adapter. - `static/compliance/source/protocols/media-buy/scenarios/dependency_impairment.yaml` — split `sync_creative_with_assignment` into `sync_creative` + `assign_creative_to_package`. - `static/compliance/source/protocols/media-buy/scenarios/dependency_impairment_cardinality.yaml` — same split + `bid_price: 10.0` on packages. - `.github/workflows/training-agent-storyboards.yml`, `scripts/run-storyboards-matrix.sh` — floor bump. - 868a051: feat(schema): add `result` and `include_result` to `tasks/get` request/response (closes #3123) `tasks/get` had no typed field for the completion payload — buyers polling an async `create_media_buy` (or any submitted-arm task) could see `status: completed` but had no schema-backed path to retrieve `media_buy_id` and `packages`. The push-notification webhook schema already defined this pattern correctly (`result: $ref async-response-data.json`); the polling API simply never got the same field. **Schema changes (both additive, non-breaking):** - `static/schemas/source/core/tasks-get-response.json` — adds optional `result: $ref /schemas/core/async-response-data.json`. Present when `status` is `completed` and `include_result: true` was requested; absent otherwise. For `failed`/`canceled` tasks, sellers continue to use the existing `error` field — `result` is for the success terminal only. Mirrors the `result` field in `mcp-webhook-payload.json` so push and pull paths return the same payload shape. - `static/schemas/source/core/tasks-get-request.json` — adds optional `include_result: boolean` (default `false`). Signals that the caller wants the completion payload on the response. **Docs:** - `docs/protocol/calling-an-agent.mdx` — adds a completed `tasks/get` example showing the `result` field, closing the documentation gap identified in the issue. - `docs/building/implementation/task-lifecycle.mdx`, `async-operations.mdx`, `error-handling.mdx`, `orchestrator-design.mdx` — re-introduces `include_result: true` in the polling examples that patch #3127 stripped (now spec-backed by this PR's schema additions). Non-breaking: `result` is optional on both request and response. Sellers omitting it on non-completed tasks or on requests without `include_result: true` remain spec-conformant. Existing `adcp-client` consumers relying on informal `additionalProperties` passthrough continue to work; the typed field gives SDKs a stable, named field to key on. Unblocks adcp-client#967 (polling-cycle hardening). - f45191b: spec: allow multi-tenant seller-agent operators to publish more than 20 `brand.json` `agents[]` entries and clarify per-tenant JWKS resolution. `brand.json` no longer caps `agents[]` at 20 entries, allowing one same-type sales-agent entry per tenant or property-scoped endpoint. The seller setup guidance now documents the A1 static-shard pattern: verifiers resolve keys from the authenticated agent URL to exactly one `agents[].url` entry, use that entry's `jwks_uri` or the default origin JWKS, and reject duplicate matching entries as ambiguous rather than selecting by agent `type` or request-payload tenant fields. - 563eaf4: spec(tmp): add required `seller_agent_url` to `context_match_request`. The context-match request now carries `seller_agent_url`, matching the identity-match request's field shape and placement (PR #3687). The resolution semantics are deliberately actor-specific, not a mirror: on the context path the **provider** resolves the active package set it has **synced** for the asking seller, whereas on the identity path the **buyer agent** resolves the set it has **registered**. When `package_ids` is omitted, evaluation runs against that seller's full active set; a `seller_agent_url` the provider has not synced packages for MUST return an empty offer set rather than fall back to another seller's set. This reverses the prior decision (PR #3063's seller-attribution section) that kept seller identity off `context_match_request`. That section argued the provider already holds the sync-time `seller_agent` binding so the request field is redundant, and that putting seller on the context path opens a request-time filtering vector. In practice a provider serves many sellers and needs the asking seller's identity on the wire to scope its active-set resolution without a deployment-pinned constant — the same need the buyer agent has on the identity path, even though the actor and the set it resolves against differ. The decorrelation argument does not apply: `seller_agent_url` is a single stable value identifying the asking seller, identical for every user on a placement and carrying no user identity, so it adds no per-user signal that context and identity requests could be correlated on. The package-set decorrelation guarantee constrains per-user-varying data (`package_ids`), which is unchanged. Required, consistent with identity-match. `context_match_request` is `x-status: experimental`, so the added required field is permitted pre-stable. Files: - `static/schemas/source/tmp/context-match-request.json` — `seller_agent_url` property (string, uri) added to `properties` and to `required`. - `docs/trusted-match/specification.mdx` — §Seller Attribution "Placement rationale", the Router participant row, and the "What This Is Not" bullet rewritten so the normative text matches: both request types carry `seller_agent_url`; the package-side `seller_agent` remains attribution-only; neither may be used as a per-user filter. - `docs/trusted-match/{index,buyer-guide,context-and-identity,ai-mediation}.mdx` and `docs/trusted-match/surfaces/{web,mobile,ctv,ai-assistants,retail-media}.mdx` — request examples updated with `seller_agent_url`. - `tests/example-validation-simple.test.cjs` — both context-match request fixtures updated. - 1e44c04: TMP Identity Match: add required `seller_agent_url` to the request and make `package_ids` optional. **Why.** The buyer's identity-match service already keeps the authoritative set of active packages it has registered per seller. Carrying that set on every request was redundant and forced publishers to enumerate ALL active packages on every call to avoid the set-correlation attack on Context Match. Identifying the seller by URL lets the buyer resolve the package set itself. **Changes to `static/schemas/source/tmp/identity-match-request.json`.** - New required field `seller_agent_url` (`string`, `format: uri`). The seller agent's API endpoint URL. Compared using the AdCP URL canonicalization rules, consistent with `seller_agent.agent_url` on `AvailablePackage` and `agent_url` in `adagents.json`. - `package_ids` is now optional. When omitted, the buyer evaluates against the full active set registered for `seller_agent_url`. When provided, the ALL-active-packages rule still applies — partial sets remain a correlation risk. - Top-level description updated to reflect both modes. **Spec changes alongside the schema.** - Reversed prior stance forbidding seller identity on `identity_match_request`. The "What This Is Not" / SellerAgentRef guidance has been narrowed to apply only to `context_match_request`. - Added a fail-closed rule: when `seller_agent_url` matches no seller for which the buyer has registered active packages, the buyer MUST return an empty `eligible_package_ids`, not fall back to another seller's set. - Defined precedence when both `seller_agent_url` and `package_ids` are present: buyer evaluates against the intersection of its registered active set and `package_ids`; unknown IDs are silently dropped (not error-surfaced) so the response cannot leak registry membership. - Reframed the package-set-decorrelation invariant as **statistical independence of `package_ids` from the current placement**, with two acceptable modes: all-active and fuzzed (random sample padded with synthetic non-existent IDs that the buyer silently drops). The page-specific subset remains forbidden. - Strengthened temporal decorrelation: random delay alone leaks the pairing through ordering. Publishers SHOULD also randomize whether Context Match or Identity Match is sent first — each opportunity SHOULD have a roughly equal probability either way. **Privacy boundary.** `seller_agent_url` identifies the seller agent, not the user; no leakage across the identity boundary. Routers do NOT strip it (unlike `country`) — buyers need it to resolve the package set. **Backwards compatibility.** Breaking for the experimental TMP schema (`x-status: experimental`): callers MUST now send `seller_agent_url`. The relaxation of `package_ids` is non-breaking on its own — previously valid requests remain valid as long as they also include `seller_agent_url`. - cdfe3ad: Add an experimental verified-identity attestation surface to TMP Identity Match, letting a publisher (or a network/issuer-as-RP) forward a **verifiable** proof about a user — proof-of-personhood and/or age — so the buyer verifies the claim cryptographically instead of trusting an assertion. Issuer-agnostic; World ID is the first scheme. **Schema changes (additive):** - `enums/uid-type.json` — adds `world_id_nullifier` (Sybil-resistant, rp-scoped, unlinkable pseudonym; asserts nothing on its own — trust comes from the accompanying attestation). - `enums/attestation-claim.json` (new) — closed, issuer-agnostic claim set: `unique_human`, `age_over_13/16/18/21`. Age is threshold-only and resolves to eligibility, never a wire attribute. - `tmp/identity-match-request.json` — adds an optional `attestation` object per `identities[]` entry (`issuer`, `scheme`, `relying_party_id`, `action`, `claims[]`, `verification_level`, `signal_binding`, `proof`, `expires_at`) and an optional top-level `sealed_credentials[]` (`{audience_kid, payload}`, TMPX envelope) for the network-as-RP carrier. `issuer` is a vendor BrandRef (`core/brand-ref.json`, canonical domain) — the same vendor-reference shape as measurement/signals vendors; the relying party is namespaced by the issuer as `(issuer.domain, issuer.brand_id, relying_party_id)`. **Contract-bearing note:** `identity-match-request.json` is `additionalProperties: false` on purpose (the identity privacy boundary). These fields are a deliberate, reviewed widening — they carry proof _about_ the identity (identity side of the boundary), not page context. Shipped as `x-status: experimental`; not subject to deprecation cycles until 3.0.0 GA. **Conformance invariants (normative):** verify every accepted `scheme`; treat an unverifiable attestation as "no attestation", never as asserted-true; reject on failed `signal_binding`, `relying_party_id` provenance, or `expires_at`; decrypt only `sealed_credentials` whose `audience_kid` you hold; bound attestation + sealed-credential count/size. **Router handling of `sealed_credentials[]` (normative):** forward each entry only to the provider owning its `audience_kid` (not broadcast); fold `sealed_credentials` into the per-provider re-signature canonical bytes; include a `sealed_credentials_hash` in the dedup cache key. relying_party_id ownership is published in `brand.json` `identity_relying_parties[]`; age jurisdiction→threshold tables live in the AdCP Policy Registry and resolve to `eligible_package_ids`. Advertised via a new `trusted_match.verified_identity` experimental feature id. Full design: `specs/tmp-verified-identity-attestation.md`. - 505cb4f: feat(trusted-match): scope the world_id_nullifier TMPX token to its relying party Register `world_id_nullifier` in the TMPX Type ID registry, and define its token as relying-party-scoped: a 16-byte digest of the proof's `relying_party_id` followed by the 32-byte nullifier. A World ID nullifier is meaningful only within the `rp_id` it was minted for, but the `rp_id` rides the request-side `attestation`, which does not round-trip into the `tmpx` exposure token. With only the bare nullifier in the token, the out-of-band impression tracker cannot attribute an exposure to its relying party or reconstruct the `(rp_id, nullifier)` key the buyer caps on. Embedding the `rp_id` digest closes that: the tracker matches the digest against the relying parties it accepts, keys frequency state on `(rp_id, nullifier)`, and no `rp_id` cleartext crosses into the token. Open (WG): the digest width (16 bytes proposed) and whether a digest-plus-registry lookup suffices versus carrying a registry-assigned relying-party id. `world_id_nullifier` is gated by the experimental `trusted_match.verified_identity` feature, so its token layout is not yet frozen. - b44996f: spec(manifest): publish `manifest.json` + structured `enumMetadata` to stop SDK drift (adcp#3725) Adds two additive artifacts to every released schema bundle: 1. **`enums/error-code.json` gains an `enumMetadata` block.** Every error code now carries structured `recovery` (correctable | transient | terminal) and `suggestion` fields. SDKs MUST consume this block instead of parsing `Recovery: X` prose out of `enumDescriptions`. A build-time lint rejects any drift between the structured value and the prose. Root cause for adcp-client#1135 (17 missing codes, 3 wrong recovery classifications shipped in TS SDK for over a year). 2. **`manifest.json` at `/schemas/{version}/manifest.json` (and `/schemas/latest/manifest.json` for nightly codegen).** Single canonical artifact listing every tool (with `protocol`, `mutating`, `request_schema`, `response_schema`, `async_response_schemas`, `specialisms`), every error code (with `recovery`, `description`, `suggestion`), an `error_code_policy` block (defining `default_unknown_recovery` so SDKs handle non-spec codes from non-conforming sellers correctly), and every storyboard specialism (with `protocol`, `entry_point_tools`, `exercised_tools`). Validates against `/schemas/{version}/manifest.schema.json`. Generated deterministically from existing source — no new authored content. Lets SDKs derive their internal tool/error tables from one place at codegen time instead of hand-transcribing the spec. `mutating` is derived using the same classifier the idempotency-key lint enforces (single source of truth — manifest and lint can never disagree). The read-only verb pattern was tightened in the process: it now anchors at the start so tools like `create-collection-list` and `delete-property-list` are no longer mis-classified as read-only because they happen to contain `-list-` mid-name. `search-` was added as a read-only verb. Specialisms expose two distinct tool sets per #3725 review feedback: `entry_point_tools` (the curated minimal contract from `index.yaml.required_tools` — what the spec asserts implementers MUST ship) and `exercised_tools` (the full surface — union of own phases and every linked scenario, derived by walking `phases[].steps[].task` and resolving `requires_scenarios`). SDK authors should size their tool registration against `exercised_tools` to ensure they handle every call the conformance kit will make. Migration: SDKs targeting 3.0.x continue to work unchanged — `enumDescriptions` and the existing `index.json` are retained verbatim. SDKs targeting 3.1+ should switch to `enumMetadata` for error recovery and `manifest.json` for tool/specialism enumeration. The prose "Recovery: X" sentence embedded in each `enumDescriptions` value is stripped from the manifest's per-code `description` to avoid double-encoding; it remains in `enumDescriptions` for the human-readable narrative until a future minor formally deprecates it. Until then, the lint guarantees both surfaces stay synchronized. - af1d287: spec(creative): add pre-call discriminators for creative-transformer refinement retention and fan-out multiplicity. Lets a buyer agent know — before sending — what a creative agent supports, instead of probing and handling failures. Additive and optional (all fields default to "unsupported / unbounded"), and the keystone the spend-control and conformance follow-ons build on. - `get_adcp_capabilities` → `creative.refinable_retention_seconds` (integer): the guaranteed-minimum window a produced `build_variant_id` stays refinable. Replaces the prose-only "agent-defined window" with a machine-readable floor; omit to keep it agent-defined. - `get_adcp_capabilities` → `creative.multiplicity` (object): `supports_catalog_fanout` + `max_creatives_limit`, `supports_variants` + `max_variants_limit`, and `variant_dimensions[]`. Over-limit `max_creatives`/`max_variants` are **clamped** to the ceilings (shortfall via `items_returned` < `items_total`), not rejected — consistent with `item_limit`'s "use the lesser" rule. Absent means no fan-out. - `transformer.json` → optional `multiplicity` that narrows the agent-level object per transformer (ceilings ≤ agent, `variant_dimensions` ⊆ agent). - `build_creative` docs note the clamp behavior on `max_creatives`/`max_variants`. - 1652b93: Unify metric accountability into a single timestamped contract array covering both standard and vendor-defined metrics. Reshapes `package.committed_metrics` and `by_package.missing_metrics` from string arrays to discriminated object arrays. Closes the audit gap for vendor metrics (#3519), adds mid-flight contract amendments (#3518), and supersedes the parallel-array design that shipped hours ago in #3510. **Why a unified shape.** AdCP had grown five different metric adjectives (`available`, `required`, `committed`, `requested`, `missing`) across two parallel surfaces (standard via the closed `available-metric.json` enum; vendor via the structured `vendor_metric_extensions`). The contract layer (committed/missing) is the right place to unify because: 1. Buyer's reconciliation code is simpler — one array walk, one shape 2. The contract is the "agreement reached" — it doesn't matter where the metric came from (closed enum vs vendor extension) 3. Audit is symmetric — `missing_metrics` covers everything that was committed but not delivered, regardless of metric scope 4. Mid-flight amendments fit naturally — every entry is timestamped, so day-1 commitments and mid-flight additions share one shape The capability layer (`reporting_capabilities.available_metrics` and `vendor_metrics`) stays separate — capabilities use the closed vocabulary upstream, contracts use the unified shape because they need timestamps and vendor scoping. **Schemas added.** - `enums/metric-scope.json`: discriminator enum `["standard", "vendor"]`. Tags entries in unified metric arrays so consumers can branch on a literal string instead of inferring from field presence. Matches the existing AdCP discriminator pattern (`refinement_applied`, `incomplete[].scope`). **Schemas reshaped.** - `core/package.json` `committed_metrics`: was `string[]` from `available-metric.json` enum + parallel `committed_vendor_metrics` array. Now a single `[{scope, metric_id, vendor?, committed_at}]` array covering both. Each entry carries an explicit `committed_at` timestamp, so the array also serves as the contract amendment ledger. Day-1 entries share `committed_at = create_media_buy.confirmed_at`; mid-flight additions appended via `update_media_buy` carry their own timestamps. Append-only — sellers MUST reject attempts to modify or remove existing entries with `validation_error` (suggested code: `IMMUTABLE_FIELD`). The standalone `committed_vendor_metrics` field is **deleted**; vendor entries now live in the unified array with `scope: "vendor"`. - `media-buy/get-media-buy-delivery-response.json` `by_package[].missing_metrics`: was `string[]`. Now `[{scope, metric_id, vendor?}]`, symmetric with `committed_metrics` minus the timestamp (the audit channel doesn't need to carry the commitment time; it filters by it). - `missing_metrics` reconciliation rule: filters `committed_metrics` to entries where `committed_at < reporting_period.end`, then flags any not populated in the report. A metric committed mid-flight is audited only from its commitment timestamp forward — matches the IAB Open Measurement §4.3 precedent for accountability boundaries when measurement starts mid-flight. **Measurement-standard qualifier on standard entries.** Standard-scope entries on `committed_metrics` and `missing_metrics` MAY carry an optional `qualifier` object disambiguating metrics whose definition varies by measurement standard. v1 defines a single qualifier key — `viewability_standard` (`mrc` | `groupm`) — required when the seller commits to a specific viewability standard for any of `viewable_impressions`, `viewable_rate`, `measurable_impressions`. Without it the contract is ambiguous (MRC and GroupM are materially different thresholds and not comparable, see `viewability-standard.json`) and reconciliation falls back to whatever `viewability.standard` the delivery report happens to carry. Symmetric on `missing_metrics`: a buyer expecting MRC viewability flags a GroupM-only delivery report as missing the MRC commitment. The qualifier object is closed (`additionalProperties: false`) so future qualifiers — completion threshold, reach unit — get added explicitly in subsequent minors rather than via free-form keys. Emerged from a field discussion where a partner proposed an `ext`-level viewability rollup at root `aggregated_totals`; the right place to handle standard-disambiguation is the contract entry, not the aggregate. **Vendor metric accountability scope.** PR #3492 deliberately scoped vendor metrics as advisory in v1 ("buyers verify out-of-band via `measurable_impressions` coverage"). With this PR, the advisory-vs-accountable distinction moves to the contract layer rather than the metric scope: any metric (standard or vendor) that appears in `committed_metrics` is accountable. Sellers who can't credibly attest to a vendor metric SHOULD NOT stamp it; absence keeps that metric advisory and reconciliation falls back to coverage plus out-of-band verification. **Closes/supersedes.** - Closes #3518 (mid-flight amendments — every entry has its own `committed_at`, so amendments are just new entries; no separate `additional_committed_metrics` array needed) - Closes #3519 (vendor-metric audit symmetry — vendor entries live in the unified `missing_metrics` array; no separate `missing_vendor_metrics` field needed) - Supersedes the parallel-array design from #3510. The `string[]` shape introduced there merged hours before this PR and had zero GA adopters; the breaking change is taking advantage of the open window to land the cleaner final shape before adoption hardens. **Wired in.** - `core/package.json`: reshape `committed_metrics`, delete `committed_vendor_metrics`. - `media-buy/get-media-buy-delivery-response.json`: reshape `missing_metrics` and update the description to declare the reconciliation rule (`committed_at < reporting_period.end`). - `enums/metric-scope.json`: new shared discriminator. - `docs/media-buy/task-reference/create_media_buy.mdx`: rewrite the "Reporting contract on confirmed packages" section with a worked example showing day-1 + mid-flight entries and the `qualifier.viewability_standard` on viewability metrics. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: update `missing_metrics` bullet with the discriminated-shape example and the qualifier-symmetric reconciliation note. - `docs/media-buy/media-buys/optimization-reporting.mdx`: update the Vendor-Defined Metrics section to reflect that the advisory-vs-accountable distinction now lives at the contract layer (any committed metric is accountable, regardless of scope). **Backwards compatibility.** Both `committed_metrics` and `missing_metrics` are optional. The fields landed in #3472 and #3510 hours before this PR with `string[]` shape; that shape is now replaced with a discriminated object array. Adopters who jumped on the `string[]` shape immediately need to update; this is judged acceptable given the field's optional status, the absence of any GA implementations, and the meaningful improvement in the final conceptual model. **WG review.** This PR involves a v1.x scope shift on vendor-metric accountability and a breaking reshape of two newly-merged optional fields. Worth WG visibility before merge. Refs #3518, #3519. Builds on #3472, #3492, #3510. - 2f88e59: Document the normative attestation-mode selection rule for upstream_traffic compliance checks. Conforming runners now have one explicit raw-vs-digest decision order for query_upstream_traffic that preserves assertion coverage, including the non-JSON identifier_paths case where raw mode is required to avoid grading an otherwise evaluable assertion as not_applicable. Storyboard authors should rely on that rule instead of non-schema attestation-mode hints. Closes #5080. - f6af651: spec(url-asset): add SHOULD on `url_type`, role-based fallback, and mechanism-vs-purpose clarification (#2986 step 2) `url_type` was optional with no fallback rule, so a conformant URL asset that omitted it left receivers guessing — buyers would either pick a default mechanism (with bad blast-radius if a clickthrough fired as a pixel) or refuse to render. Two parallel vocabularies (`url-asset-type` mechanism: 3 values; `url-asset-requirements.role` purpose: 6 values) compounded the confusion because the docs treated them as the same thing. This change: - Adds a top-level description on `url-asset` stating senders SHOULD include `url_type` on every URL asset, and defining the receiver fallback: when `url_type` is absent, receivers SHOULD fall back to the format's `url-asset-requirements.role` (clickthrough/landing_page → `clickthrough` mechanism; \*\_tracker roles → `tracker_pixel`); when neither is present, receivers MAY reject rather than guess. - Updates the `url_type` property description to frame it explicitly as the receiver's invocation mechanism, and points at the role fallback for senders that omit it. - Updates `url-asset-requirements.role` description to call out the mechanism-vs-purpose distinction (a `click_tracker` slot validly accepts a `tracker_pixel` URL). - Rewrites `docs/creative/asset-types.mdx` URL Asset section, replacing the old "you only need to supply the `url` value" guidance and the incorrect enum list (`impression_tracker`/`video_tracker`/`landing_page` — those were the requirement-side `role` values, not `url_type` values) with the actual `clickthrough`/`tracker_pixel`/`tracker_script` enum, the SHOULD note, and the role fallback table. Wire format unchanged. Existing senders that already include `url_type` are unaffected. Senders that omit `url_type` continue to validate but now have explicit receiver semantics; in 4.0 we plan to make `url_type` required (separate change). Closes step 2 of the rollout proposed on adcp#2986. - 9c087a2: feat(creative): v2 Phase 1 — asset_group_id vocabulary registry, `scenes` schema, `zip` asset type, video/audio mdx asset_type fixes First PR implementing the v2 creative formats RFC (#3305). Backwards-compatible additions only — no v1 producers are affected. Minor bump because this introduces new schemas (`asset-group-vocabulary.json`, `scenes.json`, `zip-asset.json`), which are additive features rather than bug fixes. **New schemas:** - `static/schemas/source/core/asset-group-vocabulary.json` — canonical registry of `asset_group_id` values (the seven existing catalog vocab entries plus 12 audit-driven additions: `video_vertical`, `video_horizontal`, `audio`, `companion_image`, `companion_banner`, `brand_name`, `body_text`, `cards`, `landing_page_url`, `privacy_policy_url`, `youtube_video_id`, `pin_id`). Includes the `landing_page_url` aliases canonicalizing six different field names today (`click_url`, `link`, `final_url`, `link_url`, `click_through_url`, `landing_url`). Non-canonical IDs remain valid for platform-specific extensions; validators MAY soft-warn on non-canonical usage. - `static/schemas/source/creative/scenes.json` — typed scene-by-scene structure used as input to `build_creative` for generative video platforms. Each scene has `order`, `duration_ms`, `description`, optional `vo` and `caption`. Renamed from "storyboard" to avoid collision with the testing-harness storyboard concept; description disambiguates from `reference-asset.json` `purpose: "storyboard"` (which describes a reference asset, not a structured plan). - `static/schemas/source/core/assets/zip-asset.json` — new asset type for bundled creatives delivered as zip archives (HTML5 banners with index.html + CSS + JS + images, MRAID-compatible interactive ads). Carries `url`, optional `max_file_size_kb`, `entry_point`, `allowed_inner_extensions`, `backup_image_url`, and SHA-256 `digest` for integrity. Distinct from inline HTML (`html` asset) and from third-party tag URLs (`url` asset with appropriate `url_type`). **Registry updates:** - `static/schemas/source/creative/asset-types/index.json` — added `zip` entry pointing at the new schema - `static/schemas/source/core/format.json` — added `IndividualZipAsset` and `GroupZipAsset` branches to the format declaration oneOf - `static/schemas/source/core/offering-asset-group.json`, `creative-manifest.json`, `creative-asset.json`, `creative/list-creatives-response.json` — added `zip-asset.json` to manifest/asset-group oneOf branches so manifests can carry zip assets **Doc fixes:** - `docs/creative/channels/video.mdx` — corrected three format-definition examples that used `asset_type: "url"` + `asset_role: "vast_url"` / `"vpaid_url"`, contradicting the schema-correct `asset_type: "vast"` used elsewhere in the same file. Updated VPAID examples to use `asset_type: "vast"` with `vpaid_enabled: true` in requirements. - `docs/creative/channels/audio.mdx:200` — same bug pattern: `asset_type: "url"` for what should be a VAST audio tag. Corrected to `asset_type: "vast"` with `delivery_type: "url"`; renamed slot key from `vast_url` to `vast_tag` for clarity. **Why minor (not patch):** new schemas and a new asset type are additive features — patch is reserved for bug fixes only. **Why not major:** no breaking changes; v1 producers and consumers continue to work unchanged. The new `zip` asset type is purely additive — receivers that don't recognize it ignore it via standard discriminator-mismatch handling. Tracks #3305 (v2 RFC). Phase 1 lays foundational primitives; subsequent phases build the canonical format catalog, `ProductFormatDeclaration` schema, and tools on top of these primitives. - 25131af: Add optional storyboard validation ids and require runners to echo them in validation results for stable per-assertion diagnostics. - b4471ce: Add `vast_tracker` and `daast_tracker` asset types for decomposed VAST/DAAST `` URLs. Creative agents can now emit per-event tracker URLs (start, quartiles, complete, etc.) as a discriminated-union alternative to a complete VAST tag; the sales agent assembles them into the VAST `` block at serve time. Adds normative creative/sales boundary: wrapper ownership belongs to the sales agent, and the `` URL stays on `url` asset with `url_type: "tracker_pixel"` (not `vast_tracker` with `vast_event: "impression"`). **Tracker asset constraints (from authoritative spec):** - `offset` pattern aligns with the VAST 4.2 XSD `Tracking@offset` constraint (`vast_4.2.xsd` line 146): `HH:MM:SS[.mmm]` with two-digit hours and minutes/seconds 00–59, or an integer percentage 0–100 suffixed with `%`. Negative offsets are not permitted — the VAST XSD pattern has no leading-minus branch. - A JSON Schema `if/then` requires `offset` whenever `vast_event` / `daast_event` is `progress` (mirrors the XSD documentation: "Must be present for progress event"). - `vast_event` / `daast_event` exclude both VAST/DAAST element-children that don't live under `` (`impression`, `clickTracking`, `customClick`, `error`) and ``-element children (`viewable`, `notViewable`, `viewUndetermined`, `measurableImpression`, `viewableImpression`). - Each tracker carries a `target` field (`linear` | `non_linear` | `companion` for VAST; `linear` | `companion` for DAAST, since DAAST has no `` element) so the sales agent places the tracker under the correct `` parent during XML assembly. **Tracking-event enum corrections (corrective alignment to spec):** - VAST: add the five VAST 4.2 events that were missing from `vast-tracking-event.json` (`acceptInvitation`, `adExpand`, `adCollapse`, `minimize`, `overlayViewDuration` — all in the XSD enumeration). Drop `notUsed`, which was incorrectly inherited from earlier draft work and is not in the VAST 4.2 XSD `Tracking@event` enumeration. `fullscreen` / `exitFullscreen` are kept and labeled as VAST 2.x / 3.x compat. - DAAST: add `rewind` (DAAST 1.1 §3.2.1.7 lists it explicitly). Drop `loaded`, which is not in DAAST 1.1 §3.2.1.7. `progress` is retained per DAAST 1.1 §3.2.4.3. These enum corrections are nominally breaking for the existing `tracking_events` field on the `vast` / `daast` asset types, but the dropped values were never spec-correct (`notUsed` is not in the VAST 4.2 XSD; `loaded` is not in DAAST 1.1 §3.2.1.7) — fixing them up before the new tracker assets reference these enums avoids carrying the inconsistency forward. - 1431b6e: Add vendor-defined metric extensions — a structured pointer surface for proprietary measurement metrics (attention scores, emissions per impression, panel-based demographics, brand-lift surveys, in-flight attention panels) that don't belong in the closed `available-metric.json` enum. Resolves the closed/open enum question raised in #3460 with a structured surface instead of opening the standard vocabulary to free-form strings. **Why a parallel surface, not opening the enum.** Opening the closed enum to free-form strings (e.g., `x_*` prefixed) would solve the asymmetry with `delivery-metrics.json`'s `additionalProperties: true` posture but defeats discovery: a buyer asking "I need attention measurement" can't query a flat string namespace where every vendor uses a different name. A structured extension gives the buyer a queryable axis — `vendor` (BrandRef) — with `metric_id` as a second pin once vendors converge. **Why the surface is intentionally thin.** Per-product extensions carry only what the seller can credibly attest to: "I support this vendor's metric." Everything else — category, methodology, standard alignment, human-readable documentation, agent capabilities — is a property of the vendor's metric definition, published once at the vendor's `brand.json` `agents[type='measurement']` and queried out-of-band. Re-asserting that metadata on every seller's extension is duplication that drifts. **Schemas added.** - `core/vendor-metric-id.json`: shared identifier schema (analogous to `core/brand-id.json`) — lowercase pattern, length bounds, namespaced semantics. Reused by the declaration site, the value site, and the filter site. - `core/vendor-metric-value.json`: the reported value `{ vendor, metric_id, value, unit?, measurable_impressions?, breakdown? }`. `measurable_impressions` is the coverage denominator (vendor measurement is rarely 100% — vendors only score impressions where their SDK fires or their panel matches). Absence means coverage is unspecified; do NOT compute a coverage rate or assume full coverage when absent. The `breakdown` slot is the only escape hatch for structured payloads beyond a single scalar (panel demographic breakouts, co-view ratios, incremental decompositions); the rest of the envelope is closed (`additionalProperties: false` on the value object). This pattern parallels the existing `viewability.measurable_impressions` field. **Wired in.** - `core/reporting-capabilities.json`: new `vendor_metrics` array (parallel to `available_metrics`). Semantic uniqueness key is `(vendor.domain, vendor.brand_id, metric_id)`; sellers MUST NOT declare the same vendor metric twice. JSON Schema `uniqueItems` is not used because BrandRef carries optional fields whose absence/presence would defeat deep-equal — uniqueness is enforced at build/validation time on the semantic key. - `core/product-filters.json`: new `required_vendor_metrics` filter — each entry pins `vendor` and/or `metric_id`. Cross-vendor discovery (e.g., "any attention measurement") is the buyer agent's responsibility: the agent resolves which vendors offer a category via the vendors' `brand.json` records, then enumerates them as filter entries. Same filter-not-fail convention as the other `required_*` filters. - `core/delivery-metrics.json`: new `vendor_metric_values` array — emitted alongside standard scalars on every level that uses delivery-metrics (totals, by_package, by_creative, by_audience, etc.). One row per `(vendor.domain, vendor.brand_id, metric_id)` per reporting period. The parent `additionalProperties: true` is preserved so existing free-form vendor emissions remain conformant during migration. - `docs/media-buy/task-reference/get_products.mdx`: new filter row. - `docs/media-buy/task-reference/get_media_buy_delivery.mdx`: new `vendor_metric_values` bullet under per-package fields. - `docs/media-buy/media-buys/optimization-reporting.mdx`: new Vendor-Defined Metrics section covering declaration, the brand.json discovery anchor for vendor-side metadata, the filter shape and cross-vendor discovery responsibility, the value emission shape with the coverage denominator, the standards-driven promotion path, and the v1 accountability scope. **v1 accountability scope.** Standard `available_metrics` are subject to the `missing_metrics` contract from #3472. Vendor metrics are advisory in v1 — buyers verify out-of-band via `measurable_impressions` coverage and direct calls to the vendor's measurement agent. The asymmetry reflects what the seller can credibly attest to: SSPs typically don't have proprietary measurement numbers in their delivery pipeline; those flow from the vendor's own infrastructure. **Promotion path.** When the industry converges on a metric via a published standard, the spec adds it to the closed `available-metric.json` enum and the vendor extensions become historical aliases. Anchored on standards-body publication, not vendor-count thresholds. **Backwards compatibility.** All additions are optional. Sellers without vendor metrics see no change. The closed `available-metric.json` enum is unchanged. `additionalProperties: true` is preserved on `delivery-metrics.json` so existing free-form vendor emissions remain conformant; the structured `vendor_metric_values` array is the recommended path going forward. Refs #3460. Closes the closed/open enum question. - 952787c: Add vendor_metric optimization-goal storyboard coverage (issue #4933). New storyboard exercises the 3.1 vendor_metric goal contract: positive acceptance when all preconditions are met, and negative paths for capability mismatch and reporting-coherence mismatch. Training-agent fixtures now surface vendor_metric_optimization on products. - fb01678: Add `kind: "vendor_metric"` optimization goal — end-to-end buyer→seller→vendor binding for vendor-attested measurement (attention, brand lift, emissions, retail-media partner metrics). Closes #4644. **The problem.** The `metric` kind's enum had `attention_seconds` and `attention_score` as if they were seller-native metrics — but DoubleVerify, IAS, Adelaide, TVision, and Lumen each define attention differently with no MRC-or-equivalent shared standard. A buyer setting `{ metric: "attention_seconds" }` was asking a meaningless question — _whose_ attention model? The seller had to guess, and delivery reconciliation against `vendor_metric_values[]` (which IS vendor-keyed) couldn't close the loop. **The fix — three additions that mirror existing patterns:** 1. **`kind: "vendor_metric"` on `optimization-goal.json`** — third oneOf branch, structurally parallel to the existing `event` kind (which binds buyer-attested conversion events). Shape: ```json { "kind": "vendor_metric", "vendor": { "domain": "adelaidemetrics.com" }, "metric_id": "attention_score", "target": { "kind": "threshold_rate", "value": 70 }, "priority": 1 } ``` `vendor` is the same BrandRef shape used on `vendor_metric_values.vendor`, `reporting_capabilities.vendor_metrics[].vendor`, and `performance_standards.vendor` — symmetric across discovery, capability, commitment, optimization, and reporting surfaces. `metric_id` is the same `vendor-metric-id` reference used on the reporting side. Targets are `cost_per` and `threshold_rate` (no `maximize_value` — that's monetary-only). 2. **New `core/vendor-metric-optimization.json` capability schema** — product-level declaration of which `(vendor, metric_id)` pairs the product's bidding stack can steer toward, with `supported_targets` per pair. Referenced from `product.json` alongside `metric_optimization` and `reporting_capabilities`. Per-product, not per-seller, because measurement integrations vary by inventory (premium CTV may have DV attention integrated; remnant display won't). 3. **Three-precondition rejection rule.** Sellers MUST reject `vendor_metric` goals failing any of: - **Discovery** — `metric_id` is in the vendor's published `measurement.metrics[]` catalog. - **Capability** — `(vendor, metric_id)` is in the product's `vendor_metric_optimization.supported_metrics[]`, and `target.kind` is in the matching entry's `supported_targets`. - **Reporting coherence** — the package's `committed_metrics[]` includes a matching `{ scope: "vendor", vendor, metric_id }`. **Optimization without committed reporting is unverifiable** — the buyer can't grade the seller against a goal whose value isn't contractually reported. This precondition is what makes vendor-attested optimization meaningful at the wire level. **The deprecation.** `attention_seconds` and `attention_score` remain in the `metric` enum on `optimization-goal.json` and on `product.json` `metric_optimization.supported_metrics` for backwards compatibility this minor, marked **deprecated** in their descriptions. Slated for removal at the next major. Sellers MAY reject the deprecated values with `TERMS_REJECTED` and a pointer to the `vendor_metric` kind. Same deprecation pattern used elsewhere (e.g., `delivery_measurement.provider` → `vendors[]`). **What this unblocks.** The same `vendor_metric` shape generalizes to: - Panel-based brand lift (Kantar, Upwave, Cint) - Emissions optimization (Scope3, Good-Loop) - Retail-media partner metrics (Amazon, Walmart Connect, Criteo) - Any future vendor-attested measurement that adopters want as an optimization target **Symmetry summary** — same `(vendor, metric_id)` key across every surface: | Surface | Field | What it asserts | | ------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------- | | Discovery | Vendor's `get_adcp_capabilities.measurement.metrics[]` | "This metric exists in my catalog" | | Capability — reporting | Product's `reporting_capabilities.vendor_metrics` | "This product can report this vendor metric" | | Capability — optimization | Product's `vendor_metric_optimization.supported_metrics` (new) | "This product's bidder can steer toward this vendor metric" | | Commitment | Package's `committed_metrics` (scope: vendor) | "I commit to reporting this for this package" | | Optimization | Package's `optimization_goals` (kind: vendor_metric) (new) | "Steer delivery toward this for this package" | | Accountability | Package's `performance_standards.vendor` | "I commit to a threshold on this metric" | | Delivery — value | `vendor_metric_values` | "Here's what was measured" | | Delivery — missing | `missing_metrics` (scope: vendor) | "I committed but couldn't deliver" | **Backwards compatibility.** Additive — new schema, new oneOf branch, new optional product field, deprecated-but-still-valid enum values. Existing 3.x agents continue to validate. Buyers adopting `vendor_metric` need the matching seller-side capability + commitment in place; the three-precondition rule prevents silent acceptance of orphaned goals. **Doc updates.** New `kind: vendor_metric` section in `docs/media-buy/conversion-tracking/index.mdx` (alongside `kind: event` and `kind: metric`); Target Kinds and Choosing a Strategy tables updated; migration doc reflects deprecation routing. Opened as draft for a 7-day WG comment window before merge — measurement vendors (DV/IAS/Adelaide/Kantar/Scope3) invited to raise extension needs (e.g., `qualifier` slots for vendor sub-models) while the shape is still flexible. Closes #4644. - d3351cc: Brand protocol gains `verify_brand_claim` — a unified brand-agent task that lets partners ask the brand authoritatively whether a specific claim about its identity is true. One tool, four claim types discriminated by `claim_type`: - `subsidiary` — "Is this brand a subsidiary of yours?" (house-side) - `parent` — "Is this brand your parent house?" (leaf-side mirror, lets mutual assertion complete at the agent layer) - `property` — "Is this site / app / property one of yours?" - `trademark` — "Is this trademark yours?" The shared `VerificationStatus` enum (`owned`, `pending_review`, `transferring`, `disputed`, `not_ours`, `licensed_in`, `licensed_out`, `unknown`) captures the rich state surface crawl-based mutual-assertion can't express. Per-claim-type `details` field carries the typed response payload. Public/authorized tier split mirrors `get_brand_identity`. **Trust model is asymmetric by direction.** Signed rejections (`disputed` / `not_ours`) win unilaterally — a brand has standing to refuse association without reciprocation. Signed assertions (`owned` / `pending_review` / `transferring` / `licensed_*`) do NOT bypass mutual assertion — the reciprocating side must still confirm. When both sides have brand-agents, mutual assertion completes via two signed agent calls (subsidiary + parent claim types) without requiring a static-file crawl. Closes the malicious-house scenario: a brand can't unilaterally claim subsidiaries it doesn't own. **Cross-protocol Conformance addition to `brand.json`:** when a house publishes a brand-agent advertising `verify_brand_claim` with the relevant claim type, consumers SHOULD prefer the agent's signed response over crawl-based inference. The crawl path remains the fallback when the agent is unreachable or returns `unknown`. The email-notification SHOULD from PR #4505 continues to apply for houses without a brand-agent. **Schema additions:** - `brand/verification-status.json` — shared status enum - `brand/verify-brand-claim-request.json` — schema-level `discriminator: { propertyName: "claim_type" }` with four per-claim-type variants - `brand/verify-brand-claim-response.json` — `claim_type` echoed, `status` from the shared enum, per-claim-type `details` object **No changes to `brand.json` itself.** Additive — every existing publisher and every existing brand-agent continues to work unchanged. The single-tool design preserves AdCP's tool-count economy: new claim types (e.g., licensed_from, endorsement) are payload-discriminator additions, not new tools. Standing licensed relationships as a static brand.json publishing surface (parallel to `brand_refs[]` for ownership) remain out of scope and are tracked as a separate design alongside the rights-protocol team. `verify_brand_claim` exposes the licensed states via the brand-agent's internal records; the static-file substrate that backs them is a future RFC. - e22385c: Brand protocol gains `verify_brand_claims` — the bulk variant of `verify_brand_claim`. Same four claim types (`subsidiary`, `parent`, `property`, `trademark`), same per-claim semantics, one MCP round-trip and one rate-limit slot for up to 100 claims. Use when a caller (crawler refreshing a brand portfolio, creative-clearance pipeline batch, inventory-onboarding scan) needs to verify many claims against one brand-agent and per-call overhead dominates. **Sibling tool, not a mode flag.** `verify_brand_claim` stays as-is for one-off verifications; `verify_brand_claims` is the dedicated bulk surface. Cleaner schemas (no single-vs-bulk discriminator inside one tool), cleaner capability advertisement (each tool is advertised independently in `supported_tasks`), cleaner error semantics (per-result errors don't mix with single-target failures). **Order is preserved.** Agents MUST return `results[]` in the same order as the request's `claims[]` (positional zip-by-index). Callers pass a position-indexed batch and consume results by index. **Partial-failure semantics.** Per-claim failures (`UNSUPPORTED_CLAIM_TYPE` for one item, `AMBIGUOUS_MATCH` on one trademark query) ride on a per-result `error` field and do NOT fail the batch. Top-level `errors[]` is reserved for batch-level failures (auth, rate-limit, malformed request, over-cap claim count) — when set, `results` is absent. The two are mutually exclusive at the wire. **Caching.** Top-level `Cache-Control: max-age` represents the lowest-common max-age across the batch. Per-result staleness varies by status; callers needing finer cache control should split batches by expected volatility or re-verify volatile claims individually. **Rate-limiting.** A bulk call consumes one rate-limit slot per call, not per result. A batch of 100 hits the per-`{caller, query-target}` limit once. Agents SHOULD size bulk limits in calls/window when bulk is advertised. **Trust model unchanged and unshortened.** Mutual assertion still requires calling both sides — a `subsidiary` result returning `owned` inside a bulk batch still requires a separate `parent` call against the leaf-side agent. Bulk is round-trip economy, not a trust-model shortcut. **Schema additions:** - `brand/verify-brand-claims-request.json` — `claims[]` array with the per-item discriminator on `claim_type`. Max batch size 100; agents MAY enforce lower. - `brand/verify-brand-claims-response.json` — success arm carries `results[]` aligned to the request; per-result success mirrors `verify-brand-claim-response.json` success arm, per-result error carries an `error` field. Error arm carries batch-level `errors[]`. **No changes to `verify_brand_claim`.** Single-target tool ships unchanged; the bulk variant is purely additive. Capability advertisement is per-tool — agents MAY ship one, the other, or both. A `supported_claim_types` declaration applies to both tools when both are advertised. - 6eadf06: spec(versioning): release-precision protocol version negotiation via `adcp_version` envelope field Adds `adcp_version` (release-precision semver string, e.g. `"3.0"`, `"3.1"`, `"3.1-beta"`) as a top-level field on every request and response. Buyers send their release pin; sellers echo the release they actually served — never the seller's own latest release. Augments the existing `adcp_major_version` (integer) with finer precision and adds response-side echo, which the spec lacked. Composed once via `allOf $ref` to the new `core/version-envelope.json` schema (single source of truth across all 127 task schemas — no inline duplication). Capabilities response gains `adcp.supported_versions` (release strings, authoritative for negotiation) and `adcp.build_version` (full semver build identifier with optional pre-release and build-metadata per semver §9–§10, advisory only). `VERSION_UNSUPPORTED` error gets a standardized `error.data` shape via the new `error-details/version-unsupported.json` schema; `supported_versions` is required. Migration: spec stays SHOULD on both sides through all of 3.x (consistent with the 3.x stability guarantee that fields don't graduate optional → required within a major). The compliance grader carries the adoption pressure: advisory at 3.1, blocking failure at 3.2 for sellers that don't echo `adcp_version` or don't emit `supported_versions` on capabilities. 4.0 promotes the spec to MUST and removes `adcp_major_version`, `adcp.major_versions`, and `extensions.adcp.adcp_version`. Through 3.x, buyers SHOULD dual-emit both `adcp_version` and `adcp_major_version` so legacy 3.x sellers keep negotiating; when the two disagree at the major level the server MUST return `VERSION_UNSUPPORTED`. Fully additive on the wire (existing servers ignore `adcp_version` via `additionalProperties: true`). RFC: `specs/version-negotiation.md`. **One scoped behavior change in 17 request schemas:** the `allOf $ref` envelope-composition pattern requires permissive `additionalProperties` at root (draft-07 doesn't bypass parent strict-mode through `allOf`). 17 request schemas under `collection/`, `governance/`, `property/`, and `tmp/` previously declared `additionalProperties: false`; this PR flips them to `true` so the envelope's fields are accepted. Strict request validation returns at draft 2019-09 via `unevaluatedProperties: false` (tracked in #3534). The new lint at `tests/lint-version-envelope.test.cjs` enforces the invariant going forward. - 4ad0e82: Add `video_placement_types` declarations to products and placements, plus a matching `get_products.filters.video_placement_types` discovery filter, using IAB Tech Lab/OpenRTB 2.6 video placement definitions with AdCP-native field names. - 7a48ee4: Webhooks are signed with the agent's `request-signing` key — there is no separate webhook key purpose. The webhook verifier checklist (step 8) now accepts `adcp_use == "request-signing"` as canonical, with the deprecated `"webhook-signing"` still accepted for backward compatibility (removal tracked in adcontextprotocol/adcp#5555). Operators that want separate key material for webhooks publish a second `"request-signing"` key with a distinct `kid` and sign webhooks with it — key isolation comes from the `kid`, not a distinct `adcp_use`. Any other key-purpose failure — `"response-signing"`/`"governance-signing"`, absent `adcp_use`, or a missing `verify` key_op — is rejected with `webhook_signature_key_purpose_invalid`. `webhook_mode_mismatch` is unchanged and remains reserved for the HMAC-vs-9421 auth-mode selector mismatch. The relaxation is one-directional and safe: cross-protocol confusion is prevented by the RFC 9421 `tag` (`adcp/webhook-signing/v1`, part of the signed base, checked at step 3) and mandatory `content-digest` coverage — not by the key-purpose discriminator. A captured request signature carries `tag=adcp/request-signing/v1` and is rejected at step 3, so it can never be replayed as a webhook. The reverse remains forbidden: a webhook-signing key MUST NOT verify a request signature (request verification still requires `adcp_use == "request-signing"` exactly). Conformance vectors updated: former negative `webhook-signing/negative/008-wrong-adcp-use` (request-signing key rejected) becomes positive `webhook-signing/positive/008-request-signing-key-reuse` (accepted); a new negative `008-wrong-adcp-use` covers a `response-signing` key, still rejected. Semver note: this is `minor` because it widens verifier acceptance and deprecates the old key purpose without removing any wire-compatible signer or verifier behavior. The future removal of `"webhook-signing"` from the accepted webhook key-purpose set is tracked in adcontextprotocol/adcp#5555 and will be a major-version change. - 6fcedae: Adds webhook receiver envelope conformance coverage for delivery reporting webhooks. - Adds `media_buy_delivery` as the task type for persistent delivery-report webhook events. - Extends `async-response-data` with the payload-only delivery-report result shape used under `mcp-webhook-payload.result`. - Adds receiver replay vectors that accept full MCP webhook envelopes and reject bare `notification_type` delivery results, missing envelope fields, and invalid top-level task statuses. - Clarifies docs that reporting webhook signatures cover the exact raw bytes of the full envelope, not a reserialized inner result. Closes adcontextprotocol/adcp#5173 and adcontextprotocol/adcp#5174. - e9a79a0: Migrate prose required-when / cross-field rules to the `x-adcp-validation` extension across `get_adcp_capabilities` (closes #3827). Five fields gain machine-readable normative constraints that the storyboard runner and SDK validators can now enforce programmatically; previously these rules lived only in description prose. **Fields migrated:** - `request_signing.required_for` — `subset_of: "request_signing.supported_for"` (an operation can't be required without being supported) - `request_signing.warn_for` — `disjoint_with: "request_signing.required_for"` plus `subset_of: "request_signing.supported_for"` (mutually exclusive with required_for; both must be subsets of supported) - `webhook_signing.supported` — `verifier_constraints.must_equal_when: { value: true, any_of: [...] }` keyed on `media_buy.reporting_delivery_methods` including `webhook` or `media_buy.content_standards.supports_webhook_delivery: true` (closes a downgrade vector — emitting state-changing webhooks unsigned) - `identity.key_origins` — `verifier_constraints.purpose_anchoring` mapping each purpose to the signing posture that must be declared elsewhere on the response (e.g., `request_signing` purpose requires non-empty `request_signing.supported_for`/`required_for`) **Sub-key vocabulary extended** in `docs/reference/schema-extensions.mdx`: - `forbidden_when` (inverse of `required_when`) - `disjoint_with` (item-level mutual exclusion across array fields) - `subset_of` (item-level subset constraint across array fields) Codegen consumers and JSON Schema validators ignore `x-` keys, so the wire format is unchanged. Storyboard runners that don't yet recognize a sub-key MUST skip it and emit an "unrecognized validation rule" warning per the existing convention. **Excluded from migration (already enforced natively):** - `adcp.idempotency` — the discriminated `oneOf` already requires `replay_ttl_seconds` in the supported branch and forbids it in the unsupported branch. - `webhook_signing.algorithms` — the `enum` on each item already enforces the allowlist. Backwards compatibility: strictly additive on the wire. Verifiers that ignore `x-adcp-validation` continue to work; the existing prose descriptions still document the rules. Storyboard runners gain enforceable assertions for invariants that were previously prose-only. ### Patch Changes - 5a73382: Fix: CSRF middleware now exempts the per-tenant training-agent MCP route shape (`//mcp[-strict[-required|-forbidden]]`). Without this, requests to those routes — mounted at root via host-based dispatch on `test-agent.adcontextprotocol.org` — returned 403 `csrf_token_mismatch` before reaching the verifier, even for unsigned negative vectors that should have surfaced `request_signature_required`. The existing path-based exemption list only matched the legacy single-URL shape (`/mcp-strict`) and the AAO mount prefix (`/api/training-agent/`), missing the per-tenant URLs introduced when the strict routes moved to `//mcp-strict`. Pattern-matched on path shape rather than hostname because `req.hostname` is derived from `X-Forwarded-Host` under `trust proxy = 1`, which Fly's edge forwards as-received from the client. A hostname-based bypass would have let an attacker spoof `X-Forwarded-Host: test-agent.adcontextprotocol.org` on a cookie-authenticated route and skip CSRF. Path shape isn't client-spoofable. Unblocks `adcp grade request-signing https://test-agent.adcontextprotocol.org//mcp-strict` against the live test agent. Closes adcp#2368. - 89619fb: 3.1.0 docs + scenario sweep — three remaining small fixes batched ahead of GA (2026-05-29): - **#4574** Cleanup of stale `list_authorized_properties` references (replaced by `get_adcp_capabilities` portfolio in v3): - `static/compliance/source/specialisms/signal-owned/index.yaml` — narrative rewritten to reflect the v3 retirement. - `skills/adcp-media-buy/SKILL.md` — table row + dedicated section removed; `get_adcp_capabilities` row updated to mention portfolio surface. - `server/src/addie/mcp/adcp-tools.ts` — removed from the ADCP_TASK_REGISTRY map so Addie's MCP routing no longer advertises the retired task. - `tests/addie/__snapshots__/adcp-tool-schema-drift.test.ts.snap` — snapshot updated to match. - **#4713** Surface 3.1 version negotiation in three docs surfaces previously describing the legacy integer-only contract: - `docs/reference/whats-new-in-v3.mdx § Per-request version declaration` — leads with release-precision `adcp_version` + `adcp.supported_versions`; legacy `adcp_major_version` retained as backwards-compatible. - `docs/building/by-layer/L0/a2a-guide.mdx` and `mcp-guide.mdx` — agent/server card notes updated with release-precision framing and a cross-link to `versioning.mdx § Version negotiation`. - **#4712** `static/compliance/source/universal/error-compliance.yaml` (phase `version_negotiation`) — added a release-precision `VERSION_UNSUPPORTED` probe (`adcp_version: "99.0"`) as the sibling to the existing integer-only probe. Advisory at 3.1; promotes to required at the 3.2 storyboard cut. Closes the gap where an integer-only validator could pass all storyboards while shipping a broken 3.1 buyer experience. Three sibling issues closed without code change (already done on main or upstream): - #4466 — adagents.mdx `authorization_type` is now `(required)` on main. - #3981 — sponsored-intelligence si_get_offering `context_outputs` path is now `offering.offering_id` on main. - #3555 — push-notification-config.json `url` description now documents port permissiveness on main. - #4519 — refine_products scenario `brief` already removed on main. - #4462 — schema's `ttl_sec` is the required field; the commit cited in the issue body was reverted/never landed. - #3349 — references `scenarios/signals.js` in adcp-client; spec storyboards already use correct field names. - dcd78b9: Two coordinated updates ahead of 3.1 beta: - **Docs banner switch**: `docs.json` banner content updated from "🎉 AdCP 3.0 is now GA — see what's new" → "🚀 AdCP 3.1 beta is now available — see what's new". Links to `/docs/reference/whats-new-in-3-1`. The 3.0 GA banner had been displayed on docs.adcontextprotocol.org for months and was out of date. AAO main site (`server/public/index.html`) banner intentionally stays on "3.0 GA" — that audience is operators/agencies/members, beta messaging adds confusion without value. - **`whats-new-in-3-1.mdx` updates**: - **Beta status callout** at top: status is 3.1 beta; spec feature-complete; SDK + grader advisory-only during beta; GA target 2026-05-29; adopters can pin `adcp_version: "3.1-beta"` today. - **New "Final-spec clarifications (WG-review batch)" section** covering the 10 normative tightenings from PR #4796 (`4c124545f1`): `PROPOSAL_NOT_FOUND`, forward-compatible `error.code` decoding, `idempotency_key` required on every task request, MCP tool wrapper envelope tolerance, MCP serialization normalization (drops `payload.required`, adds `context` envelope field), idempotency replay returns historical snapshot, `refine[]` finalize-exclusivity, `pending_creatives` status disambiguation, `notices` advisory channel on runner-output-contract. The clarifications batch shipped after the original whats-new page was written; this catches the page up to current main. - e815fc8: Prepare 3.1 release-candidate docs and training surfaces: add a 3.0 to 3.1 migration guide, refresh current RC guidance to `3.1-rc.15`, and advertise the current RC from the training agent while retaining prior RC pins. - 1ebc729: Add 3.0 storyboard compatibility checks for the training agent and release flow. - d62358e: Document `generate` as a sibling of `path` on `context_outputs[]` entries in `storyboard-schema.yaml`. Mutual-exclusion with `path` (exactly one required). Supported generators: `uuid_v4` and `opaque_id` (both mint a UUID v4; the two names exist for spec-vs-implementation framing). Aligns the spec-side schema with the runner-side support shipped in adcp-client#1006. Closes #3216. - 68a9309: Harden brand-registry write path against adversarial parent claims (#3467). The brand-hierarchy auto-link path in `org-filters.ts` walks `brands.house_domain` and gates membership inheritance on `brand_manifest->'classification'->>'confidence' = 'high'`. Pre-fix, every community write surface (`save_brand` MCP tool, `editDiscoveredBrand`, `upsertDiscoveredBrand`) accepted both fields as opaque JSON, so an attacker could submit `house_domain = paying-target.example` plus `brand_manifest = { classification: { confidence: 'high' } }`, then sign in from `@attacker.example` to inherit a WorkOS membership in the paying target. Three bounded fixes at the DB layer in `brand-db.ts`: (1) `classification.*` is stripped from any caller-supplied `brand_manifest` and routed exclusively through a new typed `UpsertDiscoveredBrandInput.classification` field that only the brand-classifier service writes; (2) `house_domain` is canonicalized and validated on every write (rejects control characters, malformed shapes, and self-references); (3) edits preserve the prior trusted `classification` block so a refresh-logos edit doesn't silently drop the classifier's verdict. Regression test in `server/tests/unit/brand-db-house-domain-validation.test.ts` exercises the hostile payload against all three write methods. - d844dd0: Disable vitest `fileParallelism` for the server suite. The module-level `pool` singleton in `db/index.ts` is shared across tests in a worker — running files in parallel let one file's `afterAll(closeDatabase)` null the pool while a sibling was mid-query, producing "Database not initialized" 500s that looked like transient Anthropic flakes. Closes #3695. - e22f19a: Security: handle WorkOS `user.deleted` for primary-bound users (#3718). When a WorkOS user that is the primary credential on a multi-credential identity was deleted (operator action, account closure, or GDPR/CCPA erasure webhook), the CASCADE on `identity_workos_users.workos_user_id` dropped the binding and left the identity with zero primaries. `attachIdentityId` then resolved `primary_workos_user_id` to NULL, skipped the id-swap, and the surviving secondary signed in to an empty workspace — a denial-of-service against any non-primary user, reachable end-user-initiated via GDPR/CCPA. The `user.deleted` handler now promotes the longest-bound surviving secondary to primary in a single transaction before the CASCADE fires, mirroring the `findSuccessorForPromotion` pattern already used by `deleteMembership`. The handler also invalidates the session/JWT cache for both the deleted user and the promoted successor to close the 60-second window where a cached id-swap could still route reads to the dead binding. Promotion failures emit `logger.warn` (auto-routed to `#admin-errors`) plus an explicit `notifySystemError` ops alert, then return 200 so WorkOS doesn't retry-storm the webhook; the identity is left in a recoverable state for an admin to repair. - fd764a2: Cap response body size on AAO discovery fetches. `@adcp/sdk` now ships native `transport.maxResponseBytes` support (mid-stream abort with `ResponseTooLargeError`), so we pass it to the three `AdCPClient` constructors in `capabilities.ts`: 4 MB for `discoverMCPTools` / `discoverA2ATools` (legitimate large agents reach ~2 MB with 500 tools) and 1 MB for `fetchMeasurementCapabilities`. Closes #3731. Known limitation: `getAgentInfo` / `mcpClient.listTools()` in the SDK do not yet route through the size-limit wrapper, so the 4 MB cap on the discovery constructors is dormant until the SDK wraps that path. Tracking upstream at adcontextprotocol/adcp-client#1799. - d7d105a: Compare `ADMIN_API_KEY` with `crypto.timingSafeEqual` instead of `===`. Length-mismatch path runs a same-length dummy compare to keep total work constant. `Buffer.from(..., 'latin1')` makes the ASCII-only assumption on the key explicit. Closes #4209. - 97daa5a: Clarify account namespace semantics for `account_id` references. Account-id mode now explicitly covers both seller-defined IDs supplied out-of-band and upstream-managed namespaces discovered through `list_accounts`; sellers MUST expose `list_accounts` when a credential can access more than one account and SHOULD expose a singleton row when a credential can access exactly one account. `sync_accounts` provisioning remains the buyer-declared natural-key path, and sellers MAY echo `account_id` there only if they continue accepting natural-key `AccountRef` values for subsequent calls. Required-account tasks must receive an explicit `AccountRef`; optional account omission is task-local, not a hidden credential-implied default. These statements codify the existing 3.0 account-scoped request expectations without changing the wire shape, fields, enums, or discriminators. Refs #4341. - 9e4378c: Server: fire badge issuance on owner-driven compliance runs. The per-version badge fan-out (membership-org resolution + `processAgentBadges` loop across `SUPPORTED_BADGE_VERSIONS`) is extracted into a shared `runBadgeFanOut()` helper in `services/badge-issuance.ts`, and the two owner-driven paths now call it immediately after `recordComplianceRun`: - `evaluate_agent_quality` (member-tools) — full comply runs from an agent owner. - `POST /api/registry/agents/:url/storyboard/:storyboardId/run` — single-storyboard re-runs from the dashboard. The helper reads the latest per-storyboard state from `agent_storyboard_status` (rather than trusting the run's own inputs), so a single-storyboard owner re-run doesn't degrade badges for storyboards it didn't touch. Owner-facing impact: an owner who fixes a compliance issue and re-runs sees the badge update on the next page load instead of waiting up to a heartbeat cycle. Heartbeat behavior is unchanged — it still emits the verification-change Slack notification; owner paths skip the notify because the result is already delivered in chat / HTTP response. Closes #4376. - 168b71d: Fix misleading "Professional tier or higher" copy across the public-listing UX. The code accepts four API-access tiers (Professional, Builder, Member, Leader), but error messages, dashboard tooltips, Addie's behavior rules, OpenAPI schema descriptions, and docs all said "Professional tier or higher" — readable as "Professional and tiers more expensive than it" rather than the intended "any paid tier". Addie repeatedly told Builder customers to upgrade to Professional, which is both wrong and a lower-priced tier. Replaces the phrase with explicit tier lists ("Professional, Builder, Member, or Leader" or "paying AAO members") across 11 surfaces. No behavior change. - a9e292c: Add `server/src/scripts/audit-brand-domain-www-mismatch.ts` — dry-run audit identifying orgs whose past `brand_revisions` were written to a different brand domain than their current `organization_domains.is_primary=true` row (most commonly `www.` vs ``). Surfaces the blast radius for issue #4448 (Stage 2 #4159 drift), which manifests as publish-path manifest updates landing on a brand row the user has not previously curated. Read-only; no schema changes; feeds a follow-up backfill decision. - a4bb6fd: Fix `syncOrganizationDomains` (WorkOS `organization.updated` webhook) so `organizations.email_domain` is sourced from `organization_domains.is_primary=true` rather than `org.domains[0]`. WorkOS's domain-array order is not stable — orgs with a verified root + a `failed` www variant could have WorkOS list www first, overwriting `email_domain` to the wrong value on every webhook fire even though our table's `is_primary` row was correct. Scope3 hit this in prod: `email_domain` had drifted to `www.scope3.com` while `is_primary=true` was on `scope3.com`, causing downstream lookups like `brand-enrichment.ts`'s `WHERE email_domain = $1` to miss the org row entirely. Adds `server/src/scripts/sync-email-domain-from-is-primary.ts` (dry-run + `--apply`) to clear the pre-fix backlog and an integration test pinning the new behavior. - 0078057: `sync-email-domain-from-is-primary.ts` now classifies drift into three buckets (`null`, `www_drift`, `mismatched`) and only applies the `null` class by default. `www_drift` (the Scope3 class) is opt-in via `--include-www-drift`. `mismatched` is the subsidiary/M&A class (e.g. `linkedin.com` vs `microsoft.com`) and is never auto-fixed — those cases are surfaced for human review and are better modeled via `brands.house_domain` + `brand_domain_aliases` than by overwriting `email_domain`. Applied to prod 2026-05-12: 7 null cases backfilled. - a9e292c: Add `server/src/scripts/reconcile-brand-domain-www-mismatch.ts` — one-shot reconciliation for the three orgs identified by the #4448 audit (Affinity Answers, BidMachine, Scope3). Per affected org, copies `brand_manifest.agents` from `www.` into `` (deduped on agent url), marks the www brand row `manifest_orphaned=true`, and inserts a `brand_domain_aliases` row routing `www.` → `` (Scope3 already has the alias and an empty www stub — only the orphan step runs there). Idempotent; dry-run by default; `--apply` to persist. Resolves the publish-path drift introduced when Stage 2 of #4159 (`5163d21425`) moved brand-domain authority to `organization_domains.is_primary` without backfilling orgs whose prior brand curation lived on the www variant. - c4b9ea8: Fix: XSS in the adagents.json builder when rendering a hostile remote `adagents.json` or agent card. Any admin who validated a domain whose `adagents.json` or A2A agent-card contained script tags / event handlers in `card_data.name`, `validation.errors[*]`, `validation.warnings[*]`, `agent_cards[*].errors`, `agent_url`, or `domain` would have executed attacker-controlled JS in the admin's session. Reflections in `displayValidationResults()` and `displayAgentCardsResults()` now route every interpolated field through `escapeHtml()`, including the raw-data `
` block (which previously emitted unescaped JSON, letting an attacker break out with `