diff --git a/.changeset/creative-addons-3-3.md b/.changeset/creative-addons-3-3.md
new file mode 100644
index 00000000000..17b6ffa3916
--- /dev/null
+++ b/.changeset/creative-addons-3-3.md
@@ -0,0 +1,5 @@
+---
+"adcontextprotocol": minor
+---
+
+Add the proposed get_creative_addons task for operator-registered fragments adjacent to unchanged image, HTML5, and display-tag creatives. Define pre-commit disclosure through existing proposal and product-feed bindings, deterministic assembly and failure rules, and mandatory adcp/addon-chain/v1 signed variant evidence. The 3.3 target remains subject to Working Group approval of scope and the dedicated signing profile.
diff --git a/docs/accounts/tasks/sync_accounts.mdx b/docs/accounts/tasks/sync_accounts.mdx
index dc5d1a59d31..7ea072d963e 100644
--- a/docs/accounts/tasks/sync_accounts.mdx
+++ b/docs/accounts/tasks/sync_accounts.mdx
@@ -732,3 +732,7 @@ asyncio.run(main())
- [Accounts and agents](/docs/building/by-layer/L2/accounts-and-agents) -- Billing models, trust models, and authorized operators
- [Brand protocol](/docs/brand-protocol/brand-json) -- How seller agents resolve brand identity from the `brand.domain`
- [get_adcp_capabilities](/docs/protocol/get_adcp_capabilities) -- Discover `supported_billing` and `require_operator_auth` before syncing accounts
+
+## Creative addon disclosure (proposed 3.3)
+
+A successful account response row may expose account-scoped `creative_addons`. It inherits that row’s account identity and does not by itself prove completeness or freshness for a product purchase. The account revision is not an addon acknowledgment. See [pre-commit disclosure](/docs/creative/task-reference/get_creative_addons#disclosure-before-commitment).
diff --git a/docs/building/by-layer/L1/security.mdx b/docs/building/by-layer/L1/security.mdx
index 34d0e68c8e2..dc2ef3f0b72 100644
--- a/docs/building/by-layer/L1/security.mdx
+++ b/docs/building/by-layer/L1/security.mdx
@@ -1042,7 +1042,9 @@ A task is admitted to the designated list only when its response payload is the
- **`verify_brand_claim`** and its bulk variant **`verify_brand_claims`** (Brand Protocol). The responding brand-agent signs its response payload as a JWS envelope under the brand's `adcp_use: "response-signing"` key. The signature is load-bearing for the direction-asymmetric trust model — see [`verify_brand_claim` trust model](/docs/brand-protocol/tasks/verify_brand_claim#trust-model) and [Building a brand agent — Signing setup](/docs/brand-protocol/building-a-brand-agent#signing-setup).
-Any task not on this list MUST NOT sign its response under any signing primitive. General-purpose response-signing helpers applying RFC 9421 §2.2.9 to arbitrary tools (regardless of which `tag` or `adcp_use` string they coin) are operating outside this profile and are not 3.x-conformant; the only response-signing primitive the spec authorizes in 3.x is payload-envelope JWS on the designated-tasks list.
+The proposed creative addon support for 3.3 lets `get_creative_delivery` carry a signed `variants[].addon_chain` artifact under the dedicated [`adcp/addon-chain/v1` profile](/docs/creative/addon-chain-signing). Explicit human approval of this bounded artifact signature remains pending. Its proposed scope excludes signing the whole task response and using `response-signing` keys for addon evidence.
+
+Apart from that scoped addon artifact, any task not on the designated list MUST NOT sign its response under any signing primitive. General-purpose response-signing helpers applying RFC 9421 §2.2.9 to arbitrary tools (regardless of which `tag` or `adcp_use` string they coin) are operating outside this profile and are not 3.x-conformant; whole-response signing remains limited to payload-envelope JWS on the designated-tasks list.
The `adcp_use: "response-signing"` value is therefore reserved at the JWK layer for the payload-envelope primitive. **Keys published with `adcp_use: "response-signing"` MUST sign only payload-envelope JWS as defined in this section; using such a key to produce RFC 9421 §2.2.9 transport signatures is a profile violation regardless of the task being signed.** If a future major version scopes RFC 9421 transport response signing for any task, it MUST use a distinct `adcp_use` value (e.g., `"response-transport-signing"`) so verifiers can disambiguate the primitive from the JWK alone — the brand-protocol value cannot be retconned to cover both. List growth and additional primitives are normative decisions deferred to future spec versions.
@@ -2804,3 +2806,7 @@ function logSecurityEvent(eventType, details) {
- **Webhooks**: See [Webhooks](/docs/building/by-layer/L3/webhooks) for webhook security patterns
- **Error Handling**: See [Error Handling](/docs/building/by-layer/L3/error-handling) for authentication errors
- **Orchestrator Design**: See [Orchestrator Design](/docs/building/operating/orchestrator-design) for multi-tenant security
+
+## Creative addon object signatures (proposed 3.3)
+
+The proposed [addon-chain signing profile](/docs/creative/addon-chain-signing) uses dedicated Ed25519 keys resolved through the supported 3.x identity/JWKS infrastructure. Its use in 3.3 requires explicit human approval of the relationship to DR-0023. It changes no Web Bot Auth transport behavior. Do not treat an addon signature as verified without the profile’s key-authority, context, and retained-history checks.
diff --git a/docs/creative/addon-chain-signing.mdx b/docs/creative/addon-chain-signing.mdx
new file mode 100644
index 00000000000..cc8a78b22dc
--- /dev/null
+++ b/docs/creative/addon-chain-signing.mdx
@@ -0,0 +1,185 @@
+---
+title: Addon-chain signing
+description: Proposed AdCP 3.3 Ed25519 object-signature profile for seller-adjacent execution evidence.
+---
+
+
+This is the proposed `adcp/addon-chain/v1` profile for
+[#6700](https://github.com/adcontextprotocol/adcp/issues/6700).
+[DR-0023](https://github.com/adcontextprotocol/adcp/blob/main/governance/decisions/DR-0023-agent-identity-is-an-origin.md)
+keeps legacy discovery working through 3.x and schedules broader JWS document
+signing for 3.4. This bounded use in 3.3 requires explicit human approval and a
+recorded decision on that sequencing. No approval is implied by this draft.
+
+
+This profile signs the Sales Agent's assembly claim from
+[`get_creative_addons`](/docs/creative/task-reference/get_creative_addons).
+Its signature covers the ordered entries and actual effective configuration, bound
+to the unchanged creative and its delivery context. It does not change Web Bot
+Auth or substitute a transport signature for retained object evidence.
+
+## Encoding and hashes
+
+V1 uses SHA-256, encoded as `sha256:` followed by 64 lowercase hexadecimal digits.
+Every JSON hash input uses [RFC 8785 JCS](https://www.rfc-editor.org/rfc/rfc8785)
+and UTF-8 with no BOM. Producers and verifiers MUST reject duplicate object keys,
+invalid Unicode, non-finite numbers, or data outside JCS's supported domain.
+No Unicode, whitespace, newline, or URL normalization is performed on string
+values. Hashing and parsing MUST use the same input bytes.
+
+- `content_hash`: SHA-256 of the exact UTF-8 fragment `content` bytes. `bytes` is
+ their byte count, not the number of characters. The seller validates the hash
+ before assembly. Static references use the same rule.
+- `chain_hash`: SHA-256 of the JCS encoding of the complete `entries` array,
+ including order and every optional metadata field present. It excludes the
+ surrounding object and signature.
+- `configuration_hash`: SHA-256 of the JCS encoding of the complete
+ `configuration` object carried beside the entries.
+- `creative_hash`: SHA-256 of a format-specific JCS object defined below. It
+ identifies the selected buyer creative material immediately before addon
+ assembly, excluding the seller container and all addon fragments.
+
+| Format | Exact `creative_hash` input |
+| --- | --- |
+| `image` | `{ "format_kind": "image", "content_hash": H }`, where `H` hashes the exact selected image entity bytes before addon assembly, after transport content decoding and before any addon processing. A URL alone is insufficient. |
+| `html5` | `{ "format_kind": "html5", "content_type": T, "content_hash": H }`. `T` is `application/zip` for the exact accepted bundle archive bytes, or `text/html` for the exact UTF-8 buyer document bytes. `H` hashes those bytes. Do not re-zip, normalize markup, or hash the seller wrapper. A route that cannot retain the source bytes is unsupported. |
+| `display_tag` | `{ "format_kind": "display_tag", "asset": A }`, where `A` is the complete selected canonical display-tag asset object, including its `delivery_type` and every supplied field. Include both URLs for `paired_redirect`. Preserve macro tokens; hash before the existing authorized macro-resolution step. Do not parse or rewrite tag markup. |
+
+For remote images or bundles, the seller MUST retain the exact material associated
+with that execution so later URL changes cannot redefine it. Buyers verify the
+creative hash against the independently known selected material and source
+revision. If they cannot obtain that material, they may verify the seller signature
+but MUST NOT describe the creative-content binding as independently verified.
+A display-tag or addon loader hash commits to the tag/loader, not to code fetched
+later from a mutable URL. This profile does not make that remote code immutable.
+
+## JWS shape
+
+`signed_attestation` carries `profile: adcp/addon-chain/v1` and the flattened
+[JWS](https://www.rfc-editor.org/rfc/rfc7515) members `protected`, `payload`, and
+`signature`. All three use canonical, unpadded base64url. The profile member is
+AdCP metadata beside the flattened JWS; no unprotected JWS header is allowed.
+Detached payloads and `b64: false` are unsupported.
+
+The protected header has exactly these members:
+
+```json
+{
+ "$schema": "/schemas/core/creative-addon-chain.json#/definitions/ProtectedHeader", "alg": "EdDSA", "kid": "seller-addon-key", "typ": "adcp-addon-chain+jws" }
+```
+
+`EdDSA` means Ed25519 in v1. Verifiers MUST reject every other algorithm or type,
+unknown header members, and any embedded `jwk`, `jku`, or `x5u` trust source.
+The header need not be JCS-serialized; its transmitted bytes are signed verbatim.
+
+`payload` is base64url of the exact UTF-8 JCS encoding of the claims object.
+After decoding and parsing, a verifier MUST compare its JCS serialization to the
+decoded bytes. The signing input is ASCII
+`BASE64URL(protected-header-bytes) + "." + BASE64URL(payload-bytes)`.
+The 64-byte Ed25519 signature is base64url-encoded.
+
+The [claims schema](https://adcontextprotocol.org/schemas/latest/core/creative-addon-chain.json#/definitions/Claims)
+requires `typ`, `agent_url`, `publisher_domain`, `creative_id`, `creative_hash`,
+`chain_hash`, `configuration_hash`, `format_kind`, `variant_id`, `media_buy_id`,
+and integer NumericDate `iat`. Include `package_id`, `placement_id`, `revision_id`,
+and `locale_variant_id` whenever applicable to the associated reporting context.
+A verifier MUST compare both presence and value against trusted reporting context,
+including identifiers inherited from the containing creative or media-buy row.
+It MUST reject a missing, extra, or mismatched scoped identifier. Unknown claims
+are rejected in v1; extensions require a reviewed profile version.
+
+A chain cannot be moved between variants, revisions, media buys, or publishers.
+Any execution requiring additional scoping that v1 cannot unambiguously express
+is unsupported until a reviewed profile supplies it.
+
+## Signing authority
+
+V1 uses a dedicated public Ed25519 key conforming to the
+[addon signing-key schema](https://adcontextprotocol.org/schemas/latest/core/creative-addon-signing-key.json): `kty`
+`OKP`, `crv` `Ed25519`, `alg` `EdDSA`, `use` `sig`, `key_ops` `["verify"]`, and
+`adcp_use` `addon-chain-signing`, plus `kid` and the public `x` value. Public JWKs
+MUST NOT include private `d`. The key MUST NOT reuse transport-signing material.
+Introducing this purpose requires the signing-profile approval noted above.
+
+The verifier starts with the expected Sales Agent and publisher from its trusted
+transaction context, not from the attestation. It resolves the agent through the
+existing supported 3.x identity rules, including the selected
+`identity.brand_json_url` entry and that agent's JWKS. The signed `agent_url` must
+exactly match the selected agent identity under those rules. Preserve existing
+safe-fetch, endpoint-authorization, and key-refresh requirements; a presented
+attestation cannot select a replacement identity or arbitrary key URL.
+
+Where publisher pins apply, the signing public key MUST match both the authorized
+agent JWKS and the publisher's applicable pin by key material, using the existing
+[RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK-thumbprint comparison.
+A shared `kid` is insufficient. Conflicting duplicate key IDs or purpose metadata
+fail verification. The publisher is derived from trusted inventory/transaction
+context; the signed `publisher_domain` must agree. Pin absence follows the existing
+trust rules and MUST NOT be interpreted as permission to bypass a required pin.
+
+## Verification and retained history
+
+A verifier MUST:
+
+1. Validate the chain and decoded header/claims schemas. Enforce canonical encoding
+ and the profile identifier.
+2. Resolve authorized key material from trusted identity and publisher context.
+ Enforce dedicated purpose, algorithm, and revocation checks.
+3. Verify the JWS signature and exact canonical payload bytes.
+4. Recompute `chain_hash` and `configuration_hash`; compare every covered identity
+ to the reporting context and the creative hash to independently known material.
+5. Validate configuration coverage and ordering. Each applicable required vendor
+ has applied entries only; each applicable best-effort vendor has fragments or
+ exactly one omission. Entry identities, source, declared position, and static
+ hash agree with the indexed registration. No fragment body is exposed.
+
+`iat` is a nonnegative integer number of seconds since the Unix epoch. Reject an
+issuance more than 60 seconds in the future relative to the trusted verification
+clock. Historical chains do not expire solely because they are old; retention and
+key authority still govern whether they can be verified.
+
+Rotation adds a fresh `kid` and distinct key material. Sellers SHOULD overlap
+public keys through the reporting retention period and MUST retain verifiable key
+authority evidence for historical chains. A verifier may use a retained key only
+with a trusted record that it was authorized for this agent and publisher at the
+relevant time. Archiving a bare key or trusting a key embedded in a chain is
+insufficient. Do not reuse a `kid` for different material.
+
+Either the agent JWKS or the publisher pin can revoke authorization. Apply every applicable `revoked_at`, so the earliest revocation controls; conflicting purpose metadata also fails verification. Reject chains with `iat` at or after any applicable revocation. A compromised
+key could backdate `iat`, so an earlier timestamp alone cannot establish validity.
+Historical verification after revocation additionally requires a trusted record
+that the exact signed JWS was observed and verified before revocation, together
+with the then-valid agent authorization and applicable publisher pin. The
+observation time must be at or after `iat` and before revocation. Store a digest of
+the exact JWS and the covered chain/configuration alongside that evidence so it
+cannot be transplanted to another signature. A publisher's explicit compromise
+policy may invalidate earlier evidence as well; that policy takes precedence.
+
+Without sufficient historical authority or observation evidence, report the chain
+as unverifiable. Do not silently trust a removed key, an old cache without authority
+evidence, or a self-asserted signing time. Refreshing JWKS follows existing bounded
+refresh rules and does not permit redirecting discovery through the JWS.
+
+Missing evidence or any failed check MUST NOT be reported as a verified addon
+chain. An optional `detail_url` can expose authorized evidence but cannot override
+these checks. A signature is the seller's claim about assembly, not independent
+proof that JavaScript ran or that a measurement counted correctly.
+
+## Reproducible evidence and migration
+
+Published vectors live at
+`static/compliance/source/test-vectors/creative-addon-chain.json`. They cover each
+format, static/live sources, best-effort omission, and retained history. Negative
+vectors cover altered metadata, unauthorized key purposes, mismatched key pins,
+wrong delivery identities, noncanonical payloads, and revocation. The test key is
+public RFC 8032 sample material and cannot be used for an operational signer.
+
+Run `node scripts/generate-creative-addon-vectors.cjs --check` and
+`node --test tests/creative-addon-vectors.test.cjs` to reproduce the evidence.
+These executable reference checks do not replace the required seller/buyer partner
+integration against exact protocol versions.
+
+A later migration to the 3.4 document-signing framework requires explicit profile
+or version negotiation. Previously issued v1 chains retain their original
+interpretation and verification rules. This draft makes no change to DR-0023's
+transport identity or Web Bot Auth behavior.
diff --git a/docs/creative/task-reference/get_creative_addons.mdx b/docs/creative/task-reference/get_creative_addons.mdx
new file mode 100644
index 00000000000..89eca606948
--- /dev/null
+++ b/docs/creative/task-reference/get_creative_addons.mdx
@@ -0,0 +1,291 @@
+---
+title: Get Creative Addons
+description: Operator-registered fragments adjacent to an unchanged buyer creative, with pre-commit disclosure and signed execution evidence.
+---
+
+
+Proposed for AdCP 3.3 under [#6700](https://github.com/adcontextprotocol/adcp/issues/6700).
+This specification draft implements the proposed
+[decision addendum](https://github.com/adcontextprotocol/adcp/issues/6700#issuecomment-6179907162).
+Working Group approval, the signing-profile decision, and release readiness remain pending.
+
+
+`get_creative_addons` returns fragments to a Sales Agent that controls the adjacent
+execution container. The buyer creative remains the authoritative library asset.
+The Sales Agent MUST NOT rewrite it or replace it with the assembled container.
+Addon vendors MUST NOT receive its payload, including through `context`, `ext`,
+asset URLs, or diagnostics. Vendors that need the creative for evaluation use the
+existing creative-governance contract.
+
+The v1 formats are `image`, `html5`, and `display_tag`. An image executes within a
+seller container; an HTML5 bundle or document remains unchanged inside that
+container; a third-party display tag remains opaque and byte-preserved. A seller
+MUST NOT advertise a route unless it controls an adjacent container, can preserve
+the buyer asset, and can support any prerequisite required by its registered addons.
+Video and audio tracker insertion are outside this task.
+
+The addon operation occurs when materializing a delivery or trafficking
+representation. It is not an impression-time network call. One stored creative
+can therefore have different addon chains on different served variants.
+
+## Discovery and registration
+
+An addon vendor MUST declare `creative` in `supported_protocols`,
+`creative-addon` in `specialisms`, its `agent_name`, and the top-level
+`creative_addon` capability. The latter is separate from
+`creative.supported_formats`, which describes creative operations.
+
+```json
+{
+ "$schema": "/schemas/core/creative-addon-capability.json",
+ "supported_format_kinds": ["image", "html5", "display_tag"],
+ "positions": ["prepend", "append"],
+ "signed_chain": { "required": true, "profile": "adcp/addon-chain/v1" }
+}
+```
+
+A Sales Agent advertising assembly support also publishes `creative_addon` and
+`creative` protocol support. It claims the `creative-addon` vendor specialism only
+if it exposes the fragment task itself. The new obligations apply when participants
+negotiate AdCP 3.3 or later and addon support. An older or non-supporting participant
+MUST NOT be treated as acknowledging undisclosed addon configuration. Ordinary
+transactions outside this feature keep their existing contract.
+
+Registration is an operator action, outside buyer task inputs. The
+[registration schema](https://adcontextprotocol.org/schemas/latest/core/creative-addon-registration.json) describes the
+configuration; it creates no registration API. Each entry has exactly one
+`agent_url` or `static_reference`. A live registration's `agent_name` MUST match the
+vendor declaration after trimming surrounding whitespace and lowercasing. Names
+MUST be unique within a registration list after that normalization; URLs and
+normal agent authentication establish identity, not the label alone.
+
+`mode` is `required` or `best_effort`. Required entries MUST configure a positive,
+finite integer `max_hold_seconds`. Authentication stays operator-local. Credentials
+MUST NOT enter buyer disclosure or reporting. Removing a registration stops new
+calls on the next operation and does not erase past chains. A registration or
+policy change during a held operation requires re-resolution against the seller's
+current policy; it cannot extend the original deadline or silently remove a
+required obligation from that operation.
+
+A static registration supplies a fixed `content` and `position`, with supported
+`format_kinds` and optional `detail_url`. It makes no vendor call. The Sales Agent
+computes its content hash and applies the same validation and signing obligations
+as for live fragments. Seller first-party measurement loaders use this same
+registration contract. A chain attributes the seller's assembly claim; it does not
+assign counting authority, which remains governed by `billing_measurement`.
+Disclosure of broader trackers is coordinated through
+[#6878](https://github.com/adcontextprotocol/adcp/issues/6878).
+
+## Request and response
+
+The Sales Agent MUST send one batch per vendor for all creative contexts sharing
+an operation and placement. It MUST exclude undeclared format kinds before calling.
+Separate placement contexts require separate batches. `operator_url` is verified
+through normal agent authentication; `operator_name` is an informational label.
+`account_id`, if present, is the vendor-assigned account, not a buyer execution
+account. Duplicate `creative_id` values within a batch are invalid.
+
+```json
+{
+ "$schema": "/schemas/creative/get-creative-addons-request.json",
+ "adcp_version": "3.3",
+ "idempotency_key": "addon-operation-0001",
+ "operator_name": "Example Sales",
+ "operator_url": "https://sales.example/agent",
+ "publisher_domain": "publisher.example",
+ "placement": { "property_id": "homepage", "product_id": "display" },
+ "creatives": [
+ { "creative_id": "creative-1", "format_kind": "image", "dimensions": { "w": 300, "h": 250 } }
+ ]
+}
+```
+
+The vendor returns `addons`, whose items follow the
+[fragment schema](https://adcontextprotocol.org/schemas/latest/core/creative-addon-fragment.json). Each contains a
+requested `creative_id`, a declared `position`, exact UTF-8 `content`, and its
+`content_hash`. Optional `vendor_ref` is an opaque policy/version reference.
+Optional `detail_url` MUST use HTTPS and enforce access control before exposing
+nonpublic evidence. It confers no right to retrieve fragment source.
+
+```json
+{
+ "$schema": "/schemas/creative/get-creative-addons-response.json", "status": "completed", "adcp_version": "3.3", "addons": [] }
+```
+
+An empty array is a valid vendor response. For a best-effort registration it means
+a deliberate omission; for a required registration it fails the seller requirement.
+Vendors cannot exempt themselves from required mode with a `not_applicable` result.
+The vendor task is synchronous in v1. Terminal vendor errors use `status: failed`
+and `errors`, without `addons`, following the existing transport error mapping.
+Async holds belong to the invoking seller task.
+
+The new task requires `idempotency_key` and follows the protocol's idempotency
+contract. A seller operation may contain several vendor evaluation attempts.
+The vendor key identifies one evaluation attempt; the seller's existing `task_id`
+and original required-hold deadlines identify the enclosing operation.
+
+An ambiguous transport outcome or an in-flight evaluation MUST be retried with the
+same key and identical payload, including `prior_failures` and `context`. The
+seller MUST NOT use a new key to bypass an unresolved attempt. Once that attempt
+has definitively completed with an empty, failed, or seller-invalidated response,
+the seller MAY start a fresh evaluation with a new key if the original hold still
+permits recovery. That new request can carry the prior attempt's failure feedback.
+It retains the same seller task and original per-vendor deadline. A key MUST NOT
+be reused with new feedback or other changed request content.
+
+For example, an empty required result is recorded under vendor key A. A new
+attempt under key B can report that failure and obtain repaired fragments while
+the seller task remains working. A timeout of attempt B retries B unchanged; it
+does not start C until B's outcome is resolved. At the deadline the seller task
+fails even if a vendor attempt remains unresolved; a late response cannot resume
+the failed task or authorize delivery.
+
+A distinct addon operation uses fresh vendor keys. Exact replays of one attempt
+reuse its recorded outcome; live fragments MUST NOT be cached across distinct
+operations. The seller MUST avoid copying buyer correlation data into vendor
+context; it supplies vendor-safe correlation and receives that `context` echoed
+unchanged.
+
+## Validation, ordering, and failure
+
+The Sales Agent MUST verify each fragment's hash before placement and validate
+membership in the request batch, the declared format, and allowed position. It
+also checks compatibility with its execution container. A malformed response or
+unknown creative identity fails the vendor batch. An invalid fragment attributable
+to a known creative discards all of that vendor's fragments for that creative;
+valid fragments from unrelated creative contexts can remain eligible. The seller
+MUST NOT partially apply a vendor's response to an affected creative.
+
+For each creative, final placement is all `prepend` fragments in registration
+order, the unchanged buyer creative, then all `append` fragments in registration
+order. Fragments from the same vendor and position preserve response order.
+`fragment_index` is the zero-based index within that vendor's fragments for this
+creative before sorting by position. Indices MUST be unique and contiguous from zero for each applied vendor/creative set, even when fragments occupy different positions. Static references use index zero.
+
+Placement order does not promise completion order of asynchronous scripts. If a
+required addon needs a prerequisite before creative invocation, the seller MUST
+provide that prerequisite on the advertised route, or mark the route unsupported.
+This task defines no general dependency scheduler.
+
+| Failure | Classification |
+| --- | --- |
+| [`ADDON_UNREACHABLE`](/docs/building/verification/compliance-catalog#error-code-addon-unreachable) | Connection failure or call timeout |
+| [`ADDON_AUTH_FAILED`](/docs/building/verification/compliance-catalog#error-code-addon-auth-failed) | Vendor rejected operator credentials |
+| [`ADDON_HASH_MISMATCH`](/docs/building/verification/compliance-catalog#error-code-addon-hash-mismatch) | Content bytes do not match the declared hash |
+| [`ADDON_INVALID_POSITION`](/docs/building/verification/compliance-catalog#error-code-addon-invalid-position) | Unsupported or undeclared relative position |
+| [`ADDON_UNDECLARED_FORMAT`](/docs/building/verification/compliance-catalog#error-code-addon-undeclared-format) | Undeclared format or unknown creative identity |
+| [`ADDON_MALFORMED_FRAGMENT`](/docs/building/verification/compliance-catalog#error-code-addon-malformed-fragment) | Invalid container content or a required vendor supplied no valid fragment |
+
+For `best_effort`, the seller proceeds and records one omission for the affected
+registration, with `reason: vendor_omission` for a valid empty response or
+`reason: failure` and `error_code` for a failure. These states MUST remain distinct.
+
+For `required`, the affected execution MUST NOT be trafficked or delivered without
+the required fragments. If its invoking task supports async execution, it uses
+that task's existing `submitted`/`working` flow and stable `task_id`. The deadline is
+the first failed attempt time plus `max_hold_seconds`; retries, restarts, and policy
+re-resolution MUST NOT extend it. If multiple required vendors fail, each retains
+its own original deadline; expiry of any unresolved requirement fails the affected
+execution. Successful recovery before all applicable deadlines resumes the same
+operation. Expiry, or an invoking task without async support, returns the normal
+terminal error arm with [`ADDON_REQUIRED_UNAVAILABLE`](/docs/building/verification/compliance-catalog#error-code-addon-required-unavailable) and `recovery: terminal`.
+
+The error SHOULD identify affected `creative_ids` and the disclosed `agent_name`
+in structured details, without credential or internal diagnostic leakage. The
+buyer creative's lifecycle MUST remain unchanged. The seller MUST NOT silently
+downgrade required mode. Buyers follow task status while nonterminal; later retries
+use the invoking task's existing idempotency contract and grant no policy override.
+
+The operator MAY impose vendor-count, fragment-size, and latency limits. Exceeding
+a local limit follows the same required/best-effort behavior. Silent truncation is
+prohibited. V1 declares no universal numerical cap or `max_latency_ms` capability.
+
+### Vendor failure feedback
+
+`prior_failures` carries only the receiving vendor's pending failures using the
+[failure schema](https://adcontextprotocol.org/schemas/latest/core/creative-addon-failure.json). The seller clears an
+echoed batch only after receiving a successful task response that acknowledges
+receipt by completing that call. A failed or ambiguous call retains the entries;
+vendors MUST tolerate duplicate feedback. Optional `notification_url` receives
+`{ "prior_failures": [...] }` POST bodies using the same entry shape and configured
+operator authentication. A successful 2xx notification acknowledges that batch;
+failed pushes retain it for retry or the next task call. No separate AdCP task is
+introduced. URLs follow existing outbound safe-fetch and credential-scoping rules.
+
+## Disclosure before commitment
+
+`creative_addons` on [`get_products`](/docs/media-buy/task-reference/get_products) product rows describes known product scope;
+canonical [`list_products`](/docs/media-buy/task-reference/list_products) rows expose the same field for direct purchase.
+[`sync_accounts`](/docs/accounts/tasks/sync_accounts) may return account-scoped configuration on each successful account
+row. A failed row MUST NOT present configuration as resolved. Account scope inherits
+the identity of that row and does not imply completeness for a particular product.
+The account `revision` is not an addon acknowledgment token.
+
+A disclosure states `scope`, `complete`, and its ordered `vendors`, with the signing
+profile. Omission means undeclared. Only `complete: true` with an empty `vendors`
+array means an explicitly empty set. Each vendor discloses its normalized identity,
+source, mode, declared formats and positions, and required hold period if applicable.
+Static references also disclose their fixed `static_content_hash`, without content.
+The seller resolves account and product rules into one complete product-scoped
+snapshot; the buyer MUST NOT concatenate partial disclosures or infer precedence.
+
+Before spend commitment the seller MUST establish that the complete configuration
+seen by the buyer still applies. V1 uses these existing binding mechanisms:
+
+| Purchase path | Binding and freshness rule |
+| --- | --- |
+| [`request_proposals`](/docs/media-buy/task-reference/request_proposals) / [`refine_proposals`](/docs/media-buy/task-reference/refine_proposals) → [`accept_proposal`](/docs/media-buy/task-reference/accept_proposal) | Each `commercial_terms.purchases[]` carries its complete resolved `creative_addons`. Existing `terms_digest` covers this field. At finalize and atomically at acceptance, the seller compares the snapshot with current effective configuration. A change requires a successor proposal; return [`TERMS_REJECTED`](/docs/building/verification/compliance-catalog#error-code-terms-rejected) before commitment. |
+| `list_products` → [`buy_products`](/docs/media-buy/task-reference/buy_products) | The returned product feed must be resolved for the authenticated account and include complete product disclosures. The existing `feed_version` identifies that immutable account-scoped view and changes whenever an effective addon rule changes. At purchase, the seller checks that version and current configuration atomically, records the exact disclosure in the accepted proposal, and rejects stale terms with `TERMS_REJECTED`. A global or partial feed that cannot bind account-specific configuration cannot support this route. |
+| Legacy [`create_media_buy`](/docs/media-buy/task-reference/create_media_buy) / [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy), or any path lacking either binding | Addon-enabled spend commitments return [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature) before mutation. The caller can use a supported canonical path. A future binding needs explicit schema and WG review before the path is advertised. |
+
+This adds no buyer-echoed configuration token. `get_products` or `sync_accounts`
+can inform compatibility, but a successful read alone does not prove freshness at
+commitment. The check, accepted snapshot, and commitment MUST be atomic with respect
+to configuration changes. The same rule applies to amendments that add or change
+addon-enabled delivery obligations. Non-spend operations and cancellation retain
+their existing lifecycle contracts.
+
+A buyer can decline a transaction before commitment. No request may disable,
+substitute, or reorder a seller-required addon.
+
+### Changes after commitment
+
+The accepted snapshot records the configuration disclosed at purchase. Publisher
+authority continues after purchase: the seller MAY change its execution
+configuration after commitment. It MUST retain the
+accepted snapshot and each actual execution's later configuration; a new
+configuration cannot overwrite historical evidence or create a runtime buyer veto.
+Each changed execution configuration is reported on a distinct immutable variant
+with its own signed chain, as specified below. The purchase binding establishes
+freshness at commitment; it does not promise the same addon set for the entire buy.
+Code deployed outside this addon mechanism remains outside its completeness claim.
+
+## Signed delivery reporting
+
+Every execution represented by negotiated addon support carries an `addon_chain`
+on its [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery) variant, including an empty configured set,
+static-only chains, and best-effort omissions. The reporting row MUST identify the
+source creative and its `media_buy_id`. A change in applied content, omissions, or
+effective configuration creates a distinct immutable `variant_id`; reporting MUST
+NOT aggregate different chains under one variant. The chain is never written back
+to [`list_creatives`](/docs/creative/task-reference/list_creatives) or the buyer's creative asset.
+
+The chain contains the actual complete configuration and ordered applied/omitted
+entries. Applied entries expose their fragment hash and UTF-8 byte count. Fragment
+bodies and credentials MUST NOT appear. Entries include their `registration_index`
+so a verifier can match them to the disclosed configuration. Applied entries appear
+first in deterministic placement order; omitted entries follow in registration
+order. `order` is each entry's zero-based index in that serialized list, not a
+claim that an omitted fragment executed.
+
+Every applicable registered vendor MUST have either its applied fragments or one
+omission; required vendors cannot be omitted on a delivered execution. Each entry's
+identity and source MUST match its registration. Vendors whose declared formats do
+not cover the creative are outside that execution's applicable set.
+
+The [addon-chain signing profile](/docs/creative/addon-chain-signing) defines the
+exact hash and trust checks. The signature attributes the Sales Agent's assembly
+claim. It does not independently prove browser execution or measurement accuracy.
+Retain chains and the verification evidence for the existing reporting/audit
+retention period. Independent vendor signatures and a separate audit retrieval
+task are deferred.
diff --git a/docs/creative/task-reference/get_creative_delivery.mdx b/docs/creative/task-reference/get_creative_delivery.mdx
index 354b0ce9687..aeb90e1f3e3 100644
--- a/docs/creative/task-reference/get_creative_delivery.mdx
+++ b/docs/creative/task-reference/get_creative_delivery.mdx
@@ -493,3 +493,7 @@ Agents that support this task expose `get_creative_delivery` in their tool surfa
```
Buyers discover candidate creative agents through the registry, then confirm the live endpoint's tool and `get_adcp_capabilities` surfaces. This applies to any endpoint implementing the Creative Protocol, including a sales agent with creative capabilities.
+
+## Creative addon evidence (proposed 3.3)
+
+When addon support is negotiated, each served variant carries its immutable `addon_chain`, including the effective configuration and mandatory Sales Agent signature. Different chains must not be aggregated under one variant. Fragment bodies remain private. See [signed delivery reporting](/docs/creative/task-reference/get_creative_addons#signed-delivery-reporting) and the [signing profile](/docs/creative/addon-chain-signing).
diff --git a/docs/media-buy/task-reference/accept_proposal.mdx b/docs/media-buy/task-reference/accept_proposal.mdx
index 32d8050c483..45ca2ec91bf 100644
--- a/docs/media-buy/task-reference/accept_proposal.mdx
+++ b/docs/media-buy/task-reference/accept_proposal.mdx
@@ -59,3 +59,6 @@ Under `inventory_hold` semantics (the default), an unexpired committed proposal
Experimental in AdCP 3.3. A committed `media_buy_update` or `media_buy_cancellation` proposal with `proposal_initiator: "seller"` is a seller offer against an accepted buy, read through [`list_proposals`](/docs/media-buy/task-reference/list_proposals). Accept it exactly like any committed proposal, with its `proposal_terms_digest`. Acceptance applies the amendment or negotiated cancellation to the buy named by `media_buy_id`. If the buy's `revision` has moved past the offer's `base_media_buy_revision`, the base-revision check fails and nothing is applied; `list_proposals` already reports such an offer `withdrawn`, and accepting a `withdrawn` snapshot returns [`INVALID_STATE`](/docs/building/verification/compliance-catalog#error-code-invalid-state). After `expires_at` the offer's disposition is `expired`, and `accept_proposal` returns [`PROPOSAL_EXPIRED`](/docs/building/verification/compliance-catalog#error-code-proposal-expired); a lapse is not a decline, and the signed terms stand. See [Seller offers](/docs/media-buy/product-discovery/proposal-negotiation#seller-offers).
+## Creative addon commitments (proposed 3.3)
+
+For negotiated addon support, `commercial_terms.purchases[].creative_addons` contains the complete resolved configuration and is covered by `terms_digest`. Finalization and acceptance must reject stale configuration before commitment. See [the complete binding rules](/docs/creative/task-reference/get_creative_addons#disclosure-before-commitment).
diff --git a/docs/media-buy/task-reference/buy_products.mdx b/docs/media-buy/task-reference/buy_products.mdx
index 982a3a5652d..e65a01f24f4 100644
--- a/docs/media-buy/task-reference/buy_products.mdx
+++ b/docs/media-buy/task-reference/buy_products.mdx
@@ -75,3 +75,7 @@ A completed purchase MAY include `warnings[]` for non-blocking observations at c
A submitted arm MAY carry `expected_confirmation_at` (added in 3.3) when confirmation depends on a scheduled event such as a booking round or manual review window; buyers SHOULD schedule a status check for that time instead of polling tightly. It is an estimate, not a commitment. See [Expected confirmation time](/docs/media-buy/task-reference/create_media_buy#expected-confirmation-time).
The accepted snapshot records the published terms the buyer purchased. It does not imply that every direct product is non-guaranteed: discovery mode, buyer composition, and delivery guarantee are independent dimensions.
+
+## Creative addon commitments (proposed 3.3)
+
+Addon-enabled direct purchases require a complete account-resolved disclosure in the selected product feed. The existing `feed_version` must change whenever the effective addon configuration changes. The seller atomically validates freshness and retains the exact disclosure in the accepted commercial snapshot. See [the complete binding rules](/docs/creative/task-reference/get_creative_addons#disclosure-before-commitment).
diff --git a/docs/media-buy/task-reference/get_products.mdx b/docs/media-buy/task-reference/get_products.mdx
index aba73f5a85b..e9ec387e0ab 100644
--- a/docs/media-buy/task-reference/get_products.mdx
+++ b/docs/media-buy/task-reference/get_products.mdx
@@ -2217,3 +2217,7 @@ After discovering products:
- [Pricing Models](/docs/media-buy/advanced-topics/pricing-models) - CPM, CPCV, CPP explained
- [Brief Expectations](/docs/media-buy/product-discovery/brief-expectations) - How to write effective briefs
- [Media Products](/docs/media-buy/product-discovery/media-products) - Product structure and fields
+
+## Creative addon disclosure (proposed 3.3)
+
+A product may disclose known seller-side addons in `creative_addons`. Omission means undeclared; a complete empty vendor list explicitly declares none. Product and account information must resolve to a complete, current snapshot before commitment. See [disclosure and purchase bindings](/docs/creative/task-reference/get_creative_addons#disclosure-before-commitment).
diff --git a/docs/protocol/get_adcp_capabilities.mdx b/docs/protocol/get_adcp_capabilities.mdx
index 0fde8aef1f5..6fe490a991e 100644
--- a/docs/protocol/get_adcp_capabilities.mdx
+++ b/docs/protocol/get_adcp_capabilities.mdx
@@ -2602,3 +2602,7 @@ After discovering capabilities:
- [adagents.json Specification](/docs/governance/property/adagents) - Publisher authorization files
- [Product Filters](/docs/media-buy/task-reference/get_products#filters) - Capability-aware filtering
- [Content Standards](/docs/governance/content-standards) - Brand safety configuration
+
+## Creative addon capability (proposed 3.3)
+
+The top-level `creative_addon` block declares supported adjacent-execution formats and positions, with mandatory `adcp/addon-chain/v1` signing. Vendors also declare `agent_name` and the `creative-addon` specialism. See [get_creative_addons](/docs/creative/task-reference/get_creative_addons#discovery-and-registration).
diff --git a/docs/reference/test-vectors/index.mdx b/docs/reference/test-vectors/index.mdx
index 1eaf3ea51ef..0062fdaa29a 100644
--- a/docs/reference/test-vectors/index.mdx
+++ b/docs/reference/test-vectors/index.mdx
@@ -28,6 +28,7 @@ SDKs SHOULD fetch versioned paths where available and record the version under t
| Set | What it pins | Source | CDN |
|---|---|---|---|
+| [`creative-addon-chain`](https://github.com/adcontextprotocol/adcp/blob/main/static/compliance/source/test-vectors/creative-addon-chain.json) | Proposed 3.3 `adcp/addon-chain/v1` Ed25519/JCS signatures and seller assembly evidence, including revocation and bounded recovery cases. WG approval remains pending. | `static/compliance/source/test-vectors/creative-addon-chain.json` | `/compliance/latest/test-vectors/creative-addon-chain.json` |
| [`supply-path`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/supply-path) | Fail-closed owner-sold supply-path states, leg diagnostics, collection/property scope, revocations and applicable IAB evidence | `static/compliance/source/test-vectors/supply-path/` | `/compliance/latest/test-vectors/supply-path/` |
| [`reporting-summary`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/reporting-summary) | Future expectations in complete reporting summaries, scope guards, timestamp formats, and unchanged view and health constraints (next 3.2 RC) | `static/compliance/source/test-vectors/reporting-summary/` | `/compliance/latest/test-vectors/reporting-summary/` |
| [`request-signing`](https://github.com/adcontextprotocol/adcp/tree/main/static/compliance/source/test-vectors/request-signing) | RFC 9421 request-signing profile: canonical signature base, covered components, signature params, tag namespace, alg allowlist, `adcp_use` discriminator, replay dedup, revocation, content-digest semantics, and URL canonicalization | `static/compliance/source/test-vectors/request-signing/` | `/compliance/latest/test-vectors/request-signing/` |
diff --git a/docs/snippets/compliance-error-codes.mdx b/docs/snippets/compliance-error-codes.mdx
index b2ae494db13..3dc413dbf7a 100644
--- a/docs/snippets/compliance-error-codes.mdx
+++ b/docs/snippets/compliance-error-codes.mdx
@@ -22,6 +22,13 @@ description: "Canonical AdCP error codes with recovery classifications, remediat
| `ACCOUNT_SETUP_REQUIRED` | correctable | check details.setup for URL or instructions |
| `ACCOUNT_SUSPENDED` | terminal | contact seller to resolve suspension |
| `ACTION_NOT_ALLOWED` | correctable | branch on error.details.reason: for wrong_status, wait for or transition to a status listed under the action's allowed_statuses; for mode_mismatch, this is a flow switch (not a retry against update_media_buy) — follow the mode named in available_actions[<action>].mode; for condition_unresolved, wait until the seller advertises the action after resolving its opaque condition; for not_supported_on_product or not_supported_on_buy, do not retry — the action is unavailable on this buy and buyer must select a different product or renegotiate, using error.details.decline_reason when present to inform that renegotiation |
+| `ADDON_AUTH_FAILED` | terminal | operator repairs vendor credentials; buyer credentials are unchanged |
+| `ADDON_HASH_MISMATCH` | terminal | discard affected fragments and have the vendor correct the content hash |
+| `ADDON_INVALID_POSITION` | terminal | vendor corrects the position to one declared for this route |
+| `ADDON_MALFORMED_FRAGMENT` | terminal | operator or vendor repairs the required fragment integration |
+| `ADDON_REQUIRED_UNAVAILABLE` | terminal | surface the seller infrastructure failure; a later user-directed retry follows the invoking task idempotency contract |
+| `ADDON_UNDECLARED_FORMAT` | terminal | vendor corrects the format or creative identity in the response |
+| `ADDON_UNREACHABLE` | transient | seller retries within the original finite hold or records best-effort omission |
| `AGENT_BLOCKED` | terminal | surface to a human at the buyer — the relationship is permanently denied and is reinstated only through offline operator action with the seller, not via any seller-callable AdCP task |
| `AGENT_SUSPENDED` | terminal | surface to a human at the buyer — the agent cannot unilaterally lift a suspension; re-onboarding with the seller offline may resolve |
| `AMBIGUOUS_BIDDING_POLICY` | correctable | remove either the canonical bidding block or all legacy bidding fields from the effective package |
@@ -229,6 +236,69 @@ Account has been suspended. Recovery: terminal (contact seller to resolve suspen
The requested mutation maps to an action that is not currently available on this media buy. Sellers MUST populate {"error.details"} with {"attempted_action"} (the structured available-action identifier the request maps to), {"reason"} (an {"action-not-allowed-reason"} value: {"wrong_status"}, {"not_supported_on_product"}, {"not_supported_on_buy"}, {"mode_mismatch"}, or {"condition_unresolved"}), and {"currently_available_actions"} (echo of the buy's resolved {"available_actions[]"} so the buyer SDK can offer recovery without a separate get_media_buys round-trip). For {"not_supported_on_buy"}, sellers SHOULD also populate {"decline_reason"} from {"seller-policy-decline-reason"} when they have a structured reason and disclosure does not expose private thresholds or enforcement controls. {"decline_reason"} SHOULD be omitted for structural reasons. All producer-authored explanatory text, including message, suggestion, and extension detail values, MUST remain buyer-safe and MUST NOT contain vendor identifiers, internal object names, internal IDs, stack traces, or other producer-private implementation details. Recovery: correctable when {"reason"} is {"wrong_status"} (wait for or transition to an allowed status), {"mode_mismatch"} (re-issue through the appropriate flow), or {"condition_unresolved"} (wait until the seller can resolve the condition and advertises the action). Terminal-for-this-buy when {"reason"} is {"not_supported_on_product"} or {"not_supported_on_buy"} — buyers select a different product or renegotiate buy terms.
+
+
+
+
+
+**Suggested action:** operator repairs vendor credentials; buyer credentials are unchanged
+
+Addon vendor rejected operator credentials. Recovery: terminal (operator repairs vendor credentials; buyer credentials are unchanged).
+
+
+
+
+
+
+**Suggested action:** discard affected fragments and have the vendor correct the content hash
+
+Fragment content_hash differs from SHA-256 of the exact UTF-8 content. Discard all this vendor's fragments for the affected creative. Recovery: terminal (vendor must correct the fragment).
+
+
+
+
+
+
+**Suggested action:** vendor corrects the position to one declared for this route
+
+Fragment position is unsupported by the contract or the vendor declaration. Recovery: terminal (vendor must correct the response).
+
+
+
+
+
+
+**Suggested action:** operator or vendor repairs the required fragment integration
+
+Fragment cannot be safely assembled in the declared seller container, or a required vendor supplied no valid fragment. Recovery: terminal (operator or vendor repairs the integration).
+
+
+
+
+
+
+**Suggested action:** surface the seller infrastructure failure; a later user-directed retry follows the invoking task idempotency contract
+
+Seller-required addon could not be applied within the finite original hold deadline, or the invoking task cannot hold asynchronously. The affected execution MUST NOT deliver; buyer creative lifecycle is unchanged. Use the invoking task's normal terminal error arm. Recovery: terminal (surface seller infrastructure failure; a later user-directed retry follows the original task's idempotency contract).
+
+
+
+
+
+
+**Suggested action:** vendor corrects the format or creative identity in the response
+
+Fragment targets an undeclared format or unknown creative context. Recovery: terminal (vendor must correct the response).
+
+
+
+
+
+
+**Suggested action:** seller retries within the original finite hold or records best-effort omission
+
+The addon vendor could not be reached before the call timeout. Recovery: transient (seller follows the finite required hold or records best-effort omission).
+
diff --git a/package.json b/package.json
index ada67a9aa4f..bbe155709fe 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"verify:schema-discovery": "node scripts/verify-schema-discovery.mjs",
"typecheck": "tsc --project server/tsconfig.json --noEmit && npm run typecheck:fixed-trace-rollout-tests",
"typecheck:fixed-trace-rollout-tests": "node scripts/typecheck-fixed-trace-rollout-tests.mjs",
- "test:schemas": "node tests/schema-validation.test.cjs && node --test tests/seller-offers-contract.test.cjs tests/service-commitments.test.cjs tests/sync-platform-id.test.cjs tests/assignment-landing-page.test.cjs tests/assignment-trackers.test.cjs tests/assignment-replace.test.cjs tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/catalog-ingestion-capabilities.test.cjs tests/sdk-implementation-metadata.test.cjs tests/seller-optimized-package-controls.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/terminal-package-retention-storyboard.test.cjs tests/total-budget-redistribution-currency.test.cjs tests/dooh-allocation.test.cjs tests/x-added-in-3-3-sweep.test.cjs tests/performance-standard-metric-id.test.cjs tests/identity-absence-coherence.test.cjs tests/match-keys.test.cjs tests/execution-requirements.test.cjs tests/execution-readiness.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/price-adjustments-vectors.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/creative-start-policy.test.cjs tests/canonical-forecast-point-parity.test.cjs tests/creative-revisions.test.cjs tests/creative-live-updates.test.cjs tests/creative-delivery-contracts.test.cjs tests/tracker-execution-contracts.test.cjs tests/tracker-execution-package-integration.test.cjs tests/metric-identity-coherence.test.cjs tests/print-metrics.test.cjs tests/installment-lifecycle-reporting.test.cjs tests/rights-channel-restrictions.test.cjs tests/daypart-time-of-day.test.cjs tests/audience-size-range.test.cjs tests/media-buy-lineage.test.cjs tests/delivery-projection.test.cjs tests/viewable-rate-optimization-goal.test.cjs tests/sort-contract-delivery-reporting.test.cjs tests/time-based-views-contract.test.cjs tests/metric-qualifier-parity.test.cjs tests/collection-mechanism.test.cjs tests/requested-metrics-contract.test.cjs tests/auto-breakdown-negotiation-contract.test.cjs tests/format-delivery-reporting-contract.test.cjs tests/inventory-delivery-reporting-contract.test.cjs tests/property-delivery-conformance.test.cjs tests/lint-schema-enum-drift.test.cjs tests/lint-schema-required-declared.test.cjs tests/format-asset-type-parity.test.cjs tests/canonical-manifest-constructibility.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs tests/buyer-reason.test.cjs tests/mutation-outcome.test.cjs tests/error-recovery-vectors.test.cjs tests/reporting-status-contract.test.cjs tests/reporting-reconciliation-fixture.test.cjs tests/reporting-core-fixture.test.cjs tests/reporting-native-version-ref-bounds.test.cjs tests/trust-json-v1.test.cjs tests/refine-proposals-expected-confirmation-at.test.cjs tests/structured-package-refine.test.cjs tests/hold-policy.test.cjs tests/hold-policy-vectors.test.cjs tests/canonical-installments.test.cjs tests/withdrawn-to-4-0.test.cjs tests/collection-catalog.test.cjs tests/collection-copies-vectors.test.cjs tests/account-change-record.test.cjs tests/notification-config-payload-view.test.cjs tests/account-requests.test.cjs tests/reporting-subscriptions.test.cjs tests/reporting-subscription-probe.test.cjs tests/reporting-subscription-coverage-vectors.test.cjs tests/reporting-subscription-coverage-properties.test.cjs tests/product-roster-coverage-vectors.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting",
+ "test:schemas": "node tests/schema-validation.test.cjs && node --test tests/seller-offers-contract.test.cjs tests/service-commitments.test.cjs tests/sync-platform-id.test.cjs tests/assignment-landing-page.test.cjs tests/assignment-trackers.test.cjs tests/assignment-replace.test.cjs tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/catalog-ingestion-capabilities.test.cjs tests/sdk-implementation-metadata.test.cjs tests/seller-optimized-package-controls.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/terminal-package-retention-storyboard.test.cjs tests/total-budget-redistribution-currency.test.cjs tests/dooh-allocation.test.cjs tests/x-added-in-3-3-sweep.test.cjs tests/performance-standard-metric-id.test.cjs tests/identity-absence-coherence.test.cjs tests/match-keys.test.cjs tests/execution-requirements.test.cjs tests/execution-readiness.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/price-adjustments-vectors.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/creative-start-policy.test.cjs tests/canonical-forecast-point-parity.test.cjs tests/creative-revisions.test.cjs tests/creative-live-updates.test.cjs tests/creative-delivery-contracts.test.cjs tests/creative-addons.test.cjs tests/creative-addon-vectors.test.cjs tests/tracker-execution-contracts.test.cjs tests/tracker-execution-package-integration.test.cjs tests/metric-identity-coherence.test.cjs tests/print-metrics.test.cjs tests/installment-lifecycle-reporting.test.cjs tests/rights-channel-restrictions.test.cjs tests/daypart-time-of-day.test.cjs tests/audience-size-range.test.cjs tests/media-buy-lineage.test.cjs tests/delivery-projection.test.cjs tests/viewable-rate-optimization-goal.test.cjs tests/sort-contract-delivery-reporting.test.cjs tests/time-based-views-contract.test.cjs tests/metric-qualifier-parity.test.cjs tests/collection-mechanism.test.cjs tests/requested-metrics-contract.test.cjs tests/auto-breakdown-negotiation-contract.test.cjs tests/format-delivery-reporting-contract.test.cjs tests/inventory-delivery-reporting-contract.test.cjs tests/property-delivery-conformance.test.cjs tests/lint-schema-enum-drift.test.cjs tests/lint-schema-required-declared.test.cjs tests/format-asset-type-parity.test.cjs tests/canonical-manifest-constructibility.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs tests/buyer-reason.test.cjs tests/mutation-outcome.test.cjs tests/error-recovery-vectors.test.cjs tests/reporting-status-contract.test.cjs tests/reporting-reconciliation-fixture.test.cjs tests/reporting-core-fixture.test.cjs tests/reporting-native-version-ref-bounds.test.cjs tests/trust-json-v1.test.cjs tests/refine-proposals-expected-confirmation-at.test.cjs tests/structured-package-refine.test.cjs tests/hold-policy.test.cjs tests/hold-policy-vectors.test.cjs tests/canonical-installments.test.cjs tests/withdrawn-to-4-0.test.cjs tests/collection-catalog.test.cjs tests/collection-copies-vectors.test.cjs tests/account-change-record.test.cjs tests/notification-config-payload-view.test.cjs tests/account-requests.test.cjs tests/reporting-subscriptions.test.cjs tests/reporting-subscription-probe.test.cjs tests/reporting-subscription-coverage-vectors.test.cjs tests/reporting-subscription-coverage-properties.test.cjs tests/product-roster-coverage-vectors.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting",
"test:performance-feedback": "node --test --test-force-exit --test-timeout=30000 tests/performance-feedback-contract.test.cjs",
"test:dist-schema-version-ids": "node --test --test-force-exit --test-timeout=30000 tests/dist-schema-version-ids.test.cjs",
"test:examples": "node tests/example-validation-simple.test.cjs && npm run test:tmp-context-merge",
diff --git a/scripts/build-schemas.cjs b/scripts/build-schemas.cjs
index 810d0e47a86..7bbf850e766 100644
--- a/scripts/build-schemas.cjs
+++ b/scripts/build-schemas.cjs
@@ -125,7 +125,7 @@ const MCP_ROLE_PROFILE_LINE_TOOLS = {
'3.3.0': ['list_proposals', 'report_media_buy_issue', 'get_media_buy_issue', 'request_account'],
},
creative: {
- '3.3.0': ['request_account'],
+ '3.3.0': ['request_account', 'get_creative_addons'],
},
};
diff --git a/scripts/generate-creative-addon-vectors.cjs b/scripts/generate-creative-addon-vectors.cjs
new file mode 100644
index 00000000000..e134e8cb8de
--- /dev/null
+++ b/scripts/generate-creative-addon-vectors.cjs
@@ -0,0 +1,131 @@
+#!/usr/bin/env node
+// Deterministic public conformance data. RFC 8032 test key; NEVER use for production.
+const fs = require('node:fs');
+const path = require('node:path');
+const crypto = require('node:crypto');
+const canonicalize = require('canonicalize').default;
+const output = path.join(__dirname, '../static/compliance/source/test-vectors/creative-addon-chain.json');
+const sha = value => `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
+const bytes = value => Buffer.from(canonicalize(value), 'utf8');
+const encode = value => bytes(value).toString('base64url');
+const key = { kid: 'addon-vector-key', kty: 'OKP', crv: 'Ed25519', alg: 'EdDSA', use: 'sig',
+ key_ops: ['verify'], adcp_use: 'addon-chain-signing',
+ x: Buffer.from('d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a', 'hex').toString('base64url') };
+const privateKey = { kty: key.kty, crv: key.crv, x: key.x,
+ d: Buffer.from('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', 'hex').toString('base64url') };
+const privateObject = crypto.createPrivateKey({ key: privateKey, format: 'jwk' });
+const timestamp = '2026-10-14T12:00:00Z';
+const iat = Date.parse(timestamp) / 1000;
+function sign(payload, header = { alg: 'EdDSA', kid: key.kid, typ: 'adcp-addon-chain+jws' }, canonical = true) {
+ const protectedHeader = encode(header);
+ const payloadEncoded = canonical ? encode(payload) : Buffer.from(JSON.stringify(payload, null, 2)).toString('base64url');
+ return { profile: 'adcp/addon-chain/v1', protected: protectedHeader, payload: payloadEncoded,
+ signature: crypto.sign(null, Buffer.from(`${protectedHeader}.${payloadEncoded}`), privateObject).toString('base64url') };
+}
+const fragment = '';
+const fragmentHash = sha(Buffer.from(fragment, 'utf8'));
+const contexts = [
+ { format_kind: 'image', content_hash: sha(Buffer.from('89504e470d0a1a0a', 'hex')) },
+ { format_kind: 'html5', content_type: 'text/html', content_hash: sha(Buffer.from('
Creative
', 'utf8')) },
+ { format_kind: 'display_tag', asset: { asset_type: 'display_tag', delivery_type: 'inline_markup', markup_type: 'javascript', markup: '' } },
+];
+const valid = contexts.map((creative_hash_input, i) => {
+ const source = i === 1 ? 'static_reference' : 'live';
+ const vendor = { agent_name: 'measure', source, mode: i === 2 ? 'best_effort' : 'required',
+ format_kinds: ['image', 'html5', 'display_tag'], positions: ['append'],
+ ...(source === 'live' ? { agent_url: 'https://measurement.example/agent' } : { static_content_hash: fragmentHash }),
+ ...(i === 2 ? {} : { max_hold_seconds: 60 }) };
+ const configuration = { scope: 'product', product_id: 'display', complete: true, signed_chain_profile: 'adcp/addon-chain/v1', vendors: [vendor] };
+ const entry = { order: 0, registration_index: 0, agent_name: vendor.agent_name, source,
+ ...(source === 'live' ? { agent_url: vendor.agent_url } : {}), timestamp,
+ ...(i === 2 ? { status: 'omitted', reason: 'vendor_omission' } : { status: 'applied', position: 'append', fragment_index: 0, content_hash: fragmentHash, bytes: Buffer.byteLength(fragment) }) };
+ const expected = { agent_url: 'https://sales.example/agent', publisher_domain: 'publisher.example',
+ creative_id: `creative-${i}`, format_kind: creative_hash_input.format_kind, variant_id: `variant-${i}`,
+ media_buy_id: 'buy-1', package_id: 'package-1', revision_id: 'revision-1', product_id: 'display',
+ creative_hash: sha(bytes(creative_hash_input)) };
+ const chain = { creative_id: expected.creative_id, creative_hash: expected.creative_hash, configuration, entries: [entry], chain_hash: sha(bytes([entry])) };
+ const { product_id, ...covered } = expected;
+ const claims = { typ: 'adcp-addon-chain+jws', ...covered, chain_hash: chain.chain_hash, configuration_hash: sha(bytes(configuration)), iat };
+ chain.signed_attestation = sign(claims);
+ return { id: ['live-image', 'static-html5', 'omitted-display-tag'][i], creative_hash_input, fragment, fragment_hash: fragmentHash, chain,
+ trust: { key: structuredClone(key), publisher_pin: structuredClone(key), expected, now: iat + 30 } };
+});
+const invalid = [];
+function negative(id, expected, mutate, base = 0) {
+ const changed = structuredClone(valid[base]); mutate(changed);
+ const edits = [];
+ function diff(a,b,at='') {
+ for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
+ const location = at ? `${at}.${k}` : k;
+ if (!(k in b)) { edits.push({ path: location, remove: true }); continue; }
+ if (!(k in a)) { edits.push({ path: location, value: b[k] }); continue; }
+ if (a[k] && b[k] && typeof a[k] === 'object' && typeof b[k] === 'object') diff(a[k], b[k], location);
+ else if (a[k] !== b[k]) edits.push({ path: location, value: b[k] });
+ }
+ }
+ diff(valid[base], changed);invalid.push({ id, base, edits, expected });
+}
+function resign(vector, mutateClaims = () => {}) {
+ const claims = JSON.parse(Buffer.from(vector.chain.signed_attestation.payload, 'base64url'));
+ mutateClaims(claims);
+ vector.chain.signed_attestation = sign(claims);
+}
+negative('altered-fragment-hash', 'chain_hash', v => { v.chain.entries[0].content_hash = `sha256:${'0'.repeat(64)}`; });
+negative('altered-configuration', 'configuration_hash', v => { v.chain.configuration.vendors[0].mode = 'best_effort'; });
+negative('wrong-key-purpose', 'key_purpose', v => { v.trust.key.adcp_use = 'request-signing'; });
+negative('same-kid-wrong-publisher-material', 'publisher_pin', v => { v.trust.publisher_pin.x = 'A'.repeat(43); });
+negative('wrong-variant', 'context', v => { v.trust.expected.variant_id = 'another-variant'; });
+negative('wrong-publisher', 'context', v => { v.trust.expected.publisher_domain = 'other.example'; });
+negative('wrong-creative', 'creative_hash', v => { v.trust.expected.creative_hash = `sha256:${'0'.repeat(64)}`; });
+negative('wrong-revision', 'context', v => { v.trust.expected.revision_id = 'revision-2'; });
+negative('missing-delivery-binding', 'context', v => { resign(v, p => delete p.package_id); });
+negative('jws-key-url-not-a-trust-root', 'header', v => { const p=JSON.parse(Buffer.from(v.chain.signed_attestation.payload,'base64url'));v.chain.signed_attestation=sign(p,{alg:'EdDSA',kid:key.kid,typ:'adcp-addon-chain+jws',jku:'https://attacker.example/jwks'}); });
+negative('noncanonical-payload', 'canonicalization', v => { const p=JSON.parse(Buffer.from(v.chain.signed_attestation.payload,'base64url'));v.chain.signed_attestation=sign(p,undefined,false); });
+negative('corrupt-signature', 'signature', v => { v.chain.signed_attestation.signature = 'A'.repeat(86); });
+negative('padded-base64url', 'encoding', v => { v.chain.signed_attestation.protected += '='; });
+negative('future-issuance', 'time', v => resign(v, p => { p.iat += 120; }));
+negative('revoked-at-issuance', 'revocation', v => { v.trust.key.revoked_at=timestamp; });
+negative('backdated-without-observed-history', 'revocation', v => { v.trust.key.revoked_at='2026-10-14T12:01:00Z';v.trust.now=iat+120; });
+negative('rewritten-observation-history', 'revocation', v => { v.trust.key.revoked_at='2026-10-14T12:01:00Z';v.trust.observed_at=iat+20;v.trust.observed_chain_hash=`sha256:${'0'.repeat(64)}`; });
+negative('signed-required-omission', 'required_omission', v => { v.chain.configuration.vendors[0].mode='required';v.chain.configuration.vendors[0].max_hold_seconds=60;resign(v,p=>{p.configuration_hash=sha(bytes(v.chain.configuration));}); },2);
+negative('signed-registration-mismatch', 'entry', v => { v.chain.entries[0].agent_name='other-vendor';v.chain.chain_hash=sha(bytes(v.chain.entries));resign(v,p=>{p.chain_hash=v.chain.chain_hash;}); });
+negative('signed-invalid-order', 'entry', v => { v.chain.entries[0].order=1;v.chain.chain_hash=sha(bytes(v.chain.entries));resign(v,p=>{p.chain_hash=v.chain.chain_hash;}); });
+negative('publisher-pin-revoked-at-issuance', 'revocation', v => { v.trust.publisher_pin.revoked_at=timestamp; });
+negative('publisher-pin-purpose-conflict', 'publisher_pin', v => { v.trust.publisher_pin.adcp_use='request-signing'; });
+negative('signed-missing-byte-count', 'schema', v => { delete v.chain.entries[0].bytes;v.chain.chain_hash=sha(bytes(v.chain.entries));resign(v,p=>{p.chain_hash=v.chain.chain_hash;}); });
+negative('unknown-signed-claim', 'schema', v => { resign(v,p=>{p.unrecognized='claim';}); });
+negative('signed-fragment-index-gap', 'entry', v => { v.chain.entries[0].fragment_index=1;v.chain.chain_hash=sha(bytes(v.chain.entries));resign(v,p=>{p.chain_hash=v.chain.chain_hash;}); });
+negative('signed-reversed-omission-order', 'order', v => {
+ v.chain.configuration.vendors.push({...v.chain.configuration.vendors[0],agent_name:'second',agent_url:'https://second.example/agent'});
+ const first=v.chain.entries[0];
+ v.chain.entries=[{...first,order:0,registration_index:1,agent_name:'second',agent_url:'https://second.example/agent'},{...first,order:1}];
+ v.chain.chain_hash=sha(bytes(v.chain.entries));resign(v,p=>{p.chain_hash=v.chain.chain_hash;p.configuration_hash=sha(bytes(v.chain.configuration));});
+},2);
+negative('signed-duplicate-fragment-index-across-positions', 'entry', v => {
+ v.chain.configuration.vendors[0].positions=['prepend','append'];
+ v.chain.entries=[{...v.chain.entries[0],position:'prepend'},{...v.chain.entries[0],order:1,position:'append'}];
+ v.chain.chain_hash=sha(bytes(v.chain.entries));resign(v,p=>{p.chain_hash=v.chain.chain_hash;p.configuration_hash=sha(bytes(v.chain.configuration));});
+});
+const historical = structuredClone(valid[0]);historical.id='historical-after-rotation-and-revocation';historical.trust.key.revoked_at='2026-10-14T12:01:00Z';historical.trust.observed_at=iat+20;historical.trust.observed_chain_hash=historical.chain.chain_hash;historical.trust.observed_attestation_hash=sha(bytes(historical.chain.signed_attestation));historical.trust.now=iat+86400;valid.push(historical);
+const required = { source: 'live', mode: 'required', max_hold_seconds: 60, fragments: [] };
+const behavior = [
+ { id:'required-empty-keeps-original-task', input:{async:true,task_id:'task-1',elapsed_seconds:59,vendors:[required]},expected:{status:'working',task_id:'task-1'} },
+ { id:'required-empty-at-deadline-fails', input:{async:true,task_id:'task-1',elapsed_seconds:60,vendors:[required]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} },
+ { id:'required-without-async-fails', input:{async:false,task_id:'task-1',elapsed_seconds:0,vendors:[required]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} },
+ { id:'best-effort-deliberate-omission',input:{vendors:[{...required,mode:'best_effort'}]},expected:{status:'completed',fragments:[],omitted:[{registration_index:0,reason:'vendor_omission'}]} },
+ { id:'best-effort-network-failure',input:{vendors:[{...required,mode:'best_effort',error_code:'ADDON_UNREACHABLE'}]},expected:{status:'completed',fragments:[],omitted:[{registration_index:0,reason:'failure'}]} },
+ { id:'required-bad-hash-fails',input:{async:false,elapsed_seconds:0,vendors:[{...required,fragments:[{position:'append',content:'x',content_hash:`sha256:${'0'.repeat(64)}`}]}]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} },
+ { id:'static-does-not-call-vendor',input:{vendors:[{source:'static_reference',mode:'required',position:'append',content:'static'}]},expected:{status:'completed',fragments:['static'],omitted:[]} },
+ { id:'prepend-order-is-not-reversed',input:{vendors:[{source:'live',mode:'required',fragments:[{position:'append',content:'A1',content_hash:sha('A1')},{position:'prepend',content:'A0',content_hash:sha('A0')}]},{source:'live',mode:'required',fragments:[{position:'prepend',content:'B0',content_hash:sha('B0')},{position:'append',content:'B1',content_hash:sha('B1')}]}]},expected:{status:'completed',fragments:['A0','B0','A1','B1'],omitted:[]} }
+];
+behavior.push(
+ { id:'late-recovery-cannot-resume-expired-operation',input:{async:true,task_id:'task-1',elapsed_seconds:61,vendors:[{...required,first_failure_seconds:0,fragments:[{position:'append',content:'repaired',content_hash:sha('repaired')}]}]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} },
+ { id:'later-expired-vendor-overrides-earlier-pending',input:{async:true,task_id:'task-1',elapsed_seconds:30,vendors:[required,{...required,max_hold_seconds:10}]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} },
+ { id:'per-vendor-original-deadline-survives-restart',input:{async:true,task_id:'task-1',elapsed_seconds:90,vendors:[{...required,first_failure_seconds:30,max_hold_seconds:60},{...required,first_failure_seconds:70,max_hold_seconds:60}]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} },
+ { id:'later-first-failure-keeps-its-own-deadline',input:{async:true,task_id:'task-1',elapsed_seconds:90,vendors:[{...required,first_failure_seconds:50,max_hold_seconds:60}]},expected:{status:'working',task_id:'task-1'} },
+ { id:'recovered-earlier-vendor-cannot-mask-expired-other-vendor',input:{async:true,task_id:'task-1',elapsed_seconds:30,vendors:[{...required,fragments:[{position:'append',content:'repaired',content_hash:sha('repaired')}]},{...required,max_hold_seconds:10}]},expected:{status:'failed',code:'ADDON_REQUIRED_UNAVAILABLE'} }
+);
+const fixture = { $schema:'creative-addon-chain.schema.json', profile:'adcp/addon-chain/v1', note:'Public RFC 8032 test key only. Reference vectors are not production integration evidence.',test_private_key:privateKey,valid,invalid,behavior };
+const serialized=JSON.stringify(fixture,null,2)+'\n';
+if(process.argv.includes('--check')) { if(!fs.existsSync(output)||fs.readFileSync(output,'utf8')!==serialized) { console.error('Creative addon vectors are stale');process.exitCode=1; } else console.log('Creative addon vectors are reproducible'); }
+else { fs.writeFileSync(output,serialized);console.log(`Wrote ${valid.length} valid, ${invalid.length} invalid, and ${behavior.length} behavior vectors`); }
diff --git a/server/src/services/adcp-taxonomy.ts b/server/src/services/adcp-taxonomy.ts
index 0b1cf13dd86..352ba82b7b5 100644
--- a/server/src/services/adcp-taxonomy.ts
+++ b/server/src/services/adcp-taxonomy.ts
@@ -103,6 +103,7 @@ export type AdcpSpecialism =
| 'collection-lists'
| 'content-standards'
| 'creative-ad-server'
+ | 'creative-addon'
| 'creative-generative'
| 'creative-template'
| 'creative-transformers'
@@ -138,6 +139,7 @@ export const ADCP_SPECIALISMS: readonly AdcpSpecialism[] = [
'collection-lists',
'content-standards',
'creative-ad-server',
+ 'creative-addon',
'creative-generative',
'creative-template',
'creative-transformers',
diff --git a/server/src/training-agent/idempotency.ts b/server/src/training-agent/idempotency.ts
index b75edd44555..467984ec721 100644
--- a/server/src/training-agent/idempotency.ts
+++ b/server/src/training-agent/idempotency.ts
@@ -95,6 +95,7 @@ export const MUTATING_TOOLS: ReadonlySet = new Set([
'delete_collection_list',
'delete_property_list',
'decline_proposals',
+ 'get_creative_addons',
'log_event',
'provide_performance_feedback',
'request_proposals',
diff --git a/server/tests/unit/adcp-taxonomy.test.ts b/server/tests/unit/adcp-taxonomy.test.ts
index 703765ca6f5..e6827fa67ca 100644
--- a/server/tests/unit/adcp-taxonomy.test.ts
+++ b/server/tests/unit/adcp-taxonomy.test.ts
@@ -46,6 +46,8 @@ describe('specialism status', () => {
it('respects preview status from the compliance catalog', () => {
expect(getSpecialismStatus('sponsored-intelligence')).toBe('preview');
expect(isStableSpecialism('sponsored-intelligence')).toBe(false);
+ expect(getSpecialismStatus('creative-addon')).toBe('preview');
+ expect(isStableSpecialism('creative-addon')).toBe(false);
});
it('treats unknown specialisms as stable (safe default)', () => {
diff --git a/server/tests/unit/idempotency.test.ts b/server/tests/unit/idempotency.test.ts
index 40c383f8c96..199aa9ed9cc 100644
--- a/server/tests/unit/idempotency.test.ts
+++ b/server/tests/unit/idempotency.test.ts
@@ -77,6 +77,7 @@ describe('idempotency facade', () => {
'report_media_buy_issue',
'si_initiate_session', 'si_send_message',
'acquire_rights', 'update_rights', 'creative_approval',
+ 'get_creative_addons',
]) {
expect(isMutatingTool(name), `${name} should be mutating`).toBe(true);
}
diff --git a/static/compliance/source/specialisms/creative-addon/index.yaml b/static/compliance/source/specialisms/creative-addon/index.yaml
new file mode 100644
index 00000000000..1cfa2442c5d
--- /dev/null
+++ b/static/compliance/source/specialisms/creative-addon/index.yaml
@@ -0,0 +1,119 @@
+id: creative_addon
+version: "1.0.0"
+title: "Creative addon vendor"
+protocol: creative
+category: creative_addon
+summary: "Fragment-only vendor discovery and operation-scoped generation for seller-controlled adjacent execution."
+status: preview
+track: creative
+required_tools:
+ - get_adcp_capabilities
+ - get_creative_addons
+
+narrative: |
+ A Sales Agent asks a registered addon vendor for fragments using creative
+ context only. The vendor has no buyer asset or markup. This specialism checks
+ the vendor wire contract; it does not certify a seller's assembly runtime,
+ key resolution, signing implementation, or spend-commitment atomicity.
+ Seller signing and failure cases are published separately in
+ test-vectors/creative-addon-chain.json. A named seller/buyer integration is
+ still a release gate for the proposed 3.3 feature.
+
+agent:
+ interaction_model: stateless_transform
+ capabilities:
+ - creative_addon
+caller:
+ role: sales_agent
+ example: "Example Sales"
+prerequisites:
+ description: |
+ Use an authenticated operator identity authorized to request fragments.
+ The example image context must be supported by the registered vendor.
+ Vendors supporting only another declared format substitute that format
+ when exercising the same contract. No buyer creative payload is supplied.
+
+phases:
+ - id: discovery
+ title: "Discover the vendor contract"
+ steps:
+ - id: capabilities
+ title: "Verify addon capability and mandatory signing profile"
+ task: get_adcp_capabilities
+ schema_ref: "protocol/get-adcp-capabilities-request.json"
+ response_schema_ref: "protocol/get-adcp-capabilities-response.json"
+ doc_ref: "/protocol/get_adcp_capabilities"
+ stateful: false
+ expected: "Declare the creative protocol and creative-addon specialism, with the mandatory signed-chain profile."
+ sample_request:
+ context:
+ correlation_id: "creative_addon--capabilities"
+ validations:
+ - check: response_schema
+ description: "Capability response conforms to the public schema"
+ - check: field_contains
+ path: "supported_protocols[*]"
+ value: "creative"
+ description: "Vendor participates in the creative protocol"
+ - check: field_contains
+ path: "specialisms[*]"
+ value: "creative-addon"
+ description: "Vendor claims fragment-only addon support"
+ - check: field_present
+ path: "agent_name"
+ description: "Operator can check the declared vendor identity"
+ - check: field_value
+ path: "creative_addon.signed_chain.required"
+ value: true
+ description: "Signed assembly evidence is mandatory"
+ - check: field_value
+ path: "creative_addon.signed_chain.profile"
+ value: "adcp/addon-chain/v1"
+ description: "Vendor declares the supported signature profile"
+ - check: field_value
+ path: "context.correlation_id"
+ value: "creative_addon--capabilities"
+ description: "Caller context is echoed unchanged"
+ - id: fragments
+ title: "Request operation-scoped fragments"
+ steps:
+ - id: image_context
+ title: "Return fragments without receiving creative content"
+ task: get_creative_addons
+ schema_ref: "creative/get-creative-addons-request.json"
+ response_schema_ref: "creative/get-creative-addons-response.json"
+ doc_ref: "/creative/task-reference/get_creative_addons"
+ stateful: true
+ expected: |
+ Return a completed addons response. An empty array is a deliberate
+ vendor omission; seller-required handling is exercised separately.
+ Every returned creative_id must be creative-1, and every fragment's
+ content_hash must hash its exact UTF-8 content. The automated checks
+ below cover wire shape; membership and hash checks need the seller
+ integration evidence and are not claimed by response_schema.
+ sample_request:
+ adcp_version: "3.3"
+ idempotency_key: "$generate:uuid_v4#creative_addon_fragments"
+ operator_name: "Example Sales"
+ operator_url: "https://sales.example/agent"
+ publisher_domain: "publisher.example"
+ creatives:
+ - creative_id: "creative-1"
+ format_kind: "image"
+ dimensions: { w: 300, h: 250 }
+ context:
+ correlation_id: "creative_addon--fragments"
+ validations:
+ - check: response_schema
+ description: "Fragment response conforms to the public schema"
+ - check: field_value
+ path: "status"
+ value: "completed"
+ description: "The vendor completed this batch"
+ - check: field_present
+ path: "addons"
+ description: "The completed response includes the fragments or an explicit empty array"
+ - check: field_value
+ path: "context.correlation_id"
+ value: "creative_addon--fragments"
+ description: "Caller context is echoed unchanged"
diff --git a/static/compliance/source/test-vectors/creative-addon-chain.json b/static/compliance/source/test-vectors/creative-addon-chain.json
new file mode 100644
index 00000000000..bbd887cb6ea
--- /dev/null
+++ b/static/compliance/source/test-vectors/creative-addon-chain.json
@@ -0,0 +1,1220 @@
+{
+ "$schema": "creative-addon-chain.schema.json",
+ "profile": "adcp/addon-chain/v1",
+ "note": "Public RFC 8032 test key only. Reference vectors are not production integration evidence.",
+ "test_private_key": {
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+ "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"
+ },
+ "valid": [
+ {
+ "id": "live-image",
+ "creative_hash_input": {
+ "format_kind": "image",
+ "content_hash": "sha256:4c4b6a3be1314ab86138bef4314dde022e600960d8689a2c8f8631802d20dab6"
+ },
+ "fragment": "",
+ "fragment_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "chain": {
+ "creative_id": "creative-0",
+ "creative_hash": "sha256:749731ec1830ea3a59a517479b60ae955b54929fb38ebe2b81a444b7a4bc388d",
+ "configuration": {
+ "scope": "product",
+ "product_id": "display",
+ "complete": true,
+ "signed_chain_profile": "adcp/addon-chain/v1",
+ "vendors": [
+ {
+ "agent_name": "measure",
+ "source": "live",
+ "mode": "required",
+ "format_kinds": [
+ "image",
+ "html5",
+ "display_tag"
+ ],
+ "positions": [
+ "append"
+ ],
+ "agent_url": "https://measurement.example/agent",
+ "max_hold_seconds": 60
+ }
+ ]
+ },
+ "entries": [
+ {
+ "order": 0,
+ "registration_index": 0,
+ "agent_name": "measure",
+ "source": "live",
+ "agent_url": "https://measurement.example/agent",
+ "timestamp": "2026-10-14T12:00:00Z",
+ "status": "applied",
+ "position": "append",
+ "fragment_index": 0,
+ "content_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "bytes": 61
+ }
+ ],
+ "chain_hash": "sha256:42781004a452d65343e590f72c4c47dc431baec0cbacbbc19b3e5adc40a85c15",
+ "signed_attestation": {
+ "profile": "adcp/addon-chain/v1",
+ "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFkZG9uLXZlY3Rvci1rZXkiLCJ0eXAiOiJhZGNwLWFkZG9uLWNoYWluK2p3cyJ9",
+ "payload": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjQyNzgxMDA0YTQ1MmQ2NTM0M2U1OTBmNzJjNGM0N2RjNDMxYmFlYzBjYmFjYmJjMTliM2U1YWRjNDBhODVjMTUiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9",
+ "signature": "UvuimFPxx79MVfKbPEd6DvXvqGHGK26FpMuD5kQ2rM8vw4qXKgxtaUItx1N2pJyHoxD2KjMXYjdc0RJnq4JLDw"
+ }
+ },
+ "trust": {
+ "key": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "publisher_pin": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "expected": {
+ "agent_url": "https://sales.example/agent",
+ "publisher_domain": "publisher.example",
+ "creative_id": "creative-0",
+ "format_kind": "image",
+ "variant_id": "variant-0",
+ "media_buy_id": "buy-1",
+ "package_id": "package-1",
+ "revision_id": "revision-1",
+ "product_id": "display",
+ "creative_hash": "sha256:749731ec1830ea3a59a517479b60ae955b54929fb38ebe2b81a444b7a4bc388d"
+ },
+ "now": 1791979230
+ }
+ },
+ {
+ "id": "static-html5",
+ "creative_hash_input": {
+ "format_kind": "html5",
+ "content_type": "text/html",
+ "content_hash": "sha256:e52b8569e88070ceab86c5dedcc82d01df6ab3211ebabcc88a1940dc84a01207"
+ },
+ "fragment": "",
+ "fragment_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "chain": {
+ "creative_id": "creative-1",
+ "creative_hash": "sha256:57ce6fe02588e83b2a80eb153bf7d29419c18539f32de89f9e36e03ff0871eb5",
+ "configuration": {
+ "scope": "product",
+ "product_id": "display",
+ "complete": true,
+ "signed_chain_profile": "adcp/addon-chain/v1",
+ "vendors": [
+ {
+ "agent_name": "measure",
+ "source": "static_reference",
+ "mode": "required",
+ "format_kinds": [
+ "image",
+ "html5",
+ "display_tag"
+ ],
+ "positions": [
+ "append"
+ ],
+ "static_content_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "max_hold_seconds": 60
+ }
+ ]
+ },
+ "entries": [
+ {
+ "order": 0,
+ "registration_index": 0,
+ "agent_name": "measure",
+ "source": "static_reference",
+ "timestamp": "2026-10-14T12:00:00Z",
+ "status": "applied",
+ "position": "append",
+ "fragment_index": 0,
+ "content_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "bytes": 61
+ }
+ ],
+ "chain_hash": "sha256:7dae8229ac74444076a764412bb013bce1963d8613bdcb0f403488f000624977",
+ "signed_attestation": {
+ "profile": "adcp/addon-chain/v1",
+ "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFkZG9uLXZlY3Rvci1rZXkiLCJ0eXAiOiJhZGNwLWFkZG9uLWNoYWluK2p3cyJ9",
+ "payload": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjdkYWU4MjI5YWM3NDQ0NDA3NmE3NjQ0MTJiYjAxM2JjZTE5NjNkODYxM2JkY2IwZjQwMzQ4OGYwMDA2MjQ5NzciLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6M2U1OGYwYjNlZmJlMzkxMDYyMWY0NWY1MzU1YmU4MWFkNjVlNzIzYzc1MDgyMjM0MmZjMTI3MDNlYTBiNDBmMiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NTdjZTZmZTAyNTg4ZTgzYjJhODBlYjE1M2JmN2QyOTQxOWMxODUzOWYzMmRlODlmOWUzNmUwM2ZmMDg3MWViNSIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMSIsImZvcm1hdF9raW5kIjoiaHRtbDUiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMSJ9",
+ "signature": "cnEP9Eycpgb-Jur3W-yoC66Ca8HHd6Ut7Sk6HcalfiUIq3-qCO6__rRGeOyK7lmAYUjE9C9GFKXbW8HQX9x_DA"
+ }
+ },
+ "trust": {
+ "key": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "publisher_pin": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "expected": {
+ "agent_url": "https://sales.example/agent",
+ "publisher_domain": "publisher.example",
+ "creative_id": "creative-1",
+ "format_kind": "html5",
+ "variant_id": "variant-1",
+ "media_buy_id": "buy-1",
+ "package_id": "package-1",
+ "revision_id": "revision-1",
+ "product_id": "display",
+ "creative_hash": "sha256:57ce6fe02588e83b2a80eb153bf7d29419c18539f32de89f9e36e03ff0871eb5"
+ },
+ "now": 1791979230
+ }
+ },
+ {
+ "id": "omitted-display-tag",
+ "creative_hash_input": {
+ "format_kind": "display_tag",
+ "asset": {
+ "asset_type": "display_tag",
+ "delivery_type": "inline_markup",
+ "markup_type": "javascript",
+ "markup": ""
+ }
+ },
+ "fragment": "",
+ "fragment_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "chain": {
+ "creative_id": "creative-2",
+ "creative_hash": "sha256:01c38412f035a6c4b8db2519b705aeef63d7bc7ca832675c96cbd0487398faa2",
+ "configuration": {
+ "scope": "product",
+ "product_id": "display",
+ "complete": true,
+ "signed_chain_profile": "adcp/addon-chain/v1",
+ "vendors": [
+ {
+ "agent_name": "measure",
+ "source": "live",
+ "mode": "best_effort",
+ "format_kinds": [
+ "image",
+ "html5",
+ "display_tag"
+ ],
+ "positions": [
+ "append"
+ ],
+ "agent_url": "https://measurement.example/agent"
+ }
+ ]
+ },
+ "entries": [
+ {
+ "order": 0,
+ "registration_index": 0,
+ "agent_name": "measure",
+ "source": "live",
+ "agent_url": "https://measurement.example/agent",
+ "timestamp": "2026-10-14T12:00:00Z",
+ "status": "omitted",
+ "reason": "vendor_omission"
+ }
+ ],
+ "chain_hash": "sha256:c4b6cc12dd74b8bc0aa97661fc7fcb180f84fcea8b5fe37d6fb3414a981f614b",
+ "signed_attestation": {
+ "profile": "adcp/addon-chain/v1",
+ "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFkZG9uLXZlY3Rvci1rZXkiLCJ0eXAiOiJhZGNwLWFkZG9uLWNoYWluK2p3cyJ9",
+ "payload": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OmM0YjZjYzEyZGQ3NGI4YmMwYWE5NzY2MWZjN2ZjYjE4MGY4NGZjZWE4YjVmZTM3ZDZmYjM0MTRhOTgxZjYxNGIiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6YmUxODNjODA1OTI0NmRiNTYyZDg4NDZiZTAzZDE5MjE0NjBlYzkwNTJjMTExOTA2MDUyZTc1YzIyMWNlYmJkOSIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6MDFjMzg0MTJmMDM1YTZjNGI4ZGIyNTE5YjcwNWFlZWY2M2Q3YmM3Y2E4MzI2NzVjOTZjYmQwNDg3Mzk4ZmFhMiIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMiIsImZvcm1hdF9raW5kIjoiZGlzcGxheV90YWciLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMiJ9",
+ "signature": "8Ex1UrQzVbr7pXjsTcF-NtCjglqCwLp08sa4apPAZ_RmnLNH7MGfFlhKXuA8wqadeXhXvQ_BvJ02i16sqbgEDg"
+ }
+ },
+ "trust": {
+ "key": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "publisher_pin": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "expected": {
+ "agent_url": "https://sales.example/agent",
+ "publisher_domain": "publisher.example",
+ "creative_id": "creative-2",
+ "format_kind": "display_tag",
+ "variant_id": "variant-2",
+ "media_buy_id": "buy-1",
+ "package_id": "package-1",
+ "revision_id": "revision-1",
+ "product_id": "display",
+ "creative_hash": "sha256:01c38412f035a6c4b8db2519b705aeef63d7bc7ca832675c96cbd0487398faa2"
+ },
+ "now": 1791979230
+ }
+ },
+ {
+ "id": "historical-after-rotation-and-revocation",
+ "creative_hash_input": {
+ "format_kind": "image",
+ "content_hash": "sha256:4c4b6a3be1314ab86138bef4314dde022e600960d8689a2c8f8631802d20dab6"
+ },
+ "fragment": "",
+ "fragment_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "chain": {
+ "creative_id": "creative-0",
+ "creative_hash": "sha256:749731ec1830ea3a59a517479b60ae955b54929fb38ebe2b81a444b7a4bc388d",
+ "configuration": {
+ "scope": "product",
+ "product_id": "display",
+ "complete": true,
+ "signed_chain_profile": "adcp/addon-chain/v1",
+ "vendors": [
+ {
+ "agent_name": "measure",
+ "source": "live",
+ "mode": "required",
+ "format_kinds": [
+ "image",
+ "html5",
+ "display_tag"
+ ],
+ "positions": [
+ "append"
+ ],
+ "agent_url": "https://measurement.example/agent",
+ "max_hold_seconds": 60
+ }
+ ]
+ },
+ "entries": [
+ {
+ "order": 0,
+ "registration_index": 0,
+ "agent_name": "measure",
+ "source": "live",
+ "agent_url": "https://measurement.example/agent",
+ "timestamp": "2026-10-14T12:00:00Z",
+ "status": "applied",
+ "position": "append",
+ "fragment_index": 0,
+ "content_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "bytes": 61
+ }
+ ],
+ "chain_hash": "sha256:42781004a452d65343e590f72c4c47dc431baec0cbacbbc19b3e5adc40a85c15",
+ "signed_attestation": {
+ "profile": "adcp/addon-chain/v1",
+ "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFkZG9uLXZlY3Rvci1rZXkiLCJ0eXAiOiJhZGNwLWFkZG9uLWNoYWluK2p3cyJ9",
+ "payload": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjQyNzgxMDA0YTQ1MmQ2NTM0M2U1OTBmNzJjNGM0N2RjNDMxYmFlYzBjYmFjYmJjMTliM2U1YWRjNDBhODVjMTUiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9",
+ "signature": "UvuimFPxx79MVfKbPEd6DvXvqGHGK26FpMuD5kQ2rM8vw4qXKgxtaUItx1N2pJyHoxD2KjMXYjdc0RJnq4JLDw"
+ }
+ },
+ "trust": {
+ "key": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+ "revoked_at": "2026-10-14T12:01:00Z"
+ },
+ "publisher_pin": {
+ "kid": "addon-vector-key",
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "alg": "EdDSA",
+ "use": "sig",
+ "key_ops": [
+ "verify"
+ ],
+ "adcp_use": "addon-chain-signing",
+ "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
+ },
+ "expected": {
+ "agent_url": "https://sales.example/agent",
+ "publisher_domain": "publisher.example",
+ "creative_id": "creative-0",
+ "format_kind": "image",
+ "variant_id": "variant-0",
+ "media_buy_id": "buy-1",
+ "package_id": "package-1",
+ "revision_id": "revision-1",
+ "product_id": "display",
+ "creative_hash": "sha256:749731ec1830ea3a59a517479b60ae955b54929fb38ebe2b81a444b7a4bc388d"
+ },
+ "now": 1792065600,
+ "observed_at": 1791979220,
+ "observed_chain_hash": "sha256:42781004a452d65343e590f72c4c47dc431baec0cbacbbc19b3e5adc40a85c15",
+ "observed_attestation_hash": "sha256:2946301a346536615159adc3ad2fd41951a8f342cd3a1d2da2263ed46fee61fe"
+ }
+ }
+ ],
+ "invalid": [
+ {
+ "id": "altered-fragment-hash",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.entries.0.content_hash",
+ "value": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ ],
+ "expected": "chain_hash"
+ },
+ {
+ "id": "altered-configuration",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.configuration.vendors.0.mode",
+ "value": "best_effort"
+ }
+ ],
+ "expected": "configuration_hash"
+ },
+ {
+ "id": "wrong-key-purpose",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.key.adcp_use",
+ "value": "request-signing"
+ }
+ ],
+ "expected": "key_purpose"
+ },
+ {
+ "id": "same-kid-wrong-publisher-material",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.publisher_pin.x",
+ "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ }
+ ],
+ "expected": "publisher_pin"
+ },
+ {
+ "id": "wrong-variant",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.expected.variant_id",
+ "value": "another-variant"
+ }
+ ],
+ "expected": "context"
+ },
+ {
+ "id": "wrong-publisher",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.expected.publisher_domain",
+ "value": "other.example"
+ }
+ ],
+ "expected": "context"
+ },
+ {
+ "id": "wrong-creative",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.expected.creative_hash",
+ "value": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ ],
+ "expected": "creative_hash"
+ },
+ {
+ "id": "wrong-revision",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.expected.revision_id",
+ "value": "revision-2"
+ }
+ ],
+ "expected": "context"
+ },
+ {
+ "id": "missing-delivery-binding",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjQyNzgxMDA0YTQ1MmQ2NTM0M2U1OTBmNzJjNGM0N2RjNDMxYmFlYzBjYmFjYmJjMTliM2U1YWRjNDBhODVjMTUiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicHVibGlzaGVyX2RvbWFpbiI6InB1Ymxpc2hlci5leGFtcGxlIiwicmV2aXNpb25faWQiOiJyZXZpc2lvbi0xIiwidHlwIjoiYWRjcC1hZGRvbi1jaGFpbitqd3MiLCJ2YXJpYW50X2lkIjoidmFyaWFudC0wIn0"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "kNpwmX7tiffEvX6CUThSAouShIRrTX_w2FLVjQUX5iCpR0qlHPcQ_zez9-zuJ6zH1rL7dFhnKq_avi3zbehiAw"
+ }
+ ],
+ "expected": "context"
+ },
+ {
+ "id": "jws-key-url-not-a-trust-root",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.protected",
+ "value": "eyJhbGciOiJFZERTQSIsImprdSI6Imh0dHBzOi8vYXR0YWNrZXIuZXhhbXBsZS9qd2tzIiwia2lkIjoiYWRkb24tdmVjdG9yLWtleSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIn0"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "je-zScRhk5j-LDjUFythR_WaaSkDfSIqLiS4Zc306AIsb0cSER-wqP19gHsZrXjqmCYOg2aegI8C86QV4Au6BA"
+ }
+ ],
+ "expected": "header"
+ },
+ {
+ "id": "noncanonical-payload",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "ewogICJhZ2VudF91cmwiOiAiaHR0cHM6Ly9zYWxlcy5leGFtcGxlL2FnZW50IiwKICAiY2hhaW5faGFzaCI6ICJzaGEyNTY6NDI3ODEwMDRhNDUyZDY1MzQzZTU5MGY3MmM0YzQ3ZGM0MzFiYWVjMGNiYWNiYmMxOWIzZTVhZGM0MGE4NWMxNSIsCiAgImNvbmZpZ3VyYXRpb25faGFzaCI6ICJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsCiAgImNyZWF0aXZlX2hhc2giOiAic2hhMjU2Ojc0OTczMWVjMTgzMGVhM2E1OWE1MTc0NzliNjBhZTk1NWI1NDkyOWZiMzhlYmUyYjgxYTQ0NGI3YTRiYzM4OGQiLAogICJjcmVhdGl2ZV9pZCI6ICJjcmVhdGl2ZS0wIiwKICAiZm9ybWF0X2tpbmQiOiAiaW1hZ2UiLAogICJpYXQiOiAxNzkxOTc5MjAwLAogICJtZWRpYV9idXlfaWQiOiAiYnV5LTEiLAogICJwYWNrYWdlX2lkIjogInBhY2thZ2UtMSIsCiAgInB1Ymxpc2hlcl9kb21haW4iOiAicHVibGlzaGVyLmV4YW1wbGUiLAogICJyZXZpc2lvbl9pZCI6ICJyZXZpc2lvbi0xIiwKICAidHlwIjogImFkY3AtYWRkb24tY2hhaW4randzIiwKICAidmFyaWFudF9pZCI6ICJ2YXJpYW50LTAiCn0"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "0ygZiUh8Gw_fwLofa2Q7XwYWyZxHqf0Dq_Wbj3qt-wlwNufFT5XcBNzxuisZAssrpv5qXyBRKAqiDSgbjlYhBw"
+ }
+ ],
+ "expected": "canonicalization"
+ },
+ {
+ "id": "corrupt-signature",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ }
+ ],
+ "expected": "signature"
+ },
+ {
+ "id": "padded-base64url",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.protected",
+ "value": "eyJhbGciOiJFZERTQSIsImtpZCI6ImFkZG9uLXZlY3Rvci1rZXkiLCJ0eXAiOiJhZGNwLWFkZG9uLWNoYWluK2p3cyJ9="
+ }
+ ],
+ "expected": "encoding"
+ },
+ {
+ "id": "future-issuance",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjQyNzgxMDA0YTQ1MmQ2NTM0M2U1OTBmNzJjNGM0N2RjNDMxYmFlYzBjYmFjYmJjMTliM2U1YWRjNDBhODVjMTUiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkzMjAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "41FK4gHr4ehN8ieNLMkyr0nz3AzvY4VG6saDOGsgwmBspM1_wEB6f6AmtrUqT_hSN6ARJfvN-qMo8DrNBphLAA"
+ }
+ ],
+ "expected": "time"
+ },
+ {
+ "id": "revoked-at-issuance",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.key.revoked_at",
+ "value": "2026-10-14T12:00:00Z"
+ }
+ ],
+ "expected": "revocation"
+ },
+ {
+ "id": "backdated-without-observed-history",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.key.revoked_at",
+ "value": "2026-10-14T12:01:00Z"
+ },
+ {
+ "path": "trust.now",
+ "value": 1791979320
+ }
+ ],
+ "expected": "revocation"
+ },
+ {
+ "id": "rewritten-observation-history",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.key.revoked_at",
+ "value": "2026-10-14T12:01:00Z"
+ },
+ {
+ "path": "trust.observed_at",
+ "value": 1791979220
+ },
+ {
+ "path": "trust.observed_chain_hash",
+ "value": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ ],
+ "expected": "revocation"
+ },
+ {
+ "id": "signed-required-omission",
+ "base": 2,
+ "edits": [
+ {
+ "path": "chain.configuration.vendors.0.mode",
+ "value": "required"
+ },
+ {
+ "path": "chain.configuration.vendors.0.max_hold_seconds",
+ "value": 60
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OmM0YjZjYzEyZGQ3NGI4YmMwYWE5NzY2MWZjN2ZjYjE4MGY4NGZjZWE4YjVmZTM3ZDZmYjM0MTRhOTgxZjYxNGIiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6MDFjMzg0MTJmMDM1YTZjNGI4ZGIyNTE5YjcwNWFlZWY2M2Q3YmM3Y2E4MzI2NzVjOTZjYmQwNDg3Mzk4ZmFhMiIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMiIsImZvcm1hdF9raW5kIjoiZGlzcGxheV90YWciLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMiJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "8-WYqDhoPIMw53nB5xBolEHml_B-PD3CxOJcGWzKiI4dYdj56cgqH0_W-CbkUbYikO1iiC2tERw061XQunu_DQ"
+ }
+ ],
+ "expected": "required_omission"
+ },
+ {
+ "id": "signed-registration-mismatch",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.entries.0.agent_name",
+ "value": "other-vendor"
+ },
+ {
+ "path": "chain.chain_hash",
+ "value": "sha256:d3ead4079358d552a47c24dc3f9b3eafab74b8b72547abfcec0c468d94d25d98"
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OmQzZWFkNDA3OTM1OGQ1NTJhNDdjMjRkYzNmOWIzZWFmYWI3NGI4YjcyNTQ3YWJmY2VjMGM0NjhkOTRkMjVkOTgiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "xxAJc5KZXfCroenUGBeZiy94M35bGRjI4qu4UFp1HjD0Kn42Yfpz1pESc52KBxrGpWpJ0X2d05VB7SuPE0EdDQ"
+ }
+ ],
+ "expected": "entry"
+ },
+ {
+ "id": "signed-invalid-order",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.entries.0.order",
+ "value": 1
+ },
+ {
+ "path": "chain.chain_hash",
+ "value": "sha256:c041763c53c4b16d434924d5871857a209f02a2cd332bf84627b595295240d85"
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OmMwNDE3NjNjNTNjNGIxNmQ0MzQ5MjRkNTg3MTg1N2EyMDlmMDJhMmNkMzMyYmY4NDYyN2I1OTUyOTUyNDBkODUiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "1wlUsByPkWwEdFNQ85ObtgJXX-HXFbbPCkEqJXgRpWJzOZoSIOE2NiipIGCq-qbOcHMBSDSkCtGzVSXV6dG6CA"
+ }
+ ],
+ "expected": "entry"
+ },
+ {
+ "id": "publisher-pin-revoked-at-issuance",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.publisher_pin.revoked_at",
+ "value": "2026-10-14T12:00:00Z"
+ }
+ ],
+ "expected": "revocation"
+ },
+ {
+ "id": "publisher-pin-purpose-conflict",
+ "base": 0,
+ "edits": [
+ {
+ "path": "trust.publisher_pin.adcp_use",
+ "value": "request-signing"
+ }
+ ],
+ "expected": "publisher_pin"
+ },
+ {
+ "id": "signed-missing-byte-count",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.entries.0.bytes",
+ "remove": true
+ },
+ {
+ "path": "chain.chain_hash",
+ "value": "sha256:7ea0f8c6400acefdde46c0b5c0a7726170c4725461fb9c4d5760d207b53e827a"
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjdlYTBmOGM2NDAwYWNlZmRkZTQ2YzBiNWMwYTc3MjYxNzBjNDcyNTQ2MWZiOWM0ZDU3NjBkMjA3YjUzZTgyN2EiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "W9C-9S4uZdnK9uPgYlkh8-vbF2umuEPyUkIdkiQtUrlysDODVkwjtJN4n61jLGGky9T3Fm4VG8rV-mzqJlfhBA"
+ }
+ ],
+ "expected": "schema"
+ },
+ {
+ "id": "unknown-signed-claim",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjQyNzgxMDA0YTQ1MmQ2NTM0M2U1OTBmNzJjNGM0N2RjNDMxYmFlYzBjYmFjYmJjMTliM2U1YWRjNDBhODVjMTUiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidW5yZWNvZ25pemVkIjoiY2xhaW0iLCJ2YXJpYW50X2lkIjoidmFyaWFudC0wIn0"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "1MysOVMqWoB0ssw9Vu7EF4gtbnFwbf-JRFYbkqfMgQuEUJuJdBBNAdYVj0P68h2EWRw1qPMV6ykz7mVR1Rb1DA"
+ }
+ ],
+ "expected": "schema"
+ },
+ {
+ "id": "signed-fragment-index-gap",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.entries.0.fragment_index",
+ "value": 1
+ },
+ {
+ "path": "chain.chain_hash",
+ "value": "sha256:64a3173f8ce60a786d957e38c11cd8a486f184069034da6ed99cfe318bc33a7c"
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjY0YTMxNzNmOGNlNjBhNzg2ZDk1N2UzOGMxMWNkOGE0ODZmMTg0MDY5MDM0ZGE2ZWQ5OWNmZTMxOGJjMzNhN2MiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZmM5MTJiODAyZmRhNTZmZGFhZTIyMjhiZTM2OTVlZTkxY2Q2YTY4ZGM3YWE5M2Y4OTlmNGE1NDYwOTZiM2Y3ZiIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "VTrI2TtGFgQIhz8fzYVIHFPjpCBXQtKPqtddLza4fli2w9ofP9Eawrp568c6DmizTxNYM31W1ADqK6DwlxH3Dg"
+ }
+ ],
+ "expected": "entry"
+ },
+ {
+ "id": "signed-reversed-omission-order",
+ "base": 2,
+ "edits": [
+ {
+ "path": "chain.configuration.vendors.1",
+ "value": {
+ "agent_name": "second",
+ "source": "live",
+ "mode": "best_effort",
+ "format_kinds": [
+ "image",
+ "html5",
+ "display_tag"
+ ],
+ "positions": [
+ "append"
+ ],
+ "agent_url": "https://second.example/agent"
+ }
+ },
+ {
+ "path": "chain.entries.0.registration_index",
+ "value": 1
+ },
+ {
+ "path": "chain.entries.0.agent_name",
+ "value": "second"
+ },
+ {
+ "path": "chain.entries.0.agent_url",
+ "value": "https://second.example/agent"
+ },
+ {
+ "path": "chain.entries.1",
+ "value": {
+ "order": 1,
+ "registration_index": 0,
+ "agent_name": "measure",
+ "source": "live",
+ "agent_url": "https://measurement.example/agent",
+ "timestamp": "2026-10-14T12:00:00Z",
+ "status": "omitted",
+ "reason": "vendor_omission"
+ }
+ },
+ {
+ "path": "chain.chain_hash",
+ "value": "sha256:4981dc7ee322641c28bd05018359dfad11813303360d7df8799b159697560953"
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjQ5ODFkYzdlZTMyMjY0MWMyOGJkMDUwMTgzNTlkZmFkMTE4MTMzMDMzNjBkN2RmODc5OWIxNTk2OTc1NjA5NTMiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6MzI4M2FmZDgzMGZlZmE1ODBkODg3ZWIxZjk5NzYwYjQwNGI5OWY5N2RmNTNkOGYxZmFmYTVhNTk2ODNjMTkyOSIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6MDFjMzg0MTJmMDM1YTZjNGI4ZGIyNTE5YjcwNWFlZWY2M2Q3YmM3Y2E4MzI2NzVjOTZjYmQwNDg3Mzk4ZmFhMiIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMiIsImZvcm1hdF9raW5kIjoiZGlzcGxheV90YWciLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMiJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "ekBYpIzXufRrNKtsFroMIXLlCK42mffO5gywP4Sw3YcvODk08qTMPgoKcANSOP14xpHuIqCCY5PX-8FPvK6LBg"
+ }
+ ],
+ "expected": "order"
+ },
+ {
+ "id": "signed-duplicate-fragment-index-across-positions",
+ "base": 0,
+ "edits": [
+ {
+ "path": "chain.configuration.vendors.0.positions.0",
+ "value": "prepend"
+ },
+ {
+ "path": "chain.configuration.vendors.0.positions.1",
+ "value": "append"
+ },
+ {
+ "path": "chain.entries.0.position",
+ "value": "prepend"
+ },
+ {
+ "path": "chain.entries.1",
+ "value": {
+ "order": 1,
+ "registration_index": 0,
+ "agent_name": "measure",
+ "source": "live",
+ "agent_url": "https://measurement.example/agent",
+ "timestamp": "2026-10-14T12:00:00Z",
+ "status": "applied",
+ "position": "append",
+ "fragment_index": 0,
+ "content_hash": "sha256:1a80add994605ab8ceebe062b3a2014ab0170388bb12bbfcee4f276154622a39",
+ "bytes": 61
+ }
+ },
+ {
+ "path": "chain.chain_hash",
+ "value": "sha256:0a53ca7066358b14218f9b27f64664ea71e273d2653cf9399f34148c22d4a6d6"
+ },
+ {
+ "path": "chain.signed_attestation.payload",
+ "value": "eyJhZ2VudF91cmwiOiJodHRwczovL3NhbGVzLmV4YW1wbGUvYWdlbnQiLCJjaGFpbl9oYXNoIjoic2hhMjU2OjBhNTNjYTcwNjYzNThiMTQyMThmOWIyN2Y2NDY2NGVhNzFlMjczZDI2NTNjZjkzOTlmMzQxNDhjMjJkNGE2ZDYiLCJjb25maWd1cmF0aW9uX2hhc2giOiJzaGEyNTY6ZDRlMzE2MDVkMzFjOTFjMTI2ZWJkZTJiMWRlY2E3YWY1OGIyYTQ5ODU3OWU4YTRhNjI4OTM4MTg3ZTNjNDFkYyIsImNyZWF0aXZlX2hhc2giOiJzaGEyNTY6NzQ5NzMxZWMxODMwZWEzYTU5YTUxNzQ3OWI2MGFlOTU1YjU0OTI5ZmIzOGViZTJiODFhNDQ0YjdhNGJjMzg4ZCIsImNyZWF0aXZlX2lkIjoiY3JlYXRpdmUtMCIsImZvcm1hdF9raW5kIjoiaW1hZ2UiLCJpYXQiOjE3OTE5NzkyMDAsIm1lZGlhX2J1eV9pZCI6ImJ1eS0xIiwicGFja2FnZV9pZCI6InBhY2thZ2UtMSIsInB1Ymxpc2hlcl9kb21haW4iOiJwdWJsaXNoZXIuZXhhbXBsZSIsInJldmlzaW9uX2lkIjoicmV2aXNpb24tMSIsInR5cCI6ImFkY3AtYWRkb24tY2hhaW4randzIiwidmFyaWFudF9pZCI6InZhcmlhbnQtMCJ9"
+ },
+ {
+ "path": "chain.signed_attestation.signature",
+ "value": "SHgEW66J6mwKeDk_Lw5KCLu_5PUEjVQef6w61HopHZS5ZtKWkIrf9AV4rKJCDVYM1ETKmoN3V63UskXntOIeCw"
+ }
+ ],
+ "expected": "entry"
+ }
+ ],
+ "behavior": [
+ {
+ "id": "required-empty-keeps-original-task",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 59,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": []
+ }
+ ]
+ },
+ "expected": {
+ "status": "working",
+ "task_id": "task-1"
+ }
+ },
+ {
+ "id": "required-empty-at-deadline-fails",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 60,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": []
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ },
+ {
+ "id": "required-without-async-fails",
+ "input": {
+ "async": false,
+ "task_id": "task-1",
+ "elapsed_seconds": 0,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": []
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ },
+ {
+ "id": "best-effort-deliberate-omission",
+ "input": {
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "best_effort",
+ "max_hold_seconds": 60,
+ "fragments": []
+ }
+ ]
+ },
+ "expected": {
+ "status": "completed",
+ "fragments": [],
+ "omitted": [
+ {
+ "registration_index": 0,
+ "reason": "vendor_omission"
+ }
+ ]
+ }
+ },
+ {
+ "id": "best-effort-network-failure",
+ "input": {
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "best_effort",
+ "max_hold_seconds": 60,
+ "fragments": [],
+ "error_code": "ADDON_UNREACHABLE"
+ }
+ ]
+ },
+ "expected": {
+ "status": "completed",
+ "fragments": [],
+ "omitted": [
+ {
+ "registration_index": 0,
+ "reason": "failure"
+ }
+ ]
+ }
+ },
+ {
+ "id": "required-bad-hash-fails",
+ "input": {
+ "async": false,
+ "elapsed_seconds": 0,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": [
+ {
+ "position": "append",
+ "content": "x",
+ "content_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ ]
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ },
+ {
+ "id": "static-does-not-call-vendor",
+ "input": {
+ "vendors": [
+ {
+ "source": "static_reference",
+ "mode": "required",
+ "position": "append",
+ "content": "static"
+ }
+ ]
+ },
+ "expected": {
+ "status": "completed",
+ "fragments": [
+ "static"
+ ],
+ "omitted": []
+ }
+ },
+ {
+ "id": "prepend-order-is-not-reversed",
+ "input": {
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "fragments": [
+ {
+ "position": "append",
+ "content": "A1",
+ "content_hash": "sha256:16a36e86f6fed5d465ff332511a0ce1a863b55d364b25a7cdaa25db19abf9648"
+ },
+ {
+ "position": "prepend",
+ "content": "A0",
+ "content_hash": "sha256:aa508c2187fca56f397ff75adc52b94e02f38122cdd48bd42105106e5e0f8e14"
+ }
+ ]
+ },
+ {
+ "source": "live",
+ "mode": "required",
+ "fragments": [
+ {
+ "position": "prepend",
+ "content": "B0",
+ "content_hash": "sha256:05816a1560db947d6ff798e30909816f400f14230e9a06afac8f8b213127aa21"
+ },
+ {
+ "position": "append",
+ "content": "B1",
+ "content_hash": "sha256:5b950e77941d01cdf246d00b1ece546bc95234b77d98b44c9187e2733afa696a"
+ }
+ ]
+ }
+ ]
+ },
+ "expected": {
+ "status": "completed",
+ "fragments": [
+ "A0",
+ "B0",
+ "A1",
+ "B1"
+ ],
+ "omitted": []
+ }
+ },
+ {
+ "id": "late-recovery-cannot-resume-expired-operation",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 61,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": [
+ {
+ "position": "append",
+ "content": "repaired",
+ "content_hash": "sha256:3baf75ce884746c426cafdff7e0a3ed3a52b4d70fb6dbf94bc754be4273a868a"
+ }
+ ],
+ "first_failure_seconds": 0
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ },
+ {
+ "id": "later-expired-vendor-overrides-earlier-pending",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 30,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": []
+ },
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 10,
+ "fragments": []
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ },
+ {
+ "id": "per-vendor-original-deadline-survives-restart",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 90,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": [],
+ "first_failure_seconds": 30
+ },
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": [],
+ "first_failure_seconds": 70
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ },
+ {
+ "id": "later-first-failure-keeps-its-own-deadline",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 90,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": [],
+ "first_failure_seconds": 50
+ }
+ ]
+ },
+ "expected": {
+ "status": "working",
+ "task_id": "task-1"
+ }
+ },
+ {
+ "id": "recovered-earlier-vendor-cannot-mask-expired-other-vendor",
+ "input": {
+ "async": true,
+ "task_id": "task-1",
+ "elapsed_seconds": 30,
+ "vendors": [
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 60,
+ "fragments": [
+ {
+ "position": "append",
+ "content": "repaired",
+ "content_hash": "sha256:3baf75ce884746c426cafdff7e0a3ed3a52b4d70fb6dbf94bc754be4273a868a"
+ }
+ ]
+ },
+ {
+ "source": "live",
+ "mode": "required",
+ "max_hold_seconds": 10,
+ "fragments": []
+ }
+ ]
+ },
+ "expected": {
+ "status": "failed",
+ "code": "ADDON_REQUIRED_UNAVAILABLE"
+ }
+ }
+ ]
+}
diff --git a/static/compliance/source/test-vectors/creative-addon-chain.schema.json b/static/compliance/source/test-vectors/creative-addon-chain.schema.json
new file mode 100644
index 00000000000..fca2e2f7487
--- /dev/null
+++ b/static/compliance/source/test-vectors/creative-addon-chain.schema.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "creative-addon-chain.schema.json",
+ "title": "Creative addon chain conformance vectors",
+ "type": "object",
+ "required": ["profile", "test_private_key", "valid", "invalid", "behavior"],
+ "properties": {
+ "profile": { "const": "adcp/addon-chain/v1" },
+ "test_private_key": { "type": "object", "description": "Public RFC 8032 fixture secret. Never use operationally." },
+ "valid": {
+ "type": "array", "minItems": 3,
+ "items": {
+ "type": "object",
+ "required": ["id", "creative_hash_input", "fragment", "fragment_hash", "chain", "trust"],
+ "properties": {
+ "id": { "type": "string" },
+ "chain": { "$ref": "/schemas/core/creative-addon-chain.json" },
+ "trust": {
+ "type": "object", "required": ["key", "publisher_pin", "expected", "now"],
+ "properties": {
+ "key": { "$ref": "/schemas/core/creative-addon-signing-key.json" },
+ "publisher_pin": { "$ref": "/schemas/core/creative-addon-signing-key.json" }
+ }
+ }
+ }
+ }
+ },
+ "invalid": {
+ "type": "array", "minItems": 1,
+ "items": {
+ "type": "object", "required": ["id", "base", "edits", "expected"],
+ "properties": {
+ "base": { "type": "integer", "minimum": 0 },
+ "edits": { "type": "array", "minItems": 1 },
+ "expected": { "type": "string" }
+ }
+ }
+ },
+ "behavior": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["id", "input", "expected"] } }
+ }
+}
diff --git a/static/schemas/source/account/sync-accounts-response.json b/static/schemas/source/account/sync-accounts-response.json
index f6cbd589028..1471f6846c8 100644
--- a/static/schemas/source/account/sync-accounts-response.json
+++ b/static/schemas/source/account/sync-accounts-response.json
@@ -25,6 +25,13 @@
"items": {
"type": "object",
"properties": {
+ "creative_addons": {
+ "allOf": [
+ { "$ref": "/schemas/core/creative-addon-disclosure.json" },
+ { "properties": { "scope": { "const": "account" } } }
+ ],
+ "description": "Account-scoped current configuration for this successful response row. Completeness is relative to this account, not all product-specific rules. No credentials or loader bodies are disclosed."
+ },
"account_id": {
"type": "string",
"description": "Seller-assigned account identifier and the account's identity. A seller never issues an account_id it might later change. Sellers MUST return it on every non-failed provisioning result, except a pending_approval row that may still resolve to a different account: that row carries request_id (experimental, account.requests) and no account_id. Sellers MUST accept account_id as an AccountRef on every account-scoped task and MUST NOT change it for the life of the account. Buyers store it and send it on subsequent create_media_buy and other account-scoped operations.",
@@ -247,6 +254,10 @@
]
},
"allOf": [
+ {
+ "if": { "properties": { "action": { "const": "failed" } }, "required": ["action"] },
+ "then": { "not": { "required": ["creative_addons"] } }
+ },
{
"description": "Settings-update-mode results (echoing the request's `account` field) anchor the row on `account` alone; provisioning-mode results anchor it on the buyer-declared `brand` + `operator` instead, never both. Mirrors the same discriminator sync_governance's response rows already use.",
"if": {
diff --git a/static/schemas/source/core/canonical-product.json b/static/schemas/source/core/canonical-product.json
index 59bf4aa1af1..09b4a9da916 100644
--- a/static/schemas/source/core/canonical-product.json
+++ b/static/schemas/source/core/canonical-product.json
@@ -146,6 +146,12 @@
"items": { "$ref": "/schemas/core/canonical-product-action.json" },
"uniqueItems": true
},
+ "creative_addons": {
+ "allOf": [
+ { "$ref": "/schemas/core/creative-addon-disclosure.json" },
+ { "properties": { "scope": { "const": "product" } } }
+ ]
+ },
"hold_policy": {
"$ref": "/schemas/core/hold-policy.json",
"x-status": "experimental",
diff --git a/static/schemas/source/core/creative-addon-capability.json b/static/schemas/source/core/creative-addon-capability.json
new file mode 100644
index 00000000000..99e2b79ac22
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-capability.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-capability.json",
+ "title": "Creative Addon Capability",
+ "description": "Seller-adjacent fragment support. Omission means unsupported. All advertised routes require a seller-controlled adjacent container and the mandatory signing profile.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "supported_format_kinds": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag"
+ ]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "positions": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "prepend",
+ "append"
+ ]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "signed_chain": {
+ "type": "object",
+ "properties": {
+ "required": {
+ "type": "boolean",
+ "const": true
+ },
+ "profile": {
+ "type": "string",
+ "const": "adcp/addon-chain/v1"
+ }
+ },
+ "required": [
+ "required",
+ "profile"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "supported_format_kinds",
+ "positions",
+ "signed_chain"
+ ],
+ "additionalProperties": false
+}
diff --git a/static/schemas/source/core/creative-addon-chain.json b/static/schemas/source/core/creative-addon-chain.json
new file mode 100644
index 00000000000..016b7d7e801
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-chain.json
@@ -0,0 +1,449 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-chain.json",
+ "title": "Creative Addon Chain",
+ "description": "Signed seller assembly claim for one immutable served variant. Exposes hashes and metadata, never fragment code. A new effective configuration or chain requires a distinct variant_id.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "creative_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "creative"
+ },
+ "creative_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "configuration": {
+ "$ref": "/schemas/core/creative-addon-disclosure.json",
+ "description": "Complete effective configuration actually used for this execution, retained immutably. All fields are covered by configuration_hash in signed claims."
+ },
+ "entries": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "order": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "registration_index": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "agent_name": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^\\S(?:.*\\S)?$",
+ "description": "Canonical agent_name, normalized by trimming surrounding whitespace and converting to lowercase. Identity is checked against the selected vendor, never inferred from this label alone."
+ },
+ "agent_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "source": {
+ "type": "string",
+ "enum": [
+ "live",
+ "static_reference"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string",
+ "const": "applied"
+ },
+ "position": {
+ "type": "string",
+ "enum": [
+ "prepend",
+ "append"
+ ]
+ },
+ "fragment_index": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "content_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "bytes": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "vendor_ref": {
+ "type": "string",
+ "minLength": 1
+ },
+ "detail_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ },
+ "required": [
+ "order",
+ "registration_index",
+ "agent_name",
+ "source",
+ "timestamp",
+ "status",
+ "position",
+ "fragment_index",
+ "content_hash",
+ "bytes"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "source": {
+ "const": "live"
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
+ "then": {
+ "required": [
+ "agent_url"
+ ]
+ },
+ "else": {
+ "not": {
+ "required": [
+ "agent_url"
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "order": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "registration_index": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "agent_name": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^\\S(?:.*\\S)?$",
+ "description": "Canonical agent_name, normalized by trimming surrounding whitespace and converting to lowercase. Identity is checked against the selected vendor, never inferred from this label alone."
+ },
+ "agent_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "source": {
+ "type": "string",
+ "enum": [
+ "live",
+ "static_reference"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string",
+ "const": "omitted"
+ },
+ "reason": {
+ "type": "string",
+ "enum": [
+ "vendor_omission",
+ "failure"
+ ]
+ },
+ "error_code": {
+ "type": "string",
+ "enum": [
+ "ADDON_UNREACHABLE",
+ "ADDON_AUTH_FAILED",
+ "ADDON_HASH_MISMATCH",
+ "ADDON_INVALID_POSITION",
+ "ADDON_UNDECLARED_FORMAT",
+ "ADDON_MALFORMED_FRAGMENT",
+ "ADDON_REQUIRED_UNAVAILABLE"
+ ]
+ }
+ },
+ "required": [
+ "order",
+ "registration_index",
+ "agent_name",
+ "source",
+ "timestamp",
+ "status",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "source": {
+ "const": "live"
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
+ "then": {
+ "required": [
+ "agent_url"
+ ]
+ },
+ "else": {
+ "not": {
+ "required": [
+ "agent_url"
+ ]
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "reason": {
+ "const": "failure"
+ }
+ },
+ "required": [
+ "reason"
+ ]
+ },
+ "then": {
+ "required": [
+ "error_code"
+ ]
+ },
+ "else": {
+ "not": {
+ "required": [
+ "error_code"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "discriminator": {
+ "propertyName": "status"
+ }
+ },
+ "minItems": 0
+ },
+ "chain_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "signed_attestation": {
+ "type": "object",
+ "properties": {
+ "profile": {
+ "type": "string",
+ "const": "adcp/addon-chain/v1"
+ },
+ "protected": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[A-Za-z0-9_-]+$"
+ },
+ "payload": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[A-Za-z0-9_-]+$"
+ },
+ "signature": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[A-Za-z0-9_-]{86}$"
+ }
+ },
+ "required": [
+ "profile",
+ "protected",
+ "payload",
+ "signature"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "creative_id",
+ "creative_hash",
+ "configuration",
+ "entries",
+ "chain_hash",
+ "signed_attestation"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "properties": {
+ "configuration": {
+ "properties": {
+ "complete": {
+ "const": true
+ },
+ "scope": {
+ "const": "product"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "definitions": {
+ "Claims": {
+ "type": "object",
+ "properties": {
+ "typ": {
+ "type": "string",
+ "const": "adcp-addon-chain+jws"
+ },
+ "agent_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "publisher_domain": {
+ "type": "string",
+ "minLength": 1
+ },
+ "creative_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "creative"
+ },
+ "creative_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "chain_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "configuration_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "format_kind": {
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag"
+ ]
+ },
+ "variant_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "served_variant"
+ },
+ "media_buy_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "media_buy"
+ },
+ "package_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "package"
+ },
+ "placement_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "revision_id": {
+ "$ref": "/schemas/core/creative-revision-id.json"
+ },
+ "locale_variant_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "creative_locale_variant"
+ },
+ "iat": {
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "required": [
+ "typ",
+ "agent_url",
+ "publisher_domain",
+ "creative_id",
+ "creative_hash",
+ "chain_hash",
+ "configuration_hash",
+ "format_kind",
+ "variant_id",
+ "media_buy_id",
+ "iat"
+ ],
+ "additionalProperties": false
+ },
+ "ProtectedHeader": {
+ "type": "object",
+ "properties": {
+ "alg": {
+ "type": "string",
+ "const": "EdDSA"
+ },
+ "kid": {
+ "type": "string",
+ "minLength": 1
+ },
+ "typ": {
+ "type": "string",
+ "const": "adcp-addon-chain+jws"
+ }
+ },
+ "required": [
+ "alg",
+ "kid",
+ "typ"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "x-adcp-validation": {
+ "verifier_constraints": {
+ "cryptographic_validation": "Verify JWS, chain/configuration hashes, dedicated key purpose, publisher authority, and every delivery identity using the addon-chain-signing specification. Shape validation is insufficient.",
+ "entry_coverage": "Every configured applicable registration has one omission or at least one applied entry; required registrations MUST have applied entries only. Entries MUST match their registration identity and source.",
+ "ordering": "Applied entries are prepend then append, ordered by registration_index then fragment_index; omissions follow in registration order. order is the zero-based position in entries."
+ },
+ "spec": "docs/creative/addon-chain-signing.mdx"
+ }
+}
diff --git a/static/schemas/source/core/creative-addon-disclosure.json b/static/schemas/source/core/creative-addon-disclosure.json
new file mode 100644
index 00000000000..f3c34902fcc
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-disclosure.json
@@ -0,0 +1,196 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-disclosure.json",
+ "title": "Creative Addon Disclosure",
+ "description": "Read-only seller configuration in registration order. complete=true with vendors=[] explicitly declares an empty set; omission is undeclared. Product and account disclosures are resolved into one complete product snapshot before commitment. Contains no fragment bodies or credentials.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "scope": {
+ "type": "string",
+ "enum": [
+ "product",
+ "account"
+ ]
+ },
+ "product_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "product"
+ },
+ "complete": {
+ "type": "boolean"
+ },
+ "signed_chain_profile": {
+ "type": "string",
+ "const": "adcp/addon-chain/v1"
+ },
+ "vendors": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "agent_name": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^\\S(?:.*\\S)?$",
+ "description": "Canonical agent_name, normalized by trimming surrounding whitespace and converting to lowercase. Identity is checked against the selected vendor, never inferred from this label alone."
+ },
+ "source": {
+ "type": "string",
+ "enum": [
+ "live",
+ "static_reference"
+ ]
+ },
+ "agent_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "mode": {
+ "type": "string",
+ "enum": [
+ "required",
+ "best_effort"
+ ]
+ },
+ "max_hold_seconds": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "format_kinds": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag"
+ ]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "positions": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "prepend",
+ "append"
+ ]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "static_content_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ },
+ "required": [
+ "agent_name",
+ "source",
+ "mode",
+ "format_kinds",
+ "positions"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "mode": {
+ "const": "required"
+ }
+ },
+ "required": [
+ "mode"
+ ]
+ },
+ "then": {
+ "required": [
+ "max_hold_seconds"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "source": {
+ "const": "live"
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
+ "then": {
+ "required": [
+ "agent_url"
+ ],
+ "not": {
+ "required": [
+ "static_content_hash"
+ ]
+ }
+ },
+ "else": {
+ "required": [
+ "static_content_hash"
+ ],
+ "not": {
+ "required": [
+ "agent_url"
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "minItems": 0
+ }
+ },
+ "required": [
+ "scope",
+ "complete",
+ "signed_chain_profile",
+ "vendors"
+ ],
+ "additionalProperties": false,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "scope": {
+ "const": "product"
+ }
+ },
+ "required": [
+ "scope"
+ ]
+ },
+ "then": {
+ "required": [
+ "product_id"
+ ]
+ },
+ "else": {
+ "not": {
+ "required": [
+ "product_id"
+ ]
+ }
+ }
+ }
+ ],
+ "x-adcp-validation": {
+ "verifier_constraints": {
+ "vendor_identity": "agent_name MUST be unique after lowercase-and-trim normalization within this configuration",
+ "scope_binding": "product_id MUST equal the containing product or purchase; account scope inherits identity from its sync_accounts row"
+ }
+ }
+}
diff --git a/static/schemas/source/core/creative-addon-failure.json b/static/schemas/source/core/creative-addon-failure.json
new file mode 100644
index 00000000000..4da570d7618
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-failure.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-failure.json",
+ "title": "Creative Addon Failure",
+ "description": "Feedback to the affected vendor only. The Sales Agent MUST exclude other vendors, buyer payloads, and credentials. The same entry shape is used in prior_failures and optional notification_url POST bodies.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "error_code": {
+ "type": "string",
+ "enum": [
+ "ADDON_UNREACHABLE",
+ "ADDON_AUTH_FAILED",
+ "ADDON_HASH_MISMATCH",
+ "ADDON_INVALID_POSITION",
+ "ADDON_UNDECLARED_FORMAT",
+ "ADDON_MALFORMED_FRAGMENT",
+ "ADDON_REQUIRED_UNAVAILABLE"
+ ]
+ },
+ "creative_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "creative"
+ },
+ "format_kind": {
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag"
+ ]
+ },
+ "position": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Observed position, including invalid values, for diagnosis."
+ },
+ "vendor_ref": {
+ "type": "string",
+ "minLength": 1
+ },
+ "detail": {
+ "type": "string",
+ "minLength": 1
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "error_code",
+ "creative_id",
+ "format_kind",
+ "timestamp"
+ ],
+ "additionalProperties": false
+}
diff --git a/static/schemas/source/core/creative-addon-fragment.json b/static/schemas/source/core/creative-addon-fragment.json
new file mode 100644
index 00000000000..3295931f006
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-fragment.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-fragment.json",
+ "title": "Creative Addon Fragment",
+ "description": "Vendor-to-Sales-Agent fragment. The Sales Agent checks SHA-256 over exact UTF-8 content bytes before placement. This object MUST NOT appear in buyer reporting.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "creative_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "creative"
+ },
+ "position": {
+ "type": "string",
+ "enum": [
+ "prepend",
+ "append"
+ ]
+ },
+ "content": {
+ "type": "string",
+ "minLength": 1
+ },
+ "content_hash": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "vendor_ref": {
+ "type": "string",
+ "minLength": 1
+ },
+ "detail_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ },
+ "required": [
+ "creative_id",
+ "position",
+ "content",
+ "content_hash"
+ ],
+ "additionalProperties": false
+}
diff --git a/static/schemas/source/core/creative-addon-registration.json b/static/schemas/source/core/creative-addon-registration.json
new file mode 100644
index 00000000000..3f62465a5dd
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-registration.json
@@ -0,0 +1,130 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-registration.json",
+ "title": "Creative Addon Registration",
+ "description": "Operator-local configuration, never a buyer mutation surface. Exactly one live agent URL or static reference is required. Required mode has a finite deadline measured from the first failed attempt of the invoking operation.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "agent_name": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^\\S(?:.*\\S)?$",
+ "description": "Canonical agent_name, normalized by trimming surrounding whitespace and converting to lowercase. Identity is checked against the selected vendor, never inferred from this label alone."
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "agent_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "notification_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "mode": {
+ "type": "string",
+ "enum": [
+ "required",
+ "best_effort"
+ ]
+ },
+ "max_hold_seconds": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "static_reference": {
+ "type": "object",
+ "properties": {
+ "format_kinds": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag"
+ ]
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ },
+ "position": {
+ "type": "string",
+ "enum": [
+ "prepend",
+ "append"
+ ]
+ },
+ "content": {
+ "type": "string",
+ "minLength": 1
+ },
+ "detail_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ },
+ "required": [
+ "format_kinds",
+ "position",
+ "content"
+ ],
+ "additionalProperties": false
+ },
+ "auth": {
+ "type": "object",
+ "description": "Operator-local credential configuration. Never serialized into disclosure, vendor context, or reporting. Storage and supported credential mechanisms are implementation-specific."
+ }
+ },
+ "required": [
+ "agent_name",
+ "mode"
+ ],
+ "additionalProperties": false,
+ "anyOf": [
+ {
+ "required": [
+ "agent_url"
+ ]
+ },
+ {
+ "required": [
+ "static_reference"
+ ]
+ }
+ ],
+ "not": {
+ "required": [
+ "agent_url",
+ "static_reference"
+ ]
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "mode": {
+ "const": "required"
+ }
+ },
+ "required": [
+ "mode"
+ ]
+ },
+ "then": {
+ "required": [
+ "max_hold_seconds"
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/core/creative-addon-signing-key.json b/static/schemas/source/core/creative-addon-signing-key.json
new file mode 100644
index 00000000000..25cb9564221
--- /dev/null
+++ b/static/schemas/source/core/creative-addon-signing-key.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/creative-addon-signing-key.json",
+ "title": "Creative Addon Signing Key",
+ "description": "Dedicated public key for adcp/addon-chain/v1, resolved through trusted Sales Agent identity and existing publisher pins. This schema does not establish authorization or a trust anchor.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "kid": {
+ "type": "string",
+ "minLength": 1
+ },
+ "kty": {
+ "type": "string",
+ "const": "OKP"
+ },
+ "crv": {
+ "type": "string",
+ "const": "Ed25519"
+ },
+ "alg": {
+ "type": "string",
+ "const": "EdDSA"
+ },
+ "use": {
+ "type": "string",
+ "const": "sig"
+ },
+ "key_ops": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "const": "verify"
+ },
+ "minItems": 1,
+ "maxItems": 1
+ },
+ "adcp_use": {
+ "type": "string",
+ "const": "addon-chain-signing"
+ },
+ "x": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[A-Za-z0-9_-]{43}$"
+ },
+ "revoked_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "kid",
+ "kty",
+ "crv",
+ "alg",
+ "use",
+ "key_ops",
+ "adcp_use",
+ "x"
+ ],
+ "additionalProperties": false
+}
diff --git a/static/schemas/source/core/creative-variant.json b/static/schemas/source/core/creative-variant.json
index 4110d8cf044..cbe05c12449 100644
--- a/static/schemas/source/core/creative-variant.json
+++ b/static/schemas/source/core/creative-variant.json
@@ -54,6 +54,9 @@
"description": "Buyer-assigned locale variant that supplied the served assets. Required for every delivered variant of a localized creative, including default fallback delivery. Omitted for unlocalized creatives.",
"x-entity": "creative_locale_variant"
},
+ "addon_chain": {
+ "$ref": "/schemas/core/creative-addon-chain.json"
+ },
"manifest": {
"$ref": "/schemas/core/creative-manifest.json",
"description": "The rendered creative manifest for this variant — the actual output that was served, not the input assets. In 3.2 it carries canonical `format_kind`, optional `format_option_ref`, and the resolved assets (specific headline, image, video, etc. the platform selected or generated). For Tier 2, shows which asset combination was picked. For Tier 3, contains the generated assets which may differ entirely from the input brand identity. Pass to preview_creative to re-render."
diff --git a/static/schemas/source/core/product.json b/static/schemas/source/core/product.json
index 4120c9d4c7a..574bb151bc0 100644
--- a/static/schemas/source/core/product.json
+++ b/static/schemas/source/core/product.json
@@ -170,6 +170,12 @@
"$ref": "/schemas/core/cancellation-policy.json",
"description": "Cancellation terms for this product. Declares the minimum notice period required before cancellation takes effect and any penalties for insufficient notice. Relevant for guaranteed delivery products. Buyers accept these terms by creating a media buy against the product."
},
+ "creative_addons": {
+ "allOf": [
+ { "$ref": "/schemas/core/creative-addon-disclosure.json" },
+ { "properties": { "scope": { "const": "product" } } }
+ ]
+ },
"hold_policy": {
"$ref": "/schemas/core/hold-policy.json",
"x-status": "experimental",
diff --git a/static/schemas/source/creative/get-creative-addons-request.json b/static/schemas/source/creative/get-creative-addons-request.json
new file mode 100644
index 00000000000..8df524185bf
--- /dev/null
+++ b/static/schemas/source/creative/get-creative-addons-request.json
@@ -0,0 +1,138 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/creative/get-creative-addons-request.json",
+ "title": "Get Creative Addons Request",
+ "description": "One operation-scoped batch from an authorized Sales Agent to a registered addon vendor. Authentication establishes operator identity; operator_name is informational.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "properties": {
+ "adcp_version": {
+ "$ref": "/schemas/core/version-envelope.json#/properties/adcp_version"
+ },
+ "adcp_major_version": {
+ "$ref": "/schemas/core/version-envelope.json#/properties/adcp_major_version"
+ },
+ "idempotency_key": {
+ "type": "string",
+ "minLength": 16,
+ "maxLength": 255,
+ "pattern": "^[A-Za-z0-9_.:-]{16,255}$"
+ },
+ "operator_name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "operator_url": {
+ "type": "string",
+ "minLength": 1,
+ "format": "uri",
+ "pattern": "^https://"
+ },
+ "publisher_domain": {
+ "type": "string",
+ "minLength": 1
+ },
+ "account_id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Vendor-assigned account identity; not the buyer execution account."
+ },
+ "placement": {
+ "type": "object",
+ "properties": {
+ "property_id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "product_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "product"
+ }
+ },
+ "required": [],
+ "additionalProperties": false
+ },
+ "creatives": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "creative_id": {
+ "type": "string",
+ "minLength": 1,
+ "x-entity": "creative"
+ },
+ "format_kind": {
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag"
+ ]
+ },
+ "dimensions": {
+ "type": "object",
+ "properties": {
+ "w": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "h": {
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "required": [
+ "w",
+ "h"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "creative_id",
+ "format_kind"
+ ],
+ "additionalProperties": false
+ },
+ "minItems": 1
+ },
+ "prior_failures": {
+ "type": "array",
+ "items": {
+ "$ref": "/schemas/core/creative-addon-failure.json"
+ },
+ "minItems": 1
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "required": [
+ "idempotency_key",
+ "operator_name",
+ "operator_url",
+ "publisher_domain",
+ "creatives"
+ ],
+ "additionalProperties": false,
+ "x-tool-summary": "Return seller-adjacent fragments from creative context without receiving buyer creative content.",
+ "x-mutates-state": true,
+ "x-idempotency-key-required": true,
+ "allOf": [
+ {
+ "$ref": "/schemas/core/version-envelope.json"
+ }
+ ],
+ "x-adcp-validation": {
+ "verifier_constraints": {
+ "privacy": "No creative payload, markup, asset URL, or buyer secrets may appear anywhere, including context and ext.",
+ "creative_identity": "creative_id values are unique within one batch; every returned creative_id MUST be a member of this batch.",
+ "idempotency": "The key identifies one vendor evaluation attempt. Ambiguous/in-flight retries preserve key and exact payload. After a definitive unusable result, a fresh attempt may use a new key and new failure feedback within the same seller task and original hold deadline. Distinct addon operations never reuse live fragments."
+ }
+ }
+}
diff --git a/static/schemas/source/creative/get-creative-addons-response.json b/static/schemas/source/creative/get-creative-addons-response.json
new file mode 100644
index 00000000000..bf073e45ef8
--- /dev/null
+++ b/static/schemas/source/creative/get-creative-addons-response.json
@@ -0,0 +1,78 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/creative/get-creative-addons-response.json",
+ "title": "Get Creative Addons Response",
+ "description": "Terminal vendor result. addons=[] is an intentional vendor omission, which the seller MUST treat as failure for a required registration. A terminal errors result supplies no fragments.",
+ "x-added-in": "3.3.0",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "/schemas/core/version-envelope.json"
+ },
+ {
+ "$ref": "/schemas/core/protocol-envelope.json"
+ },
+ {
+ "if": {
+ "required": [
+ "addons"
+ ]
+ },
+ "then": {
+ "properties": {
+ "status": {
+ "const": "completed"
+ }
+ }
+ },
+ "else": {
+ "properties": {
+ "status": {
+ "const": "failed"
+ }
+ }
+ }
+ }
+ ],
+ "properties": {
+ "addons": {
+ "type": "array",
+ "items": {
+ "$ref": "/schemas/core/creative-addon-fragment.json"
+ },
+ "minItems": 0
+ },
+ "errors": {
+ "type": "array",
+ "items": {
+ "$ref": "/schemas/core/error.json"
+ },
+ "minItems": 1
+ },
+ "context": {
+ "$ref": "/schemas/core/context.json"
+ },
+ "ext": {
+ "$ref": "/schemas/core/ext.json"
+ }
+ },
+ "anyOf": [
+ {
+ "required": [
+ "addons"
+ ]
+ },
+ {
+ "required": [
+ "errors"
+ ]
+ }
+ ],
+ "not": {
+ "required": [
+ "addons",
+ "errors"
+ ]
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/enums/error-code.json b/static/schemas/source/enums/error-code.json
index 4d6a9216651..9688c45f60a 100644
--- a/static/schemas/source/enums/error-code.json
+++ b/static/schemas/source/enums/error-code.json
@@ -10,6 +10,13 @@
],
"x-deprecated-enum-values-doc": "Deprecated in 3.3 with account.identity_updates; retained through 3.x and removed in 4.0.",
"enum": [
+ "ADDON_UNREACHABLE",
+ "ADDON_AUTH_FAILED",
+ "ADDON_HASH_MISMATCH",
+ "ADDON_INVALID_POSITION",
+ "ADDON_UNDECLARED_FORMAT",
+ "ADDON_MALFORMED_FRAGMENT",
+ "ADDON_REQUIRED_UNAVAILABLE",
"INVALID_REQUEST",
"AUTH_REQUIRED",
"AUTH_MISSING",
@@ -136,6 +143,13 @@
"RIGHTS_CHANNEL_VIOLATION"
],
"enumDescriptions": {
+ "ADDON_UNREACHABLE": "The addon vendor could not be reached before the call timeout. Recovery: transient (seller follows the finite required hold or records best-effort omission).",
+ "ADDON_AUTH_FAILED": "Addon vendor rejected operator credentials. Recovery: terminal (operator repairs vendor credentials; buyer credentials are unchanged).",
+ "ADDON_HASH_MISMATCH": "Fragment content_hash differs from SHA-256 of the exact UTF-8 content. Discard all this vendor's fragments for the affected creative. Recovery: terminal (vendor must correct the fragment).",
+ "ADDON_INVALID_POSITION": "Fragment position is unsupported by the contract or the vendor declaration. Recovery: terminal (vendor must correct the response).",
+ "ADDON_UNDECLARED_FORMAT": "Fragment targets an undeclared format or unknown creative context. Recovery: terminal (vendor must correct the response).",
+ "ADDON_MALFORMED_FRAGMENT": "Fragment cannot be safely assembled in the declared seller container, or a required vendor supplied no valid fragment. Recovery: terminal (operator or vendor repairs the integration).",
+ "ADDON_REQUIRED_UNAVAILABLE": "Seller-required addon could not be applied within the finite original hold deadline, or the invoking task cannot hold asynchronously. The affected execution MUST NOT deliver; buyer creative lifecycle is unchanged. Use the invoking task's normal terminal error arm. Recovery: terminal (surface seller infrastructure failure; a later user-directed retry follows the original task's idempotency contract).",
"INVALID_REQUEST": "Request is malformed, missing required fields, or violates schema constraints. Recovery: correctable (check request parameters and fix).",
"AUTH_REQUIRED": "**Deprecated** — use `AUTH_MISSING` (no credentials presented) or `AUTH_INVALID` (credentials presented and rejected). Retained as a backward-compatible alias during the 3.x deprecation window. Recovery: correctable.",
"AUTH_MISSING": "No credentials were presented. Sellers MUST return this code when no `Authorization` header was included in the request. Recovery: correctable (provide credentials via the auth header and retry).",
@@ -262,6 +276,13 @@
"RIGHTS_CHANNEL_VIOLATION": "Advisory. A creative's rights[] channels / excluded_channels restriction (core/rights-constraint.json) does not permit the media channel of the requested output, so building it would produce a derivative the rights grant does not allow to run there. Creative agents SHOULD check rights[].channels against the channel each requested target capability serves (for example its applies_to_channels, where declared) before generating, and SHOULD surface this code rather than silently building; the restriction is advisory in 3.3, so a creative agent MAY proceed and attach this code as a non-terminal entry in errors[] on an otherwise-successful build, or MAY decline the affected output with a terminal error. error.field SHOULD point at the offending rights entry (e.g. creative_manifest.rights[0].excluded_channels, or creative_manifests[N].rights[i].channels for a multi-capability build) and error.details SHOULD include rights_id, the requested channel, and capability_id when the build named one. Distinct from CREATIVE_REJECTED (content-policy or brand-safety review) and from the governance plan channel check, which is a hard check by the governance agent. Recovery: correctable (target a capability whose channel the grant permits, confirm the rights entry applies to this output, or have the holder grant the channel; the last is a human or holder action, not an autonomous retry). Advisory in 3.3 and may tighten while the surface is experimental."
},
"enumMetadata": {
+ "ADDON_UNREACHABLE": {"recovery": "transient", "suggestion": "seller retries within the original finite hold or records best-effort omission"},
+ "ADDON_AUTH_FAILED": {"recovery": "terminal", "suggestion": "operator repairs vendor credentials; buyer credentials are unchanged"},
+ "ADDON_HASH_MISMATCH": {"recovery": "terminal", "suggestion": "discard affected fragments and have the vendor correct the content hash"},
+ "ADDON_INVALID_POSITION": {"recovery": "terminal", "suggestion": "vendor corrects the position to one declared for this route"},
+ "ADDON_UNDECLARED_FORMAT": {"recovery": "terminal", "suggestion": "vendor corrects the format or creative identity in the response"},
+ "ADDON_MALFORMED_FRAGMENT": {"recovery": "terminal", "suggestion": "operator or vendor repairs the required fragment integration"},
+ "ADDON_REQUIRED_UNAVAILABLE": {"recovery": "terminal", "suggestion": "surface the seller infrastructure failure; a later user-directed retry follows the invoking task idempotency contract"},
"$comment": "Structured recovery classification and remediation hints for each error code. SDKs MUST consume this block instead of parsing 'Recovery: X' from enumDescriptions prose. Each entry is { recovery, suggestion }. recovery is one of: correctable (caller can fix and retry), transient (retry with backoff), terminal (no autonomous recovery - operator intervention required). enumDescriptions is retained for human readability and will continue to carry the canonical narrative; the recovery classification embedded in that prose is normative and MUST match the value here.",
"INVALID_REQUEST": {
"recovery": "correctable",
diff --git a/static/schemas/source/enums/specialism.json b/static/schemas/source/enums/specialism.json
index 12d1b4b2260..85d3bd05cea 100644
--- a/static/schemas/source/enums/specialism.json
+++ b/static/schemas/source/enums/specialism.json
@@ -20,6 +20,7 @@
"collection-lists",
"content-standards",
"creative-ad-server",
+ "creative-addon",
"creative-generative",
"creative-template",
"creative-transformers",
@@ -45,6 +46,7 @@
"sponsored-intelligence"
],
"enumDescriptions": {
+ "creative-addon": "Operator-registered vendor that returns seller-adjacent fragments without receiving buyer creative content.",
"audience-sync": "Syncs buyer-provided audience segments into a platform for activation",
"brand-rights": "Brand identity and rights licensing (talent, music, stock media)",
"buyer-activation": "Buyer agent that creates media buys with correct idempotency keys, handles transient failures with retry, and coordinates creative assignment",
diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json
index ac0c20c563d..875102c20da 100644
--- a/static/schemas/source/index.json
+++ b/static/schemas/source/index.json
@@ -45,6 +45,34 @@
"$ref": "/schemas/core/product.json",
"description": "Represents available advertising inventory"
},
+ "creative-addon-capability": {
+ "$ref": "/schemas/core/creative-addon-capability.json",
+ "description": "Creative Addon Capability"
+ },
+ "creative-addon-registration": {
+ "$ref": "/schemas/core/creative-addon-registration.json",
+ "description": "Creative Addon Registration"
+ },
+ "creative-addon-disclosure": {
+ "$ref": "/schemas/core/creative-addon-disclosure.json",
+ "description": "Creative Addon Disclosure"
+ },
+ "creative-addon-fragment": {
+ "$ref": "/schemas/core/creative-addon-fragment.json",
+ "description": "Creative Addon Fragment"
+ },
+ "creative-addon-failure": {
+ "$ref": "/schemas/core/creative-addon-failure.json",
+ "description": "Creative Addon Failure"
+ },
+ "creative-addon-chain": {
+ "$ref": "/schemas/core/creative-addon-chain.json",
+ "description": "Creative Addon Chain"
+ },
+ "creative-addon-signing-key": {
+ "$ref": "/schemas/core/creative-addon-signing-key.json",
+ "description": "Creative Addon Signing Key"
+ },
"canonical-product": {
"$ref": "/schemas/core/canonical-product.json",
"description": "Canonical-only product view for the AdCP 3.2 split product and proposal tools"
@@ -2419,6 +2447,14 @@
"description": "Response payload with transformer descriptors — input/output formats, typed config params, account-scoped enumerable option values when expanded, and per-account pricing"
}
},
+ "get-creative-addons": {
+ "request": {
+ "$ref": "/schemas/creative/get-creative-addons-request.json"
+ },
+ "response": {
+ "$ref": "/schemas/creative/get-creative-addons-response.json"
+ }
+ },
"get-creative-delivery": {
"request": {
"$ref": "/schemas/creative/get-creative-delivery-request.json",
diff --git a/static/schemas/source/media-buy/product-fields.json b/static/schemas/source/media-buy/product-fields.json
index 8a3a489dcf8..836caf4645d 100644
--- a/static/schemas/source/media-buy/product-fields.json
+++ b/static/schemas/source/media-buy/product-fields.json
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/media-buy/product-fields.json",
"title": "Product Response Fields",
- "description": "Canonical product fields a buyer requests from list_products. Required product_id and name fields are always returned. list_applications also overrides projection whenever a property or collection list is in the effective targeting because it is the decision receipt for seller-specific list matching. targeting_resolution and expires_at override projection whenever the seller returns modifications, as does collection_targeting_allowed whenever returned overlay_support declares collection_list or collection_lists. Legacy named-format fields remain available only through get_products. A projection that omits collections also omits the response-level collections array. On the canonical list_products path, requesting installments also returns collections, because a canonical installment names its collection and canonical-product.json requires the product to declare it. Legacy get_products shares this enum but not that rule: its installment keeps the deprecated single-namespace shorthand, so it can be returned without collections.",
+ "description": "Canonical product fields a buyer requests from list_products. Required product_id and name fields are always returned. For negotiated addon support, complete creative_addons disclosure overrides projection on every offer eligible for direct purchase; a caller cannot suppress the pre-commit compatibility snapshot. list_applications also overrides projection whenever a property or collection list is in the effective targeting because it is the decision receipt for seller-specific list matching. targeting_resolution and expires_at override projection whenever the seller returns modifications, as does collection_targeting_allowed whenever returned overlay_support declares collection_list or collection_lists. Legacy named-format fields remain available only through get_products. A projection that omits collections also omits the response-level collections array. On the canonical list_products path, requesting installments also returns collections, because a canonical installment names its collection and canonical-product.json requires the product to declare it. Legacy get_products shares this enum but not that rule: its installment keeps the deprecated single-namespace shorthand, so it can be returned without collections.",
"type": "array",
"minItems": 1,
"uniqueItems": true,
@@ -19,6 +19,7 @@
"sponsored_placement_types",
"social_placement_surfaces",
"format_options",
+ "creative_addons",
"placements",
"delivery_type",
"exclusivity",
diff --git a/static/schemas/source/media-buy/product-purchase.json b/static/schemas/source/media-buy/product-purchase.json
index ee2c1765628..904274f161f 100644
--- a/static/schemas/source/media-buy/product-purchase.json
+++ b/static/schemas/source/media-buy/product-purchase.json
@@ -15,6 +15,24 @@
}
},
"properties": {
+ "creative_addons": {
+ "allOf": [
+ {
+ "$ref": "/schemas/core/creative-addon-disclosure.json"
+ },
+ {
+ "properties": {
+ "complete": {
+ "const": true
+ },
+ "scope": {
+ "const": "product"
+ }
+ }
+ }
+ ],
+ "description": "Seller-resolved complete configuration for this purchase, covered by commercial_terms and terms_digest. Required when addon support is negotiated. At commitment the seller atomically compares it with current effective configuration; stale terms fail before mutation."
+ },
"product_id": {
"type": "string",
"minLength": 1,
diff --git a/static/schemas/source/protocol/get-adcp-capabilities-response.json b/static/schemas/source/protocol/get-adcp-capabilities-response.json
index 367ea9b68fe..5008a75b0b0 100644
--- a/static/schemas/source/protocol/get-adcp-capabilities-response.json
+++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json
@@ -19,6 +19,8 @@
}
},
"allOf": [
+ {"if": {"required": ["creative_addon"]}, "then": {"properties": {"supported_protocols": {"contains": {"const": "creative"}}}}},
+ {"if": {"properties": {"specialisms": {"contains": {"const": "creative-addon"}}}, "required": ["specialisms"]}, "then": {"required": ["creative_addon", "agent_name"]}},
{
"if": {
"properties": {
@@ -3220,6 +3222,14 @@
},
"additionalProperties": true
},
+ "agent_name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Addon vendor identity; required with creative-addon specialism. Operators compare the declared and registered names after lowercase-and-trim normalization."
+ },
+ "creative_addon": {
+ "$ref": "/schemas/core/creative-addon-capability.json"
+ },
"creative": {
"type": "object",
"description": "Creative protocol capabilities. Only present if creative is in supported_protocols.",
diff --git a/tests/creative-addon-vectors.test.cjs b/tests/creative-addon-vectors.test.cjs
new file mode 100644
index 00000000000..f7ed0fc9ff3
--- /dev/null
+++ b/tests/creative-addon-vectors.test.cjs
@@ -0,0 +1,195 @@
+// Executable reference checks for the normative vectors. This is not a seller runtime.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+const crypto = require('node:crypto');
+const { canonicalize } = require('@adcp/sdk');
+const Ajv = require('ajv');
+const addFormats = require('ajv-formats');
+const schemas = path.join(__dirname, '../static/schemas/source');
+const ajv = new Ajv({ strict: false, allErrors: true, discriminator: true });
+addFormats(ajv);
+for (const file of fs.readdirSync(schemas, { recursive: true })) {
+ if (!file.endsWith('.json')) continue;
+ const schema = JSON.parse(fs.readFileSync(path.join(schemas, file)));
+ if (schema.$id) ajv.addSchema(schema);
+}
+const validChain = ajv.getSchema('/schemas/core/creative-addon-chain.json');
+const validClaims = ajv.getSchema('/schemas/core/creative-addon-chain.json#/definitions/Claims');
+const validHeader = ajv.getSchema('/schemas/core/creative-addon-chain.json#/definitions/ProtectedHeader');
+const validKey = ajv.getSchema('/schemas/core/creative-addon-signing-key.json');
+const fixturePath = path.join(__dirname, '../static/compliance/source/test-vectors/creative-addon-chain.json');
+const sha = value => `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
+const jsonBytes = value => Buffer.from(canonicalize(value), 'utf8');
+const encode = value => jsonBytes(value).toString('base64url');
+function decode(value) {
+ assert.match(value, /^[A-Za-z0-9_-]+$/);
+ const bytes = Buffer.from(value, 'base64url');
+ assert.equal(bytes.toString('base64url'), value, 'noncanonical base64url');
+ return bytes;
+}
+function keyThumbprint(key) { return sha(jsonBytes({ crv: key.crv, kty: key.kty, x: key.x })); }
+function evaluate(chain, trust) {
+ try {
+ const jws = chain.signed_attestation;
+ if (jws.profile !== 'adcp/addon-chain/v1') return 'profile';
+ const header = JSON.parse(decode(jws.protected));
+ if (!validHeader(header)) return 'header';
+ const key = trust.key;
+ if (!validKey(key) || key.kid !== header.kid) return 'key_purpose';
+ if (trust.publisher_pin) {
+ if (keyThumbprint(key) !== keyThumbprint(trust.publisher_pin)) return 'publisher_pin';
+ for (const field of ['alg', 'use', 'adcp_use']) {
+ if (trust.publisher_pin[field] !== undefined && trust.publisher_pin[field] !== key[field]) return 'publisher_pin';
+ }
+ if (trust.publisher_pin.key_ops && !trust.publisher_pin.key_ops.includes('verify')) return 'publisher_pin';
+ }
+ if (!crypto.verify(null, Buffer.from(`${jws.protected}.${jws.payload}`), crypto.createPublicKey({ key, format: 'jwk' }), decode(jws.signature))) return 'signature';
+ const payloadBytes = decode(jws.payload);
+ const claims = JSON.parse(payloadBytes);
+ if (!jsonBytes(claims).equals(payloadBytes)) return 'canonicalization';
+ if (!validChain(chain) || !validClaims(claims)) return 'schema';
+ if (!Number.isSafeInteger(claims.iat) || claims.iat < 0 || claims.iat > trust.now + 60) return 'time';
+ for (const authority of [key, trust.publisher_pin].filter(Boolean)) {
+ if (!authority.revoked_at) continue;
+ const revoked = Date.parse(authority.revoked_at) / 1000;
+ if (!Number.isFinite(revoked) || claims.iat >= revoked || !trust.observed_at || trust.observed_at >= revoked || trust.observed_at < claims.iat || trust.observed_chain_hash !== chain.chain_hash || trust.observed_attestation_hash !== sha(jsonBytes(jws))) return 'revocation';
+ }
+ for (const field of ['agent_url', 'publisher_domain', 'creative_id', 'format_kind', 'variant_id', 'media_buy_id', 'package_id', 'placement_id', 'revision_id', 'locale_variant_id']) {
+ if (claims[field] !== trust.expected[field]) return 'context';
+ }
+ if (claims.typ !== 'adcp-addon-chain+jws' || claims.creative_id !== chain.creative_id || claims.creative_hash !== chain.creative_hash || chain.creative_hash !== trust.expected.creative_hash) return 'creative_hash';
+ if (sha(jsonBytes(chain.entries)) !== chain.chain_hash || claims.chain_hash !== chain.chain_hash) return 'chain_hash';
+ if (sha(jsonBytes(chain.configuration)) !== claims.configuration_hash) return 'configuration_hash';
+ if (chain.configuration.complete !== true || chain.configuration.scope !== 'product' || chain.configuration.product_id !== trust.expected.product_id) return 'configuration';
+ let lastApplied = [-1, -1, -1];
+ let sawOmission = false;
+ let lastOmission = -1;
+ const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
+ for (const [i, entry] of chain.entries.entries()) {
+ if (entry.order !== i || 'content' in entry || 'auth' in entry) return 'entry';
+ const vendor = chain.configuration.vendors[entry.registration_index];
+ if (!vendor || entry.agent_name !== vendor.agent_name || entry.source !== vendor.source || entry.agent_url !== vendor.agent_url) return 'entry';
+ if (entry.status === 'applied') {
+ if (sawOmission || !vendor.positions.includes(entry.position) || !vendor.format_kinds.includes(claims.format_kind)) return 'order';
+ const tuple = [entry.position === 'prepend' ? 0 : 1, entry.registration_index, entry.fragment_index];
+ if (compare(tuple, lastApplied) <= 0) return 'order';
+ lastApplied = tuple;
+ if (vendor.source === 'static_reference' && entry.content_hash !== vendor.static_content_hash) return 'entry';
+ } else if (entry.status === 'omitted') {
+ sawOmission = true;
+ if (entry.registration_index <= lastOmission) return 'order';
+ lastOmission = entry.registration_index;
+ if (vendor.mode === 'required') return 'required_omission';
+ if (!['failure', 'vendor_omission'].includes(entry.reason) || (entry.reason === 'failure') !== ('error_code' in entry)) return 'entry';
+ } else return 'entry';
+ }
+ for (const [index, vendor] of chain.configuration.vendors.entries()) {
+ const entries = chain.entries.filter(entry => entry.registration_index === index);
+ if (!vendor.format_kinds.includes(claims.format_kind)) { if (entries.length) return 'entry'; continue; }
+ if (!entries.length || (entries.some(e => e.status === 'omitted') && entries.length !== 1)) return 'coverage';
+ const fragments = entries.filter(e => e.status === 'applied').map(e => e.fragment_index).sort((a, b) => a - b);
+ if (fragments.some((value, index) => value !== index)) return 'entry';
+ }
+ return 'verified';
+ } catch { return 'encoding'; }
+}
+function assemble(input) {
+ // Model the seller decision boundary; vendors are supplied as recorded outcomes.
+ const applied = []; const omitted = [];
+ let pending = false;
+ for (const [registration_index, vendor] of input.vendors.entries()) {
+ if (vendor.mode === 'required' && vendor.first_failure_seconds !== undefined &&
+ input.elapsed_seconds >= vendor.first_failure_seconds + vendor.max_hold_seconds) {
+ return { status: 'failed', code: 'ADDON_REQUIRED_UNAVAILABLE' };
+ }
+ if (vendor.source === 'static_reference') {
+ applied.push({ registration_index, fragment_index: 0, position: vendor.position, content: vendor.content });
+ continue;
+ }
+ const invalid = vendor.fragments?.some(f => sha(Buffer.from(f.content, 'utf8')) !== f.content_hash || !['prepend', 'append'].includes(f.position));
+ const failed = vendor.error_code || invalid || !vendor.fragments?.length;
+ if (failed) {
+ if (vendor.mode === 'required') {
+ const deadline = (vendor.first_failure_seconds ?? 0) + vendor.max_hold_seconds;
+ if (!input.async || input.elapsed_seconds >= deadline) return { status: 'failed', code: 'ADDON_REQUIRED_UNAVAILABLE' };
+ pending = true;
+ continue;
+ }
+ omitted.push({ registration_index, reason: vendor.error_code || invalid ? 'failure' : 'vendor_omission' });
+ continue;
+ }
+ vendor.fragments.forEach((f, fragment_index) => applied.push({ ...f, registration_index, fragment_index }));
+ }
+ if (pending) return { status: 'working', task_id: input.task_id };
+ applied.sort((a, b) => (a.position === 'prepend' ? 0 : 1) - (b.position === 'prepend' ? 0 : 1) || a.registration_index - b.registration_index || a.fragment_index - b.fragment_index);
+ return { status: 'completed', fragments: applied.map(f => f.content), omitted };
+}
+
+test('published addon vectors exist and verify using independent crypto and JCS implementations', () => {
+ assert.ok(fs.existsSync(fixturePath), 'missing published creative addon vectors');
+ const fixture = JSON.parse(fs.readFileSync(fixturePath));
+ for (const vector of fixture.valid) {
+ assert.equal(sha(Buffer.from(vector.fragment, 'utf8')), vector.fragment_hash, vector.id);
+ assert.equal(sha(jsonBytes(vector.creative_hash_input)), vector.chain.creative_hash, vector.id);
+ assert.equal(evaluate(vector.chain, vector.trust), 'verified', vector.id);
+ assert.equal(crypto.sign(null, Buffer.from(`${vector.chain.signed_attestation.protected}.${vector.chain.signed_attestation.payload}`), crypto.createPrivateKey({ key: fixture.test_private_key, format: 'jwk' })).toString('base64url'), vector.chain.signed_attestation.signature, `${vector.id} deterministic signature`);
+ }
+});
+
+test('negative signature vectors reject altered chains, wrong authority and mismatched delivery context', () => {
+ assert.ok(fs.existsSync(fixturePath), 'missing published creative addon vectors');
+ const fixture = JSON.parse(fs.readFileSync(fixturePath));
+ const failures = [];
+ for (const vector of fixture.invalid) {
+ const base = structuredClone(fixture.valid[vector.base || 0]);
+ for (const edit of vector.edits) {
+ let target = base;
+ const keys = edit.path.split('.');
+ const last = keys.pop();
+ for (const key of keys) target = target[key];
+ if (edit.remove) delete target[last]; else target[last] = edit.value;
+ }
+ const actual = evaluate(base.chain, base.trust);
+ if (actual !== vector.expected) failures.push({ id: vector.id, actual, expected: vector.expected });
+ }
+ assert.deepEqual(failures, []);
+});
+
+test('behavior vectors preserve required holds and explicit best-effort omissions', () => {
+ assert.ok(fs.existsSync(fixturePath), 'missing published creative addon vectors');
+ const fixture = JSON.parse(fs.readFileSync(fixturePath));
+ const actual = fixture.behavior.map(vector => ({ id: vector.id, result: assemble(vector.input) }));
+ const expected = fixture.behavior.map(vector => ({ id: vector.id, result: vector.expected }));
+ assert.deepEqual(actual, expected);
+});
+
+// Check a recorded recovery exchange, including the identity boundary between
+// transport replay and a fresh vendor evaluation. No network/runtime is simulated.
+function verifyRecoveryTrace(trace) {
+ const first = trace[0];
+ for (let i = 1; i < trace.length; i++) {
+ const before = trace[i - 1]; const after = trace[i];
+ if (after.task_id !== first.task_id || after.deadline !== first.deadline) return 'seller_operation_changed';
+ const sameKey = before.request.idempotency_key === after.request.idempotency_key;
+ if (sameKey && !jsonBytes(before.request).equals(jsonBytes(after.request))) return 'idempotency_conflict';
+ if (before.outcome === 'ambiguous' && !sameKey) return 'unresolved_attempt';
+ if (before.outcome !== 'ambiguous' && sameKey && after.outcome !== before.outcome) return 'changed_replay';
+ if (after.at >= after.deadline && after.outcome === 'applied') return 'late_delivery';
+ }
+ return 'valid';
+}
+
+test('required empty response can recover with fresh evaluation and feedback while ambiguous retries preserve the exact request', () => {
+ const first = { task_id: 'seller-task-1', deadline: 60, at: 1,
+ request: { idempotency_key: 'vendor-attempt-0001', creatives: [{ creative_id: 'c', format_kind: 'image' }] }, outcome: 'empty' };
+ const repairedRequest = { ...first.request, idempotency_key: 'vendor-attempt-0002', prior_failures: [{ error_code: 'ADDON_MALFORMED_FRAGMENT', creative_id: 'c', format_kind: 'image', timestamp: '2026-10-14T12:00:01Z' }] };
+ const pending = { ...first, at: 20, request: repairedRequest, outcome: 'ambiguous' };
+ const recovered = { ...pending, at: 25, outcome: 'applied' };
+ assert.equal(verifyRecoveryTrace([first, pending, recovered]), 'valid');
+ assert.equal(verifyRecoveryTrace([first, { ...pending, request: { ...repairedRequest, idempotency_key: first.request.idempotency_key } }]), 'idempotency_conflict');
+ assert.equal(verifyRecoveryTrace([first, pending, { ...recovered, request: { ...repairedRequest, idempotency_key: 'vendor-attempt-0003' } }]), 'unresolved_attempt');
+ assert.equal(verifyRecoveryTrace([first, { ...pending, deadline: 90 }]), 'seller_operation_changed');
+ assert.equal(verifyRecoveryTrace([first, pending, { ...recovered, at: 60 }]), 'late_delivery');
+});
diff --git a/tests/creative-addons.test.cjs b/tests/creative-addons.test.cjs
new file mode 100644
index 00000000000..61c17802e2a
--- /dev/null
+++ b/tests/creative-addons.test.cjs
@@ -0,0 +1,169 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+const Ajv = require('ajv');
+const addFormats = require('ajv-formats');
+
+const root = path.join(__dirname, '../static/schemas/source');
+const ajv = new Ajv({ strict: false, allErrors: true, discriminator: true });
+addFormats(ajv);
+ajv.addFormat('uri-template', true);
+for (const file of fs.readdirSync(root, { recursive: true })) {
+ if (!file.endsWith('.json')) continue;
+ const schema = JSON.parse(fs.readFileSync(path.join(root, file)));
+ if (schema.$id) ajv.addSchema(schema);
+}
+const clone = structuredClone;
+function validate(name, value, expected = true) {
+ const validator = ajv.getSchema(`/schemas/${name}.json`);
+ assert.ok(validator, `missing ${name} contract`);
+ assert.equal(validator(value), expected, JSON.stringify(validator.errors));
+}
+const hash = `sha256:${'a'.repeat(64)}`;
+const registration = {
+ agent_name: 'sentinel', mode: 'required', max_hold_seconds: 60,
+ agent_url: 'https://addons.example/agent',
+};
+const disclosure = {
+ scope: 'product', product_id: 'display', complete: true,
+ signed_chain_profile: 'adcp/addon-chain/v1',
+ vendors: [{ agent_name: 'sentinel', source: 'live', agent_url: 'https://addons.example/agent',
+ mode: 'required', max_hold_seconds: 60, format_kinds: ['image'], positions: ['prepend'] }],
+};
+const request = {
+ idempotency_key: 'addon-operation-0001', operator_name: 'Example Sales',
+ operator_url: 'https://sales.example/agent', publisher_domain: 'publisher.example',
+ creatives: [{ creative_id: 'creative-1', format_kind: 'image', dimensions: { w: 300, h: 250 } }],
+};
+const fragment = { creative_id: 'creative-1', position: 'prepend', content: '', content_hash: hash };
+const chain = {
+ creative_id: 'creative-1', creative_hash: hash, configuration: disclosure,
+ entries: [{ order: 0, registration_index: 0, fragment_index: 0, agent_name: 'sentinel',
+ agent_url: 'https://addons.example/agent', source: 'live', status: 'applied', position: 'prepend',
+ content_hash: hash, bytes: 17, timestamp: '2026-10-14T12:00:00Z' }],
+ chain_hash: hash,
+ signed_attestation: { profile: 'adcp/addon-chain/v1', protected: 'e30', payload: 'e30', signature: 'a'.repeat(86) },
+};
+
+test('fragment request batches context without admitting creative payloads or unsupported formats', () => {
+ validate('creative/get-creative-addons-request', request);
+ for (const format_kind of ['html5', 'display_tag']) {
+ validate('creative/get-creative-addons-request', { ...request, creatives: [{ creative_id: 'c', format_kind }] });
+ }
+ for (const mutation of [
+ r => { r.creatives[0].format_kind = 'video_vast'; },
+ r => { r.creatives[0].creative_manifest = { assets: [] }; },
+ r => { r.creatives[0].content = 'buyer content'; },
+ r => { r.creatives = []; },
+ r => { delete r.idempotency_key; },
+ r => { r.creatives[0].dimensions.w = 0; },
+ ]) { const r = clone(request); mutation(r); validate('creative/get-creative-addons-request', r, false); }
+});
+
+test('response separates successful omissions from terminal errors and rejects invalid fragments', () => {
+ validate('creative/get-creative-addons-response', { status: 'completed', addons: [fragment] });
+ validate('creative/get-creative-addons-response', { status: 'completed', addons: [] });
+ validate('creative/get-creative-addons-response', { status: 'failed', errors: [{ code: 'SERVICE_UNAVAILABLE', message: 'Unavailable' }] });
+ validate('creative/get-creative-addons-response', { addons: [], errors: [{ code: 'SERVICE_UNAVAILABLE' }] }, false);
+ validate('creative/get-creative-addons-response', { errors: [] }, false);
+ for (const change of [{ position: 'inside' }, { content_hash: 'sha256:wrong' }, { content: '' }]) {
+ validate('core/creative-addon-fragment', { ...fragment, ...change }, false);
+ }
+});
+
+test('registration requires one source and a finite positive hold for seller-required vendors', () => {
+ validate('core/creative-addon-registration', registration);
+ const staticVendor = { agent_name: 'meter', mode: 'best_effort', static_reference: {
+ format_kinds: ['image'], position: 'append', content: '' } };
+ validate('core/creative-addon-registration', staticVendor);
+ validate('core/creative-addon-registration', { ...staticVendor, agent_url: registration.agent_url }, false);
+ const noHold = clone(registration); delete noHold.max_hold_seconds;
+ validate('core/creative-addon-registration', noHold, false);
+ for (const value of [0, -1, 0.1]) validate('core/creative-addon-registration', { ...registration, max_hold_seconds: value }, false);
+});
+
+test('disclosure separates undeclared, complete-empty and scoped resolved configuration without exposing loader code', () => {
+ validate('core/creative-addon-disclosure', disclosure);
+ validate('core/creative-addon-disclosure', { ...disclosure, vendors: [] });
+ const bad = clone(disclosure); delete bad.product_id;
+ validate('core/creative-addon-disclosure', bad, false);
+ validate('core/creative-addon-disclosure', { ...disclosure, vendors: [{ ...disclosure.vendors[0], content: 'secret' }] }, false);
+ validate('core/canonical-product', { product_id: 'display', name: 'Display', creative_addons: disclosure });
+ validate('media-buy/product-purchase', { product_id: 'display', pricing_option_id: 'cpm', creative_addons: disclosure });
+ validate('media-buy/product-purchase', { product_id: 'display', pricing_option_id: 'cpm', creative_addons: { ...disclosure, complete: false } }, false);
+ validate('media-buy/product-purchase-input', { product_id: 'display', pricing_option_id: 'cpm' });
+ validate('media-buy/product-purchase-input', { product_id: 'display', pricing_option_id: 'cpm', creative_addons: disclosure }, false);
+});
+
+test('buyer-facing chains require signatures and distinguish omission reasons without fragment disclosure', () => {
+ validate('core/creative-addon-chain', chain);
+ validate('core/creative-variant', { variant_id: 'variant-1', addon_chain: chain });
+ const unsigned = clone(chain); delete unsigned.signed_attestation;
+ validate('core/creative-addon-chain', unsigned, false);
+ for (const key of ['content', 'auth']) {
+ const bad = clone(chain); bad.entries[0][key] = 'secret';
+ validate('core/creative-addon-chain', bad, false);
+ }
+ const omitted = clone(chain);
+ omitted.entries = [{ order: 0, registration_index: 0, agent_name: 'sentinel', source: 'live',
+ agent_url: registration.agent_url, status: 'omitted', reason: 'vendor_omission', timestamp: '2026-10-14T12:00:00Z' }];
+ validate('core/creative-addon-chain', omitted);
+ omitted.entries[0].reason = 'failure';
+ validate('core/creative-addon-chain', omitted, false);
+ omitted.entries[0].error_code = 'ADDON_UNREACHABLE';
+ validate('core/creative-addon-chain', omitted);
+});
+
+test('signing keys enforce dedicated public Ed25519 material and reject private keys', () => {
+ const key = { kid: 'addon-1', kty: 'OKP', crv: 'Ed25519', alg: 'EdDSA', use: 'sig',
+ key_ops: ['verify'], adcp_use: 'addon-chain-signing', x: 'a'.repeat(43) };
+ validate('core/creative-addon-signing-key', key);
+ for (const change of [{ adcp_use: 'request-signing' }, { alg: 'none' }, { key_ops: ['sign'] }, { d: 'secret' }]) {
+ validate('core/creative-addon-signing-key', { ...key, ...change }, false);
+ }
+});
+
+test('addon capability cannot opt out of signed chains or claim unsupported formats', () => {
+ const capability = { supported_format_kinds: ['image'], positions: ['prepend'],
+ signed_chain: { required: true, profile: 'adcp/addon-chain/v1' } };
+ validate('core/creative-addon-capability', capability);
+ validate('core/creative-addon-capability', { ...capability, signed_chain: { ...capability.signed_chain, required: false } }, false);
+ validate('core/creative-addon-capability', { ...capability, supported_format_kinds: ['video_vast'] }, false);
+ validate('enums/specialism', 'creative-addon');
+});
+
+test('addon task is discoverable in the creative protocol and terminal status matches its domain outcome', () => {
+ const index = JSON.parse(fs.readFileSync(path.join(root, 'index.json')));
+ assert.ok(index.schemas.creative.tasks['get-creative-addons'], 'task belongs to creative protocol');
+ validate('creative/get-creative-addons-response', { status: 'failed', addons: [] }, false);
+ validate('creative/get-creative-addons-response', { status: 'completed', errors: [{ code: 'SERVICE_UNAVAILABLE' }] }, false);
+ validate('creative/get-creative-addons-response', { status: 'completed', addons: [], errors: [{ code: 'SERVICE_UNAVAILABLE' }] }, false);
+});
+
+test('published positive vectors validate their wire shapes and decoded signed claims', () => {
+ const fixtures = path.join(__dirname, '../static/compliance/source/test-vectors');
+ const fixture = JSON.parse(fs.readFileSync(path.join(fixtures, 'creative-addon-chain.json')));
+ const vectorSchema = JSON.parse(fs.readFileSync(path.join(fixtures, 'creative-addon-chain.schema.json')));
+ const validator = ajv.compile(vectorSchema);
+ assert.equal(validator(fixture), true, JSON.stringify(validator.errors));
+ const claims = ajv.getSchema('/schemas/core/creative-addon-chain.json#/definitions/Claims');
+ const header = ajv.getSchema('/schemas/core/creative-addon-chain.json#/definitions/ProtectedHeader');
+ for (const vector of fixture.valid) {
+ assert.equal(claims(JSON.parse(Buffer.from(vector.chain.signed_attestation.payload, 'base64url'))), true, JSON.stringify(claims.errors));
+ assert.equal(header(JSON.parse(Buffer.from(vector.chain.signed_attestation.protected, 'base64url'))), true, JSON.stringify(header.errors));
+ }
+});
+
+test('product and account disclosures cannot cross scopes or report failed resolution as complete', () => {
+ const accountDisclosure = { ...disclosure, scope: 'account' }; delete accountDisclosure.product_id;
+ validate('core/canonical-product', { product_id: 'display', name: 'Display', creative_addons: accountDisclosure }, false);
+ const account = { account: { account_id: 'account-1' }, action: 'unchanged', status: 'active', creative_addons: accountDisclosure };
+ validate('account/sync-accounts-response', { status: 'completed', accounts: [account] });
+ validate('account/sync-accounts-response', { status: 'completed', accounts: [{ ...account, creative_addons: disclosure }] }, false);
+ validate('account/sync-accounts-response', { status: 'completed', accounts: [{ ...account, action: 'failed', errors: [{ code: 'PERMISSION_DENIED' }] }] }, false);
+});
+
+test('canonical product projections can request the addon compatibility disclosure', () => {
+ validate('media-buy/product-fields', ['product_id', 'creative_addons']);
+});
diff --git a/tests/mcp-schema-analysis.test.cjs b/tests/mcp-schema-analysis.test.cjs
index a577d41902a..389856e2e22 100644
--- a/tests/mcp-schema-analysis.test.cjs
+++ b/tests/mcp-schema-analysis.test.cjs
@@ -58,8 +58,9 @@ test("input-field weight report attributes the largest transitive schema graphs"
// Core gender adds one shared predicate reached by five inputs.
// core/task-id.json, the one task_id definition, is reached by one input
// (+1 instance, +1 unique).
- assert.equal(report.definition_instances, 711);
- assert.equal(report.unique_definitions, 192);
+ // Seller-resolved addon disclosure adds one shared purchase definition.
+ assert.equal(report.definition_instances, 712);
+ assert.equal(report.unique_definitions, 193);
assert.equal(report.repeated_definitions, 136);
assert.ok(report.repeated_definition_bytes > 180_000);
@@ -184,7 +185,7 @@ test("shared dictionary resolves every experimental tool schema when explicitly
assert.equal(view.dictionary.$id, DICTIONARY_ID);
// Must match the intentionally pinned unique-definition inventory above.
- assert.equal(Object.keys(view.dictionary.$defs).length, 192);
+ assert.equal(Object.keys(view.dictionary.$defs).length, 193);
for (const tool of Object.values(view.tools)) {
assert.equal(tool.inputSchema.$defs, undefined);
assert.match(
diff --git a/tests/mcp-schema-projection.test.cjs b/tests/mcp-schema-projection.test.cjs
index 07d6ff758ff..eebeb455d94 100644
--- a/tests/mcp-schema-projection.test.cjs
+++ b/tests/mcp-schema-projection.test.cjs
@@ -1703,11 +1703,18 @@ for (const [relativePath, expectedDefinition] of [
});
}
-test('generated production profile exposes the active 3.2 surface without compliance annotations', () => {
+test('creative addon discovery is gated to the 3.3 role catalog', () => {
+ assert.equal(mcpRoleProfileTools('creative', '3.2.6').includes('get_creative_addons'), false);
+ assert.equal(mcpRoleProfileTools('creative', '3.3.0').includes('get_creative_addons'), true);
+});
+
+test('generated production profile exposes the active surface without compliance annotations', () => {
assert.ok(fs.existsSync(PRODUCTION_PROFILE_DIR), 'production profile is missing');
const canonicalManifest = readJson(path.join(LATEST_DIR, 'manifest.json'));
const profile = readJson(path.join(PRODUCTION_PROFILE_DIR, 'manifest.json'));
+ assert.equal(canonicalManifest.tools.get_creative_addons.added_in, '3.3.0');
+ assert.equal(Boolean(profile.tools.get_creative_addons), semver.gte(ACTIVE_SURFACE_VERSION, '3.3.0'));
assert.equal(profile.profile, 'production');
assert.equal(profile.surface_version, ACTIVE_SURFACE_VERSION);
assert.equal(profile.annotation_mode, 'structural');
@@ -1878,6 +1885,8 @@ test('generated role profiles are host-compatible discovery catalogs with bounde
assert.equal(modelContext.profile, profileName);
assert.equal(modelContext.view, 'client-prompt-inputs');
+ assert.equal(modelContext.surface_version, ACTIVE_SURFACE_VERSION);
+ assert.equal(modelContext.compatibility_scope, mcpRoleProfileCompatibilityScope(ACTIVE_SURFACE_VERSION));
assert.equal(modelContext.annotation_mode, 'model-context');
assert.equal(modelContext.discovery_profile, '../manifest.json');
assert.equal(
@@ -1959,7 +1968,7 @@ test('generated role profiles are host-compatible discovery catalogs with bounde
assert.ok(canonicalManifest.tools[compatibilityFacade].deprecated_in);
}
- const creativeTools = new Set(MCP_ROLE_PROFILE_TOOLS.creative);
+ const creativeTools = new Set(mcpRoleProfileTools('creative', ACTIVE_SURFACE_VERSION));
const activeCreativeTools = Object.entries(canonicalManifest.tools)
.filter(([, tool]) => tool.protocol === 'creative')
.filter(([, tool]) => !tool.deprecated_in || semver.gt(tool.deprecated_in, ACTIVE_SURFACE_VERSION))
diff --git a/tests/targeting-null-clear.test.cjs b/tests/targeting-null-clear.test.cjs
index bf207422e10..79a2aa842ad 100644
--- a/tests/targeting-null-clear.test.cjs
+++ b/tests/targeting-null-clear.test.cjs
@@ -206,12 +206,16 @@ describe('request-only targeting null-clear semantics', () => {
test('codegen-safe product purchase input copies remain in strict-schema parity', () => {
const strict = readSchema('/schemas/media-buy/product-purchase.json');
const input = readSchema('/schemas/media-buy/product-purchase-input.json');
+ // creative_addons is seller-resolved evidence in the accepted snapshot.
+ // Direct buyers bind it through feed_version, without echoing the disclosure.
+ assert.ok(Object.hasOwn(strict.properties, 'creative_addons'));
+ assert.equal(Object.hasOwn(input.properties, 'creative_addons'), false);
assert.deepEqual(
Object.keys(input.properties).sort(),
- Object.keys(strict.properties).filter(name => !PROPOSAL_ONLY_PURCHASE_FIELDS.has(name)).sort()
+ Object.keys(strict.properties).filter(name => name !== 'creative_addons' && !PROPOSAL_ONLY_PURCHASE_FIELDS.has(name)).sort()
);
for (const property of Object.keys(strict.properties)) {
- if (property === 'targeting_overlay') continue;
+ if (property === 'targeting_overlay' || property === 'creative_addons') continue;
if (PROPOSAL_ONLY_PURCHASE_FIELDS.has(property)) {
assert.equal(
input.properties[property],