Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ recursive-include src/adcp py.typed
recursive-include src/adcp/_schemas/2.5 *.json
recursive-include src/adcp/_schemas/3.0 *.json
recursive-include src/adcp/_schemas/3.1 *.json
recursive-include src/adcp/_schemas/3.2.0-beta.3 *.json
recursive-include src/adcp/_schemas/3.2.0-beta.4 *.json
prune src/adcp/_schemas/3.1.0-*
21 changes: 16 additions & 5 deletions MIGRATION_ADCP_3.1_TO_3.2.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Migrating an integration from AdCP 3.1 to 3.2 beta

Python SDK 8 beta supports the AdCP `3.2.0-beta.3` schemas and the compact
Python SDK 8 beta supports the AdCP `3.2.0-beta.4` schemas and the compact
product/media-buy lifecycle that becomes the foundation of AdCP 4.0. The SDK
continues to support AdCP 3.0 and 3.1, and the deprecated
`get_products`/`create_media_buy`/`update_media_buy` lifecycle remains available
Expand All @@ -16,16 +16,17 @@ from the version alone; read `media_buy.lifecycle_tools` or MCP `tools/list`.
Use the release-precision prerelease identifier while 3.2 is in beta:

```python
client = ADCPClient(agent, adcp_version="3.2-beta.3")
server = adcp_server("seller", adcp_version="3.2-beta.3")
client = ADCPClient(agent, adcp_version="3.2-beta.4")
server = adcp_server("seller", adcp_version="3.2-beta.4")
```

`"3.2"` intentionally does not alias to a prerelease. Exact prerelease pins
prevent a deployment from silently changing contracts when 3.2 stable ships.

## Beta.3 integration notes
## Beta.4 integration notes

AdCP 3.2.0-beta.3 adds placement presentation and delegated preview metadata.
AdCP 3.2.0-beta.4 retains beta.3 placement presentation and delegated preview
metadata and adds the signed products-only brief compatibility contract.
The SDK exports `PlacementPresentationDocument`,
`PlacementPresentationReference`, `PublisherDesignatedPreviewProvider`,
`PreviewRendererMetadata`, and `ReferenceRenderer` from `adcp` and
Expand All @@ -48,6 +49,16 @@ selected product-option sets with `CONFLICTING_SELECTORS` before applying
precedence. This comparison is product-aware application logic; JSON Schema's
Draft 7 validator cannot enforce selector equivalence by itself.

When an older seller returns products without a proposal, a 3.2 compatibility
layer may project `outcome: products_available` with either a real seller-fenced
`listed_purchase` or an explicitly lossy `legacy_create` continuation. Use
`adcp.compat.LegacyPurchaseCoordinator` for the latter. It binds the principal,
account, exact source patch version, original seller session, full observed
product/pricing transaction, selected products, and accepted losses before an
atomic single-use claim. See
[Durable legacy purchase continuations](docs/legacy-purchase-continuations.md)
for storage, reconciliation, and migration requirements.

## Choose the lifecycle subset

| Workflow | Compact tools |
Expand Down
2 changes: 1 addition & 1 deletion MIGRATION_v7_to_v8.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Migrating from Python SDK 7 to 8

