Skip to content

Commit 384ad03

Browse files
authored
feat(protocol)!: add AdCP 3.2 beta.5 parity (#1064)
* feat(protocol): add AdCP 3.2 beta.5 parity * test(decisioning): reject loose submitted responses * fix(webhooks): bound processing claim leases
1 parent c70b06d commit 384ad03

1,497 files changed

Lines changed: 1913808 additions & 1699 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MANIFEST.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
include ADCP_VERSION
1+
include src/adcp/ADCP_VERSION
22
include README.md
33
include LICENSE
44
include MIGRATION*.md
@@ -10,5 +10,5 @@ recursive-include src/adcp py.typed
1010
recursive-include src/adcp/_schemas/2.5 *.json
1111
recursive-include src/adcp/_schemas/3.0 *.json
1212
recursive-include src/adcp/_schemas/3.1 *.json
13-
recursive-include src/adcp/_schemas/3.2.0-beta.4 *.json
13+
recursive-include src/adcp/_schemas/3.2.0-beta.5 *.json
1414
prune src/adcp/_schemas/3.1.0-*

README.md

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -269,14 +269,14 @@ async with ADCPMultiAgentClient(
269269
print(f"✅ Sync completion: {len(result.data.products)} products")
270270

271271
if result.status == "submitted":
272-
# Agent will send webhook when complete
272+
# Poll or let the seller's external durable publisher send the webhook.
273273
print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
274274
# Connections automatically cleaned up here
275275
```
276276

277277
## AdCP version support
278278

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

287287
adcp.get_adcp_sdk_version() # SDK package version, e.g. "8.0.0b1"
288-
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.2.0-beta.4"
288+
adcp.get_adcp_spec_version() # AdCP spec this build targets, e.g. "3.2.0-beta.5"
289289
```
290290

291291
If you talk to an agent on a newer spec than this SDK validates, the response
@@ -547,10 +547,10 @@ For the protocol-default RFC 9421 mode, use `WebhookReceiver` as shown below.
547547

548548
### Signed webhooks (AdCP 3.0): receiver quickstart
549549

550-
AdCP 3.0 webhooks are signed under the RFC 9421 profile
550+
AdCP webhooks are signed under the RFC 9421 profile
551551
(`adcp/webhook-signing/v1`) and carry a required `idempotency_key` for
552-
at-least-once dedup. The `WebhookReceiver` packages verify + dedupe + parse
553-
into one call so you don't have to re-derive the normative checklist:
552+
at-least-once delivery. The `WebhookReceiver` packages verification, immutable
553+
key-to-payload binding, claim ownership, and parsing into one call:
554554

555555
```python
556556
from flask import Flask, request, Response
@@ -562,14 +562,24 @@ from adcp.webhooks import (
562562
WebhookVerifyOptions,
563563
)
564564

565-
# One resolver per publisher. In production, wire an async JWKS fetcher
566-
# pointed at the publisher's `adagents.json`.
565+
# Discover this value from the publisher's
566+
# webhook_signing.delivery_retry_horizon_seconds capability. Receiver proof
567+
# must live for at least the whole advertised retry horizon.
568+
publisher_retry_horizon_seconds = 86400
569+
570+
# One resolver per publisher. In production, wire an async JWKS fetcher and a
571+
# durable multi-process backend (PgBackend rather than MemoryBackend).
567572
jwks = StaticJwksResolver(publisher_jwks_dict)
568573

569574
receiver = WebhookReceiver(
570575
config=WebhookReceiverConfig(
571576
verify_options=WebhookVerifyOptions(jwks_resolver=jwks),
572-
dedup=WebhookDedupStore(MemoryBackend(), ttl_seconds=86400),
577+
dedup=WebhookDedupStore(
578+
MemoryBackend(), # local example only; use PgBackend in production
579+
ttl_seconds=publisher_retry_horizon_seconds,
580+
),
581+
receiver_scope="buyer-account-123",
582+
publisher_scope_for=lambda _signer: "seller-agent-456",
573583
),
574584
)
575585

@@ -581,13 +591,16 @@ async def hook():
581591
method=request.method, url=request.url,
582592
headers=dict(request.headers), body=request.get_data(),
583593
)
584-
if outcome.rejected:
585-
return Response(status=401, headers=outcome.response_headers)
586-
# Spec: MUST return 2xx on duplicates so the at-least-once sender stops
587-
# retrying. A duplicate is a no-op, not an error.
588-
if outcome.duplicate:
589-
return Response(status=200)
590-
process(outcome.payload) # typed McpWebhookPayload
594+
# 409 for same-key/different-payload, 503 for an identical delivery still
595+
# being processed, 2xx only for a durably completed exact duplicate.
596+
if outcome.http_status is not None:
597+
return Response(status=outcome.http_status, headers=outcome.response_headers)
598+
try:
599+
await process(outcome.payload) # typed McpWebhookPayload
600+
except Exception:
601+
await receiver.release(outcome) # exact retry may claim again
602+
raise
603+
await receiver.acknowledge(outcome) # durable proof after publication
591604
return Response(status=200)
592605
```
593606

@@ -599,7 +612,12 @@ from adcp.webhooks import LegacyHmacFallback
599612

600613
config = WebhookReceiverConfig(
601614
verify_options=WebhookVerifyOptions(jwks_resolver=jwks),
602-
dedup=WebhookDedupStore(MemoryBackend(), ttl_seconds=86400),
615+
dedup=WebhookDedupStore(
616+
MemoryBackend(), # local example only; use PgBackend in production
617+
ttl_seconds=publisher_retry_horizon_seconds,
618+
),
619+
receiver_scope="buyer-account-123",
620+
publisher_scope_for=lambda _signer: "seller-agent-456",
603621
legacy_hmac=LegacyHmacFallback.from_shared_secret(
604622
secret=os.environ["WEBHOOK_SHARED_SECRET"].encode(),
605623
sender_identity="publisher-buyerco",
@@ -624,6 +642,7 @@ async with sender:
624642
url="https://buyer.example.com/webhooks/adcp/create_media_buy/op_abc",
625643
task_id="task_456",
626644
task_type="create_media_buy",
645+
operation_id="op_abc",
627646
status="completed",
628647
result={"media_buy_id": "mb_1"},
629648
)

0 commit comments

Comments
 (0)