From 0b4f42bed05ae0357e4be3e1e9525d429fb7473d Mon Sep 17 00:00:00 2001 From: Aleksander Sekowski Date: Sun, 5 Jul 2026 17:23:43 -0700 Subject: [PATCH] feat(creative): normative VAST validation contract with capability-gated levels and error codes The format-layer contract for a vast asset is a single vast_version string; nothing requires parsing the document, checking Ad/MediaFile presence, verifying the version attribute, or bounding wrapper chains, and the error enum has no VAST codes. Define three seller-declared validation levels (structural, document, wrapper) via creative_specs.vast_validation on get_adcp_capabilities, following the media_buy.governance_aware capability-gating pattern, with normative checks in the video channel docs and three new correctable error codes (VAST_PARSE_FAILED, VAST_VERSION_MISMATCH, VAST_WRAPPER_DEPTH_EXCEEDED). Validation runs at sync_creatives including dry_run; validate_input stays manifest-structure-only. Macro correctness is explicitly out of scope. --- .changeset/feat-vast-validation-contract.md | 13 +++++++ docs/creative/channels/video.mdx | 38 +++++++++++++++++++ .../task-reference/sync_creatives.mdx | 2 + scripts/error-code-drift-dispositions.json | 15 ++++++++ static/schemas/source/enums/error-code.json | 22 ++++++++++- .../get-adcp-capabilities-response.json | 6 +++ 6 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 .changeset/feat-vast-validation-contract.md diff --git a/.changeset/feat-vast-validation-contract.md b/.changeset/feat-vast-validation-contract.md new file mode 100644 index 0000000000..4639699920 --- /dev/null +++ b/.changeset/feat-vast-validation-contract.md @@ -0,0 +1,13 @@ +--- +"adcontextprotocol": minor +--- + +Define a normative VAST validation contract for `vast` creative assets. Today the entire format-layer contract for a VAST asset is the `vast_version` string in `vast-asset-requirements.json`: nothing in the spec requires parsing the document, checking for an `` or ``, verifying the version attribute, or bounding wrapper chains, and `error-code.json` has no VAST codes, so a structurally valid manifest can carry an unplayable tag that fails silently at serve time. + +This change adds: + +- `creative_specs.vast_validation` on `get_adcp_capabilities` (`structural` | `document` | `wrapper`, default `structural`), following the capability-gating pattern of `media_buy.governance_aware`: sellers that do not inspect VAST documents keep the default and are unaffected. +- A "VAST Validation" section in the video channel docs specifying the checks at each level: document parse, root element and `` agreement with the declared `vast_version` / format requirement / seller `vast_versions`, `` and `` presence, HTTPS URLs, wrapper resolution bounded by the format's existing `max_wrapper_depth`, loop detection, per-hop timeout, and terminal-document checks. Validation runs at `sync_creatives` (including `dry_run`); `validate_input` stays manifest-structure-only. A passing preflight is explicitly not approval of future responses from a decisioning endpoint. +- Three error codes with `enumDescriptions` and `enumMetadata`: `VAST_PARSE_FAILED`, `VAST_VERSION_MISMATCH`, `VAST_WRAPPER_DEPTH_EXCEEDED` (all correctable, with `error.details.reason` discriminators). + +Macro correctness and substitution verification are explicitly out of scope (in-flight WG work on click-tracker insertion and decisioning-time substitution). Additive; no change for sellers that do not declare the capability. diff --git a/docs/creative/channels/video.mdx b/docs/creative/channels/video.mdx index 2616961120..035598e58c 100644 --- a/docs/creative/channels/video.mdx +++ b/docs/creative/channels/video.mdx @@ -756,6 +756,44 @@ AdCP supports all VAST versions: - **VAST 4.1** - Enhanced ad pod support - **VAST 4.2** - Latest specification (recommended) +### VAST Validation + +Format-layer validation of `vast` assets is structural: the manifest must satisfy the format's `requirements` (today, `vast_version`), and [`validate_input`](/docs/creative/canonical-formats#validation-flow--validate_input) checks manifest shape only. The VAST document itself is opaque at that level, so a structurally valid manifest can still carry an unplayable tag that fails silently at serve time. + +AdCP defines three validation levels for `vast` assets. Sellers declare the level they perform via `creative_specs.vast_validation` in [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities); absent means `structural`. Validation runs at `sync_creatives` time, including `dry_run`. `validate_input` remains manifest-structure-only at every level. + +| Level | What the seller checks | +|-------|------------------------| +| `structural` (default) | Manifest shape and format `requirements` only. The VAST document is not inspected. | +| `document` | Everything in `structural`, plus the VAST document itself. | +| `wrapper` | Everything in `document`, plus wrapper-chain resolution. | + +**Document-level validation.** Sellers declaring `document` or `wrapper`: + +- MUST parse the document (inline `content`, or one response fetched from `url`) as XML and reject unparseable documents with `VAST_PARSE_FAILED` (`error.details.reason: "not_xml"`). +- MUST reject documents whose root element is not `` with `VAST_PARSE_FAILED` (`error.details.reason: "no_vast_root"`). +- MUST reject documents whose `` attribute is missing or does not match the asset's declared `vast_version`, the format's `vast_version` requirement, or the seller's declared `creative_specs.vast_versions`, with `VAST_VERSION_MISMATCH` (`error.details.supported_versions` lists the accepted versions). +- SHOULD reject documents containing no `` element, and `` linear creatives carrying no ``, with `VAST_PARSE_FAILED` (`error.details.reason: "no_ad"` / `"no_media_file"`). +- SHOULD verify that media, tracking, and click URLs use HTTPS. +- MUST treat unresolved ad-server macros in URLs (`[MACRO]`, `${MACRO}`, `{UNIVERSAL_MACRO}`) as opaque tokens, never as validation failures. Macro correctness and substitution verification are out of scope for this contract. + +**Wrapper-chain validation.** Sellers declaring `wrapper` additionally: + +- MUST resolve `` redirects via `` up to the format's declared `max_wrapper_depth` (when present) and reject deeper chains with `VAST_WRAPPER_DEPTH_EXCEEDED` (`error.details.reason: "depth"`). +- MUST reject chains that revisit a `` already seen (a loop) with `VAST_WRAPPER_DEPTH_EXCEEDED` (`error.details.reason: "loop"`). +- SHOULD apply a per-hop timeout, rejecting with `VAST_WRAPPER_DEPTH_EXCEEDED` (`error.details.reason: "timeout"`), and MUST fetch wrapper hops over HTTPS only. +- MUST apply document-level checks to the terminal `` document and reject with the corresponding document-level code on failure. + +**Preflight, not approval.** A VAST endpoint can return a different document per request (decisioning, rotation, macros). Passing validation at sync time is a preflight of the tag's shape at that moment, not a guarantee about future responses. Sellers MUST NOT treat a passing preflight as approval of all future responses and MAY re-validate during delivery. Sellers fetching `url` assets or wrapper hops SHOULD apply the same server-side request protections (private-address blocking, redirect and size limits) they apply to any other agent-supplied URL fetch. + +**Error codes.** In all cases `error.field` identifies the offending asset path in the manifest. + +| Code | Meaning | +|------|---------| +| `VAST_PARSE_FAILED` | Document is not well-formed XML, has no `` root, no ``, or an inline linear creative has no `` | +| `VAST_VERSION_MISMATCH` | Document's `` disagrees with the declared `vast_version`, the format requirement, or the seller's supported versions | +| `VAST_WRAPPER_DEPTH_EXCEEDED` | Wrapper chain exceeds `max_wrapper_depth`, loops, or a hop timed out | + ### VPAID Support VPAID (Video Player Ad-Serving Interface Definition) enables interactive video ads: diff --git a/docs/creative/task-reference/sync_creatives.mdx b/docs/creative/task-reference/sync_creatives.mdx index 701e2a2bff..a033d87680 100644 --- a/docs/creative/task-reference/sync_creatives.mdx +++ b/docs/creative/task-reference/sync_creatives.mdx @@ -832,6 +832,8 @@ Poll `tasks/get` or wait for the webhook. The completion artifact carries the `c | `FORMAT_MISMATCH` | Assets don't match format requirements | Verify asset types and specifications match format definition | | `CREATIVE_IN_ACTIVE_DELIVERY` | Creative is assigned to an active, non-paused package (blocks updates and `delete_missing` deletions) | Pause the package first, or create a new creative version | +Sellers that declare `creative_specs.vast_validation` of `document` or `wrapper` additionally validate `vast` assets at sync time (including `dry_run`) and can return `VAST_PARSE_FAILED`, `VAST_VERSION_MISMATCH`, or `VAST_WRAPPER_DEPTH_EXCEEDED`. See [VAST Validation](/docs/creative/channels/video#vast-validation). + ## Best practices 1. **Use upsert semantics** - Same `creative_id` updates existing creative rather than creating duplicates. This allows iterative creative development. Note: updates are blocked for creatives in active delivery (see #7). diff --git a/scripts/error-code-drift-dispositions.json b/scripts/error-code-drift-dispositions.json index ac5d388883..4b9c7dfcca 100644 --- a/scripts/error-code-drift-dispositions.json +++ b/scripts/error-code-drift-dispositions.json @@ -235,6 +235,21 @@ "disposition": "held-for-next-minor", "target_version": "3.1", "note": "From sync_accounts polymorphic-key refactor (#2261). Returned per-entry when the seller does not implement the entry's `sync_accounts` mode — settings-update sellers (explicit-account platforms) reject natural-key entries; provisioning-only sellers reject AccountRef-keyed entries. Wire change — held for 3.1." + }, + "VAST_PARSE_FAILED": { + "disposition": "held-for-next-minor", + "target_version": "3.2", + "note": "VAST validation contract: document-level validation of vast assets at sync_creatives for sellers declaring creative_specs.vast_validation of document or wrapper. New VAST-validation surface; wire change, held for 3.2." + }, + "VAST_VERSION_MISMATCH": { + "disposition": "held-for-next-minor", + "target_version": "3.2", + "note": "VAST validation contract: the VAST document's version attribute disagrees with the declared vast_version, the format requirement, or the seller's vast_versions. New VAST-validation surface; wire change, held for 3.2." + }, + "VAST_WRAPPER_DEPTH_EXCEEDED": { + "disposition": "held-for-next-minor", + "target_version": "3.2", + "note": "VAST validation contract: wrapper-chain resolution exceeded max_wrapper_depth, looped, or timed out, for sellers declaring creative_specs.vast_validation: wrapper. New VAST-validation surface; wire change, held for 3.2." } } } diff --git a/static/schemas/source/enums/error-code.json b/static/schemas/source/enums/error-code.json index 0a5b1a253f..d51daab491 100644 --- a/static/schemas/source/enums/error-code.json +++ b/static/schemas/source/enums/error-code.json @@ -96,7 +96,10 @@ "FEED_FETCH_FAILED", "INVALID_FEED_FORMAT", "ITEM_VALIDATION_FAILED", - "CATALOG_LIMIT_EXCEEDED" + "CATALOG_LIMIT_EXCEEDED", + "VAST_PARSE_FAILED", + "VAST_VERSION_MISMATCH", + "VAST_WRAPPER_DEPTH_EXCEEDED" ], "enumDescriptions": { "INVALID_REQUEST": "Request is malformed, missing required fields, or violates schema constraints. Recovery: correctable (check request parameters and fix).", @@ -190,7 +193,10 @@ "FEED_FETCH_FAILED": "Platform could not fetch the catalog feed URL during sync_catalogs. Recovery: correctable (check URL accessibility, authentication, and that the content matches the declared feed_format).", "INVALID_FEED_FORMAT": "Catalog feed content does not match the declared feed_format. Recovery: correctable (verify the feed content matches the declared format, e.g. XML for google_merchant_center).", "ITEM_VALIDATION_FAILED": "One or more catalog items failed schema validation during sync_catalogs. Recovery: correctable (check item_issues for per-item rejection reasons and fix the offending items).", - "CATALOG_LIMIT_EXCEEDED": "The account has reached its maximum catalog count. Recovery: correctable (remove unused catalogs, or contact the seller to raise the limit)." + "CATALOG_LIMIT_EXCEEDED": "The account has reached its maximum catalog count. Recovery: correctable (remove unused catalogs, or contact the seller to raise the limit).", + "VAST_PARSE_FAILED": "A submitted `vast` asset failed document-level validation: the inline `content` (or the document fetched from `url`) is not well-formed XML, has no `` root element, contains no `` element, or an `` linear creative carries no ``. Returned by sellers that declare `creative_specs.vast_validation` of `document` or `wrapper`; sellers at the default `structural` level do not inspect the VAST document and MUST NOT return this code. Distinct from `VALIDATION_ERROR` (manifest-level format validation): the manifest was structurally valid, the VAST document inside it was not. Sellers SHOULD set `error.field` to the offending asset path and SHOULD populate `error.details.reason` with one of `not_xml`, `no_vast_root`, `no_ad`, `no_media_file`. Unresolved ad-server macros in URLs (`[MACRO]`, `${MACRO}`, `{UNIVERSAL_MACRO}`) are opaque tokens, not parse failures. Recovery: correctable (fix the VAST document and resubmit).", + "VAST_VERSION_MISMATCH": "A submitted `vast` asset's document declares a `` attribute that does not match the asset's declared `vast_version`, does not satisfy the format's `vast_version` requirement, or is not in the seller's declared `creative_specs.vast_versions`. Returned by sellers performing document-level VAST validation (`creative_specs.vast_validation` of `document` or `wrapper`). Distinct from `VERSION_UNSUPPORTED` (AdCP protocol version negotiation, unrelated to creative documents). Sellers SHOULD set `error.field` to the offending asset path and SHOULD include the accepted versions in `error.details.supported_versions`. Recovery: correctable (re-tag with an accepted VAST version, or correct the asset's declared `vast_version`).", + "VAST_WRAPPER_DEPTH_EXCEEDED": "Resolving a `vast` asset's wrapper chain failed: the chain exceeded the format's declared `max_wrapper_depth`, revisited a `VASTAdTagURI` already seen in the chain (a loop), or a hop did not resolve within the seller's per-hop timeout. Returned by sellers that declare `creative_specs.vast_validation: \"wrapper\"`. Sellers SHOULD set `error.field` to the offending asset path and SHOULD populate `error.details.reason` with one of `depth`, `loop`, `timeout`, plus `error.details.depth` with the depth reached. Recovery: correctable (flatten the chain, reduce redirects below the format's `max_wrapper_depth`, or fix the failing hop)." }, "enumMetadata": { "$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.", @@ -561,6 +567,18 @@ "CATALOG_LIMIT_EXCEEDED": { "recovery": "correctable", "suggestion": "remove unused catalogs, or contact the seller to raise the limit" + }, + "VAST_PARSE_FAILED": { + "recovery": "correctable", + "suggestion": "inspect error.details.reason (not_xml, no_vast_root, no_ad, no_media_file), fix the VAST document, and resubmit" + }, + "VAST_VERSION_MISMATCH": { + "recovery": "correctable", + "suggestion": "re-tag with a version from error.details.supported_versions, or correct the asset's declared vast_version to match the document" + }, + "VAST_WRAPPER_DEPTH_EXCEEDED": { + "recovery": "correctable", + "suggestion": "inspect error.details.reason (depth, loop, timeout); flatten the wrapper chain, reduce redirects below the format's max_wrapper_depth, or fix the failing hop" } } } diff --git a/static/schemas/source/protocol/get-adcp-capabilities-response.json b/static/schemas/source/protocol/get-adcp-capabilities-response.json index e5b85850e5..859ffa7c05 100644 --- a/static/schemas/source/protocol/get-adcp-capabilities-response.json +++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json @@ -299,6 +299,12 @@ "simid": { "type": "boolean", "description": "SIMID support for interactive video ads" + }, + "vast_validation": { + "type": "string", + "enum": ["structural", "document", "wrapper"], + "default": "structural", + "description": "Level of VAST asset validation the seller performs at sync_creatives (including dry_run): 'structural' checks manifest shape and format requirements only and never inspects the VAST document; 'document' additionally parses the VAST document and can return VAST_PARSE_FAILED / VAST_VERSION_MISMATCH; 'wrapper' additionally resolves the wrapper chain and can return VAST_WRAPPER_DEPTH_EXCEEDED. Absent means 'structural'. See the VAST Validation section of the video channel documentation for the normative checks at each level." } } },