SDK 8 beta also updates the generated protocol surface from AdCP 3.1.15 to
AdCP 3.2.0-beta.3 and adds the compact product/media-buy lifecycle. The old
AdCP 3.2.0-beta.4 and adds the compact product/media-buy lifecycle. The old
3.x lifecycle remains supported. See
[Migrating an integration from AdCP 3.1 to 3.2 beta](MIGRATION_ADCP_3.1_TO_3.2.md)
for lifecycle selection, capability declarations, and the compatibility test
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ async with ADCPMultiAgentClient(

## AdCP version support

The SDK 8 beta line is built against **AdCP 3.2.0-beta.3**, makes canonical
The SDK 8 beta line is built against **AdCP 3.2.0-beta.4**, makes canonical
creatives the primary Python contract, and negotiates AdCP 3.0, 3.1, and the
exact 3.2 beta wire dialect. The SDK package version and protocol version are
intentionally independent:
Expand All @@ -285,7 +285,7 @@ intentionally independent:
import adcp

adcp.get_adcp_sdk_version() # SDK package version, e.g. "8.0.0b1"
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.2.0-beta.3"
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.2.0-beta.4"
```

If you talk to an agent on a newer spec than this SDK validates, the response
Expand All @@ -301,6 +301,7 @@ forward traffic degrades gracefully rather than failing.
- **[Migrating from SDK 6 to 7](https://github.com/adcontextprotocol/adcp-client-python/blob/main/MIGRATION_v6_to_v7.md)** - Breaking API, security, concurrency, and webhook changes
- **[Migrating from SDK 7 to 8](https://github.com/adcontextprotocol/adcp-client-python/blob/main/MIGRATION_v7_to_v8.md)** - Secure webhook defaults and telemetry changes
- **[Migrating from AdCP 3.1 to 3.2 beta](MIGRATION_ADCP_3.1_TO_3.2.md)** - Compact lifecycle adoption and old/new compatibility matrix
- **[Durable legacy purchase continuations](docs/legacy-purchase-continuations.md)** - Safe products-only compatibility redemption and crash recovery
- **[Testing your AdCP server](docs/testing-your-adcp-server.md)** - In-process harness for unit tests plus storyboard-runner compliance grading
- **[Universal macro translation](docs/universal-macro-translation.md)** - Producer-side pixel URL translation, trust boundary, and diagnostics
- **[Multi-tenant contract](docs/multi-tenant-contract.md)** - Scope invariants every multi-tenant agent must satisfy
Expand Down
75 changes: 26 additions & 49 deletions SCHEMA_DELTAS.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,30 @@
# Generated-types delta
# Generated-types delta: AdCP 3.2.0-beta.3 → 3.2.0-beta.4

## Files added
## Added types

- `core/placement_presentation.py` — BoxDecoration, Canvas, Color, CreativeSlot, Fit, ImageDecoration, ImageRef, Layer, PlacementPresentationDocument, Rectangle, TextDecoration
- `core/presentation_ref.py` — PlacementPresentationReference
- `core/preview_provider.py` — PublisherDesignatedPreviewProvider, Route
- `core/preview_renderer_metadata.py` — PreviewRendererMetadata, RenderingOrigin
- `core/reference_renderer.py` — Provenance, ReferenceRenderer
- `core/forecast_dimension_time.py`: `TimeForecastDimension`
- `enums/availability_status.py`: `AvailabilityStatus`
- `media_buy/outcome_target.py`: metric/event goals and `OutcomeTarget`
- `media_buy/legacy_purchase_continuation_input.py`:
`CompatibilityPurchaseCoordinatorInput` and `AcceptedLoss`

## Field changes
## Field and enum changes

- `adagents.py`
- `AdcpAgentsAuthorization210`: `+catalog_role`
- `AdcpAgentsAuthorization211`: `+catalog_role`
- `AdcpAgentsAuthorization212`: `+catalog_role`
- `AdcpAgentsAuthorization213`: `+catalog_role`
- `AdcpAgentsAuthorization27`: `+catalog_role`
- `AdcpAgentsAuthorization28`: `+catalog_role`
- `AdcpAgentsAuthorization29`: `+catalog_role`
- `brand_discovery.py`
- `ImageAsset`: `+file_size_bytes`
- `bundled/protocol/get_adcp_capabilities_response.py`
- **classes added**: Preview, RenderingOrigin, Route
- `Creative`: `+preview`
- `Logo`: `+file_size_bytes`
- `Params7`: `+max_file_size_mb`
- `compliance/comply_test_controller_request.py`
- `Operation`: `+expire_proposal`, `+prepare`
- `Params`: `+proposal_id`
- `core/assets/asset_union.py`
- `ImageAsset`: `+file_size_bytes`
- `core/assets/image_asset.py`
- `ImageAsset`: `+file_size_bytes`
- `core/placement_definition.py`
- `PlacementDefinition`: `+presentation_ref`, `+preview_provider`
- `creative/preview_render.py`
- `PreviewRender1`: `+renderer`
- `PreviewRender2`: `+renderer`
- `PreviewRender3`: `+renderer`
- `enums/error_code.py`
- `ErrorCode`: `+CONFLICTING_SELECTORS`
- `formats/canonical/audio_hosted.py`
- `CanonicalFormatHostedAudio`: `+max_file_size_mb`
- `media_buy/accept_proposal_request.py`
- `AcceptProposalRequest`: `+adcp_major_version`
- `media_buy/buy_products_request.py`
- `BuyProductsRequest`: `+adcp_major_version`
- `media_buy/control_media_buy_request.py`
- `ControlMediaBuyRequest`: `+adcp_major_version`
- `protocol/get_adcp_capabilities_response.py`
- **classes added**: Preview, RenderingOrigin, Route
- `Creative`: `+preview`
- `protocol/get_adcp_capabilities_response.py`: media-buy capabilities add
`availability_horizon` and `outcome_target`.
- `core/forecast_point.py` and `core/canonical_forecast_point.py`: forecast
points add `availability_status`; dimensions now accept time windows.
- `core/product_offer_filters.py`: offer filters add `availability_horizon`.
- `media_buy/product_discovery_criteria.py`: criteria add `outcome_target`.
- `media_buy/request_proposals_response.py`: adds the
`products_available` outcome, partial-result `incomplete` metadata, and
listed/legacy purchase continuations.
- `media_buy/control_media_buy_request.py`: adds mutable display `name`.
- `core/canonical_media_buy_action.py` and
`enums/media_buy_valid_action.py`: add `update_name`.
- `core/targeting_overlay_support.py`: country include/exclude support may
advertise `max_values_per_package`; proximity support adds the same limit.
- `extensions/extension_meta.py`: extension metadata requires `$id`.
- `core/assets/card_asset.py`: card provenance resolves to the canonical
provenance model.
- `media_buy/package_update.py`: package cancellation precedence is clarified.
159 changes: 159 additions & 0 deletions docs/legacy-purchase-continuations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Durable legacy purchase continuations

AdCP 3.2 can project an established 2.5, 3.0, or 3.1 products-only brief
response as `outcome: products_available`. A `legacy_create` continuation is a
lossy bridge back to that seller's `create_media_buy`; it is not a proposal,
seller feed fence, or reusable credential.

The Python coordinator validates the SDK-local beta.4
`CompatibilityPurchaseCoordinatorInput`, binds it to the complete discovery
transaction, atomically claims the token, and stores the result for exact
replay. The coordinator input must never be sent to an AdCP seller.

## Configure a durable coordinator

```python
from adcp.compat import (
LegacyPurchaseCoordinator,
ReconciliationResult,
SqliteCompatibilityContinuationStore,
)

store = SqliteCompatibilityContinuationStore("state/adcp-continuations.sqlite3")

async def execute_legacy_purchase(execution):
# Route using execution.target_binding to the same authenticated seller
# connection/account session used for discovery. Authorize create_media_buy
# here; authority from request_proposals does not transfer.
return await legacy_client.call_tool(
"create_media_buy",
execution.legacy_create_request,
adcp_version=execution.source_adcp_version,
)

async def reconcile_legacy_purchase(execution, operation):
result = await lookup_by_original_transaction_identity(execution)
if result.found:
return ReconciliationResult.applied(result.payload)
if result.authoritatively_absent:
return ReconciliationResult.not_applied()
return ReconciliationResult.ambiguous()

coordinator = LegacyPurchaseCoordinator(
store=store,
executor=execute_legacy_purchase,
reconciler=reconcile_legacy_purchase,
)
```

At projection time, persist every binding and return the generated opaque
token in `purchase_continuation`:

```python
token = await coordinator.issue_legacy_create_continuation(
principal_id=authenticated_principal,
account=account,
source_adcp_version="3.1.15", # exact negotiated patch release
expires_at=expires_at,
observed_request=complete_get_products_request,
observed_response=complete_get_products_response,
product_ids=[product["product_id"] for product in products],
losses=["feed_version_not_atomic", "pricing_version_not_atomic"],
target_binding=stable_seller_session_id,
)
```

Redeem only after the buyer explicitly accepts the exact loss set:

```python
result = await coordinator.continue_legacy_purchase(
compatibility_input,
principal_id=authenticated_principal,
target_binding=stable_seller_session_id,
)
```

`InMemoryCompatibilityContinuationStore` is intentionally rejected by the
coordinator's production default. It can be enabled with
`allow_non_durable_store=True` for tests only. SQLite is safe across local
processes sharing one ordinary local filesystem. Distributed deployments
should implement `CompatibilityContinuationStore` on their transactional
database and preserve the same atomic state transitions.

The SQLite ledger is created with mode `0600` and an existing file with group
or other access is rejected. This is access control, not encryption; use an
encrypted volume or an application-owned encrypted store when payloads require
encryption at rest. `purge_resolved_before(cutoff)` removes only old succeeded
and never-claimed continuations. It deliberately retains claimed, `in_flight`,
and `ambiguous` operations regardless of age.

## What the application owns

The SDK cannot infer security or commercial identity. The application must:

- derive `principal_id` from authenticated state, never a request-body claim,
and make it globally unambiguous by binding issuer, tenant, and subject;
- preserve the original account and seller target/session, especially for 2.5,
whose wire request has no account field;
- encrypt sensitive stored discovery payloads at rest, set a state-aware
retention policy, and restrict ledger access. Never purge unresolved
`in_flight` or `ambiguous` operations automatically;
- authorize the actual `create_media_buy` call and select its credentials;
- implement authoritative reconciliation using a seller transaction identity;
- keep the exact negotiated patch version and full observed product/pricing
payload until expiry and reconciliation retention have elapsed.

The opaque token is generated with at least 128 bits of randomness and only its
SHA-256 hash is stored. A principal mismatch is reported as not found to avoid
cross-tenant token enumeration. Natural account comparison excludes mutable
display metadata such as `operator_unit.name` but includes the account's actual
natural key.

## Claim and crash behavior

The durable operation ledger moves through:

```text
claimed -> in_flight -> succeeded
\-> ambiguous
```

The token is consumed when the first seller mutation is reserved. Exact
`(principal, idempotency_key, full logical payload)` retries replay the stored
result. Reusing the key with changed input conflicts, and another operation
cannot claim the token.

An exception, timeout, or cancellation observed by the coordinator after
`in_flight` is marked `ambiguous`. A hard process loss leaves the durable row
`in_flight`; recovery remains closed until the application fences the old
executor and explicitly transitions that row to `ambiguous`. The SDK never
reopens the token by elapsed time and never blindly resends the legacy request.
A reconciler may then prove that the mutation was applied and supply its
result, or prove it was not applied and allow the same durable operation to
resume. An inconclusive or absent reconciler raises
`CompatibilityContinuationError` with code `ambiguous_legacy_mutation` and an
`operation_id` for operators.

## Validation before mutation

Before the atomic claim, the coordinator rejects expiry, principal/account or
target rebinding, product substitution, package-set drift, duplicate selected
IDs, stale/partial/excess loss consent, and source-schema violations. The
nested request is validated against the exact source-version
`create-media-buy-request` schema and must use explicit packages. AdCP 2.5
continuations must also declare `mutation_idempotency_not_guaranteed`.

`listed_purchase` is different: it is executable only with a real
account-scoped seller feed and unchanged seller-issued `feed_version` and
optional `pricing_version` fences. This legacy-create coordinator does not
fabricate those fences or redeem listed purchases.

## Reverse compatibility facade

A 3.2 seller may keep genuine legacy `get_products` and `create_media_buy`
facades for established buyers. If those facades are backed by compact tools,
the application must provide one private atomic transaction that preserves the
legacy product, price, and inline-creative contract. A `buy_products` followed
by creative synchronization is a non-atomic saga and must be rejected before
the facade is advertised. The SDK's pure legacy wire adapters do not expose a
reverse facade automatically.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ adcp = [
"_schemas/2.5/**/*.json",
"_schemas/3.0/**/*.json",
"_schemas/3.1/**/*.json",
"_schemas/3.2.0-beta.3/**/*.json",
"_schemas/3.2.0-beta.4/**/*.json",
# Vendored canonical-formats reference fixtures (14 v2 products +
# 50-entry v1 catalog) so :mod:`adcp.canonical_formats.fixtures`
# can serve them to adopter test suites without forcing each
Expand Down
Loading
Loading