From df89996f13a48e3d716d16906d4bc30a084d75e6 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 5 Sep 2026 19:48:30 +0000 Subject: [PATCH 1/2] fix(codegen): upgrade to secure generator 0.64 --- SCHEMA_DELTAS.md | 158 +- pyproject.toml | 4 +- scripts/post_generate_fixes.py | 65 + src/adcp/types/_ergonomic.py | 29 +- src/adcp/types/_generated.py | 388 ++-- .../types/generated_poc/a2ui/component.py | 6 +- .../types/generated_poc/a2ui/si_catalog.py | 6 +- .../account/list_account_changes_response.py | 8 +- .../account/sync_governance_response.py | 80 +- src/adcp/types/generated_poc/adagents.py | 116 +- .../brand/creative_approval_response.py | 96 +- .../brand/search_brands_response.py | 20 +- .../brand/verify_brand_claim_request.py | 18 +- .../types/generated_poc/brand_discovery.py | 44 +- .../get_adcp_capabilities_response.py | 136 +- .../comply_test_controller_response.py | 447 ++++- .../create_content_standards_response.py | 32 +- .../list_content_standards_response.py | 30 +- .../update_content_standards_response.py | 42 +- .../generated_poc/core/account_change.py | 5 +- .../core/account_change_recorded_webhook.py | 4 +- src/adcp/types/generated_poc/core/app_item.py | 5 +- .../core/attestation_capabilities.py | 18 +- .../generated_poc/core/audience_evidence.py | 12 +- .../core/audience_evidence_requirements.py | 5 +- .../core/canonical_media_buy_features.py | 10 +- .../core/creative_representation.py | 216 +-- .../core/delivery_metric_aggregate.py | 10 +- .../generated_poc/core/forecast_point.py | 10 +- src/adcp/types/generated_poc/core/format.py | 82 +- .../core/geo_place_requirement.py | 12 +- .../generated_poc/core/geo_place_support.py | 5 +- .../core/geo_region_requirement.py | 7 +- .../generated_poc/core/geo_region_support.py | 7 +- .../generated_poc/core/media_buy_features.py | 10 +- .../core/package_format_snapshot.py | 43 +- .../generated_poc/core/postal_area_support.py | 17 +- .../product_audience_evidence_requirements.py | 4 +- .../generated_poc/core/product_change_map.py | 3 +- .../generated_poc/core/product_filters.py | 3 +- .../core/product_format_declaration.py | 1666 ++++++++++++++++- .../core/product_signal_targeting_option.py | 13 +- .../core/reporting_file_entry.py | 4 +- .../core/signal_coverage_forecast.py | 10 +- .../core/targeting_overlay_support.py | 12 +- .../types/generated_poc/core/transformer.py | 340 +--- .../creative/list_creatives_response.py | 266 +-- .../creative/preview_creative_request.py | 6 +- .../creative/sync_creatives_request.py | 267 +-- .../generated_poc/enums/signal_value_type.py | 4 +- .../governance/check_governance_request.py | 11 +- .../governance/reported_outcome_error.py | 12 +- .../governance/sync_plans_response.py | 6 +- .../types/generated_poc/manifest_schema.py | 8 +- .../media_buy/decline_proposals_response.py | 12 +- .../get_reporting_status_response.py | 9 +- .../media_buy/package_request.py | 275 +-- .../generated_poc/media_buy/package_update.py | 273 +-- .../media_buy/refine_proposals_response.py | 32 +- .../sync_reporting_receipts_response.py | 12 +- .../get_adcp_capabilities_response.py | 16 +- .../protocol/sync_principal_response.py | 6 +- .../si_sponsored_context_receipt.py | 6 +- .../trusted_match/context_match_response.py | 5 +- tests/test_extra_policy.py | 39 +- 65 files changed, 3418 insertions(+), 2105 deletions(-) diff --git a/SCHEMA_DELTAS.md b/SCHEMA_DELTAS.md index 338034ea5..dd30d4453 100644 --- a/SCHEMA_DELTAS.md +++ b/SCHEMA_DELTAS.md @@ -2,90 +2,94 @@ ## Files added -- `core/package_delivery_metric_value.py` — PackageDeliveryMetricValue, Qualifier -- `core/reporting_adjustment.py` — AccountingPeriod, ReasonCode, ReportingAdjustment -- `core/reporting_adjustment_receipt.py` — ReportingAdjustmentReceipt, ReportingAdjustmentRejectionCode, Status -- `core/reporting_file_object_ref.py` — ReportingFileObjectReference -- `core/reporting_ledger_changed_webhook.py` — ChangeKind, ReportingLedgerChangedWebhook -- `core/reporting_native_version_ref.py` — ReportingNativeVersionReference -- `core/reporting_reliability_statistics.py` — AdjustmentMagnitudeItem, Basis, Evidence, LatencyPercentiles, ReportingReliabilityMeasurementPeriod, ReportingReliabilityStatistics -- `enums/reach_aggregation.py` — ReachAggregation -- `enums/vendor_relationship.py` — VendorRelationship +- `account/sync_governance_response.py` — Account, GovernanceAgent, Status, SyncGovernanceResponse1, SyncGovernanceResponse2 +- `brand/creative_approval_response.py` — CreativeApprovalResponse1, CreativeApprovalResponse2, CreativeApprovalResponse3, CreativeApprovalResponse4 +- `compliance/comply_test_controller_response.py` — AttestationMode, ComplyResponseArm, ComplyTestControllerResponse1, ComplyTestControllerResponse2, ComplyTestControllerResponse3, ComplyTestControllerResponse4, ComplyTestControllerResponse5, ComplyTestControllerResponse6, ComplyTestControllerResponse7, ComplyTestControllerResponse8, Error, Forced, IdentifierMatchProof, Method, Purpose, RecordedCalls, RecordedCalls1, RecordedCalls2, Suggestion +- `content_standards/create_content_standards_response.py` — CreateContentStandardsResponse1, CreateContentStandardsResponse2 +- `content_standards/list_content_standards_response.py` — ListContentStandardsResponse1, ListContentStandardsResponse2 +- `content_standards/update_content_standards_response.py` — UpdateContentStandardsResponse1, UpdateContentStandardsResponse2 ## Field changes +- `a2ui/si_catalog.py` + - **classes added**: Variant4 + - **classes removed**: Variant2 +- `account/list_account_changes_response.py` + - **classes added**: Status22 + - `ListAccountChangesResponse`: `+status` +- `adagents.py` + - **classes added**: AuthorizedAgents10, AuthorizedAgents11, AuthorizedAgents12, AuthorizedAgents13, AuthorizedAgents15, AuthorizedAgents16, AuthorizedAgents17, AuthorizedAgents18, AuthorizedAgents19, AuthorizedAgents20, AuthorizedAgents22, AuthorizedAgents23, AuthorizedAgents24, AuthorizedAgents25, AuthorizedAgents26, AuthorizedAgents27, AuthorizedAgents29, AuthorizedAgents30, AuthorizedAgents31, AuthorizedAgents32, AuthorizedAgents33, AuthorizedAgents34, AuthorizedAgents36, AuthorizedAgents37, AuthorizedAgents38, AuthorizedAgents39, AuthorizedAgents40, AuthorizedAgents41, AuthorizedAgents43, AuthorizedAgents44, AuthorizedAgents45, AuthorizedAgents46, AuthorizedAgents47, AuthorizedAgents48, AuthorizedAgents8, AuthorizedAgents9 +- `brand/search_brands_response.py` + - **classes added**: SearchBrandsResponse - `bundled/protocol/get_adcp_capabilities_response.py` - - **classes added**: AdjustmentMagnitudeItem, Basis, Evidence, LatencyPercentiles, PeriodTimezonePolicy, ReliabilityStatistic, ReportingReliabilityMeasurementPeriod - - `Alignment`: `+source_timezone` - - `ReportingDelivery`: `+ledger_notification`, `+reliability_statistics`, `+reliable_reporting_version`, `+revision_content_task` - - `Schedule`: `+period_timezone_policy` -- `compliance/comply_test_controller_request.py` - - `Operation`: `+advance_within_retention`, `+probe_scheduler_dst`, `+publish_adjustment`, `+publish_nonempty`, `+publish_official_adjustment`, `+revoke_access`, `+suppress_readiness` -- `core/committed_metric.py` - - `CommittedMetric2`: `+methodology_version` + - **classes added**: CallerEventType, Disclosure6, GeoPostalAreasAdditionalPropertyEnum + - **classes removed**: Disclosure2 +- `core/audience_evidence.py` + - **classes removed**: Subject17, Subject27 +- `core/creative_representation.py` + - **classes removed**: Assets, CreativeRepresentation1, CreativeRepresentation2, CreativeRepresentation3, CreativeRepresentation4, CreativeRepresentation5 + - `CreativeRepresentation`: `+format_kind`, `+representation_id`, `+source` `-root` - `core/delivery_metric_aggregate.py` - - **classes added**: Field0 - - **classes removed**: DeliveryMetricAggregate1 -- `core/forecast_vendor_metric_value.py` - - `ForecastVendorMetricValue`: `+measurable_play_seconds`, `+measurable_plays` -- `core/notification_config.py` - - `EventType`: `+reporting_ledger_changed` -- `core/reporting_capabilities.py` - - `VendorMetric`: `+vendor_relationship` -- `core/reporting_delivery_capabilities.py` - - `ReportingDeliveryCapabilities`: `+ledger_notification`, `+reliability_statistics`, `+reliable_reporting_version`, `+revision_content_task` -- `core/reporting_file_entry.py` - - `ReportingFileEntry`: `+native_version_ref` -- `core/reporting_obligation.py` - - `ReportingObligation`: `+accepted_adjustment_receipt_count`, `+adjustment_count`, `+adjustment_receipt_count`, `+pending_adjustment_count` -- `core/reporting_receipt.py` - - `ReportingReceipt`: `+supersedes_reporting_receipt_id` -- `core/reporting_report_definition.py` - - **classes added**: ContractVersion - - `ReportCalendarTimezoneBasis`: `+schedule_timezone` - - `RestatementPolicy`: `+official_correction_mode` -- `core/reporting_revision.py` - - `ReportingRevision`: `+revision_content_sha256` -- `core/reporting_schedule.py` - - `ReportingScheduleAlignment`: `+source_timezone` -- `core/reporting_schedule_offering.py` - - **classes added**: PeriodTimezonePolicy - - `ReportingScheduleOffering`: `+period_timezone_policy` -- `core/reporting_status_issue.py` - - `Code`: `+ADJUSTMENT_RECEIPT_REJECTED`, `+ADJUSTMENT_RECEIPT_REQUIRED`, `+RECEIPT_REJECTED`, `+RECEIPT_REQUIRED` -- `core/vendor_metric_value.py` - - `VendorMetricValue`: `+measurable_play_seconds`, `+measurable_plays`, `+vendor_relationship` -- `core/x_entity_types.py` - - `XEntityTypes`: `+reporting_adjustment` -- `enums/notification_type.py` - - `NotificationType`: `+reporting_ledger_changed` + - **classes added**: DeliveryMetricAggregate1 +- `core/format.py` + - **classes added**: Assets10, Assets9 + - **classes removed**: Assets38, Assets39 + - `Assets16`: `-requirements` + - `Assets18`: `+requirements` + - `Assets21`: `-requirements` + - `Assets23`: `+asset_group_id`, `+assets`, `+max_count`, `+min_count`, `+required`, `+selection_mode` `-asset_type` + - `Assets24`: `+root` `-asset_type`, `-item_type`, `-requirements` + - `Assets25`: `+asset_type`, `+requirements` `-asset_group_id`, `-assets`, `-item_type`, `-max_count`, `-min_count`, `-required`, `-selection_mode` + - `Assets26`: `+asset_type`, `+requirements` `-root` + - `Assets33`: `-requirements` + - `Assets35`: `+requirements` +- `core/package_format_snapshot.py` + - `PackageFormatSnapshot17`: `+format_kind`, `+params` +- `core/postal_area_support.py` + - **classes added**: PostalAreaSupportAdditionalPropertyEnum +- `core/product_format_declaration.py` + - **classes added**: ProductFormatDeclaration1, ProductFormatDeclaration10, ProductFormatDeclaration11, ProductFormatDeclaration12, ProductFormatDeclaration13, ProductFormatDeclaration14, ProductFormatDeclaration15, ProductFormatDeclaration16, ProductFormatDeclaration2, ProductFormatDeclaration3, ProductFormatDeclaration4, ProductFormatDeclaration5, ProductFormatDeclaration6, ProductFormatDeclaration7, ProductFormatDeclaration8, ProductFormatDeclaration9 + - `ProductFormatDeclaration`: `+root` `-applies_to_channels`, `-canonical_formats_only`, `-display_name`, `-experimental`, `-format_option_id`, `-format_schema`, `-format_shape`, `-locale_policy`, `-macro_resolution_capabilities`, `-publisher_domain`, `-sample_render_url`, `-seller_preference`, `-technical_requirements_complete`, `-tracker_execution_contract`, `-v1_format_ref` +- `core/transformer.py` + - **classes removed**: InputFormat34, Transformer1, Transformer2 + - `Transformer`: `+description`, `+input_format_ids`, `+input_formats`, `+metadata`, `+multiplicity`, `+name`, `+output_capability_ids`, `+output_format_ids`, `+params`, `+pricing_options`, `+transformer_id`, `+voice_synthesis_ref` `-root` +- `creative/list_creatives_response.py` + - **classes added**: Creative + - **classes removed**: AssignedPackage1, Assignments1, Creatives, Creatives1 +- `creative/preview_creative_request.py` + - **classes added**: Input10 + - **classes removed**: Input12 +- `creative/sync_creatives_request.py` + - **classes removed**: Assets, Creative61, Creative62, Creative63, Input + - `Creative`: `+localization`, `+revision_id` `-root` +- `governance/check_governance_request.py` + - **classes added**: Subject8, Subject9 - `governance/sync_plans_response.py` - - **classes added**: Status45 - - **classes removed**: Status44 -- `media_buy/get_media_buy_delivery_request.py` - - `GetMediaBuyDeliveryRequest`: `+pagination`, `+reporting_revision_id` -- `media_buy/get_media_buy_delivery_response.py` - - **classes added**: ReportingRevisionBinding - - `AggregatedTotals`: `+reach_aggregation` - - `ByPackageItem`: `+metric_values` - - `ByPackageItem1`: `+currency` - - `GetMediaBuyDeliveryResponse`: `+pagination`, `+reporting_revision`, `+reporting_revision_binding`, `+reporting_rows` - - `MediaBuyDelivery`: `+currency` - - `Totals`: `-spend` -- `media_buy/get_reporting_status_request.py` - - `GetReportingStatusRequest`: `+changes_after` + - **classes added**: Status50 + - **classes removed**: Status45 +- `media_buy/decline_proposals_response.py` + - **classes added**: Results4, Results5 - `media_buy/get_reporting_status_response.py` - - `GetReportingStatusResponse`: `+adjustment_receipts`, `+adjustments`, `+changes_checkpoint` -- `media_buy/media_buy_delivery_webhook_result.py` - - `ByPackageItem`: `+metric_values` - - `MediaBuyDelivery`: `+currency` - - `Totals`: `-spend` + - **classes added**: Status + - `GetReportingStatusResponse`: `+status` - `media_buy/package_request.py` - - `CommittedMetrics2`: `+methodology_version` -- `media_buy/sync_reporting_receipts_request.py` - - `SyncReportingReceiptsRequest`: `+adjustment_receipts` + - **classes removed**: Assets, Creative, Creative41, Creative42, Input +- `media_buy/package_update.py` + - **classes removed**: Assets, Creative, Creative51, Creative52, Input +- `media_buy/refine_proposals_response.py` + - **classes added**: Results11, Results16, Results17, Results18 + - **classes removed**: Results13, Results6, Results7 + - `Results12`: `+outcome`, `+proposal`, `+proposals`, `+reason`, `+reason_code`, `+source_proposal_id`, `+suggestions`, `+targeting_resolution`, `+unsatisfied_constraints`, `+unsatisfied_product_changes` `-root` + - `Results14`: `+root` `-outcome`, `-proposal`, `-proposals`, `-reason`, `-reason_code`, `-source_proposal_id`, `-suggestions`, `-targeting_resolution`, `-unsatisfied_constraints`, `-unsatisfied_product_changes` + - `Results8`: `+root` `-outcome`, `-proposal`, `-proposals`, `-reason`, `-reason_code`, `-source_proposal_id`, `-suggestions`, `-targeting_resolution`, `-unsatisfied_constraints`, `-unsatisfied_product_changes` - `media_buy/sync_reporting_receipts_response.py` - - **classes added**: Results20, Results21 + - **classes added**: Results22, Results23 + - **classes removed**: Results18, Results19 + - `Results20`: `+receipt` `-adjustment_receipt` + - `Results21`: `+receipt` `-adjustment_receipt` +- `protocol/sync_principal_response.py` + - **classes added**: Action32 + - **classes removed**: Action29 - `sponsored_intelligence/si_sponsored_context_receipt.py` - - **classes added**: Status42 - - **classes removed**: Status41 + - **classes added**: Status45 + - **classes removed**: Status42 diff --git a/pyproject.toml b/pyproject.toml index 75b017def..f85f5d762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,7 @@ dev = [ # Pin to exact version: codegen's variant numbering (e.g. CreateMediaBuyResponse1 vs # CreateMediaBuyResponse) shifts between versions, producing diff churn and breaking # generated-code imports that reference specific suffixes. - "datamodel-code-generator[http]==0.63.0", + "datamodel-code-generator[http]==0.64.0", # Runs Starlette app lifespan under httpx.ASGITransport in tests — # the canonical library for what httpx doesn't do natively. Used by # tests/test_mcp_middleware_composition.py and future integration @@ -340,7 +340,7 @@ skips = ["B101"] # Allow assert in code (we're not using -O optimization) [dependency-groups] dev = [ - "datamodel-code-generator==0.63.0", + "datamodel-code-generator==0.64.0", "pre-commit>=4.4.0", "types-protobuf>=7.34.1.20260408", # Pinned in the dev group so ``uv run mypy`` (used by the pre-commit diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index 3c93a77bb..926a3b3c5 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -629,6 +629,70 @@ def _ensure_configdict_import(content: str) -> str: return "from pydantic import ConfigDict\n\n" + content +_TYPED_EXTRA_ASSIGNMENT = re.compile( + r"^(?P[A-Za-z_]\w*)\.__annotations__\['__pydantic_extra__'\] = " + r"(?P.+?)\n(?P=class_name)\.model_rebuild\(force=True\)\n?", + re.MULTILINE | re.DOTALL, +) + + +def _inline_typed_extra_annotations(content: str) -> tuple[str, int]: + """Move generated typed-extra annotations into their Pydantic classes. + + datamodel-code-generator 0.64 emits a post-class mutation of + ``__annotations__`` followed by ``model_rebuild(force=True)`` for typed + ``additionalProperties``. Pydantic does not rediscover fields added to + ``__annotations__`` after class creation, so the generated model allows + arbitrary extra values instead of validating them against the schema. + Declaring ``__pydantic_extra__`` in the class body activates Pydantic's + documented typed-extra validation path. + """ + fixed = 0 + while match := _TYPED_EXTRA_ASSIGNMENT.search(content): + class_name = match.group("class_name") + class_headers = list( + re.finditer( + rf"^class {re.escape(class_name)}\b[^\n]*:\n", + content[: match.start()], + re.MULTILINE, + ) + ) + if not class_headers: + raise ValueError( + f"Generated typed-extra assignment has no class declaration: {class_name}" + ) + + annotation_lines = match.group("annotation").splitlines() + declaration = f" __pydantic_extra__: {annotation_lines[0]}\n" + declaration += "".join(f" {line}\n" for line in annotation_lines[1:]) + + insertion_offset = class_headers[-1].end() + content = content[: match.start()] + content[match.end() :] + content = content[:insertion_offset] + declaration + content[insertion_offset:] + fixed += 1 + + return content, fixed + + +def fix_typed_additional_properties() -> None: + """Make schema-valued ``additionalProperties`` validate at runtime.""" + fixed = 0 + modified_files = 0 + for py_path in OUTPUT_DIR.rglob("*.py"): + content = py_path.read_text() + updated, file_fixed = _inline_typed_extra_annotations(content) + if not file_fixed: + continue + py_path.write_text(updated) + fixed += file_fixed + modified_files += 1 + + print( + f" Inlined {fixed} typed additionalProperties annotation(s) " + f"across {modified_files} file(s)" + ) + + def _find_indented_field_block(content: str, field_name: str) -> tuple[int, int] | None: """Return absolute offsets for a generated four-space field block.""" cursor = 0 @@ -5315,6 +5379,7 @@ def main(argv: list[str] | None = None): fix_preview_creative_request_discriminator, add_deprecated_field_metadata, apply_open_payload_config, + fix_typed_additional_properties, fix_deprecated_rootmodel_fields, fix_constr_type_annotations, unwrap_rootmodel_unions, diff --git a/src/adcp/types/_ergonomic.py b/src/adcp/types/_ergonomic.py index 6b67a40d4..507ae2960 100644 --- a/src/adcp/types/_ergonomic.py +++ b/src/adcp/types/_ergonomic.py @@ -48,6 +48,7 @@ from adcp.types.generated_poc.core.canonical_format_kind import CanonicalFormatKind from adcp.types.generated_poc.core.context import ContextObject +from adcp.types.generated_poc.core.creative_asset import CreativeAsset from adcp.types.generated_poc.core.creative_assignment import CreativeAssignment from adcp.types.generated_poc.core.error import Error from adcp.types.generated_poc.core.ext import ExtensionObject @@ -107,8 +108,7 @@ from adcp.types.generated_poc.creative.list_creatives_request import AssignmentProjection from adcp.types.generated_poc.media_buy.get_products_request import BuyingMode from adcp.types.generated_poc.media_buy.get_products_response import CacheScope -from adcp.types.generated_poc.media_buy.package_request import Creative as PackageRequestCreative -from adcp.types.generated_poc.media_buy.package_update import Creative as PackageUpdateCreative +from adcp.types.generated_poc.creative.list_creatives_response import Creative from adcp.types.generated_poc.media_buy.list_creative_formats_response import Source from adcp.types.generated_poc.creative.list_creative_formats_request import Type @@ -217,7 +217,9 @@ def _apply_coercion() -> None: _patch_field_annotation( ListCreativesRequest, "assignment_projection", - Annotated[AssignmentProjection | None, BeforeValidator(coerce_to_enum(AssignmentProjection))], + Annotated[ + AssignmentProjection | None, BeforeValidator(coerce_to_enum(AssignmentProjection)) + ], ) _patch_field_annotation( ListCreativesRequest, @@ -288,7 +290,7 @@ def _apply_coercion() -> None: # - format_kind: CanonicalFormatKind | str | None # - pacing: Pacing | str | None # - creative_assignments: list[CreativeAssignment] (accepts subclass instances) - # - creatives: Sequence[PackageRequestCreative] (accepts subclass instances) + # - creatives: Sequence[CreativeAsset] (accepts subclass instances) # - context: ContextObject | dict | None # - ext: ExtensionObject | dict | None _patch_field_annotation( @@ -313,8 +315,8 @@ def _apply_coercion() -> None: PackageRequest, "creatives", Annotated[ - Sequence[PackageRequestCreative] | None, - BeforeValidator(coerce_subclass_list(PackageRequestCreative)), + Sequence[CreativeAsset] | None, + BeforeValidator(coerce_subclass_list(CreativeAsset)), ], ) _patch_field_annotation( @@ -368,7 +370,7 @@ def _apply_coercion() -> None: # Apply coercion to PackageUpdate # - pacing: Pacing | str | None # - creative_assignments: list[CreativeAssignment] (accepts subclass instances) - # - creatives: list[PackageUpdateCreative] (accepts subclass instances) + # - creatives: list[CreativeAsset] (accepts subclass instances) # - context: ContextObject | dict | None # - ext: ExtensionObject | dict | None _patch_field_annotation( @@ -388,8 +390,8 @@ def _apply_coercion() -> None: PackageUpdate, "creatives", Annotated[ - list[PackageUpdateCreative] | None, - BeforeValidator(coerce_subclass_list(PackageUpdateCreative)), + list[CreativeAsset] | None, + BeforeValidator(coerce_subclass_list(CreativeAsset)), ], ) _patch_field_annotation( @@ -452,6 +454,7 @@ def _apply_coercion() -> None: # Apply coercion to ListCreativesResponse # - context: ContextObject | dict | None # - status: TaskStatus | str | None + # - creatives: Sequence[Creative] (accepts subclass instances) # - errors: list[Error] (accepts subclass instances) # - ext: ExtensionObject | dict | None _patch_field_annotation( @@ -464,6 +467,14 @@ def _apply_coercion() -> None: "status", Annotated[TaskStatus | None, BeforeValidator(coerce_to_enum(TaskStatus))], ) + _patch_field_annotation( + ListCreativesResponse, + "creatives", + Annotated[ + Sequence[Creative], + BeforeValidator(coerce_subclass_list(Creative)), + ], + ) _patch_field_annotation( ListCreativesResponse, "errors", diff --git a/src/adcp/types/_generated.py b/src/adcp/types/_generated.py index 898344218..feedbddb8 100644 --- a/src/adcp/types/_generated.py +++ b/src/adcp/types/_generated.py @@ -10,7 +10,7 @@ DO NOT EDIT MANUALLY. Generated from: https://github.com/adcontextprotocol/adcp/tree/main/schemas -Generation date: 2026-09-05 11:10:06 UTC +Generation date: 2026-09-05 19:08:43 UTC """ # ruff: noqa: E501, I001 @@ -258,7 +258,7 @@ Text, Type, Variant, - Variant2, + Variant4, ) from adcp.types.generated_poc.a2ui.surface import A2UiSurface from adcp.types.generated_poc.a2ui.user_action import A2UiUserAction @@ -290,6 +290,7 @@ Kind, ListAccountChangesResponse, SourceCoverageItem, + Status22, ) from adcp.types.generated_poc.account.list_accounts_request import ListAccountsRequest from adcp.types.generated_poc.account.list_accounts_response import ListAccountsResponse @@ -312,7 +313,11 @@ GovernanceAgent, SyncGovernanceRequest, ) -from adcp.types.generated_poc.account.sync_governance_response import SyncGovernanceResponse +from adcp.types.generated_poc.account.sync_governance_response import ( + SyncGovernanceResponse, + SyncGovernanceResponse1, + SyncGovernanceResponse2, +) from adcp.types.generated_poc.adagents import ( AdcpAgentsAuthorization, AdcpAgentsAuthorization1, @@ -332,17 +337,53 @@ AdcpAgentsAuthorization29, AuthorizedAgents, AuthorizedAgents1, + AuthorizedAgents10, + AuthorizedAgents11, + AuthorizedAgents12, + AuthorizedAgents13, AuthorizedAgents14, + AuthorizedAgents15, + AuthorizedAgents16, + AuthorizedAgents17, + AuthorizedAgents18, + AuthorizedAgents19, AuthorizedAgents2, + AuthorizedAgents20, AuthorizedAgents21, + AuthorizedAgents22, + AuthorizedAgents23, + AuthorizedAgents24, + AuthorizedAgents25, + AuthorizedAgents26, + AuthorizedAgents27, AuthorizedAgents28, + AuthorizedAgents29, AuthorizedAgents3, + AuthorizedAgents30, + AuthorizedAgents31, + AuthorizedAgents32, + AuthorizedAgents33, + AuthorizedAgents34, AuthorizedAgents35, + AuthorizedAgents36, + AuthorizedAgents37, + AuthorizedAgents38, + AuthorizedAgents39, AuthorizedAgents4, + AuthorizedAgents40, + AuthorizedAgents41, AuthorizedAgents42, + AuthorizedAgents43, + AuthorizedAgents44, + AuthorizedAgents45, + AuthorizedAgents46, + AuthorizedAgents47, + AuthorizedAgents48, AuthorizedAgents5, AuthorizedAgents6, AuthorizedAgents7, + AuthorizedAgents8, + AuthorizedAgents9, Contact, Country, DelegationType, @@ -369,7 +410,13 @@ Disclosure, ) from adcp.types.generated_poc.brand.creative_approval_request import CreativeApprovalRequest -from adcp.types.generated_poc.brand.creative_approval_response import CreativeApprovalResponse +from adcp.types.generated_poc.brand.creative_approval_response import ( + CreativeApprovalResponse, + CreativeApprovalResponse1, + CreativeApprovalResponse2, + CreativeApprovalResponse3, + CreativeApprovalResponse4, +) from adcp.types.generated_poc.brand.get_brand_identity_request import ( Field1, GetBrandIdentityRequest, @@ -519,7 +566,24 @@ TargetHealth, ) from adcp.types.generated_poc.compliance.comply_test_controller_response import ( + AttestationMode, + ComplyResponseArm, ComplyTestControllerResponse, + ComplyTestControllerResponse1, + ComplyTestControllerResponse2, + ComplyTestControllerResponse3, + ComplyTestControllerResponse4, + ComplyTestControllerResponse5, + ComplyTestControllerResponse6, + ComplyTestControllerResponse7, + ComplyTestControllerResponse8, + Forced, + IdentifierMatchProof, + Method, + Purpose, + RecordedCalls, + RecordedCalls1, + RecordedCalls2, ) from adcp.types.generated_poc.compliance.task_completion_data import ComplianceTaskCompletionData from adcp.types.generated_poc.content_standards.artifact import ( @@ -566,6 +630,8 @@ ) from adcp.types.generated_poc.content_standards.create_content_standards_response import ( CreateContentStandardsResponse, + CreateContentStandardsResponse1, + CreateContentStandardsResponse2, ) from adcp.types.generated_poc.content_standards.get_content_standards_request import ( GetContentStandardsRequest, @@ -591,12 +657,16 @@ ) from adcp.types.generated_poc.content_standards.list_content_standards_response import ( ListContentStandardsResponse, + ListContentStandardsResponse1, + ListContentStandardsResponse2, ) from adcp.types.generated_poc.content_standards.update_content_standards_request import ( UpdateContentStandardsRequest, ) from adcp.types.generated_poc.content_standards.update_content_standards_response import ( UpdateContentStandardsResponse, + UpdateContentStandardsResponse1, + UpdateContentStandardsResponse2, ) from adcp.types.generated_poc.content_standards.validate_content_delivery_request import ( Record, @@ -727,7 +797,7 @@ from adcp.types.generated_poc.core.assets.image_asset import FocalPointItem, ImageAsset from adcp.types.generated_poc.core.assets.javascript_asset import JavascriptAsset from adcp.types.generated_poc.core.assets.markdown_asset import MarkdownAsset -from adcp.types.generated_poc.core.assets.pixel_tracker_asset import Method, PixelTrackerAsset +from adcp.types.generated_poc.core.assets.pixel_tracker_asset import PixelTrackerAsset from adcp.types.generated_poc.core.assets.published_post_asset import ( IdentityRef, PublishedPostAsset, @@ -1107,14 +1177,7 @@ CreativeOperationFormatDeclaration, ) from adcp.types.generated_poc.core.creative_policy import CreativePolicy, ProvenanceRequirements -from adcp.types.generated_poc.core.creative_representation import ( - CreativeRepresentation, - CreativeRepresentation1, - CreativeRepresentation2, - CreativeRepresentation3, - CreativeRepresentation4, - CreativeRepresentation5, -) +from adcp.types.generated_poc.core.creative_representation import CreativeRepresentation from adcp.types.generated_poc.core.creative_representation_set import CreativeRepresentationSet from adcp.types.generated_poc.core.creative_revision_id import CreativeRevisionId from adcp.types.generated_poc.core.creative_variable import CreativeVariable, VariableType @@ -1140,6 +1203,7 @@ from adcp.types.generated_poc.core.delivery_forecast import DeliveryForecast from adcp.types.generated_poc.core.delivery_metric_aggregate import ( DeliveryMetricAggregate, + DeliveryMetricAggregate1, DeliveryMetricAggregate2, Field0, Qualifier3, @@ -1239,6 +1303,7 @@ from adcp.types.generated_poc.core.forecast_range import ForecastRange from adcp.types.generated_poc.core.forecast_vendor_metric_value import ForecastVendorMetricValue from adcp.types.generated_poc.core.format import ( + Assets10, Assets11, Assets12, Assets13, @@ -1266,8 +1331,7 @@ Assets35, Assets36, Assets37, - Assets38, - Assets39, + Assets9, Assets94, BaseGroupAsset, BaseIndividualAsset, @@ -1628,7 +1692,13 @@ System3, System9, ) -from adcp.types.generated_poc.core.postal_area_support import CAEnum, GBEnum, ME, PostalAreaSupport +from adcp.types.generated_poc.core.postal_area_support import ( + CAEnum, + GBEnum, + ME, + PostalAreaSupport, + PostalAreaSupportAdditionalPropertyEnum, +) from adcp.types.generated_poc.core.postal_country_system import ( PostalCountrySystem, PostalCountrySystem1, @@ -1723,7 +1793,25 @@ SignalTargetingItem7, TargetingMode, ) -from adcp.types.generated_poc.core.product_format_declaration import ProductFormatDeclaration +from adcp.types.generated_poc.core.product_format_declaration import ( + ProductFormatDeclaration, + ProductFormatDeclaration1, + ProductFormatDeclaration10, + ProductFormatDeclaration11, + ProductFormatDeclaration12, + ProductFormatDeclaration13, + ProductFormatDeclaration14, + ProductFormatDeclaration15, + ProductFormatDeclaration16, + ProductFormatDeclaration2, + ProductFormatDeclaration3, + ProductFormatDeclaration4, + ProductFormatDeclaration5, + ProductFormatDeclaration6, + ProductFormatDeclaration7, + ProductFormatDeclaration8, + ProductFormatDeclaration9, +) from adcp.types.generated_poc.core.product_identity import ProductIdentity from adcp.types.generated_poc.core.product_offer_filters import ( AvailabilityHorizon, @@ -2229,40 +2317,6 @@ InputFormat31, InputFormat32, InputFormat33, - InputFormat34, - InputFormat341, - InputFormat3410, - InputFormat3411, - InputFormat3412, - InputFormat3413, - InputFormat3414, - InputFormat3415, - InputFormat3416, - InputFormat3417, - InputFormat3418, - InputFormat3419, - InputFormat342, - InputFormat3420, - InputFormat3421, - InputFormat3422, - InputFormat3423, - InputFormat3424, - InputFormat3425, - InputFormat3426, - InputFormat3427, - InputFormat3428, - InputFormat3429, - InputFormat343, - InputFormat3430, - InputFormat3431, - InputFormat3432, - InputFormat3433, - InputFormat344, - InputFormat345, - InputFormat346, - InputFormat347, - InputFormat348, - InputFormat349, InputFormat4, InputFormat5, InputFormat6, @@ -2270,14 +2324,10 @@ InputFormat8, InputFormat9, Multiplicity, - Multiplicity3, OutputCapabilityId, Transformer, - Transformer1, - Transformer2, VariantDimension, VoiceSynthesisRefItem, - VoiceSynthesisRefItem1, ) from adcp.types.generated_poc.core.transformer_param import Option, TransformerParam, ValueSource from adcp.types.generated_poc.core.truncation_sentinel import FieldTruncation, TruncationSentinel @@ -2405,12 +2455,9 @@ ) from adcp.types.generated_poc.creative.list_creatives_response import ( AssignedPackage, - AssignedPackage1, Assignments, - Assignments1, Creatives, Creatives1, - Indicator4, IndicatorTypesEvaluatedEnum, ListCreativesResponse, LocalizationUnavailable, @@ -2426,7 +2473,7 @@ ListTransformersResponseCreativeAgent, ) from adcp.types.generated_poc.creative.preview_creative_request import ( - Input12, + Input10, PreviewCreativeRequest, Request, RequestType, @@ -2465,11 +2512,6 @@ AssignmentOperations1, AssignmentOperations2, AssignmentOperations3, - Creative61, - Creative62, - Creative63, - Creative64, - Creative65, PlacementId, SyncCreativesRequest, ) @@ -2689,6 +2731,8 @@ RuntimeAttestation1, Subject4, Subject6, + Subject8, + Subject9, ) from adcp.types.generated_poc.governance.check_governance_response import ( CanonicalPayload, @@ -2770,7 +2814,7 @@ ) from adcp.types.generated_poc.governance.sync_plans_response import ( ResolvedPolicy, - Status45, + Status50, SyncPlansResponse, ) from adcp.types.generated_poc.manifest import Model @@ -2992,6 +3036,8 @@ Results1, Results2, Results3, + Results4, + Results5, ) from adcp.types.generated_poc.media_buy.get_media_buy_delivery_request import ( CatalogItem, @@ -3114,19 +3160,9 @@ CommittedMetrics, CommittedMetrics1, CommittedMetrics2, - Creative41, - Creative42, - Creative43, - Creative44, - Creative45, PackageRequest, ) from adcp.types.generated_poc.media_buy.package_update import ( - Creative51, - Creative52, - Creative53, - Creative54, - Creative55, KeywordTargetsAddItem, KeywordTargetsRemoveItem, NegativeKeywordsAddItem, @@ -3200,12 +3236,13 @@ RefineProposalsResponse1, RefineProposalsResponse2, Results10, + Results11, Results12, - Results13, Results14, Results15, - Results6, - Results7, + Results16, + Results17, + Results18, Results8, Results9, UnsatisfiedConstraint, @@ -3282,10 +3319,10 @@ SyncReportingReceiptsRequest, ) from adcp.types.generated_poc.media_buy.sync_reporting_receipts_response import ( - Results18, - Results19, Results20, Results21, + Results22, + Results23, SyncReportingReceiptsResponse, ) from adcp.types.generated_poc.media_buy.update_media_buy_async_response_input_required import ( @@ -3475,6 +3512,10 @@ from adcp.types.generated_poc.protocol.get_principal_request import GetPrincipalRequest from adcp.types.generated_poc.protocol.get_principal_response import ( GetPrincipalResponse, + PrincipalCurrentResult, + PrincipalReadFailedResult, + PrincipalRecognizedResult, + PrincipalUnconfiguredResult, Result6, Result7, Result9, @@ -3494,7 +3535,10 @@ SyncPrincipalRequest, ) from adcp.types.generated_poc.protocol.sync_principal_response import ( - Action29, + Action32, + PrincipalAppliedResult, + PrincipalSyncFailedResult, + PrincipalValidatedResult, Result17, Result19, SyncPrincipalResponse, @@ -3572,7 +3616,7 @@ DisclosureCommitment, HostReceipt, SiSponsoredContextReceipt, - Status42, + Status45, ) from adcp.types.generated_poc.sponsored_intelligence.si_terminate_session_request import ( SiTerminateSessionRequest, @@ -3645,6 +3689,9 @@ from adcp.types.generated_poc.media_buy.sync_audiences_response import ( Audience as _AudienceFromSyncAudiencesResponse, ) +from adcp.types.generated_poc.compliance.comply_test_controller_response import ( + Error as _ErrorFromComplyTestControllerResponse, +) from adcp.types.generated_poc.core.error import Error as _ErrorFromError from adcp.types.generated_poc.core.provenance import DeclaredBy as _DeclaredByFromProvenance from adcp.types.generated_poc.sponsored_intelligence.si_sponsored_context import ( @@ -3704,16 +3751,6 @@ # Backward compatibility aliases for renamed types MediaBuyPackage = _PackageFromGetMediaBuysResponse Transport = Transport1 # type: ignore[misc,assignment] -ComplyTestControllerResponse1 = ComplyTestControllerResponse -ComplyTestControllerResponse2 = ComplyTestControllerResponse -ComplyTestControllerResponse3 = ComplyTestControllerResponse -ComplyTestControllerResponse4 = ComplyTestControllerResponse -CreateContentStandardsResponse1 = CreateContentStandardsResponse -CreateContentStandardsResponse2 = CreateContentStandardsResponse -ListContentStandardsResponse1 = ListContentStandardsResponse -ListContentStandardsResponse2 = ListContentStandardsResponse -UpdateContentStandardsResponse1 = UpdateContentStandardsResponse -UpdateContentStandardsResponse2 = UpdateContentStandardsResponse # Explicit exports __all__ = [ @@ -3817,8 +3854,8 @@ "Action", "Action11", "Action22", - "Action29", "Action3", + "Action32", "Action4", "Action9", "ActionBinding", @@ -3939,6 +3976,7 @@ "AssetVariant", "Assets", "Assets1", + "Assets10", "Assets11", "Assets12", "Assets13", @@ -3968,12 +4006,10 @@ "Assets35", "Assets36", "Assets37", - "Assets38", - "Assets39", "Assets4", + "Assets9", "Assets94", "AssignedPackage", - "AssignedPackage1", "Assignment", "AssignmentOperations", "AssignmentOperations1", @@ -3981,7 +4017,6 @@ "AssignmentOperations3", "AssignmentProjection", "Assignments", - "Assignments1", "AsyncAdcpVersion", "Attestation", "AttestationCapabilities", @@ -3992,6 +4027,7 @@ "AttestationIssuer1", "AttestationIssuer2", "AttestationIssuer3", + "AttestationMode", "AttestationRef", "AttestationReference", "AttestationSubject", @@ -4061,17 +4097,53 @@ "AuthorizedAgentBaseFields", "AuthorizedAgents", "AuthorizedAgents1", + "AuthorizedAgents10", + "AuthorizedAgents11", + "AuthorizedAgents12", + "AuthorizedAgents13", "AuthorizedAgents14", + "AuthorizedAgents15", + "AuthorizedAgents16", + "AuthorizedAgents17", + "AuthorizedAgents18", + "AuthorizedAgents19", "AuthorizedAgents2", + "AuthorizedAgents20", "AuthorizedAgents21", + "AuthorizedAgents22", + "AuthorizedAgents23", + "AuthorizedAgents24", + "AuthorizedAgents25", + "AuthorizedAgents26", + "AuthorizedAgents27", "AuthorizedAgents28", + "AuthorizedAgents29", "AuthorizedAgents3", + "AuthorizedAgents30", + "AuthorizedAgents31", + "AuthorizedAgents32", + "AuthorizedAgents33", + "AuthorizedAgents34", "AuthorizedAgents35", + "AuthorizedAgents36", + "AuthorizedAgents37", + "AuthorizedAgents38", + "AuthorizedAgents39", "AuthorizedAgents4", + "AuthorizedAgents40", + "AuthorizedAgents41", "AuthorizedAgents42", + "AuthorizedAgents43", + "AuthorizedAgents44", + "AuthorizedAgents45", + "AuthorizedAgents46", + "AuthorizedAgents47", + "AuthorizedAgents48", "AuthorizedAgents5", "AuthorizedAgents6", "AuthorizedAgents7", + "AuthorizedAgents8", + "AuthorizedAgents9", "AuthorizedAgentsUnion", "Availability", "AvailabilityHorizon", @@ -4415,12 +4487,17 @@ "ComplianceStatus", "ComplianceTaskCompletionData", "ComplianceTesting", + "ComplyResponseArm", "ComplyTestControllerRequest", "ComplyTestControllerResponse", "ComplyTestControllerResponse1", "ComplyTestControllerResponse2", "ComplyTestControllerResponse3", "ComplyTestControllerResponse4", + "ComplyTestControllerResponse5", + "ComplyTestControllerResponse6", + "ComplyTestControllerResponse7", + "ComplyTestControllerResponse8", "Component", "Components", "CompositionModel", @@ -4507,21 +4584,6 @@ "CreatePropertyListRequest", "CreatePropertyListResponse", "Creative", - "Creative41", - "Creative42", - "Creative43", - "Creative44", - "Creative45", - "Creative51", - "Creative52", - "Creative53", - "Creative54", - "Creative55", - "Creative61", - "Creative62", - "Creative63", - "Creative64", - "Creative65", "CreativeAction", "CreativeActivationMethod", "CreativeAgent", @@ -4530,6 +4592,10 @@ "CreativeApprovalMode", "CreativeApprovalRequest", "CreativeApprovalResponse", + "CreativeApprovalResponse1", + "CreativeApprovalResponse2", + "CreativeApprovalResponse3", + "CreativeApprovalResponse4", "CreativeApprovalStatus", "CreativeAsset", "CreativeAssets", @@ -4559,11 +4625,6 @@ "CreativeQuality", "CreativeRejectedDetails", "CreativeRepresentation", - "CreativeRepresentation1", - "CreativeRepresentation2", - "CreativeRepresentation3", - "CreativeRepresentation4", - "CreativeRepresentation5", "CreativeRepresentationSet", "CreativeRepresentationUnresolvedDetails", "CreativeRevisionContentMismatchDetails", @@ -4648,6 +4709,7 @@ "DeliveryJurisdiction", "DeliveryMeasurement", "DeliveryMetricAggregate", + "DeliveryMetricAggregate1", "DeliveryMetricAggregate2", "DeliveryMetrics", "DeliveryMetrics2", @@ -4816,6 +4878,7 @@ "FollowUp", "FontRole2", "Fonts", + "Forced", "ForecastMethod", "ForecastPoint", "ForecastPointDimensions", @@ -4970,6 +5033,7 @@ "Idempotency1", "IdempotencyRequirement", "Identifier", + "IdentifierMatchProof", "Identifiers", "Identity", "IdentityMatchRequest", @@ -5001,7 +5065,6 @@ "Indicator", "Indicator1", "Indicator2", - "Indicator4", "IndicatorBearingResourceState", "IndicatorScope", "IndicatorType", @@ -5012,7 +5075,7 @@ "IndustryIdentifier", "Initiator", "Input", - "Input12", + "Input10", "Input2", "InputFormat", "InputFormat1", @@ -5042,40 +5105,6 @@ "InputFormat31", "InputFormat32", "InputFormat33", - "InputFormat34", - "InputFormat341", - "InputFormat3410", - "InputFormat3411", - "InputFormat3412", - "InputFormat3413", - "InputFormat3414", - "InputFormat3415", - "InputFormat3416", - "InputFormat3417", - "InputFormat3418", - "InputFormat3419", - "InputFormat342", - "InputFormat3420", - "InputFormat3421", - "InputFormat3422", - "InputFormat3423", - "InputFormat3424", - "InputFormat3425", - "InputFormat3426", - "InputFormat3427", - "InputFormat3428", - "InputFormat3429", - "InputFormat343", - "InputFormat3430", - "InputFormat3431", - "InputFormat3432", - "InputFormat3433", - "InputFormat344", - "InputFormat345", - "InputFormat346", - "InputFormat347", - "InputFormat348", - "InputFormat349", "InputFormat4", "InputFormat5", "InputFormat6", @@ -5325,7 +5354,6 @@ "MotionLevel", "MraidVersion", "Multiplicity", - "Multiplicity3", "NativeCommitEvidence", "NegativeKeyword", "NegativeKeywords", @@ -5552,6 +5580,7 @@ "PostalArea19", "PostalArea2", "PostalAreaSupport", + "PostalAreaSupportAdditionalPropertyEnum", "PostalCodeSystem", "PostalCountrySystem", "PostalCountrySystem1", @@ -5595,10 +5624,17 @@ "PricingStructure", "PrimaryCountry", "Principal", + "PrincipalAppliedResult", "PrincipalChangedWebhook", + "PrincipalCurrentResult", "PrincipalDeclarationsState", "PrincipalKind", + "PrincipalReadFailedResult", + "PrincipalRecognizedResult", "PrincipalState", + "PrincipalSyncFailedResult", + "PrincipalUnconfiguredResult", + "PrincipalValidatedResult", "PriorDestinationRef", "PrivacyPolicyAcknowledged", "ProducerIdentity", @@ -5617,6 +5653,22 @@ "ProductDoohScreenResolution", "ProductFilters", "ProductFormatDeclaration", + "ProductFormatDeclaration1", + "ProductFormatDeclaration10", + "ProductFormatDeclaration11", + "ProductFormatDeclaration12", + "ProductFormatDeclaration13", + "ProductFormatDeclaration14", + "ProductFormatDeclaration15", + "ProductFormatDeclaration16", + "ProductFormatDeclaration2", + "ProductFormatDeclaration3", + "ProductFormatDeclaration4", + "ProductFormatDeclaration5", + "ProductFormatDeclaration6", + "ProductFormatDeclaration7", + "ProductFormatDeclaration8", + "ProductFormatDeclaration9", "ProductIdentity", "ProductOfferFilters", "ProductPayloadView", @@ -5727,6 +5779,7 @@ "PurchaseType", "Purge", "PurgeKind", + "Purpose", "PushNotificationConfig", "Qualifier", "Qualifier1", @@ -5752,6 +5805,9 @@ "RecommendedAction", "ReconciliationStatus", "Record", + "RecordedCalls", + "RecordedCalls1", + "RecordedCalls2", "Recovery", "Rectangle", "Reference", @@ -5954,18 +6010,21 @@ "Results", "Results1", "Results10", + "Results11", "Results12", - "Results13", "Results14", "Results15", + "Results16", + "Results17", "Results18", - "Results19", "Results2", "Results20", "Results21", + "Results22", + "Results23", "Results3", - "Results6", - "Results7", + "Results4", + "Results5", "Results8", "Results9", "RetiredDestination", @@ -6124,8 +6183,9 @@ "StartingPosition", "State", "Status", - "Status42", + "Status22", "Status45", + "Status50", "StatusFilter", "StatusSummary", "Statuses", @@ -6168,6 +6228,8 @@ "Subject37", "Subject4", "Subject6", + "Subject8", + "Subject9", "SubjectCategory", "SubjectFacet", "Suggestion", @@ -6231,6 +6293,8 @@ "SyncEventSourcesResponse2", "SyncGovernanceRequest", "SyncGovernanceResponse", + "SyncGovernanceResponse1", + "SyncGovernanceResponse2", "SyncPlansRequest", "SyncPlansResponse", "SyncPrincipalRequest", @@ -6347,8 +6411,6 @@ "TranscriptSource1", "Transform", "Transformer", - "Transformer1", - "Transformer2", "TransformerParam", "Transition", "TransitionMode", @@ -6432,7 +6494,7 @@ "ValueSource", "VariableType", "Variant", - "Variant2", + "Variant4", "VariantAxis", "VariantDimension", "Variants", @@ -6532,7 +6594,6 @@ "Voice", "VoiceSynthesis", "VoiceSynthesisRefItem", - "VoiceSynthesisRefItem1", "VpaidVersion", "WarnForItem", "Warning", @@ -6573,6 +6634,7 @@ "_AudienceFromSyncAudiencesResponse", "_DeclaredByFromProvenance", "_DeclaredByFromSiSponsoredContext", + "_ErrorFromComplyTestControllerResponse", "_ErrorFromError", "_PackageFromPackage", "_ProductIdFromProductDiscoveryCriteria", diff --git a/src/adcp/types/generated_poc/a2ui/component.py b/src/adcp/types/generated_poc/a2ui/component.py index 974f70ba9..732c35c6e 100644 --- a/src/adcp/types/generated_poc/a2ui/component.py +++ b/src/adcp/types/generated_poc/a2ui/component.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: a2ui/component.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -20,5 +20,7 @@ class A2UiComponent(AdCPBaseModel): ] = None component: Annotated[ dict[str, dict[str, Any]], - Field(description='Component definition (keyed by component type)'), + Field( + description='Component definition (keyed by component type)', max_length=1, min_length=1 + ), ] diff --git a/src/adcp/types/generated_poc/a2ui/si_catalog.py b/src/adcp/types/generated_poc/a2ui/si_catalog.py index 7c579a0f9..82fc7d47e 100644 --- a/src/adcp/types/generated_poc/a2ui/si_catalog.py +++ b/src/adcp/types/generated_poc/a2ui/si_catalog.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: a2ui/si_catalog.json -# timestamp: 2026-09-01T19:37:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -56,7 +56,7 @@ class Action(AdCPBaseModel): ] = None -class Variant2(StrEnum): +class Variant4(StrEnum): primary = 'primary' secondary = 'secondary' text = 'text' @@ -65,7 +65,7 @@ class Variant2(StrEnum): class Button(AdCPBaseModel): label: Annotated[bound_value.A2UiBoundValue, Field(description='Button label text')] action: Annotated[Action, Field(description='Action to trigger on click')] - variant: Variant2 | None = Variant2.primary + variant: Variant4 | None = Variant4.primary disabled: bound_value.A2UiBoundValue | None = None diff --git a/src/adcp/types/generated_poc/account/list_account_changes_response.py b/src/adcp/types/generated_poc/account/list_account_changes_response.py index 1a29a9567..c61f46c51 100644 --- a/src/adcp/types/generated_poc/account/list_account_changes_response.py +++ b/src/adcp/types/generated_poc/account/list_account_changes_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: account/list_account_changes_response.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -70,6 +70,11 @@ class SourceCoverageItem(AdCPBaseModel): resource_types: Annotated[list[ResourceType], Field(max_length=50)] +class Status22(StrEnum): + completed = 'completed' + failed = 'failed' + + class ListAccountChangesResponse(AdcpVersionEnvelope, ProtocolEnvelope): model_config = ConfigDict( extra='allow', @@ -117,3 +122,4 @@ class ListAccountChangesResponse(AdcpVersionEnvelope, ProtocolEnvelope): errors: list[error.Error] | None = None context: context_1.ContextObject | None = None ext: ext_1.ExtensionObject | None = None + status: Status22 diff --git a/src/adcp/types/generated_poc/account/sync_governance_response.py b/src/adcp/types/generated_poc/account/sync_governance_response.py index 859ca1d24..7f67fce5a 100644 --- a/src/adcp/types/generated_poc/account/sync_governance_response.py +++ b/src/adcp/types/generated_poc/account/sync_governance_response.py @@ -1,12 +1,86 @@ # generated by datamodel-codegen: # filename: account/sync_governance_response.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from adcp.types._str_enum import StrEnum +from typing import Annotated + +from adcp.types.base import AdCPBaseModel +from pydantic import AnyUrl, ConfigDict, Field + +from ..core import account_ref +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope -class SyncGovernanceResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass +class Status(StrEnum): + synced = 'synced' + failed = 'failed' + + +class GovernanceAgent(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + url: Annotated[AnyUrl, Field(description='Governance agent endpoint URL.')] + + +class SyncGovernanceResponse2(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + errors: Annotated[ + list[error.Error], + Field( + description='Operation-level errors (e.g., authentication failure, service unavailable)', + min_length=1, + ), + ] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class Account(AdCPBaseModel): + model_config = ConfigDict( + extra='allow', + ) + account: Annotated[ + account_ref.AccountReference1 | account_ref.AccountReference2, Field(description='Account reference, echoed from request') + ] + status: Annotated[ + Status, + Field( + description='Sync result. synced: governance agents persisted. failed: could not complete (see errors).' + ), + ] + governance_agents: Annotated[ + list[GovernanceAgent] | None, + Field( + description='Governance agent now synced on this account. Reflects the persisted state after sync. Exactly one entry; the array shape mirrors the request schema and the one-agent-per-account invariant. See sync_governance request schema.', + max_length=1, + min_length=1, + ), + ] = None + errors: Annotated[ + list[error.Error] | None, + Field( + description="Per-account errors (only present when status is 'failed')", min_length=1 + ), + ] = None + + +class SyncGovernanceResponse1(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + accounts: Annotated[list[Account], Field(description='Per-account sync results')] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +SyncGovernanceResponse = SyncGovernanceResponse1 | SyncGovernanceResponse2 diff --git a/src/adcp/types/generated_poc/adagents.py b/src/adcp/types/generated_poc/adagents.py index ea1125c3d..372f78c65 100644 --- a/src/adcp/types/generated_poc/adagents.py +++ b/src/adcp/types/generated_poc/adagents.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: adagents.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -465,34 +465,125 @@ class AuthorizedAgents6(AuthorizedAgentBaseFields): ] +class AuthorizedAgents8(AuthorizedAgents1): + authorization_type: Literal['property_ids'] = 'property_ids' +class AuthorizedAgents9(AuthorizedAgents2): + authorization_type: Literal['property_tags'] = 'property_tags' +class AuthorizedAgents11(AuthorizedAgents4): + authorization_type: Literal['publisher_properties'] = 'publisher_properties' +class AuthorizedAgents12(AuthorizedAgents5): + authorization_type: Literal['signal_ids'] = 'signal_ids' +class AuthorizedAgents13(AuthorizedAgents6): + authorization_type: Literal['signal_tags'] = 'signal_tags' +class AuthorizedAgents15(AuthorizedAgents1): + authorization_type: Literal['property_ids'] = 'property_ids' +class AuthorizedAgents16(AuthorizedAgents2): + authorization_type: Literal['property_tags'] = 'property_tags' +class AuthorizedAgents18(AuthorizedAgents4): + authorization_type: Literal['publisher_properties'] = 'publisher_properties' +class AuthorizedAgents19(AuthorizedAgents5): + authorization_type: Literal['signal_ids'] = 'signal_ids' +class AuthorizedAgents20(AuthorizedAgents6): + authorization_type: Literal['signal_tags'] = 'signal_tags' +class AuthorizedAgents22(AuthorizedAgents1): + authorization_type: Literal['property_ids'] = 'property_ids' +class AuthorizedAgents23(AuthorizedAgents2): + authorization_type: Literal['property_tags'] = 'property_tags' +class AuthorizedAgents25(AuthorizedAgents4): + authorization_type: Literal['publisher_properties'] = 'publisher_properties' +class AuthorizedAgents26(AuthorizedAgents5): + authorization_type: Literal['signal_ids'] = 'signal_ids' +class AuthorizedAgents27(AuthorizedAgents6): + authorization_type: Literal['signal_tags'] = 'signal_tags' + + +class AuthorizedAgents29(AuthorizedAgents1): + authorization_type: Literal['property_ids'] = 'property_ids' + + +class AuthorizedAgents30(AuthorizedAgents2): + authorization_type: Literal['property_tags'] = 'property_tags' + + +class AuthorizedAgents32(AuthorizedAgents4): + authorization_type: Literal['publisher_properties'] = 'publisher_properties' + + +class AuthorizedAgents33(AuthorizedAgents5): + authorization_type: Literal['signal_ids'] = 'signal_ids' + + +class AuthorizedAgents34(AuthorizedAgents6): + authorization_type: Literal['signal_tags'] = 'signal_tags' + + +class AuthorizedAgents36(AuthorizedAgents1): + authorization_type: Literal['property_ids'] = 'property_ids' + + +class AuthorizedAgents37(AuthorizedAgents2): + authorization_type: Literal['property_tags'] = 'property_tags' + + +class AuthorizedAgents39(AuthorizedAgents4): + authorization_type: Literal['publisher_properties'] = 'publisher_properties' + + +class AuthorizedAgents40(AuthorizedAgents5): + authorization_type: Literal['signal_ids'] = 'signal_ids' + + +class AuthorizedAgents41(AuthorizedAgents6): + authorization_type: Literal['signal_tags'] = 'signal_tags' + + +class AuthorizedAgents43(AuthorizedAgents1): + authorization_type: Literal['property_ids'] = 'property_ids' + + +class AuthorizedAgents44(AuthorizedAgents2): + authorization_type: Literal['property_tags'] = 'property_tags' + + +class AuthorizedAgents46(AuthorizedAgents4): + authorization_type: Literal['publisher_properties'] = 'publisher_properties' + + +class AuthorizedAgents47(AuthorizedAgents5): + authorization_type: Literal['signal_ids'] = 'signal_ids' + + +class AuthorizedAgents48(AuthorizedAgents6): + authorization_type: Literal['signal_tags'] = 'signal_tags' + class AuthorizedAgents3(AuthorizedAgentBaseFields): model_config = ConfigDict( @@ -585,22 +676,45 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) +class AuthorizedAgents10(AuthorizedAgents3): + authorization_type: Literal['inline_properties'] = 'inline_properties' + AuthorizedAgents7 = AuthorizedAgents +class AuthorizedAgents17(AuthorizedAgents3): + authorization_type: Literal['inline_properties'] = 'inline_properties' + + AuthorizedAgents14 = AuthorizedAgents +class AuthorizedAgents24(AuthorizedAgents3): + authorization_type: Literal['inline_properties'] = 'inline_properties' + + AuthorizedAgents21 = AuthorizedAgents +class AuthorizedAgents31(AuthorizedAgents3): + authorization_type: Literal['inline_properties'] = 'inline_properties' + + AuthorizedAgents28 = AuthorizedAgents +class AuthorizedAgents38(AuthorizedAgents3): + authorization_type: Literal['inline_properties'] = 'inline_properties' + + AuthorizedAgents35 = AuthorizedAgents +class AuthorizedAgents45(AuthorizedAgents3): + authorization_type: Literal['inline_properties'] = 'inline_properties' + + AuthorizedAgents42 = AuthorizedAgents diff --git a/src/adcp/types/generated_poc/brand/creative_approval_response.py b/src/adcp/types/generated_poc/brand/creative_approval_response.py index a916801cf..23b1ee0ec 100644 --- a/src/adcp/types/generated_poc/brand/creative_approval_response.py +++ b/src/adcp/types/generated_poc/brand/creative_approval_response.py @@ -1,12 +1,102 @@ # generated by datamodel-codegen: # filename: brand/creative_approval_response.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from typing import Annotated, Literal + +from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field + +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope -class CreativeApprovalResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass +class CreativeApprovalResponse1(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + approval_status: Annotated[ + Literal['approved'], + Field( + description='Creative has been approved for distribution. Renamed from `status` in 3.1 to free the top-level `status` key for the envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization (#4878).' + ), + ] = 'approved' + rights_id: str + creative_id: Annotated[ + str | None, Field(description="Echo of the buyer's creative identifier") + ] = None + creative_url: AnyUrl | None = None + approved_at: AwareDatetime | None = None + conditions: Annotated[ + list[str] | None, + Field(description="Conditions on the approval (e.g., 'approved for NL market only')"), + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class CreativeApprovalResponse2(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + approval_status: Annotated[Literal['rejected'], Field(description='Creative was rejected')] = 'rejected' + rights_id: str + creative_id: Annotated[ + str | None, Field(description="Echo of the buyer's creative identifier") + ] = None + creative_url: AnyUrl | None = None + reason: Annotated[str, Field(description='Why the creative was rejected')] + suggestions: Annotated[ + list[str] | None, + Field( + description='Actionable feedback for revision. If present, the buyer can revise and resubmit the creative. If absent, the rejection is final for this creative concept.' + ), + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class CreativeApprovalResponse3(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + approval_status: Annotated[ + Literal['pending_review'], Field(description='Creative is queued for review') + ] = 'pending_review' + rights_id: str + creative_id: Annotated[ + str | None, Field(description="Echo of the buyer's creative identifier") + ] = None + creative_url: AnyUrl | None = None + estimated_response_time: Annotated[ + str | None, Field(description="Expected time for review (e.g., '24h', '2 business days')") + ] = None + status_url: Annotated[ + AnyUrl | None, + Field( + description='URL to poll for updated approval status. GET this URL to receive a creative-approval-response. Poll at reasonable intervals (suggested: every 5 minutes, back off after 1 hour to every 30 minutes). Stop polling after estimated_response_time has elapsed and the approval_status is still pending_review.' + ), + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class CreativeApprovalResponse4(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + errors: Annotated[list[error.Error], Field(min_length=1)] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +CreativeApprovalResponse = ( + CreativeApprovalResponse1 + | CreativeApprovalResponse2 + | CreativeApprovalResponse3 + | CreativeApprovalResponse4 +) diff --git a/src/adcp/types/generated_poc/brand/search_brands_response.py b/src/adcp/types/generated_poc/brand/search_brands_response.py index c393cb2d2..18f6cb59e 100644 --- a/src/adcp/types/generated_poc/brand/search_brands_response.py +++ b/src/adcp/types/generated_poc/brand/search_brands_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: brand/search_brands_response.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -10,6 +10,10 @@ from adcp.types.base import AdCPBaseModel from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 +from ..core import pagination_response from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope from ..enums import advertiser_industry, right_use @@ -170,4 +174,16 @@ class SearchBrandResult(AdCPBaseModel): class SearchBrandsResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass + brands: Annotated[ + list[SearchBrandResult] | None, + Field(description='Brand stubs matching the query, ranked by relevance'), + ] = None + pagination: Annotated[ + pagination_response.PaginationResponse | None, + Field( + description='Pagination metadata. Present when has_more is true or total_count is known.' + ), + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + errors: Annotated[list[error.Error] | None, Field(min_length=1)] = None diff --git a/src/adcp/types/generated_poc/brand/verify_brand_claim_request.py b/src/adcp/types/generated_poc/brand/verify_brand_claim_request.py index 5641fc121..516818f2b 100644 --- a/src/adcp/types/generated_poc/brand/verify_brand_claim_request.py +++ b/src/adcp/types/generated_poc/brand/verify_brand_claim_request.py @@ -1,12 +1,11 @@ # generated by datamodel-codegen: # filename: brand/verify_brand_claim_request.json +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Any, Annotated - -from pydantic import AnyUrl, ConfigDict, Field +from typing import Any from ..core.version_envelope import AdcpVersionEnvelope @@ -19,14 +18,5 @@ class ClaimType(StrEnum): class VerifyBrandClaimRequest(AdcpVersionEnvelope): - model_config = ConfigDict( - extra='allow', - ) - claim_type: Annotated[ - ClaimType, - Field(description='Discriminates the kind of brand claim being verified.'), - ] - claim: Annotated[ - dict[str, Any], - Field(description='Claim payload. Shape varies by claim_type.'), - ] + claim_type: ClaimType + claim: dict[str, Any] diff --git a/src/adcp/types/generated_poc/brand_discovery.py b/src/adcp/types/generated_poc/brand_discovery.py index c57170b14..c062b76c3 100644 --- a/src/adcp/types/generated_poc/brand_discovery.py +++ b/src/adcp/types/generated_poc/brand_discovery.py @@ -1,11 +1,11 @@ # generated by datamodel-codegen: # filename: _brand_discovery.json -# timestamp: 2026-09-05T11:10:00+00:00 +# timestamp: 2026-09-05T19:08:37+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Annotated, Any, Literal +from typing import Annotated, Any, Dict, Literal from adcp.types.base import AdCPBaseModel from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, RootModel @@ -184,7 +184,7 @@ class PortfolioEntry(AdCPBaseModel): class LocalizedName(RootModel[dict[str, str]]): - root: Annotated[dict[str, str], Field(min_length=1)] + root: Annotated[dict[str, str], Field(max_length=1, min_length=1)] class LocalizedScalar1(RootModel[list[LocalizedName]]): @@ -211,7 +211,7 @@ def __getattr__(self, name: str) -> Any: return getattr(self.root, name) class LocalizedStringListEntry(RootModel[dict[str, list[str]]]): - root: Annotated[dict[str, list[str]], Field(min_length=1)] + root: Annotated[dict[str, list[str]], Field(max_length=1, min_length=1)] class LocalizedStringList1(RootModel[list[LocalizedStringListEntry]]): @@ -401,6 +401,10 @@ class GuidelineSeverity(StrEnum): class Colors(AdCPBaseModel): + __pydantic_extra__: Dict[str, ColorValue] + model_config = ConfigDict( + extra='allow', + ) primary: ColorValue | None = None secondary: ColorValue | None = None accent: ColorValue | None = None @@ -415,6 +419,8 @@ class Colors(AdCPBaseModel): surface_2: ColorValue | None = None + + class WeightRangeItem(RootModel[int]): root: Annotated[int, Field(ge=100, le=900)] @@ -492,10 +498,16 @@ def __getattr__(self, name: str) -> Any: return getattr(self.root, name) class Fonts(AdCPBaseModel): + __pydantic_extra__: Dict[str, FontRole] + model_config = ConfigDict( + extra='allow', + ) primary: Annotated[FontRole | None, Field(description='Primary font family')] = None secondary: Annotated[FontRole | None, Field(description='Secondary font family')] = None + + class AssetContentType(StrEnum): image = 'image' video = 'video' @@ -1155,6 +1167,10 @@ class GraphicStyle(AdCPBaseModel): class BorderRadius(AdCPBaseModel): + __pydantic_extra__: Dict[str, str] + model_config = ConfigDict( + extra='allow', + ) none: Annotated[str | None, Field(description="Explicitly sharp corners (e.g., '0')")] = None default: Annotated[ str | None, @@ -1174,7 +1190,13 @@ class BorderRadius(AdCPBaseModel): ) + + class Elevation(AdCPBaseModel): + __pydantic_extra__: Dict[str, str] + model_config = ConfigDict( + extra='allow', + ) none: Annotated[str | None, Field(description="No shadow (e.g., 'none')")] = None subtle: Annotated[ str | None, @@ -1194,7 +1216,13 @@ class Elevation(AdCPBaseModel): ] = None + + class Scale(AdCPBaseModel): + __pydantic_extra__: Dict[str, str] + model_config = ConfigDict( + extra='allow', + ) xs: Annotated[str | None, Field(description="Extra small spacing (e.g., '4px')")] = None sm: Annotated[str | None, Field(description="Small spacing (e.g., '8px')")] = None md: Annotated[str | None, Field(description="Medium spacing (e.g., '16px')")] = None @@ -1205,6 +1233,8 @@ class Scale(AdCPBaseModel): ] = None + + class Spacing(AdCPBaseModel): model_config = ConfigDict( extra='forbid', @@ -1721,6 +1751,10 @@ class AssetLibrary(AdCPBaseModel): class TypeScale(AdCPBaseModel): + __pydantic_extra__: Dict[str, TypeScaleEntry] + model_config = ConfigDict( + extra='allow', + ) base_width: Annotated[ str | None, Field( @@ -1734,6 +1768,8 @@ class TypeScale(AdCPBaseModel): cta: TypeScaleEntry | None = None + + class Contact(AdCPBaseModel): model_config = ConfigDict( extra='allow', diff --git a/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py b/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py index fec7f887b..b5c221bfc 100644 --- a/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py +++ b/src/adcp/types/generated_poc/bundled/protocol/get_adcp_capabilities_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: bundled/protocol/get_adcp_capabilities_response.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from datetime import date from adcp.types._str_enum import StrEnum -from typing import Annotated, Any, Literal +from typing import Annotated, Any, Dict, Literal from adcp.types.base import AdCPBaseModel from pydantic import AnyUrl, AwareDatetime, ConfigDict, EmailStr, Field, RootModel, StringConstraints @@ -360,8 +360,66 @@ class ReportingDestinationOffering(AdCPBaseModel): ] -class CallerEventType(AdCPBaseModel): - pass +class CallerEventType( + RootModel[ + Literal[ + 'scheduled', + 'final', + 'delayed', + 'adjusted', + 'window_update', + 'impairment', + 'creative.status_changed', + 'creative.assignment_changed', + 'indicators.changed', + 'creative.purged', + 'account.status_changed', + 'account.change_recorded', + 'product.created', + 'product.updated', + 'product.priced', + 'product.removed', + 'signal.created', + 'signal.updated', + 'signal.priced', + 'signal.removed', + 'wholesale_feed.bulk_change', + 'capabilities.changed', + 'reporting.delivery_ready', + 'reporting.status_changed', + 'reporting.ledger_changed', + 'principal.changed', + ] + ] +): + root: Literal[ + 'scheduled', + 'final', + 'delayed', + 'adjusted', + 'window_update', + 'impairment', + 'creative.status_changed', + 'creative.assignment_changed', + 'indicators.changed', + 'creative.purged', + 'account.status_changed', + 'account.change_recorded', + 'product.created', + 'product.updated', + 'product.priced', + 'product.removed', + 'signal.created', + 'signal.updated', + 'signal.priced', + 'signal.removed', + 'wholesale_feed.bulk_change', + 'capabilities.changed', + 'reporting.delivery_ready', + 'reporting.status_changed', + 'reporting.ledger_changed', + 'principal.changed', + ] class Principal(AdCPBaseModel): @@ -915,8 +973,8 @@ class AcceptedVerifier(AdCPBaseModel): extra='forbid', ) agent_url: AnyUrl - claim_types: list[AnyUrl] | None = None - proof_formats: list[AnyUrl] | None = None + claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None + proof_formats: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None ext: Annotated[ dict[str, Any] | None, Field( @@ -1662,7 +1720,10 @@ class GeoRegions(AdCPBaseModel): ) countries: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], Countries | Countries5], - Field(description='Selectable ISO subdivision values keyed by ISO 3166-1 alpha-2 country.'), + Field( + description='Selectable ISO subdivision values keyed by ISO 3166-1 alpha-2 country.', + min_length=1, + ), ] catalog_version: Annotated[ str | None, @@ -1713,7 +1774,10 @@ class GeoRegionsExclude(AdCPBaseModel): ) countries: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], Countries6 | Countries7], - Field(description='Selectable ISO subdivision values keyed by ISO 3166-1 alpha-2 country.'), + Field( + description='Selectable ISO subdivision values keyed by ISO 3166-1 alpha-2 country.', + min_length=1, + ), ] catalog_version: Annotated[ str | None, @@ -1775,7 +1839,18 @@ class CAEnum(StrEnum): full = 'full' +class GeoPostalAreasAdditionalPropertyEnum(StrEnum): + postal_code = 'postal_code' + custom = 'custom' + + class GeoPostalAreas(AdCPBaseModel): + __pydantic_extra__: Dict[ + str, list[GeoPostalAreasAdditionalPropertyEnum] + ] + model_config = ConfigDict( + extra='allow', + ) US: Annotated[list[ME] | None, Field(min_length=1)] = None GB: Annotated[list[GBEnum] | None, Field(min_length=1)] = None CA: Annotated[list[CAEnum] | None, Field(min_length=1)] = None @@ -1800,6 +1875,8 @@ class GeoPostalAreas(AdCPBaseModel): at_plz: Annotated[bool | None, Field(deprecated=True)] = None + + class Countries8(StrEnum): airport = 'airport' borough = 'borough' @@ -1910,7 +1987,8 @@ class GeoPlaces(AdCPBaseModel): countries: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], list[Countries8 | Countries9]], Field( - description='Supported place types keyed by ISO 3166-1 alpha-2 country. Only explicitly listed country/type pairs are supported.' + description='Supported place types keyed by ISO 3166-1 alpha-2 country. Only explicitly listed country/type pairs are supported.', + min_length=1, ), ] catalog: Annotated[ @@ -2347,6 +2425,10 @@ class DiscoveryMode(StrEnum): class Features(AdCPBaseModel): + __pydantic_extra__: Dict[str, bool] + model_config = ConfigDict( + extra='allow', + ) catalog_signals: Annotated[ bool | None, Field( @@ -2356,6 +2438,8 @@ class Features(AdCPBaseModel): ] = None + + class Signals(AdCPBaseModel): data_provider_domains: Annotated[ list[DataProviderDomain] | None, @@ -2389,7 +2473,8 @@ class SignalActivation(AdCPBaseModel): claim_types: Annotated[ list[AnyUrl], Field( - description='Signal-quality claim identifiers accepted for this governed action. Values are open absolute URIs and MUST be a subset of adcp.attestations.accepted_claim_types.' + description='Signal-quality claim identifiers accepted for this governed action. Values are open absolute URIs and MUST be a subset of adcp.attestations.accepted_claim_types.', + min_length=1, ), ] @@ -4180,13 +4265,15 @@ class AcceptedIssuer(AdCPBaseModel): claim_types: Annotated[ list[AnyUrl] | None, Field( - description='Optional subset of accepted_claim_types this issuer may assert. Omit to allow any globally accepted claim type for this issuer.' + description='Optional subset of accepted_claim_types this issuer may assert. Omit to allow any globally accepted claim type for this issuer.', + min_length=1, ), ] = None proof_formats: Annotated[ list[AnyUrl] | None, Field( - description='Optional subset of accepted_proof_formats allowed for this issuer. Omit to allow any globally accepted proof format for this issuer.' + description='Optional subset of accepted_proof_formats allowed for this issuer. Omit to allow any globally accepted proof format for this issuer.', + min_length=1, ), ] = None credential_origins: Annotated[ @@ -4219,13 +4306,15 @@ class Attestations(AdCPBaseModel): accepted_claim_types: Annotated[ list[AnyUrl], Field( - description='Open claim identifiers the evaluator is prepared to evaluate. Each value is an absolute URI. Absence means the evaluator has not advertised portable-attestation support; an empty list is not permitted.' + description='Open claim identifiers the evaluator is prepared to evaluate. Each value is an absolute URI. Absence means the evaluator has not advertised portable-attestation support; an empty list is not permitted.', + min_length=1, ), ] accepted_proof_formats: Annotated[ list[AnyUrl], Field( - description='Open credential/proof format identifiers the evaluator can verify. Values are absolute URIs rather than a protocol enum so issuers can adopt new formats without AdCP endorsement.' + description='Open credential/proof format identifiers the evaluator can verify. Values are absolute URIs rather than a protocol enum so issuers can adopt new formats without AdCP endorsement.', + min_length=1, ), ] supported_delivery_methods: Annotated[ @@ -4728,7 +4817,8 @@ class Targeting(AdCPBaseModel): geo_places: Annotated[ dict[GeoPlaces1 | GeoPlaces2, GeoPlaces] | None, Field( - description='Place targeting support keyed by collision-safe identifier system. Each system declares exact country-to-place-type combinations, accepted catalog versions, and a machine-readable resolver. Sellers MUST reject unsupported systems, country/type pairs, versions, and identifiers rather than silently dropping them.' + description='Place targeting support keyed by collision-safe identifier system. Each system declares exact country-to-place-type combinations, accepted catalog versions, and a machine-readable resolver. Sellers MUST reject unsupported systems, country/type pairs, versions, and identifiers rather than silently dropping them.', + min_length=1, ), ] = None age_restriction: Annotated[ @@ -4871,7 +4961,7 @@ class Jurisdiction6(Jurisdiction): pass -class Disclosure2(AdCPBaseModel): +class Disclosure3(AdCPBaseModel): required: Annotated[ bool, Field( @@ -4941,7 +5031,7 @@ class Provenance2(AdCPBaseModel): ), ] = None disclosure: Annotated[ - Disclosure2 | None, + Disclosure3 | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), @@ -5165,7 +5255,7 @@ class Jurisdiction7(Jurisdiction): pass -class Disclosure3(AdCPBaseModel): +class Disclosure4(AdCPBaseModel): required: Annotated[ bool, Field( @@ -5235,7 +5325,7 @@ class Provenance3(AdCPBaseModel): ), ] = None disclosure: Annotated[ - Disclosure3 | None, + Disclosure4 | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), @@ -5458,7 +5548,7 @@ class Jurisdiction8(Jurisdiction): pass -class Disclosure4(AdCPBaseModel): +class Disclosure5(AdCPBaseModel): required: Annotated[ bool, Field( @@ -5528,7 +5618,7 @@ class Provenance4(AdCPBaseModel): ), ] = None disclosure: Annotated[ - Disclosure4 | None, + Disclosure5 | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), @@ -5744,7 +5834,7 @@ class Jurisdiction9(Jurisdiction): pass -class Disclosure5(AdCPBaseModel): +class Disclosure6(AdCPBaseModel): required: Annotated[ bool, Field( @@ -5814,7 +5904,7 @@ class Provenance5(AdCPBaseModel): ), ] = None disclosure: Annotated[ - Disclosure5 | None, + Disclosure6 | None, Field( description='Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.' ), diff --git a/src/adcp/types/generated_poc/compliance/comply_test_controller_response.py b/src/adcp/types/generated_poc/compliance/comply_test_controller_response.py index ce78f07ad..bf000fc95 100644 --- a/src/adcp/types/generated_poc/compliance/comply_test_controller_response.py +++ b/src/adcp/types/generated_poc/compliance/comply_test_controller_response.py @@ -1,12 +1,453 @@ # generated by datamodel-codegen: # filename: compliance/comply_test_controller_response.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from adcp.types._str_enum import StrEnum +from typing import Annotated, Any, Literal + +from adcp.types.base import AdCPBaseModel +from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel + +from ..core import context as context_1 +from ..core import ext as ext_1 from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope +from ..creative import audit_observation + + +class ComplyResponseArm(StrEnum): + submitted = 'submitted' + input_required = 'input-required' + rejected = 'rejected' + + +class Suggestion(RootModel[str]): + root: Annotated[str, Field(max_length=1000, min_length=1)] + + +class Forced(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + arm: Annotated[ + ComplyResponseArm, Field(description='ComplyResponseArm the seller will emit on the next forced operation response.') + ] + task_id: Annotated[ + str | None, + Field( + description="Echo of the registered task_id. Present only when arm is 'submitted' (the arm that emits a task envelope).", + max_length=128, + ), + ] = None + reason: Annotated[ + str | None, + Field( + description="Echo of the deterministic buyer-facing rejection reason. Required when arm is 'rejected'.", + max_length=2000, + min_length=1, + ), + ] = None + suggestions: Annotated[ + list[Suggestion] | None, + Field( + description='Echo of the optional deterministic alternatives registered for a rejected get_products response.', + max_length=20, + min_length=1, + ), + ] = None + + +class Method(StrEnum): + GET = 'GET' + POST = 'POST' + PUT = 'PUT' + PATCH = 'PATCH' + DELETE = 'DELETE' + HEAD = 'HEAD' + OPTIONS = 'OPTIONS' + + +class AttestationMode(StrEnum): + raw = 'raw' + digest = 'digest' + + +class Purpose(StrEnum): + platform_primary = 'platform_primary' + measurement = 'measurement' + attribution = 'attribution' + creative_serving = 'creative_serving' + identity = 'identity' + other = 'other' + + +class IdentifierMatchProof(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + identifier_value_sha256: Annotated[ + str, + Field( + description='Echo of one digest from `params.identifier_value_digests` so the runner can pair this proof with the identifier it queried.', + pattern='^[a-f0-9]{64}$', + ), + ] + found: Annotated[ + bool, + Field( + description='True if any string token in the recorded payload hashes to the queried digest. False otherwise.' + ), + ] + + +class RecordedCalls1(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + method: Annotated[Method, Field(description='HTTP method of the outbound call.')] + endpoint: Annotated[ + str, + Field( + description="Composed ` ` string used for `endpoint_pattern` matching, e.g. 'POST https://api.tiktok.com/v2/audience/upload'. Convenience field — the runner can also reconstruct from `method` + URL components." + ), + ] + url: Annotated[ + AnyUrl, + Field( + description='Full URL of the outbound call (scheme + host + path + query). Treated as untrusted agent-controlled input by report renderers — see runner-output-contract.yaml > security.rendered_output_fencing.' + ), + ] + host: Annotated[ + str | None, + Field( + description='Host portion of the URL, useful for grouping calls by upstream platform.' + ), + ] = None + path: Annotated[ + str | None, Field(description='Path portion of the URL (without query string).') + ] = None + content_type: Annotated[ + str, + Field( + description="Media type of the outbound request body, mirroring the agent's outbound `Content-Type` header (e.g., `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`). In raw mode this describes the returned `payload`; in digest mode it describes the body the digest was computed over. Storyboard `payload_must_contain` JSONPath-lite assertions are valid only when content_type is `application/json` or has a `+json` suffix AND attestation_mode is `raw` — digest mode and non-JSON content types grade `payload_must_contain` as not_applicable. Required so the runner can choose the right matcher deterministically." + ), + ] + attestation_mode: Annotated[ + Literal['raw'], + Field( + description="Per-call attestation mode echoing the request's `params.attestation_mode`. Required on every recorded_call so the `oneOf` discriminator always has an explicit value to dispatch on — no implicit defaults inside oneOf branches. Adopters MAY unilaterally downgrade a `raw` request to `digest` for a specific call when their policy requires it (e.g., the call carried regulated PII the adopter can't return raw). The runner reads this field to know which assertions to apply." + ), + ] = 'raw' + purpose: Annotated[ + Purpose | None, + Field( + description="Optional adopter-supplied semantic tag for the call's role. Values: `platform_primary` for the primary upstream platform the adapter is integrating with (e.g., a TikTok audience-upload call from a sales-social adapter); `measurement` for ancillary calls to measurement vendors (DV, IAS, Nielsen, MOAT); `attribution` for server-side conversion APIs (TTD Trans-API, Meta CAPI, AppsFlyer/Branch postbacks) that flow alongside primary platform calls in a buy-step; `creative_serving` for ad-server / CDN / tag-build calls (GAM tag generation, VAST/CDN fetches, creative trafficking); `identity` for ID-graph / hashing-service calls (LiveRamp, ID5, UID2); `other` for everything else (config fetches, internal telemetry, consent signal exchange). Lets storyboards scope `upstream_traffic` assertions via `purpose_filter` so a buyer-agent adapter that legitimately calls measurement vendors during a single buy step doesn't muddy the platform-primary assertion. Calls without a `purpose` field are treated as `purpose: other` for `purpose_filter` matching — adopters who haven't classified are matched only by storyboards filtering on `other` (or by storyboards with no `purpose_filter`). Self-reported, not adversarially trustworthy — same trust model as the rest of recorded_calls; misclassification by a façade is bounded by the runner's reporting of unclassified-call counts in `actual` when filters match zero." + ), + ] = None + payload: Annotated[ + Any, + Field( + description="Request body the agent sent. Required when `attestation_mode` is `raw` (or omitted); MUST be absent when `attestation_mode` is `digest`. Object when content_type is JSON-shaped and the controller decoded it; string otherwise. The `x-adcp-open-payload: true` annotation applies to the decoded JSON/object arm of this mixed field; non-JSON string payloads remain scalar values governed by the surrounding schema. Adopters MUST apply the recursive secret-key redaction described in this branch's top-level description before emission — secrets at any depth (Authorization values inlined into JSON bodies, embedded JWTs, presigned-URL tokens, OAuth refresh tokens) MUST be replaced with the literal string `[redacted]`. Storyboards that assert `payload_must_contain` or `identifier_paths` are matching against THIS field — secrets MUST be redacted but storyboard-supplied identifiers (hashed PII, request correlation values) MUST NOT be redacted. Adopters SHOULD cap individual payload size at 64 KiB; payloads exceeding that SHOULD be truncated with a trailing `[…truncated]` marker — large payloads bloat compliance reports and the LLM-rendered context windows that consume them.", + max_length=65536, + ), + ] + payload_digest_sha256: Annotated[ + str | None, + Field( + description='SHA-256 digest of the canonicalized outbound request body, lowercase hex (64 chars). Required when `attestation_mode` is `digest`; MUST be absent when `attestation_mode` is `raw`. Canonicalization order is normative: (1) controllers MUST apply the recursive secret-key redaction (same pattern as the raw-mode payload field) BEFORE computing the digest; (2) for `application/json` and `*+json` content types, controllers MUST then serialize the redacted body to RFC 8785 (JCS) canonical form — sorted keys, no extraneous whitespace — and digest the resulting bytes. Storyboard-supplied identifiers (hashed PII, request correlation values) MUST NOT be redacted before digest computation; they are the load-bearing match target for `identifier_match_proofs` and digesting them away makes echo verification impossible. Both `payload_digest_sha256` AND `identifier_match_proofs` MUST be computed against the same post-redaction canonical bytes — diverging the two surfaces breaks coherence between digest replay and identifier echo. JCS edge cases: when a digest-mode `query_upstream_traffic` response cannot be produced because the parsed JSON-like value tree contains a non-finite numeric value (`NaN`, `+Infinity`, or `-Infinity`), controllers MUST NOT coerce that value to `null`, a string, or any other placeholder for digest computation; they MUST return the typed ControllerError code `JCS_NON_FINITE_NUMBER`. Runners MUST grade the affected upstream_traffic digest validation `not_applicable` because RFC 8785 forbids non-finite numbers. Payloads carrying numeric identifiers MUST serialize them as JSON strings before digest computation (adtech bid payloads regularly carry IDs outside ±2^53 where I-JSON / RFC 7493 number round-tripping diverges across implementations). For non-JSON content types the digest is computed over the post-redaction raw body bytes.', + pattern='^[a-f0-9]{64}$', + ), + ] = None + payload_length: Annotated[ + int, + Field( + description='Byte length of the post-redaction body bytes represented by this recorded_call. Required in both `raw` and `digest` modes — symmetric across modes so runners can detect adopter-side truncation regardless of attestation choice. In `raw` mode this MUST equal the UTF-8 byte length of the emitted `payload` value after the same recursive secret-key redaction the controller applied before returning it. In `digest` mode this MUST equal the exact number of bytes fed into SHA-256 for `payload_digest_sha256`: RFC 8785 (JCS) canonical bytes for JSON-shaped content after redaction, or the post-redaction raw body bytes for non-JSON content. Digest-mode `payload_length` is therefore NOT the original outbound body length before JSON parsing, redaction, or canonicalization. Mismatch between observed payload length and reported `payload_length` is a controller-side bug worth surfacing in the report.', + ge=0, + ), + ] + identifier_match_proofs: Annotated[ + list[IdentifierMatchProof] | None, + Field( + description='Per-identifier echo proofs for digest-mode calls. Required when `attestation_mode` is `digest` AND the request supplied `params.identifier_value_digests`; MUST be absent or empty otherwise. Each entry corresponds to one digest from the request. Capped at 64 to match the request-side `params.identifier_value_digests` cap. Lets storyboards verify `identifier_paths` echo in digest mode without ever transmitting plaintext identifiers to the controller. SHA-256 is a privacy mechanism here, not a trust mechanism — controllers self-report `found` and a determined façade can return any boolean; consumers MUST NOT treat digest-mode passing as cryptographically more trustworthy than raw mode. Tokenization is normative: for `application/json` and `*+json` content types, controllers MUST scan exactly the JSON string-typed leaf values of the post-redaction canonicalized body — no substring matching, no word splitting, no case folding, no Unicode normalization. A token matches when its SHA-256 hash equals one of the requested digests byte-for-byte. For non-JSON content types (form-urlencoded, multipart, plain text), `identifier_match_proofs` MUST be empty and runner-side `identifier_paths` assertions targeting those calls grade `not_applicable` — token boundaries are not portably defined across non-JSON shapes.', + max_length=64, + ), + ] = None + timestamp: Annotated[ + AwareDatetime, + Field( + description="ISO 8601 timestamp the adopter recorded the outbound call. MUST reflect the adopter's wall clock at the moment the outbound request was sent (not log-flush time), and MUST be monotonically non-decreasing across recorded_calls of a single response. Used by runners to scope assertions to a specific storyboard step's window — see runner-output-contract.yaml > validation_result for the timestamp boundary semantics." + ), + ] + status_code: Annotated[ + int | None, + Field( + description='HTTP status code returned by the upstream. Optional — adopters MAY omit when the call was instrumented before the response arrived.', + ge=100, + le=599, + ), + ] = None + + +class RecordedCalls2(AdCPBaseModel): + model_config = ConfigDict( + extra='forbid', + ) + method: Annotated[Method, Field(description='HTTP method of the outbound call.')] + endpoint: Annotated[ + str, + Field( + description="Composed ` ` string used for `endpoint_pattern` matching, e.g. 'POST https://api.tiktok.com/v2/audience/upload'. Convenience field — the runner can also reconstruct from `method` + URL components." + ), + ] + url: Annotated[ + AnyUrl, + Field( + description='Full URL of the outbound call (scheme + host + path + query). Treated as untrusted agent-controlled input by report renderers — see runner-output-contract.yaml > security.rendered_output_fencing.' + ), + ] + host: Annotated[ + str | None, + Field( + description='Host portion of the URL, useful for grouping calls by upstream platform.' + ), + ] = None + path: Annotated[ + str | None, Field(description='Path portion of the URL (without query string).') + ] = None + content_type: Annotated[ + str, + Field( + description="Media type of the outbound request body, mirroring the agent's outbound `Content-Type` header (e.g., `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`). In raw mode this describes the returned `payload`; in digest mode it describes the body the digest was computed over. Storyboard `payload_must_contain` JSONPath-lite assertions are valid only when content_type is `application/json` or has a `+json` suffix AND attestation_mode is `raw` — digest mode and non-JSON content types grade `payload_must_contain` as not_applicable. Required so the runner can choose the right matcher deterministically." + ), + ] + attestation_mode: Annotated[ + Literal['digest'], + Field( + description="Per-call attestation mode echoing the request's `params.attestation_mode`. Required on every recorded_call so the `oneOf` discriminator always has an explicit value to dispatch on — no implicit defaults inside oneOf branches. Adopters MAY unilaterally downgrade a `raw` request to `digest` for a specific call when their policy requires it (e.g., the call carried regulated PII the adopter can't return raw). The runner reads this field to know which assertions to apply." + ), + ] = 'digest' + purpose: Annotated[ + Purpose | None, + Field( + description="Optional adopter-supplied semantic tag for the call's role. Values: `platform_primary` for the primary upstream platform the adapter is integrating with (e.g., a TikTok audience-upload call from a sales-social adapter); `measurement` for ancillary calls to measurement vendors (DV, IAS, Nielsen, MOAT); `attribution` for server-side conversion APIs (TTD Trans-API, Meta CAPI, AppsFlyer/Branch postbacks) that flow alongside primary platform calls in a buy-step; `creative_serving` for ad-server / CDN / tag-build calls (GAM tag generation, VAST/CDN fetches, creative trafficking); `identity` for ID-graph / hashing-service calls (LiveRamp, ID5, UID2); `other` for everything else (config fetches, internal telemetry, consent signal exchange). Lets storyboards scope `upstream_traffic` assertions via `purpose_filter` so a buyer-agent adapter that legitimately calls measurement vendors during a single buy step doesn't muddy the platform-primary assertion. Calls without a `purpose` field are treated as `purpose: other` for `purpose_filter` matching — adopters who haven't classified are matched only by storyboards filtering on `other` (or by storyboards with no `purpose_filter`). Self-reported, not adversarially trustworthy — same trust model as the rest of recorded_calls; misclassification by a façade is bounded by the runner's reporting of unclassified-call counts in `actual` when filters match zero." + ), + ] = None + payload: Annotated[ + Any | None, + Field( + description="Request body the agent sent. Required when `attestation_mode` is `raw` (or omitted); MUST be absent when `attestation_mode` is `digest`. Object when content_type is JSON-shaped and the controller decoded it; string otherwise. The `x-adcp-open-payload: true` annotation applies to the decoded JSON/object arm of this mixed field; non-JSON string payloads remain scalar values governed by the surrounding schema. Adopters MUST apply the recursive secret-key redaction described in this branch's top-level description before emission — secrets at any depth (Authorization values inlined into JSON bodies, embedded JWTs, presigned-URL tokens, OAuth refresh tokens) MUST be replaced with the literal string `[redacted]`. Storyboards that assert `payload_must_contain` or `identifier_paths` are matching against THIS field — secrets MUST be redacted but storyboard-supplied identifiers (hashed PII, request correlation values) MUST NOT be redacted. Adopters SHOULD cap individual payload size at 64 KiB; payloads exceeding that SHOULD be truncated with a trailing `[…truncated]` marker — large payloads bloat compliance reports and the LLM-rendered context windows that consume them.", + max_length=65536, + ), + ] = None + payload_digest_sha256: Annotated[ + str, + Field( + description='SHA-256 digest of the canonicalized outbound request body, lowercase hex (64 chars). Required when `attestation_mode` is `digest`; MUST be absent when `attestation_mode` is `raw`. Canonicalization order is normative: (1) controllers MUST apply the recursive secret-key redaction (same pattern as the raw-mode payload field) BEFORE computing the digest; (2) for `application/json` and `*+json` content types, controllers MUST then serialize the redacted body to RFC 8785 (JCS) canonical form — sorted keys, no extraneous whitespace — and digest the resulting bytes. Storyboard-supplied identifiers (hashed PII, request correlation values) MUST NOT be redacted before digest computation; they are the load-bearing match target for `identifier_match_proofs` and digesting them away makes echo verification impossible. Both `payload_digest_sha256` AND `identifier_match_proofs` MUST be computed against the same post-redaction canonical bytes — diverging the two surfaces breaks coherence between digest replay and identifier echo. JCS edge cases: when a digest-mode `query_upstream_traffic` response cannot be produced because the parsed JSON-like value tree contains a non-finite numeric value (`NaN`, `+Infinity`, or `-Infinity`), controllers MUST NOT coerce that value to `null`, a string, or any other placeholder for digest computation; they MUST return the typed ControllerError code `JCS_NON_FINITE_NUMBER`. Runners MUST grade the affected upstream_traffic digest validation `not_applicable` because RFC 8785 forbids non-finite numbers. Payloads carrying numeric identifiers MUST serialize them as JSON strings before digest computation (adtech bid payloads regularly carry IDs outside ±2^53 where I-JSON / RFC 7493 number round-tripping diverges across implementations). For non-JSON content types the digest is computed over the post-redaction raw body bytes.', + pattern='^[a-f0-9]{64}$', + ), + ] + payload_length: Annotated[ + int, + Field( + description='Byte length of the post-redaction body bytes represented by this recorded_call. Required in both `raw` and `digest` modes — symmetric across modes so runners can detect adopter-side truncation regardless of attestation choice. In `raw` mode this MUST equal the UTF-8 byte length of the emitted `payload` value after the same recursive secret-key redaction the controller applied before returning it. In `digest` mode this MUST equal the exact number of bytes fed into SHA-256 for `payload_digest_sha256`: RFC 8785 (JCS) canonical bytes for JSON-shaped content after redaction, or the post-redaction raw body bytes for non-JSON content. Digest-mode `payload_length` is therefore NOT the original outbound body length before JSON parsing, redaction, or canonicalization. Mismatch between observed payload length and reported `payload_length` is a controller-side bug worth surfacing in the report.', + ge=0, + ), + ] + identifier_match_proofs: Annotated[ + list[IdentifierMatchProof] | None, + Field( + description='Per-identifier echo proofs for digest-mode calls. Required when `attestation_mode` is `digest` AND the request supplied `params.identifier_value_digests`; MUST be absent or empty otherwise. Each entry corresponds to one digest from the request. Capped at 64 to match the request-side `params.identifier_value_digests` cap. Lets storyboards verify `identifier_paths` echo in digest mode without ever transmitting plaintext identifiers to the controller. SHA-256 is a privacy mechanism here, not a trust mechanism — controllers self-report `found` and a determined façade can return any boolean; consumers MUST NOT treat digest-mode passing as cryptographically more trustworthy than raw mode. Tokenization is normative: for `application/json` and `*+json` content types, controllers MUST scan exactly the JSON string-typed leaf values of the post-redaction canonicalized body — no substring matching, no word splitting, no case folding, no Unicode normalization. A token matches when its SHA-256 hash equals one of the requested digests byte-for-byte. For non-JSON content types (form-urlencoded, multipart, plain text), `identifier_match_proofs` MUST be empty and runner-side `identifier_paths` assertions targeting those calls grade `not_applicable` — token boundaries are not portably defined across non-JSON shapes.', + max_length=64, + ), + ] = None + timestamp: Annotated[ + AwareDatetime, + Field( + description="ISO 8601 timestamp the adopter recorded the outbound call. MUST reflect the adopter's wall clock at the moment the outbound request was sent (not log-flush time), and MUST be monotonically non-decreasing across recorded_calls of a single response. Used by runners to scope assertions to a specific storyboard step's window — see runner-output-contract.yaml > validation_result for the timestamp boundary semantics." + ), + ] + status_code: Annotated[ + int | None, + Field( + description='HTTP status code returned by the upstream. Optional — adopters MAY omit when the call was instrumented before the response arrived.', + ge=100, + le=599, + ), + ] = None + + +class RecordedCalls(RootModel[RecordedCalls1 | RecordedCalls2]): + root: Annotated[RecordedCalls1 | RecordedCalls2, Field(discriminator='attestation_mode')] + def __getattr__(self, name: str) -> Any: + """Proxy attribute access to the wrapped type.""" + if name.startswith('_'): + raise AttributeError(name) + return getattr(self.root, name) + +class Error(StrEnum): + INVALID_TRANSITION = 'INVALID_TRANSITION' + INVALID_STATE = 'INVALID_STATE' + NOT_FOUND = 'NOT_FOUND' + UNKNOWN_SCENARIO = 'UNKNOWN_SCENARIO' + INVALID_PARAMS = 'INVALID_PARAMS' + FORBIDDEN = 'FORBIDDEN' + JCS_NON_FINITE_NUMBER = 'JCS_NON_FINITE_NUMBER' + INTERNAL_ERROR = 'INTERNAL_ERROR' + + +class ComplyTestControllerResponse1(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + scenarios: Annotated[ + list[str], + Field( + description='Scenarios this seller has implemented. Runners and sellers MUST accept unknown scenario strings (open-for-extension) — new scenarios may be added in additive releases. Adopters who advertise `catalog_item_availability_probe` support deterministic cross-principal reference, eligibility-gate, expiry-clock, and catalog-generation tests for the catalog availability storyboard. Adopters who advertise `compact_product_lifecycle_probe` support deterministic synchronous list/request/finalize/decline/accept/control/readback behavior for a prepared product and strict post-deadline expiry of a committed proposal. Adopters who advertise `compact_direct_buy_lifecycle_probe` support deterministic synchronous list/buy/control/readback behavior for a prepared product. Adopters who advertise `reporting_core_lifecycle_probe` support deterministic obligation-before-report, clock-health, and zero-row reporting tests without wall-clock waits. `reliable_reporting_core_integrity_probe`, `reliable_reporting_managed_delivery_probe`, and `reliable_reporting_reconciled_billing_probe` seed the source-calendar/checkpoint, managed-resource, and receipt/adjustment workflows used by the Reliable Reporting tier storyboards. Adopters who advertise `force_creative_purge` opt in to deterministic creative purge coverage for account-level lifecycle webhooks. Adopters who advertise `seed_measurement_catalog` opt in to deterministic measurement-catalog fixtures used by vendor_metric precondition storyboards. Adopters who advertise `query_upstream_traffic` opt in to the upstream-traffic conformance contract; storyboards that declare `check: upstream_traffic` grade not_applicable against adopters who do not advertise it. Adopters who advertise `query_provenance_audit_observations` opt in to sandbox-only audit-observation assertions for accepted creatives. Adopters who advertise `force_upstream_unavailable` opt in to stale-cache conformance testing via the `stale_response_advisory` storyboard.' + ), + ] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse2(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + previous_state: Annotated[str, Field(description='State before this transition')] + current_state: Annotated[str, Field(description='State after this transition')] + message: Annotated[ + str | None, Field(description='Human-readable description of the transition') + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse3(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + simulated: Annotated[ + dict[str, Any], + Field(description='Values injected or applied by this call. Shape depends on scenario.'), + ] + cumulative: Annotated[ + dict[str, Any] | None, + Field(description='Running totals across all simulation calls (simulate_delivery only)'), + ] = None + message: str | None = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse4(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + forced: Annotated[ + Forced, + Field( + description='Echo of the registered directive. The next matching operation call from this sandbox account will return the named arm.' + ), + ] + message: Annotated[str | None, Field(description='Human-readable acknowledgement.')] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse5(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + message: Annotated[str | None, Field(description='Human-readable acknowledgement.')] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse6(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + creative_id: Annotated[ + str, Field(description='Creative ID whose audit observations were queried.') + ] + audit_observations: Annotated[ + list[audit_observation.CreativeAuditObservation], + Field(description='Audit observations recorded for the creative in the sandbox session.'), + ] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse7(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[True] + recorded_calls: Annotated[ + list[RecordedCalls], + Field( + description="Outbound HTTP calls caused by the requesting principal in the requested window, ordered by `timestamp` ascending. Cross-caller calls MUST NOT appear here. Each item declares its `attestation_mode`: `raw` items carry the full `payload`; `digest` items carry `payload_digest_sha256` + `payload_length` + optional `identifier_match_proofs[]` instead, for adopters who can't return raw payloads under their privacy/data-residency policy." + ), + ] + total_count: Annotated[ + int, + Field( + description='Total calls in the requested window before any pagination — `recorded_calls.length` may be smaller when `params.limit` truncated the response.', + ge=0, + ), + ] + truncated: Annotated[ + bool | None, + Field( + description='True when `total_count > recorded_calls.length`. Runners MAY raise the `limit` and re-query, but storyboards SHOULD declare assertions that fit within the default 100-call window — a truncated response is a signal that the storyboard step is causing more upstream activity than expected.' + ), + ] = None + since_timestamp: Annotated[ + AwareDatetime, + Field( + description="Echo of the `since_timestamp` the runner requested (or the session-start timestamp the adopter substituted when the runner omitted it). Informational — the runner SHOULD use its own clock-bracket of when it issued the AdCP step request, not this echo, when attributing recorded_calls to a specific step. The controller is part of the adopter's claimed conformance; the echo is not adversarially trustworthy." + ), + ] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ComplyTestControllerResponse8(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Literal[False] + error: Annotated[ + Error, + Field( + description='Structured error code. `JCS_NON_FINITE_NUMBER` is reserved for digest-mode upstream_traffic responses that cannot be RFC 8785/JCS-canonicalized because the parsed JSON-like value tree contains a non-finite numeric value (`NaN`, `+Infinity`, or `-Infinity`); controllers MUST NOT coerce those values during digest computation, and runners grade that validation `not_applicable`, not failed.' + ), + ] + error_detail: Annotated[ + str | None, Field(description='Human-readable explanation of the failure') + ] = None + current_state: Annotated[ + str | None, Field(description='Current state of the entity, or null if not found') + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None -class ComplyTestControllerResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass +ComplyTestControllerResponse = ( + ComplyTestControllerResponse1 + | ComplyTestControllerResponse2 + | ComplyTestControllerResponse3 + | ComplyTestControllerResponse4 + | ComplyTestControllerResponse5 + | ComplyTestControllerResponse6 + | ComplyTestControllerResponse7 + | ComplyTestControllerResponse8 +) diff --git a/src/adcp/types/generated_poc/content_standards/create_content_standards_response.py b/src/adcp/types/generated_poc/content_standards/create_content_standards_response.py index 9f53bf548..09049ae19 100644 --- a/src/adcp/types/generated_poc/content_standards/create_content_standards_response.py +++ b/src/adcp/types/generated_poc/content_standards/create_content_standards_response.py @@ -1,12 +1,38 @@ # generated by datamodel-codegen: # filename: content_standards/create_content_standards_response.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from typing import Annotated + +from pydantic import Field + +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope -class CreateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass +class CreateContentStandardsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): + standards_id: Annotated[ + str, Field(description='Unique identifier for the created standards configuration') + ] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class CreateContentStandardsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): + errors: list[error.Error] + conflicting_standards_id: Annotated[ + str | None, + Field( + description='If the error is a scope conflict, the ID of the existing standards that conflict' + ), + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +CreateContentStandardsResponse = CreateContentStandardsResponse1 | CreateContentStandardsResponse2 diff --git a/src/adcp/types/generated_poc/content_standards/list_content_standards_response.py b/src/adcp/types/generated_poc/content_standards/list_content_standards_response.py index 0eb76942f..6ec07cde4 100644 --- a/src/adcp/types/generated_poc/content_standards/list_content_standards_response.py +++ b/src/adcp/types/generated_poc/content_standards/list_content_standards_response.py @@ -1,12 +1,36 @@ # generated by datamodel-codegen: # filename: content_standards/list_content_standards_response.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from typing import Annotated + +from pydantic import Field + +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 +from ..core import pagination_response from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope +from . import content_standards + + +class ListContentStandardsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): + errors: list[error.Error] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class ListContentStandardsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): + standards: Annotated[ + list[content_standards.ContentStandards], + Field(description='Array of content standards configurations matching the filter criteria'), + ] + pagination: pagination_response.PaginationResponse | None = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None -class ListContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass +ListContentStandardsResponse = ListContentStandardsResponse1 | ListContentStandardsResponse2 diff --git a/src/adcp/types/generated_poc/content_standards/update_content_standards_response.py b/src/adcp/types/generated_poc/content_standards/update_content_standards_response.py index 33abd5e87..dd721ffaa 100644 --- a/src/adcp/types/generated_poc/content_standards/update_content_standards_response.py +++ b/src/adcp/types/generated_poc/content_standards/update_content_standards_response.py @@ -1,12 +1,48 @@ # generated by datamodel-codegen: # filename: content_standards/update_content_standards_response.json -# timestamp: 2026-05-22T13:15:12+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from ..core import context as context_1 +from ..core import error +from ..core import ext as ext_1 from ..core.protocol_envelope import ProtocolEnvelope from ..core.version_envelope import AdcpVersionEnvelope -class UpdateContentStandardsResponse(AdcpVersionEnvelope, ProtocolEnvelope): - pass +class UpdateContentStandardsResponse1(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Annotated[ + Literal[True], Field(description='Indicates the update was applied successfully') + ] + standards_id: Annotated[str, Field(description='ID of the updated standards configuration')] + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +class UpdateContentStandardsResponse2(AdcpVersionEnvelope, ProtocolEnvelope): + model_config = ConfigDict( + extra='allow', + ) + success: Annotated[Literal[False], Field(description='Indicates the update failed')] + errors: Annotated[ + list[error.Error], Field(description='Errors that occurred during the update', min_length=1) + ] + conflicting_standards_id: Annotated[ + str | None, + Field( + description='If scope change conflicts with another configuration, the ID of the conflicting standards' + ), + ] = None + context: context_1.ContextObject | None = None + ext: ext_1.ExtensionObject | None = None + + +UpdateContentStandardsResponse = UpdateContentStandardsResponse1 | UpdateContentStandardsResponse2 diff --git a/src/adcp/types/generated_poc/core/account_change.py b/src/adcp/types/generated_poc/core/account_change.py index aa43fe17e..af8954f9d 100644 --- a/src/adcp/types/generated_poc/core/account_change.py +++ b/src/adcp/types/generated_poc/core/account_change.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/account_change.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -45,7 +45,8 @@ class Resource(AdCPBaseModel): parent_ids: Annotated[ dict[str, str] | None, Field( - description='Additional canonical parent identifiers needed to repair a nested resource, for example media_buy_id and package_id for a creative assignment. Keys and values MUST NOT contain sensitive payload data.' + description='Additional canonical parent identifiers needed to repair a nested resource, for example media_buy_id and package_id for a creative assignment. Keys and values MUST NOT contain sensitive payload data.', + max_length=12, ), ] = None diff --git a/src/adcp/types/generated_poc/core/account_change_recorded_webhook.py b/src/adcp/types/generated_poc/core/account_change_recorded_webhook.py index 4fd6b5233..0579e4149 100644 --- a/src/adcp/types/generated_poc/core/account_change_recorded_webhook.py +++ b/src/adcp/types/generated_poc/core/account_change_recorded_webhook.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/account_change_recorded_webhook.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -18,7 +18,7 @@ class Resource(AdCPBaseModel): ) type: Annotated[str, Field(max_length=100, min_length=1, pattern='^[a-z][a-z0-9_.-]{0,99}$')] resource_id: Annotated[str, Field(max_length=255, min_length=1)] - parent_ids: dict[str, str] | None = None + parent_ids: Annotated[dict[str, str] | None, Field(max_length=12)] = None class AccountChangeRecordedWebhook(AdCPBaseModel): diff --git a/src/adcp/types/generated_poc/core/app_item.py b/src/adcp/types/generated_poc/core/app_item.py index adf16f63d..d7cfaa505 100644 --- a/src/adcp/types/generated_poc/core/app_item.py +++ b/src/adcp/types/generated_poc/core/app_item.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/app_item.json -# timestamp: 2026-06-01T00:32:59+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -79,7 +79,8 @@ class AppItem(AdCPBaseModel): screenshots: Annotated[ list[AnyUrl] | None, Field( - description='App store screenshot URLs. Used by platforms for creative generation when native store assets are not available.' + description='App store screenshot URLs. Used by platforms for creative generation when native store assets are not available.', + min_length=1, ), ] = None preview_video_url: Annotated[ diff --git a/src/adcp/types/generated_poc/core/attestation_capabilities.py b/src/adcp/types/generated_poc/core/attestation_capabilities.py index 40d887cba..bd3e47881 100644 --- a/src/adcp/types/generated_poc/core/attestation_capabilities.py +++ b/src/adcp/types/generated_poc/core/attestation_capabilities.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/attestation_capabilities.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -53,8 +53,8 @@ class AcceptedVerifier(AdCPBaseModel): extra='forbid', ) agent_url: AnyUrl - claim_types: list[AnyUrl] | None = None - proof_formats: list[AnyUrl] | None = None + claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None + proof_formats: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None ext: ext_1.ExtensionObject | None = None @@ -66,13 +66,15 @@ class AcceptedIssuer(AdCPBaseModel): claim_types: Annotated[ list[AnyUrl] | None, Field( - description='Optional subset of accepted_claim_types this issuer may assert. Omit to allow any globally accepted claim type for this issuer.' + description='Optional subset of accepted_claim_types this issuer may assert. Omit to allow any globally accepted claim type for this issuer.', + min_length=1, ), ] = None proof_formats: Annotated[ list[AnyUrl] | None, Field( - description='Optional subset of accepted_proof_formats allowed for this issuer. Omit to allow any globally accepted proof format for this issuer.' + description='Optional subset of accepted_proof_formats allowed for this issuer. Omit to allow any globally accepted proof format for this issuer.', + min_length=1, ), ] = None credential_origins: Annotated[ @@ -99,13 +101,15 @@ class AttestationCapabilities(AdCPBaseModel): accepted_claim_types: Annotated[ list[AnyUrl], Field( - description='Open claim identifiers the evaluator is prepared to evaluate. Each value is an absolute URI. Absence means the evaluator has not advertised portable-attestation support; an empty list is not permitted.' + description='Open claim identifiers the evaluator is prepared to evaluate. Each value is an absolute URI. Absence means the evaluator has not advertised portable-attestation support; an empty list is not permitted.', + min_length=1, ), ] accepted_proof_formats: Annotated[ list[AnyUrl], Field( - description='Open credential/proof format identifiers the evaluator can verify. Values are absolute URIs rather than a protocol enum so issuers can adopt new formats without AdCP endorsement.' + description='Open credential/proof format identifiers the evaluator can verify. Values are absolute URIs rather than a protocol enum so issuers can adopt new formats without AdCP endorsement.', + min_length=1, ), ] supported_delivery_methods: Annotated[ diff --git a/src/adcp/types/generated_poc/core/audience_evidence.py b/src/adcp/types/generated_poc/core/audience_evidence.py index e44866610..2719e219c 100644 --- a/src/adcp/types/generated_poc/core/audience_evidence.py +++ b/src/adcp/types/generated_poc/core/audience_evidence.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/audience_evidence.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -117,13 +117,10 @@ class Subject26(Subject22): ) -class Subject27(Subject23): +class Subject27(Subject24): model_config = ConfigDict( extra='forbid', ) - agent_url: Annotated[ - AnyUrl, Field(description='Canonical HTTPS endpoint of the agent the claim concerns.') - ] class Subject32(Subject12): @@ -191,7 +188,7 @@ class Subject36(Subject32): ] = None -class Subject37(Subject33): +class Subject37(Subject34): model_config = ConfigDict( extra='forbid', ) @@ -230,11 +227,10 @@ class Subject16(Subject12): brand: brand_ref.BrandReference -class Subject17(Subject13): +class Subject17(Subject14): model_config = ConfigDict( extra='forbid', ) - brand: brand_ref.BrandReference class Subject(RootModel[Subject15 | Subject16 | Subject17]): diff --git a/src/adcp/types/generated_poc/core/audience_evidence_requirements.py b/src/adcp/types/generated_poc/core/audience_evidence_requirements.py index 2d9b7bf41..1da41bba7 100644 --- a/src/adcp/types/generated_poc/core/audience_evidence_requirements.py +++ b/src/adcp/types/generated_poc/core/audience_evidence_requirements.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/audience_evidence_requirements.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -127,7 +127,8 @@ class AudienceEvidenceRequirements(AdCPBaseModel): accepted_attestation_claim_types: Annotated[ list[AnyUrl] | None, Field( - description='Optional acceptable claim types when attestation is used. Buyer-supplied claim types further constrain but never broaden seller policy.' + description='Optional acceptable claim types when attestation is used. Buyer-supplied claim types further constrain but never broaden seller policy.', + min_length=1, ), ] = None ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/core/canonical_media_buy_features.py b/src/adcp/types/generated_poc/core/canonical_media_buy_features.py index 90d58b917..355163674 100644 --- a/src/adcp/types/generated_poc/core/canonical_media_buy_features.py +++ b/src/adcp/types/generated_poc/core/canonical_media_buy_features.py @@ -1,18 +1,22 @@ # generated by datamodel-codegen: # filename: core/canonical_media_buy_features.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:29:24+00:00 from __future__ import annotations -from typing import Annotated +from typing import Annotated, Dict from adcp.types.base import AdCPBaseModel -from pydantic import Field +from pydantic import ConfigDict, Field from . import bidding_policy_capability class CanonicalMediaBuyFeatures(AdCPBaseModel): + __pydantic_extra__: Dict[str, bool] + model_config = ConfigDict( + extra='allow', + ) property_filtering: bool | None = None catalog_management: bool | None = None reporting_commitment_snapshots: Annotated[ diff --git a/src/adcp/types/generated_poc/core/creative_representation.py b/src/adcp/types/generated_poc/core/creative_representation.py index a1b20960f..a6b48e291 100644 --- a/src/adcp/types/generated_poc/core/creative_representation.py +++ b/src/adcp/types/generated_poc/core/creative_representation.py @@ -1,23 +1,15 @@ # generated by datamodel-codegen: # filename: core/creative_representation.json -# timestamp: 2026-09-01T19:37:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations -from typing import Any, Annotated +from typing import Annotated, Any from adcp.types.base import AdCPBaseModel -from pydantic import ConfigDict, Field, RootModel, StringConstraints +from pydantic import ConfigDict, Field -from . import brand_ref, canonical_format_kind, creative_assets -from . import ext as ext_1 -from . import format_id as format_id_1 -from . import format_option_ref as format_option_ref_1 -from . import industry_identifier -from . import provenance as provenance_1 -from . import representation_selection as representation_selection_1 -from . import rights_constraint -from .assets import asset_union +from .creative_manifest import CreativeManifest class Source(AdCPBaseModel): @@ -37,7 +29,7 @@ class Source(AdCPBaseModel): ] = None -class CreativeRepresentation3(AdCPBaseModel): +class CreativeRepresentation(CreativeManifest): model_config = ConfigDict( extra='allow', ) @@ -50,200 +42,4 @@ class CreativeRepresentation3(AdCPBaseModel): ), ] source: Source - - -class Assets(RootModel[list[asset_union.AssetVariant]]): - root: Annotated[list[asset_union.AssetVariant], Field(min_length=1)] - - -class CreativeRepresentation1(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for 3.x compatibility. New manifests use canonical `format_kind` and, when product routing requires it, `format_option_ref`. Mutually exclusive with format_kind.', - ), - ] - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind | None, - Field( - description="Canonical 3.2 path. The canonical format name this manifest targets (e.g., `image`, `video_hosted`, `audio_vast`, `seller_rendered_stateful_display`, `coordinated_placements`). Selects the contract against which the seller validates the manifest's assets. Mutually exclusive with deprecated `format_id`." - ), - ] = None - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product carries multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the manifest to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Present when this seller-bound manifest was selected from a CreativeRepresentationSet. Preserves creative, complete revision digest, and selected representation lineage through sync and reporting.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description="Map of slot keys to actual asset content. Legacy named-format path: each key matches an `asset_id` from the format's `assets` array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). 3.1+ canonical-format path: each key matches an `asset_group_id` from the format's `slots` declaration drawn from the canonical vocabulary registry (e.g., 'images_landscape', 'video', 'published_post', 'landing_page_url', 'vast_tag', 'script', 'creative_brief'). Either path produces the same envelope shape; only the slot-key vocabulary differs.\n\nEach slot value is **either** a single asset object (most slots — image, video, published_post, vast_tag, landing_page_url, etc.) **or** an array of asset objects (slots with `min`/`max` counts on the format declaration — `cards` on `image_carousel`, `headlines` / `descriptions` / `images_landscape` on `responsive_creative`, etc.). Single-vs-array shape is governed by the format's `slots[].min` and `slots[].max` parameters: when `max > 1` (or when the slot is conceptually a pool), the value MUST be an array; when the slot is single-valued, the value MUST be a single object. Each asset value (single or array element) carries an `asset_type` discriminator (image, video, audio, vast, daast, text, markdown, url, html, css, webhook, javascript, brief, catalog, published_post, zip, card) that selects the matching asset schema. Validators with OpenAPI-style discriminator support use `asset_type` to report errors against only the selected branch instead of all branches." - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description="Component-addressed asset maps for `coordinated_placements`. Each key MUST match one `params.components[].component_id`; its value supplies that component's canonical slots. Shared assets remain in top-level `assets` and are injected only into components named by `shared_slots[].consumed_by`. This namespace allows two components to use the same canonical slot name, such as `image_main`, without collision. It MUST be absent for non-`coordinated_placements` manifests." - ), - ] = None - brand: Annotated[ - brand_ref.BrandReference | None, - Field( - description="Brand identity reference (BrandRef — `domain` plus optional `brand_id` for house-of-brands; plus optional inline `brand_kit_override` for per-creative tweaks where brand.json is missing/stale). When present, the seller pulls master brand identity (logo, palette, fonts, voice, and visual guidelines) from the brand's brand.json automatically; supported fields present in `brand_kit_override` take precedence, and all other master identity fields continue to come from brand.json. Catalogs supply product or item payload. Catalog item asset groups — including an item-level `logo` for a property or franchise — are item identity selected through format field bindings; they do not override brand.json's master logo or other brand identity fields. v2 formats no longer redeclare brand_logo / brand_colors / brand_voice as explicit slots — brand identity is implicit context." - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints attached to this creative. Buyer-carried fields are informational until a serving party evaluates an issuer-bound attestation reference under its own policy. Only a verified, unexpired, unrevoked, digest-matched evaluation can support serving authorization; verification_url is never authority.' - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this specific manifest (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). When present, overrides creative-level identifiers. Use when different format versions of the same source creative have distinct traffic identifiers (e.g., the :15 and :30 cuts, or separate TV and radio versions). Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative manifest. Serves as the default provenance for all assets in this manifest. An asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - ext: ext_1.ExtensionObject | None = None - - -class CreativeRepresentation2(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject | None, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for 3.x compatibility. New manifests use canonical `format_kind` and, when product routing requires it, `format_option_ref`. Mutually exclusive with format_kind.', - ), - ] = None - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description="Canonical 3.2 path. The canonical format name this manifest targets (e.g., `image`, `video_hosted`, `audio_vast`, `seller_rendered_stateful_display`, `coordinated_placements`). Selects the contract against which the seller validates the manifest's assets. Mutually exclusive with deprecated `format_id`." - ), - ] - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product carries multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the manifest to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Present when this seller-bound manifest was selected from a CreativeRepresentationSet. Preserves creative, complete revision digest, and selected representation lineage through sync and reporting.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description="Map of slot keys to actual asset content. Legacy named-format path: each key matches an `asset_id` from the format's `assets` array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). 3.1+ canonical-format path: each key matches an `asset_group_id` from the format's `slots` declaration drawn from the canonical vocabulary registry (e.g., 'images_landscape', 'video', 'published_post', 'landing_page_url', 'vast_tag', 'script', 'creative_brief'). Either path produces the same envelope shape; only the slot-key vocabulary differs.\n\nEach slot value is **either** a single asset object (most slots — image, video, published_post, vast_tag, landing_page_url, etc.) **or** an array of asset objects (slots with `min`/`max` counts on the format declaration — `cards` on `image_carousel`, `headlines` / `descriptions` / `images_landscape` on `responsive_creative`, etc.). Single-vs-array shape is governed by the format's `slots[].min` and `slots[].max` parameters: when `max > 1` (or when the slot is conceptually a pool), the value MUST be an array; when the slot is single-valued, the value MUST be a single object. Each asset value (single or array element) carries an `asset_type` discriminator (image, video, audio, vast, daast, text, markdown, url, html, css, webhook, javascript, brief, catalog, published_post, zip, card) that selects the matching asset schema. Validators with OpenAPI-style discriminator support use `asset_type` to report errors against only the selected branch instead of all branches." - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description="Component-addressed asset maps for `coordinated_placements`. Each key MUST match one `params.components[].component_id`; its value supplies that component's canonical slots. Shared assets remain in top-level `assets` and are injected only into components named by `shared_slots[].consumed_by`. This namespace allows two components to use the same canonical slot name, such as `image_main`, without collision. It MUST be absent for non-`coordinated_placements` manifests." - ), - ] = None - brand: Annotated[ - brand_ref.BrandReference | None, - Field( - description="Brand identity reference (BrandRef — `domain` plus optional `brand_id` for house-of-brands; plus optional inline `brand_kit_override` for per-creative tweaks where brand.json is missing/stale). When present, the seller pulls master brand identity (logo, palette, fonts, voice, and visual guidelines) from the brand's brand.json automatically; supported fields present in `brand_kit_override` take precedence, and all other master identity fields continue to come from brand.json. Catalogs supply product or item payload. Catalog item asset groups — including an item-level `logo` for a property or franchise — are item identity selected through format field bindings; they do not override brand.json's master logo or other brand identity fields. v2 formats no longer redeclare brand_logo / brand_colors / brand_voice as explicit slots — brand identity is implicit context." - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints attached to this creative. Buyer-carried fields are informational until a serving party evaluates an issuer-bound attestation reference under its own policy. Only a verified, unexpired, unrevoked, digest-matched evaluation can support serving authorization; verification_url is never authority.' - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this specific manifest (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). When present, overrides creative-level identifiers. Use when different format versions of the same source creative have distinct traffic identifiers (e.g., the :15 and :30 cuts, or separate TV and radio versions). Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative manifest. Serves as the default provenance for all assets in this manifest. An asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - ext: ext_1.ExtensionObject | None = None - - -class CreativeRepresentation4(CreativeRepresentation1, CreativeRepresentation3): - model_config = ConfigDict( - extra='allow', - ) - representation_id: Annotated[ - str, - Field( - description='Stable representation identifier, unique within the enclosing CreativeRepresentationSet revision.', - min_length=1, - pattern='^[a-zA-Z0-9_-]+$', - ), - ] - source: Source - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description="Canonical 3.2 path. The canonical format name this manifest targets (e.g., `image`, `video_hosted`, `audio_vast`, `seller_rendered_stateful_display`, `coordinated_placements`). Selects the contract against which the seller validates the manifest's assets. Mutually exclusive with deprecated `format_id`." - ), - ] - - -class CreativeRepresentation5(CreativeRepresentation2, CreativeRepresentation3): - model_config = ConfigDict( - extra='allow', - ) - representation_id: Annotated[ - str, - Field( - description='Stable representation identifier, unique within the enclosing CreativeRepresentationSet revision.', - min_length=1, - pattern='^[a-zA-Z0-9_-]+$', - ), - ] - source: Source - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description="Canonical 3.2 path. The canonical format name this manifest targets (e.g., `image`, `video_hosted`, `audio_vast`, `seller_rendered_stateful_display`, `coordinated_placements`). Selects the contract against which the seller validates the manifest's assets. Mutually exclusive with deprecated `format_id`." - ), - ] - - -class CreativeRepresentation(RootModel[CreativeRepresentation4 | CreativeRepresentation5]): - root: Annotated[ - CreativeRepresentation4 | CreativeRepresentation5, - Field( - description="One equivalent trafficking representation retained within a CreativeRepresentationSet before seller binding. `representation_id` is stable within its parent revision and identifies a representation, not rotation, A/B content, build output, locale, or a seller's served execution variant.", - title='Creative Representation', - ), - ] - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the wrapped type.""" - if name.startswith('_'): - raise AttributeError(name) - return getattr(self.root, name) + format_kind: Any diff --git a/src/adcp/types/generated_poc/core/delivery_metric_aggregate.py b/src/adcp/types/generated_poc/core/delivery_metric_aggregate.py index d831dcbfd..47b61c03c 100644 --- a/src/adcp/types/generated_poc/core/delivery_metric_aggregate.py +++ b/src/adcp/types/generated_poc/core/delivery_metric_aggregate.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/delivery_metric_aggregate.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -119,6 +119,10 @@ class Field0(AdCPBaseModel): ] = None +class DeliveryMetricAggregate1(Field0): + scope: Literal['standard'] = 'standard' + + class Qualifier3(AdCPBaseModel): model_config = ConfigDict( extra='forbid', @@ -194,9 +198,9 @@ class DeliveryMetricAggregate2(AdCPBaseModel): ] = None -class DeliveryMetricAggregate(RootModel[Field0 | DeliveryMetricAggregate2]): +class DeliveryMetricAggregate(RootModel[DeliveryMetricAggregate1 | DeliveryMetricAggregate2]): root: Annotated[ - Field0 | DeliveryMetricAggregate2, + DeliveryMetricAggregate1 | DeliveryMetricAggregate2, Field( description='One delivery metric value partitioned by metric scope and qualifier. Its standard branch is used at package grain by `by_package[].metric_values`; both branches are retained for the deprecated cross-buy `aggregated_totals.metric_aggregates` compatibility field. Row-symmetric with `package.committed_metrics` and delivery `missing_metrics` so buyers can reconcile by `(scope, metric_id, qualifier)`.', discriminator='scope', diff --git a/src/adcp/types/generated_poc/core/forecast_point.py b/src/adcp/types/generated_poc/core/forecast_point.py index 968ec0f49..823061307 100644 --- a/src/adcp/types/generated_poc/core/forecast_point.py +++ b/src/adcp/types/generated_poc/core/forecast_point.py @@ -1,10 +1,10 @@ # generated by datamodel-codegen: # filename: core/forecast_point.json -# timestamp: 2026-08-21T04:35:18+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations -from typing import Annotated +from typing import Annotated, Dict from adcp.types.base import AdCPBaseModel from pydantic import ConfigDict, Field @@ -28,6 +28,10 @@ class CoverageRate(ForecastRange): class Metrics(AdCPBaseModel): + __pydantic_extra__: Dict[str, forecast_range.ForecastRange] + model_config = ConfigDict( + extra='allow', + ) audience_size: forecast_range.ForecastRange | None = None reach: forecast_range.ForecastRange | None = None frequency: forecast_range.ForecastRange | None = None @@ -52,6 +56,8 @@ class Metrics(AdCPBaseModel): ] = None + + class ViewableRate(CoverageRate): pass diff --git a/src/adcp/types/generated_poc/core/format.py b/src/adcp/types/generated_poc/core/format.py index 719ae1f42..b7d4923f0 100644 --- a/src/adcp/types/generated_poc/core/format.py +++ b/src/adcp/types/generated_poc/core/format.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/format.json -# timestamp: 2026-09-01T19:37:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -297,149 +297,151 @@ class Assets(BaseIndividualAsset): requirements: image_asset_requirements.ImageAssetRequirements | None = None -class Assets11(BaseIndividualAsset): +class Assets9(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['video'] = 'video' requirements: video_asset_requirements.VideoAssetRequirements | None = None -class Assets12(BaseIndividualAsset): +class Assets10(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['audio'] = 'audio' requirements: audio_asset_requirements.AudioAssetRequirements | None = None -class Assets13(BaseIndividualAsset): +class Assets11(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['text'] = 'text' requirements: text_asset_requirements.TextAssetRequirements | None = None -class Assets14(BaseIndividualAsset): +class Assets12(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['markdown'] = 'markdown' requirements: markdown_asset_requirements.MarkdownAssetRequirements | None = None -class Assets15(BaseIndividualAsset): +class Assets13(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['html'] = 'html' requirements: html_asset_requirements.HtmlAssetRequirements | None = None -class Assets16(BaseIndividualAsset): +class Assets14(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['css'] = 'css' requirements: css_asset_requirements.CssAssetRequirements | None = None -class Assets17(BaseIndividualAsset): +class Assets15(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['javascript'] = 'javascript' requirements: javascript_asset_requirements.JavascriptAssetRequirements | None = None -class Assets18(BaseIndividualAsset): +class Assets16(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['zip'] = 'zip' -class Assets19(BaseIndividualAsset): +class Assets17(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['vast'] = 'vast' requirements: vast_asset_requirements.VastAssetRequirements | None = None -class Assets20(BaseIndividualAsset): +class Assets18(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['daast'] = 'daast' requirements: daast_asset_requirements.DaastAssetRequirements | None = None -class Assets21(BaseIndividualAsset): +class Assets19(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['url'] = 'url' requirements: url_asset_requirements.UrlAssetRequirements | None = None -class Assets22(BaseIndividualAsset): +class Assets20(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['webhook'] = 'webhook' requirements: webhook_asset_requirements.WebhookAssetRequirements | None = None -class Assets23(BaseIndividualAsset): +class Assets21(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['brief'] = 'brief' -class Assets27(BaseGroupAsset): +class Assets25(BaseGroupAsset): asset_type: Literal['image'] = 'image' requirements: image_asset_requirements.ImageAssetRequirements | None = None -class Assets28(BaseGroupAsset): +class Assets26(BaseGroupAsset): asset_type: Literal['video'] = 'video' requirements: video_asset_requirements.VideoAssetRequirements | None = None -class Assets29(BaseGroupAsset): +class Assets27(BaseGroupAsset): asset_type: Literal['audio'] = 'audio' requirements: audio_asset_requirements.AudioAssetRequirements | None = None -class Assets30(BaseGroupAsset): +class Assets28(BaseGroupAsset): asset_type: Literal['text'] = 'text' requirements: text_asset_requirements.TextAssetRequirements | None = None -class Assets31(BaseGroupAsset): +class Assets29(BaseGroupAsset): asset_type: Literal['markdown'] = 'markdown' requirements: markdown_asset_requirements.MarkdownAssetRequirements | None = None -class Assets32(BaseGroupAsset): +class Assets30(BaseGroupAsset): asset_type: Literal['html'] = 'html' requirements: html_asset_requirements.HtmlAssetRequirements | None = None -class Assets33(BaseGroupAsset): +class Assets31(BaseGroupAsset): asset_type: Literal['css'] = 'css' requirements: css_asset_requirements.CssAssetRequirements | None = None -class Assets34(BaseGroupAsset): +class Assets32(BaseGroupAsset): asset_type: Literal['javascript'] = 'javascript' requirements: javascript_asset_requirements.JavascriptAssetRequirements | None = None -class Assets35(BaseGroupAsset): +class Assets33(BaseGroupAsset): asset_type: Literal['zip'] = 'zip' -class Assets36(BaseGroupAsset): +class Assets34(BaseGroupAsset): asset_type: Literal['vast'] = 'vast' requirements: vast_asset_requirements.VastAssetRequirements | None = None -class Assets37(BaseGroupAsset): +class Assets35(BaseGroupAsset): asset_type: Literal['daast'] = 'daast' requirements: daast_asset_requirements.DaastAssetRequirements | None = None -class Assets38(BaseGroupAsset): +class Assets36(BaseGroupAsset): asset_type: Literal['url'] = 'url' requirements: url_asset_requirements.UrlAssetRequirements | None = None -class Assets39(BaseGroupAsset): +class Assets37(BaseGroupAsset): asset_type: Literal['webhook'] = 'webhook' requirements: webhook_asset_requirements.WebhookAssetRequirements | None = None -class Assets26( +class Assets24( RootModel[ - Assets27 + Assets25 + | Assets26 + | Assets27 | Assets28 | Assets29 | Assets30 @@ -450,12 +452,12 @@ class Assets26( | Assets35 | Assets36 | Assets37 - | Assets38 - | Assets39 ] ): root: Annotated[ - Assets27 + Assets25 + | Assets26 + | Assets27 | Assets28 | Assets29 | Assets30 @@ -465,9 +467,7 @@ class Assets26( | Assets34 | Assets35 | Assets36 - | Assets37 - | Assets38 - | Assets39, + | Assets37, Field(discriminator='asset_type'), ] def __getattr__(self, name: str) -> Any: @@ -476,7 +476,7 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) -class Assets25(AdCPBaseModel): +class Assets23(AdCPBaseModel): item_type: Annotated[ Literal['repeatable_group'], Field(description='Discriminator indicating this is a repeatable asset group'), @@ -505,11 +505,11 @@ class Assets25(AdCPBaseModel): ), ] = SelectionMode.sequential assets: Annotated[ - list[Assets26], Field(description='Assets within each repetition of this group') + list[Assets24], Field(description='Assets within each repetition of this group') ] -class Assets24(BaseIndividualAsset): +class Assets22(BaseIndividualAsset): item_type: Literal['individual'] = 'individual' asset_type: Literal['catalog'] = 'catalog' requirements: catalog_requirements.CatalogRequirements | None = None @@ -2238,6 +2238,8 @@ class Format(AdCPBaseModel): assets: Annotated[ list[ Assets + | Assets9 + | Assets10 | Assets11 | Assets12 | Assets13 @@ -2251,8 +2253,6 @@ class Format(AdCPBaseModel): | Assets21 | Assets22 | Assets23 - | Assets24 - | Assets25 ] | None, Field( @@ -2346,4 +2346,4 @@ class Format(AdCPBaseModel): # Backward compatibility for the pre-beta3 generated repeatable group name. -Assets94 = Assets25 +Assets94 = Assets23 diff --git a/src/adcp/types/generated_poc/core/geo_place_requirement.py b/src/adcp/types/generated_poc/core/geo_place_requirement.py index 71de6a9ac..431a464cd 100644 --- a/src/adcp/types/generated_poc/core/geo_place_requirement.py +++ b/src/adcp/types/generated_poc/core/geo_place_requirement.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/geo_place_requirement.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -20,7 +20,10 @@ class CatalogRequirement(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - countries: dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], list[geo_place_type.GeographicPlaceType]] + countries: Annotated[ + dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], list[geo_place_type.GeographicPlaceType]], + Field(min_length=1), + ] system_versions: Annotated[ list[SystemVersion] | None, Field( @@ -34,4 +37,7 @@ class GeographicPlaceRequirement(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - systems: dict[geo_place_system.GeographicPlaceIdentifierSystem, CatalogRequirement] + systems: Annotated[ + dict[geo_place_system.GeographicPlaceIdentifierSystem, CatalogRequirement], + Field(min_length=1), + ] diff --git a/src/adcp/types/generated_poc/core/geo_place_support.py b/src/adcp/types/generated_poc/core/geo_place_support.py index bbc3b110b..a61a89725 100644 --- a/src/adcp/types/generated_poc/core/geo_place_support.py +++ b/src/adcp/types/generated_poc/core/geo_place_support.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/geo_place_support.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -19,7 +19,8 @@ class GeographicPlaceSystemSupport(AdCPBaseModel): countries: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], list[geo_place_type.GeographicPlaceType]], Field( - description='Supported place types keyed by ISO 3166-1 alpha-2 country. Only explicitly listed country/type pairs are supported.' + description='Supported place types keyed by ISO 3166-1 alpha-2 country. Only explicitly listed country/type pairs are supported.', + min_length=1, ), ] catalog: geo_place_catalog_capability.GeographicPlaceCatalogCapability diff --git a/src/adcp/types/generated_poc/core/geo_region_requirement.py b/src/adcp/types/generated_poc/core/geo_region_requirement.py index 67d380e28..28d057f27 100644 --- a/src/adcp/types/generated_poc/core/geo_region_requirement.py +++ b/src/adcp/types/generated_poc/core/geo_region_requirement.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/geo_region_requirement.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -45,5 +45,8 @@ class GeographicRegionRequirement(AdCPBaseModel): ) countries: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], Countries | Countries1], - Field(description='Required ISO subdivision support keyed by ISO 3166-1 alpha-2 country.'), + Field( + description='Required ISO subdivision support keyed by ISO 3166-1 alpha-2 country.', + min_length=1, + ), ] diff --git a/src/adcp/types/generated_poc/core/geo_region_support.py b/src/adcp/types/generated_poc/core/geo_region_support.py index 4b855a334..20b347691 100644 --- a/src/adcp/types/generated_poc/core/geo_region_support.py +++ b/src/adcp/types/generated_poc/core/geo_region_support.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/geo_region_support.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -48,7 +48,10 @@ class GeographicRegionSupport(AdCPBaseModel): ) countries: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], Countries | Countries3], - Field(description='Selectable ISO subdivision values keyed by ISO 3166-1 alpha-2 country.'), + Field( + description='Selectable ISO subdivision values keyed by ISO 3166-1 alpha-2 country.', + min_length=1, + ), ] catalog_version: Annotated[ str | None, diff --git a/src/adcp/types/generated_poc/core/media_buy_features.py b/src/adcp/types/generated_poc/core/media_buy_features.py index 03330a772..28ac05209 100644 --- a/src/adcp/types/generated_poc/core/media_buy_features.py +++ b/src/adcp/types/generated_poc/core/media_buy_features.py @@ -1,18 +1,22 @@ # generated by datamodel-codegen: # filename: core/media_buy_features.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:29:24+00:00 from __future__ import annotations -from typing import Annotated +from typing import Annotated, Dict from adcp.types.base import AdCPBaseModel -from pydantic import Field +from pydantic import ConfigDict, Field from . import bidding_policy_capability class MediaBuyFeatures(AdCPBaseModel): + __pydantic_extra__: Dict[str, bool] + model_config = ConfigDict( + extra='allow', + ) inline_creative_management: Annotated[ bool | None, Field( diff --git a/src/adcp/types/generated_poc/core/package_format_snapshot.py b/src/adcp/types/generated_poc/core/package_format_snapshot.py index 9154aa57e..86cd1b33a 100644 --- a/src/adcp/types/generated_poc/core/package_format_snapshot.py +++ b/src/adcp/types/generated_poc/core/package_format_snapshot.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/package_format_snapshot.json -# timestamp: 2026-09-01T19:37:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -88,6 +88,8 @@ class PackageFormatSnapshot17(AdCPBaseModel): pattern='^sha256:[a-f0-9]{64}$', ), ] = None + format_kind: Any + params: Any class PackageFormatSnapshot1(AdCPBaseModel): @@ -1663,7 +1665,7 @@ class PackageFormatSnapshot16(AdCPBaseModel): ] -class PackageFormatSnapshot18(PackageFormatSnapshot1, PackageFormatSnapshot17): +class PackageFormatSnapshot18(PackageFormatSnapshot1): model_config = ConfigDict( extra='allow', ) @@ -1711,7 +1713,7 @@ class PackageFormatSnapshot18(PackageFormatSnapshot1, PackageFormatSnapshot17): params: image.CanonicalFormatImage -class PackageFormatSnapshot19(PackageFormatSnapshot2, PackageFormatSnapshot17): +class PackageFormatSnapshot19(PackageFormatSnapshot2): model_config = ConfigDict( extra='allow', ) @@ -1759,7 +1761,7 @@ class PackageFormatSnapshot19(PackageFormatSnapshot2, PackageFormatSnapshot17): params: html5.CanonicalFormatHtml5Banner -class PackageFormatSnapshot20(PackageFormatSnapshot3, PackageFormatSnapshot17): +class PackageFormatSnapshot20(PackageFormatSnapshot3): model_config = ConfigDict( extra='allow', ) @@ -1807,7 +1809,7 @@ class PackageFormatSnapshot20(PackageFormatSnapshot3, PackageFormatSnapshot17): params: display_tag.CanonicalFormatDisplayTag -class PackageFormatSnapshot21(PackageFormatSnapshot4, PackageFormatSnapshot17): +class PackageFormatSnapshot21(PackageFormatSnapshot4): model_config = ConfigDict( extra='allow', ) @@ -1855,7 +1857,7 @@ class PackageFormatSnapshot21(PackageFormatSnapshot4, PackageFormatSnapshot17): params: image_carousel.CanonicalFormatImageCarousel -class PackageFormatSnapshot22(PackageFormatSnapshot5, PackageFormatSnapshot17): +class PackageFormatSnapshot22(PackageFormatSnapshot5): model_config = ConfigDict( extra='allow', ) @@ -1903,7 +1905,7 @@ class PackageFormatSnapshot22(PackageFormatSnapshot5, PackageFormatSnapshot17): params: video_hosted.CanonicalFormatHostedVideo -class PackageFormatSnapshot23(PackageFormatSnapshot6, PackageFormatSnapshot17): +class PackageFormatSnapshot23(PackageFormatSnapshot6): model_config = ConfigDict( extra='allow', ) @@ -1951,7 +1953,7 @@ class PackageFormatSnapshot23(PackageFormatSnapshot6, PackageFormatSnapshot17): params: video_vast.CanonicalFormatVastVideo -class PackageFormatSnapshot24(PackageFormatSnapshot7, PackageFormatSnapshot17): +class PackageFormatSnapshot24(PackageFormatSnapshot7): model_config = ConfigDict( extra='allow', ) @@ -1999,7 +2001,7 @@ class PackageFormatSnapshot24(PackageFormatSnapshot7, PackageFormatSnapshot17): params: audio_hosted.CanonicalFormatHostedAudio -class PackageFormatSnapshot25(PackageFormatSnapshot8, PackageFormatSnapshot17): +class PackageFormatSnapshot25(PackageFormatSnapshot8): model_config = ConfigDict( extra='allow', ) @@ -2047,7 +2049,7 @@ class PackageFormatSnapshot25(PackageFormatSnapshot8, PackageFormatSnapshot17): params: audio_vast.CanonicalFormatVastAudio -class PackageFormatSnapshot26(PackageFormatSnapshot9, PackageFormatSnapshot17): +class PackageFormatSnapshot26(PackageFormatSnapshot9): model_config = ConfigDict( extra='allow', ) @@ -2095,7 +2097,7 @@ class PackageFormatSnapshot26(PackageFormatSnapshot9, PackageFormatSnapshot17): params: audio_daast.CanonicalFormatDaastAudio -class PackageFormatSnapshot27(PackageFormatSnapshot10, PackageFormatSnapshot17): +class PackageFormatSnapshot27(PackageFormatSnapshot10): model_config = ConfigDict( extra='allow', ) @@ -2143,7 +2145,7 @@ class PackageFormatSnapshot27(PackageFormatSnapshot10, PackageFormatSnapshot17): params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven -class PackageFormatSnapshot28(PackageFormatSnapshot11, PackageFormatSnapshot17): +class PackageFormatSnapshot28(PackageFormatSnapshot11): model_config = ConfigDict( extra='allow', ) @@ -2191,7 +2193,7 @@ class PackageFormatSnapshot28(PackageFormatSnapshot11, PackageFormatSnapshot17): params: native_in_feed.CanonicalFormatNativeInFeed -class PackageFormatSnapshot29(PackageFormatSnapshot12, PackageFormatSnapshot17): +class PackageFormatSnapshot29(PackageFormatSnapshot12): model_config = ConfigDict( extra='allow', ) @@ -2239,7 +2241,7 @@ class PackageFormatSnapshot29(PackageFormatSnapshot12, PackageFormatSnapshot17): params: responsive_creative.CanonicalFormatResponsiveCreative -class PackageFormatSnapshot30(PackageFormatSnapshot13, PackageFormatSnapshot17): +class PackageFormatSnapshot30(PackageFormatSnapshot13): model_config = ConfigDict( extra='allow', ) @@ -2287,7 +2289,7 @@ class PackageFormatSnapshot30(PackageFormatSnapshot13, PackageFormatSnapshot17): params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement -class PackageFormatSnapshot31(PackageFormatSnapshot14, PackageFormatSnapshot17): +class PackageFormatSnapshot31(PackageFormatSnapshot14): model_config = ConfigDict( extra='allow', ) @@ -2335,7 +2337,7 @@ class PackageFormatSnapshot31(PackageFormatSnapshot14, PackageFormatSnapshot17): params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay -class PackageFormatSnapshot32(PackageFormatSnapshot15, PackageFormatSnapshot17): +class PackageFormatSnapshot32(PackageFormatSnapshot15): model_config = ConfigDict( extra='allow', ) @@ -2383,7 +2385,7 @@ class PackageFormatSnapshot32(PackageFormatSnapshot15, PackageFormatSnapshot17): params: coordinated_placements.CanonicalFormatCoordinatedPlacements -class PackageFormatSnapshot33(PackageFormatSnapshot16, PackageFormatSnapshot17): +class PackageFormatSnapshot33(PackageFormatSnapshot16): model_config = ConfigDict( extra='allow', ) @@ -2428,12 +2430,7 @@ class PackageFormatSnapshot33(PackageFormatSnapshot16, PackageFormatSnapshot17): ), ] = None format_kind: Literal['custom'] = 'custom' - params: Annotated[ - dict[str, Any], - Field( - description="Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`." - ), - ] + params: Annotated[dict[str, Any], Field(description="Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`.")] class PackageFormatSnapshot( diff --git a/src/adcp/types/generated_poc/core/postal_area_support.py b/src/adcp/types/generated_poc/core/postal_area_support.py index beae89203..b48fe94dc 100644 --- a/src/adcp/types/generated_poc/core/postal_area_support.py +++ b/src/adcp/types/generated_poc/core/postal_area_support.py @@ -1,14 +1,14 @@ # generated by datamodel-codegen: # filename: core/postal_area_support.json -# timestamp: 2026-06-07T22:46:16+00:00 +# timestamp: 2026-09-05T19:29:24+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Annotated, Literal +from typing import Annotated, Dict, Literal from adcp.types.base import AdCPBaseModel -from pydantic import Field +from pydantic import ConfigDict, Field class ME(StrEnum): @@ -26,7 +26,18 @@ class CAEnum(StrEnum): full = 'full' +class PostalAreaSupportAdditionalPropertyEnum(StrEnum): + postal_code = 'postal_code' + custom = 'custom' + + class PostalAreaSupport(AdCPBaseModel): + __pydantic_extra__: Dict[ + str, list[PostalAreaSupportAdditionalPropertyEnum] + ] + model_config = ConfigDict( + extra='allow', + ) US: Annotated[list[ME] | None, Field(min_length=1)] = None GB: Annotated[list[GBEnum] | None, Field(min_length=1)] = None CA: Annotated[list[CAEnum] | None, Field(min_length=1)] = None diff --git a/src/adcp/types/generated_poc/core/product_audience_evidence_requirements.py b/src/adcp/types/generated_poc/core/product_audience_evidence_requirements.py index a461b9ed2..2bf60994b 100644 --- a/src/adcp/types/generated_poc/core/product_audience_evidence_requirements.py +++ b/src/adcp/types/generated_poc/core/product_audience_evidence_requirements.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/product_audience_evidence_requirements.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -120,5 +120,5 @@ class ProductAudienceEvidenceRequirements(AdCPBaseModel): accepted_attestation_issuers: Annotated[ list[AcceptedAttestationIssuers] | None, Field(min_length=1) ] = None - accepted_attestation_claim_types: list[AnyUrl] | None = None + accepted_attestation_claim_types: Annotated[list[AnyUrl] | None, Field(min_length=1)] = None ext: ext_1.ExtensionObject | None = None diff --git a/src/adcp/types/generated_poc/core/product_change_map.py b/src/adcp/types/generated_poc/core/product_change_map.py index 21d68e52d..c6f2fee66 100644 --- a/src/adcp/types/generated_poc/core/product_change_map.py +++ b/src/adcp/types/generated_poc/core/product_change_map.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/product_change_map.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -20,6 +20,7 @@ class ProductChangeMap(RootModel[dict[Annotated[str, StringConstraints(min_lengt dict[Annotated[str, StringConstraints(min_length=1)], ProductChangeMap1], Field( description='Product IDs mapped to deterministic membership actions. Object keys are product identifiers, so contradictory actions for one product cannot be represented.', + min_length=1, title='Product Change Map', ), ] diff --git a/src/adcp/types/generated_poc/core/product_filters.py b/src/adcp/types/generated_poc/core/product_filters.py index b4bc4d844..89b73d6bb 100644 --- a/src/adcp/types/generated_poc/core/product_filters.py +++ b/src/adcp/types/generated_poc/core/product_filters.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/product_filters.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -583,6 +583,7 @@ class ProductFilters(AdCPBaseModel): Field( deprecated=True, description='Deprecated: Use trusted_match filter instead. Filter to products executable through specific agentic ad exchanges. URLs are canonical identifiers.', + min_length=1, ), ] = None trusted_match: Annotated[ diff --git a/src/adcp/types/generated_poc/core/product_format_declaration.py b/src/adcp/types/generated_poc/core/product_format_declaration.py index 475915bb7..c5f36d0b5 100644 --- a/src/adcp/types/generated_poc/core/product_format_declaration.py +++ b/src/adcp/types/generated_poc/core/product_format_declaration.py @@ -1,16 +1,33 @@ # generated by datamodel-codegen: # filename: core/product_format_declaration.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Annotated +from typing import Annotated, Any, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AnyUrl, Field +from pydantic import AnyUrl, Field, RootModel from ..enums import channels +from ..formats.canonical import ( + agent_placement, + audio_daast, + audio_hosted, + audio_vast, + coordinated_placements, + display_tag, + html5, + image, + image_carousel, + native_in_feed, + responsive_creative, + seller_rendered_stateful_display, + sponsored_placement, + video_hosted, + video_vast, +) from . import creative_locale_policy, format_id, macro_resolution_capability, platform_extension_ref from . import tracker_execution_contract as tracker_execution_contract_1 @@ -21,7 +38,7 @@ class SellerPreference(StrEnum): discouraged = 'discouraged' -class ProductFormatDeclaration(AdCPBaseModel): +class ProductFormatDeclaration1(AdCPBaseModel): format_option_id: Annotated[ str | None, Field( @@ -115,3 +132,1644 @@ class ProductFormatDeclaration(AdCPBaseModel): description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' ), ] = None + format_kind: Literal['image'] = 'image' + params: image.CanonicalFormatImage + + +class ProductFormatDeclaration2(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['html5'] = 'html5' + params: html5.CanonicalFormatHtml5Banner + + +class ProductFormatDeclaration3(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['display_tag'] = 'display_tag' + params: display_tag.CanonicalFormatDisplayTag + + +class ProductFormatDeclaration4(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['image_carousel'] = 'image_carousel' + params: image_carousel.CanonicalFormatImageCarousel + + +class ProductFormatDeclaration5(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['video_hosted'] = 'video_hosted' + params: video_hosted.CanonicalFormatHostedVideo + + +class ProductFormatDeclaration6(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['video_vast'] = 'video_vast' + params: video_vast.CanonicalFormatVastVideo + + +class ProductFormatDeclaration7(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['audio_hosted'] = 'audio_hosted' + params: audio_hosted.CanonicalFormatHostedAudio + + +class ProductFormatDeclaration8(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['audio_vast'] = 'audio_vast' + params: audio_vast.CanonicalFormatVastAudio + + +class ProductFormatDeclaration9(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['audio_daast'] = 'audio_daast' + params: audio_daast.CanonicalFormatDaastAudio + + +class ProductFormatDeclaration10(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['sponsored_placement'] = 'sponsored_placement' + params: sponsored_placement.CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven + + +class ProductFormatDeclaration11(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['native_in_feed'] = 'native_in_feed' + params: native_in_feed.CanonicalFormatNativeInFeed + + +class ProductFormatDeclaration12(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['responsive_creative'] = 'responsive_creative' + params: responsive_creative.CanonicalFormatResponsiveCreative + + +class ProductFormatDeclaration13(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['agent_placement'] = 'agent_placement' + params: agent_placement.CanonicalFormatAgentPlacementAiSurfaceSponsoredPlacement + + +class ProductFormatDeclaration14(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['seller_rendered_stateful_display'] = 'seller_rendered_stateful_display' + params: seller_rendered_stateful_display.CanonicalFormatSellerRenderedStatefulDisplay + + +class ProductFormatDeclaration15(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['coordinated_placements'] = 'coordinated_placements' + params: coordinated_placements.CanonicalFormatCoordinatedPlacements + + +class ProductFormatDeclaration16(AdCPBaseModel): + format_option_id: Annotated[ + str | None, + Field( + description="Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + ), + ] = None + publisher_domain: Annotated[ + str | None, + Field( + description="Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.", + pattern='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', + ), + ] = None + tracker_execution_contract: Annotated[ + tracker_execution_contract_1.TrackerExecutionContract | None, + Field( + description='Seller- or publisher-authored commitment describing which first-class manifest trackers the selected format option accepts and initiates in production. The seller-returned Product declaration is binding; publisher and placement declarations are upstream inputs that the seller resolves into that effective contract. Presence requires a stable format_option_id. Creative-agent capability projections, transformer inputs, and deprecated canonical_parameters MUST reject this seller-authority field rather than copying it.' + ), + ] = None + macro_resolution_capabilities: Annotated[ + list[macro_resolution_capability.MacroProcessingCapability] | None, + Field( + description='Binding format-option processing capabilities for exact macro dialect identities, semantics, operations, actors, contexts, and encodings. Absence means undeclared, not supported on the opt-in declared-token path. Seller-wide capabilities are only a ceiling. This field does not claim that a buyer tracker asset is honored or fired.', + min_length=1, + ), + ] = None + technical_requirements_complete: Annotated[ + bool | None, + Field( + description='Completeness assertion for technical creative acceptance constraints in this declaration. When true, the declaring party asserts that every technical constraint within its authority is expressed by this declaration (including fetched custom-format and platform-extension schemas), and every omitted technical field means no constraint at that layer. A creative that satisfies the complete effective technical contract MUST NOT later be rejected for an undisclosed technical constraint. When false or absent, omitted technical constraints are undeclared: consumers MUST NOT interpret omission as support, no constraint, or a prose/default value. The effective product/placement contract is complete only when every applicable product, publisher, and placement declaration asserts true. This assertion is limited to technical acceptance; it does not waive policy, legal, security, malware, transport/fetch, corrupted-content, or materially misdeclared-asset checks. Creative size fields ending in `_kb` use exactly 1,000 bytes per KB and fields ending in `_mb` use exactly 1,000,000 bytes per MB.' + ), + ] = None + display_name: Annotated[ + str | None, + Field( + description="Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn." + ), + ] = None + sample_render_url: Annotated[ + AnyUrl | None, + Field( + description='Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.' + ), + ] = None + applies_to_channels: Annotated[ + list[channels.MediaChannel] | None, + Field( + description="Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`." + ), + ] = None + seller_preference: Annotated[ + SellerPreference | None, + Field( + description="Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it." + ), + ] = None + locale_policy: Annotated[ + creative_locale_policy.CreativeLocalePolicy | None, + Field( + description='Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.' + ), + ] = None + canonical_formats_only: Annotated[ + bool | None, + Field( + description='When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: "custom"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.' + ), + ] = False + experimental: Annotated[ + bool | None, + Field( + description="When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum." + ), + ] = False + format_shape: Annotated[ + str | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn\'t yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.' + ), + ] = None + v1_format_ref: Annotated[ + list[format_id.FormatReferenceStructuredObject] | None, + Field( + description="Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.", + min_length=1, + ), + ] = None + format_schema: Annotated[ + platform_extension_ref.PlatformExtensionReference | None, + Field( + description='REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape\'s actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that\'s why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`\'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.local`, `.localhost`, `.internal`, `.test`, `.example`, `.invalid`). Cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, `kubernetes.default.svc`) are explicitly forbidden — these are credential-leak primitives. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK\'s discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document\'s parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a \'valid\' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.' + ), + ] = None + format_kind: Literal['custom'] = 'custom' + params: Annotated[ + dict[str, Any], + Field( + description="Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`." + ), + ] + + +class ProductFormatDeclaration( + RootModel[ + ProductFormatDeclaration1 + | ProductFormatDeclaration2 + | ProductFormatDeclaration3 + | ProductFormatDeclaration4 + | ProductFormatDeclaration5 + | ProductFormatDeclaration6 + | ProductFormatDeclaration7 + | ProductFormatDeclaration8 + | ProductFormatDeclaration9 + | ProductFormatDeclaration10 + | ProductFormatDeclaration11 + | ProductFormatDeclaration12 + | ProductFormatDeclaration13 + | ProductFormatDeclaration14 + | ProductFormatDeclaration15 + | ProductFormatDeclaration16 + ] +): + root: Annotated[ + ProductFormatDeclaration1 + | ProductFormatDeclaration2 + | ProductFormatDeclaration3 + | ProductFormatDeclaration4 + | ProductFormatDeclaration5 + | ProductFormatDeclaration6 + | ProductFormatDeclaration7 + | ProductFormatDeclaration8 + | ProductFormatDeclaration9 + | ProductFormatDeclaration10 + | ProductFormatDeclaration11 + | ProductFormatDeclaration12 + | ProductFormatDeclaration13 + | ProductFormatDeclaration14 + | ProductFormatDeclaration15 + | ProductFormatDeclaration16, + Field( + description='Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries only the canonical creative-shape parameters (slots, dimensions, durations, codecs, character limits, platform_extensions, etc.). Processing metadata such as `macro_resolution_capabilities` belongs at this declaration\'s top level rather than inside `params`. Optional `format_option_id` (stable identifier for routing when a product\'s `format_options` contains multiple declarations sharing the same `format_kind`), optional `publisher_domain` (namespace for the format option when it comes from a publisher adagents.json catalog), `display_name` (seller-controlled human-readable label for dashboard and catalog UIs), and `applies_to_channels` (subset of the product\'s declared channels this declaration applies to — lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1\'s named-format pattern (where products referenced a separately-defined format file via compound `format_id`). Canonical product-bound declarations are the AdCP 3.2 authoring path; v1 named formats remain only as deprecated 3.x compatibility projections.\n\n**Closed-set semantics (normative).** `format_options[]` is the closed set of accepted formats for this product. Sellers MUST reject `create_media_buy` requests targeting any `format_kind` (or format option reference) not present in this list — typically with `UNSUPPORTED_FEATURE` or a seller-specific code; the rejection is structural, not negotiable. `seller_preference` modulates *within* the accepted set (a soft ranking hint between equally-acceptable options), it is NOT an enforcement axis. A product wanting to say \'this format is the only one that works\' lists exactly that one entry in `format_options[]`; everything else falls outside the set and is rejected by the closed-set rule.\n\n**Format matching vs satisfaction (normative).** Legacy named formats MUST be normalized to canonical declarations before comparison; do not exact-match raw `(agent_url, id)` pairs once a `format_id` has been projected through `canonical`, `v1_format_ref`, or the canonical mapping registry. Equivalence matching can treat a legacy fixed-size display ID and `format_kind: "image"` with matching `width`/`height` as the same underlying shape. Product satisfaction is stricter and directional: when this declaration specifies fixed constraints such as `width`, `height`, `duration_ms_exact`, or `duration_ms_range`, a buyer request or creative manifest MUST declare and satisfy those constraints. A broad request with no dimensions or duration does not satisfy a fixed-size or fixed-duration product; a broad product MAY accept a more specific creative unless another product constraint excludes it. Duration precedence is `duration_ms_exact` > `duration_ms_range`. Range constraints use containment: a range-based request satisfies this declaration only when every value it permits falls within this declaration\'s accepted range; overlap alone is insufficient. An exact value satisfies a range when the exact value falls inside the accepted interval. For hosted audio/video, a null range endpoint is unbounded: [null, 60000] means up to 60s, and [15000, null] means at least 15s; [null, null] is invalid because at least one endpoint must be bounded.\n\n**Custom format_kind** (`format_kind: "custom"`): for adopter-defined shapes that don\'t fit the 15 canonicals (branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue.', + discriminator='format_kind', + examples=[ + { + 'description': 'Meta Reels — narrows video_hosted (vertical orientation)', + 'data': { + 'format_kind': 'video_hosted', + 'params': { + 'orientation': 'vertical', + 'aspect_ratio': '9:16', + 'duration_ms_range': [3000, 90000], + 'min_width': 1080, + 'min_height': 1920, + 'max_file_size_mb': 200, + 'video_codecs': ['h264'], + 'audio_codecs': ['aac'], + 'headline_max_chars': 25, + 'primary_text_max_chars': 72, + 'captions': 'recommended', + 'cta_values': ['LEARN_MORE', 'SHOP_NOW', 'DOWNLOAD', 'SIGN_UP'], + 'composition_model': 'deterministic', + 'platform_extensions': [ + { + 'uri': 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel', + 'digest': 'sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2', + } + ], + }, + }, + }, + { + 'description': 'IAB Medium Rectangle (300x250) — narrows image', + 'data': { + 'format_kind': 'image', + 'params': { + 'width': 300, + 'height': 250, + 'max_file_size_kb': 200, + 'image_formats': ['jpg', 'png', 'gif'], + 'ssl_required': True, + 'composition_model': 'deterministic', + 'cta_values': ['LEARN_MORE', 'SHOP_NOW', 'GET_OFFER'], + }, + }, + }, + { + 'description': 'Québec French-only display image — same image shape with a seller-enforced creative locale constraint', + 'data': { + 'format_kind': 'image', + 'format_option_id': 'quebec_display_image', + 'display_name': 'Québec display image — French creative', + 'canonical_formats_only': True, + 'locale_policy': {'accepted_language_ranges': ['fr']}, + 'params': { + 'width': 300, + 'height': 250, + 'max_file_size_kb': 200, + 'image_formats': ['jpg', 'png'], + }, + }, + }, + { + 'description': "Podcast 30s host-read — narrows audio_hosted with a `script` slot the seller's host reads verbatim. No separate `inputs` map; the script lives in the manifest's `assets` like any other text asset.", + 'data': { + 'format_kind': 'audio_hosted', + 'params': { + 'duration_ms_exact': 30000, + 'audio_codecs': ['mp3', 'aac'], + 'audio_sample_rates': [44100, 48000], + 'audio_channels': ['stereo'], + 'loudness_lufs': -16, + 'asset_source': 'publisher_host_recorded', + 'buyer_asset_acceptance': 'rejected', + 'composition_model': 'deterministic', + 'slots': [ + { + 'asset_group_id': 'script', + 'required': True, + 'asset_type': 'text', + 'max_chars': 800, + }, + { + 'asset_group_id': 'offering_ref', + 'required': False, + 'asset_type': 'text', + }, + ], + 'production_window_business_days': 7, + }, + }, + }, + { + 'description': "StreamHaus Branded Feature — a still-custom branded-content format with format_schema pointing at the fictional seller's hosted schema. Buyer agents fetch the schema by uri@digest (cached, immutable) and validate the manifest structurally. `canonical_formats_only: true` is required for custom declarations because no v1 named format expresses this bespoke composition.", + 'data': { + 'format_kind': 'custom', + 'canonical_formats_only': True, + 'format_shape': 'branded_content', + 'format_schema': { + 'uri': 'https://ads.streamhaus.example/schemas/formats/branded_feature_v1', + 'digest': 'sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3', + }, + 'format_option_id': 'streamhaus_branded_feature', + 'display_name': 'StreamHaus branded feature', + 'applies_to_channels': ['display'], + 'params': { + 'components': [ + {'placement_type': 'feature_header', 'required': True}, + {'placement_type': 'sponsored_story_card', 'required': True}, + {'placement_type': 'sponsorship_lockup', 'required': True}, + ], + 'exclusivity_window_hours': 12, + 'ssl_required': True, + }, + }, + }, + ], + title='Product Format Declaration', + ), + ] + def __getattr__(self, name: str) -> Any: + """Proxy attribute access to the wrapped type.""" + if name.startswith('_'): + raise AttributeError(name) + return getattr(self.root, name) diff --git a/src/adcp/types/generated_poc/core/product_signal_targeting_option.py b/src/adcp/types/generated_poc/core/product_signal_targeting_option.py index 878f420d2..c7d5a0fa1 100644 --- a/src/adcp/types/generated_poc/core/product_signal_targeting_option.py +++ b/src/adcp/types/generated_poc/core/product_signal_targeting_option.py @@ -1,15 +1,15 @@ # generated by datamodel-codegen: # filename: core/product_signal_targeting_option.json -# timestamp: 2026-06-04T19:44:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Annotated +from typing import Annotated, Any from pydantic import ConfigDict, Field -from . import signal_ref, vendor_pricing_option +from . import vendor_pricing_option from .signal_listing import SignalListing @@ -65,9 +65,4 @@ class ProductSignalTargetingOption(SignalListing): min_length=1, ), ] = None - signal_ref: Annotated[ - signal_ref.SignalRef, - Field( - description="Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined in a data provider's published adagents.json signals[]; use scope 'signal_source' with signal_source_url for a source-native signal." - ), - ] + signal_ref: Any diff --git a/src/adcp/types/generated_poc/core/reporting_file_entry.py b/src/adcp/types/generated_poc/core/reporting_file_entry.py index b94017b3d..5647ef2c8 100644 --- a/src/adcp/types/generated_poc/core/reporting_file_entry.py +++ b/src/adcp/types/generated_poc/core/reporting_file_entry.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/reporting_file_entry.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -21,4 +21,4 @@ class ReportingFileEntry(AdCPBaseModel): size_bytes: Annotated[int, Field(ge=0)] sha256: Annotated[str, Field(pattern='^[A-Fa-f0-9]{64}$')] row_count: Annotated[int, Field(ge=0)] - partition: dict[str, str] | None = None + partition: Annotated[dict[str, str] | None, Field(max_length=32)] = None diff --git a/src/adcp/types/generated_poc/core/signal_coverage_forecast.py b/src/adcp/types/generated_poc/core/signal_coverage_forecast.py index 0e5d891fb..92660e82b 100644 --- a/src/adcp/types/generated_poc/core/signal_coverage_forecast.py +++ b/src/adcp/types/generated_poc/core/signal_coverage_forecast.py @@ -1,11 +1,11 @@ # generated by datamodel-codegen: # filename: core/signal_coverage_forecast.json -# timestamp: 2026-08-21T04:35:18+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Annotated, Literal +from typing import Annotated, Dict, Literal from adcp.types.base import AdCPBaseModel from pydantic import AwareDatetime, ConfigDict, Field, RootModel @@ -29,6 +29,10 @@ class CoverageRate(forecast_point.CoverageRate): class Metrics(forecast_point.Metrics): + __pydantic_extra__: Dict[str, forecast_range.ForecastRange] + model_config = ConfigDict( + extra='allow', + ) audience_size: forecast_range.ForecastRange | None = None reach: forecast_range.ForecastRange | None = None frequency: forecast_range.ForecastRange | None = None @@ -53,6 +57,8 @@ class Metrics(forecast_point.Metrics): ] + + class Kind(StrEnum): inventory = 'inventory' product = 'product' diff --git a/src/adcp/types/generated_poc/core/targeting_overlay_support.py b/src/adcp/types/generated_poc/core/targeting_overlay_support.py index 4ea303492..e23963e27 100644 --- a/src/adcp/types/generated_poc/core/targeting_overlay_support.py +++ b/src/adcp/types/generated_poc/core/targeting_overlay_support.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/targeting_overlay_support.json -# timestamp: 2026-09-03T18:44:01+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -87,7 +87,10 @@ class PlaceCatalogSupport(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - countries: dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], list[geo_place_type.GeographicPlaceType]] + countries: Annotated[ + dict[Annotated[str, StringConstraints(pattern=r'^[A-Z]{2}$')], list[geo_place_type.GeographicPlaceType]], + Field(min_length=1), + ] current_version: Annotated[ str, Field( @@ -108,7 +111,10 @@ class PlaceSupport(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) - systems: dict[geo_place_system.GeographicPlaceIdentifierSystem, PlaceCatalogSupport] + systems: Annotated[ + dict[geo_place_system.GeographicPlaceIdentifierSystem, PlaceCatalogSupport], + Field(min_length=1), + ] max_values_per_package: Annotated[int | None, Field(ge=1)] = None max_packages: Annotated[ int | None, diff --git a/src/adcp/types/generated_poc/core/transformer.py b/src/adcp/types/generated_poc/core/transformer.py index e1c10df58..fdaa52290 100644 --- a/src/adcp/types/generated_poc/core/transformer.py +++ b/src/adcp/types/generated_poc/core/transformer.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: core/transformer.json -# timestamp: 2026-09-01T19:37:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -105,18 +105,6 @@ class Multiplicity(AdCPBaseModel): ] = None -class VoiceSynthesisRefItem1(VoiceSynthesisRefItem): - pass - - -class InputFormat3417(InputFormat17): - pass - - -class Multiplicity3(Multiplicity): - pass - - class InputFormat1(AdCPBaseModel): format_option_id: Annotated[ str | None, @@ -1798,7 +1786,7 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) -class Transformer1(AdCPBaseModel): +class Transformer(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) @@ -1853,261 +1841,6 @@ class Transformer1(AdCPBaseModel): min_length=1, ), ] = None - output_capability_ids: Annotated[ - list[OutputCapabilityId], - Field( - description="Canonical output capabilities this transformer can produce. Every value MUST match this agent's get_adcp_capabilities `creative.supported_formats[].capability_id`. A build_creative request's target_capability_id(s) MUST be a subset of this array.", - min_length=1, - ), - ] - params: Annotated[ - list[transformer_param.TransformerParam] | None, - Field( - description="Configuration knobs this transformer exposes. The buyer supplies values in build_creative `config`, keyed by each param's `field`. Enumerable param values (e.g. account-specific voices) are returned only when requested via list_transformers `expand_params`." - ), - ] = None - pricing_options: Annotated[ - list[vendor_pricing_option.VendorPricingOption] | None, - Field( - description='Per-account rate-card options for using this transformer. Present when the list_transformers request set include_pricing=true with an account. The applied option is echoed back per-leaf on the build_creative response and reconciled via report_usage.', - min_length=1, - ), - ] = None - multiplicity: Annotated[ - Multiplicity | None, - Field( - description="Optional per-transformer fan-out limits that NARROW the agent-level get_adcp_capabilities `creative.multiplicity`. Same shape as the agent-level object. When present, this transformer's authoritative; its ceilings (max_creatives_limit / max_variants_limit) MUST NOT exceed the agent ceilings, and its variant_dimensions MUST be a subset of the agent's. Omit to inherit the agent-level capability unchanged." - ), - ] = None - - -class InputFormat341(InputFormat1): - pass - - -class InputFormat342(InputFormat2): - pass - - -class InputFormat343(InputFormat3): - pass - - -class InputFormat344(InputFormat4): - pass - - -class InputFormat345(InputFormat5): - pass - - -class InputFormat346(InputFormat6): - pass - - -class InputFormat347(InputFormat7): - pass - - -class InputFormat348(InputFormat8): - pass - - -class InputFormat349(InputFormat9): - pass - - -class InputFormat3410(InputFormat10): - pass - - -class InputFormat3411(InputFormat11): - pass - - -class InputFormat3412(InputFormat12): - pass - - -class InputFormat3413(InputFormat13): - pass - - -class InputFormat3414(InputFormat14): - pass - - -class InputFormat3415(InputFormat15): - pass - - -class InputFormat3416(InputFormat16): - pass - - -class InputFormat3418(InputFormat341, InputFormat3417): - pass - - -class InputFormat3419(InputFormat342, InputFormat3417): - pass - - -class InputFormat3420(InputFormat343, InputFormat3417): - pass - - -class InputFormat3421(InputFormat344, InputFormat3417): - pass - - -class InputFormat3422(InputFormat345, InputFormat3417): - pass - - -class InputFormat3423(InputFormat346, InputFormat3417): - pass - - -class InputFormat3424(InputFormat347, InputFormat3417): - pass - - -class InputFormat3425(InputFormat348, InputFormat3417): - pass - - -class InputFormat3426(InputFormat349, InputFormat3417): - pass - - -class InputFormat3427(InputFormat3410, InputFormat3417): - pass - - -class InputFormat3428(InputFormat3411, InputFormat3417): - pass - - -class InputFormat3429(InputFormat3412, InputFormat3417): - pass - - -class InputFormat3430(InputFormat3413, InputFormat3417): - pass - - -class InputFormat3431(InputFormat3414, InputFormat3417): - pass - - -class InputFormat3432(InputFormat3415, InputFormat3417): - pass - - -class InputFormat3433(InputFormat3416, InputFormat3417): - pass - - -class InputFormat34( - RootModel[ - InputFormat3418 - | InputFormat3419 - | InputFormat3420 - | InputFormat3421 - | InputFormat3422 - | InputFormat3423 - | InputFormat3424 - | InputFormat3425 - | InputFormat3426 - | InputFormat3427 - | InputFormat3428 - | InputFormat3429 - | InputFormat3430 - | InputFormat3431 - | InputFormat3432 - | InputFormat3433 - ] -): - root: ( - InputFormat3418 - | InputFormat3419 - | InputFormat3420 - | InputFormat3421 - | InputFormat3422 - | InputFormat3423 - | InputFormat3424 - | InputFormat3425 - | InputFormat3426 - | InputFormat3427 - | InputFormat3428 - | InputFormat3429 - | InputFormat3430 - | InputFormat3431 - | InputFormat3432 - | InputFormat3433 - ) - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the wrapped type.""" - if name.startswith('_'): - raise AttributeError(name) - return getattr(self.root, name) - -class Transformer2(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - transformer_id: Annotated[ - str, - Field( - description='Stable identifier for this transformer within the agent. Pass to build_creative `transformer_id` to select it.' - ), - ] - name: Annotated[ - str, - Field( - description="Human-readable transformer name (e.g. 'Voiceover — Isaac', 'Veo 3 text-to-video')." - ), - ] - description: Annotated[ - str | None, - Field(description='Plain-text explanation of what this transformer produces and how.'), - ] = None - metadata: Annotated[ - dict[str, Any] | None, - Field( - description='Transformer-specific attributes a buyer can filter or display (e.g. provider, modality, language).' - ), - ] = None - voice_synthesis_ref: Annotated[ - list[VoiceSynthesisRefItem1] | None, - Field( - description='Optional discovery/audit anchors for voice transformers provisioned from brand-agent voice_synthesis entries. Informational only: these references help buyers match a discovered transformer to brand/rights-agent provenance, but they do not assert build-time authorization or require the creative agent to perform rights-token validation.', - min_length=1, - ), - ] = None - input_format_ids: Annotated[ - list[format_id.FormatReferenceStructuredObject] | None, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named formats this transformer accepts as input. Use input_formats with canonical declarations.', - ), - ] = None - output_format_ids: Annotated[ - list[format_id.FormatReferenceStructuredObject], - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named formats this transformer can produce. Use output_capability_ids.', - min_length=1, - ), - ] - input_formats: Annotated[ - list[InputFormat34] | None, - Field( - description='Canonical format declarations this transformer accepts as input. Omitted means it builds from a brief or raw assets rather than transforming an existing creative. Compatibility uses canonical constraint satisfaction, not identifier equality. Transformer self-description has no seller production authority, so tracker_execution_contract and tracker_execution_contract_digest are forbidden.', - min_length=1, - ), - ] = None output_capability_ids: Annotated[ list[OutputCapabilityId] | None, Field( @@ -2129,75 +1862,8 @@ class Transformer2(AdCPBaseModel): ), ] = None multiplicity: Annotated[ - Multiplicity3 | None, + Multiplicity | None, Field( description="Optional per-transformer fan-out limits that NARROW the agent-level get_adcp_capabilities `creative.multiplicity`. Same shape as the agent-level object. When present, this transformer's authoritative; its ceilings (max_creatives_limit / max_variants_limit) MUST NOT exceed the agent ceilings, and its variant_dimensions MUST be a subset of the agent's. Omit to inherit the agent-level capability unchanged." ), ] = None - - -class Transformer(RootModel[Transformer1 | Transformer2]): - root: Annotated[ - Transformer1 | Transformer2, - Field( - description="An agent-offered, account-scoped, selectable unit of creative build capability — the creative analog of a media-buy product. A transformer maps input formats to output formats and exposes typed configuration `params[]`. Discovered via list_transformers and selected by `transformer_id` in build_creative. The agent chooses granularity: a distinct voice or model may be its own transformer, or a single transformer may expose `voice`/`model` as an enumerable param. Whether something is a distinct transformer vs. a param is the agent's call — surface separately-priced or capability-distinct alternatives as transformers; keep scalar dials as params.", - examples=[ - { - 'transformer_id': 'audiostack_voiceover', - 'name': 'Voiceover', - 'description': 'Script-to-audio voiceover with account-configured voices.', - 'metadata': {'provider': 'audiostack', 'modality': 'audio'}, - 'voice_synthesis_ref': [ - { - 'brand_agent': { - 'url': 'https://brand.novabrands.example/mcp', - 'id': 'nova_brand_agent', - }, - 'voice_id': 'sara', - 'rights_id': 'rgt_sara_voice_2026', - } - ], - 'output_capability_ids': ['audio_vo'], - 'params': [ - { - 'field': 'voice', - 'type': 'string', - 'value_source': 'enumerable', - 'default': 'sara', - 'description': 'Narration voice, incl. custom voices.', - }, - { - 'field': 'mastering_preset', - 'type': 'string', - 'value_source': 'inline', - 'allowed_values': ['broadcast', 'podcast', 'music'], - 'default': 'broadcast', - }, - { - 'field': 'speaking_rate', - 'type': 'number', - 'value_source': 'range', - 'minimum': 0.5, - 'maximum': 2, - 'default': 1, - }, - ], - 'pricing_options': [ - { - 'pricing_option_id': 'vo_per_second_standard', - 'model': 'per_unit', - 'unit': 'second', - 'unit_price': 0.05, - 'currency': 'USD', - } - ], - } - ], - title='Transformer', - ), - ] - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the wrapped type.""" - if name.startswith('_'): - raise AttributeError(name) - return getattr(self.root, name) diff --git a/src/adcp/types/generated_poc/creative/list_creatives_response.py b/src/adcp/types/generated_poc/creative/list_creatives_response.py index bd3bb43c3..180c7b409 100644 --- a/src/adcp/types/generated_poc/creative/list_creatives_response.py +++ b/src/adcp/types/generated_poc/creative/list_creatives_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/list_creatives_response.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -151,10 +151,6 @@ class Purge(AdCPBaseModel): ] -class Indicator4(Indicator): - pass - - class StatusSummary(AdCPBaseModel): model_config = ConfigDict( extra='allow', @@ -289,106 +285,11 @@ class Assignments(AdCPBaseModel): ] = None -class AssignedPackage1(IndicatorBearingResourceState): - model_config = ConfigDict( - extra='allow', - ) - indicator_types_evaluated: Annotated[ - list[IndicatorTypesEvaluatedEnum] | None, - Field( - description='Indicator types covered by this snapshot. Required whenever indicators is present. Types omitted from this list remain unknown even when indicators is empty. Every returned indicator.type MUST appear in this list.', - min_length=1, - ), - ] = None - indicators: Annotated[ - list[Indicator4] | None, - Field( - description='Current seller assertions for the indicator types and publisher/placement coverage named by the sibling evaluation fields. Omitted means unknown or not evaluated. A present empty array means evaluated with no current assertion for indicator_types_evaluated in the evaluated scope.' - ), - ] = None - package_id: Annotated[str, Field(description='Package identifier')] - media_buy_id: Annotated[ - str | None, - Field( - description='Media buy containing this package. A seller advertising list_creatives in media_buy.relationship_notifications.projection_tasks MUST include this field on every assignment row, including rows where indicators is omitted as unknown, so buyers can key and reread the relationship unambiguously when package IDs are reused across media buys.' - ), - ] = None - assigned_date: Annotated[AwareDatetime, Field(description='When this assignment was created')] - approval_status: Annotated[ - creative_approval_status.CreativeApprovalStatus | None, - Field( - description='Aggregate approval state for this creative in this package assignment. This mirrors the same relationship in get_media_buys. Sellers advertising list_creatives as an indicator projection task MUST include it; partially_approved requires approval_scopes.' - ), - ] = None - rejection_reason: Annotated[ - str | None, - Field( - description='Human-readable explanation when approval_status is rejected. Mirrors get_media_buys for the same relationship.' - ), - ] = None - approval_scopes: Annotated[ - list[creative_approval_scope.ScopedCreativeApproval] | None, - Field( - description='Complete, disjoint publisher/placement approval partition when approval_status is partially_approved. A normalized scope appears once. For one publisher, use either one publisher-wide row or placement-specific rows, never both. Omit when one approval_status applies uniformly to the whole assignment. The same scoped outcomes are mirrored on get_media_buys.', - min_length=2, - ), - ] = None - indicators_as_of: Annotated[ - AwareDatetime | None, - Field( - description='When the seller last completed the evaluation represented by indicators for this relationship. Required whenever indicators is present, including an empty array.' - ), - ] = None - indicators_evaluated_scope: Annotated[ - list[indicator_scope.IndicatorScope] | None, - Field( - description='Optional publisher or placement scopes covered by this evaluation. Omit when indicators covers the whole package–creative assignment. When present, scopes not listed remain unknown; every returned indicator.scope entry MUST be contained by this set.', - min_length=1, - ), - ] = None - - -class Assignments1(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - assignment_count: Annotated[ - int, Field(description='Total number of active package assignments', ge=0) - ] - returned_assignment_count: Annotated[ - int | None, - Field( - description='Number of rows present in assigned_packages for this response.', - ge=0, - le=200, - ), - ] = None - matching_assignment_count: Annotated[ - int | None, - Field( - description='Total active assignments matching filters.indicator_types. MUST be present exactly when assignment_projection was matching. May exceed returned_assignment_count.', - ge=0, - ), - ] = None - assignments_truncated: Annotated[ - bool | None, - Field( - description='True exactly when more qualifying assignments exist than were returned under assignment_limit. Qualifying means assignment_count for the all projection and matching_assignment_count for the matching projection. Buyers needing complete state repair through get_media_buys.' - ), - ] = None - assigned_packages: Annotated[ - list[AssignedPackage1] | None, - Field( - description='Bounded package assignment projection. Under assignment_projection: matching, contains only assignments carrying a requested indicator type; otherwise contains active assignments up to assignment_limit. The response ceiling is enforced via verifier_constraints rather than maxItems, so payloads from 3.1 sellers remain schema-valid.' - ), - ] = None - - class Assets(RootModel[list[asset_union.AssetVariant]]): root: Annotated[list[asset_union.AssetVariant], Field(min_length=1)] -class Creatives(AdCPBaseModel): +class Creative(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) @@ -410,12 +311,12 @@ class Creatives(AdCPBaseModel): ] = None name: Annotated[str, Field(description='Human-readable creative name')] format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject, + format_id_1.FormatReferenceStructuredObject | None, Field( deprecated=True, description='**DEPRECATED in 3.2.** Legacy named-format path. New listed creatives use `format_kind` and optional `format_option_ref`.', ), - ] + ] = None format_kind: Annotated[ canonical_format_kind.CanonicalFormatKind | None, Field( @@ -532,160 +433,14 @@ class Creatives(AdCPBaseModel): @model_validator(mode='after') - def _reject_canonical_format_ref(self) -> Creatives: - if self.format_kind is not None: - raise ValueError('format_id and format_kind are mutually exclusive') + def _validate_format_reference_xor(self) -> Creative: + if (self.format_id is None) == (self.format_kind is None): + raise ValueError('exactly one of format_id and format_kind is required') return self -class Creatives1(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[str, Field(description='Unique identifier for the creative')] - revision_id: Annotated[ - creative_revision_id.CreativeRevisionId | None, - Field( - description='Current buyer-authored input revision for this creative. Present when the current effective content has revision identity and the seller advertises creative.supports_revisions; omitted after an accepted content-bearing legacy update without revision_id.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Exact source-set and selected-output lineage retained when the current stored creative was resolved from a CreativeRepresentationSet.' - ), - ] = None - account: Annotated[ - account_1.Account | None, Field(description='Account that owns this creative') - ] = None - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject | None, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path. New listed creatives use `format_kind` and optional `format_option_ref`.', - ), - ] = None - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description='Canonical 3.2 path. The canonical format kind this creative targets. Mutually exclusive with deprecated `format_id`.' - ), - ] - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='Optional reference to the concrete canonical format option this creative targets. Required when `format_kind` alone is ambiguous in the enclosing product context.' - ), - ] = None - status: Annotated[ - creative_status.CreativeStatus, Field(description='Current approval status of the creative') - ] - created_date: Annotated[AwareDatetime, Field(description='When the creative was created')] - updated_date: Annotated[AwareDatetime, Field(description='When the creative was last modified')] - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets] | None, - Field( - description='Assets for this creative, keyed by asset_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema.' - ), - ] = None - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Preserved component-addressed asset maps for `coordinated_placements`, keyed by coordinated component ID.' - ), - ] = None - localization: Annotated[ - creative_localization_readback.CreativeLocalizationReadback | None, - Field( - description='Authoritative exact materialized locale-variant state. Present for localized creatives when complete. The enclosing creative status is the single review lifecycle for all variants.' - ), - ] = None - localization_unavailable: Annotated[ - LocalizationUnavailable | None, - Field( - description='Per-creative fail-closed state returned instead of localization when the seller knows the creative is localized but cannot construct complete exact readback. The creative remains in this page and counts toward query_summary.returned and pagination; buyers may continue using the base creative fields but MUST NOT infer locale eligibility.' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description="Exact rights constraints retained from the buyer's creative submission. Presence is presentation readback, not proof that the seller accepted or verified the rights.", - min_length=1, - ), - ] = None - rights_attestation_evaluations: Annotated[ - list[rights_attestation_evaluation.RightsAttestationEvaluation] | None, - Field( - description="Complete seller-produced verifier-of-record results for retained rights references. Buyers MUST ignore any evaluation they originally supplied and rely only on this seller readback for this seller's eligibility decision. This array has no independent item ceiling because rights is not capped; under a required policy every applicable retained constraint needs a corresponding current verified result.", - min_length=1, - ), - ] = None - concept_id: Annotated[ - str | None, - Field( - description='Creative concept this creative belongs to. Concepts group related creatives across sizes and formats.' - ), - ] = None - concept_name: Annotated[str | None, Field(description='Human-readable concept name')] = None - variables: Annotated[ - list[creative_variable.CreativeVariable] | None, - Field( - description='Dynamic content variables (DCO slots) for this creative. Included when include_variables=true.' - ), - ] = None - assignments: Annotated[ - Assignments1 | None, - Field(description='Current package assignments (included when include_assignments=true)'), - ] = None - snapshot: Annotated[ - Snapshot | None, - Field( - description='Lightweight delivery snapshot (included when include_snapshot=true). For detailed performance analytics, use get_creative_delivery.' - ), - ] = None - snapshot_unavailable_reason: Annotated[ - snapshot_unavailable_reason_1.SnapshotUnavailableReason | None, - Field( - description='Machine-readable reason the snapshot is omitted. Present only when include_snapshot was true and snapshot data is unavailable for this creative.' - ), - ] = None - items: Annotated[ - list[creative_item.CreativeItem] | None, - Field( - description='Items for multi-asset formats like carousels and native ads (included when include_items=true)' - ), - ] = None - pricing_options: Annotated[ - list[vendor_pricing_option.VendorPricingOption] | None, - Field( - description='Pricing options for using this creative (serving, delivery). Used by ad servers and library agents. Transformation agents expose build pricing on canonical transformer.pricing_options entries from list_transformers instead. Present when include_pricing=true and account provided. The buyer passes the applied pricing_option_id in report_usage.', - min_length=1, - ), - ] = None - purge: Annotated[ - Purge | None, - Field( - description="Tombstone block — present only when this record is a soft-purged creative surfaced via `include_purged: true`. The record's `status` field reflects the last status before purge (frozen — buyers MUST treat the creative as gone; assignments, snapshot, and serving operations no longer apply). Tombstones surface for the seller's webhook activity retention window (30 days from `purge.at`). Hard purges (`purge_kind: hard` on the webhook) do not surface on this read — the [`creative.purged`](https://adcontextprotocol.org/schemas/v3/creative/creative-purged-webhook.json) webhook is the only signal." - ), - ] = None - webhook_activity: Annotated[ - list[webhook_activity_record.WebhookActivityRecord] | None, - Field( - description='Recent webhook fires scoped to this creative — creative.status_changed, creative.purged, creative.assignment_changed, and assignment-level indicators.changed deliveries. Present only when include_webhook_activity is true. Account-anchored records include subscriber_id; the parent creative_id disambiguates the record. Retention: 30 days from completed_at. See snapshot-and-log.mdx § Webhook activity log pattern.', - max_length=200, - ), - ] = None - - - @model_validator(mode='after') - def _reject_legacy_format_ref(self) -> Creatives1: - if self.format_id is not None: - raise ValueError('format_id and format_kind are mutually exclusive') - return self +Creatives = Creative +Creatives1 = Creative class ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope): @@ -697,8 +452,7 @@ class ListCreativesResponse(AdcpVersionEnvelope, ProtocolEnvelope): ] pagination: pagination_response.PaginationResponse creatives: Annotated[ - Sequence[Creatives | Creatives1], - Field(description='Array of creative assets matching the query'), + Sequence[Creative], Field(description='Array of creative assets matching the query') ] format_summary: Annotated[ dict[Annotated[str, StringConstraints(pattern=r'^[a-zA-Z0-9_-]+$')], int] | None, diff --git a/src/adcp/types/generated_poc/creative/preview_creative_request.py b/src/adcp/types/generated_poc/creative/preview_creative_request.py index b8716f35a..23bfc4941 100644 --- a/src/adcp/types/generated_poc/creative/preview_creative_request.py +++ b/src/adcp/types/generated_poc/creative/preview_creative_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: creative/preview_creative_request.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -49,7 +49,7 @@ class Input(AdCPBaseModel): ] = None -class Input12(AdCPBaseModel): +class Input10(AdCPBaseModel): model_config = ConfigDict( extra='allow', ) @@ -92,7 +92,7 @@ class Request(AdCPBaseModel): ), ] = None inputs: Annotated[ - list[Input12] | None, + list[Input10] | None, Field( description='Array of input sets for generating multiple preview variants', min_length=1 ), diff --git a/src/adcp/types/generated_poc/creative/sync_creatives_request.py b/src/adcp/types/generated_poc/creative/sync_creatives_request.py index 8903f8d21..223fed237 100644 --- a/src/adcp/types/generated_poc/creative/sync_creatives_request.py +++ b/src/adcp/types/generated_poc/creative/sync_creatives_request.py @@ -1,45 +1,24 @@ # generated by datamodel-codegen: # filename: creative/sync_creatives_request.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from typing import Any, Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import ConfigDict, Field, RootModel, StringConstraints +from pydantic import ConfigDict, Field, RootModel -from ..core import account_ref, canonical_format_kind +from ..core import account_ref from ..core import context as context_1 -from ..core import creative_assets, creative_localization, creative_revision_id +from ..core import creative_localization, creative_revision_id from ..core import ext as ext_1 -from ..core import format_id as format_id_1 -from ..core import format_option_ref as format_option_ref_1 -from ..core import industry_identifier, placement_ref -from ..core import provenance as provenance_1 from ..core import push_notification_config as push_notification_config_1 -from ..core import representation_selection as representation_selection_1 -from ..core import rights_constraint -from ..core.assets import asset_union +from ..core.creative_asset import CreativeAsset from ..core.version_envelope import AdcpVersionEnvelope -from ..enums import creative_status from ..enums import validation_mode as validation_mode_1 -class Input(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - name: Annotated[str, Field(description='Human-readable name for this preview variant')] - macros: Annotated[ - dict[str, str] | None, Field(description='Macro values to apply for this preview') - ] = None - context_description: Annotated[ - str | None, - Field(description='Natural language description of the context for AI-generated content'), - ] = None - - class Assignment(AdCPBaseModel): model_config = ConfigDict( extra='forbid', @@ -114,11 +93,7 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) -class Assets(RootModel[list[asset_union.AssetVariant]]): - root: Annotated[list[asset_union.AssetVariant], Field(min_length=1)] - - -class Creative63(AdCPBaseModel): +class Creative(CreativeAsset): revision_id: Annotated[ creative_revision_id.CreativeRevisionId | None, Field( @@ -133,236 +108,6 @@ class Creative63(AdCPBaseModel): ] = None -class Creative61(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[ - str, - Field( - description='Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten.' - ), - ] - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', - ), - ] - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind | None, - Field( - description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' - ), - ] = None - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description='Assets required by the format, keyed by asset_id or canonical asset_group_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1` like carousel `cards` or responsive_creative `headlines`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema, including reference assets such as `published_post` when a product accepts already-published post references.' - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' - ), - ] = None - inputs: Annotated[ - list[Input] | None, - Field( - description='Preview contexts for generative formats - defines what scenarios to generate previews for' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - status: Annotated[ - creative_status.CreativeStatus | None, - Field( - description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." - ), - ] = None - weight: Annotated[ - float | None, - Field( - description='Optional delivery weight for creative rotation when uploading via create_media_buy or update_media_buy (0-100). If omitted, platform determines rotation. Only used during upload to media buy - not stored in creative library.', - ge=0.0, - le=100.0, - ), - ] = None - placement_refs: Annotated[ - list[placement_ref.PlacementReference] | None, - Field( - description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", - min_length=1, - ), - ] = None - placement_ids: Annotated[ - list[str] | None, - Field( - deprecated=True, - description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', - min_length=1, - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', - min_length=1, - ), - ] = None - - -class Creative62(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[ - str, - Field( - description='Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten.' - ), - ] - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject | None, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', - ), - ] = None - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' - ), - ] - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description='Assets required by the format, keyed by asset_id or canonical asset_group_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1` like carousel `cards` or responsive_creative `headlines`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema, including reference assets such as `published_post` when a product accepts already-published post references.' - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' - ), - ] = None - inputs: Annotated[ - list[Input] | None, - Field( - description='Preview contexts for generative formats - defines what scenarios to generate previews for' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - status: Annotated[ - creative_status.CreativeStatus | None, - Field( - description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." - ), - ] = None - weight: Annotated[ - float | None, - Field( - description='Optional delivery weight for creative rotation when uploading via create_media_buy or update_media_buy (0-100). If omitted, platform determines rotation. Only used during upload to media buy - not stored in creative library.', - ge=0.0, - le=100.0, - ), - ] = None - placement_refs: Annotated[ - list[placement_ref.PlacementReference] | None, - Field( - description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", - min_length=1, - ), - ] = None - placement_ids: Annotated[ - list[str] | None, - Field( - deprecated=True, - description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', - min_length=1, - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', - min_length=1, - ), - ] = None - - -class Creative64(Creative61, Creative63): - pass - - -class Creative65(Creative62, Creative63): - pass - - -class Creative(RootModel[Creative64 | Creative65]): - root: Creative64 | Creative65 - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the wrapped type.""" - if name.startswith('_'): - raise AttributeError(name) - return getattr(self.root, name) - class SyncCreativesRequest(AdcpVersionEnvelope): model_config = ConfigDict( extra='allow', diff --git a/src/adcp/types/generated_poc/enums/signal_value_type.py b/src/adcp/types/generated_poc/enums/signal_value_type.py index e7ef655d2..4cd32b23c 100644 --- a/src/adcp/types/generated_poc/enums/signal_value_type.py +++ b/src/adcp/types/generated_poc/enums/signal_value_type.py @@ -1,11 +1,13 @@ # generated by datamodel-codegen: # filename: enums/signal_value_type.json -# timestamp: 2026-05-02T19:36:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum +from pydantic import Field + class SignalValueType(StrEnum): binary = 'binary' diff --git a/src/adcp/types/generated_poc/governance/check_governance_request.py b/src/adcp/types/generated_poc/governance/check_governance_request.py index 64be17aba..70592ff46 100644 --- a/src/adcp/types/generated_poc/governance/check_governance_request.py +++ b/src/adcp/types/generated_poc/governance/check_governance_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/check_governance_request.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -291,6 +291,13 @@ class Subject6(AdCPBaseModel): ext: ext_1.ExtensionObject | None = None +class Subject8(Subject): + type: Literal['agent'] = 'agent' + + +class Subject9(Subject6): + type: Literal['resource'] = 'resource' + class Subject4(AdCPBaseModel): model_config = ConfigDict( @@ -444,7 +451,7 @@ class CheckGovernanceRequest2(AdcpVersionEnvelope): class RuntimeAttestation1(AttestationReference): subject: Annotated[ - Subject4 | Subject | Subject6 | None, + Subject4 | Subject8 | Subject9 | None, Field( description='Typed identity of the entity or object an attestation credential is about. Brand and agent subjects reuse canonical AdCP identities. Other resources use an open, URI-namespaced resource_type plus an identifier whose namespace is explicit. Evaluators MUST compare the resolved credential subject to this complete typed identity, not to id alone.', discriminator='type', diff --git a/src/adcp/types/generated_poc/governance/reported_outcome_error.py b/src/adcp/types/generated_poc/governance/reported_outcome_error.py index 0eec32045..1a7a78472 100644 --- a/src/adcp/types/generated_poc/governance/reported_outcome_error.py +++ b/src/adcp/types/generated_poc/governance/reported_outcome_error.py @@ -1,14 +1,14 @@ # generated by datamodel-codegen: # filename: governance/reported_outcome_error.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:29:24+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from typing import Any, Annotated +from typing import Any, Annotated, Dict from adcp.types.base import AdCPBaseModel -from pydantic import Field, RootModel, StringConstraints +from pydantic import ConfigDict, Field, RootModel, StringConstraints class Recovery(StrEnum): @@ -79,10 +79,14 @@ def __getattr__(self, name: str) -> Any: return getattr(self.root, name) class BoundedObject(RootModel[dict[Annotated[str, StringConstraints(max_length=128)], BoundedValue]]): - root: dict[Annotated[str, StringConstraints(max_length=128)], BoundedValue] + root: Annotated[dict[Annotated[str, StringConstraints(max_length=128)], BoundedValue], Field(max_length=32)] class ReportedOutcomeError(AdCPBaseModel): + __pydantic_extra__: Dict[str, BoundedValue] + model_config = ConfigDict( + extra='allow', + ) code: Annotated[str | None, Field(max_length=64, min_length=1)] = None message: Annotated[str | None, Field(max_length=4000)] = None field: Annotated[str | None, Field(max_length=1000)] = None diff --git a/src/adcp/types/generated_poc/governance/sync_plans_response.py b/src/adcp/types/generated_poc/governance/sync_plans_response.py index 87632c9ab..a34ab1210 100644 --- a/src/adcp/types/generated_poc/governance/sync_plans_response.py +++ b/src/adcp/types/generated_poc/governance/sync_plans_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: governance/sync_plans_response.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -22,7 +22,7 @@ class Status(StrEnum): error = 'error' -class Status45(StrEnum): +class Status50(StrEnum): active = 'active' inactive = 'inactive' @@ -32,7 +32,7 @@ class Category(AdCPBaseModel): extra='forbid', ) category_id: Annotated[str, Field(description='Validation category identifier.')] - status: Annotated[Status45, Field(description='Whether this category is active for this plan.')] + status: Annotated[Status50, Field(description='Whether this category is active for this plan.')] class Source(StrEnum): diff --git a/src/adcp/types/generated_poc/manifest_schema.py b/src/adcp/types/generated_poc/manifest_schema.py index 934f3611e..35edc4a8c 100644 --- a/src/adcp/types/generated_poc/manifest_schema.py +++ b/src/adcp/types/generated_poc/manifest_schema.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: manifest.schema.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -291,7 +291,8 @@ class AdcpManifest(AdCPBaseModel): tools: Annotated[ dict[ToolName, Tools], Field( - description='Every tool the AdCP spec defines, keyed by tool name (the snake_case name used in MCP/A2A invocations).' + description='Every tool the AdCP spec defines, keyed by tool name (the snake_case name used in MCP/A2A invocations).', + min_length=1, ), ] task_result_resolution: Annotated[ @@ -309,7 +310,8 @@ class AdcpManifest(AdCPBaseModel): error_codes: Annotated[ dict[str, ErrorCodes], Field( - description="Every standard error code in the AdCP error vocabulary, keyed by the SCREAMING_SNAKE code. Mirrors enums/error-code.json's enum + enumMetadata + enumDescriptions. Open-set: see error_code_policy for unknown-code handling." + description="Every standard error code in the AdCP error vocabulary, keyed by the SCREAMING_SNAKE code. Mirrors enums/error-code.json's enum + enumMetadata + enumDescriptions. Open-set: see error_code_policy for unknown-code handling.", + min_length=1, ), ] specialisms: Annotated[ diff --git a/src/adcp/types/generated_poc/media_buy/decline_proposals_response.py b/src/adcp/types/generated_poc/media_buy/decline_proposals_response.py index e6dd2055d..9b2c67bad 100644 --- a/src/adcp/types/generated_poc/media_buy/decline_proposals_response.py +++ b/src/adcp/types/generated_poc/media_buy/decline_proposals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/decline_proposals_response.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -59,10 +59,16 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) +class Results4(Results1): + outcome: Literal['declined'] = 'declined' -class Results3(RootModel[Results1 | Results2]): - root: Annotated[Results1 | Results2, Field(discriminator='outcome')] +class Results5(Results2): + outcome: Literal['unable'] = 'unable' + + +class Results3(RootModel[Results4 | Results5]): + root: Annotated[Results4 | Results5, Field(discriminator='outcome')] def __getattr__(self, name: str) -> Any: """Proxy attribute access to the wrapped type.""" if name.startswith('_'): diff --git a/src/adcp/types/generated_poc/media_buy/get_reporting_status_response.py b/src/adcp/types/generated_poc/media_buy/get_reporting_status_response.py index cba42f4d7..c4b6c1d0b 100644 --- a/src/adcp/types/generated_poc/media_buy/get_reporting_status_response.py +++ b/src/adcp/types/generated_poc/media_buy/get_reporting_status_response.py @@ -1,9 +1,10 @@ # generated by datamodel-codegen: # filename: media_buy/get_reporting_status_response.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations +from adcp.types._str_enum import StrEnum from typing import Annotated from adcp.types.base import AdCPBaseModel @@ -42,6 +43,11 @@ class ObligationCounts(AdCPBaseModel): complete: Annotated[int, Field(ge=0)] +class Status(StrEnum): + completed = 'completed' + failed = 'failed' + + class DeliveryConfigGeneration(AdCPBaseModel): model_config = ConfigDict( extra='forbid', @@ -178,3 +184,4 @@ class GetReportingStatusResponse(AdcpVersionEnvelope, ProtocolEnvelope): errors: list[error.Error] | None = None context: context_1.ContextObject | None = None ext: ext_1.ExtensionObject | None = None + status: Status diff --git a/src/adcp/types/generated_poc/media_buy/package_request.py b/src/adcp/types/generated_poc/media_buy/package_request.py index aeff7d7dc..015583a4d 100644 --- a/src/adcp/types/generated_poc/media_buy/package_request.py +++ b/src/adcp/types/generated_poc/media_buy/package_request.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/package_request.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -8,51 +8,26 @@ from typing import Annotated, Any, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StringConstraints, model_validator +from pydantic import AwareDatetime, ConfigDict, Field, RootModel, model_validator from ..core import audience_evidence_pin from ..core import audience_evidence_requirements as audience_evidence_requirements_1 from ..core import bidding_policy, brand_ref, canonical_format_kind, catalog from ..core import context as context_1 -from ..core import creative_assets, creative_assignment, duration +from ..core import creative_asset, creative_assignment, duration from ..core import ext as ext_1 -from ..core import format_id as format_id_1 -from ..core import format_option_ref as format_option_ref_1 -from ..core import industry_identifier +from ..core import format_id, format_option_ref from ..core import measurement_terms as measurement_terms_1 -from ..core import optimization_goal, performance_standard, placement_ref -from ..core import provenance as provenance_1 -from ..core import representation_selection as representation_selection_1 -from ..core import rights_constraint, targeting, vendor_metric_id -from ..core.assets import asset_union +from ..core import optimization_goal, performance_standard, targeting, vendor_metric_id from ..core.version_envelope import AdcpVersionEnvelope from ..enums import attribution_methodology as attribution_methodology_1 from ..enums import available_metric from ..enums import completion_source as completion_source_1 -from ..enums import creative_status from ..enums import lift_dimension as lift_dimension_1 from ..enums import pacing as pacing_1 from ..enums import viewability_standard as viewability_standard_1 -class Input(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - name: Annotated[str, Field(description='Human-readable name for this preview variant')] - macros: Annotated[ - dict[str, str] | None, Field(description='Macro values to apply for this preview') - ] = None - context_description: Annotated[ - str | None, - Field(description='Natural language description of the context for AI-generated content'), - ] = None - - -class Creative43(AdCPBaseModel): - pass - - class Qualifier(AdCPBaseModel): model_config = ConfigDict( extra='forbid', @@ -128,240 +103,6 @@ def __getattr__(self, name: str) -> Any: raise AttributeError(name) return getattr(self.root, name) -class Assets(RootModel[list[asset_union.AssetVariant]]): - root: Annotated[list[asset_union.AssetVariant], Field(min_length=1)] - - -class Creative41(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[ - str, - Field( - description='Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten.' - ), - ] - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', - ), - ] - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind | None, - Field( - description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' - ), - ] = None - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description='Assets required by the format, keyed by asset_id or canonical asset_group_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1` like carousel `cards` or responsive_creative `headlines`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema, including reference assets such as `published_post` when a product accepts already-published post references.' - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' - ), - ] = None - inputs: Annotated[ - list[Input] | None, - Field( - description='Preview contexts for generative formats - defines what scenarios to generate previews for' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - status: Annotated[ - creative_status.CreativeStatus | None, - Field( - description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." - ), - ] = None - weight: Annotated[ - float | None, - Field( - description='Optional delivery weight for creative rotation when uploading via create_media_buy or update_media_buy (0-100). If omitted, platform determines rotation. Only used during upload to media buy - not stored in creative library.', - ge=0.0, - le=100.0, - ), - ] = None - placement_refs: Annotated[ - list[placement_ref.PlacementReference] | None, - Field( - description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", - min_length=1, - ), - ] = None - placement_ids: Annotated[ - list[str] | None, - Field( - deprecated=True, - description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', - min_length=1, - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', - min_length=1, - ), - ] = None - - -class Creative42(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[ - str, - Field( - description='Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten.' - ), - ] - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject | None, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', - ), - ] = None - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' - ), - ] - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description='Assets required by the format, keyed by asset_id or canonical asset_group_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1` like carousel `cards` or responsive_creative `headlines`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema, including reference assets such as `published_post` when a product accepts already-published post references.' - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' - ), - ] = None - inputs: Annotated[ - list[Input] | None, - Field( - description='Preview contexts for generative formats - defines what scenarios to generate previews for' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - status: Annotated[ - creative_status.CreativeStatus | None, - Field( - description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." - ), - ] = None - weight: Annotated[ - float | None, - Field( - description='Optional delivery weight for creative rotation when uploading via create_media_buy or update_media_buy (0-100). If omitted, platform determines rotation. Only used during upload to media buy - not stored in creative library.', - ge=0.0, - le=100.0, - ), - ] = None - placement_refs: Annotated[ - list[placement_ref.PlacementReference] | None, - Field( - description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", - min_length=1, - ), - ] = None - placement_ids: Annotated[ - list[str] | None, - Field( - deprecated=True, - description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', - min_length=1, - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', - min_length=1, - ), - ] = None - - -class Creative44(Creative41, Creative43): - pass - - -class Creative45(Creative42, Creative43): - pass - - -class Creative(RootModel[Creative44 | Creative45]): - root: Creative44 | Creative45 - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the wrapped type.""" - if name.startswith('_'): - raise AttributeError(name) - return getattr(self.root, name) - class PackageRequest(AdcpVersionEnvelope): model_config = ConfigDict( extra='allow', @@ -373,7 +114,7 @@ class PackageRequest(AdcpVersionEnvelope): ), ] format_ids: Annotated[ - list[format_id_1.FormatReferenceStructuredObject] | None, + list[format_id.FormatReferenceStructuredObject] | None, Field( deprecated=True, description='Deprecated in AdCP 3.2; removed in AdCP 4.0. Legacy named-format selector retained for older 3.x peers. New buyers MUST NOT emit this field. Sellers MUST normalize every entry through the canonical mapping path before product satisfaction checks; an entry that cannot be normalized is rejected with `UNSUPPORTED_FEATURE` before any equivalence check. When this field coexists with `format_option_refs` or `format_kind` plus `params`, sellers MUST compare the product option sets selected by each resolved route. Legacy parameter compatibility follows the asymmetric v2-narrows-v1 relation defined by canonical formats, not raw object equality. Different format shapes, selected option sets, or incompatible dimensions are rejected with `CONFLICTING_SELECTORS`; sellers MUST NOT silently ignore the legacy projection. Equivalent dual emission remains valid during the 3.x compatibility window. If omitted and no canonical selector is present, all formats supported by the product are active.', @@ -381,7 +122,7 @@ class PackageRequest(AdcpVersionEnvelope): ), ] = None format_option_refs: Annotated[ - list[format_option_ref_1.FormatOptionReference] | None, + list[format_option_ref.FormatOptionReference] | None, Field( description='Canonical 3.2 format-option selector. Each reference matches one target product `format_options[]` entry. Publisher-backed options match `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match `{ scope: "product", format_option_id }`. Sellers reject unresolved options with `UNSUPPORTED_FEATURE` and a field path to the failing entry before comparing co-present routes. New buyers MUST use this route by itself and MUST NOT dual-emit either a direct canonical selector or deprecated `format_ids`. Receivers handling older 3.x multi-route requests MUST resolve every present route, require each route to select the same product option set, and reject disagreement with `CONFLICTING_SELECTORS` before treating `format_option_refs` as authoritative.', min_length=1, @@ -522,7 +263,7 @@ class PackageRequest(AdcpVersionEnvelope): ), ] = None creatives: Annotated[ - Sequence[Creative] | None, + Sequence[creative_asset.CreativeAsset] | None, Field( description="Upload creative assets inline and assign to this package. Native localization is not accepted on this path; use sync_creatives before assigning the library creative. When the seller also advertises creative.has_creative_library: true, these creatives enter the seller's creative library and can be reused by creative_id while retained; inline-only sellers may store them as package-scoped assets. Use creative_assignments instead for existing library creatives.", max_length=100, diff --git a/src/adcp/types/generated_poc/media_buy/package_update.py b/src/adcp/types/generated_poc/media_buy/package_update.py index 3a04dd787..2e18cc535 100644 --- a/src/adcp/types/generated_poc/media_buy/package_update.py +++ b/src/adcp/types/generated_poc/media_buy/package_update.py @@ -1,26 +1,19 @@ # generated by datamodel-codegen: # filename: media_buy/package_update.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations -from typing import Any, Annotated, Literal +from typing import Annotated, Literal from adcp.types.base import AdCPBaseModel -from pydantic import AwareDatetime, ConfigDict, Field, RootModel, StringConstraints +from pydantic import AwareDatetime, ConfigDict, Field -from ..core import bidding_policy, canonical_format_kind, catalog +from ..core import bidding_policy, catalog from ..core import context as context_1 -from ..core import creative_assets, creative_assignment +from ..core import creative_asset, creative_assignment from ..core import ext as ext_1 -from ..core import format_id as format_id_1 -from ..core import format_option_ref as format_option_ref_1 -from ..core import industry_identifier, optimization_goal, placement_ref -from ..core import provenance as provenance_1 -from ..core import representation_selection as representation_selection_1 -from ..core import rights_constraint, targeting -from ..core.assets import asset_union -from ..enums import creative_status +from ..core import optimization_goal, targeting from ..enums import match_type as match_type_1 from ..enums import pacing as pacing_1 @@ -64,258 +57,6 @@ class NegativeKeywordsRemoveItem(AdCPBaseModel): match_type: match_type_1.MatchType -class Input(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - name: Annotated[str, Field(description='Human-readable name for this preview variant')] - macros: Annotated[ - dict[str, str] | None, Field(description='Macro values to apply for this preview') - ] = None - context_description: Annotated[ - str | None, - Field(description='Natural language description of the context for AI-generated content'), - ] = None - - -class Creative53(AdCPBaseModel): - pass - - -class Assets(RootModel[list[asset_union.AssetVariant]]): - root: Annotated[list[asset_union.AssetVariant], Field(min_length=1)] - - -class Creative51(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[ - str, - Field( - description='Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten.' - ), - ] - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', - ), - ] - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind | None, - Field( - description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' - ), - ] = None - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description='Assets required by the format, keyed by asset_id or canonical asset_group_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1` like carousel `cards` or responsive_creative `headlines`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema, including reference assets such as `published_post` when a product accepts already-published post references.' - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' - ), - ] = None - inputs: Annotated[ - list[Input] | None, - Field( - description='Preview contexts for generative formats - defines what scenarios to generate previews for' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - status: Annotated[ - creative_status.CreativeStatus | None, - Field( - description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." - ), - ] = None - weight: Annotated[ - float | None, - Field( - description='Optional delivery weight for creative rotation when uploading via create_media_buy or update_media_buy (0-100). If omitted, platform determines rotation. Only used during upload to media buy - not stored in creative library.', - ge=0.0, - le=100.0, - ), - ] = None - placement_refs: Annotated[ - list[placement_ref.PlacementReference] | None, - Field( - description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", - min_length=1, - ), - ] = None - placement_ids: Annotated[ - list[str] | None, - Field( - deprecated=True, - description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', - min_length=1, - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', - min_length=1, - ), - ] = None - - -class Creative52(AdCPBaseModel): - model_config = ConfigDict( - extra='allow', - ) - creative_id: Annotated[ - str, - Field( - description='Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten.' - ), - ] - name: Annotated[str, Field(description='Human-readable creative name')] - format_id: Annotated[ - format_id_1.FormatReferenceStructuredObject | None, - Field( - deprecated=True, - description='**DEPRECATED in 3.2.** Legacy named-format path retained for older 3.x peers. New creative assets use `format_kind` and optional `format_option_ref`.', - ), - ] = None - format_kind: Annotated[ - canonical_format_kind.CanonicalFormatKind, - Field( - description='Canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with deprecated `format_id`.' - ), - ] - format_option_ref: Annotated[ - format_option_ref_1.FormatOptionReference | None, - Field( - description='3.1+ format-option path, optional. Structured format option reference matching one of the target product\'s `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context.' - ), - ] = None - representation_selection: Annotated[ - representation_selection_1.RepresentationSelection | None, - Field( - description='Readback lineage to the complete CreativeRepresentationSet revision and representation selected before this seller-bound creative was synced.' - ), - ] = None - assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z0-9_]+$')], asset_union.AssetVariant | Assets], - Field( - description='Assets required by the format, keyed by asset_id or canonical asset_group_id. Each slot value is either a single asset object or an array of asset objects (for slots with `min`/`max > 1` like carousel `cards` or responsive_creative `headlines`). Each asset value carries an `asset_type` discriminator that selects the matching asset schema, including reference assets such as `published_post` when a product accepts already-published post references.' - ), - ] - component_assets: Annotated[ - dict[Annotated[str, StringConstraints(pattern=r'^[a-z][a-z0-9_]*$')], creative_assets.CreativeAssets] | None, - Field( - description='Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind.' - ), - ] = None - inputs: Annotated[ - list[Input] | None, - Field( - description='Preview contexts for generative formats - defines what scenarios to generate previews for' - ), - ] = None - tags: Annotated[ - list[str] | None, Field(description='User-defined tags for organization and searchability') - ] = None - status: Annotated[ - creative_status.CreativeStatus | None, - Field( - description="For generative creatives: set to 'approved' to finalize, 'rejected' to request regeneration with updated assets/message. Omit for non-generative creatives (system will set based on processing state)." - ), - ] = None - weight: Annotated[ - float | None, - Field( - description='Optional delivery weight for creative rotation when uploading via create_media_buy or update_media_buy (0-100). If omitted, platform determines rotation. Only used during upload to media buy - not stored in creative library.', - ge=0.0, - le=100.0, - ), - ] = None - placement_refs: Annotated[ - list[placement_ref.PlacementReference] | None, - Field( - description="Optional structured product-context placement references where this uploaded creative should run when uploading via create_media_buy or update_media_buy. These items always use placement-ref product-context semantics, even when tolerated additional members make an item resemble placement-identity. Receivers match only against the target package's committed placement set; kind and seller_agent are non-authoritative for routing and MUST NOT expand or reinterpret that set. A receiver MUST reject a ref when the enclosing product and committed set do not yield one unambiguous match. New senders SHOULD include publisher_domain for publisher-catalog placements. If omitted, creative runs on all buyer-targetable placements. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`. Only used during upload to media buy - not stored in creative library.", - min_length=1, - ), - ] = None - placement_ids: Annotated[ - list[str] | None, - Field( - deprecated=True, - description='Legacy shorthand array of placement IDs where this creative should run when uploading via create_media_buy or update_media_buy. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. If omitted, creative runs on all buyer-targetable placements. If `placement_refs` is also present, receivers MUST ignore this field. Only used during upload to media buy - not stored in creative library.', - min_length=1, - ), - ] = None - industry_identifiers: Annotated[ - list[industry_identifier.IndustryIdentifier] | None, - Field( - description='Industry-standard or market-specific identifiers for this creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). In broadcast and scheduled audio/video buying, these identifiers tie the creative to rotation instructions, clearance records, and traffic systems. A creative may have multiple identifiers when different systems reference the same asset. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support.' - ), - ] = None - provenance: Annotated[ - provenance_1.Provenance | None, - Field( - description='Provenance metadata for this creative. Serves as the default provenance for all manifests and assets within this creative. A manifest or asset with its own provenance replaces this object entirely (no field-level merging).' - ), - ] = None - rights: Annotated[ - list[rights_constraint.RightsConstraint] | None, - Field( - description='Rights constraints that MUST survive sync, package assignment, and list readback. Buyer-carried constraints and references do not authorize serving; the seller evaluates them under media_buy.rights_attestations and its adcp.attestations policy.', - min_length=1, - ), - ] = None - - -class Creative54(Creative51, Creative53): - pass - - -class Creative55(Creative52, Creative53): - pass - - -class Creative(RootModel[Creative54 | Creative55]): - root: Creative54 | Creative55 - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the wrapped type.""" - if name.startswith('_'): - raise AttributeError(name) - return getattr(self.root, name) - class PackageUpdate(AdCPBaseModel): model_config = ConfigDict( extra='allow', @@ -440,7 +181,7 @@ class PackageUpdate(AdCPBaseModel): ), ] = None creatives: Annotated[ - list[Creative] | None, + list[creative_asset.CreativeAsset] | None, Field( description="Replace this package's inline creative assets. Native localization is not accepted on this path; use sync_creatives before assigning the library creative. When the seller also advertises creative.has_creative_library: true, new inline creatives enter the seller's creative library and can be reused by creative_id while retained; inline-only sellers may store them as package-scoped assets. Use creative_assignments instead for existing library creatives.", max_length=100, diff --git a/src/adcp/types/generated_poc/media_buy/refine_proposals_response.py b/src/adcp/types/generated_poc/media_buy/refine_proposals_response.py index f0c6e7b01..b73ee2c17 100644 --- a/src/adcp/types/generated_poc/media_buy/refine_proposals_response.py +++ b/src/adcp/types/generated_poc/media_buy/refine_proposals_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/refine_proposals_response.json -# timestamp: 2026-08-28T20:03:29+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -214,7 +214,7 @@ class Proposal6(Proposal3): pass -class Results7(AdCPBaseModel): +class Results9(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -257,7 +257,7 @@ class Results7(AdCPBaseModel): ] = None -class Results8(AdCPBaseModel): +class Results10(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -300,7 +300,7 @@ class Results8(AdCPBaseModel): ] = None -class Results9(AdCPBaseModel): +class Results11(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -349,7 +349,7 @@ class Results9(AdCPBaseModel): ] = None -class Results10(AdCPBaseModel): +class Results12(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -392,15 +392,15 @@ class Results10(AdCPBaseModel): ] = None -class Results6(RootModel[Results7 | Results8 | Results9 | Results10]): - root: Annotated[Results7 | Results8 | Results9 | Results10, Field(discriminator='outcome')] +class Results8(RootModel[Results9 | Results10 | Results11 | Results12]): + root: Annotated[Results9 | Results10 | Results11 | Results12, Field(discriminator='outcome')] def __getattr__(self, name: str) -> Any: """Proxy attribute access to the wrapped type.""" if name.startswith('_'): raise AttributeError(name) return getattr(self.root, name) -class Results13(AdCPBaseModel): +class Results15(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -443,7 +443,7 @@ class Results13(AdCPBaseModel): ] = None -class Results14(AdCPBaseModel): +class Results16(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -486,7 +486,7 @@ class Results14(AdCPBaseModel): ] = None -class Results15(AdCPBaseModel): +class Results17(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -535,8 +535,12 @@ class Results15(AdCPBaseModel): ] = None -class Results12(RootModel[Results13 | Results14 | Results15 | Results10]): - root: Annotated[Results13 | Results14 | Results15 | Results10, Field(discriminator='outcome')] +class Results18(Results12): + outcome: Literal['unable'] = 'unable' + + +class Results14(RootModel[Results15 | Results16 | Results17 | Results18]): + root: Annotated[Results15 | Results16 | Results17 | Results18, Field(discriminator='outcome')] def __getattr__(self, name: str) -> Any: """Proxy attribute access to the wrapped type.""" if name.startswith('_'): @@ -549,7 +553,7 @@ class RefineProposalsResponse1(AdCPBaseModel): ) adcp_version: version_envelope.AdcpVersion | None = None results: Annotated[ - list[Results6] | Results, + list[Results8] | Results, Field( description="Ordered results. If any result is finalized, every result MUST be finalized; a finalize batch either creates every requested hold or none. Every returned proposal carries parent_proposal_id equal to the result's source_proposal_id, making negotiation lineage reconstructible from the proposals alone.", min_length=1, @@ -576,7 +580,7 @@ class RefineProposalsResponse2(CompactTaskSubmitted): ) adcp_version: version_envelope.AdcpVersion | None = None results: Annotated[ - list[Results12] | Results | None, + list[Results14] | Results | None, Field( description="Ordered results. If any result is finalized, every result MUST be finalized; a finalize batch either creates every requested hold or none. Every returned proposal carries parent_proposal_id equal to the result's source_proposal_id, making negotiation lineage reconstructible from the proposals alone.", min_length=1, diff --git a/src/adcp/types/generated_poc/media_buy/sync_reporting_receipts_response.py b/src/adcp/types/generated_poc/media_buy/sync_reporting_receipts_response.py index 2406e0b1b..1e90f6013 100644 --- a/src/adcp/types/generated_poc/media_buy/sync_reporting_receipts_response.py +++ b/src/adcp/types/generated_poc/media_buy/sync_reporting_receipts_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: media_buy/sync_reporting_receipts_response.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -28,7 +28,7 @@ class Results(AdCPBaseModel): errors: Annotated[list[error.Error], Field(max_length=16, min_length=1)] -class Results18(AdCPBaseModel): +class Results20(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -36,7 +36,7 @@ class Results18(AdCPBaseModel): receipt: reporting_receipt.ReportingReceipt -class Results19(AdCPBaseModel): +class Results21(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -44,7 +44,7 @@ class Results19(AdCPBaseModel): receipt: reporting_receipt.ReportingReceipt -class Results20(AdCPBaseModel): +class Results22(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -52,7 +52,7 @@ class Results20(AdCPBaseModel): adjustment_receipt: reporting_adjustment_receipt.ReportingAdjustmentReceipt -class Results21(AdCPBaseModel): +class Results23(AdCPBaseModel): model_config = ConfigDict( extra='forbid', ) @@ -71,7 +71,7 @@ class SyncReportingReceiptsResponse(AdcpVersionEnvelope, ProtocolEnvelope): ), ] = 'completed' results: Annotated[ - list[Results18 | Results19 | Results20 | Results21 | Results], + list[Results20 | Results21 | Results22 | Results23 | Results], Field(max_length=100, min_length=1), ] context: context_1.ContextObject | None = None diff --git a/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py b/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py index 60493a770..823e5fa15 100644 --- a/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py +++ b/src/adcp/types/generated_poc/protocol/get_adcp_capabilities_response.py @@ -1,12 +1,12 @@ # generated by datamodel-codegen: # filename: protocol/get_adcp_capabilities_response.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations from datetime import date from adcp.types._str_enum import StrEnum -from typing import Any, Annotated, Literal +from typing import Any, Annotated, Dict, Literal from adcp.types.base import AdCPBaseModel from pydantic import AnyUrl, AwareDatetime, ConfigDict, Field, RootModel @@ -926,6 +926,10 @@ class DiscoveryMode(StrEnum): class Features(AdCPBaseModel): + __pydantic_extra__: Dict[str, bool] + model_config = ConfigDict( + extra='allow', + ) catalog_signals: Annotated[ bool | None, Field( @@ -935,6 +939,8 @@ class Features(AdCPBaseModel): ] = None + + class Signals(AdCPBaseModel): data_provider_domains: Annotated[ list[DataProviderDomain] | None, @@ -968,7 +974,8 @@ class SignalActivation(AdCPBaseModel): claim_types: Annotated[ list[AnyUrl], Field( - description='Signal-quality claim identifiers accepted for this governed action. Values are open absolute URIs and MUST be a subset of adcp.attestations.accepted_claim_types.' + description='Signal-quality claim identifiers accepted for this governed action. Values are open absolute URIs and MUST be a subset of adcp.attestations.accepted_claim_types.', + min_length=1, ), ] @@ -1769,7 +1776,8 @@ class Targeting(AdCPBaseModel): ] | None, Field( - description='Place targeting support keyed by collision-safe identifier system. Each system declares exact country-to-place-type combinations, accepted catalog versions, and a machine-readable resolver. Sellers MUST reject unsupported systems, country/type pairs, versions, and identifiers rather than silently dropping them.' + description='Place targeting support keyed by collision-safe identifier system. Each system declares exact country-to-place-type combinations, accepted catalog versions, and a machine-readable resolver. Sellers MUST reject unsupported systems, country/type pairs, versions, and identifiers rather than silently dropping them.', + min_length=1, ), ] = None age_restriction: Annotated[ diff --git a/src/adcp/types/generated_poc/protocol/sync_principal_response.py b/src/adcp/types/generated_poc/protocol/sync_principal_response.py index 06bf1ec64..6cebe0778 100644 --- a/src/adcp/types/generated_poc/protocol/sync_principal_response.py +++ b/src/adcp/types/generated_poc/protocol/sync_principal_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: protocol/sync_principal_response.json -# timestamp: 2026-09-01T19:37:00+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -25,7 +25,7 @@ class Action(StrEnum): cleared = 'cleared' -class Action29(StrEnum): +class Action32(StrEnum): would_update = 'would_update' would_be_unchanged = 'would_be_unchanged' would_clear = 'would_clear' @@ -36,7 +36,7 @@ class Result(AdCPBaseModel): extra='forbid', ) kind: Literal['validated'] = 'validated' - action: Action29 + action: Action32 dry_run: Literal[True] warnings: Annotated[list[error.Error] | None, Field(max_length=16)] = None diff --git a/src/adcp/types/generated_poc/sponsored_intelligence/si_sponsored_context_receipt.py b/src/adcp/types/generated_poc/sponsored_intelligence/si_sponsored_context_receipt.py index 725a8e573..f0355bab8 100644 --- a/src/adcp/types/generated_poc/sponsored_intelligence/si_sponsored_context_receipt.py +++ b/src/adcp/types/generated_poc/sponsored_intelligence/si_sponsored_context_receipt.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: sponsored_intelligence/si_sponsored_context_receipt.json -# timestamp: 2026-09-05T11:09:57+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -19,7 +19,7 @@ class Status(StrEnum): rejected = 'rejected' -class Status42(StrEnum): +class Status45(StrEnum): accepted = 'accepted' not_required = 'not_required' @@ -29,7 +29,7 @@ class DisclosureCommitment(AdCPBaseModel): extra='allow', ) status: Annotated[ - Status42, + Status45, Field( description="Host commitment status for the disclosure obligation. Use accepted when the declaration requires disclosure and the host will satisfy it; use not_required only when the declaration's disclosure_obligation.required is false. A host that will not satisfy a required disclosure rejects the sponsored context." ), diff --git a/src/adcp/types/generated_poc/trusted_match/context_match_response.py b/src/adcp/types/generated_poc/trusted_match/context_match_response.py index 166d09489..86af4adcc 100644 --- a/src/adcp/types/generated_poc/trusted_match/context_match_response.py +++ b/src/adcp/types/generated_poc/trusted_match/context_match_response.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: trusted_match/context_match_response.json -# timestamp: 2026-08-17T23:02:13+00:00 +# timestamp: 2026-09-05T19:08:34+00:00 from __future__ import annotations @@ -80,6 +80,7 @@ class ContextMatchResponseRouterPublisher(AdcpVersionEnvelope, ProtocolEnvelope) dict[Annotated[str, StringConstraints(pattern=r'^[A-Za-z0-9_]+$', min_length=1, max_length=64)], SignalsByProvider] | None, Field( - description="Router-authored map of provider targeting pairs, keyed by the publisher-assigned provider_id from provider registration. For every provider response containing a non-empty signals.targeting_kvs list, the router copies the complete list unchanged into that provider's bucket. The router derives the map key from its registration and MUST ignore or reject provider-supplied signals_by_provider data. A provider with no targeting pairs is omitted. Publishers resolve each (provider_id, key) tuple to a local ad-server destination and drop tuples that have no local mapping." + description="Router-authored map of provider targeting pairs, keyed by the publisher-assigned provider_id from provider registration. For every provider response containing a non-empty signals.targeting_kvs list, the router copies the complete list unchanged into that provider's bucket. The router derives the map key from its registration and MUST ignore or reject provider-supplied signals_by_provider data. A provider with no targeting pairs is omitted. Publishers resolve each (provider_id, key) tuple to a local ad-server destination and drop tuples that have no local mapping.", + min_length=1, ), ] = None diff --git a/tests/test_extra_policy.py b/tests/test_extra_policy.py index 693f294a4..d7a07ea1e 100644 --- a/tests/test_extra_policy.py +++ b/tests/test_extra_policy.py @@ -2,7 +2,7 @@ Validates that: - AdCPBaseModel defaults to extra='ignore' (forward-compatible) -- Generated types with additionalProperties: true override to extra='allow' +- Generated types with open or typed additionalProperties override to extra='allow' - Generated types with x-adcp-open-payload: true preserve open payload fields - Types without additionalProperties inherit ignore from base - Consumer subclasses can override extra policy freely @@ -20,11 +20,13 @@ from adcp._version import _read_packaged_version from adcp.types.base import AdCPBaseModel +from adcp.types.generated_poc.core.media_buy_features import MediaBuyFeatures from adcp.types.generated_poc.governance.check_governance_request import CheckGovernanceRequest from adcp.validation.version import resolve_bundle_key from scripts.post_generate_fixes import ( _ensure_configdict_import, _first_generated_class_name, + _inline_typed_extra_annotations, _open_payload_class_names, _set_class_extra_allow, ) @@ -64,7 +66,7 @@ class DefaultType(AdCPBaseModel): class TestGeneratedTypeOverrides: - """Generated types with additionalProperties: true override to extra='allow'.""" + """Generated types with additionalProperties override to extra='allow'.""" def test_allow_override_stores_extra_fields(self) -> None: class ExtensibleType(AdCPBaseModel): @@ -101,6 +103,14 @@ def test_open_payload_field_preserves_extension_data(self) -> None: assert obj.payload == payload + def test_typed_additional_properties_are_preserved_and_validated(self) -> None: + """Schema-valued additionalProperties retain their generated value type.""" + features = MediaBuyFeatures(seller_extension=True) + + assert features.seller_extension is True + with pytest.raises(ValidationError, match="bool_type"): + MediaBuyFeatures(seller_extension=[]) + def test_named_open_payload_schema_injects_extra_allow(self) -> None: """Named x-adcp-open-payload schemas get explicit generated model_config.""" schema = { @@ -162,6 +172,21 @@ def test_configdict_import_added_when_no_pydantic_import_exists(self) -> None: ) compile(updated, "", "exec") + def test_typed_extra_annotation_is_moved_into_model_class(self) -> None: + content = ( + "class FeatureFlags(AdCPBaseModel):\n" + " model_config = ConfigDict(extra='allow')\n\n\n" + "FeatureFlags.__annotations__['__pydantic_extra__'] = Dict[str, bool]\n" + "FeatureFlags.model_rebuild(force=True)\n" + ) + + updated, fixed = _inline_typed_extra_annotations(content) + + assert fixed == 1 + assert " __pydantic_extra__: Dict[str, bool]\n" in updated + assert "FeatureFlags.__annotations__" not in updated + assert "FeatureFlags.model_rebuild" not in updated + def test_titleless_root_fallback_skips_leading_enum(self) -> None: schema = {"type": "object", "x-adcp-open-payload": True} class_names, anonymous_count = _open_payload_class_names(schema) @@ -240,7 +265,8 @@ def _schema_allows_extra(obj: Any, all_schemas: dict[str, Any]) -> bool: return False if open_payload is True: return True - if obj.get("additionalProperties") is True: + additional_properties = obj.get("additionalProperties") + if additional_properties is True or isinstance(additional_properties, dict): return True # Follow $ref to check composed schemas if "$ref" in obj: @@ -319,6 +345,13 @@ def test_open_payload_true_allows_extra_even_without_additional_properties(self) """x-adcp-open-payload is an authoritative open-payload signal.""" assert self._schema_allows_extra({"x-adcp-open-payload": True}, {}) + def test_typed_additional_properties_allows_extra(self) -> None: + """A schema-valued additionalProperties keyword is an open object contract.""" + assert self._schema_allows_extra( + {"additionalProperties": {"type": "boolean"}}, + {}, + ) + def test_open_payload_false_wins_over_additional_properties(self) -> None: """x-adcp-open-payload false prevents accidental widening of structured objects.""" assert not self._schema_allows_extra( From b75a43886289426b847230f4ad8fe1a391399342 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 5 Sep 2026 20:02:01 +0000 Subject: [PATCH 2/2] fix(codegen): remove unused generated Field import --- scripts/post_generate_fixes.py | 53 +++++++++++++++++++ .../generated_poc/enums/signal_value_type.py | 4 +- tests/test_code_generation.py | 35 ++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py index 926a3b3c5..42958aa63 100644 --- a/scripts/post_generate_fixes.py +++ b/scripts/post_generate_fixes.py @@ -693,6 +693,58 @@ def fix_typed_additional_properties() -> None: ) +def _remove_unused_pydantic_field_import(source: str) -> tuple[str, bool]: + """Remove a generated ``Field`` import when the module never references it.""" + tree = ast.parse(source) + if any( + isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id == "Field" + for node in ast.walk(tree) + ): + return source, False + + lines = source.splitlines(keepends=True) + changed = False + for node in reversed(list(ast.walk(tree))): + if not isinstance(node, ast.ImportFrom) or node.module != "pydantic": + continue + if not any(alias.name == "Field" and alias.asname is None for alias in node.names): + continue + + remaining = [ + alias for alias in node.names if alias.name != "Field" or alias.asname is not None + ] + start = node.lineno - 1 + end = node.end_lineno or node.lineno + if remaining: + names = ", ".join( + alias.name if alias.asname is None else f"{alias.name} as {alias.asname}" + for alias in remaining + ) + newline = "\n" if lines[end - 1].endswith("\n") else "" + lines[start:end] = [f"from pydantic import {names}{newline}"] + else: + if start > 0 and not lines[start - 1].strip(): + start -= 1 + del lines[start:end] + changed = True + + return "".join(lines), changed + + +def remove_unused_pydantic_field_imports() -> None: + """Remove spurious ``Field`` imports emitted for generated enum modules.""" + modified_files = 0 + for py_path in OUTPUT_DIR.rglob("*.py"): + source = py_path.read_text() + updated, changed = _remove_unused_pydantic_field_import(source) + if not changed: + continue + py_path.write_text(updated) + modified_files += 1 + + print(f" Removed unused pydantic.Field imports from {modified_files} file(s)") + + def _find_indented_field_block(content: str, field_name: str) -> tuple[int, int] | None: """Return absolute offsets for a generated four-space field block.""" cursor = 0 @@ -5430,6 +5482,7 @@ def main(argv: list[str] | None = None): fix_update_rights_legacy_response_defaults, fix_list_creatives_format_reference_xor, rewrite_generated_enums_to_strenum, + remove_unused_pydantic_field_imports, strip_extra_blank_lines_at_eof, ] for fix in fixes: diff --git a/src/adcp/types/generated_poc/enums/signal_value_type.py b/src/adcp/types/generated_poc/enums/signal_value_type.py index 4cd32b23c..2fa0d1f83 100644 --- a/src/adcp/types/generated_poc/enums/signal_value_type.py +++ b/src/adcp/types/generated_poc/enums/signal_value_type.py @@ -1,13 +1,11 @@ # generated by datamodel-codegen: # filename: enums/signal_value_type.json -# timestamp: 2026-09-05T19:08:34+00:00 +# timestamp: 2026-09-05T19:58:21+00:00 from __future__ import annotations from adcp.types._str_enum import StrEnum -from pydantic import Field - class SignalValueType(StrEnum): binary = 'binary' diff --git a/tests/test_code_generation.py b/tests/test_code_generation.py index 4ac354b46..c7dd32978 100644 --- a/tests/test_code_generation.py +++ b/tests/test_code_generation.py @@ -131,6 +131,41 @@ def test_normalize_enum_descriptions_recurses_into_embedded_schemas(): assert schema["$defs"]["state"]["x-enum-descriptions"] == ["Available", "Finished"] +def test_post_generate_removes_only_unused_pydantic_field_imports(): + from scripts.post_generate_fixes import _remove_unused_pydantic_field_import + + unused = ( + "from pydantic import ConfigDict, Field\n\n" + "class Example:\n" + " model_config = ConfigDict(extra='forbid')\n" + ) + updated, changed = _remove_unused_pydantic_field_import(unused) + + assert changed + assert "from pydantic import ConfigDict\n" in updated + assert "Field" not in updated + assert _remove_unused_pydantic_field_import(updated) == (updated, False) + + used = "from pydantic import Field\n\nvalue = Field(default=None)\n" + assert _remove_unused_pydantic_field_import(used) == (used, False) + + standalone = ( + "from __future__ import annotations\n\n" + "from adcp.types._str_enum import StrEnum\n\n" + "from pydantic import Field\n\n\n" + "class Example(StrEnum):\n" + " value = 'value'\n" + ) + cleaned, changed = _remove_unused_pydantic_field_import(standalone) + assert changed + assert cleaned == ( + "from __future__ import annotations\n\n" + "from adcp.types._str_enum import StrEnum\n\n\n" + "class Example(StrEnum):\n" + " value = 'value'\n" + ) + + def test_flatten_validation_oneof_accepts_branch_annotations(): from scripts.generate_types import flatten_validation_oneof