diff --git a/scripts/post_generate_fixes.py b/scripts/post_generate_fixes.py
index da4a78876..7f32c089e 100644
--- a/scripts/post_generate_fixes.py
+++ b/scripts/post_generate_fixes.py
@@ -3984,6 +3984,7 @@ def __init__(self, relative: str, base: str, schema_rel: Path):
self.nested_names: set[str] = set()
self.local_ref_types: dict[str, str] = {}
self.root_schema: dict[str, Any] = {}
+ self.root_composes_protocol_envelope = False
self.needs_protocol_envelope = False
self.needs_media_buy_helpers = False
self.needs_sequence = False
@@ -4211,16 +4212,51 @@ def emit_nested(self, preferred: str, schema: dict[str, Any]) -> str:
self.nested.append("\n".join(lines))
return class_name
+ def composes_protocol_envelope(self, schema: dict[str, Any]) -> bool:
+ """Return whether ``schema`` composes ``core/protocol-envelope.json``.
+
+ The check walks the document's own ``allOf`` chain, including
+ nested ``allOf`` groupings, and resolves every ``$ref`` through
+ :func:`_resolve_schema_ref` so canonical ``https://`` refs,
+ root-relative ``/schemas/`` refs and ``../core/`` relative refs all
+ land on the same target.
+ """
+
+ parts = schema.get("allOf")
+ if not isinstance(parts, list):
+ return False
+ for part in parts:
+ if not isinstance(part, dict):
+ continue
+ ref = part.get("$ref")
+ if isinstance(ref, str):
+ try:
+ ref_rel = _resolve_schema_ref(self.schema_rel, ref)
+ except ValueError:
+ ref_rel = None
+ if ref_rel is not None and ref_rel.as_posix() == "core/protocol-envelope.json":
+ return True
+ if self.composes_protocol_envelope(part):
+ return True
+ return False
+
def emit_response_class(self, class_name: str, arm: dict[str, Any]) -> str:
props = arm.get("properties") or {}
required = set(arm.get("required") or [])
is_submitted = (
props.get("status", {}).get("const") == "submitted" and "task_id" in props
)
+ # A root-level ``allOf`` applies to the whole document, so every
+ # ``oneOf`` arm of a response whose root composes
+ # ``core/protocol-envelope.json`` carries the protocol envelope:
+ # success arms, error arms and submitted arms alike.
+ inherits_protocol_envelope = is_submitted or self.root_composes_protocol_envelope
bases = (
- "AdcpVersionEnvelope, ProtocolEnvelope" if is_submitted else "AdcpVersionEnvelope"
+ "AdcpVersionEnvelope, ProtocolEnvelope"
+ if inherits_protocol_envelope
+ else "AdcpVersionEnvelope"
)
- if is_submitted:
+ if inherits_protocol_envelope:
self.needs_protocol_envelope = True
lines = [f"class {class_name}({bases}):"]
if is_submitted:
@@ -4320,6 +4356,7 @@ def emit_response_class(self, class_name: str, arm: dict[str, Any]) -> str:
def render(self, schema: dict[str, Any]) -> str:
self.root_schema = schema
+ self.root_composes_protocol_envelope = self.composes_protocol_envelope(schema)
arms = schema.get("oneOf") or schema.get("anyOf") or []
if not arms:
arms = [schema]
diff --git a/src/adcp/types/canonical_creative.py b/src/adcp/types/canonical_creative.py
index 09e850165..d1c1e8a3b 100644
--- a/src/adcp/types/canonical_creative.py
+++ b/src/adcp/types/canonical_creative.py
@@ -46,6 +46,8 @@
from adcp.types.generated_poc.core.product import Product as _LegacyProduct
from adcp.types.generated_poc.core.product_filters import ProductFilters as _LegacyProductFilters
from adcp.types.generated_poc.core.product_format_declaration import SellerPreference
+from adcp.types.generated_poc.core.protocol_envelope import ProtocolEnvelope
+from adcp.types.generated_poc.core.version_envelope import AdcpVersionEnvelope
from adcp.types.generated_poc.creative.get_creative_delivery_response import (
Creative as _LegacyDeliveryCreative,
)
@@ -411,6 +413,32 @@ def _serialize_canonical_model(
)
+#: Protocol envelopes a generated wire model may compose at its schema root.
+#: A canonical clone copies the envelope *fields*, but a clone built on
+#: ``CanonicalBoundaryModel`` alone would drop the envelope *ancestry* — so
+#: ``issubclass(GetProductsResponse, ProtocolEnvelope)`` would be ``False`` even
+#: though the response carries ``status``/``replayed``/``task_id``. Re-declare
+#: the envelopes as additional bases so the canonical surface keeps the same
+#: ancestry as the generated surface it replaces.
+_ENVELOPE_BASES: tuple[type[AdCPBaseModel], ...] = (AdcpVersionEnvelope, ProtocolEnvelope)
+
+
+def _canonical_clone_bases(source: type[AdCPBaseModel]) -> tuple[type[AdCPBaseModel], ...]:
+ """Return the clone bases for ``source``: its envelopes, then the boundary.
+
+ ``CanonicalBoundaryModel`` comes last on purpose. Pydantic merges
+ ``model_config`` across bases left to right, so the right-most base wins;
+ the envelopes inherit :class:`AdCPBaseModel`'s ``extra`` policy and would
+ otherwise override the boundary's ``extra="allow"`` and start dropping
+ caller-supplied extension keys. Method resolution is unaffected — the
+ envelopes override nothing, so ``CanonicalBoundaryModel`` still supplies
+ ``model_dump``/``model_json_schema`` ahead of :class:`AdCPBaseModel`.
+ """
+
+ envelopes = tuple(envelope for envelope in _ENVELOPE_BASES if issubclass(source, envelope))
+ return (*envelopes, CanonicalBoundaryModel)
+
+
def _canonical_clone(
name: str,
source: type[AdCPBaseModel],
@@ -420,7 +448,7 @@ def _canonical_clone(
) -> type[CanonicalBoundaryModel]:
model = create_model( # type: ignore[call-overload]
name,
- __base__=CanonicalBoundaryModel,
+ __base__=_canonical_clone_bases(source),
__module__=__name__,
__validators__={
"_serialize_canonical": model_serializer(mode="wrap")(_serialize_canonical_model)
diff --git a/src/adcp/types/canonical_creative.pyi b/src/adcp/types/canonical_creative.pyi
index 4c049fe8c..3da56d7ff 100644
--- a/src/adcp/types/canonical_creative.pyi
+++ b/src/adcp/types/canonical_creative.pyi
@@ -4,6 +4,9 @@ from typing import Any, ClassVar, Literal, TypeAlias, TypeVar
from adcp.types.base import AdCPBaseModel
from adcp.types.generated_poc.core.canonical_format_kind import CanonicalFormatKind
+from adcp.types.generated_poc.core.protocol_envelope import ProtocolEnvelope
+from adcp.types.generated_poc.core.version_envelope import AdcpVersionEnvelope
+from adcp.types.generated_poc.enums.task_status import TaskStatus
from adcp.types.legacy import LegacyFormatId
_T = TypeVar("_T", bound=AdCPBaseModel)
@@ -11,6 +14,30 @@ _T = TypeVar("_T", bound=AdCPBaseModel)
class CanonicalBoundaryModel(AdCPBaseModel):
__adcp_canonical_creative_model__: ClassVar[bool]
+class _CanonicalResponseEnvelope(AdcpVersionEnvelope, ProtocolEnvelope, CanonicalBoundaryModel):
+ """Stub-only Liskov bridge for canonical responses; not a runtime class.
+
+ The canonical response clones inherit :class:`AdcpVersionEnvelope`,
+ :class:`ProtocolEnvelope` and :class:`CanonicalBoundaryModel` directly at
+ runtime, so ``isinstance``/``issubclass`` agree with this stub. Collapsing
+ them into one private ancestor exists purely for the type checker: several
+ schema arms pin an envelope field to a ``Literal``, and narrowing an
+ inherited mutable attribute is a Liskov violation. Relaxing the pinned
+ fields to ``Any`` once here lets each arm declare its precise literal with
+ no per-field suppression, and keeps plain-string construction
+ (``status="completed"``) working for adopters.
+
+ This is a bridge, not a public type. Every concrete response below
+ re-declares ``status`` with the exact annotation its runtime model carries,
+ so no adopter ever reads ``Any`` off one of them — enforced by
+ ``test_canonical_response_stub_status_matches_runtime``. The ``= ...``
+ matters as much as the type: ``status`` is defaulted on every runtime
+ response, so a bare ``status: Any`` would make the synthesized ``__init__``
+ demand it and reject a plain ``GetMediaBuysResponse(media_buys=[])``.
+ """
+
+ status: Any = ...
+
class Format(CanonicalBoundaryModel):
format_option_id: str | None
publisher_domain: str | None
@@ -70,7 +97,7 @@ class DeliveryCreative(CanonicalBoundaryModel):
class CreativeFilters(CanonicalBoundaryModel): ...
class ProductFilters(CanonicalBoundaryModel): ...
-class PackageRequest(CanonicalBoundaryModel):
+class PackageRequest(AdcpVersionEnvelope, CanonicalBoundaryModel):
product_id: str
format_option_refs: list[Any] | None
creatives: list[CreativeAsset] | None
@@ -93,7 +120,7 @@ class Package(CanonicalBoundaryModel):
**data: Any,
) -> None: ...
-class GetProductsRequest(CanonicalBoundaryModel):
+class GetProductsRequest(AdcpVersionEnvelope, CanonicalBoundaryModel):
account: Any
filters: ProductFilters | None
fields: Any
@@ -101,7 +128,8 @@ class GetProductsRequest(CanonicalBoundaryModel):
time_budget: Any
pagination: Any
-class GetProductsResponse(CanonicalBoundaryModel):
+class GetProductsResponse(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
products: list[Product] | None
proposals: Any
refinement_applied: Any
@@ -114,19 +142,23 @@ class GetProductsResponse(CanonicalBoundaryModel):
**data: Any,
) -> None: ...
-class CreateMediaBuyRequest(CanonicalBoundaryModel):
+class CreateMediaBuyRequest(AdcpVersionEnvelope, CanonicalBoundaryModel):
account: Any
packages: list[PackageRequest] | None
-class UpdateMediaBuyRequest(CanonicalBoundaryModel):
+class UpdateMediaBuyRequest(AdcpVersionEnvelope, CanonicalBoundaryModel):
account: Any
media_buy_id: str
packages: list[PackageUpdate] | None
new_packages: list[PackageRequest] | None
-class CreateMediaBuyResponse1(CanonicalBoundaryModel):
+class CreateMediaBuyResponse1(_CanonicalResponseEnvelope):
media_buy_id: str
packages: list[Package]
+ # AdCP 3.2 drops the synchronous task-envelope status from this arm, so the
+ # runtime model pins it to the single outcome and defaults it. Mirror both
+ # halves: the literal and the default.
+ status: Literal["completed"] = ...
# Required *and* nullable: the schema lists confirmed_at in the success
# branch's ``required`` while typing it ``["string", "null"]``. A buy
# awaiting seller commitment carries the key with a null value.
@@ -135,45 +167,56 @@ class CreateMediaBuyResponse1(CanonicalBoundaryModel):
self,
*,
media_buy_id: str,
- status: Any,
confirmed_at: datetime | None,
revision: int,
packages: list[Package],
+ status: Literal["completed"] = ...,
media_buy_status: Any = ...,
**data: Any,
) -> None: ...
-class CreateMediaBuyResponse2(CanonicalBoundaryModel): ...
-class CreateMediaBuyResponse3(CanonicalBoundaryModel): ...
+class CreateMediaBuyResponse2(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
+
+class CreateMediaBuyResponse3(_CanonicalResponseEnvelope):
+ status: Literal[TaskStatus.submitted] = ...
CreateMediaBuyResponse: TypeAlias = (
CreateMediaBuyResponse1 | CreateMediaBuyResponse2 | CreateMediaBuyResponse3
)
-class UpdateMediaBuyResponse1(CanonicalBoundaryModel):
+class UpdateMediaBuyResponse1(_CanonicalResponseEnvelope):
media_buy_id: str
- status: Literal["completed"]
+ # The 3.x schema arm pins ``status`` to the single synchronous outcome.
+ # ``_CanonicalResponseEnvelope`` is what makes this precise literal legal
+ # without a per-field suppression, and it keeps ``status="completed"``
+ # constructible — see tests/type_checks/extend_response_with_sequence.py.
+ status: Literal["completed"] = ...
revision: int
media_buy_status: Any = ...
affected_packages: Sequence[Package] | None = ...
-class UpdateMediaBuyResponse2(CanonicalBoundaryModel): ...
-class UpdateMediaBuyResponse3(CanonicalBoundaryModel): ...
+class UpdateMediaBuyResponse2(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
+
+class UpdateMediaBuyResponse3(_CanonicalResponseEnvelope):
+ status: Literal[TaskStatus.submitted] = ...
UpdateMediaBuyResponse: TypeAlias = (
UpdateMediaBuyResponse1 | UpdateMediaBuyResponse2 | UpdateMediaBuyResponse3
)
-class SyncCreativesRequest(CanonicalBoundaryModel):
+class SyncCreativesRequest(AdcpVersionEnvelope, CanonicalBoundaryModel):
account: Any
creatives: list[CreativeAsset]
-class ListCreativesRequest(CanonicalBoundaryModel):
+class ListCreativesRequest(AdcpVersionEnvelope, CanonicalBoundaryModel):
account: Any
filters: CreativeFilters | None
fields: Any
-class ListCreativesResponse(CanonicalBoundaryModel):
+class ListCreativesResponse(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
creatives: list[Creative]
class MediaBuyPackage(CanonicalBoundaryModel): ...
@@ -181,12 +224,15 @@ class MediaBuyPackage(CanonicalBoundaryModel): ...
class MediaBuy(CanonicalBoundaryModel):
packages: Sequence[MediaBuyPackage]
-class GetMediaBuysResponse(CanonicalBoundaryModel):
+class GetMediaBuysResponse(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
media_buys: Sequence[MediaBuy]
-class GetMediaBuyDeliveryResponse(CanonicalBoundaryModel): ...
+class GetMediaBuyDeliveryResponse(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
-class GetCreativeDeliveryResponse(CanonicalBoundaryModel):
+class GetCreativeDeliveryResponse(_CanonicalResponseEnvelope):
+ status: TaskStatus = ...
creatives: Sequence[DeliveryCreative]
PRIMARY_CANONICAL_MODELS: tuple[type[CanonicalBoundaryModel], ...]
diff --git a/src/adcp/types/generated_poc/account/get_account_financials_response.py b/src/adcp/types/generated_poc/account/get_account_financials_response.py
index 2d5f405d1..f000dd0e3 100644
--- a/src/adcp/types/generated_poc/account/get_account_financials_response.py
+++ b/src/adcp/types/generated_poc/account/get_account_financials_response.py
@@ -16,6 +16,7 @@
from ..core import date_range as date_range_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import payment_terms as payment_terms_1
@@ -54,7 +55,7 @@ class Invoice(AdcpVersionEnvelope):
paid_date: date | None = None
-class GetAccountFinancialsResponse1(AdcpVersionEnvelope):
+class GetAccountFinancialsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
account: account_ref_1.AccountReference
currency: Annotated[str, StringConstraints(pattern='^[A-Z]{3}$')]
@@ -70,7 +71,7 @@ class GetAccountFinancialsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class GetAccountFinancialsResponse2(AdcpVersionEnvelope):
+class GetAccountFinancialsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/account/sync_accounts_response.py b/src/adcp/types/generated_poc/account/sync_accounts_response.py
index f8f28c406..d75c57dc2 100644
--- a/src/adcp/types/generated_poc/account/sync_accounts_response.py
+++ b/src/adcp/types/generated_poc/account/sync_accounts_response.py
@@ -20,6 +20,7 @@
from ..core import notification_config as notification_config_1
from ..core import operator_unit as operator_unit_1
from ..core import reporting_delivery_config_state as reporting_delivery_config_state_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import account_scope as account_scope_1
from ..enums import billing_party as billing_party_1
from ..enums import payment_terms as payment_terms_1
@@ -68,7 +69,7 @@ class Account(AdcpVersionEnvelope):
authorization: account_authorization_1.AccountAuthorization | None = None
-class SyncAccountsResponse1(AdcpVersionEnvelope):
+class SyncAccountsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
dry_run: bool | None = None
accounts: list[Account]
@@ -76,7 +77,7 @@ class SyncAccountsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class SyncAccountsResponse2(AdcpVersionEnvelope):
+class SyncAccountsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/brand/acquire_rights_response.py b/src/adcp/types/generated_poc/brand/acquire_rights_response.py
index a98ea84ab..825371fd7 100644
--- a/src/adcp/types/generated_poc/brand/acquire_rights_response.py
+++ b/src/adcp/types/generated_poc/brand/acquire_rights_response.py
@@ -15,6 +15,7 @@
from ..core import ext as ext_1
from ..core import generation_credential as generation_credential_1
from ..core import push_notification_config as push_notification_config_1
+from ..core.protocol_envelope import ProtocolEnvelope
class Disclosure(AdcpVersionEnvelope):
@@ -23,7 +24,7 @@ class Disclosure(AdcpVersionEnvelope):
text: str | None = None
-class AcquireRightsResponse1(AdcpVersionEnvelope):
+class AcquireRightsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
rights_id: str
rights_status: Literal['acquired'] = 'acquired'
@@ -39,7 +40,7 @@ class AcquireRightsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class AcquireRightsResponse2(AdcpVersionEnvelope):
+class AcquireRightsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
rights_id: str
rights_status: Literal['pending_approval'] = 'pending_approval'
@@ -50,7 +51,7 @@ class AcquireRightsResponse2(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class AcquireRightsResponse3(AdcpVersionEnvelope):
+class AcquireRightsResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
rights_id: str
rights_status: Literal['rejected'] = 'rejected'
@@ -61,7 +62,7 @@ class AcquireRightsResponse3(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class AcquireRightsResponse4(AdcpVersionEnvelope):
+class AcquireRightsResponse4(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/brand/get_brand_identity_response.py b/src/adcp/types/generated_poc/brand/get_brand_identity_response.py
index ff165d2d7..69ccf26d0 100644
--- a/src/adcp/types/generated_poc/brand/get_brand_identity_response.py
+++ b/src/adcp/types/generated_poc/brand/get_brand_identity_response.py
@@ -12,6 +12,7 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import asset_content_type as asset_content_type_1
from ..enums import right_use as right_use_1
@@ -105,7 +106,7 @@ class Rights(AdcpVersionEnvelope):
content_restrictions: list[str] | None = None
-class GetBrandIdentityResponse1(AdcpVersionEnvelope):
+class GetBrandIdentityResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
brand_id: str
house: House
@@ -127,7 +128,7 @@ class GetBrandIdentityResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class GetBrandIdentityResponse2(AdcpVersionEnvelope):
+class GetBrandIdentityResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/brand/get_rights_response.py b/src/adcp/types/generated_poc/brand/get_rights_response.py
index 2b01adace..f949927e9 100644
--- a/src/adcp/types/generated_poc/brand/get_rights_response.py
+++ b/src/adcp/types/generated_poc/brand/get_rights_response.py
@@ -13,6 +13,7 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import right_type as right_type_1
from ..enums import right_use as right_use_1
@@ -55,7 +56,7 @@ class Excluded(AdcpVersionEnvelope):
suggestions: list[str] | None = None
-class GetRightsResponse1(AdcpVersionEnvelope):
+class GetRightsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
rights: list[Right]
excluded: list[Excluded] | None = None
@@ -63,7 +64,7 @@ class GetRightsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class GetRightsResponse2(AdcpVersionEnvelope):
+class GetRightsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/brand/update_rights_response.py b/src/adcp/types/generated_poc/brand/update_rights_response.py
index 69ed76006..7678cdef9 100644
--- a/src/adcp/types/generated_poc/brand/update_rights_response.py
+++ b/src/adcp/types/generated_poc/brand/update_rights_response.py
@@ -14,9 +14,10 @@
from ..core import error as error_1
from ..core import ext as ext_1
from ..core import generation_credential as generation_credential_1
+from ..core.protocol_envelope import ProtocolEnvelope
-class UpdateRightsResponse1(AdcpVersionEnvelope):
+class UpdateRightsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
rights_id: str
terms: rights_terms_1.RightsTerms
@@ -30,7 +31,7 @@ class UpdateRightsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class UpdateRightsResponse2(AdcpVersionEnvelope):
+class UpdateRightsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py b/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py
index 669bfc5f0..c0a3c7623 100644
--- a/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py
+++ b/src/adcp/types/generated_poc/content_standards/calibrate_content_response.py
@@ -12,6 +12,7 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import binary_verdict as binary_verdict_1
from ..enums import feature_check_status as feature_check_status_1
@@ -25,7 +26,7 @@ class Feature(AdcpVersionEnvelope):
confidence: Annotated[float, Field(ge=0, le=1)] | None = None
-class CalibrateContentResponse1(AdcpVersionEnvelope):
+class CalibrateContentResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
verdict: binary_verdict_1.BinaryVerdict
confidence: Annotated[float, Field(ge=0, le=1)] | None = None
@@ -35,7 +36,7 @@ class CalibrateContentResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class CalibrateContentResponse2(AdcpVersionEnvelope):
+class CalibrateContentResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: list[error_1.Error]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py b/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py
index 06b885e35..51020f83b 100644
--- a/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py
+++ b/src/adcp/types/generated_poc/content_standards/get_content_standards_response.py
@@ -12,15 +12,16 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
-class GetContentStandardsResponse1(AdcpVersionEnvelope):
+class GetContentStandardsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
context: context_1.ContextObject | None = None
ext: ext_1.ExtensionObject | None = None
-class GetContentStandardsResponse2(AdcpVersionEnvelope):
+class GetContentStandardsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: list[error_1.Error]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py b/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py
index 4639ef1f6..54d0504e5 100644
--- a/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py
+++ b/src/adcp/types/generated_poc/content_standards/get_media_buy_artifacts_response.py
@@ -14,6 +14,7 @@
from ..core import error as error_1
from ..core import ext as ext_1
from ..core import pagination_response as pagination_response_1
+from ..core.protocol_envelope import ProtocolEnvelope
class BrandContext(AdcpVersionEnvelope):
@@ -42,7 +43,7 @@ class CollectionInfo(AdcpVersionEnvelope):
effective_rate: float | None = None
-class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope):
+class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
media_buy_id: str
artifacts: list[Artifact]
@@ -52,7 +53,7 @@ class GetMediaBuyArtifactsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class GetMediaBuyArtifactsResponse2(AdcpVersionEnvelope):
+class GetMediaBuyArtifactsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: list[error_1.Error]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py b/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py
index 616211201..0c75fb0d5 100644
--- a/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py
+++ b/src/adcp/types/generated_poc/content_standards/validate_content_delivery_response.py
@@ -12,6 +12,7 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import binary_verdict as binary_verdict_1
from ..enums import feature_check_status as feature_check_status_1
@@ -39,7 +40,7 @@ class Result(AdcpVersionEnvelope):
features: list[Feature] | None = None
-class ValidateContentDeliveryResponse1(AdcpVersionEnvelope):
+class ValidateContentDeliveryResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
summary: Summary
results: list[Result]
@@ -47,7 +48,7 @@ class ValidateContentDeliveryResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class ValidateContentDeliveryResponse2(AdcpVersionEnvelope):
+class ValidateContentDeliveryResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: list[error_1.Error]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/creative/get_creative_features_response.py b/src/adcp/types/generated_poc/creative/get_creative_features_response.py
index 1bfe0665c..f4f1c0f68 100644
--- a/src/adcp/types/generated_poc/creative/get_creative_features_response.py
+++ b/src/adcp/types/generated_poc/creative/get_creative_features_response.py
@@ -15,9 +15,10 @@
from ..core import creative_consumption as creative_consumption_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
-class GetCreativeFeaturesResponse1(AdcpVersionEnvelope):
+class GetCreativeFeaturesResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
results: list[creative_feature_result_1.CreativeFeatureResult]
detail_url: AnyUrl | None = None
@@ -30,7 +31,7 @@ class GetCreativeFeaturesResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class GetCreativeFeaturesResponse2(AdcpVersionEnvelope):
+class GetCreativeFeaturesResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: list[error_1.Error]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/creative/preview_creative_response.py b/src/adcp/types/generated_poc/creative/preview_creative_response.py
index 1b5172672..c565a5f68 100644
--- a/src/adcp/types/generated_poc/creative/preview_creative_response.py
+++ b/src/adcp/types/generated_poc/creative/preview_creative_response.py
@@ -69,7 +69,7 @@ class Preview3(AdcpVersionEnvelope):
renders: Annotated[list[preview_render_1.PreviewRender], Field(min_length=1)]
-class PreviewCreativeResponse1(AdcpVersionEnvelope):
+class PreviewCreativeResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
response_type: Literal['single'] = 'single'
previews: Annotated[list[Preview], Field(min_length=1)]
@@ -80,7 +80,7 @@ class PreviewCreativeResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class PreviewCreativeResponse2(AdcpVersionEnvelope):
+class PreviewCreativeResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
response_type: Literal['batch'] = 'batch'
results: Annotated[list[Result], Field(min_length=1)]
@@ -88,7 +88,7 @@ class PreviewCreativeResponse2(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class PreviewCreativeResponse3(AdcpVersionEnvelope):
+class PreviewCreativeResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
response_type: Literal['variant'] = 'variant'
variant_id: str
diff --git a/src/adcp/types/generated_poc/creative/sync_creatives_response.py b/src/adcp/types/generated_poc/creative/sync_creatives_response.py
index c8ac62e48..cebca0004 100644
--- a/src/adcp/types/generated_poc/creative/sync_creatives_response.py
+++ b/src/adcp/types/generated_poc/creative/sync_creatives_response.py
@@ -41,7 +41,7 @@ class Creative(AdcpVersionEnvelope):
assignment_errors: dict[Annotated[str, StringConstraints(pattern='^[a-zA-Z0-9_-]+$')], str] | None = None
-class SyncCreativesResponse1(AdcpVersionEnvelope):
+class SyncCreativesResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
dry_run: bool | None = None
creatives: list[Creative]
@@ -50,7 +50,7 @@ class SyncCreativesResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class SyncCreativesResponse2(AdcpVersionEnvelope):
+class SyncCreativesResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/build_creative_response.py b/src/adcp/types/generated_poc/media_buy/build_creative_response.py
index f15b4f518..05c595c51 100644
--- a/src/adcp/types/generated_poc/media_buy/build_creative_response.py
+++ b/src/adcp/types/generated_poc/media_buy/build_creative_response.py
@@ -132,7 +132,7 @@ class Estimate(AdcpVersionEnvelope):
per_leaf: list[PerLeaf] | None = None
-class BuildCreativeResponse1(AdcpVersionEnvelope):
+class BuildCreativeResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
creative_manifest: creative_manifest_1.CreativeManifest
build_variant_id: str | None = None
@@ -149,14 +149,14 @@ class BuildCreativeResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class BuildCreativeResponse2(AdcpVersionEnvelope):
+class BuildCreativeResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
ext: ext_1.ExtensionObject | None = None
-class BuildCreativeResponse3(AdcpVersionEnvelope):
+class BuildCreativeResponse3(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
creative_manifests: Annotated[list[creative_manifest_1.CreativeManifest], Field(min_length=1)]
sandbox: bool | None = None
@@ -171,7 +171,7 @@ class BuildCreativeResponse3(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class BuildCreativeResponse4(AdcpVersionEnvelope):
+class BuildCreativeResponse4(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
creatives: Annotated[list[Creative], Field(min_length=1)]
items_total: Annotated[int, Field(ge=0)] | None = None
@@ -190,7 +190,7 @@ class BuildCreativeResponse4(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class BuildCreativeResponse5(AdcpVersionEnvelope):
+class BuildCreativeResponse5(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
mode: Literal['estimate'] = 'estimate'
estimate: Estimate
diff --git a/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py b/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py
index 5ad3e2564..7e94a7f3a 100644
--- a/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py
+++ b/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py
@@ -26,7 +26,7 @@
from adcp.types.media_buy_status_helpers import MEDIA_BUY_LEGACY_STATUS_VALUES, unwrap_enum_value
-class CreateMediaBuyResponse1(AdcpVersionEnvelope):
+class CreateMediaBuyResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
status: Literal['completed'] = 'completed'
proposal_id: Annotated[str, StringConstraints(min_length=1)] | None = None
@@ -77,7 +77,7 @@ def _normalize_legacy_status(cls, data: Any) -> Any:
return data
-class CreateMediaBuyResponse2(AdcpVersionEnvelope):
+class CreateMediaBuyResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/log_event_response.py b/src/adcp/types/generated_poc/media_buy/log_event_response.py
index e2119072d..6733e580f 100644
--- a/src/adcp/types/generated_poc/media_buy/log_event_response.py
+++ b/src/adcp/types/generated_poc/media_buy/log_event_response.py
@@ -12,6 +12,7 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
class PartialFailure(AdcpVersionEnvelope):
@@ -21,7 +22,7 @@ class PartialFailure(AdcpVersionEnvelope):
message: str
-class LogEventResponse1(AdcpVersionEnvelope):
+class LogEventResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
events_received: Annotated[int, Field(ge=0)]
events_processed: Annotated[int, Field(ge=0)]
@@ -33,7 +34,7 @@ class LogEventResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class LogEventResponse2(AdcpVersionEnvelope):
+class LogEventResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py b/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py
index fb80edf90..865af640b 100644
--- a/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py
+++ b/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py
@@ -12,9 +12,10 @@
from ..core import context as context_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
-class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope):
+class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
success: Literal[True]
feedback_id: Annotated[str, StringConstraints(min_length=1)] | None = None
@@ -27,7 +28,7 @@ class ProvidePerformanceFeedbackResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class ProvidePerformanceFeedbackResponse2(AdcpVersionEnvelope):
+class ProvidePerformanceFeedbackResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py b/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py
index a27287e78..1e1f47c8c 100644
--- a/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py
+++ b/src/adcp/types/generated_poc/media_buy/sync_audiences_response.py
@@ -55,7 +55,7 @@ class Audience(AdcpVersionEnvelope):
errors: list[error_1.Error] | None = None
-class SyncAudiencesResponse1(AdcpVersionEnvelope):
+class SyncAudiencesResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
audiences: list[Audience]
sandbox: bool | None = None
@@ -63,7 +63,7 @@ class SyncAudiencesResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class SyncAudiencesResponse2(AdcpVersionEnvelope):
+class SyncAudiencesResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py b/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py
index 39fb3ef7c..e9295ef8c 100644
--- a/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py
+++ b/src/adcp/types/generated_poc/media_buy/sync_catalogs_response.py
@@ -45,7 +45,7 @@ class Catalog(AdcpVersionEnvelope):
warnings: list[str] | None = None
-class SyncCatalogsResponse1(AdcpVersionEnvelope):
+class SyncCatalogsResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
status: Literal['completed'] | None = None
dry_run: bool | None = None
@@ -57,7 +57,7 @@ class SyncCatalogsResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class SyncCatalogsResponse2(AdcpVersionEnvelope):
+class SyncCatalogsResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[Any], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py b/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py
index 9331f2691..2a2294556 100644
--- a/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py
+++ b/src/adcp/types/generated_poc/media_buy/sync_event_sources_response.py
@@ -14,6 +14,7 @@
from ..core import event_source_health as event_source_health_1
from ..core import event_surface as event_surface_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
from ..enums import action_source as action_source_1
from ..enums import event_type as event_type_1
@@ -42,7 +43,7 @@ class EventSource(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class SyncEventSourcesResponse1(AdcpVersionEnvelope):
+class SyncEventSourcesResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
event_sources: list[EventSource]
sandbox: bool | None = None
@@ -50,7 +51,7 @@ class SyncEventSourcesResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class SyncEventSourcesResponse2(AdcpVersionEnvelope):
+class SyncEventSourcesResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py b/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py
index d4e42d09f..4880fea45 100644
--- a/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py
+++ b/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py
@@ -26,7 +26,7 @@
from adcp.types.media_buy_status_helpers import MEDIA_BUY_LEGACY_STATUS_VALUES, unwrap_enum_value
-class UpdateMediaBuyResponse1(AdcpVersionEnvelope):
+class UpdateMediaBuyResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
status: Literal['completed'] = 'completed'
media_buy_id: str
@@ -73,7 +73,7 @@ def _normalize_legacy_status(cls, data: Any) -> Any:
return data
-class UpdateMediaBuyResponse2(AdcpVersionEnvelope):
+class UpdateMediaBuyResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/src/adcp/types/generated_poc/signals/activate_signal_response.py b/src/adcp/types/generated_poc/signals/activate_signal_response.py
index 8c6ee7abb..4fc188c45 100644
--- a/src/adcp/types/generated_poc/signals/activate_signal_response.py
+++ b/src/adcp/types/generated_poc/signals/activate_signal_response.py
@@ -13,9 +13,10 @@
from ..core import deployment as deployment_1
from ..core import error as error_1
from ..core import ext as ext_1
+from ..core.protocol_envelope import ProtocolEnvelope
-class ActivateSignalResponse1(AdcpVersionEnvelope):
+class ActivateSignalResponse1(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
deployments: list[deployment_1.Deployment]
sandbox: bool | None = None
@@ -23,7 +24,7 @@ class ActivateSignalResponse1(AdcpVersionEnvelope):
ext: ext_1.ExtensionObject | None = None
-class ActivateSignalResponse2(AdcpVersionEnvelope):
+class ActivateSignalResponse2(AdcpVersionEnvelope, ProtocolEnvelope):
model_config = ConfigDict(extra='allow')
errors: Annotated[list[error_1.Error], Field(min_length=1)]
context: context_1.ContextObject | None = None
diff --git a/tests/test_code_generation.py b/tests/test_code_generation.py
index 4b31084f4..50acc8cad 100644
--- a/tests/test_code_generation.py
+++ b/tests/test_code_generation.py
@@ -1634,3 +1634,171 @@ def test_generated_create_media_buy_success_matches_schema_nullability():
)
assert ast.unparse(confirmed_at.annotation) == "AwareDatetime | None"
assert confirmed_at.value is None
+
+
+def _write_response_arm_fixture(
+ tmp_path,
+ monkeypatch,
+ *,
+ root_all_of: list[dict],
+ arms: list[dict],
+):
+ """Drive the response-arm emitter over a synthetic account response schema.
+
+ Only ``account/sync-accounts-response.json`` is materialized, so every other
+ entry in ``response_specs`` is skipped for missing schema/target.
+ """
+ import json
+
+ from scripts import post_generate_fixes
+
+ generated_dir = tmp_path / "generated_poc"
+ target = generated_dir / "account" / "sync_accounts_response.py"
+ target.parent.mkdir(parents=True)
+ target.write_text(
+ "# generated by datamodel-codegen:\n"
+ "# filename: account/sync_accounts_response.json\n\n"
+ "from __future__ import annotations\n\n"
+ "from ..core.version_envelope import AdcpVersionEnvelope\n\n\n"
+ "class SyncAccountsResponse(AdcpVersionEnvelope):\n"
+ " pass\n"
+ )
+
+ schema_dir = tmp_path / "schemas"
+ schema_path = schema_dir / "account" / "sync-accounts-response.json"
+ schema_path.parent.mkdir(parents=True)
+ schema_path.write_text(
+ json.dumps(
+ {
+ "title": "Sync Accounts Response",
+ "type": "object",
+ "allOf": root_all_of,
+ "oneOf": arms,
+ }
+ )
+ )
+
+ monkeypatch.setattr(post_generate_fixes, "OUTPUT_DIR", generated_dir)
+ monkeypatch.setattr(post_generate_fixes, "SCHEMA_DIR", schema_dir)
+ post_generate_fixes.restore_response_variant_aliases()
+ source = target.read_text()
+
+ # Regenerating from the same schema must be a no-op.
+ post_generate_fixes.restore_response_variant_aliases()
+ assert target.read_text() == source
+ compile(source, str(target), "exec")
+ return source
+
+
+def _arm_bases(source: str, class_name: str) -> list[str]:
+ import ast
+
+ node = next(
+ item
+ for item in ast.parse(source).body
+ if isinstance(item, ast.ClassDef) and item.name == class_name
+ )
+ return [ast.unparse(base) for base in node.bases]
+
+
+_SUCCESS_ARM = {
+ "type": "object",
+ "properties": {"accounts": {"type": "array", "items": {"type": "string"}}},
+ "required": ["accounts"],
+}
+_ERROR_ARM = {
+ "type": "object",
+ "properties": {"errors": {"type": "array", "items": {"type": "string"}}},
+ "required": ["errors"],
+}
+_SUBMITTED_ARM = {
+ "type": "object",
+ "properties": {
+ "status": {"const": "submitted"},
+ "task_id": {"type": "string"},
+ },
+ "required": ["status", "task_id"],
+}
+
+
+@pytest.mark.parametrize(
+ "protocol_envelope_ref",
+ [
+ "../core/protocol-envelope.json",
+ "/schemas/core/protocol-envelope.json",
+ "https://adcontextprotocol.org/schemas/3.2.0-rc.1/core/protocol-envelope.json",
+ ],
+ ids=["relative", "root-relative", "canonical-url"],
+)
+def test_root_all_of_protocol_envelope_reaches_every_one_of_arm(
+ tmp_path, monkeypatch, protocol_envelope_ref
+):
+ """A root ``allOf`` applies to the whole document, so every arm inherits it.
+
+ Regression test for #1136: only the ``status: submitted`` arm used to pick
+ up ``ProtocolEnvelope``, leaving ``status``/``replayed``/``task_id`` untyped
+ on the success and error arms of the same response.
+ """
+ source = _write_response_arm_fixture(
+ tmp_path,
+ monkeypatch,
+ root_all_of=[
+ {"$ref": "../core/version-envelope.json"},
+ {"$ref": protocol_envelope_ref},
+ ],
+ arms=[_SUCCESS_ARM, _ERROR_ARM],
+ )
+
+ assert "from ..core.protocol_envelope import ProtocolEnvelope" in source
+ for class_name in ("SyncAccountsResponse1", "SyncAccountsResponse2"):
+ assert _arm_bases(source, class_name) == ["AdcpVersionEnvelope", "ProtocolEnvelope"]
+
+
+def test_nested_root_all_of_protocol_envelope_is_detected(tmp_path, monkeypatch):
+ """The root ``allOf`` walk descends into nested ``allOf`` groupings."""
+ source = _write_response_arm_fixture(
+ tmp_path,
+ monkeypatch,
+ root_all_of=[
+ {"allOf": [{"$ref": "../core/protocol-envelope.json"}]},
+ ],
+ arms=[_SUCCESS_ARM, _ERROR_ARM],
+ )
+
+ for class_name in ("SyncAccountsResponse1", "SyncAccountsResponse2"):
+ assert _arm_bases(source, class_name) == ["AdcpVersionEnvelope", "ProtocolEnvelope"]
+
+
+def test_arms_keep_version_envelope_only_without_root_protocol_envelope(tmp_path, monkeypatch):
+ """The base is attached from the root schema, not sprayed unconditionally.
+
+ A response whose root does not compose the protocol envelope keeps the
+ plain version envelope on its ordinary arms; the ``submitted`` arm still
+ gets ``ProtocolEnvelope`` from its own task-envelope shape.
+ """
+ source = _write_response_arm_fixture(
+ tmp_path,
+ monkeypatch,
+ root_all_of=[{"$ref": "../core/version-envelope.json"}],
+ arms=[_SUCCESS_ARM, _ERROR_ARM, _SUBMITTED_ARM],
+ )
+
+ assert _arm_bases(source, "SyncAccountsResponse1") == ["AdcpVersionEnvelope"]
+ assert _arm_bases(source, "SyncAccountsResponse2") == ["AdcpVersionEnvelope"]
+ assert _arm_bases(source, "SyncAccountsResponse3") == [
+ "AdcpVersionEnvelope",
+ "ProtocolEnvelope",
+ ]
+
+
+def test_unrelated_root_all_of_ref_does_not_attach_protocol_envelope(tmp_path, monkeypatch):
+ """Only ``core/protocol-envelope.json`` counts — not any ``core`` ref."""
+ source = _write_response_arm_fixture(
+ tmp_path,
+ monkeypatch,
+ root_all_of=[{"$ref": "../core/push-notification-config.json"}],
+ arms=[_SUCCESS_ARM],
+ )
+
+ assert _arm_bases(source, "SyncAccountsResponse1") == ["AdcpVersionEnvelope"]
+ assert "from ..core.protocol_envelope import ProtocolEnvelope" not in source
diff --git a/tests/test_decisioning_specialisms.py b/tests/test_decisioning_specialisms.py
index 50aef8b2b..4fadcd313 100644
--- a/tests/test_decisioning_specialisms.py
+++ b/tests/test_decisioning_specialisms.py
@@ -507,18 +507,34 @@ def test_build_creative_response_includes_submitted_arm() -> None:
"""The spec now includes the task-submitted arm in build_creative responses."""
import typing
- from adcp.types import LegacyBuildCreativeResponse
+ from adcp.types import GeneratedTaskStatus, LegacyBuildCreativeResponse
arms = typing.get_args(LegacyBuildCreativeResponse)
assert len(arms) > 0, "LegacyBuildCreativeResponse should be a Union of arms"
+
+ # The root ``allOf`` composes core/protocol-envelope.json, so ``task_id``
+ # and ``status`` are present on *every* arm. Mere presence therefore does
+ # not identify the submitted arm; the pinned ``Literal`` annotation and its
+ # matching default do.
+ assert all({"task_id", "status"}.issubset(arm.model_fields) for arm in arms)
+
submitted_arms = [
arm
for arm in arms
- if hasattr(arm, "model_fields")
- and {"task_id", "status"}.issubset(set(arm.model_fields.keys()))
+ if arm.model_fields["status"].annotation
+ == typing.Literal[GeneratedTaskStatus.submitted] # type: ignore[valid-type]
+ and arm.model_fields["status"].default is GeneratedTaskStatus.submitted
]
assert [arm.__name__ for arm in submitted_arms] == ["BuildCreativeResponse6"]
+ # Only the submitted arm's own schema branch makes ``task_id`` required;
+ # the inherited envelope field stays optional on the others.
+ submitted = submitted_arms[0]
+ assert submitted.model_fields["task_id"].is_required()
+ assert not any(
+ arm.model_fields["task_id"].is_required() for arm in arms if arm is not submitted
+ )
+
# ---- CreativeAdServerPlatform ----
diff --git a/tests/test_protocol_envelope_inheritance.py b/tests/test_protocol_envelope_inheritance.py
new file mode 100644
index 000000000..4b39b67ab
--- /dev/null
+++ b/tests/test_protocol_envelope_inheritance.py
@@ -0,0 +1,325 @@
+"""Every response arm carries the root ``allOf`` protocol envelope (#1136).
+
+Each AdCP response schema composes ``core/protocol-envelope.json`` at its root
+via ``allOf``, so ``status``, ``task_id``, ``message``, ``context_id``,
+``replayed``, ``timestamp``, ``push_notification_config``,
+``governance_context``, ``context``, ``payload`` and ``adcp_error`` are part of
+every response's contract — on the success arm, the error arm and the submitted
+arm alike. The code generator used to attach :class:`ProtocolEnvelope` only to
+the ``status: submitted`` arm, leaving those fields untyped on the rest: a
+seller that set one wrote a pydantic extra and a buyer that read one got an
+``AttributeError``.
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Literal, get_args, get_origin
+
+import pytest
+from pydantic import BaseModel
+
+from adcp._version import _read_packaged_version
+from adcp.types import ProtocolEnvelope, canonical_creative
+from adcp.types import aliases as aliases_module
+from adcp.types.generated_poc.enums.task_status import TaskStatus
+from adcp.validation.version import resolve_bundle_key
+
+_ENVELOPE_FIELDS = frozenset(ProtocolEnvelope.model_fields)
+
+
+def _schema_dir() -> Path:
+ return Path("schemas") / "cache" / resolve_bundle_key(_read_packaged_version())
+
+
+def _public_response_classes() -> list[tuple[str, type[BaseModel]]]:
+ """Public ``*SuccessResponse`` / ``*ErrorResponse`` aliases that are classes.
+
+ Non-discriminated ``oneOf`` responses also expose bare union aliases, which
+ are ``types.UnionType`` rather than classes; those are out of scope here.
+ """
+ found: list[tuple[str, type[BaseModel]]] = []
+ for name in sorted(dir(aliases_module)):
+ if name.startswith("_") or not name.endswith(("SuccessResponse", "ErrorResponse")):
+ continue
+ obj = getattr(aliases_module, name)
+ if isinstance(obj, type) and issubclass(obj, BaseModel):
+ found.append((name, obj))
+ return found
+
+
+def test_public_response_aliases_inherit_protocol_envelope() -> None:
+ """The alias surface adopters import must carry the envelope."""
+ classes = _public_response_classes()
+ assert len(classes) > 20, "expected the full public response-alias surface"
+
+ missing = [name for name, obj in classes if not issubclass(obj, ProtocolEnvelope)]
+ assert missing == [], "response aliases missing the ProtocolEnvelope base: " + ", ".join(
+ missing
+ )
+
+
+def test_public_response_aliases_declare_every_envelope_field() -> None:
+ """Envelope fields are declared fields, never ``extra`` bags."""
+ for name, obj in _public_response_classes():
+ assert _ENVELOPE_FIELDS <= set(obj.model_fields), name
+
+
+@pytest.mark.parametrize(
+ "relative",
+ [
+ "account/get_account_financials_response.py",
+ "account/sync_accounts_response.py",
+ "brand/acquire_rights_response.py",
+ "brand/get_brand_identity_response.py",
+ "brand/get_rights_response.py",
+ "brand/update_rights_response.py",
+ "content_standards/calibrate_content_response.py",
+ "content_standards/get_content_standards_response.py",
+ "content_standards/get_media_buy_artifacts_response.py",
+ "content_standards/validate_content_delivery_response.py",
+ "creative/get_creative_features_response.py",
+ "creative/preview_creative_response.py",
+ "creative/sync_creatives_response.py",
+ "media_buy/build_creative_response.py",
+ "media_buy/create_media_buy_response.py",
+ "media_buy/log_event_response.py",
+ "media_buy/provide_performance_feedback_response.py",
+ "media_buy/sync_audiences_response.py",
+ "media_buy/sync_catalogs_response.py",
+ "media_buy/sync_event_sources_response.py",
+ "media_buy/update_media_buy_response.py",
+ "signals/activate_signal_response.py",
+ ],
+)
+def test_generated_arms_match_their_root_schema_composition(relative: str) -> None:
+ """Every emitted arm mirrors whether its ROOT schema composes the envelope."""
+ import importlib
+ import re
+
+ module_name = "adcp.types.generated_poc." + relative.removesuffix(".py").replace("/", ".")
+ module = importlib.import_module(module_name)
+
+ schema_path = _schema_dir() / Path(relative).with_suffix(".json").as_posix().replace("_", "-")
+ schema = json.loads(schema_path.read_text())
+ root_refs = [part.get("$ref", "") for part in schema.get("allOf", []) if isinstance(part, dict)]
+ root_composes_envelope = any(ref.endswith("core/protocol-envelope.json") for ref in root_refs)
+ assert root_composes_envelope, f"{relative}: fixture assumes a root protocol envelope"
+
+ # ``__all__`` is emitted as ``[union_alias, *numbered_arms, *nested]``; the
+ # arms are exactly the ```` names, never the nested helper models.
+ base_name = module.__all__[0]
+ arm_pattern = re.compile(rf"{re.escape(base_name)}\d+$")
+ arms = [
+ getattr(module, name) for name in module.__all__ if arm_pattern.fullmatch(name) is not None
+ ]
+ assert len(arms) == len(schema["oneOf"]), f"{relative}: arm count drifted from the schema"
+ for arm in arms:
+ assert issubclass(arm, ProtocolEnvelope), f"{relative}: {arm.__name__}"
+
+
+def test_error_arm_can_carry_envelope_state() -> None:
+ """The error arm of a ``oneOf`` is an envelope too, not a bare payload."""
+ from adcp.types.aliases import CreateMediaBuyErrorResponse
+
+ error = CreateMediaBuyErrorResponse.model_validate(
+ {
+ "errors": [{"code": "INVALID_BUDGET", "message": "too low"}],
+ "status": "rejected",
+ "task_id": "task_1",
+ "replayed": True,
+ }
+ )
+ assert error.status == "rejected"
+ assert error.task_id == "task_1"
+ assert error.replayed is True
+ assert error.model_extra == {}
+
+
+def test_replayed_and_status_round_trip_as_declared_fields() -> None:
+ """Idempotency rule 4: a replayed response sets ``replayed`` on the envelope."""
+ from adcp.types.aliases import SyncCreativesSuccessResponse
+
+ response = SyncCreativesSuccessResponse.model_validate({"creatives": []})
+ assert response.replayed is False
+ assert response.model_extra == {}
+
+ response.replayed = True
+ dumped = response.model_dump(mode="json")
+ assert dumped["replayed"] is True
+ # ``status`` is required on every task response envelope and now serializes
+ # from a declared field rather than being dropped.
+ assert dumped["status"] == "completed"
+
+ reparsed = SyncCreativesSuccessResponse.model_validate(dumped)
+ assert reparsed.replayed is True
+ assert reparsed.model_extra == {}
+
+
+def test_envelope_state_used_to_land_in_extra() -> None:
+ """Pin the exact regression: envelope keys are not ``extra`` any more."""
+ from adcp.types.aliases import LogEventSuccessResponse
+
+ response = LogEventSuccessResponse.model_validate(
+ {
+ "events_received": 0,
+ "events_processed": 0,
+ "status": "completed",
+ "replayed": True,
+ "context_id": "ctx_1",
+ }
+ )
+ assert response.model_extra == {}
+ assert response.context_id == "ctx_1"
+
+
+# ---- canonical_creative dynamic clones ----
+
+
+_CANONICAL_RESPONSE_CLONES = (
+ "GetProductsResponse",
+ "CreateMediaBuyResponse1",
+ "CreateMediaBuyResponse2",
+ "CreateMediaBuyResponse3",
+ "UpdateMediaBuyResponse1",
+ "UpdateMediaBuyResponse2",
+ "UpdateMediaBuyResponse3",
+ "ListCreativesResponse",
+ "GetMediaBuysResponse",
+ "GetMediaBuyDeliveryResponse",
+ "GetCreativeDeliveryResponse",
+)
+
+
+@pytest.mark.parametrize("name", _CANONICAL_RESPONSE_CLONES)
+def test_canonical_clone_preserves_envelope_ancestry(name: str) -> None:
+ """``_canonical_clone`` rebuilds fields; it must keep the ancestry too."""
+ model = getattr(canonical_creative, name)
+ assert issubclass(model, ProtocolEnvelope), name
+ assert _ENVELOPE_FIELDS <= set(model.model_fields), name
+
+
+@pytest.mark.parametrize("name", [*_CANONICAL_RESPONSE_CLONES, "GetProductsRequest", "Product"])
+def test_canonical_clone_keeps_the_boundary_extra_policy(name: str) -> None:
+ """The envelope bases must not override ``CanonicalBoundaryModel``'s config.
+
+ Pydantic merges ``model_config`` left to right across bases, so an envelope
+ listed after the boundary model would reinstate :class:`AdCPBaseModel`'s
+ ``extra="ignore"`` and silently drop caller-supplied extension keys.
+ """
+ model = getattr(canonical_creative, name)
+ assert model.model_config["extra"] == "allow", name
+
+
+def test_canonical_clone_still_round_trips_extension_keys() -> None:
+ """The config regression above is observable through an unknown key."""
+ request = canonical_creative.GetProductsRequest.model_validate(
+ {"promoted_offering": "test", "buying_mode": "brief"}
+ )
+ assert request.promoted_offering == "test"
+
+
+def test_canonical_response_keeps_its_schema_pinned_status() -> None:
+ """Adding the envelope base must not relax an arm's pinned ``status``."""
+ response = canonical_creative.UpdateMediaBuyResponse1.model_validate(
+ {"media_buy_id": "mb_1", "revision": 2}
+ )
+ assert response.status == "completed"
+ assert (
+ canonical_creative.UpdateMediaBuyResponse1.model_fields["status"].annotation
+ == Literal["completed"]
+ )
+
+
+def test_canonical_responses_construct_without_status() -> None:
+ """``status`` is defaulted at runtime, so construction must not demand it.
+
+ The static half of this — that the stub's synthesized ``__init__`` agrees —
+ is pinned in ``tests/type_checks/response_envelope_fields.py``.
+ """
+ listed = canonical_creative.ListCreativesResponse(
+ creatives=[],
+ query_summary={"total_matching": 0, "returned": 0},
+ pagination={"has_more": False},
+ )
+ assert listed.status == TaskStatus.completed
+
+ buys = canonical_creative.GetMediaBuysResponse(media_buys=[])
+ assert buys.status == TaskStatus.completed
+
+ accepted = canonical_creative.UpdateMediaBuyResponse3(task_id="task_2")
+ assert accepted.status == TaskStatus.submitted
+
+
+def _stub_status_annotation(runtime_annotation: object) -> str:
+ """Spell a runtime ``status`` annotation the way the stub must declare it."""
+ if runtime_annotation is TaskStatus:
+ return "TaskStatus"
+ if get_origin(runtime_annotation) is Literal:
+ (member,) = get_args(runtime_annotation)
+ if isinstance(member, TaskStatus):
+ return f"Literal[TaskStatus.{member.name}]"
+ return f"Literal['{member}']"
+ raise AssertionError(f"unhandled runtime status annotation: {runtime_annotation!r}")
+
+
+def test_canonical_response_stub_status_matches_runtime() -> None:
+ """Every concrete canonical response re-declares its exact ``status``.
+
+ ``_CanonicalResponseEnvelope`` relaxes ``status`` to ``Any`` so the arms'
+ ``Literal`` pins do not trip the Liskov check. That relaxation is a bridge,
+ not a public type: a concrete response that forgets to re-declare ``status``
+ silently hands adopters ``Any``, which type-checks against anything and
+ hides the very narrowing this module is about. Pin stub and runtime
+ together so the bridge cannot leak.
+ """
+ import ast
+
+ stub_path = Path(canonical_creative.__file__).with_suffix(".pyi")
+ stub = ast.parse(stub_path.read_text())
+
+ declared: dict[str, str] = {}
+ for node in stub.body:
+ if not isinstance(node, ast.ClassDef):
+ continue
+ if not any(
+ isinstance(base, ast.Name) and base.id == "_CanonicalResponseEnvelope"
+ for base in node.bases
+ ):
+ continue
+ status = next(
+ (
+ item
+ for item in node.body
+ if isinstance(item, ast.AnnAssign)
+ and isinstance(item.target, ast.Name)
+ and item.target.id == "status"
+ ),
+ None,
+ )
+ assert status is not None, (
+ f"{node.name} inherits the relaxed ``status: Any`` bridge without "
+ f"re-declaring its own — adopters would read ``Any``"
+ )
+ declared[node.name] = ast.unparse(status.annotation)
+
+ # Guard the other direction too: a newly added canonical response must show
+ # up in the stub rather than quietly skipping this check.
+ runtime_responses = {
+ name
+ for name in dir(canonical_creative)
+ if not name.startswith("_")
+ and isinstance(getattr(canonical_creative, name), type)
+ and issubclass(getattr(canonical_creative, name), ProtocolEnvelope)
+ # ``ProtocolEnvelope`` itself is imported into this namespace as a base;
+ # only the canonical clones built on it are in scope here.
+ and issubclass(getattr(canonical_creative, name), canonical_creative.CanonicalBoundaryModel)
+ }
+ assert declared.keys() == runtime_responses
+
+ for name, stub_annotation in sorted(declared.items()):
+ runtime = getattr(canonical_creative, name).model_fields["status"]
+ assert stub_annotation == _stub_status_annotation(runtime.annotation), name
+ # The stub marks the field defaulted (``= ...``); the runtime agrees.
+ assert not runtime.is_required(), name
diff --git a/tests/type_checks/response_envelope_fields.py b/tests/type_checks/response_envelope_fields.py
new file mode 100644
index 000000000..c05b25a03
--- /dev/null
+++ b/tests/type_checks/response_envelope_fields.py
@@ -0,0 +1,119 @@
+"""Adopter pattern: read and write protocol-envelope state on a response arm.
+
+Every AdCP response schema composes ``core/protocol-envelope.json`` at its
+root, so ``status``, ``task_id``, ``replayed`` and friends belong to every
+response arm — the success arm, the error arm and the submitted arm alike.
+Before #1136 the generator attached the envelope base only to the submitted
+arm, so a seller setting ``response.replayed = True`` on any of the other 19
+success shapes wrote a pydantic *extra*, and a buyer reading it got an
+``AttributeError``. Statically the attribute did not exist at all.
+
+This file pins the typed surface: the envelope fields must be visible to a type
+checker on ordinary success and error arms, on the canonical-boundary clones,
+and through ``ProtocolEnvelope`` as a common ancestor of the arms of one
+``oneOf`` — which previously had no shared base below ``AdcpVersionEnvelope``.
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+from typing_extensions import assert_type
+
+from adcp.types import (
+ CreateMediaBuyErrorResponse,
+ CreateMediaBuyResponse1,
+ GeneratedTaskStatus,
+ GetMediaBuysResponse,
+ GetProductsResponse,
+ ListCreativesResponse,
+ ProtocolEnvelope,
+ SyncCreativesResponse1,
+ UpdateMediaBuyResponse3,
+)
+
+
+def echo_envelope(response: ProtocolEnvelope) -> str | None:
+ """A boundary can take any response arm through the shared ancestor."""
+ return response.task_id
+
+
+# --- A plain success arm carries envelope state ---
+
+synced = SyncCreativesResponse1(creatives=[])
+synced.replayed = True
+synced.task_id = "task_1"
+synced.context_id = "ctx_1"
+
+replayed: bool | None = synced.replayed
+task_id: str | None = synced.task_id
+assert replayed is True
+assert task_id == "task_1"
+assert echo_envelope(synced) == "task_1"
+
+# --- The canonical-boundary clones keep their generated source's ancestry ---
+
+created = CreateMediaBuyResponse1(
+ media_buy_id="mb_1",
+ status="completed",
+ confirmed_at=None,
+ revision=1,
+ packages=[],
+)
+assert echo_envelope(created) is None
+
+# The error arm of the same ``oneOf`` is an envelope too, so both arms share a
+# base a seller can annotate against.
+rejected = CreateMediaBuyErrorResponse.model_validate(
+ {"errors": [{"code": "INVALID_BUDGET", "message": "too low"}]}
+)
+rejected.replayed = True
+assert echo_envelope(rejected) is None
+
+# --- ``status`` keeps its exact runtime type on every concrete response ---
+#
+# The stub ancestor relaxes ``status`` to ``Any`` purely to make the arms'
+# narrowing legal (see ``_CanonicalResponseEnvelope``). ``assert_type`` is what
+# proves that relaxation does not leak: an annotation like ``pinned: str =
+# created.status`` would pass silently from ``Any`` and prove nothing.
+
+# Pinned to the single synchronous outcome by the 3.2 schema arm.
+assert_type(created.status, Literal["completed"])
+
+# Ordinary arm — the wide envelope enum, no narrowing in the schema.
+products = GetProductsResponse()
+assert_type(products.status, GeneratedTaskStatus)
+
+# Async task-envelope arm — pinned to the submitted member.
+submitted = UpdateMediaBuyResponse3(task_id="task_1")
+assert_type(submitted.status, Literal[GeneratedTaskStatus.submitted])
+
+
+# The envelope's own view stays wide, which is what makes the arms' narrowing
+# a genuine refinement rather than a redefinition.
+def read_envelope_status(response: ProtocolEnvelope) -> GeneratedTaskStatus:
+ return response.status
+
+
+assert read_envelope_status(created) == "completed"
+
+# --- ``status`` is defaulted, so construction must not demand it ---
+#
+# ``status`` has a default on every runtime response. If the stub declared it
+# without ``= ...`` the synthesized ``__init__`` would require it, and these
+# three plain constructions would fail to type-check even though the runtime
+# model defaults the field. (``ListCreativesResponse`` also has required
+# ``query_summary``/``pagination`` fields the stub does not enumerate, so only
+# the ``status`` half of its signature is asserted here; the runtime
+# construction is covered in tests/test_protocol_envelope_inheritance.py.)
+
+listed = ListCreativesResponse(creatives=[])
+assert_type(listed.status, GeneratedTaskStatus)
+
+buys = GetMediaBuysResponse(media_buys=[])
+assert_type(buys.status, GeneratedTaskStatus)
+assert buys.status == "completed"
+
+accepted = UpdateMediaBuyResponse3(task_id="task_2")
+assert_type(accepted.status, Literal[GeneratedTaskStatus.submitted])
+assert accepted.status == "submitted"