Repository navigation
chore: merge main into next through 9.2.0 - #1578
Merged
Merged
Conversation
Match AdCP 3.2 by making the canonical SyncCreativesRequest.creatives field optional while retaining the nonempty constraint when supplied. The inherited validator still requires assets or assignments, and older-version schema validation continues to require creatives. Guard the reference seller's upload loop so existing creative assignments can proceed without uploading assets. Cover public Python/JSON parsing, typed dispatch, schema compatibility, canonical assets, and the example's upstream attachment behavior. Fixes #1545 BREAKING CHANGE: SyncCreativesRequest.creatives is now list[CreativeAsset] | None. Handlers must guard it before iteration or use request.creatives or [] for assignment-only requests.
…python-issue-1545 fix(types)!: allow assignment-only creative syncs
* feat(compat): negotiate and translate AdCP minor versions * fix(compat): harden negotiated version boundaries and regressions * docs(compat): clarify preflight failure results * fix(client): retain callback evidence and discovery failures * fix(a2a): isolate capability discovery from business sessions
…clares (#1518) `core/format-id.json` declares four parameters beyond the `(agent_url, id)` identity — `width`, `height`, `duration_ms` and `pixel_ratio`. Three helpers in `compat_helpers.py` each named three of them in a hand-written tuple, so none of them looked at `pixel_ratio`: - `format_is_supported` accepted a creative that claimed no pixel density for a product requiring 2x, and accepted one claiming a conflicting density; - `formats_are_equivalent` called two references with conflicting densities equal; - `_upgrade_legacy_format_id` discarded the `pixel_ratio` it was handed. Derive the set from the model's fields instead of listing it, so a parameter added to the schema is compared without anyone remembering to name it. A fourth literal would only reset the clock: an independent implementation of the same comparison hit this bug from its own hand-written list of the same three names. `pixel_ratio` keeps one deliberate per-field rule, because the schema gives it one: omitting it selects "the backward-compatible 1x variant", so an absent density reads as 1x rather than as unspecified. A product fixed at 2x therefore refuses a silent request while one fixed at 1x still serves it — without that, closing the gap would introduce the mirror-image false rejection. The new parametrized test reads the parameter list from the bundled schema rather than from the model, so it fails when a helper stops comparing something the spec declares. Closes #1517
…ves field (#1548) * fix(types): inherit the generated constraints on the canonical creatives field The canonical SyncCreativesRequest redeclares `creatives` to narrow the element type to CreativeAsset, and assigned a fresh Field() to do it. A fresh FieldInfo replaces the inherited one wholesale, so the generated field's max_length=100 and the bundle's description were lost: measured against the pinned 3.2 schema, the model published a node with neither maxItems nor description, and accepted 101 creatives where the schema declares maxItems: 100. That drop predates #1546 -- the line read `Field(min_length=1)` before it, with the same two pieces missing. `_inherit` in the same module exists for this case; its docstring warns that a bare Field() "would silently drop min_length and friends". The redeclaration now spells only the annotation it narrows and inherits everything else. What this does NOT do: it does not refuse an explicit JSON null for `creatives`. The generated parent renders the optional property as `list[Creative] | None`, so a null is admitted before any canonical redeclaration is consulted -- deleting the redeclaration outright leaves it accepted. That half is a generator-level rule, filed separately as #1547. Two tests: one grades the published node against the bundled node on type, description, minItems and maxItems; the other asserts the ceiling is enforced rather than advertised, on both the model and the JSON-Schema arm. * fix(types): inherit the generated creatives constraints on PackageUpdate too PackageUpdate redeclared creatives with only min_length=1, dropping the generated maxItems=100 and description, so it accepted 101 creatives that validate_request rejects. Use _inherit like SyncCreativesRequest, and restore the note on why SyncCreativesRequest.creatives is optional. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Brian O'Kelley <bokelley@scope3.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… they route by (#1501) * fix(types): generate the open asset unions, and derive the vocabulary they route by Closes #1486. `Format.assets` routed 14 of the 21 `asset_type` values the pinned schema declares, and the repeatable group's `assets` routed 12 of its 21. The missing ones did not fail: they matched the `_unknown` Tag and validated as `UnknownFormatAsset` / `UnknownGroupAsset`, so a consumer got an untyped object with the asset's fields in `__pydantic_extra__` where the schema promises a typed one -- while the generated class for every missing value sat in `domains/core/format.py`, unused. Format.assets missing card daast_tracker display_tag pixel_tracker published_post vast_tracker zip RepeatableAssetGroup.assets missing those, plus brief catalog The cause was a hand-written vocabulary beside a generated union. `aliases.py` declared both unions, both fallback classes, and two frozensets of 14 and 12 `asset_type` values; `_forward_compat._apply_forward_compat()` then rewrote `Format.assets` and the group's `assets` over the generated annotation at import time. The frozensets are what went stale, and they decide first: the callable `Discriminator` reads the frozenset and returns `_unknown` BEFORE pydantic reaches the arms, so adding arms would have changed routing not at all. `_forward_compat`'s own docstring used `pixel_tracker` as its EXAMPLE of a "novel asset_type"; by then the example was an instance of the defect. So the union is now emitted where its arms are declared. `_OPEN_ASSET_UNIONS` in `scripts/post_generate_fixes.py` writes, per slot, the fallback class, the `asset_type` vocabulary, the callable discriminator and the tagged union into `domains/core/format.py`, and repoints the carrying field. The generator knows each arm's `asset_type` literal because it emitted that literal, so nothing is declared and nothing can go stale: a schema that adds an asset type adds its class AND its tag in the same run. `_forward_compat`'s asset patch is deleted rather than rewired -- with the generated annotation correct there is nothing left to patch -- and `aliases.py` re-exports the four names instead of defining them. The arms keep their genuine asymmetry. Each union is derived from its own base class, because a group slot carries no nested `repeatable_group`. The group container is derived too, by its `item_type` discriminator rather than by name, for the reason `restore_format_asset_numbered_aliases` already derives it: the generator renumbers that class (`Assets94` once, `Assets29` now). `_unknown` is still an arm, so completeness costs none of the openness #744 bought: a value the schema does not declare still reaches the fallback. Six derived refusals, each fire-probed on a mutated copy of the generated module rather than on the schema, so none of them can pass vacuously: * an arm the derivation cannot see while the field still carries it, and an arm the field carries that the base does not own -- the membership check requires the field's current element union to resolve to exactly the arm set the alias was built from, and names the symmetric difference; * a duplicate `asset_type` across two arms (a `Tag` must be unique); * a carrying field that is renamed, or that stops being a list; * no class declaring `item_type='repeatable_group'`. Four tests in `tests/test_forward_compat_assets.py` asserted that `pixel_tracker` parses as `UnknownFormatAsset`. The obligation is right and the value was wrong -- they agreed with the stale frozenset rather than with the schema -- so the novel value is now derived from the generated classes: a hardcoded "novel" type can become declared by a schema refresh without the test noticing, which is how those four came to be wrong. A fifth restated the stale 14-value list as its own assertion and now reads the derivation. Added `TestDeclaredAssetTypesAreNotDegraded`, parametrized over the generated classes on both slots, asserting at the PARSE level that each declared type reaches its own class with no untyped extras -- plus one case comparing that population against the two arms of the pinned schema, read at their own depths and selected by `item_type` const, so a generator that stopped emitting a class would shrink the population loudly rather than grade fewer rows in silence. `adcp.types.UnknownFormatAsset` and `UnknownGroupAsset` now resolve to the classes in `domains.core.format` rather than the ones in `aliases`. The public name tracks the class the unions actually route to, as it did before, so `isinstance` against the public name is unaffected; `tests/fixtures/ public_api_snapshot.json` records the repoint and the gained arms. * fix(types): refuse a non-string asset_type instead of crashing the asset discriminator A list or dict asset_type raised TypeError (unhashable) from the frozenset membership check, which escapes a caller's except ValidationError. Route non-strings to the fallback arm so validation refuses them. Fixed in the post-generation template and the regenerated format module. Also add an adopter type-check fixture so FormatAssetUnion stays usable as a type expression under mypy and pyright. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Brian O'Kelley <bokelley@scope3.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…orted side too (#1553) * fix(canonical-formats): read an omitted pixel_ratio as 1x on the supported side too `format_is_supported` applied the implied-1x rule only to the requested reference. A supported 300x250 that omits `pixel_ratio` was skipped as "unfixed", so it accepted a 2x creative — the mirror image of the gap #1518 closed for a 2x product receiving a request that omits the field. `core/format-id.json` describes the omitted field as "the backward-compatible 1x variant" without naming a side, so read it the same way on both. The reading applies only to a dimensioned reference: `pixel_ratio` "Requires `width` and `height`", so a bare `display_image` template names no variant to be 1x of and stays broad. A legacy `display_300x250` upgrades to a dimensioned reference and is therefore the 1x variant. `formats_are_equivalent` keeps its documented loose contract (an omitted parameter is unspecified on either side), which is already symmetric. Refs #1518, #1517 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(canonical-formats): apply implied-1x pixel_ratio in formats_are_equivalent too `formats_are_equivalent` still treated an omitted `pixel_ratio` as unspecified, so it called 300x250 and 300x250 at 2x the same format while `format_is_supported` refused one for the other. An omitted width or duration names the template, a family covering every variant, so "unspecified" is right for those. An omitted density on a sized reference names one variant — the 1x render of a 300x250 image, not the 600x500 one — so read it as 1x here as well. A size-less template still has no density and stays broad. The schema sanitizer's legacy-tuple shape check listed the reference's properties by hand and missed `pixel_ratio`; derive the set from the model. Refs #1518, #1517 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ery reports (#1552) AdCP 3.3 (adcp#7895, closing adcp#7885) standardizes an optional reporting_webhook.operation_id with push_notification_config.operation_id's constraints: sellers echo it verbatim on every media_buy_delivery report and MUST NOT send reports to a registration that omits it. - Align the 3.2 ReportingWebhook.operation_id extension's description with the 3.3 semantics; pin constraint parity with PushNotificationConfig so the 9.x extension and the SDK 10 generated field agree. - Add create_media_buy_delivery_webhook_payload and WebhookSender.prepare_media_buy_delivery / send_media_buy_delivery, which build the envelope from the stored registration (URL, operation_id, token). A registration without operation_id raises ReportingWebhookOperationIdMissingError before any network I/O and logs a structured WARNING (adcp_event=reporting_webhook.operation_id_missing). - create_mcp_webhook_payload no longer defaults notification_id to "{task_id}.terminal" for media_buy_delivery: delivery reports are point-in-time events deduped by idempotency_key. - Docs: seller migration for existing registrations, buyer guidance. Refs #1092 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ack for 9.0 adopters (#1554) * feat(reporting): replace empty ledgers from sync migrations, and roll back the owned ledger Add replace_empty_legacy_reporting_sync for Alembic and other synchronous migration frameworks. It accepts a sync psycopg 3 or psycopg2 connection, or a SQLAlchemy Connection on either driver (op.get_bind()), and joins the caller's transaction in a savepoint, so the drop commits atomically with the revision's version bump and a refusal leaves the caller's transaction usable. It never starts an event loop. Add replace_empty_owned_reporting (async and sync), the reverse: under the same isolation check, schema advisory lock and ACCESS EXCLUSIVE table locks it drops an empty caller-owned ledger so an 8.x image can start again. Rows written by installation and startup (the unpruned change-feed horizon, row-storage bindings) do not count as state; a registered quarantine archive is refused. All four entry points share one statement generator, so the sync and async paths enforce identical guards. Functions that reference a reporting table or its row type are now dropped before the tables, and the rest after them, so CHECK constraints on packaged functions no longer block removal. The migration guide gains an Alembic recipe and an 8.x rollback section. Refs #1449, #1441 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(types): use Format to read and publish declarations, ProductFormatDeclaration to author them Stored declarations can predate the strict root-schema check, so grading them on every read turns data quality into failed product listings. Say plainly that Format is the lenient read and publish path and ProductFormatDeclaration the authoring check, and show how to report issues without failing the read. Refs #1449 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main CI has failed the conventional-commit policy on every push since 5ce9ee4, the GitHub merge commit for #1546. Its body carries only the PR title fix(types)!, while the merged commit 81c2a8b carries the BREAKING CHANGE footer, so the release signal is intact. Add the exact sha and subject to HISTORICAL_BREAKING_SUBJECTS, like the two earlier exceptions. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… forbids one (#1555) The generator rendered every optional property as `T | None = None`, which admitted an explicit JSON `null` the pinned schema rejects. A pre-generation pass now grades each optional property with the bundled validator and marks the ones that refuse `null`; the first post-generation fix turns the mark into `adcp.types.base.NoExplicitNull`, appended to the field's `Annotated` metadata. The annotation, default and attribute access are unchanged. `NoExplicitNull` is a `json_or_python` core schema: the JSON side is the type without its null arm, so `model_validate_json` refuses `null` with the type's own error and `model_json_schema()` publishes no null arm; the Python side keeps `T | None`, so `Model(field=None)` and `model_validate` on a dict still read `None` as "not supplied", symmetric with `model_dump`'s `exclude_none` default. `ADCP_STRICT_VALIDATION=1` refuses a Python `None` too. Properties whose schema admits `null` are generated without the marker. Also: the eight submitted response arms drop a leftover `validate_default=True`; the three literal-widening fixes carry the marker; the parity suite grades the model in JSON mode and no longer declares the explicit-null divergence. Fixes #1547 Refs #1522, #1414, #1409, #1397, #1359, #1362 BREAKING CHANGE: a JSON document carrying `null` for an optional property whose schema does not admit `null` is now refused by `model_validate_json` and `TypeAdapter.validate_json`, and the published JSON Schema of such a property has no null arm. A Python `None` is still accepted. JSON produced with `exclude_none=False` must be read back in Python mode (`model_validate(json.loads(...))`). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nge (#1563) * docs(review): require a pathway back for every 3.x wire change Adds the adcontextprotocol/adcp DR-0033 rule (and DR-0032's lossless-or-refuse translation bar) to PR review. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(review): apply the pathway rule to every major's minor releases Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1570) #1555 added NoExplicitNull to every generated optional, non-nullable property, including push_notification_config.operation_id. The 3.2 forward-compat reporting_webhook.operation_id from #1552 is pinned to that field's constraints and kept the old metadata, so the parity test failed on main. Add the marker to the extension, compare the published schema against the push field, and cover the explicit-null refusal. Also raise the Postgres core conformance lane timeout from 30 to 45 minutes; it runs 25-29 minutes on main and slow runners cancelled it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng-storage (#1557) Publish the PostgreSQL ledger store, the chunked row storage with its filesystem, GCS, S3 and Azure providers, the host change feed, period retirement, row migration, the caller-ownership migration helpers and the PostgreSQL service factory as adcp-reporting-storage, a second distribution in this repository with its own Release Please component. adcp keeps the store protocols, in-memory stores, producer, handlers, row encoding and digests, and never imports the new package. The root component excludes packages/adcp-reporting-storage so storage-only commits do not release adcp. Fixes #1510. Refs #1490, #1488, #1489, #1491. BREAKING CHANGE: the PostgreSQL reporting stores, the row storage providers, the change feed, the ownership-migration helpers and `ReliableReportingService.postgres` leave `adcp`; install `adcp-reporting-storage[pg]` (plus `gcs`/`s3`/`azure`) and import them from `adcp_reporting_storage`. The `adcp[reporting-gcs]`, `[reporting-s3]` and `[reporting-azure]` extras are removed. See docs/reporting-storage-package-migration.md.
Remove the prerelease keys from the root release-please package so the next adcp release on main is stable 9.0.0, flip the release-channel contract test, and update the releasing guide and README install notes. adcp-reporting-storage stays on its beta channel; SDK 10 prereleases continue on next. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Release-As: 9.0.0
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
Brings main through the SDK 9.0.0 GA release (1dcb846) into next: #1546 and #1548 creative-sync constraints, #1518 and #1553 format parameter comparisons, #1501 open asset unions, #1559 minor-version negotiation, #1550 reporting readiness, #1552 and #1570 reporting_webhook.operation_id, #1554 migration helpers, #1555 the NoExplicitNull codegen rule, #1557 the adcp-reporting-storage split, #1563, #1566, and #1567 with the 9.0.0 release. main's release files (release-please-config.json, its manifest, CHANGELOG.md, the pyproject.toml version) come along as on every sync; next's alpha config, manifest and changelog are unchanged in channel. Conflicts: - scripts/export_resolution.py: snapshot both next's signing and experimental identity namespaces and main's reporting, ledger and adcp_reporting_storage namespaces. - scripts/normalize_pyproject_prerelease.py: keep next's per-target-branch release lines and main's per-component pyproject mapping and duplicate refusal. main accepts the adcp and adcp-reporting-storage release PRs; next accepts only adcp's. - tests/fixtures/public_api_snapshot.json: regenerated; it equals an entry-level three-way merge of both sides. - tests/test_release_configuration.py: keep main's stable-channel and storage-component tests and next's alpha-channel test. Semantic fixes: - tests/test_wba_authorization_errors.py: #1559's typed client discovers the seller with get_adcp_capabilities before the first task, so next's fake MCP session (#1543) answers discovery the way main's #1559 fakes do. - tests/test_normalize_pyproject_prerelease.py: next refuses a storage release PR. adcp-reporting-storage on next: not released from next. next's config has no storage package and excludes its path from the root, the normalizer refuses a storage release PR into next, release-please-next.yml queues release-publish.yml with package=adcp, and release-publish.yml refuses package=adcp-reporting-storage outside main. The storage package's adcp range on next is >=9.0.0b4,<11: main's <10 would reject 10.0.0aN, so next's first alpha release PR could not install the workspace. RELEASING.md records this and adds a ratification step that narrows the range again. Generated code: scripts/generate_types.py on next's pinned schemas (still 3.2.4; next's 3.3 work is the separate identity preview) reproduces the merged src/adcp/types tree exactly, with no renumbering, and make validate-generated is clean. Next's generated ReportingWebhook does not declare operation_id, so main's forward-compat patch stays live; its existing guard retires it once a bundle declares the field (adcp#7895). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#1576) The test proves the pinned transport still validates the hostname; a certificate mismatch raises before any response. Asserting 200 made it fail whenever example.com answered 503, which happened twice this week on main and on #1557. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lit (#1577) adcp 9.0.0b4 predates the storage split and lacks the modules this package imports (adcp.reporting.ledger._record_rows, adcp.reporting.production.store, adcp.reporting.outbox.durable), so installing it with the b4 beta fails at import. Require adcp 9.0.0. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(main): release adcp-reporting-storage 0.1.0-beta.1 * chore: normalize prerelease version to PEP 440 --------- Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…#1579) PackageRequest.creatives redeclared with only min_length=1, dropping the generated maxItems=100 and description, so the model accepted 101 inline creatives that validate_request rejects. #1548 fixed the same override on SyncCreativesRequest and PackageUpdate; use _inherit here too and cover both package models. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…class postgres_reliable_reporting_service always constructed the base ReliableReportingService, while ReliableReportingService.memory builds cls. Hosts override the installed tool methods on a subclass, so the Embedded Sales Agent had to copy the factory body to keep its subclass. Add a service_class keyword, typed so the call returns the subclass. Fixes #1588 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The rc41-rc48 lockfiles under scripts/ci/reporting_interop/npm pin historical JavaScript SDK releases whose installed bytes the interop matrix verifies. A Dependabot bump (#1556) breaks those controls, so ignore every dependency in those directories. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1592) build_mcp_error_result and the A2A failed-task payload carried only the adcp_error envelope. AdCP's L3 error handling says a fatal failure SHOULD populate both layers, and storyboards such as media_buy_seller/product_signal_targeting read errors[0].field, so a seller raising AdcpError as documented failed those steps. Add errors: [adcp_error] on both transports. Fixes #1558 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(main): release adcp-reporting-storage 0.1.0-beta.2 * chore: normalize prerelease version to PEP 440 --------- Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
* chore(main): release adcp-reporting-storage 0.1.0-beta.3 * chore: normalize prerelease version to PEP 440 --------- Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…st (#1601) The IPR push policy maps every main commit to the PR whose merge commit it is. #1589 and #1595 were rebase-merged, so their first commits match no PR and main's IPR check has failed since. List those two exact commits with their PR numbers; authorship is still checked. Document squash-only merges in the releasing guide. #1591's test expected ext={'note': None} to dump as {} by default, but #1600 deliberately keeps nulls inside opaque ext and context objects. Expect the null. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
* chore(main): release adcp-reporting-storage 0.1.0-beta.4 * chore: normalize prerelease version to PEP 440 --------- Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com> Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
…all with (#1605) 9.0.0 moved the PostgreSQL reporting stores into adcp-reporting-storage before that package was on PyPI, so the documented install failed and Postgres adopters were stuck on the last beta. Before an adcp release builds, require each companion distribution to have a published, non-yanked release whose adcp requirement accepts the new version. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… summary (#1609) convert_payload logged one WARNING per dropped field on every conversion, on both sides of the wire. A 3.2 seller answering a 3.1 get_products with 50 products wrote 150 WARNING lines, three per product, for drops it must make and already records in ToolContext.metadata. convert_payload now takes loss_log_level (default WARNING) and every caller passes it explicitly: - server dispatch (request upgrade and response downgrade, MCP and A2A): DEBUG per loss, plus one INFO summary per conversion with tool, source -> target, loss count and distinct pointers with array indexes collapsed to *, capped at ten - client outgoing request and incoming response conversion: WARNING - account-key recomputation and catalog evidence reconstruction: DEBUG, since they repeat a conversion that already warned or round-trip the client's own lossless upgrade Embedded polling results are converted through the non-logging core, so their losses are logged once with the prefixed pointer instead of twice. ESA feedback on 9.0 version conversion. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1611) Since 9.0.0 (#1559) server dispatch upgrades an older buyer's request before the handler runs, so resolve_requested_adcp_version(params) inside a handler returns the served version ("3.2" for an explicit 3.1 claim). Adopters branching on it lost those branches silently. Document the behaviour as the breaking change it is and point handlers at ToolContext.resolved_adcp_version for the buyer's negotiated contract. - expand resolve_requested_adcp_version's docstring - rewrite the ToolContext.resolved_adcp_version docstring (wire contract, server pin, MCP 3.0 / A2A None for unversioned traffic, capabilities) - add a section to docs/cross-version-conversion.md and a row to MIGRATION_v8_to_v9.md - pin the documented contract in the existing server dispatch test ESA feedback on 9.0 version conversion. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…as import (#1626) The deprecated adcp.types.generated_poc.* alias registers the canonical adcp.types.domains.* module object under a second sys.modules key. _realize_lazy_exports filtered its snapshot on the sys.modules key but marked module.__name__ visited, so the alias key was never retired and the walk never terminated. Mark the key the snapshot filtered on and walk each module object once by identity, so an alias and its target are the same module. As a backstop the walk is capped at 100 passes (the SDK surface stabilizes in about 4); if it still finds newly imported modules after that it raises RuntimeError naming them instead of spinning. Fixes #1619 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
normalize_url(" https://interchange.io ") returned "interchange.io " because
urlparse drops leading whitespace but keeps trailing whitespace in the path,
so an adagents.json entry with a padded url did not match its agent. Strip
before parsing; _normalize_domain already does the same.
Signing paths keep byte-exact canonicalization: canonicalize_target_uri is a
signature-base canonicalizer, and the brand.json / adagents entry matches in
signing select keys or the signer identity and fail closed.
Fixes #1624
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t key (#1628) coerce_legacy_assets() / infer_asset_type() let an exact type-name key win over field presence, but the type names came from a hand-kept list of 14 that predated AdCP 3.2. An untyped asset under `display_tag` was inferred as text, url or image from its other keys. Derive the vocabulary from the generated AssetVariant union instead, so the key list is the 21 asset types the pinned schema declares (adds display_tag, zip, card, published_post, pixel_tracker, vast_tracker, daast_tracker) and a schema bump extends it in the same regeneration. Imported lazily on first use. Fixes #1624 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1630) * fix(compat): never downgrade into a payload the older schema refuses Projection dropped what the target release could not hold but never checked that the parent it left behind was still valid. A notification config whose event types were all 3.2-only kept event_types: [] below 3.1's minItems: 1, and a structured 3.2 geo_regions capability became {} where 3.1 allows only a boolean. When a node loses or coarsens a direct child, its own shape keywords (required, minItems, minProperties, unions, if/then) are now checked against the older schema. If the newer schema accepted the original and the older one refuses the remainder: - a boolean | object capability whose older contract is boolean maps to true when the structure declares any support (coarsened_to_boolean) - an optional field is omitted (unrepresentable_in_target) - a response array item is omitted (unrepresentable_in_target) - a request array item is never dropped: a shortened sync list can read as removal under delete_missing, so the nearest optional field holding it is omitted instead - a required chain to the root raises ADCPVersionConversionError at the refused value Every downgrade then validates against the older bundled schema and raises a structured error for anything still refused, such as a required enum member the older release cannot spell. Payloads already invalid in their own release are projected but neither repaired nor rejected. Also fixes a crash resolving $ref pointers that index into a list (#/oneOf/N, comply_test_controller). A new sweep converts every bundled 3.2 schema example, the canonical parity cases, the audience-evidence vectors and new fixtures down to 3.1 and 3.0. Before: 21 of 168 conversions failed (19 refused by the target schema, 2 crashes). After: 0; 10 are listed, intentional structured errors. Fixes #1621 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(compat): blame only drops that cause a refusal; never repair requests Review follow-ups: - A node is dropped only when the drop caused the refusal: the target must accept it with the dropped children restored. Otherwise the final check raises, so a 3.1 delivery row missing spend is not silently discarded just because a 3.2-only sibling was also dropped. - Requests are never repaired by omission: an absent filter widens the search and a shortened sync list can read as removal. They fail with a structured error instead. - The final check validates against the same portable schemas the projection used, without the validator's payload size cap. - Structured-error reasons are value-free; pointers from the final check locate the value in the converted payload. - Chained $ref nodes resolve fully for the shape checks. - convert_payload(require_valid_target=False) for evidence reconstruction, used by the negotiated catalog purchase round-trip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(compat): keep the final check on for status-less responses The SDK validators default a missing status envelope; the final check now does the same, so a direct caller converting a status-less 3.2 response still gets the guarantee. Also restore only removed (not coarsened) list items when attributing a refusal, and word false-schema refusals. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1633) PrincipalResolverError(403, code="identity_org_mismatch") answers 403 {"error": "identity_org_mismatch"} on MCP and A2A. Codes must match ^[a-z][a-z0-9_]{0,63}$; anything else falls back to the generic body. Exception text is never sent, and the default bodies are unchanged. Fixes #1623 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, unresolved references as REFERENCE_NOT_FOUND (#1631) installed_reporting_call projected STATUS_PROJECTION_UNAVAILABLE, HISTORY_UNAVAILABLE and LOOKUP_UNAVAILABLE as terminal. A pending status lifecycle and retained history the seller still owes resolve on the seller side, so they are transient. A reporting reference that does not resolve for the caller answers the standard, nondisclosing REFERENCE_NOT_FOUND (correctable) with a generic message. Fixes #1618 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…own wire error (#1632) _invoke_platform_method caught only AdcpError and pydantic ValidationError, so IdempotencyConflictError raised by IdempotencyStore.wrap reached the buyer as INTERNAL_ERROR / terminal. ADCPTaskError subclasses now keep their code and recovery through the same field extraction serve() uses, on the sync path and in handoff tasks. Fixes #1622 Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ration (#1625) Fixes #1620. Same class as #1587/#1610; a mechanical extension of #1548, #1579 and #1591. ## The defect Optional fields that `adcp.types.canonical_creative` redeclares did not carry the never-null marker (`_RefuseJsonNull` plus `exclude_if`) their generated counterparts do, so a nested dump through a plain Pydantic parent emitted `null` for them (`Product.placements`, `Placement.format_options`, `CreateMediaBuyRequest.packages`, `UpdateMediaBuyRequest.packages` / `new_packages`, `GetProductsResponse.products`, `GetProductsRequest.filters`, `ListCreativesRequest.filters`). Root cause: hand redeclarations that narrow the annotation used a bare `Field(...)` instead of inheriting the generated `FieldInfo`. The same redeclarations had also dropped descriptions and, on `CreateMediaBuyRequest.packages` / `UpdateMediaBuyRequest.packages`, the generated `min_length=1`. ## The fix - `adcp.types.base.inherit_field(source, name, *, required=False)` — the former `canonical_creative._inherit`, moved to `base` so every hand-written layer can use it; `required=True` drops the default for a subclass that makes an optional generated field mandatory (`Product.format_options`, `CreativeAsset.format_kind`, `Creative.format_kind`). `_inherit` remains as the alias `canonical_creative` has used since #1548. - Every redeclaration of a generated field now goes through it: 17 in `canonical_creative.py`, `capabilities.MediaBuy.reporting_delivery`, and `projections` (`AccountResponse.billing_entity`, `AccountResponse.notification_configs` — whose `max_length=16` is now inherited rather than restated — and `_NotificationConfigResponse.authentication`). - `_forward_compat.py` was checked: its only redeclarations (`adcp_version`, `adcp_major_version` on the readback models) already match their generated parent. ## Guard test `tests/test_redeclared_fields_inherit.py` imports every `adcp` module, finds every hand-written subclass of a generated model, and asserts each redeclared field keeps its generated parent's never-null marker, constraints, description and alias. Defaults may differ (narrowing to required is the point). Two exemptions, each with a reason: `BusinessEntityResponse.bank` and `_NotificationAuthenticationResponse.credentials` are write-only projections — excluded from every dump, any value refused — so the marker and the generated `min_length` have nothing to apply to. A stale exemption fails the suite. The issue's reproduction and the eight listed fields are tested directly. `make lint`, `make typecheck-all` pass; the related suites (canonical subclasses, capabilities, composability, account projections, public API) pass. Pre-commit cannot build its python3.11 hook environment in this sandbox, so the commit skipped hooks; black, ruff and mypy were run directly. Squash-merge only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- conductor-workspace-link --> --- [Open workspace in Conductor](https://app.conductor.build/pro-user-232ad008-36c5-45c0-b430-d68f028abd4b/w/851)
…ils so digests round-trip (#1617) AnyUrl, AwareDatetime, UUID and EmailStr rewrite what they parse (a trailing slash on a bare origin, +00:00 to Z, dropped .000), so a digest-covered object such as AudienceEvidence could not reproduce its own content_digest after parse and dump. Generated uri, date-time, uuid and email fields now use wire-preserving subclasses that keep the input text when it is strictly valid wire syntax naming the same host, and normalize otherwise. AudienceEvidence and its pin, selection and canonical variants are exported from adcp.types, and AdCPBaseModel.model_dump_as_received() dumps an object as received. Behaviour change: parsed URLs, datetimes, UUIDs and emails keep their received spelling in str() and JSON output (no added trailing slash, +00:00 kept). Runtime values are subclasses, so type(x) is datetime is false while isinstance holds, and str(url)-keyed caches see different spellings of the same URL as different. Strings that are not strict wire syntax are still normalized. The JSON Schema is unchanged. Fixes #1610 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
Forward-ports main through v9.2.0 (5314faa) into next with a true merge. Conflict: packages/adcp-reporting-storage/pyproject.toml takes main's adcp floor (>=9.0.0) and keeps next's <11 ceiling, per RELEASING.md. main's companion-release gate (#1605) now runs only for a publish from main; next cuts opt-in pre-releases and the storage distribution's adcp-10 release is cut from main at ratification. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the retired generated_poc alias test main's #1625 guard requires every hand-written redeclaration of a generated field to inherit it; next's experimental identity facades predate that rule. main's #1626 test imports adcp.types.generated_poc, which next removed in SDK 10 (#1536), so it skips when the alias is absent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Forward-ports
mainthrough the SDK 9.2.0 release (v9.2.0,5314faa9) intonextwith a true merge, the same way as #1541. Merge with "Create a merge commit", never squash or rebase.This PR was first opened for the 9.0.0 sync (
2e942f93, description below). It now carries two more merge commits on top:fde93379brings innext's #1549 and #1544, and52ad11b4mergesmainthrough 9.2.0 (2e942f93..5314faa9, 37 commits).What comes from main in the 9.1 / 9.2 refresh
9.1.0 (#1585), 9.1.1 (#1612) and 9.2.0 (#1627); the
adcp-reporting-storage0.1.0-beta.1 … beta.4 releases (#1571, #1593, #1596, #1603); the wire-fidelity work #1591 (unset optional fields stay off the wire from any parent), #1625 (every hand-written redeclaration inherits its generated field) and #1617 (wire bytes kept on parsed URLs, datetimes, UUIDs and emails, with the round-trip conformance suite); the companion-release publish gate #1605; the compat fixes #1609, #1611, #1628 and #1630 (never downgrade into a payload the older schema refuses); #1579, #1592, #1594, #1597, #1598, #1599, #1600, #1601, #1602, #1608, #1626, #1629, #1631, #1632, #1633, #1576, #1577, #1590 and the two rebase-merged storage commits.Conflicts and resolutions (this refresh)
packages/adcp-reporting-storage/pyproject.toml: the only conflict.mainmoved the storage floor toadcp>=9.0.0,<10(fix(reporting-storage): require the adcp release that contains the split #1577);nextcarries<11so the workspace installs oncenextcuts a 10.0.0 pre-release. Resolved toadcp>=9.0.0,<11, exactly as RELEASING.md prescribes ("take main's floor, keep<11").test_storage_distribution_pins_one_adcp_majoralready accepts the new floor on both lines.tests/fixtures/public_api_snapshot.json,scripts/export_resolution.pyandscripts/normalize_pyproject_prerelease.py, which conflicted in the 9.0.0 round.Decision:
main's companion-release gate (#1605) does not run for anextpublish#1605 makes
release-publish.ymlrefuse anadcprelease that no publishedadcp-reporting-storagerelease installs with. On PyPI the storage package pinsadcp>=9.0.0,<10, so the gate would refuse everynextpre-release (10.0.0-beta.N) until the storage package's adcp-10 release, which is cut frommainat ratification (RELEASING.md, "Ratification"). Anextpre-release is also opt-in (pip install --pre adcp), so it cannot strand a storage adopter the way 9.0.0 GA did. The step is now conditioned oninputs.package == 'adcp' && github.ref == 'refs/heads/main';test_release_please_keeps_next_on_the_alpha_channelpins that condition, and RELEASING.md records it under "Releasing fromnext".adcp-reporting-storageis otherwise still not released fromnext(decision below, unchanged).Release identity
next's release files are untouched:release-please-config.next.json(prerelease / alpha,CHANGELOG.next.md),.release-please-manifest.next.json.main'srelease-please-config.json,.release-please-manifest.json,CHANGELOG.md,packages/adcp-reporting-storage/CHANGELOG.mdand thepyproject.tomlversion lines come along as on every sync; onlymain's workflow reads them.Generated code and wire fidelity
next's wire pin is still AdCP 3.2.4 (src/adcp/ADCP_VERSION,schemas/cache), the same asmain. The 3.3 bundle adoption is the next PR.scripts/generate_types.pyon the merged tree reproducessrc/adcp/types/byte for byte: 1088 files, 0 changes, no renumber churn to discard.make validate-generatedis clean.base.py,_wire.pyand the generator's type map);tests/test_redeclared_fields_inherit.py,tests/test_wire_strings*.pyand thetests/conformance/_round_trip.pysuite all run onnextand pass.Validation (local, Python 3.12)
make lint,make typecheck-all(mypy, strict adopter fixtures, pyright, type-ignore contract),make validate-generated: pass.make testequivalent (pytest tests/ -n auto): see the CI checks on this PR for the authoritative run. Locally, onetests/integration/test_a2a_context_id.pycase failed once under xdist withUnknown skill: create_media_buyon its second call and passes in isolation and on rerun (a cross-worker server collision, not a merge regression).Original 9.0.0 description (
2e942f93)What comes from main
#1546 / #1548 creative-sync constraints, #1518 / #1553 format parameter comparisons, #1501 open asset unions, #1559 minor-version negotiation, #1550 reporting readiness and CI rerun fixes, #1552 / #1570
reporting_webhook.operation_id, #1554 migration helpers, #1555 the generator NoExplicitNull rule, #1557 theadcp-reporting-storagesplit, #1563, #1566, #1567, and the 9.0.0 release (#1540). #1576 is still open, so it is not included.main's own release files (
release-please-config.json,.release-please-manifest.json,CHANGELOG.md, thepyproject.tomlversion line) come along as on every sync, and only main's workflow reads them. next's release identity is unchanged:release-please-config.next.jsonstays onprerelease/alphawithCHANGELOG.next.md, and.release-please-manifest.next.jsonis untouched.Conflicts and resolutions
scripts/export_resolution.py: snapshots both next'sadcp.signingandadcp.types.experimental_identitynamespaces and main'sadcp.reporting,adcp.reporting.ledgerandadcp_reporting_storagenamespaces.scripts/normalize_pyproject_prerelease.py: keeps next's per-target-branch release lines and main's per-componentpyproject.tomlmapping and duplicate-PR refusal.mainaccepts theadcpandadcp-reporting-storagerelease PRs;nextaccepts onlyadcp's.tests/fixtures/public_api_snapshot.json: regenerated withscripts/regenerate_public_api_snapshot.py. I checked that it equals an entry-level three-way merge of base, next and main: 0 differences across 8333 names.tests/test_release_configuration.py: keeps main's stable-channel and storage-component tests and next's alpha-channel test, which now also pins the storage decision below.Semantic fixes
tests/test_wba_authorization_errors.py: with feat(compat): negotiate and translate AdCP minor versions #1559, the typed client discovers the seller withget_adcp_capabilitiesbefore the first task. next's fake MCP session (feat(identity): verify JWS artifacts on the issuer's Web Bot Auth key directory #1543) only answeredget_products, so 4 WBA cases failed after the merge. It now answers discovery the same way main's feat(compat): negotiate and translate AdCP minor versions #1559 test fakes do..github/workflows/release-please-next.yml:release-publish.ymlnow has a requiredpackageinput (feat(reporting)!: ship the Reliable Reporting storage as adcp-reporting-storage #1557), so next's dispatch passes-f package=adcpexplicitly.Decision:
adcp-reporting-storageis not released fromnextrelease-please-config.next.jsonhas no storage package. Its rootadcppackage gets main'sexclude-paths: ["packages/adcp-reporting-storage"], so storage-only commits stay out of the 10.0 alpha changelog. The channel keys are unchanged.next(new test case).release-publish.ymlrefusespackage=adcp-reporting-storageunless it runs frommain.adcp>=9.0.0b4,<10. Under PEP 440,<10excludes10.0.0aN, so next's first alpha release PR could not install the workspace (-e . -e ./packages/adcp-reporting-storage), and its CI and publish would fail. Onnextthe range isadcp>=9.0.0b4,<11.test_storage_distribution_pins_one_adcp_majorchecks<10on main and<11on next.RELEASING.mdrecords this under "Releasing fromnext", including how to resolve a future floor change from main (take main's floor, keep<11). It also adds a ratification step to narrow the range to adcp 10 and plan the storage package's adcp-10 release frommain.Generated code
src/adcp/ADCP_VERSION,schemas/cache), the same as main. next's 3.3 work is the separate identity preview (scripts/generate_identity_preview.py→src/adcp/types/_identity_preview.py).scripts/generate_types.pyon next's pinned schemas reproduces the mergedsrc/adcp/types/tree byte for byte: 1088 files, 0 changes, noClassNameN → ClassNameMrenumbering to discard.make validate-generatedis clean.datamodel-codegeninvocation and does not go through the fix(codegen)!: refuse a JSON null on optional properties whose schema forbids one #1555 NoExplicitNull pass.make validate-generateddoes not check it either. Applying NoExplicitNull there would change the experimental identity types' behaviour, so it belongs in its own PR.reporting_webhook.operation_id(#1552 / #1570)next's generated
ReportingWebhookdoes not declareoperation_id, because its wire schemas are 3.2.4. So the forward-compat patch stays live onnext, exactly as onmain. Its existingif "operation_id" not in ReportingWebhook.model_fieldsguard turns it into a no-op once a 3.3 bundle declares the field (adcp#7895). No change needed.Other overlap checked
src/adcp/reporting/**: next has no changes since the last sync, so feat(reporting)!: ship the Reliable Reporting storage as adcp-reporting-storage #1557's move intopackages/adcp-reporting-storageapplies cleanly.3.3version strings (wire pin 3.2.4). Negotiation runs before next's experimental identity response types, which still parse capability and principal responses.src/adcp/client.py,webhook_sender.py,protocols/base.py,types/__init__.py,ci.yml,ci_scope.py,check_main_policies.py: both sides' changes merged without overlap.Validation (local)
make lint,make typecheck-all(mypy, the strict adopter checks, pyright, the type-ignore contract), andmake validate-generated: pass.make test: before the two test fixes above, 17722 passed, 5 failed (the 4 WBA cases and the release-config test), 2672 skipped. After the fixes, both affected files pass (57 passed).pg-conformancecore lane (the CI command, throughscripts/reporting_test_harness.py) against a local PostgreSQL 16 UTF8 database: 2817 passed, 7 skipped.2e942f93.🤖 Generated with Claude Code
Open workspace in Conductor