Skip to content

Commit f50b2e2

Browse files
authored
Merge branch 'main' into fix/task-polling
2 parents a022fb2 + 92b6994 commit f50b2e2

79 files changed

Lines changed: 5805 additions & 733 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.

‎.github/workflows/ci.yml‎

Lines changed: 50 additions & 173 deletions
Large diffs are not rendered by default.

‎docs/agent-resolution-33.md‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Agent resolution and publisher pins in AdCP 3.3
2+
3+
This implementation follows [adcp#7882](https://github.com/adcontextprotocol/adcp/pull/7882)
4+
at commit `175bcd3b023244aeb97dbdcce8fed674ebd61302`. The protocol change
5+
is still under review. Historical bundled 3.1/3.2 schemas retain their published
6+
verifier constraints; these helpers implement the new canonical matching rules.
7+
8+
Agent URLs are the primary selector. Scheme/host case, default ports, dot
9+
segments, and percent-encoded unreserved characters normalize; trailing slashes
10+
and tenant paths remain significant. Optional type and id selectors only narrow
11+
a URL match. Duplicate canonical matches within one collection fail, including
12+
duplicates with identical ids. House Portfolio operator discovery scans
13+
`house.agents[]` and all inline `brands[].agents[]`; repeated attestations across
14+
collections count once when their type and resolved JWKS source agree.
15+
16+
Direct resolver construction and the shared resolver builder now require
17+
`agent_url`:
18+
19+
```python
20+
from adcp.signing import BrandJsonJwksResolver
21+
22+
resolver = BrandJsonJwksResolver(
23+
"https://operator.example.com/brand.json",
24+
agent_url="https://operator.example.com/sales",
25+
agent_type="sales",
26+
)
27+
```
28+
29+
Direct resolvers use operator-style collection selection, including all sibling
30+
collections in a House Portfolio unless `brand_id` selects one inline brand.
31+
For a relying-party record, select the surface's applicable collection explicitly;
32+
use `resolve_governance_jwks` for governance rather than an unscoped direct resolver.
33+
For operator discovery, `async_resolve_agent` and `verify_from_agent_url` invoke
34+
`get_adcp_capabilities` via MCP by default, or A2A with `protocol="a2a"`. They use
35+
the advertised `identity.brand_json_url`, enforce origin binding, and check every
36+
declared key origin. Cross-domain origin binding accepts `authorized_operators`
37+
only on a House Portfolio; its account-level brand/country scopes do not restrict
38+
key discovery. Discovery does not reuse onboarding mappings, so each call
39+
reconfirms the advertised operator record.
40+
41+
[Discovery step 7](https://github.com/adcontextprotocol/adcp/blob/175bcd3b023244aeb97dbdcce8fed674ebd61302/docs/building/by-layer/L1/security.mdx#L1331)
42+
checks every advertised `identity.key_origins` purpose against the selected
43+
agent's JWKS source, including purposes other than the signature being verified.
44+
A different host for any purpose rejects discovery with
45+
`request_signature_key_origin_mismatch`, even when the active purpose matches.
46+
The SDK follows this explicit all-purpose rule. The draft's separate guidance
47+
on origin separation remains in tension with that rule pending clarification.
48+
49+
For webhooks, use the asynchronous discovery helper:
50+
51+
```python
52+
from adcp.webhooks import verify_webhook_from_agent_url
53+
54+
sender = await verify_webhook_from_agent_url(
55+
method=request.method,
56+
url=str(request.url),
57+
headers=request.headers,
58+
body=await request.body(),
59+
agent_url=media_buy.seller_url,
60+
publisher_domains=media_buy.publisher_domains,
61+
)
62+
```
63+
64+
The seller URL and applicable publishers must come from your own media-buy
65+
record. The helper discovers the operator's JWKS, intersects it with every
66+
applicable publisher's `adagents.json` pin, and refreshes publisher records before
67+
rejecting a pin mismatch. A pin never supplies a key missing from the operator
68+
JWKS. Matching uses RFC 7638 thumbprints, so a `kid` alone or a different key with
69+
the same `kid` does not qualify. Pins reject signatures created at or after their
70+
`revoked_at` timestamp. Operator key-origin checks
71+
still apply. An unknown `kid` triggers one JWKS refetch, with a 30-second cooldown.
72+
73+
Sellers omitting `identity.brand_json_url` use the 3.x fallback: first their host's
74+
`/.well-known/brand.json`, then the registrable domain on a host 404. This path
75+
skips the advertised-record origin and key-origin checks. A malformed or
76+
unreachable advertised record never downgrades to the fallback. Webhook discovery
77+
failures return `webhook_signature_key_unknown` and log the specific
78+
`request_signature_*` cause. Fallback document resolution follows at most one
79+
`authoritative_location` or `house` indirection and rejects indirection chains.
80+
81+
For a receiver that already resolves operator keys, `WebhookVerifyOptions`
82+
accepts `publisher_pins` keyed by the applicable publisher domain. Supply
83+
`refresh_publisher_pins`, a synchronous callback returning fresh pins for the same
84+
publisher set. A missing pin uses `None`; an empty pin accepts no key. Configure
85+
the operator JWKS resolver and capabilities `expected_key_origins` alongside it.
86+
87+
Governance issuer matching is canonical. `resolve_governance_jwks` takes the
88+
authenticated buyer's exact record, `issuer`, and the governed request's
89+
`brand_domain`. It selects one applicable brand collection, preserving explicit
90+
brand overrides, and never searches sibling brands. For signed requests, pass
91+
`VerifiedSigner.operator_brand_json`, which `verify_from_agent_url` retains from
92+
discovery. Do not refetch a different document from the operator's host.

‎docs/reliable-reporting-service.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ Missing authentication is rejected. Supply
139139
`caller_resolver` for a different trusted identity model; never choose consumer
140140
identity from the request body.
141141

142+
`capability_block()` and installed capability discovery describe registered
143+
reporting offerings and installed components throughout the service lifetime.
144+
They are available before startup and remain stable while stopping, after
145+
closure, and after worker failure. An unconfigured service declares no reporting
146+
block. Use `service.ready` for local work admission; buyer tools report runtime
147+
unavailability separately. Discovery does not start workers or instantiate lazy
148+
platform tenants.
149+
150+
Managed-production discovery also retains registered destination, signing and
151+
reconciliation declarations through shutdown or failure. Build-time discovery
152+
does not construct the production graph. The development memory production
153+
graph declares no durable production tier; Core memory services still declare
154+
their inline offerings. `ReportingProductionSupport.reporting_delivery()` keeps
155+
its runtime readiness check for operator and admission callers.
156+
142157
## Decisioning platforms and lazy routers
143158

144159
Core `install()` accepts a `DecisioningPlatform`, including `LazyPlatformRouter`:
@@ -338,3 +353,15 @@ exact-read assertions after service recreation or durable recovery.
338353

339354
See [`examples/reliable_reporting_adapters.py`](../examples/reliable_reporting_adapters.py)
340355
for compact GAM-like and FreeWheel-like adapter definitions.
356+
357+
Installed Core and managed production reporting use one error policy at service
358+
admission. A created, stopping, closed, or failed service returns
359+
`SERVICE_UNAVAILABLE` with transient recovery. A missing authenticated caller
360+
returns `AUTH_REQUIRED`; explicit buyer configuration validation can raise
361+
`ReliableReportingConfigurationError(message, kind="invalid_request")`.
362+
Composition errors retain `INTERNAL_ERROR` and terminal recovery, with internal
363+
configuration details omitted from buyer responses. Ledger codes are preserved
364+
and receive the SDK's recovery classification for that code. Reporting cursor/
365+
input extensions are correctable, storage-unavailability extensions are transient,
366+
and unknown extension or integrity failures default to terminal. Disabled optional methods and aggregate delivery
367+
continue to delegate to the application.

‎docs/reporting-production.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,12 @@ migrate, construct fresh support, validate, and restart. Arbitrary serving-time
102102
DDL or search-path mutation is not automatically detected. Bound database
103103
statement execution at the adopter/database layer.
104104
The support also binds the pool's concrete identity and checks its open state
105-
on each readiness request. Closing or replacing that pool withdraws the claim
105+
on each readiness request. Closing or replacing that pool rejects fresh work
106106
even while the immutable catalog proof remains cached; rebuild the support for
107-
a new pool.
107+
a new pool. Installed capability discovery retains the registered declarations
108+
through these failures and through shutdown. It neither checks runtime readiness
109+
nor requests a database connection; use `support.reporting_delivery()` for a
110+
local readiness projection.
108111
The projection queue and all configured notification queues must retain the
109112
same pool as the ledger. Warm discovery performs no pool checkout and stays
110113
available when that valid pool is temporarily saturated.
@@ -200,7 +203,9 @@ new rc.6 forecast. The current SDK does not advertise rc.3 as a live client or
200203
server pin. New mounts, advertised schemas and rendering use the supported
201204
packaged version. Cursor/checkpoint version mismatches fail closed only after
202205
caller and signed-position verification. Changing a production mount's captured
203-
protocol pin invalidates readiness even after a successful schema proof.
206+
protocol pin invalidates both the captured declaration and runtime readiness,
207+
even after a successful schema proof. Worker lifecycle transitions preserve the
208+
registered capability promises; protocol substitution does not.
204209

205210
Integrated parents created representation-one snapshots without a protocol
206211
filter marker. Their original pages and checkpoints remain usable under rc.6,
@@ -362,7 +367,7 @@ unexpected-failure signal. Existing notification guards still
362367
check readiness before sampling and before dispatch; already-reserved work may
363368
finish under its existing transaction and deadline rules. Drain with `aclose()`,
364369
repair the failed component and construct fresh support to recover; the failed
365-
instance never silently restarts or regains its capability claim. A failing log
370+
instance never silently restarts or regains runtime readiness. A failing log
366371
sink cannot prevent the stop latch or change the safe public error.
367372
A PostgreSQL lock failure that exhausts the producer's bounded retries follows
368373
this same fail-stop policy. Other owned worker boundaries still propagate their

‎docs/webhooks/migration-from-fragmented-senders.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,10 @@ if not result.ok:
290290
* **Hooks bypassing SSRF** — hooks run before SSRF, but SSRF validates the post-rewrite URL; the boundary holds.
291291

292292
If your current sender doesn't have one of these properties, this is what you're trading up to.
293+
294+
## Reporting-stream correlation on AdCP 3.2
295+
296+
For scheduled `media_buy_delivery` notifications, see the
297+
[interim reporting operation ID guide](reporting-operation-id-3.2.md).
298+
It covers the optional registration extension, seller persistence and echo,
299+
and updating existing registrations before resuming scheduled delivery.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Reporting correlation on AdCP 3.2
2+
3+
AdCP 3.2.1 requires a buyer-supplied `operation_id` in scheduled delivery
4+
webhook envelopes, but its `reporting_webhook` schema does not declare that
5+
field. [adcp#7885](https://github.com/adcontextprotocol/adcp/issues/7885)
6+
tracks the gap; [adcp#7895](https://github.com/adcontextprotocol/adcp/pull/7895)
7+
proposes the field for 3.3.
8+
9+
The SDK restores an optional `ReportingWebhook.operation_id` as an interim
10+
extension. The published registration schema permits additional fields.
11+
Use it only after confirming seller support during integration onboarding;
12+
a 3.2 seller may accept and ignore an unknown field. This agreement does not
13+
change the released 3.2 schema or require all 3.2 implementations to support it.
14+
It needs no `ext.{namespace}` wrapper inside `reporting_webhook`.
15+
16+
```python
17+
from adcp.types import ReportingWebhook
18+
19+
registration = ReportingWebhook(
20+
url="https://buyer.example.com/webhooks/reporting",
21+
operation_id="op_reporting_001",
22+
authentication={
23+
"schemes": ["Bearer"],
24+
"credentials": "example_reporting_credential_32_chars",
25+
},
26+
reporting_frequency="daily",
27+
)
28+
wire_registration = registration.model_dump(mode="json")
29+
```
30+
31+
Supply that registration through `buy_products`, `accept_proposal`,
32+
`control_media_buy`, or the legacy `create_media_buy` / `update_media_buy`
33+
API where supported. IDs use 1–255 characters from `A–Z`, `a–z`, `0–9`,
34+
`_`, `.`, `:`, and `-`, matching `PushNotificationConfig.operation_id`.
35+
Omission and `None` remain valid; default serialization omits an absent ID.
36+
Explicit malformed values fail model validation. Historical version-scoped
37+
models keep their pinned schema contracts, including the beta.6-required field
38+
in `adcp.types.v32`.
39+
40+
For a seller implementing this agreement, persist the ID with the reporting
41+
registration, then pass that exact value to `WebhookSender.send_mcp(operation_id=...)`
42+
for every `media_buy_delivery` report. Preserve it on retries and keep existing
43+
authentication, required envelope fields, and per-delivery idempotency handling.
44+
The reporting-stream ID is independent of any task-status callback ID and does
45+
not replace a delivery's `idempotency_key`. Never derive it from the URL or
46+
fabricate it from a seller resource ID.
47+
48+
For existing registrations without a buyer-supplied ID, flag the missing value
49+
and suppress scheduled sends on this interim path. Ask buyers to update their
50+
registration with an ID before resuming delivery. `get_media_buy_delivery`
51+
remains available while arranging support. Updating the SDK alone does not
52+
backfill seller storage or restart reports.
53+
54+
Verify buyer serialization → seller validation → durable registration →
55+
scheduled send → authenticated receipt end to end before deployment. When a
56+
later protocol release defines the core field, its definition takes precedence
57+
over this interim agreement; remove the compatibility patch once generated
58+
models retain the field and the regression suite still passes.

‎examples/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ Related skills: `build-seller-agent`, `build-generative-seller-agent`,
7878
`build-signals-agent`. The seller storyboard tests live at
7979
[`../tests/test_seller_agent_storyboard.py`](../tests/test_seller_agent_storyboard.py).
8080

81+
The reference seller stores injected sandbox delivery events and splits their
82+
traffic across packages by budget, then across declared format kinds using
83+
descending catalog-order weights. Simulated video impressions represent
84+
two-second play events for `time_based_views`; these values demonstrate
85+
reporting behavior rather than measurements from a serving engine.
86+
8187
## Authentication & multi-tenancy
8288

8389
- [`mcp_with_auth_middleware.py`](mcp_with_auth_middleware.py) — multi-tenant MCP server with bearer-token auth via `BearerTokenAuthMiddleware` + `auth_context_factory`.

‎examples/multi_platform_seller/README.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,25 @@ serve(router, asgi_middleware=[(SubdomainTenantMiddleware, {...})])
156156
- **Cross-tenant state.** Each tenant is an island — its own
157157
inventory, its own buys, its own auth. The router never aggregates
158158
across tenants.
159+
160+
The examples support tenant-scoped account discovery and `list_products`.
161+
Both catalogs return `cache_scope: public`; catalog pages ignore outcome
162+
criteria, as the compact catalog contract requires. The non-guaranteed
163+
platform offers its normal auction floor and a fixed CPM quote when the buyer
164+
requests fixed-price inventory.
165+
166+
The sandbox controller's `seed_account` writes to the same account store used
167+
by normal requests. Fixtures, including their sandbox/live mode, resolve by
168+
explicit ID or natural account identity within the selected tenant. Account
169+
lists and fixture records do not cross tenant boundaries. The controller uses
170+
the framework's account mode gate; live accounts cannot invoke sandbox
171+
controls. Default local demo accounts are sandbox accounts.
172+
173+
Buy responses retain the original commitment timestamp and advance revisions
174+
on successful updates. Cancellation moves a buy to `canceled`; a second
175+
cancellation returns `NOT_CANCELLABLE`.
176+
177+
`force_get_products_arm` can register one sandbox business rejection for the
178+
next brief. Directives expire after five minutes and remain isolated by tenant,
179+
account, and authenticated caller. Anonymous requests use the example's public
180+
demo principal. Wholesale catalog reads leave the directive untouched.

0 commit comments

Comments
 (0)