diff --git a/.changeset/instrument-3-2-surfaces.md b/.changeset/instrument-3-2-surfaces.md
new file mode 100644
index 0000000000..869f33a3d0
--- /dev/null
+++ b/.changeset/instrument-3-2-surfaces.md
@@ -0,0 +1,5 @@
+---
+"adcontextprotocol": minor
+---
+
+Instrument the AdCP 3.2 beta across the training agent, compliance surface, adopter documentation, and learning system. Targeting-aware product discovery now issues bounded configured-product offers, validates purchase-time narrowing against declared overlay support, preserves proposal acceptance and lifecycle readback, and enforces retry-safe idempotency. Webhook delivery uses durable, scoped request bindings and recoverable outbox snapshots with the SDK 14 beta emitter contract. A release manifest tracks all 16 feature families across documentation, training, compliance, and runtime, with explicit dispositions for deferred candidates.
diff --git a/docs/building/by-layer/L4/choose-your-sdk.mdx b/docs/building/by-layer/L4/choose-your-sdk.mdx
index 3bf90d34b5..2daf80965c 100644
--- a/docs/building/by-layer/L4/choose-your-sdk.mdx
+++ b/docs/building/by-layer/L4/choose-your-sdk.mdx
@@ -13,20 +13,20 @@ For the layered model behind this — what each layer contains and what an SDK a
What "shipped" means at each layer is the [L0–L3 checklist](/docs/building/cross-cutting/sdk-stack#what-an-sdk-at-each-layer-should-provide).
-*Last updated: 2026-08-20.*
+*Last updated: 2026-08-23.*
| SDK | Current package | 3.2 beta support | L0 | L1 | L2 | L3 |
|---|---|---|:-:|:-:|:-:|:-:|
-| **`@adcp/sdk`** (TypeScript) | `14.0.0-beta.5` | Exact `3.2.0-beta.4` support | ✅ | ✅ | ✅ | ✅ |
-| **`adcp`** (Python) | `7.0.2` | Pending exact 3.2 support | ✅ | ⚠️ | ⚠️ | ⚠️ |
+| **`@adcp/sdk`** (TypeScript) | `14.0.0-beta.7` | Exact `3.2.0-beta.5` support | ✅ | ✅ | ✅ | ✅ |
+| **`adcp`** (Python) | `8.0.0b7` | Exact `3.2.0-beta.5` schemas and types; higher-level helpers remain partial | ✅ | ⚠️ | ⚠️ | ⚠️ |
| **`adcp/v3`** (Go) | `v3.0.0` | Pending exact 3.2 support | ⚠️ | ⚠️ | ⚠️ | ⚠️ |
Legend: ✅ shipped · ⚠️ partial / role-dependent · ❌ not yet covered. The L0–L3 columns describe the current package, not a 3.2 support claim.
-**3.2 packages are prereleases.** The TypeScript beta supports the exact beta.4
-protocol checkpoint; it does not imply compatibility with a later beta.
-Python and Go have not published exact 3.2 support statements yet. See the
+**3.2 packages are prereleases.** The TypeScript and Python betas support the
+exact beta.5 protocol checkpoint; that does not imply compatibility with a
+later beta. Go has not published an exact 3.2 support statement yet. See the
[3.2 beta program](/docs/reference/3-2-beta).
@@ -49,7 +49,7 @@ and the [Slack community](/docs/community/joining-slack).
[](https://www.npmjs.com/package/@adcp/sdk)
```bash
-npm install @adcp/sdk@14.0.0-beta.5
+npm install @adcp/sdk@14.0.0-beta.7
```
```javascript
@@ -63,7 +63,7 @@ const client = createSingleAgentClient({
});
const products = await client.listProducts({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
account: { account_id: 'account_123' },
criteria: {
offer_filters: { channels: ['ctv'] },
@@ -100,7 +100,7 @@ integrations should use the main server and per-tool type exports.
[](https://pypi.org/project/adcp/)
```bash
-pip install adcp
+pip install adcp==8.0.0b7
```
```python
@@ -158,11 +158,11 @@ Both SDKs share the same positional shape: `adcp [tool] [payload]`. The
### JavaScript CLI
```bash
-npx @adcp/sdk@14.0.0-beta.5 --help
-npx @adcp/sdk@14.0.0-beta.5 --save-auth my-agent https://sales.example.com/mcp
-npx @adcp/sdk@14.0.0-beta.5 my-agent list_products '{"adcp_version":"3.2-beta.4","account":{"account_id":"account_123"}}'
+npx @adcp/sdk@14.0.0-beta.7 --help
+npx @adcp/sdk@14.0.0-beta.7 --save-auth my-agent https://sales.example.com/mcp
+npx @adcp/sdk@14.0.0-beta.7 my-agent list_products '{"adcp_version":"3.2-beta.5","account":{"account_id":"account_123"}}'
# or against the built-in public test agent:
-npx @adcp/sdk@14.0.0-beta.5 test-mcp list_products '{"adcp_version":"3.2-beta.4"}'
+npx @adcp/sdk@14.0.0-beta.7 test-mcp list_products '{"adcp_version":"3.2-beta.5"}'
```
The CLI also drives storyboards (`adcp storyboard run`), conformance grading (`adcp grade`), and registry diagnostics. See `--help` for the full surface.
diff --git a/docs/learning/foundations/a2b-testing-your-first-agent.mdx b/docs/learning/foundations/a2b-testing-your-first-agent.mdx
index b145af31ee..7465b5a524 100644
--- a/docs/learning/foundations/a2b-testing-your-first-agent.mdx
+++ b/docs/learning/foundations/a2b-testing-your-first-agent.mdx
@@ -72,12 +72,12 @@ Use these as a quick reference while working through the module with Addie, or t
Call `get_products` with `buying_mode: "brief"` and a plain-English description of your campaign goals. The agent returns curated `products[]` and ready-to-execute `proposals[]`.
-This public training-agent lab continues to use the pre-3.2 brief flow. The 3.2
-structured discovery exercise will be enabled only after the beta SDKs and
-training runtime implement targeting-aware discovery; follow
-[issue #6199](https://github.com/adcontextprotocol/adcp/issues/6199). Until
-then, do not use this sandbox to test `get_products.targeting_overlay` or
-`required_overlay_support`, because the live runtime does not enforce them.
+This foundation lab intentionally uses the `get_products` compatibility facade
+so first-time learners can focus on the four-call lifecycle. The same public
+seller also supports compact 3.2 targeting-aware discovery. After this module,
+use the [buyer briefs supplement](/docs/learning/supplements/buyer-briefs-and-get-products)
+to practice `list_products`, `request_proposals`, and effective targeting
+readback with the pinned beta wire and SDK.
```bash
diff --git a/docs/learning/instructional-design.mdx b/docs/learning/instructional-design.mdx
index 5dfd549aaf..dd17c23700 100644
--- a/docs/learning/instructional-design.mdx
+++ b/docs/learning/instructional-design.mdx
@@ -157,7 +157,7 @@ AdCP is a living protocol. When the specification evolves — new tasks added, e
Each required demonstration is tracked by a stable ID tied to specific protocol knowledge. When a protocol change affects what a certified person should know, the system can identify which credential holders learned under the previous criteria and flag them for recertification.
-Recertification is targeted, not blanket. If a protocol update adds a new governance task but doesn't affect media buy workflows, only credentials that cover governance are flagged. Credential holders receive a notification through Addie with context on what changed and what they need to review. The decision process, delta-assessment rules, and AdCP 3.1 S2 canonical formats rationale are documented in the [recertification policy](/docs/learning/policies/recertification).
+Recertification is targeted, not blanket. If a protocol update adds a new governance task but doesn't affect media buy workflows, only credentials that cover governance are flagged. Credential holders receive a notification through Addie with context on what changed and what they need to review. The decision process, delta-assessment rules, and recorded AdCP 3.1 and 3.2 decisions are documented in the [recertification policy](/docs/learning/policies/recertification).
| Tier | Validity | Recertification |
|------|----------|-----------------|
diff --git a/docs/learning/policies/recertification.mdx b/docs/learning/policies/recertification.mdx
index 7d747d1199..c95721f6e2 100644
--- a/docs/learning/policies/recertification.mdx
+++ b/docs/learning/policies/recertification.mdx
@@ -1,7 +1,7 @@
---
title: Recertification and protocol-change updates
sidebarTitle: Recertification
-description: "AdCP certification recertification policy: targeted protocol-change updates, delta assessments, learner notifications, and the AdCP 3.1 canonical formats S2 rationale."
+description: "AdCP certification recertification policy: targeted protocol-change updates, delta assessments, learner notifications, and recorded AdCP 3.1 and 3.2 decisions."
"og:title": "AdCP - Recertification and protocol-change updates"
---
@@ -18,7 +18,8 @@ This policy is IACET-aligned and CPD-aligned. It is not an assertion of current
AdCP credentials are tied to protocol competency. When the protocol changes, the certification program determines whether existing credential holders need no action, a targeted delta assessment, or full recertification.
-This policy documents that decision process and records the AdCP 3.1 canonical formats rationale for pre-3.1 S2 Creative specialist holders.
+This policy documents that decision process and records the AdCP 3.1 canonical
+formats rationale and the AdCP 3.2 specialist-credential decision.
## Decision levels
@@ -107,6 +108,59 @@ Learners are assigned full S2 recertification instead of the delta when:
The delta-only window is 90 days after `delta_window_opens_at`. During that window, affected credential holders keep their S2 credential but are marked as requiring a protocol update. After the window closes, unresolved holders retain their historical S2 completion record, but their learner-facing S2 status is no longer current until they complete the current S2 module. Learner-facing messages must include the computed absolute deadline date.
+## AdCP 3.2 specialist decision
+
+AdCP 3.2 adds bounded competencies to three specialist credentials without
+invalidating their prior evidence. The recorded decision is therefore a
+**targeted delta assessment** for pre-3.2 holders of S1 Media Buy, S2 Creative,
+and S6 Security—not full recertification. No 3.2 learner targeting, notification,
+or credential-status change may occur during beta.
+
+Migration `550_curriculum_3_2_criteria.sql` adds stable evidence handles to the
+current module definitions so beta-path learners can demonstrate the new wire
+behavior. That curriculum instrumentation is not, by itself, a release gate or
+authorization to target prior credential holders. Evidence recorded during beta
+is provisional until program leadership maps it to the final GA artifact.
+
+| Credential | New criterion IDs | Decision rationale |
+|---|---|---|
+| S1 Media Buy | `s1_ex1_sc_compact_media_buy_lifecycle`, `s1_ex1_sc_targeting_aware_discovery`, `s1_ex1_sc_availability_and_outcome_planning` | Compact lifecycle, targeting resolution, and planning inputs extend prior discovery, proposal, purchase, and optimization evidence. |
+| S2 Creative | `s2_ex1_sc_pixel_density_and_renditions`, `s2_ex1_sc_vast_accessibility_provenance` | Pixel-density, rendition, validation, accessibility, and provenance rules narrow prior format-authoring and validation competencies. |
+| S6 Security | `s6_ex2_sc_3_2_signed_body_integrity`, `s6_ex4_sc_webhook_delivery_identity` | Body binding and binary encoding extend request-signing evidence; separating echoed `operation_id` correlation from payload `idempotency_key` deduplication extends webhook-security evidence. |
+
+For each credential, `criteria_effective_at` is the later of AdCP 3.2.0 GA and
+production deployment of `550_curriculum_3_2_criteria.sql` with the applicable
+assessment path available. `delta_window_opens_at` equals that effective point;
+`delta_window_closes_at` is 90 calendar days later at 23:59 UTC. The auditable
+record requirement is a pre-effective-date completion record for the affected
+module plus a criterion/checkpoint trail sufficient to verify its prior required
+demonstrations. A holder whose record already includes every applicable 3.2
+criterion requires no action.
+
+Assign the current full module instead of the delta when the prior record is
+missing or cannot be mapped, the learner misses the delta window, or later GA
+changes materially invalidate the beta-era mapping above. Program leadership
+must re-review this decision against the final 3.2.0 artifact before opening the
+window; if GA changes a listed competency, update the criterion and rationale
+before targeting learners.
+
+### GA activation gate
+
+The 3.2 delta is recorded but inactive during beta. Before anyone sets a 3.2
+criteria deployment timestamp or targets an existing holder, engineering and
+program leadership must verify all of the following:
+
+- the final 3.2.0 artifact still maps to the criterion IDs above;
+- each affected module has a registered delta definition and independently
+ configurable release gate—one module's window must not activate another;
+- Addie can present only the missing criteria, save criterion-level evidence,
+ and route an unmappable or late record to the full current module;
+- automated tests cover pre-GA suppression, the later-of-GA-and-deployment
+ boundary, the 90-day UTC deadline, already-complete evidence, missing records,
+ and the post-window full-recertification path; and
+- learner notification copy contains the affected credential, exact required
+ demonstrations, absolute deadline, support contact, and appeal path.
+
## CPD and accreditation disclosure
Recommended disclosure for continuing professional development records:
@@ -131,7 +185,7 @@ Email notification:
>
> Complete the delta by `` to keep your credential current. After that window, the current S2 module is required for renewal. If you believe you were targeted in error, email certification@agenticadvertising.org; we acknowledge assessment and targeting review requests within 2 business days and resolve them through the complaints process.
-## Implementation gates
+## AdCP 3.1 implementation gates
Engineering must not enable learner targeting or send learner notifications until all of the following are true:
diff --git a/docs/learning/specialist/creative.mdx b/docs/learning/specialist/creative.mdx
index 1c9f332f70..ab16f93ac1 100644
--- a/docs/learning/specialist/creative.mdx
+++ b/docs/learning/specialist/creative.mdx
@@ -37,7 +37,8 @@ The following `specialisms` fall under the `creative` domain. Each has its own c
- Understand creative agent pricing: the `pricing_options[]` array on discovery responses, how `pricing_option_id` flows through `build_creative` and `report_usage`, and why CPM ad servers show zero `vendor_cost` at build time while transformation agents do not
- Reason about tracker-slot presence — a format supports third-party measurement only if its `assets` array declares a tracker slot (e.g., `impression_tracker`). Broadcast formats intentionally omit tracker slots; measurement comes from panel and STB data via `billing_measurement`
- Attach `industry_identifiers[]` to broadcast manifests with the correct `creative-identifier-type` (`ad_id`, `isci`, `clearcast_clock`, or `idcrea`), and give each cut (`:15` vs `:30`) its own traffic identifier
-- Reason about format edge cases, accessibility, and provenance
+- Validate logical image dimensions independently from intrinsic pixels and distinguish accepted `pixel_ratios` from a required rendition set
+- Apply the seller's advertised VAST validation level, supply format-required accessibility detail, and distinguish buyer-declared provenance from independently verified evidence
## Prerequisite reading
@@ -197,15 +198,17 @@ Vendors often offer multiple pricing options per creative — volume/commitment
10. **Preview modes** — Run `preview_creative` as `request_type: "single"`, then `"batch"` (submit 5 creatives in one call and measure the speedup), then `"variant"` against a prior `get_creative_delivery` result. Compare `output_format: "url"` vs `"html"` for rendering latency.
11. **Tracker-slot audit** — For each sandbox format, inspect the `assets` array and determine whether it supports third-party measurement. Explain why assigning a DoubleVerify pixel to a broadcast spot won't work, and which `billing_measurement` vendor would instead.
12. **Broadcast identifiers** — Build a broadcast manifest with distinct `industry_identifiers[]` for `:15` and `:30` cuts of the same spot. Verify the `creative-identifier-type` values and explain why each cut needs its own traffic identifier.
+13. **Pixel density and rendition coverage** — Validate the canonical image reference vectors at 1x and 2x. Show that a 600×500 asset can satisfy a logical 300×250 slot at 2x without creating a new logical placement; trigger `pixel_ratio_ambiguous` on an ambiguous undeclared density; then satisfy `required_pixel_ratios: [1, 2]` with exactly one valid rendition at each required density. Explain why accepted densities do not require every density unless `required_pixel_ratios` says so.
+14. **VAST, accessibility, and provenance** — Read `creative_specs.vast_validation` before syncing one structurally valid but document-invalid VAST asset and predict whether the seller may return `VAST_PARSE_FAILED`. For a format with `accessibility.requires_accessible_assets: true`, submit the applicable alt text, captions, or transcript and interpret a structured accessibility violation. Finally submit synthetic creative metadata that declares `synthetic_depiction` and provenance evidence; distinguish a declaration from an independent verifier result and do not treat unverified metadata as an attestation.
## Assessment
| Dimension | Weight | What Addie evaluates |
|-----------|--------|---------------------|
| Interaction models | 20% | Correctly identifies and works with all three creative agent types |
-| Cross-platform adaptation | 15% | Adapts creatives across display, video, native, preview, measurement, and broadcast workflows |
+| Cross-platform adaptation | 15% | Adapts creatives across display, video, native, preview, measurement, broadcast, and density-aware rendition workflows |
| Canonical formats | 10% | Reads `format_options[]`, selects `format_kind` and `format_option_id`, explains single-option, multi-option, and `v1_format_ref[]` fan-out cardinality, maps `asset_source` values to buyer-provided, agent-synthesized, seller-rendered, seller-designed, and publisher-hosted workflows, and explains canonical-first/product-second validation order |
-| Compliance | 25% | Configures disclosures, provenance, and regulatory requirements |
+| Compliance | 25% | Applies advertised VAST validation, configures accessibility and disclosures, and distinguishes provenance declarations from verified evidence |
| Pricing and accounts | 15% | Understands rate cards, reads pricing from `list_creatives` and `list_transformers`, interprets build costs, closes the `report_usage` loop |
| Analytical skill | 15% | Interprets creative feature evaluation and delivery results |
diff --git a/docs/learning/specialist/media-buy.mdx b/docs/learning/specialist/media-buy.mdx
index 55ef3cbe34..41dcf76b3b 100644
--- a/docs/learning/specialist/media-buy.mdx
+++ b/docs/learning/specialist/media-buy.mdx
@@ -48,6 +48,8 @@ See the [Compliance Catalog](/docs/building/verification/compliance-catalog) for
- Interpret seller indicators as current relationship-scoped state: explain why creative fatigue belongs to one package–creative assignment, why the same creative may be fatigued on one seller but not another, and why provider methodology belongs in `ext`
- Handle pricing negotiation, budget allocation, and multi-agent orchestration
- Use refinement and package requests for complex buying scenarios
+- Use `offer_filters.availability_horizon` only when the seller advertises flexible-window availability, interpret the returned time windows and `incomplete[]` gaps, and distinguish planning availability from a hold
+- Use `criteria.outcome_target` only when advertised, verify `total_budget_guidance` and matching forecast metrics, and explain why reverse-forecast guidance is not a delivery guarantee
- Monitor and optimize campaign delivery using protocol tools
- Explain the measurement-agent loop: make the orchestrator a buyer-controlled gateway for the fixed `get_media_buy_delivery` and `provide_performance_feedback` tasks, authenticate provider output, and fan normalized assertions out to sellers without granting providers seller access
- Reason about failure modes, conflict resolution, and edge cases
@@ -214,9 +216,9 @@ See the [Quickstart](/docs/quickstart) for a walkthrough of your first call.
During the module, Addie will guide you through hands-on exercises:
-1. **Product discovery and evaluation** — Query multiple sandbox agents, compare products, evaluate pricing
+1. **Product discovery and evaluation** — Query multiple sandbox agents, compare products, evaluate pricing, and separate concrete `targeting_overlay` values from future-selection `required_overlay_support`. Verify unsupported requirements filter products, compare the result with a supported requirement, review any sparse `targeting_resolution.modifications`, and prove acceptance by reading the effective package targeting back after purchase.
2. **Pricing option selection** — From the same product's `pricing_options[]` array, select a CPM option and a CPP option. Explain what each option's `parameters` mean and why the minimum spend differs.
-3. **Proposal and forecasting** — Request proposals, analyze delivery forecasts (spend curves and availability)
+3. **Proposal and forecasting** — Request proposals and analyze delivery forecasts. On a seller advertising both planning capabilities, send a flexible `offer_filters.availability_horizon` and confirm the returned half-open windows cover the requested horizon or disclose gaps through `incomplete[]`. Then send a supported `criteria.outcome_target`, verify `total_budget_guidance` plus the matching forecast metric, and explain why neither forecast nor guidance creates a delivery guarantee before proposal finalization.
4. **Terms negotiation** — On a guaranteed product, propose `measurement_terms` with a different vendor than the seller's default, and `performance_standards` with a tighter viewability threshold. Observe seller acceptance, adjustment, or `TERMS_REJECTED`. Recover by aligning to the seller's supported vendors.
5. **Campaign commitment and optimization** — Commit a direct offer with `buy_products`, assign creatives through `sync_creatives`, monitor through `get_media_buys`, and apply an in-envelope change with `control_media_buy`. Submit compact performance feedback with a named baseline, interpreting `accepted`, `applied`, and `not_applied` without overstating causality. Explain how the 3.x `create_media_buy` and `update_media_buy` facades map to the canonical operations, and verify that the update facade requires `account`.
6. **Proposal negotiation and lifecycle walkthrough** — Following the [proposal negotiation implementation guide](/docs/media-buy/product-discovery/proposal-negotiation), act as Sam at Pinnacle Agency against StreamHaus. Use `request_proposals`, inspect `proposal_refinement.supported_dimensions`, and construct a refinement the seller can actually evaluate. Try a USD budget ceiling, a product include/omit decision, a targeting change, and multiple alternatives only when advertised; interpret `partial` and `unable` as structured counteroffers rather than generic failures. On an ask-only seller, observe the pre-mutation `UNSUPPORTED_FEATURE`, remove or translate the typed field, and retry. Finalize the chosen immutable proposal, call `accept_proposal`, and trace the resulting buy through `pending_creatives` → `sync_creatives` → `pending_start` → `active`.
diff --git a/docs/learning/specialist/security.mdx b/docs/learning/specialist/security.mdx
index 574bd1d05b..db02d8f3e2 100644
--- a/docs/learning/specialist/security.mdx
+++ b/docs/learning/specialist/security.mdx
@@ -33,8 +33,10 @@ The following `specialisms` fall under the security domain. Each has its own com
- Explain the agentic advertising threat model: credential theft, replay attacks, cross-tenant data leakage, SSRF on outbound fetches, spoofed agent identity, unauthorized governance token use, and audit log tampering
- Walk through AdCP's five-layer defense model — identity, isolation, idempotency, signed governance, auditability — and name the specific attack each layer closes
- Mint an idempotency key and produce the three observable outcomes against a sandbox — successful first call, idempotent replay (`replayed: true`, unchanged resource), and conflict on payload change — and reason about the fourth: TTL expiry. The replay window is 24h, while mutated sandbox resources are retained for at least 25h after their last persisted mutation so they remain available throughout that window. Read-only calls do not extend resource retention. Expiry is therefore reasoned about rather than observed during the lab. Explain what a missing key removes from the seller's safety guarantees.
+- For a 3.2 request-signing endpoint, require `content-digest` coverage on every signed request with a body, serialize request `Signature` and `Content-Digest` binary values as RFC 8941 padded standard Base64, and reject a legacy Base64URL request token without falling back to a second parser
- Obtain and decode a signed governance token, then use the sandbox verifier to watch the checklist accept a valid token and reject a tampered one (signature), a misaddressed one (`aud` / confused-deputy), and one signed under a revoked key — explaining the attack each failing step closes and why revocation is checked before expiry
- Specify the 6-point SSRF check on an outbound fetch (HTTPS-only enforcement, reserved-IP deny list including cloud metadata endpoints, IP-pin validation, redirect suppression, size and timeout caps, and suppressed error detail) and demonstrate the agent refusing a metadata-IP webhook target
+- Distinguish buyer-supplied `operation_id` correlation from sender-generated webhook `idempotency_key` deduplication: retries of the same byte-identical logical fire reuse the dedup key, while a changed payload or later logical observation receives a fresh key
- Design an operational runbook covering credential compromise, webhook secret rotation, governance key revocation, and cross-party incident communication
- Given an incident description, identify which defense layer failed and what specific control to harden
@@ -81,19 +83,20 @@ See the [Quickstart](/docs/quickstart) for a walkthrough of your first call.
1. **Threat model walkthrough** — Map each threat (credential theft, replay, cross-tenant leakage, SSRF, spoofed identity, unauthorized governance, audit tampering) to the specific AdCP control that closes it. Explain why no single layer is sufficient alone.
2. **Idempotency lifecycle** — Using one idempotency key on a mutating call (e.g. `create_property_list`): (a) first call — observe success; (b) identical replay — observe `replayed: true` with an unchanged resource id, confirming no new side effect; (c) same key, different payload — observe the `IDEMPOTENCY_CONFLICT` error. Then reason about the fourth outcome — expiry after the 24h replay window lapses. The sandbox retains mutated resources for at least 25h after their last persisted mutation, without extending that retention on read-only calls, so the original resource remains available throughout the replay window. Explain what a missing idempotency key means for the seller's at-most-once safety guarantee.
3. **Governance token verification** — Obtain a signed governance token from the sandbox governance agent (`sync_plans`, then an intent-phase `check_governance`) and decode its header (`alg`, `typ`, `kid`) and claims (`aud`, `sub`, `phase`, `jti`, `exp`). Then run the sandbox verifier (`comply_test_controller` scenario `verify_governance_token`) and watch the JWS checklist accept and reject tokens: a valid token passes every step; a tampered claim fails at the signature step (`governance_token_invalid`); a token bound to a different seller fails the `aud` byte-match (`governance_token_not_applicable` — confused deputy, via `mode: wrong_aud_demo`); and a token signed under a revoked key fails the revocation step (`governance_token_revoked`, via `mode: revoked_demo`). For each, explain the attack the failing step closes — and note that revocation is checked *before* expiry, so a revoked token is rejected even if it has also lapsed. (`jti` seen-before dedup — distinct from `aud` — is the step that stops replay of the same token.)
-4. **SSRF defense and endpoint activation** — Submit `https://169.254.169.254/latest/meta-data/` in an account `notification_config` and observe the synchronous `VALIDATION_ERROR` on `notification_configs[].url`. Then register a public receiver you control with `active: false`, configure it to verify the RFC 9421 `webhook.challenge` and echo the single-use nonce, and reactivate the same subscriber with `active: true`. Inspect the signed body bindings (`account_id`, `subscriber_id`, `delivery_auth`, and `event_types`) plus the covered `@target-uri` and signature expiry. Finally pause the subscriber, re-point the exercise hostname to a reserved address, and attempt reactivation: the agent re-runs the full public-target check immediately before enabling delivery and refuses the re-bound target. Specify the 6-point SSRF check and explain what each point closes — the reserved-IP/metadata deny list, IP-pin-at-connect, redirect suppression, bounded responses/timeouts, and suppressed error detail.
+4. **SSRF defense, endpoint activation, and delivery identity** — Submit `https://169.254.169.254/latest/meta-data/` in an account `notification_config` and observe the synchronous `VALIDATION_ERROR` on `notification_configs[].url`. Then register a public receiver you control with `active: false`, configure it to verify the RFC 9421 `webhook.challenge` and echo the single-use nonce, and reactivate the same subscriber with `active: true`. Inspect the signed body bindings (`account_id`, `subscriber_id`, `delivery_auth`, and `event_types`) plus the covered `@target-uri` and signature expiry. Finally pause the subscriber, re-point the exercise hostname to a reserved address, and attempt reactivation: the agent re-runs the full public-target check immediately before enabling delivery and refuses the re-bound target. Specify the 6-point SSRF check and explain what each point closes — the reserved-IP/metadata deny list, IP-pin-at-connect, redirect suppression, bounded responses/timeouts, and suppressed error detail. Capture two retry attempts of one logical fire and verify they echo the same `operation_id`, carry the same payload `idempotency_key`, and differ only in permitted transport metadata; then trigger a later changed observation and verify it receives a fresh payload key.
5. **Principal isolation** — Scope a read to a different account than the one that created a resource and interpret the result correctly: distinguish an account-scoped *not-found* from an authorization *denial*. Explain the separation model — account-scoped access — and what would break (cross-tenant read/write on a leaked token) if account-scoped tokens were not enforced.
6. **Incident runbook design** — Given a credential compromise scenario (API key leaked in a public repo), design the response: which keys to rotate and in what order, how to notify counterparties, what audit events to review, and how to verify the compromise window.
7. **Defense layer diagnosis** — Given three incident descriptions (replay attack succeeded, cross-tenant data returned, governance token accepted after key revocation), identify which layer failed in each case and what specific control to harden.
+8. **3.2 signed-body integrity** — Read `request_signing.covers_content_digest` from a 3.2 capability response. Verify a signed body succeeds only when `content-digest` is covered and matches the exact transmitted bytes; show that omitting coverage yields `request_signature_components_incomplete` and changing the body yields `request_signature_digest_mismatch`. Inspect the `Signature` and `Content-Digest` structured fields to confirm padded standard Base64, then show that an unpadded Base64URL request token is rejected as malformed rather than retried through the legacy parser. Explain why the separately routed webhook profile keeps its legacy encoding during 3.x.
## Assessment
| Dimension | Weight | What Addie evaluates |
|-----------|--------|---------------------|
| Threat model fluency | 20% | Can you name an attack and the specific layer that closes it? |
-| Hands-on idempotency | 20% | Can you produce the observable idempotency outcomes (success, replay, conflict) on demand and reason about expiry and a missing key? |
+| Replay and request integrity | 20% | Can you produce the idempotency outcomes and verify 3.2 signed-body coverage, digest binding, and strict request-profile binary parsing? |
| Governance verification | 25% | Can you walk the 15-step checklist and explain what each step prevents? |
-| SSRF discipline | 15% | Can you specify the 6-point check and demonstrate the agent refusing a metadata-IP webhook target? |
+| SSRF and webhook discipline | 15% | Can you specify the 6-point check, demonstrate metadata-IP refusal, and keep correlation separate from delivery dedup identity? |
| Operational design | 20% | Can you design a runbook for credential compromise, including rotation order and cross-party communication? |
Passing threshold: 70%.
diff --git a/docs/learning/supplements/buyer-briefs-and-get-products.mdx b/docs/learning/supplements/buyer-briefs-and-get-products.mdx
index 354b7623ec..db9e66b671 100644
--- a/docs/learning/supplements/buyer-briefs-and-get-products.mdx
+++ b/docs/learning/supplements/buyer-briefs-and-get-products.mdx
@@ -1,21 +1,21 @@
---
-title: "Buyer briefs and get_products request shape"
+title: "Buyer briefs and discovery request shape"
sidebarTitle: "Buyer briefs"
-description: "How buyer agents decompose campaign intent into get_products briefs, product filters, targeting overlays, future targeting support, and refinements."
-"og:title": "AdCP - Buyer briefs and get_products request shape"
+description: "How buyer agents decompose campaign intent into proposal briefs, offer filters, targeting overlays, future targeting support, and refinements."
+"og:title": "AdCP - Buyer briefs and discovery request shape"
---
-# Buyer briefs and get_products request shape
+# Buyer briefs and discovery request shape
-This supplement prepares buyer-side implementers to turn a human campaign request into a precise `get_products` call.
+This supplement prepares buyer-side implementers to turn a human campaign request into precise `list_products` and `request_proposals` calls. Existing 3.x callers can apply the same decomposition through the `get_products` compatibility facade.
The goal is not to make the brief verbose. The goal is to put intent in the brief and hard constraints in typed fields so the seller can curate inventory without guessing which parts are negotiable.
-This module teaches the AdCP 3.2 targeting-aware discovery contract. Use it
-against sellers and SDKs that declare 3.2 support. The public training-agent lab
-will enable these calls after the 3.2 beta SDK release; implementation is
-tracked in [issue #6199](https://github.com/adcontextprotocol/adcp/issues/6199).
+This module teaches the AdCP 3.2 targeting-aware discovery contract. The public
+training seller supports the exact `3.2-beta.5` wire bundle with
+`@adcp/sdk@14.0.0-beta.7`. Verify those advertised capabilities before the lab;
+unknown-field acceptance by an older seller is not evidence of support.
## Mental model
@@ -23,12 +23,12 @@ tracked in [issue #6199](https://github.com/adcontextprotocol/adcp/issues/6199).
| Input | Use it for | Avoid putting here |
|---|---|---|
| `brief` | Buyer intent, semantic audience language, context, tone, business goal, and requirements without a typed representation | Constraints that already have structured fields |
-| `filters` | Hard offer filters—metadata, dates, budget, availability, commercial fit, and reporting capabilities—that exclude non-matching products | Delivery targeting or soft preferences |
-| `targeting_overlay` | Exact delivery constraints known now; availability, pricing, and forecasts must account for them | Product characteristics or values the buyer has not chosen yet |
-| `required_overlay_support` | Targeting dimensions whose values will be selected independently on packages later | Current targeting values or a request for one product per value |
+| `criteria.offer_filters` | Hard offer filters—metadata, dates, budget, availability, commercial fit, and reporting capabilities—that exclude non-matching products | Delivery targeting or soft preferences |
+| `criteria.targeting_overlay` | Exact delivery constraints known now; availability, pricing, and forecasts must account for them | Product characteristics or values the buyer has not chosen yet |
+| `criteria.required_overlay_support` | Targeting dimensions whose values will be selected independently on packages later | Current targeting values or a request for one product per value |
| `brand` | The buyer brand identity the seller uses for eligibility, safety, and fit | A second copy of the campaign brief |
| `catalog` | Commerce or product-set context when the campaign is catalog-driven | General brand positioning |
-| `refine` | Specific changes to a prior discovery response | New unrelated discovery goals |
+| `refine_proposals` changes | Specific changes to a prior proposal snapshot | New unrelated discovery goals |
## What goes in the brief
@@ -44,8 +44,9 @@ Example:
```json
{
+ "$schema": "https://adcontextprotocol.org/schemas/v3/media-buy/request-proposals-request.json",
+ "adcp_version": "3.2-beta.5",
"idempotency_key": "550e8400-e29b-41d4-a716-446655442008",
- "buying_mode": "brief",
"brief": "Launch Nova Running's spring trail shoe line with outdoor enthusiasts. Favor trusted adventure and fitness contexts, avoid discount-led positioning, and prioritize packages that can support a brand-lift readout.",
"brand": {
"domain": "novarunning.example"
@@ -53,12 +54,13 @@ Example:
}
```
-## What goes in filters
+## What goes in offer filters
Use filters when a product that fails the condition should not come back.
-That exclusion rule is identical in `brief`, `wholesale`, and `refine`; the
-mode changes how the seller curates and versions the result, not whether a
-valid filter applies.
+That exclusion rule is identical across `list_products`, `request_proposals`,
+and `refine_proposals`; the operation changes whether the seller lists,
+curates, or revises offers, not whether a valid filter applies.
+The brief, wholesale/catalog listing, and refine flows therefore use the same hard-filter semantics.
Good filter candidates:
@@ -73,21 +75,24 @@ Example:
```json
{
+ "$schema": "https://adcontextprotocol.org/schemas/v3/media-buy/request-proposals-request.json",
"idempotency_key": "550e8400-e29b-41d4-a716-446655442009",
- "buying_mode": "brief",
+ "adcp_version": "3.2-beta.5",
"brief": "Launch Nova Running's spring trail shoe line with outdoor enthusiasts. Favor trusted adventure and fitness contexts.",
"brand": {
"domain": "novarunning.example"
},
- "filters": {
- "channels": ["ctv", "display"],
- "pricing_currencies": ["USD"],
- "required_metrics": ["impressions", "clicks"]
+ "criteria": {
+ "offer_filters": {
+ "channels": ["ctv", "display"],
+ "pricing_currencies": ["USD"],
+ "required_metrics": ["impressions", "clicks"]
+ }
}
}
```
-If the buyer says "ideally CTV, but display is okay," keep that preference in the brief. If they say "CTV only," use `filters.channels`.
+If the buyer says "ideally CTV, but display is okay," keep that preference in the brief. If they say "CTV only," use `criteria.offer_filters.channels`.
## What goes in targeting
@@ -95,10 +100,12 @@ Use `targeting_overlay` for exact delivery constraints the buyer knows now:
```json
{
- "targeting_overlay": {
- "geo_countries": ["US"],
- "demographics": {
- "age": { "min": 25, "max": 44, "include_unknown": false }
+ "criteria": {
+ "targeting_overlay": {
+ "geo_countries": ["US"],
+ "demographics": {
+ "age": { "min": 25, "max": 44, "include_unknown": false }
+ }
}
}
}
@@ -109,9 +116,11 @@ will be chosen later:
```json
{
- "required_overlay_support": {
- "geo_metros": { "systems": ["nielsen_dma"] },
- "placement_selection": true
+ "criteria": {
+ "required_overlay_support": {
+ "geo_metros": { "systems": ["nielsen_dma"] },
+ "placement_selection": true
+ }
}
}
```
@@ -129,21 +138,21 @@ accepts the disclosed differences.
## Brief vs. refine
-Use `buying_mode: "refine"` when the buyer is reacting to a previous discovery response. A refine request should point at what changed: remove a product, adjust budget, request more premium placements, narrow geography, or ask for alternatives.
+Use `refine_proposals` when the buyer is reacting to a previous proposal. A refinement should point at what changed: adjust budget, include or omit a product, request more premium placements, narrow geography, or ask for alternatives. Existing callers can express the equivalent flow through `get_products` with `buying_mode: "refine"`.
-Do not send a totally new campaign in `refine`; start a new `brief` request instead.
+Do not send a totally new campaign through `refine_proposals`; start a new `request_proposals` call instead.
## Implementation checklist
-- Read `get_adcp_capabilities.adcp.supported_versions`, pin a compatible 3.2 release in `adcp_version`, and validate the echoed served release before using targeting-aware discovery.
+- Read `get_adcp_capabilities.adcp.supported_versions` and `media_buy.lifecycle_tools`, pin `adcp_version: "3.2-beta.5"` for this lab, and validate the echoed served release before using targeting-aware discovery.
- If release precision is absent, major-only, or 3.1-or-earlier, omit the 3.2 fields and fall back to retained legacy targeting filters or explicit brief prose; unknown fields may otherwise be silently ignored.
- Normalize the human request into intent, hard constraints, and follow-up changes before calling the seller.
- Preserve the buyer's business language in `brief`; do not collapse it into only keywords.
-- Put product characteristics in `filters` and exact delivery constraints in `targeting_overlay`.
-- Use `required_overlay_support` only for dimensions whose values will be supplied later.
+- Put product characteristics in `criteria.offer_filters` and exact delivery constraints in `criteria.targeting_overlay` on compact tasks. Map those to the compatibility facade's top-level `filters` and `targeting_overlay` only for an existing `get_products` caller.
+- Use `criteria.required_overlay_support` only for dimensions whose values will be supplied later.
- Inspect every returned `targeting_resolution`; silence confirms an unchanged structured overlay, not prose interpretation.
-- Keep `brief` out of `wholesale` mode.
-- Persist the request tuple with the response so later `refine` calls and `wholesale_feed_version` comparisons are scoped correctly.
+- Keep `brief` on `request_proposals`; `list_products` is structured discovery and has no brief.
+- Persist the request tuple with the response so later refinements and feed-version comparisons are scoped correctly.
## Practice prompt
@@ -154,7 +163,7 @@ A buyer says:
Expected decomposition:
- Brief: family dinner kit launch, parent audience, suitable contexts, six-week launch.
-- Filters: video-capable channel/format constraints, USD pricing, completion-rate reporting.
+- Offer filters: video-capable channel/format constraints, USD pricing, completion-rate reporting.
- Targeting overlay: US and ages 25–44.
- Required overlay support: Nielsen DMA selection later.
- Brand: Acme Meals domain or BrandRef.
diff --git a/docs/learning/tracks/buyer.mdx b/docs/learning/tracks/buyer.mdx
index 67ecc45bdc..50f6c95dc2 100644
--- a/docs/learning/tracks/buyer.mdx
+++ b/docs/learning/tracks/buyer.mdx
@@ -74,7 +74,7 @@ How buyer agents orchestrate across multiple sales agents simultaneously: discov
- **Order lifecycle** — `pending_creatives` → `pending_start` → `active`; check `valid_actions` from `get_media_buys` before acting. S1 covers the full state machine and recovery
- **Version negotiation** — declare `adcp_major_version` on every request; handle `VERSION_UNSUPPORTED` by selecting a compatible seller or downgrading the payload
- **Pricing selection** — products return `pricing_options[]`; buyer selects one via `pricing_option_id` in `create_media_buy`
-- **Structured-first discovery** — use `filters` to decide which offers may be returned, `targeting_overlay` to constrain delivery, and `required_overlay_support` for dimensions selected on packages later
+- **Structured-first discovery** — on the compact 3.2 tasks, use `criteria.offer_filters` to decide which offers may be returned, `criteria.targeting_overlay` to constrain delivery, and `criteria.required_overlay_support` for dimensions selected on packages later. The 3.x `get_products` compatibility facade keeps the equivalent top-level fields
- **Two optional discovery surfaces** — the 3.2 wholesale feed (`list_products`) is the self-serve, lower-resolution surface; a brief (`request_proposals`, or `get_products` with a brief) is the higher-resolution one. Query both where the seller advertises them; a thin or absent feed is not a capability assessment
- **Resolution acceptance** — no resolution means an exact structured overlay; review `modifications` and brief-derived targeting before selecting an opaque configured `product_id`
- **Negotiated accountability** — for guaranteed buys, propose `measurement_terms` / `performance_standards`; seller accepts, adjusts, or returns `TERMS_REJECTED`. S1 covers the recovery patterns
@@ -256,11 +256,11 @@ New to running tool calls and reading JSON in a terminal? Do [A2b: Testing your
### What you'll build
-The targeting-aware discovery portion is a 3.2 assessment objective. The public
-training seller will enable it after the 3.2 beta SDK release; follow
-[issue #6199](https://github.com/adcontextprotocol/adcp/issues/6199). Until
-then, validate this portion with schema fixtures rather than assuming the live
-training runtime enforces unknown fields.
+The public training seller implements targeting-aware discovery on the exact
+AdCP `3.2-beta.5` wire bundle. Pin `@adcp/sdk@14.0.0-beta.7`, verify the
+seller's advertised version and lifecycle tools, and retain the request,
+response, and package readback as assessment evidence. Do not treat an
+uncapable seller silently ignoring unknown fields as validation.
- Account setup with `sync_accounts`
@@ -281,7 +281,7 @@ To see the seller responses your agent must handle, you can inspect the test sel
```bash
# Inspects the test SELLER's get_products response — the shape your buyer agent must parse.
-npx @adcp/sdk@latest test-mcp get_products '{"idempotency_key":"550e8400-e29b-41d4-a716-446655442065","buying_mode":"brief","brief":"your campaign brief"}'
+npx @adcp/sdk@14.0.0-beta.7 test-mcp get_products '{"adcp_version":"3.2-beta.5","idempotency_key":"550e8400-e29b-41d4-a716-446655442065","buying_mode":"brief","brief":"your campaign brief"}'
```
See [Build a caller](/docs/building/by-layer/L4/build-a-caller) for client setup and [Validate Your Agent](/docs/building/verification/validate-your-agent) for the storyboard workflow.
diff --git a/docs/media-buy/index.mdx b/docs/media-buy/index.mdx
index 9728f9b6c3..e2c3e3b84a 100644
--- a/docs/media-buy/index.mdx
+++ b/docs/media-buy/index.mdx
@@ -46,7 +46,7 @@ delivery constraints use structured criteria:
const proposals = await Promise.all(
sellers.map((seller) =>
seller.requestProposals({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-proposals-001',
account: { account_id: 'account_123' },
brand: { domain: 'acme-outdoor.example' },
@@ -108,7 +108,7 @@ draft with typed constraints:
```javascript
const refined = await streamHaus.refineProposals({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-refine-001',
refinements: [
{
@@ -154,7 +154,7 @@ creates a committed successor and an inventory hold:
```javascript
const finalized = await streamHaus.refineProposals({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-finalize-001',
refinements: [
{
@@ -170,7 +170,7 @@ accepts that exact snapshot:
```javascript
const buy = await streamHaus.acceptProposal({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-accept-001',
account: { account_id: 'account_123' },
proposal_id: 'proposal_streamhaus_003',
@@ -200,7 +200,7 @@ required format has coverage, then moves to `pending_start` or `active`.
```javascript
await streamHaus.syncCreatives({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-creatives-001',
account: { account_id: 'account_123' },
creatives: [
@@ -255,7 +255,7 @@ a package or adjust a budget inside the accepted commercial envelope, he uses
```javascript
const controlled = await streamHaus.controlMediaBuy({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-control-001',
account: { account_id: 'account_123' },
media_buy_id: buy.media_buy_id,
diff --git a/docs/media-buy/product-discovery/index.mdx b/docs/media-buy/product-discovery/index.mdx
index ac0d87df88..6954ff652b 100644
--- a/docs/media-buy/product-discovery/index.mdx
+++ b/docs/media-buy/product-discovery/index.mdx
@@ -103,7 +103,7 @@ without asking the seller to author a media plan:
```javascript
const result = await seller.listProducts({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
account: { account_id: 'account_123' },
criteria: {
offer_filters: {
@@ -121,6 +121,23 @@ The response contains products only—never proposals—and carries an opaque
to `buy_products` so the seller can reject stale offers instead of silently
applying changed terms.
+When discovery includes concrete `targeting_overlay` values, inspect each
+returned product before purchase:
+
+- No product-level `targeting_resolution` means the product accepts the
+ requested structured overlay exactly.
+- `targeting_resolution.modifications` is an ordered, sparse disclosure of the
+ alternative the seller can execute. Selecting that `product_id` accepts the
+ modified result.
+- A request-specific configured product carries `is_custom: true` and
+ `expires_at`. Purchase that exact ID before expiry; do not substitute the
+ baseline product ID or send a conflicting overlay at commitment.
+
+After `buy_products`, verify that the package readback preserves the accepted
+targeting. If the buyer requested only `required_overlay_support`, rediscover
+with concrete values before purchase when it needs a value-specific price or
+forecast; support alone does not promise inventory for every later value.
+
A catalog mirror uses the same read as its bootstrap and repair surface. Store
`feed_version`, `pricing_version`, and `cache_scope`; subscribe to `product.*`
and `wholesale_feed.bulk_change` through account notification configuration;
@@ -133,12 +150,12 @@ for the complete mirror contract.
## Seller-planned proposals
Use `request_proposals` when the seller should translate a campaign brief into
-one or more priced plans. Sam Adeyemi at Pinnacle Agency can send the same
+one or more priced plans. Sam at Pinnacle Agency can send the same
structured criteria to StreamHaus while keeping strategy in prose:
```javascript
const response = await seller.requestProposals({
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
idempotency_key: 'acme-q2-proposals-001',
account: { account_id: 'account_123' },
brand: { domain: 'acme-outdoor.example' },
@@ -166,6 +183,11 @@ The seller returns immutable draft proposals. Each revision creates a new
4. Call `accept_proposal` with both the committed `proposal_id` and
`terms_digest` before `expires_at`.
+The criteria and any configured product targeting are part of the proposal's
+commercial terms. Before finalizing, verify the selected product IDs, effective
+targeting or disclosed modifications, price, forecast assumptions, and expiry;
+after acceptance, confirm the same targeting on package readback.
+
Commercial changes after acceptance use the same proposal lineage to create an
amendment or negotiated cancellation. Operational changes inside the accepted
envelope use [`control_media_buy`](/docs/media-buy/task-reference/control_media_buy).
diff --git a/docs/media-buy/product-discovery/proposal-negotiation.mdx b/docs/media-buy/product-discovery/proposal-negotiation.mdx
index 0d2fb8431e..6b61b846ee 100644
--- a/docs/media-buy/product-discovery/proposal-negotiation.mdx
+++ b/docs/media-buy/product-discovery/proposal-negotiation.mdx
@@ -488,7 +488,7 @@ Record the authenticated buyer, source IDs, successor IDs, idempotency fingerpri
The canonical compliance scenario is `media_buy_seller/typed_proposal_negotiation`. It exercises capability gating, satisfied and unsatisfied constraints, alternatives, immutable lineage, finalize atomicity, exact replay, acceptance, amendment, cancellation, double-finalize rejection, and multi-source ordering.
-The deterministic training profiles expose the exact `3.2-beta.4` proposal-negotiation wire contract. The ordinary `/sales/mcp` route remains the backward-compatible ask-only seller. Use the [public test token](/docs/quickstart#setup) with `https://test-agent.adcontextprotocol.org` and one of these capability-distinct routes:
+The deterministic training profiles expose the exact `3.2-beta.5` proposal-negotiation wire contract. The ordinary `/sales/mcp` route remains the backward-compatible ask-only seller. Use the [public test token](/docs/quickstart#setup) with `https://test-agent.adcontextprotocol.org` and one of these capability-distinct routes:
| Profile | Route | Deterministic behavior |
| --- | --- | --- |
@@ -496,7 +496,7 @@ The deterministic training profiles expose the exact `3.2-beta.4` proposal-negot
| Constrained seller | `/sales/profiles/constrained-seller/mcp` | USD floors, product conflicts, and at most two available alternatives |
| Finalization failure | `/sales/profiles/finalization-failure/mcp` | `hold_unavailable` plus `batch_aborted` siblings with no committed successors |
-Pin the beta exactly on every call. A stable `3.2` selector intentionally negotiates to the seller's stable compatibility response instead of silently opting into beta behavior. The bundled `ADCPMultiAgentClient.simple()` proposal path cannot yet target a different exact beta ordinal. Until [adcp-client#2609](https://github.com/adcontextprotocol/adcp-client/issues/2609) ships, construct a lower-level client with `wireAdcpVersion: "3.2-beta.4"` or send raw calls as below.
+Pin the beta exactly on every call. A stable `3.2` selector intentionally negotiates to the seller's stable compatibility response instead of silently opting into beta behavior. SDK 14 beta.7 accepts `wireAdcpVersion: "3.2-beta.5"` on its single- and multi-agent client configuration; raw calls remain useful for inspecting the exact envelope as below.
### Run the constrained profile
@@ -540,14 +540,14 @@ async function callTool(id, name, args) {
}
const version = {
- adcp_version: '3.2-beta.4',
+ adcp_version: '3.2-beta.5',
adcp_major_version: 3,
};
const nonce = crypto.randomUUID();
const capabilities = await callTool(1, 'get_adcp_capabilities', version);
if (
- capabilities.adcp_version !== '3.2-beta.4'
+ capabilities.adcp_version !== '3.2-beta.5'
|| capabilities.media_buy?.supports_proposals !== true
|| !capabilities.media_buy?.lifecycle_tools?.includes('refine_proposals')
) {
diff --git a/docs/reference/3-2-beta.mdx b/docs/reference/3-2-beta.mdx
index a38b4dfecc..42cb0e485c 100644
--- a/docs/reference/3-2-beta.mdx
+++ b/docs/reference/3-2-beta.mdx
@@ -15,7 +15,7 @@ before GA.
The 3.2 beta cycle started with a protocol-first checkpoint. The current
-TypeScript SDK beta now consumes the exact signed beta.4 bundle:
+TypeScript and Python SDK betas now consume the exact signed beta.5 bundle:
| Checkpoint | Purpose | Who should test |
|---|---|---|
@@ -23,60 +23,65 @@ TypeScript SDK beta now consumes the exact signed beta.4 bundle:
| `3.2.0-beta.1` | First integration-correction checkpoint | Pinned adopters comparing beta.0 and beta.1 behavior |
| `3.2.0-beta.2` | Second integration-correction checkpoint | Pinned adopters comparing beta.1 and beta.2 behavior |
| `3.2.0-beta.3` | Third integration-correction checkpoint | Pinned adopters comparing beta.2 and beta.3 behavior |
-| `3.2.0-beta.4` | Current protocol checkpoint | Buyer and seller teams running end-to-end integrations |
-| SDK wave | `@adcp/sdk@14.0.0-beta.5` embeds `3.2.0-beta.4`; Python and Go remain pending | TypeScript adopters, Python and Go maintainers, and early integration teams |
+| `3.2.0-beta.4` | Fourth integration checkpoint | Pinned adopters comparing beta.3 and beta.4 behavior |
+| `3.2.0-beta.5` | Current protocol checkpoint | Buyer and seller teams running end-to-end integrations |
+| SDK wave | `@adcp/sdk@14.0.0-beta.7` and Python `adcp==8.0.0b7` embed `3.2.0-beta.5`; Go remains pending | TypeScript and Python adopters, Go maintainers, and early integration teams |
Earlier betas remain useful as pinned implementation checkpoints. For new
-TypeScript testing, pair SDK beta.5 with protocol beta.4. Do not combine a newer
-wire pin with an SDK generated from an older bundle unless the SDK maintainer
-explicitly documents forward compatibility.
+TypeScript or Python testing, pair the packages above with protocol beta.5. Do
+not combine a newer wire pin with an SDK generated from an older bundle unless
+the SDK maintainer explicitly documents forward compatibility.
## Know which version string to use
The artifact version, wire pin, and documentation selector are related but not
interchangeable:
-| Surface | beta.0 | beta.1 | beta.2 | beta.3 | beta.4 |
-|---|---|---|---|---|---|
-| Protocol artifact | `3.2.0-beta.0` | `3.2.0-beta.1` | `3.2.0-beta.2` | `3.2.0-beta.3` | `3.2.0-beta.4` |
-| Git tag | `v3.2.0-beta.0` | `v3.2.0-beta.1` | `v3.2.0-beta.2` | `v3.2.0-beta.3` | `v3.2.0-beta.4` |
-| `adcp_version` wire pin | `"3.2-beta.0"` | `"3.2-beta.1"` | `"3.2-beta.2"` | `"3.2-beta.3"` | `"3.2-beta.4"` |
-| Documentation selector | `3.2-beta` | `3.2-beta` | `3.2-beta` | `3.2-beta` | `3.2-beta` |
+| Surface | beta.0 | beta.1 | beta.2 | beta.3 | beta.4 | beta.5 |
+|---|---|---|---|---|---|---|
+| Protocol artifact | `3.2.0-beta.0` | `3.2.0-beta.1` | `3.2.0-beta.2` | `3.2.0-beta.3` | `3.2.0-beta.4` | `3.2.0-beta.5` |
+| Git tag | `v3.2.0-beta.0` | `v3.2.0-beta.1` | `v3.2.0-beta.2` | `v3.2.0-beta.3` | `v3.2.0-beta.4` | `v3.2.0-beta.5` |
+| `adcp_version` wire pin | `"3.2-beta.0"` | `"3.2-beta.1"` | `"3.2-beta.2"` | `"3.2-beta.3"` | `"3.2-beta.4"` | `"3.2-beta.5"` |
+| Documentation selector | `3.2-beta` | `3.2-beta` | `3.2-beta` | `3.2-beta` | `3.2-beta` | `3.2-beta` |
Only send a prerelease wire pin to a peer that advertises that exact value in
`get_adcp_capabilities.adcp.supported_versions`. Prerelease pins are exact: a
-buyer pinned to `"3.2-beta.4"` must not silently negotiate to another beta, and
+buyer pinned to `"3.2-beta.5"` must not silently negotiate to another beta, and
a stable `"3.2"` pin must not silently negotiate to a beta.
-## Use the TypeScript SDK beta
+## Use matched SDK betas
-The TypeScript SDK is the first package with exact beta.4 support. The npm
+The TypeScript SDK has exact beta.5 support. The npm
`latest` tag remains on SDK 13, so opt into the preview explicitly:
```bash
-npm install @adcp/sdk@14.0.0-beta.5
+npm install @adcp/sdk@14.0.0-beta.7
```
SDK 14 includes typed caller and server support for the compact media-buy
-lifecycle, the beta.4 schemas and compliance assets, version-aware request
+lifecycle, the beta.5 schemas and compliance assets, version-aware request
signing, governance plan-adjustment reporting, and agent notification
configuration. It retains 3.0 and 3.1 adaptation for mixed-version testing.
See [Choose your SDK](/docs/building/by-layer/L4/choose-your-sdk) for the support
matrix and install guidance.
-## Use the beta.4 artifact directly
+Python `adcp==8.0.0b7` carries the same schemas and generated types. Its
+higher-level caller/server helpers remain role-dependent, so consult the SDK
+matrix before treating schema parity as complete L1–L3 coverage.
-Download the signed beta.4 protocol bundle rather
+## Use the beta.5 artifact directly
+
+Download the signed beta.5 protocol bundle rather
than copying schemas from `main` or from the moving `/schemas/latest/` path:
```bash
-curl -fLO https://adcontextprotocol.org/protocol/3.2.0-beta.4.tgz
-curl -fLO https://adcontextprotocol.org/protocol/3.2.0-beta.4.tgz.sha256
-shasum -a 256 -c 3.2.0-beta.4.tgz.sha256
-tar -xzf 3.2.0-beta.4.tgz
+curl -fLO https://adcontextprotocol.org/protocol/3.2.0-beta.5.tgz
+curl -fLO https://adcontextprotocol.org/protocol/3.2.0-beta.5.tgz.sha256
+shasum -a 256 -c 3.2.0-beta.5.tgz.sha256
+tar -xzf 3.2.0-beta.5.tgz
```
-The extracted `adcp-3.2.0-beta.4/` directory contains:
+The extracted `adcp-3.2.0-beta.5/` directory contains:
- `manifest.json` — release version, bundle contents, skills, and file count;
- `schemas/` — source-shaped and bundled request/response schemas, including
@@ -94,13 +99,13 @@ Useful direct-artifact tests are:
1. Generate or refresh types from the pinned bundled schemas.
2. Validate representative requests and responses for the roles you implement.
3. Compare your advertised tools with `schemas/manifest.json`.
-4. Exercise raw MCP or A2A calls with `adcp_version: "3.2-beta.4"` against a
+4. Exercise raw MCP or A2A calls with `adcp_version: "3.2-beta.5"` against a
staging peer that advertises the same pin.
5. Run the compliance assets with a source-compatible runner if you maintain
one. Do not weaken production authentication to accommodate a beta runner.
-**No 3.2 verification badge is currently issued for beta.4.** Treat it as an
+**No 3.2 verification badge is currently issued for beta.5.** Treat it as an
implementation input, not a certification target.
@@ -115,10 +120,10 @@ An SDK support claim must name all of the following:
- install command and one tested validation command.
The SDK compatibility matrix lives in [Choose your SDK](/docs/building/by-layer/L4/choose-your-sdk).
-Only the TypeScript row currently names exact beta.4 support. Treat generated
-3.2 types and validators in every other SDK as unavailable until its row names
-an exact package and protocol checkpoint. Raw JSON calls may still be possible,
-but they are not equivalent to SDK support.
+The TypeScript and Python rows name exact beta.5 support, with different
+higher-layer coverage. Treat generated 3.2 types and validators in every other
+SDK as unavailable until its row names an exact package and protocol checkpoint.
+Raw JSON calls may still be possible, but they are not equivalent to SDK support.
SDK maintainers should report protocol ambiguities against the
[AdCP repository](https://github.com/adcontextprotocol/adcp/issues). Report
@@ -126,24 +131,23 @@ SDK implementation bugs in the SDK's own repository. Accepted protocol fixes
receive a changeset and ship in a later beta; published betas are never rebuilt
in place.
-## Current beta.4 checkpoint
+## Current beta.5 checkpoint
-Protocol beta.1 through beta.4 are published, and TypeScript SDK beta.5 embeds the
-exact beta.4 bundle. Before treating a test result as beta.4 integration
-evidence, confirm that:
+Protocol beta.1 through beta.5 are published. TypeScript SDK beta.7 and Python
+SDK beta.7 embed the exact beta.5 bundle. Before treating a test result as
+beta.5 integration evidence, confirm that:
-- the support matrix names the exact SDK package and beta.4 protocol bundle;
-- both peers advertise `"3.2-beta.4"` before calls use that pin;
-- the integration scenarios below run against beta.4 and retain the exact
+- the support matrix names the exact SDK package and beta.5 protocol bundle;
+- both peers advertise `"3.2-beta.5"` before calls use that pin;
+- the integration scenarios below run against beta.5 and retain the exact
package and bundle versions;
- copy/paste installation and validation commands have been executed as
published;
-- any unsupported Python or Go surface is reported as unavailable or raw-only,
- rather than inferred from TypeScript support.
+- any unsupported Python helper or Go surface is reported as unavailable or
+ raw-only rather than inferred from schema support.
Later protocol betas repeat the same protocol-tag-then-SDK-confirmation flow.
-The current TypeScript pairing does not make future beta ordinals compatible by
-default.
+The current SDK pairings do not make future beta ordinals compatible by default.
## Required integration evidence
@@ -224,7 +228,7 @@ Include this information in a beta issue:
- schema path or JSON Pointer involved;
- expected and actual behavior;
- minimal redacted payload and validation error;
-- whether the problem blocks beta.4 interoperability.
+- whether the problem blocks beta.5 interoperability.
Never include credentials, signed URLs, bearer tokens, private keys, or live
customer payloads.
diff --git a/docs/reference/migration/3-1-to-3-2.mdx b/docs/reference/migration/3-1-to-3-2.mdx
index 02e35a1db2..78ebf5772a 100644
--- a/docs/reference/migration/3-1-to-3-2.mdx
+++ b/docs/reference/migration/3-1-to-3-2.mdx
@@ -8,10 +8,11 @@ description: "Role-based migration checklist for adopting the AdCP 3.2 beta whil
# Migrating from 3.1 to 3.2
-**3.2 is in beta.** Beta.4 is the current protocol checkpoint, with exact
-TypeScript support in `@adcp/sdk@14.0.0-beta.5`. Python and Go support remain
-pending. Keep production traffic pinned to `"3.1"` while validating 3.2 in
-staging.
+**3.2 is in beta.** Beta.5 is the current protocol checkpoint, with exact
+TypeScript support in `@adcp/sdk@14.0.0-beta.7` and exact schema/type parity in
+Python `adcp==8.0.0b7`. Python higher-level helpers remain partial, and Go exact
+support remains pending. Keep production traffic pinned to `"3.1"` while
+validating 3.2 in staging.
3.2 is a minor release over 3.1. Existing integrations do not need to adopt the
@@ -27,20 +28,20 @@ For prerelease artifacts and SDK timing, use the [3.2 beta program](/docs/refere
| Step | Who | Action |
|---|---|---|
| 1 | Everyone | Keep production on `"3.1"`; choose an exact 3.2 beta artifact for staging. |
-| 2 | SDK and codegen maintainers | Generate from the signed `3.2.0-beta.3` protocol tarball, not `main` or `/schemas/latest/`. |
+| 2 | SDK and codegen maintainers | Generate from the signed `3.2.0-beta.5` protocol tarball, not `main` or `/schemas/latest/`. |
| 3 | Sellers and agents | Advertise the exact prerelease in `adcp.supported_versions` and echo the release actually served. |
| 4 | Buyers | Send `adcp_version: "3.2-beta.N"` only after exact capability discovery; never silently move between beta pins. |
| 5 | Media-buy implementations | Move create/update success handling completely to `media_buy_status`. |
| 6 | Signing implementations | Require `content-digest` coverage and migrate request `Signature` and `Content-Digest` binary values to RFC 8941 padded Base64. |
| 7 | Creative implementations | Adopt canonical format capability/product discovery and isolate legacy projection at compatibility boundaries. |
-| 8 | Compliance operators | Pair `@adcp/sdk@14.0.0-beta.5` with exact beta.4 for TypeScript testing; require an equally explicit support statement for every other SDK. |
+| 8 | Compliance operators | Pair `@adcp/sdk@14.0.0-beta.7` or Python `adcp==8.0.0b7` with exact beta.5; require an equally explicit support statement for every other SDK. |
## Required to claim 3.2 behavior
### Serve and echo the exact release
-During beta, prerelease matching is exact. A seller serving beta.4 advertises
-`"3.2-beta.4"`. Do not advertise stable `"3.2"` before GA, silently negotiate
+During beta, prerelease matching is exact. A seller serving beta.5 advertises
+`"3.2-beta.5"`. Do not advertise stable `"3.2"` before GA, silently negotiate
between beta ordinals, or treat a major-only declaration as evidence of 3.2
support.
@@ -158,8 +159,8 @@ See [Cross-role governance enforcement](/docs/reference/migration/cross-role-gov
| Creative agent | Canonical supported-format capabilities, operation selection, and explicit compatibility handling for legacy named formats. |
| Signals or measurement agent | Exact beta pin and only the attestation, targeting, or feedback capabilities actually implemented. |
| Governance provider/service | Exact beta pin, `governance.campaign` experimental declaration, and the complete cross-role authorization migration. |
-| SDK maintainer | Signed bundle ingestion, generated-type/validator tests, exact support matrix entry, and protocol feedback destined for beta.1. |
-| Compliance operator | Matching beta assets end to end; no 3.2 badge from beta.0. |
+| SDK maintainer | Signed beta.5 bundle ingestion, generated-type/validator tests, exact support matrix entry, and protocol feedback destined for a later beta. |
+| Compliance operator | Matching beta.5 assets end to end; no 3.2 badge from a beta checkpoint. |
## Rollback
diff --git a/docs/reference/release-notes.mdx b/docs/reference/release-notes.mdx
index 81fff2d23d..c1d9e73976 100644
--- a/docs/reference/release-notes.mdx
+++ b/docs/reference/release-notes.mdx
@@ -11,7 +11,7 @@ Authoritative version-by-version release record for AdCP, with cumulative change
## Version 3.2.0
-**Status:** Beta cycle — minor release targeting the 3.2.0 milestone. Beta.0 was the protocol-first cut used as the canonical input for SDK work. Beta.4 is the current checkpoint, with exact TypeScript support in `@adcp/sdk@14.0.0-beta.5`. Stable wire shapes remain backward compatible except for the ratified `media_buy_status` cleanup and request-signature encoding migration described below; explicitly experimental surfaces may carry noticed changes under the [experimental-status contract](/docs/reference/experimental-status).
+**Status:** Beta cycle — minor release targeting the 3.2.0 milestone. Beta.0 was the protocol-first cut used as the canonical input for SDK work. Beta.5 is the current checkpoint, with exact TypeScript support in `@adcp/sdk@14.0.0-beta.7` and exact Python schema/type parity in `adcp==8.0.0b7`. Stable wire shapes remain backward compatible except for the ratified `media_buy_status` cleanup and request-signature encoding migration described below; explicitly experimental surfaces may carry noticed changes under the [experimental-status contract](/docs/reference/experimental-status).
### Beta checkpoints
@@ -20,8 +20,9 @@ Authoritative version-by-version release record for AdCP, with cumulative change
| `3.2.0-beta.0` | Publish signed schemas, compliance assets, skills, and manifest for implementation feedback | SDK support follows this cut; beta.0 is not an SDK-backed certification target |
| `3.2.0-beta.1` | Incorporate the first round of integration corrections | Superseded by beta.2 for new beta testing |
| `3.2.0-beta.2` | Publish the second integration checkpoint | Superseded by beta.3 for new beta testing |
-| `3.2.0-beta.4` | Publish the current protocol checkpoint (flexible-window availability, outcome_target reverse forecasting, availability conformance) | Exact TypeScript support in `@adcp/sdk@14.0.0-beta.5`; Python and Go remain pending |
| `3.2.0-beta.3` | Corrected protocol checkpoint, superseded by beta.4 | Superseded; remains available for pinned prerelease adopters |
+| `3.2.0-beta.4` | Publish flexible-window availability, outcome_target reverse forecasting, and availability conformance | Superseded; remains available for pinned prerelease adopters |
+| `3.2.0-beta.5` | Add cross-transport async identity and convergence, policy-backed creative rejection compliance, and outcome-target conformance | Exact TypeScript support in `@adcp/sdk@14.0.0-beta.7`; exact Python schema/type parity in `adcp==8.0.0b7`; Go remains pending |
Each beta is immutable. The `3.2-beta` documentation selector advances to the latest beta, while pinned protocol, schema, compliance, and documentation artifacts remain available. See the [3.2 beta program](/docs/reference/3-2-beta).
@@ -42,8 +43,8 @@ Each beta is immutable. The `3.2-beta` documentation selector advances to the la
| A seller or service | Advertise and echo the exact prerelease served, emit canonical status/retry shapes, and declare only capabilities actually implemented. |
| A signing implementation | Require `content-digest` coverage for every signed request body on a 3.2 signing endpoint. |
| A creative implementation | Prefer canonical capability and product format discovery; isolate named-format conversion at compatibility boundaries. |
-| An SDK maintainer | Generate from the current signed beta.3 tarball, publish an exact support statement, and route accepted protocol corrections into a later beta. |
-| A compliance operator | Pair `@adcp/sdk@14.0.0-beta.5` with beta.4 for TypeScript testing; do not infer Python or Go support or issue a 3.2 badge from beta.4. |
+| An SDK maintainer | Generate from the current signed beta.5 tarball, publish an exact support statement, and route accepted protocol corrections into a later beta. |
+| A compliance operator | Pair `@adcp/sdk@14.0.0-beta.7` or Python `adcp==8.0.0b7` with beta.5; do not infer missing higher-layer helpers or Go support, and do not issue a 3.2 badge from a beta checkpoint. |
### Required 3.2 migrations
diff --git a/docs/reference/versions.mdx b/docs/reference/versions.mdx
index 81005a843c..701ff359cb 100644
--- a/docs/reference/versions.mdx
+++ b/docs/reference/versions.mdx
@@ -13,7 +13,7 @@ description: "Every published AdCP version, its status, and its end-of-life date
| Status | Versions | Wire pin | What it means |
|---|---|---|---|
-| **Beta** | 3.2 (current prerelease: `beta.4`) | `"3.2-beta.4"` | Development and staging only. TypeScript SDK support is available in `@adcp/sdk@14.0.0-beta.5`; follow the [3.2 beta program](/docs/reference/3-2-beta). |
+| **Beta** | 3.2 (current prerelease: `beta.5`) | `"3.2-beta.5"` | Development and staging only. Exact TypeScript and Python beta packages are available; follow the [3.2 beta program](/docs/reference/3-2-beta). |
| **Active** | 3.1 (current stable: 3.1.15) | `"3.1"` | Current production minor release. See [What's New in AdCP 3.1](/docs/reference/whats-new-in-3-1) for the feature set. |
| **Maintained** | 3.0 (current stable: 3.0.24) | `"3.0"` | Supported previous minor for existing production integrations. Receives compatibility and security patches. |
| **End of life** | 2.0.0, 2.1.0, 2.5.0–2.5.3 | — | No patches. Migrate to 3.0. See [v2 sunset](/docs/reference/v2-sunset). |
@@ -45,7 +45,8 @@ Supported patches don't change the wire contract. Upgrading within 3.0.x is alwa
| Version | Released | Notes |
|---|---|---|
-| **3.2.0-beta.4** | 2026-08-20 | Current prerelease. `@adcp/sdk@14.0.0-beta.5` embeds this exact checkpoint. Use `"3.2-beta.4"` only when both peers explicitly advertise it. Adds flexible-window availability discovery and the outcome_target reverse-forecast input. |
+| **3.2.0-beta.5** | 2026-08-22 | Current prerelease. `@adcp/sdk@14.0.0-beta.7` and Python `adcp==8.0.0b7` embed this exact checkpoint. Use `"3.2-beta.5"` only when both peers explicitly advertise it. |
+| 3.2.0-beta.4 | 2026-08-20 | Integration checkpoint, superseded by beta.5 for new beta testing. Adds flexible-window availability discovery and the outcome_target reverse-forecast input. |
| 3.2.0-beta.3 | 2026-08-19 | Integration checkpoint, superseded by beta.4 for new beta testing. |
| 3.2.0-beta.2 | 2026-08-19 | Integration checkpoint, superseded by beta.3 for new beta testing. |
| 3.2.0-beta.1 | 2026-08-19 | Integration-correction checkpoint, superseded by beta.2 for new beta testing. |
@@ -89,8 +90,8 @@ Targeted for early 2027. 3.x is mid-cycle — see the [planned releases table](/
| If you are… | Use |
|---|---|
| Building a new production integration today | **3.1**. Pin `"3.1"` on the wire. |
-| Implementing or testing 3.2 without an SDK | Use the signed **3.2 beta.3** artifacts with raw-wire or code-generation workflows. Follow the [beta program](/docs/reference/3-2-beta). |
-| Running an SDK-backed 3.2 integration test | Use `@adcp/sdk@14.0.0-beta.5` with exact wire pin `"3.2-beta.4"`, or another SDK only after its support matrix row names an exact checkpoint. |
+| Implementing or testing 3.2 without an SDK | Use the signed **3.2 beta.5** artifacts with raw-wire or code-generation workflows. Follow the [beta program](/docs/reference/3-2-beta). |
+| Running an SDK-backed 3.2 integration test | Use `@adcp/sdk@14.0.0-beta.7` or Python `adcp==8.0.0b7` with exact wire pin `"3.2-beta.5"`; use Go only after its support matrix row names an exact checkpoint. |
| Running an existing 3.0 integration | Stay on **3.0** while you migrate, or move to **3.1** when your SDK and validation are ready. |
| Running a 2.5.x integration | Upgrade now — out of support. Start with the [migration guide](/docs/reference/migration). |
| Running a 2.0 or 2.1 integration | Upgrade now — out of support. |
diff --git a/docs/reference/whats-new-in-3-2.mdx b/docs/reference/whats-new-in-3-2.mdx
index 0b360d2523..289389c83a 100644
--- a/docs/reference/whats-new-in-3-2.mdx
+++ b/docs/reference/whats-new-in-3-2.mdx
@@ -8,10 +8,11 @@ description: "Adopter overview of AdCP 3.2: compact media buying, targeting-awar
# What's new in AdCP 3.2
-**3.2 is in beta.** The protocol beta.4 artifacts and
-`@adcp/sdk@14.0.0-beta.5` now support the same exact checkpoint. Python and Go
-3.2 packages are still pending. Later betas require a fresh SDK support
-statement against their exact bundle. See the
+**3.2 is in beta.** The protocol beta.5 artifacts,
+`@adcp/sdk@14.0.0-beta.7`, and Python `adcp==8.0.0b7` support the same exact
+checkpoint. Python higher-level helpers remain partial, and Go exact support is
+still pending. Later betas require a fresh SDK support statement against their
+exact bundle. See the
[3.2 beta program](/docs/reference/3-2-beta) before testing.
@@ -146,8 +147,8 @@ pin an exact 3.2 release, and follow the applicable migration notice.
| A media-buy implementer | Use `media_buy_status` on create/update success payloads; root `status` is the task-envelope state. |
| A signing implementation | Require `content-digest` coverage for every signed body on a 3.2 signing endpoint. |
| A creative implementation | Prefer canonical capability and product format discovery; retain legacy conversion only at a proven compatibility boundary. |
-| An SDK maintainer | Generate from the signed beta.0 tarball, name the exact supported bundle, and feed protocol corrections into beta.1. |
-| A compliance operator | Treat beta.0 as an implementation input; treat beta.1 as SDK-backed only after the exact beta.1 SDK refresh and integration evidence publish. Do not issue a 3.2 badge from beta.0. |
+| An SDK maintainer | Generate from the signed beta.5 tarball, name the exact supported bundle, and feed accepted protocol corrections into a later beta. |
+| A compliance operator | Pair beta.5 assets with an SDK that names that exact checkpoint, retain end-to-end integration evidence, and do not issue a 3.2 badge from a beta. |
## Start here
diff --git a/package-lock.json b/package-lock.json
index bff7ae708e..3aa88e737e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "adcontextprotocol",
"version": "3.2.0-beta.5",
"dependencies": {
- "@adcp/sdk": "14.0.0-beta.5",
+ "@adcp/sdk": "14.0.0-beta.7",
"@anthropic-ai/sdk": "^0.117.1",
"@asteasolutions/zod-to-openapi": "^8.5.0",
"@contentauth/c2pa-node": "^0.8.3",
@@ -131,9 +131,9 @@
}
},
"node_modules/@adcp/sdk": {
- "version": "14.0.0-beta.5",
- "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-beta.5.tgz",
- "integrity": "sha512-SRUoJ8Qe6NlbFSQJzgEdH2nVpKHo2w4AeuAoq4BwgcyLCNsw9mdDWuI3GAhz8niFjmcMOclhNpmyIAI4Nbqe+g==",
+ "version": "14.0.0-beta.7",
+ "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-14.0.0-beta.7.tgz",
+ "integrity": "sha512-wE+ucLhBfkgnAU3yqWO3FOdKo/4rrqMgsP4XUr9prhlVRjB5KMiqXgd+mboRsPLmW7Kk6ZRaOwcrfbffaus7qg==",
"license": "Apache-2.0",
"workspaces": [
".",
diff --git a/package.json b/package.json
index 27099a00ee..bb60597408 100644
--- a/package.json
+++ b/package.json
@@ -114,7 +114,8 @@
"test:error-code-drift": "node scripts/lint-error-code-drift.cjs",
"test:language-tag-refs": "node --test --test-force-exit --test-timeout=30000 tests/lint-language-tag-refs.test.cjs && node scripts/lint-language-tag-refs.cjs",
"test:compliance-snippets": "node --test --test-force-exit --test-timeout=30000 tests/generate-compliance-snippets.node.mjs tests/link-compliance-symbols.node.mjs && npm run build:compliance -- --check",
- "test:doc-compliance-drift": "node --test --test-force-exit --test-timeout=30000 tests/lint-doc-compliance-drift.test.cjs",
+ "test:doc-compliance-drift": "node --test --test-force-exit --test-timeout=30000 tests/lint-doc-compliance-drift.test.cjs && npm run test:release-instrumentation",
+ "test:release-instrumentation": "node --test --test-force-exit --test-timeout=30000 tests/release-instrumentation.test.cjs",
"test:substitution-vector-names": "node scripts/lint-substitution-vector-names.cjs && node --test --test-force-exit --test-timeout=30000 tests/universal-macro-translation-vectors.test.cjs",
"test:unit": "npm run test:canonical-pixel-ratio && npm run test:governance-jws-vectors && vitest run --dir tests/ --pool=threads",
"test:redteam": "tsx server/src/addie/testing/redteam-cli.ts",
@@ -162,7 +163,7 @@
"docs:json-field-audit": "node scripts/docs-json-field-audit.cjs"
},
"dependencies": {
- "@adcp/sdk": "14.0.0-beta.5",
+ "@adcp/sdk": "14.0.0-beta.7",
"@anthropic-ai/sdk": "^0.117.1",
"@asteasolutions/zod-to-openapi": "^8.5.0",
"@contentauth/c2pa-node": "^0.8.3",
diff --git a/server/src/addie/config-version.ts b/server/src/addie/config-version.ts
index ee176943e8..2ddeb25def 100644
--- a/server/src/addie/config-version.ts
+++ b/server/src/addie/config-version.ts
@@ -30,7 +30,7 @@ import { loadRules, loadResponseStyle } from './rules/index.js';
* Format: YYYY.MM.N where N is incremented for multiple changes in a month
* Example: 2025.01.1, 2025.01.2, 2025.02.1
*/
-export const CODE_VERSION = '2026.08.14';
+export const CODE_VERSION = '2026.08.15';
// Types
export interface ConfigVersion {
diff --git a/server/src/addie/mcp/certification-tools.ts b/server/src/addie/mcp/certification-tools.ts
index 60e459d52d..cb2d7f1eba 100644
--- a/server/src/addie/mcp/certification-tools.ts
+++ b/server/src/addie/mcp/certification-tools.ts
@@ -3021,7 +3021,7 @@ Tell ${codingTool}: "Build a buyer agent using @adcp/sdk that connects to the pu
The SDK handles protocol details — the learner focuses on orchestration logic.
-The targeting-aware discovery portion is a 3.2 schema-preview objective until https://github.com/adcontextprotocol/adcp/issues/6199 lands. Build schema fixtures that decompose one messy request into brief, filters, targeting_overlay, and required_overlay_support; exercise acceptance and rejection of targeting_resolution.modifications; and verify package targeting readback. Do not treat the current public test agent accepting or ignoring these preview fields as validation.
+Use the exact 3.2 beta.5 wire pin with @adcp/sdk@14.0.0-beta.7 for the targeting-aware discovery portion. Decompose one messy request into brief plus criteria.offer_filters, criteria.targeting_overlay, and criteria.required_overlay_support; verify that unsupported future-selection requirements filter products; review any targeting_resolution.modifications before purchase; and verify effective package targeting on readback. Treat the get_products compatibility facade's equivalent fields as compatibility evidence, not as proof that the compact tasks work.
Reference: ${SDKS_URL}
@@ -3034,16 +3034,16 @@ PRESENT THESE INSTRUCTIONS TO THE LEARNER:
Validate in two parts.
-1. Run the legacy live buying workflow against the public test agent and share the output. Use the \`adcp\` CLI:
+1. Run the compatibility buying workflow against the public test agent and share the output. Use the \`adcp\` CLI:
\`\`\`
-npx @adcp/sdk@latest test-mcp get_products '{"brief":""}'
+npx @adcp/sdk@14.0.0-beta.7 test-mcp get_products '{"adcp_version":"3.2-beta.5","buying_mode":"brief","brief":""}'
\`\`\`
Replace \`\` with your actual brief. Then run the full buying flow: get_products (select a canonical \`format_options[]\` entry) → create_media_buy → get_adcp_capabilities on the chosen creative endpoint → sync_creatives with \`format_kind\` and optional \`format_option_ref\`.
-2. Validate the 3.2 targeting-aware objectives with schema fixtures: request decomposition, required future targeting support, disclosed modification acceptance/rejection, and effective package readback. The public agent does not validate those fields until https://github.com/adcontextprotocol/adcp/issues/6199 lands. After that issue is complete, rerun the targeting-aware fixtures live.
+2. Validate the 3.2 targeting-aware objectives live with \`list_products\` and \`request_proposals\`: request decomposition, required future targeting support, disclosed modification acceptance/rejection, and effective package readback. First retain a capability response advertising the exact served version and relevant lifecycle tools. Then retain one supported and one unsupported requirement result, the beta.5 request/response envelopes, and the post-purchase package readback. An empty result for the unsupported requirement is evidence only when the same seller returns an eligible product for the supported control request.
-Paste the live output and fixture validation results. We'll verify both the current workflow and the 3.2 preview behavior without mistaking ignored fields for success.
+Paste the live output and validation results. We'll verify both the compatibility workflow and the native 3.2 behavior.
Reference: ${VALIDATE_URL}`;
}
diff --git a/server/src/db/migrations/549_idempotency_retention_boundary.sql b/server/src/db/migrations/549_idempotency_retention_boundary.sql
new file mode 100644
index 0000000000..7864a95d08
--- /dev/null
+++ b/server/src/db/migrations/549_idempotency_retention_boundary.sql
@@ -0,0 +1,14 @@
+-- SDK 14.0.0-beta.7 separates logical replay expiry from the physical
+-- retention boundary needed to honor the protocol clock-skew window.
+ALTER TABLE adcp_idempotency
+ ADD COLUMN IF NOT EXISTS retain_until TIMESTAMPTZ;
+
+-- Existing rows predate the explicit boundary. Preserve the default 60-second
+-- protocol clock-skew window rather than making them physically removable at
+-- their logical expiry.
+UPDATE adcp_idempotency
+SET retain_until = expires_at + INTERVAL '60 seconds'
+WHERE retain_until IS NULL;
+
+CREATE INDEX IF NOT EXISTS idx_adcp_idempotency_retain_until
+ ON adcp_idempotency(retain_until, expires_at);
diff --git a/server/src/db/migrations/550_curriculum_3_2_criteria.sql b/server/src/db/migrations/550_curriculum_3_2_criteria.sql
new file mode 100644
index 0000000000..663815b7f2
--- /dev/null
+++ b/server/src/db/migrations/550_curriculum_3_2_criteria.sql
@@ -0,0 +1,181 @@
+-- Add auditable AdCP 3.2 specialist criteria and the teaching actions that
+-- produce evidence for them. Prior-holder targeting remains gated by the
+-- GA/effective-date policy in docs/learning/policies/recertification.mdx.
+
+CREATE OR REPLACE FUNCTION _append_3_2_key_concept(
+ p_module_id text,
+ p_topic text,
+ p_teaching_notes text
+) RETURNS void AS $$
+BEGIN
+ UPDATE certification_modules
+ SET lesson_plan = jsonb_set(
+ lesson_plan,
+ '{key_concepts}',
+ COALESCE(lesson_plan->'key_concepts', '[]'::jsonb) || jsonb_build_array(
+ jsonb_build_object('topic', p_topic, 'teaching_notes', p_teaching_notes)
+ )
+ )
+ WHERE id = p_module_id
+ AND NOT EXISTS (
+ SELECT 1
+ FROM jsonb_array_elements(COALESCE(lesson_plan->'key_concepts', '[]'::jsonb)) concept
+ WHERE concept->>'topic' = p_topic
+ );
+
+ IF NOT FOUND AND NOT EXISTS (SELECT 1 FROM certification_modules WHERE id = p_module_id) THEN
+ RAISE EXCEPTION 'Module % not found', p_module_id;
+ END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT _append_3_2_key_concept('S1', 'AdCP 3.2 compact planning and buying',
+ 'Use advertised lifecycle_tools to choose list_products, request_proposals, refine_proposals, buy_products, accept_proposal, and control_media_buy. Separate offer filters, current targeting, and future targeting support; require readback evidence for effective targeting. Use availability_horizon and outcome_target only when advertised, and distinguish planning guidance from a finalized commercial obligation.');
+
+SELECT _append_3_2_key_concept('S2', 'AdCP 3.2 creative interoperability',
+ 'Validate logical dimensions separately from intrinsic pixel density and distinguish accepted pixel_ratios from required_pixel_ratios rendition coverage. Apply the advertised VAST validation level, format-required accessibility detail, and provenance requirements; a declaration is not independent verification.');
+
+SELECT _append_3_2_key_concept('S6', 'AdCP 3.2 request and webhook integrity',
+ 'On 3.2 request-signing endpoints, every signed body covers content-digest and request Signature/Content-Digest sf-binary values use RFC 8941 padded standard Base64 without a legacy-parser fallback. For webhook delivery, operation_id is buyer correlation while the payload idempotency_key is sender-generated dedup identity and remains stable across exact retries.');
+
+DROP FUNCTION _append_3_2_key_concept(text, text, text);
+
+CREATE OR REPLACE FUNCTION _instrument_3_2_exercise(
+ p_module_id text,
+ p_exercise_id text,
+ p_tool text,
+ p_guidance text
+) RETURNS void AS $$
+DECLARE
+ defs jsonb;
+ updated jsonb := '[]'::jsonb;
+ ex jsonb;
+ actions jsonb;
+ exercise_matched boolean := false;
+BEGIN
+ SELECT exercise_definitions INTO defs
+ FROM certification_modules
+ WHERE id = p_module_id;
+
+ IF defs IS NULL OR jsonb_typeof(defs) <> 'array' THEN
+ RAISE EXCEPTION 'Module % not found or has no exercise_definitions array', p_module_id;
+ END IF;
+
+ FOR ex IN SELECT * FROM jsonb_array_elements(defs)
+ LOOP
+ IF ex->>'id' = p_exercise_id THEN
+ exercise_matched := true;
+ actions := COALESCE(ex->'sandbox_actions', '[]'::jsonb);
+ IF NOT EXISTS (
+ SELECT 1 FROM jsonb_array_elements(actions) action
+ WHERE action->>'tool' = p_tool AND action->>'guidance' = p_guidance
+ ) THEN
+ actions := actions || jsonb_build_array(
+ jsonb_build_object('tool', p_tool, 'guidance', p_guidance)
+ );
+ ex := jsonb_set(ex, '{sandbox_actions}', actions);
+ END IF;
+ END IF;
+ updated := updated || jsonb_build_array(ex);
+ END LOOP;
+
+ IF NOT exercise_matched THEN
+ RAISE EXCEPTION 'Exercise % not found in module %', p_exercise_id, p_module_id;
+ END IF;
+
+ UPDATE certification_modules
+ SET exercise_definitions = updated
+ WHERE id = p_module_id;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT _instrument_3_2_exercise('S1', 's1_ex1', 'request_proposals',
+ 'On the exact 3.2 beta wire, submit criteria with offer_filters, targeting_overlay, and required_overlay_support; compare supported and unsupported requirements, review sparse targeting_resolution modifications, and retain the proposal plus effective package targeting readback as evidence.');
+
+SELECT _instrument_3_2_exercise('S1', 's1_ex1', 'list_products',
+ 'When the seller advertises the planning features, test a flexible availability_horizon and an outcome_target. Verify complete window coverage or disclosed incomplete gaps, total_budget_guidance, and a matching forecast metric without claiming that forecast guidance is a guarantee.');
+
+SELECT _instrument_3_2_exercise('S2', 's2_ex1', 'sync_creatives',
+ 'Run the 3.2 canonical pixel-density and rendition reference cases, then submit VAST, accessibility, and provenance cases against the seller capabilities. Record the exact validation level and distinguish declared metadata from independently verified evidence.');
+
+SELECT _instrument_3_2_exercise('S6', 's6_ex2', 'get_adcp_capabilities',
+ 'Read the 3.2 request_signing posture, then use the request-signing conformance vectors to demonstrate required content-digest coverage, exact-byte mismatch rejection, padded standard Base64 sf-binary parsing, and rejection of a legacy Base64URL request token without fallback.');
+
+SELECT _instrument_3_2_exercise('S6', 's6_ex4', 'sync_accounts',
+ 'Capture webhook retry evidence showing buyer-supplied operation_id remains correlation while payload idempotency_key remains stable across exact retries; verify a changed logical fire gets a fresh payload key. Do not treat the SDK emitter delivery_id input as an MCP payload field.');
+
+DROP FUNCTION _instrument_3_2_exercise(text, text, text, text);
+CREATE OR REPLACE FUNCTION _append_criterion(
+ p_module_id text,
+ p_exercise_id text,
+ p_criterion_id text,
+ p_text text
+) RETURNS void AS $$
+DECLARE
+ defs jsonb;
+ updated jsonb := '[]'::jsonb;
+ ex jsonb;
+ criteria jsonb;
+ exercise_matched boolean := false;
+BEGIN
+ SELECT exercise_definitions INTO defs
+ FROM certification_modules
+ WHERE id = p_module_id;
+
+ IF defs IS NULL OR jsonb_typeof(defs) <> 'array' THEN
+ RAISE EXCEPTION 'Module % not found or has no exercise_definitions array', p_module_id;
+ END IF;
+
+ FOR ex IN SELECT * FROM jsonb_array_elements(defs)
+ LOOP
+ IF ex->>'id' = p_exercise_id THEN
+ exercise_matched := true;
+ criteria := COALESCE(ex->'success_criteria', '[]'::jsonb);
+ IF NOT EXISTS (
+ SELECT 1 FROM jsonb_array_elements(criteria) c
+ WHERE c->>'id' = p_criterion_id
+ ) THEN
+ criteria := criteria || jsonb_build_array(
+ jsonb_build_object('id', p_criterion_id, 'text', p_text)
+ );
+ ex := jsonb_set(ex, '{success_criteria}', criteria);
+ END IF;
+ END IF;
+ updated := updated || jsonb_build_array(ex);
+ END LOOP;
+
+ IF NOT exercise_matched THEN
+ RAISE EXCEPTION 'Exercise % not found in module %', p_exercise_id, p_module_id;
+ END IF;
+
+ UPDATE certification_modules
+ SET exercise_definitions = updated
+ WHERE id = p_module_id;
+END;
+$$ LANGUAGE plpgsql;
+
+-- S1 — compact buying, targeting-aware discovery, and planning inputs.
+SELECT _append_criterion('S1', 's1_ex1', 's1_ex1_sc_compact_media_buy_lifecycle',
+ 'Discovers advertised lifecycle_tools and completes the immutable request_proposals → refine_proposals → finalize → accept_proposal flow, using buy_products for direct offers and control_media_buy only for in-envelope operational changes.');
+
+SELECT _append_criterion('S1', 's1_ex1', 's1_ex1_sc_targeting_aware_discovery',
+ 'Separates concrete targeting_overlay values from required_overlay_support, filters products by binding overlay_support, reviews sparse targeting_resolution modifications before purchase, and verifies effective package targeting on readback.');
+
+SELECT _append_criterion('S1', 's1_ex1', 's1_ex1_sc_availability_and_outcome_planning',
+ 'Uses availability_horizon for flexible-window discovery and outcome_target for reverse forecasting, distinguishes forecast guidance from a delivery guarantee, and carries the selected priced product snapshot into purchase.');
+
+-- S2 — 3.2 creative quality, interoperability, and provenance.
+SELECT _append_criterion('S2', 's2_ex1', 's2_ex1_sc_pixel_density_and_renditions',
+ 'Selects logical dimensions and pixel_ratio correctly, validates intrinsic pixel dimensions, and distinguishes a required rendition set from optional creative variants or delivery-time resizing.');
+
+SELECT _append_criterion('S2', 's2_ex1', 's2_ex1_sc_vast_accessibility_provenance',
+ 'Applies the advertised VAST validation level, preserves required accessibility detail, and evaluates synthetic-depiction and provenance evidence while distinguishing declared metadata from independent verification.');
+
+-- S6 — 3.2 request and webhook integrity changes.
+SELECT _append_criterion('S6', 's6_ex2', 's6_ex2_sc_3_2_signed_body_integrity',
+ 'For a 3.2 request-signing endpoint, verifies that content-digest is covered on every body-bearing request and encodes Signature and Content-Digest binary values as RFC 8941 padded standard Base64 rather than the 3.0/3.1 Base64URL override.');
+
+SELECT _append_criterion('S6', 's6_ex4', 's6_ex4_sc_webhook_delivery_identity',
+ 'Distinguishes buyer-supplied operation_id correlation from sender-generated webhook idempotency_key deduplication, preserves one payload key across retries of the same byte-identical logical fire, and uses a fresh payload key when the payload or lifecycle observation changes.');
+
+DROP FUNCTION _append_criterion(text, text, text, text);
diff --git a/server/src/db/migrations/551_webhook_delivery_outbox.sql b/server/src/db/migrations/551_webhook_delivery_outbox.sql
new file mode 100644
index 0000000000..10b4043685
--- /dev/null
+++ b/server/src/db/migrations/551_webhook_delivery_outbox.sql
@@ -0,0 +1,54 @@
+-- SDK 14.0.0-beta.7 requires publisher-side webhook identity and the exact
+-- retry snapshot to survive process crashes and replica changes.
+
+CREATE TABLE IF NOT EXISTS adcp_webhook_delivery_bindings (
+ publisher_scope TEXT NOT NULL,
+ tenant_scope TEXT NOT NULL,
+ delivery_id TEXT NOT NULL,
+ status TEXT NOT NULL CHECK (status IN ('bound', 'retired')),
+ idempotency_key TEXT,
+ payload_fingerprint TEXT,
+ first_attempt_at TIMESTAMPTZ,
+ retain_until TIMESTAMPTZ,
+ PRIMARY KEY (publisher_scope, tenant_scope, delivery_id),
+ CHECK (
+ (status = 'bound'
+ AND idempotency_key IS NOT NULL
+ AND payload_fingerprint IS NOT NULL
+ AND first_attempt_at IS NOT NULL
+ AND retain_until IS NOT NULL)
+ OR
+ (status = 'retired'
+ AND idempotency_key IS NULL
+ AND payload_fingerprint IS NULL
+ AND first_attempt_at IS NULL
+ AND retain_until IS NULL)
+ )
+);
+
+CREATE INDEX IF NOT EXISTS idx_adcp_webhook_delivery_binding_retention
+ ON adcp_webhook_delivery_bindings(retain_until)
+ WHERE status = 'bound';
+
+CREATE TABLE IF NOT EXISTS adcp_webhook_delivery_outbox (
+ publisher_scope TEXT NOT NULL,
+ tenant_scope TEXT NOT NULL,
+ delivery_id TEXT NOT NULL,
+ snapshot_encrypted TEXT NOT NULL,
+ snapshot_iv TEXT NOT NULL,
+ snapshot_digest TEXT NOT NULL,
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+ next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ lease_until TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (publisher_scope, tenant_scope, delivery_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_adcp_webhook_delivery_outbox_pending
+ ON adcp_webhook_delivery_outbox(next_attempt_at, lease_until, created_at);
+
+COMMENT ON COLUMN adcp_webhook_delivery_outbox.snapshot_encrypted IS
+ 'AES-256-GCM encrypted exact WebhookDeliverySnapshot; may contain callback authentication credentials';
+COMMENT ON COLUMN adcp_webhook_delivery_outbox.snapshot_digest IS
+ 'Keyed SHA-256 equality evidence; never a plaintext credential hash';
diff --git a/server/src/training-agent/FRAMEWORK_MIGRATION.md b/server/src/training-agent/FRAMEWORK_MIGRATION.md
index 5404eeee73..825f137c8c 100644
--- a/server/src/training-agent/FRAMEWORK_MIGRATION.md
+++ b/server/src/training-agent/FRAMEWORK_MIGRATION.md
@@ -60,7 +60,7 @@ working during the 3.x compatibility window.
proposal negotiation is a 3.2 feature.
- Capability projection removes lifecycle and proposal-refinement metadata for
pre-3.2 callers.
-- The current wire bundle is `3.2-beta.4`; `3.2-beta.0` remains only as the
+- The current wire bundle is `3.2-beta.5`; `3.2-beta.0` remains only as the
historical feature-introduction boundary for rejected discovery responses.
## Invariants for future changes
diff --git a/server/src/training-agent/agent-notification-configs.ts b/server/src/training-agent/agent-notification-configs.ts
index 9747821c9e..2c52b10ce7 100644
--- a/server/src/training-agent/agent-notification-configs.ts
+++ b/server/src/training-agent/agent-notification-configs.ts
@@ -191,7 +191,7 @@ export async function syncAgentNotificationConfigs(
};
}
-/** Adapter for the decisioning platform's custom-tool seam. SDK 14.0.0-beta.4
+/** Adapter for the decisioning platform's custom-tool seam. SDK 14.0.0-beta.7
* projects platform capabilities over low-level protocol capabilities, so the
* built-in protocol handler cannot currently be mounted through
* createAdcpServerFromPlatform. Keep the domain implementation identical and
diff --git a/server/src/training-agent/idempotency.ts b/server/src/training-agent/idempotency.ts
index 84fcb0adb9..bb6c3de04f 100644
--- a/server/src/training-agent/idempotency.ts
+++ b/server/src/training-agent/idempotency.ts
@@ -31,7 +31,6 @@ import {
type IdempotencyCheckResult,
} from '@adcp/sdk/server';
import { isDatabaseInitialized, getPool } from '../db/client.js';
-import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
export const REPLAY_TTL_SECONDS = 86400;
@@ -45,6 +44,7 @@ const IN_FLIGHT_RETRY_HINT_CAP_SECONDS = 30;
const CLAIM_SAVE_SAFETY_SECONDS = 30;
const SCOPE_SEPARATOR = '\u001F';
const PENDING_OWNER_FIELD = '__adcp_pending_owner';
+const RETRYABLE_HASH_PREFIX = '__adcp_retryable__:';
const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_.:-]{16,255}$/;
@@ -225,12 +225,23 @@ export function createHashAwareIdempotencyStore(
const isPending = (response: unknown): boolean => response === null || pendingOwner(response) !== undefined;
+ const retryableRequestHash = (payloadHash: string): string | undefined => {
+ if (!payloadHash.startsWith(RETRYABLE_HASH_PREFIX)) return undefined;
+ const requestHash = payloadHash.slice(RETRYABLE_HASH_PREFIX.length);
+ return /^[0-9a-f]{64}$/.test(requestHash) ? requestHash : undefined;
+ };
+
const classify = (
entry: Awaited>,
expectedHash: string,
): Exclude | null => {
if (!entry) return null;
const nowSeconds = Math.floor(Date.now() / 1e3);
+ const releasedPayloadHash = retryableRequestHash(entry.payloadHash);
+ if (releasedPayloadHash !== undefined) {
+ if (entry.expiresAt + clockSkewSeconds < nowSeconds) return { kind: 'expired' };
+ return releasedPayloadHash === expectedHash ? null : { kind: 'conflict' };
+ }
if (entry.expiresAt + clockSkewSeconds < nowSeconds) {
return isPending(entry.response) ? null : { kind: 'expired' };
}
@@ -256,16 +267,30 @@ export function createHashAwareIdempotencyStore(
): Promise => {
const cacheKey = scopedKey(principal, key, extraScope);
const expectedHash = hashPayload(payload);
- const cached = classify(await backend.get(cacheKey), expectedHash);
+ const cachedEntry = await backend.get(cacheKey);
+ const cached = classify(cachedEntry, expectedHash);
if (cached) return cached;
const expiresAt = Math.floor(Date.now() / 1e3) + IN_FLIGHT_TTL_SECONDS;
const owner = randomUUID();
- const claimed = await backend.putIfAbsent(cacheKey, {
+ const claimEntry = {
payloadHash: expectedHash,
response: { [PENDING_OWNER_FIELD]: owner },
expiresAt,
- });
+ retainUntil: expiresAt + clockSkewSeconds,
+ };
+ const nowSeconds = Math.floor(Date.now() / 1e3);
+ const releasedPayloadHash = cachedEntry
+ ? retryableRequestHash(cachedEntry.payloadHash)
+ : undefined;
+ const expiredPending = cachedEntry
+ && isPending(cachedEntry.response)
+ && cachedEntry.expiresAt + clockSkewSeconds < nowSeconds;
+ const claimed = releasedPayloadHash !== undefined
+ ? await backend.replaceIfPayloadHash(cacheKey, cachedEntry!.payloadHash, claimEntry)
+ : expiredPending
+ ? await backend.replaceIfPayloadHashAndExpired(cacheKey, cachedEntry.payloadHash, claimEntry)
+ : await backend.putIfAbsent(cacheKey, claimEntry);
if (claimed) return { kind: 'miss', payloadHash: expectedHash, claimToken: owner };
const rechecked = classify(await backend.get(cacheKey), expectedHash);
@@ -281,25 +306,48 @@ export function createHashAwareIdempotencyStore(
? operation()
: withBackendKeyLock(backend, cacheKey, operation);
},
+ async renew({ principal, key, extraScope, claimToken }) {
+ const cacheKey = scopedKey(principal, key, extraScope);
+ const expiresAt = Math.floor(Date.now() / 1e3) + IN_FLIGHT_TTL_SECONDS;
+ const renewed = await renewOwnedClaim(cacheKey, claimToken, backend, pendingOwner, {
+ payloadHash: '',
+ response: { [PENDING_OWNER_FIELD]: claimToken },
+ expiresAt,
+ retainUntil: expiresAt + clockSkewSeconds,
+ });
+ if (!renewed) throw new Error('Idempotency claim ownership was lost before its lease could be renewed.');
+ },
async save({ principal, key, payloadHash, response, extraScope, claimToken }) {
const cacheKey = scopedKey(principal, key, extraScope);
+ const expiresAt = Math.floor(Date.now() / 1e3) + ttlSeconds;
const saved = await replaceOwnedClaim(cacheKey, claimToken, backend, pendingOwner, {
payloadHash,
response,
- expiresAt: Math.floor(Date.now() / 1e3) + ttlSeconds,
+ expiresAt,
+ retainUntil: expiresAt + clockSkewSeconds,
});
if (!saved) throw new Error('Idempotency claim ownership was lost before the response could be published.');
},
async release({ principal, key, extraScope, claimToken }) {
const cacheKey = scopedKey(principal, key, extraScope);
- await deleteOwnedClaim(cacheKey, claimToken, backend, pendingOwner);
+ const released = await markOwnedClaimRetryable(
+ cacheKey,
+ claimToken,
+ backend,
+ pendingOwner,
+ ttlSeconds,
+ clockSkewSeconds,
+ );
+ if (!released) throw new Error('Idempotency claim ownership was lost before it could be released.');
},
async saveTransientError({ principal, key, payloadHash, response, extraScope, claimToken }) {
const cacheKey = scopedKey(principal, key, extraScope);
+ const expiresAt = Math.floor(Date.now() / 1e3) + TRANSIENT_ERROR_TTL_SECONDS;
const saved = await replaceOwnedClaim(cacheKey, claimToken, backend, pendingOwner, {
payloadHash,
response,
- expiresAt: Math.floor(Date.now() / 1e3) + TRANSIENT_ERROR_TTL_SECONDS,
+ expiresAt,
+ retainUntil: expiresAt + clockSkewSeconds,
});
if (!saved) throw new Error('Idempotency claim ownership was lost before the response could be published.');
},
@@ -322,6 +370,32 @@ export function createHashAwareIdempotencyStore(
};
}
+async function renewOwnedClaim(
+ cacheKey: string,
+ owner: string,
+ backend: FencedIdempotencyBackend,
+ pendingOwner: (response: unknown) => string | undefined,
+ entry: Parameters[1],
+): Promise {
+ const current = await backend.get(cacheKey);
+ if (!current || pendingOwner(current.response) !== owner) return false;
+ const renewedEntry = { ...entry, payloadHash: current.payloadHash };
+ if (backend.replaceIfPendingOwner) {
+ return backend.replaceIfPendingOwner(
+ cacheKey,
+ owner,
+ renewedEntry,
+ Math.floor(Date.now() / 1e3) - 1,
+ );
+ }
+ return withBackendKeyLock(backend, cacheKey, async () => {
+ const lockedCurrent = await backend.get(cacheKey);
+ if (!lockedCurrent || pendingOwner(lockedCurrent.response) !== owner) return false;
+ await backend.put(cacheKey, { ...entry, payloadHash: lockedCurrent.payloadHash });
+ return true;
+ });
+}
+
async function replaceOwnedClaim(
cacheKey: string,
owner: string,
@@ -344,17 +418,33 @@ async function replaceOwnedClaim(
});
}
-async function deleteOwnedClaim(
+async function markOwnedClaimRetryable(
cacheKey: string,
owner: string,
backend: FencedIdempotencyBackend,
pendingOwner: (response: unknown) => string | undefined,
+ ttlSeconds: number,
+ clockSkewSeconds: number,
): Promise {
- if (backend.deleteIfPendingOwner) return backend.deleteIfPendingOwner(cacheKey, owner);
+ const current = await backend.get(cacheKey);
+ if (!current || pendingOwner(current.response) !== owner) return false;
+ const expiresAt = Math.floor(Date.now() / 1e3) + ttlSeconds;
+ const releasedEntry = {
+ payloadHash: `${RETRYABLE_HASH_PREFIX}${current.payloadHash}`,
+ response: null,
+ expiresAt,
+ retainUntil: expiresAt + clockSkewSeconds,
+ };
+ if (backend.replaceIfPendingOwner) {
+ return backend.replaceIfPendingOwner(cacheKey, owner, releasedEntry, 0);
+ }
return withBackendKeyLock(backend, cacheKey, async () => {
- const current = await backend.get(cacheKey);
- if (!current || pendingOwner(current.response) !== owner) return false;
- await backend.delete(cacheKey);
+ const lockedCurrent = await backend.get(cacheKey);
+ if (!lockedCurrent || pendingOwner(lockedCurrent.response) !== owner) return false;
+ await backend.put(cacheKey, {
+ ...releasedEntry,
+ payloadHash: `${RETRYABLE_HASH_PREFIX}${lockedCurrent.payloadHash}`,
+ });
return true;
});
}
@@ -388,11 +478,12 @@ function fencedPgBackend(): FencedIdempotencyBackend {
`UPDATE adcp_idempotency
SET payload_hash = $3,
response = $4::jsonb,
- expires_at = TO_TIMESTAMP($5)
+ expires_at = TO_TIMESTAMP($5),
+ retain_until = TO_TIMESTAMP($6)
WHERE scoped_key = $1
AND response ->> '__adcp_pending_owner' = $2
- AND expires_at > TO_TIMESTAMP($6)`,
- [cacheKey, owner, entry.payloadHash, JSON.stringify(entry.response), entry.expiresAt, minimumLeaseExpiry],
+ AND expires_at > TO_TIMESTAMP($7)`,
+ [cacheKey, owner, entry.payloadHash, JSON.stringify(entry.response), entry.expiresAt, entry.retainUntil, minimumLeaseExpiry],
);
return (result.rowCount ?? 0) > 0;
},
@@ -421,60 +512,9 @@ export function getIdempotencyStore(): OwnedIdempotencyStore {
return storeInstance;
}
-const SDK_CLAIM_SEPARATOR = '.';
-const sdkClaimContext = new AsyncLocalStorage