Skip to content

spec(webhooks): require idempotency_key on all webhook payloads (#2416)#2417

Merged
bokelley merged 3 commits into
mainfrom
bokelley/issue-2416
Apr 19, 2026
Merged

spec(webhooks): require idempotency_key on all webhook payloads (#2416)#2417
bokelley merged 3 commits into
mainfrom
bokelley/issue-2416

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Closes #2416.

  • Add a required idempotency_key to every webhook payload schema — MCP task webhook, collection-list-changed, property-list-changed, artifact-webhook — so receivers have a single, canonical dedup field across all webhook types.
  • Update webhooks.mdx Reliability section, MCP example payload, and best practices to make idempotency_key the canonical dedup key. Replace the fragile (task_id, status, timestamp) tuple guidance.
  • Update governance docs (collection_lists.mdx, property_lists.mdx) and content-standards implementation guide examples to include idempotency_key.

Why now (3.0, not 3.1)

Webhook delivery is at-least-once per spec, so receivers must dedupe — but today only mcp-webhook-payload carries dedup-usable fields, and only as the fragile (task_id, status, timestamp) tuple. The other four webhook payloads have no dedup field at all.

We're still in 3.0-rc, so adding a required field doesn't break any released integration. Deferring to 3.1 would ship the first GA with a known-gappy dedup story.

Field format

Same name and format as the request-side idempotency_key in security.mdx — string, 16–255 chars, ^[A-Za-z0-9_.:-]{16,255}$. UUID v4 is the recommended value. Reusing the exact field shape avoids inventing a second "id for dedup" concept.

Scope note — reporting-webhook.json

The issue listed core/reporting-webhook.json as a fifth payload needing the field. On review, that schema is the reporting webhook configuration (passed in create_media_buy/update_media_buy), not a payload. There is no published reporting-webhook payload schema today, so it is out of scope here. If we later publish one, it will need the same idempotency_key field.

Changeset

minor — published schema change to 4 schemas under static/schemas/source/.

Test plan

  • npm run build:schemas — bundled schemas regenerate cleanly
  • npm run test:schemas — all 7 structural checks pass
  • npm run test:examples — 31 example validations pass (including updated MCP webhook examples)
  • npm run test:composed — 12 composed-schema validations pass
  • npm run test:json-schema — 249 doc JSON blocks with $schema validate
  • npm run test:migrations, npm run test:docs-nav, npm run test:extensions, npm run test:error-handling — pass
  • npm run test:unit + npm run typecheck via precommit hook — pass

🤖 Generated with Claude Code

Webhook delivery is at-least-once, and receivers must dedupe. Prior to
this change, only mcp-webhook-payload had dedup-usable fields — as the
fragile (task_id, status, timestamp) tuple — and the governance and
artifact webhook payloads had none.

Add a required sender-generated `idempotency_key` (same format as the
request-side key: 16-255 chars matching ^[A-Za-z0-9_.:-]{16,255}$) to:

- core/mcp-webhook-payload.json
- collection/collection-list-changed-webhook.json
- property/property-list-changed-webhook.json
- content-standards/artifact-webhook-payload.json

Update webhooks.mdx Reliability, payload-format examples, and best
practices to make idempotency_key the canonical dedup field. Update
governance and content-standards example payloads to match.

Schema change to published protocol → minor bump. Safe inside 3.0-rc
(no released integrations break).

Note: core/reporting-webhook.json is the reporting *config*, not a
payload — no payload schema exists for reporting webhooks today, so
it is out of scope here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Schema Link Check Results

Commit: 0d0ae7d - spec(webhooks): duplicates MUST get 2xx, not 409 — avoid retry storms

⚠️ Warnings (schema not yet released)

These schemas exist in source but haven't been released yet. The links will be broken until the next version is published:

  • https://adcontextprotocol.org/schemas/v3/enums/specialism.json
    • Schema exists in latest (source) but not yet released in v3
    • Action: This link will work after next 3.x release is published

To fix: Either:

  1. Wait for the next release and merge this PR after the release is published
  2. Use latest instead of a version alias if you need the link to work immediately (note: latest is the development version and may change)
  3. Coordinate with maintainers to cut a new release before merging

bokelley and others added 2 commits April 19, 2026 15:45
…ntropy, DoS

Addresses code-review and security-review feedback on the initial commit:

Must Fix:
- Add required `idempotency_key` to `brand/revocation-notification.json` — it
  was also a retry-delivered payload and had a prior `notification_id` field
  with a different name/format. Rename to unify the protocol-wide dedup
  vocabulary (safe in 3.0-rc). Update `acquire_rights.mdx` and
  `walkthrough-rights-licensing.mdx`.
- Define the dedup scope across all five schemas and in webhooks.mdx:
  `(authenticated sender identity, idempotency_key)`, where sender identity
  is derived from the verified HMAC secret or Bearer credential — never a
  payload field. A receiver integrated with multiple publishers MUST NOT
  collapse their keyspaces.

Should Fix:
- Require cryptographically random values (UUID v4 recommended); predictable
  keys enable a pre-seed-the-cache attack that suppresses later legitimate
  events.
- Align receiver TTL with request-side: SHOULD persist ≥ 24h, senders
  SHOULD NOT retry beyond.
- Fix the example code: write dedup state before side effects (fail-closed
  on crash), and separate dedup storage from ordering storage — ordering
  keys on task_id, not idempotency_key, because two distinct events can
  still arrive out of order.

Nice to Have addressed:
- Explicit note that webhook receivers do not verify payload equivalence
  (unlike request-side IDEMPOTENCY_CONFLICT) — senders are solely
  responsible for generating a fresh key on every distinct event.
- Cache-growth DoS guidance: receivers SHOULD bound dedup storage per
  sender and return 429 before growing unbounded.
- Artifact-webhook wording: "per distinct emission" not "per delivery
  attempt" (the prior phrasing implied key changes on retries).
- Changeset scope list updated; called out the notification_id rename and
  property_lists signature field cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At-least-once senders interpret non-2xx as "delivery failed" and escalate
back-off. A receiver that returns 409 Conflict on a successfully-deduped
event turns correct behavior into a retry storm.

Add a normative line to the §Reliability receiver requirements, update the
example code to return 200 explicitly on both dedup and stale-event paths,
and add "Return 2xx on duplicates" to the Best practices list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 14a3864 into main Apr 19, 2026
16 checks passed
bokelley added a commit that referenced this pull request Apr 19, 2026
Following #2417, webhooks are now signed under a symmetric variant of
the existing request-signing profile: publisher signs outbound with an
adcp_use: "webhook-signing" key published in adagents.json; subscriber
verifies against the publisher's JWKS. Removes the shared-secret-in-
the-wire tradeoff of the HMAC scheme and unifies one signing mechanism
across the protocol.

- Baseline-required in 3.0 (no capability advertisement)
- HMAC-SHA256 / Bearer remain as opt-in fallback via
  push_notification_config.authentication through 3.x
- authentication removed from the schema in 4.0

Covered components fixed at @method, @target-uri, @authority,
content-type, content-digest (digest REQUIRED — the body is the event).
tag="adcp/webhook-signing/v1" is distinct from the request-signing tag
so neither signature can be replayed as the other. Verifier checklist
reuses the 14-step request-verifier flow with webhook-specific
substitutions and a parallel webhook_signature_* error taxonomy.

9421 webhook conformance vectors ship alongside the request-signing
vectors in a follow-up; webhook-hmac-sha256.json is marked legacy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 19, 2026
Four clarifications raised in downstream review of #2423:

- Mode selection is a switch, not both. Explicit rule that presence of
  push_notification_config.authentication selects exactly one signing
  mode per webhook URL; sellers MUST NOT sign both ways, buyers MUST
  NOT attempt 9421-first-then-HMAC fallback (downgrade oracle).
- JWKS cache policy for long-running flows. Explicit statement that
  verifiers MUST NOT pin a single JWKS snapshot for a task's lifetime,
  and refetch-on-kid-miss is the load-bearing mechanism for mid-task
  key rotation.
- Retry semantics for signature failures. Persistent signature failure
  is not a transient error — senders MUST stop retrying a given event
  on webhook_signature_* and route sustained failure rates to incident
  response rather than retry-storming.
- Verifier checklist portability. Explicit call-out at the top of the
  webhook verifier checklist that the 14 steps are the request-signing
  checklist with exactly two substitutions (tag value,
  direction-of-trust resolution); SDKs SHOULD share verifier code
  between the two profiles.

Also added refinements to #2426 tracking comment (ephemeral endpoint
mechanism, retry-replay shape, per-step vs shared receivers, dep chain
on #2417 + #2423, JWKS rotation test-kit coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 19, 2026
* spec(webhooks): unify webhook signing on RFC 9421 profile

Following #2417, webhooks are now signed under a symmetric variant of
the existing request-signing profile: publisher signs outbound with an
adcp_use: "webhook-signing" key published in adagents.json; subscriber
verifies against the publisher's JWKS. Removes the shared-secret-in-
the-wire tradeoff of the HMAC scheme and unifies one signing mechanism
across the protocol.

- Baseline-required in 3.0 (no capability advertisement)
- HMAC-SHA256 / Bearer remain as opt-in fallback via
  push_notification_config.authentication through 3.x
- authentication removed from the schema in 4.0

Covered components fixed at @method, @target-uri, @authority,
content-type, content-digest (digest REQUIRED — the body is the event).
tag="adcp/webhook-signing/v1" is distinct from the request-signing tag
so neither signature can be replayed as the other. Verifier checklist
reuses the 14-step request-verifier flow with webhook-specific
substitutions and a parallel webhook_signature_* error taxonomy.

9421 webhook conformance vectors ship alongside the request-signing
vectors in a follow-up; webhook-hmac-sha256.json is marked legacy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(changeset): use correct package name adcontextprotocol

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(webhooks): second-pass fixes from protocol/security/docs review

Protocol review (interop-breaking):
- Fixed JWKS publication path: seller publishes webhook-signing keys at
  the jwks_uri on its own brand.json agents[] entry (parallel to how
  request-signing agent keys work). The previous text pointed at
  adagents.json agents[].jwks_uri which is not a field on that schema.
- Fixed stale dedup-scope language in webhooks.mdx Reliability section
  that still referenced "HMAC secret or Bearer token" as the identity
  source. Now covers 9421 keyid-based identity as the default, with
  cross-scheme dedup guidance for migration.

Security review:
- Downgrade/injection resistance: buyer's webhook-signing preference
  travels in authentication on an inbound request that is typically
  not 9421-signed in 3.0, so an on-path mutator could strip or inject
  the block. Added normative rules: sellers MUST log unexpected
  authentication blocks, SHOULD require the inbound request to be
  9421-signed when authentication is present, and buyers MUST alarm
  on mode mismatch. Durable preference belongs in onboarding, not
  per-request.
- Trust anchor and blast radius: explicit paragraph documenting that
  the seller's brand.json origin is the trust anchor, compromise there
  compromises all webhooks from that seller until revocation, and
  buyers SHOULD pin the jwks_uri URL learned at onboarding.
- Webhook replay dedup sizing: per-keyid cap 100K (10x lower than
  request-side 1M) and aggregate cap 10M across all sellers, because
  a buyer-side cache sees fan-in from every seller integrated with.
- HMAC→9421 migration: buyers MUST disable HMAC verifier after cutover;
  sellers SHOULD reject authentication blocks from migrated
  counterparties; dedup keyspace SHOULD map both identity forms to the
  same logical seller.

Docs review:
- Inlined the 14-step webhook verifier checklist instead of delegating
  to the request-side checklist by reference with a bullet-list of
  substitutions. Webhook verifiers and request verifiers are often
  different codebases.
- Updated webhooks.mdx frontmatter description (HMAC → RFC 9421 default
  with legacy HMAC fallback).
- Promoted the legacy-HMAC note at the top of webhooks.mdx to a warning
  callout with a link to the downgrade-resistance rules.
- Normalized "publisher" → "seller" in webhooks.mdx for vocabulary
  consistency with the rest of the spec.
- Tightened "strictly stronger" claim to specify the dimensions on which
  9421 is stronger than HMAC (identity, key management) vs where it's
  comparable (body integrity).
- Tightened acquire_rights.mdx and collection_lists.mdx to enumerate
  the 9421 normative rules (covered components, tag) instead of only
  specifying the HMAC legacy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(webhooks): downstream review clarifications

Four clarifications raised in downstream review of #2423:

- Mode selection is a switch, not both. Explicit rule that presence of
  push_notification_config.authentication selects exactly one signing
  mode per webhook URL; sellers MUST NOT sign both ways, buyers MUST
  NOT attempt 9421-first-then-HMAC fallback (downgrade oracle).
- JWKS cache policy for long-running flows. Explicit statement that
  verifiers MUST NOT pin a single JWKS snapshot for a task's lifetime,
  and refetch-on-kid-miss is the load-bearing mechanism for mid-task
  key rotation.
- Retry semantics for signature failures. Persistent signature failure
  is not a transient error — senders MUST stop retrying a given event
  on webhook_signature_* and route sustained failure rates to incident
  response rather than retry-storming.
- Verifier checklist portability. Explicit call-out at the top of the
  webhook verifier checklist that the 14 steps are the request-signing
  checklist with exactly two substitutions (tag value,
  direction-of-trust resolution); SDKs SHOULD share verifier code
  between the two profiles.

Also added refinements to #2426 tracking comment (ephemeral endpoint
mechanism, retry-replay shape, per-step vs shared receivers, dep chain
on #2417 + #2423, JWKS rotation test-kit coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 19, 2026
…onformance

Closes the conformance gap for outbound webhook behavior that storyboards
cannot currently assert (#2426):

- idempotency_key presence on every webhook payload (#2417)
- idempotency_key byte-identical across retries of the same logical event
- RFC 9421 webhook signature validity when advertised (#2423)

Storyboards are unidirectional today (runner → agent). Verifying outbound
webhooks requires the runner to host a receiver during test execution and
observe live deliveries. This PR adds the spec-side surface.

Clean seam: the runner does NOT reimplement signature verification or
idempotency dedup. It delegates to @adcp/client primitives
(AsyncHandlerConfig.webhookDedup, WebhookMetadata.idempotency_key,
Activity.type == "webhook_duplicate") so the same code that production
receivers rely on is what the conformance runner exercises. Referenced by
URL in test-kits/webhook-callbacks-runner.yaml so the spec and client-lib
PR (adcontextprotocol/adcp-client#629) cross-reference cleanly.

Schema additions:
- New specialism enum value "webhook-callbacks"
- New specialisms/webhook-callbacks/index.yaml (preview status, four
  phases: capability discovery, idempotency_key presence,
  idempotency_key stability, signature validity)
- New test-kits/webhook-callbacks-runner.yaml (endpoint modes:
  loopback_mock default, proxy_url for conformance; per-step receiver
  URLs; 5xx-then-2xx retry-replay shape)
- New step types in storyboard-schema.yaml: expect_webhook,
  expect_webhook_retry_keys_stable, expect_webhook_signature_valid,
  plus substitution variables {{runner.webhook_base}} and
  {{runner.webhook_url:<step_id>}}, cross-specialism helpers
  (expect_max_deliveries_per_logical_event, requires_contract)

Closing a long-standing gap in universal/idempotency.yaml:
- The replay-side-effect invariant ("no duplicate webhooks on replay")
  was previously graded by a manual-audit step. Replaced with a
  programmatic expect_webhook assertion using
  expect_max_deliveries_per_logical_event: 1 gated on the
  webhook_callbacks_runner contract. Runners without a webhook receiver
  skip as not_applicable.

Preview status pending @adcp/client runner implementation tracked at
#2426.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 20, 2026
…k idempotency)

Webhooks now a full citizen of the 3.0 trust surface — signing unified on the
RFC 9421 profile (baseline-required for sellers) and every webhook payload
carries a required idempotency_key. Also picks up several spec-hardening and
governance-tightening PRs that landed post-merge.

CHANGELOG.md — Adds to trust-surface minor changes:
- Unify webhook signing on RFC 9421 profile (#2423)
- Require idempotency_key on every webhook payload (#2416, #2417)
- check_governance required on every spend-commit (#2403, #2419)
- Experimental status mechanism + custom pricing escape hatch (#2422)

Plus patch entries for:
- submitted branch on create_media_buy + ai_generated_image right-use (#2425)
- time semantics + activate_signal idempotency (#2407)
- known-limitations/privacy-considerations/why-not FAQs + platform-agnostic lint (#2427)
- scope-truthfulness pass on three audited claims (#2385, #2404)

release-notes.mdx — Renames #1 item to "Trust Surface: Idempotency, Request
Signing, Signed Governance, and Signed Webhooks" and promotes webhooks to a
first-class bullet. Updates intro paragraph to mention webhook signing + payload
idempotency. Adds 4 new rows to the breaking changes table (webhook signing,
webhook idempotency_key, revocation-notification.notification_id rename, plus
MediaBuy.pending_approval placement). New webhook migration bullet at the top
of the rc.3 adopter list.

whats-new-in-v3.mdx — Expands the trust surface section with two new paragraphs
covering webhook signing under the 9421 profile and required payload
idempotency across all five webhook payload schemas.

prerelease-upgrades.mdx — 4 new breaking-change rows (webhook signing, webhook
payload idempotency, notification_id → idempotency_key, etc.) + 8 new additive
bullets covering webhook signing, webhook idempotency, check_governance on
spend-commit, experimental status mechanism, submitted branch, time semantics,
and the new reference pages.

migration/index.mdx — Adds webhook signing and webhook idempotency rows to the
v2→v3 migration checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 20, 2026
…onformance (#2431)

* spec(compliance): webhook-callbacks specialism for outbound webhook conformance

Closes the conformance gap for outbound webhook behavior that storyboards
cannot currently assert (#2426):

- idempotency_key presence on every webhook payload (#2417)
- idempotency_key byte-identical across retries of the same logical event
- RFC 9421 webhook signature validity when advertised (#2423)

Storyboards are unidirectional today (runner → agent). Verifying outbound
webhooks requires the runner to host a receiver during test execution and
observe live deliveries. This PR adds the spec-side surface.

Clean seam: the runner does NOT reimplement signature verification or
idempotency dedup. It delegates to @adcp/client primitives
(AsyncHandlerConfig.webhookDedup, WebhookMetadata.idempotency_key,
Activity.type == "webhook_duplicate") so the same code that production
receivers rely on is what the conformance runner exercises. Referenced by
URL in test-kits/webhook-callbacks-runner.yaml so the spec and client-lib
PR (adcontextprotocol/adcp-client#629) cross-reference cleanly.

Schema additions:
- New specialism enum value "webhook-callbacks"
- New specialisms/webhook-callbacks/index.yaml (preview status, four
  phases: capability discovery, idempotency_key presence,
  idempotency_key stability, signature validity)
- New test-kits/webhook-callbacks-runner.yaml (endpoint modes:
  loopback_mock default, proxy_url for conformance; per-step receiver
  URLs; 5xx-then-2xx retry-replay shape)
- New step types in storyboard-schema.yaml: expect_webhook,
  expect_webhook_retry_keys_stable, expect_webhook_signature_valid,
  plus substitution variables {{runner.webhook_base}} and
  {{runner.webhook_url:<step_id>}}, cross-specialism helpers
  (expect_max_deliveries_per_logical_event, requires_contract)

Closing a long-standing gap in universal/idempotency.yaml:
- The replay-side-effect invariant ("no duplicate webhooks on replay")
  was previously graded by a manual-audit step. Replaced with a
  programmatic expect_webhook assertion using
  expect_max_deliveries_per_logical_event: 1 gated on the
  webhook_callbacks_runner contract. Runners without a webhook receiver
  skip as not_applicable.

Preview status pending @adcp/client runner implementation tracked at
#2426.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(compliance): refactor webhook conformance to universal, not specialism

Specialisms are opt-in capability claims (like signed-requests, where
some agents verify signed requests and some don't). Webhook emission
isn't like that — any agent that accepts push_notification_config on
any operation MUST emit conformant webhooks in 3.0. There's no agent
specializing in webhook callbacks; it's a cross-cutting requirement.

Refactored:
- specialisms/webhook-callbacks/index.yaml → universal/webhook-emission.yaml
- test-kits/webhook-callbacks-runner.yaml → test-kits/webhook-receiver-runner.yaml
  (test-kit is renamed for clarity — it provides a webhook receiver; it
  doesn't belong to any specialism)
- Test-kit applies_to now lists universals: webhook-emission (primary)
  and idempotency (no-duplicate-webhooks-on-replay assertion)
- Dropped webhook-callbacks entry from specialism.json enum
- Reframed universal/idempotency.yaml cross-reference from specialism
  to universal
- Grading: universal grades not_applicable when the agent advertises
  no webhook-emitting operations, or when the runner doesn't host a
  webhook receiver. No more claim-to-participate; any agent that
  accepts push_notification_config is in scope automatically.

Drive-by fix: push-notification-config.json description still said
"adagents.json-published key" (stale from pre-#2423 second-pass).
Updated to "brand.json agents[] jwks_uri" matching the spec text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(compliance): address 8 runner-implementation feedback items on webhook-emission

From parallel runner work in adcp-client, eight spec gaps surfaced:

1. schema_ref overload on expect_webhook. Renamed to
   webhook_payload_schema_ref so it doesn't collide with the step-level
   schema_ref/response_schema_ref used for caller→agent request/response
   schemas on emitting steps.

2. operation_id "caller" ambiguity. Clarified that the runner mints the
   operation_id in the URL template; the agent MUST NOT mint its own and
   MUST echo the runner-supplied value back in the webhook payload.

3. Retry-stability + 9421 nonce gap. expect_webhook_retry_keys_stable
   now MUST verify the 9421 signature on EVERY delivery in the retry
   loop when 9421 is in effect, using the run-scoped (keyid, nonce)
   replay store. Catches publishers that stably reuse both
   idempotency_key (correct) and 9421 nonce (incorrect) — a class of
   bug that retry-stability alone would silently miss.

4. Cross-step replay store. The runner MUST share one (keyid, nonce)
   replay store across every webhook delivery in a storyboard run
   (across every expect_webhook_signature_valid and every retry under
   expect_webhook_retry_keys_stable). Per-step stores make cross-step
   nonce replay silently undetectable.

5. retry_trigger caps. Hard-capped retry_trigger.count at 10 and
   allowlisted retry_trigger.http_status to {429, 500, 502, 503, 504}.
   Prevents typo'd storyboards from turning runners into DoS
   amplifiers in proxy_url mode.

6. HTTPS required on proxy_url endpoint mode. loopback_mock is
   in-process (no TLS surface); proxy_url now explicitly requires
   https scheme, rejectable at load time.

7. shared_receiver deferred. Fan-in dedup semantics (filter matching
   across multiple emitters, retry-replay precedence, replay store
   scoping) are underspecified. shared_receiver is out of scope for
   v1; storyboard authors MUST use per-step receivers. Future fan-in
   storyboards will either flesh out shared_receiver or pick a
   different idiom.

8. Unresolved substitution behavior. Runners MUST grade the storyboard
   not_applicable (preflight) or the step failed (step-time) when a
   {{runner.*}} substitution can't be resolved. Shipping the literal
   {{...}} token on the wire is a runner-side conformance bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp-client-python that referenced this pull request Apr 20, 2026
…receiver UX

Implements AdCP 3.0 signed-webhook support across sender and receiver sides.

Sender (new adcp.webhooks.WebhookSender):
- One class, one call per delivery. Owns the signing key; typed methods
  for every webhook kind (send_mcp, send_revocation_notification,
  send_artifact_webhook, send_collection_list_changed,
  send_property_list_changed) plus send_raw escape hatch.
- resend(result) replays the exact signed bytes under a fresh signature
  — spec-correct retry that preserves idempotency_key for receiver dedup.
- from_jwk(...) fails fast when adcp_use != "webhook-signing" with a
  message explaining key separation.
- Blocks overrides of signature-binding headers in extra_headers; rejects
  Content-Type overrides too. Snapshots the JWK mapping once to prevent
  TOCTOU in construction. Explicit __repr__ keeps the private key out of
  debug traces.

Receiver (new adcp.webhooks.WebhookReceiver):
- Verify + dedupe + parse in one call. Returns a WebhookOutcome with
  typed payload so route handlers don't try/except around crypto failures.
- RFC 9421 webhook profile (tag adcp/webhook-signing/v1, content-digest
  required, JWK adcp_use == "webhook-signing") — thin wrapper over the
  existing request-signing verifier; request-family error codes are
  retagged to webhook_signature_* at the boundary.
- Legacy HMAC-SHA256 fallback opt-in for 3.x migration
  (LegacyHmacFallback.from_shared_secret(...)); downgrade-guarded by
  default (HMAC only fires when no 9421 headers are present).
- Spec regex validation for idempotency_key and content-type check before
  parse. WWW-Authenticate values whitelisted to prevent response-header
  injection.

Dedup (WebhookDedupStore):
- Scoped by (authenticated_sender_identity, idempotency_key) with 24h TTL
  floor. Namespace parameter prevents accidental aliasing when the same
  backend is shared with the request-side IdempotencyStore.

Types: regenerated from 3.0-rc schemas — idempotency_key is now required
on all 5 webhook payloads (McpWebhookPayload, RevocationNotification,
CollectionListChangedWebhook, PropertyListChangedWebhook,
ArtifactWebhookPayload).

Tests: 1697 passing, 10 skipped. New coverage includes sender-receiver
FastAPI/ASGI end-to-end suites (13 sender, 6 receiver), byte-identity
regression, retry-replay, downgrade-attack guard, content-type rejection,
malformed idempotency_key rejection. Integration scaffold for verifying
against a real reference agent ships skipped-by-default with setup docs.

Spec: adcontextprotocol/adcp#2417
Spec: adcontextprotocol/adcp#2423

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 22, 2026
…A banners (#2302)

* docs: 3.0 release notes, migration guide, specialism threading, and GA banners (#2177)

Closes #2177 and #2290. Comprehensive documentation update for AdCP 3.0 GA.

**Release documentation:**
- Add 3.0.0 CHANGELOG entry covering every change since rc.3
- Add rc.3 → 3.0 prerelease upgrade guide with breaking changes table,
  before/after JSON examples, and schema file references
- Expand whats-new rc.3 → 3.0 highlights with Specialisms + Compliance as
  a headline 3.0 pillar
- Exit changeset prerelease mode so next release is stable 3.0.0
- Remove stale "use 2.5 for production" banner from intro; drop misleading
  "(recommended for production)" from v2.5.0 historical schema-versioning note

**Specialisms + storyboards threaded through training:**
- Add "Specialisms you can validate" tables to all 5 specialist modules
- Add "Specialisms you can claim" to publisher and platform tracks;
  "Validating across sellers" to buyer track
- Add Domain/Specialism/Storyboard glossary + Compliance claims section
  to foundations A2 module
- Map skills to specialisms in build-an-agent; note brand-rights has no
  skill yet
- Add "What changed in 3.0" callout to Compliance Catalog covering
  rename/merge/promotion
- Thread specialisms mention through intro and quickstart

**GA launch banners (#2290):**
- Add Mintlify banner to docs.json: "AdCP 3.0 is now GA — see what's new"
- Add dismissible announcement strip to AAO homepage
- Bump docs.json default version label 3.0-rc → 3.0

**Pre-commit hook fix:**
- Force vitest `pool: 'threads'` in vitest.config.ts. The default forks pool
  hangs indefinitely under non-TTY stdin (git pre-commit hook) because server
  module init in imported code keeps child processes alive. Threads share the
  parent lifecycle and exit cleanly. Same test speed.

**Other fixes:**
- Remove stale "AdCP 3.0 Proposal" banner from collection_lists.mdx
- Fix major_versions: [1] → [3] in get_adcp_capabilities examples
- Update publisher track B3 to use governance_context + purchase_type
- Trim changelog.mdx stub to match its actual role (link to GitHub)

Expert-reviewed across code, protocol, DX, docs, copy, product, security,
and education. All feedback applied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(3.0): foreground trust surface story (signing, idempotency, signed governance)

Weaves the post-rc.3 trust-surface work into the 3.0 narrative across CHANGELOG,
release notes, whats-new, and migration guides. Previously these PRs were merged
to main but not reflected in the 3.0 documentation story.

CHANGELOG.md — Adds "Breaking Changes — trust surface" and "Minor Changes —
trust surface" sections:

Breaking:
- idempotency_key required on all mutating requests (#2315)
- IO approval at task layer, not MediaBuy.pending_approval (#2270, #2351)
- Art 22 / Annex III as schema invariants (#2310, #2338)
- inventory-lists → property-lists, collection-lists split out (#2332, #2336)
- domains → protocols compliance taxonomy (#2300)

Minor:
- RFC 9421 request signing profile (#2323)
- Signed JWS governance_context (#2316)
- Universal security baseline storyboard (#2304)
- Signed-requests runner harness + runner output contract (#2350, #2352)
- Cross-instance state persistence required (#2363)
- Security narrative + principal terminology retirement (#2381)
- URL canonicalization + sf-binary pins (#2341, #2342, #2343)

Plus docs/patch entries for Operating an Agent, release cadence, CHARTER.md,
AI disclosure, creative lifecycle hardening, signals baseline, Scope3 → CSBS
rename, and numerous training-agent/storyboard fixes.

release-notes.mdx — Rewrites the 3.0.0 intro to lead with the trust surface.
Adds #1 "Trust Surface" item covering idempotency, signing, signed governance,
and universal security storyboard.

whats-new-in-v3.mdx — New "Trust surface: idempotency, request signing, and
signed governance" section at the top of New Capabilities.

prerelease-upgrades.mdx — New breaking-change rows and additive bullets for
trust-surface primitives.

migration/index.mdx — Adds idempotency_key, request signing, signed
governance_context, IO approval task-layer move, and Art 22 schema invariants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(3.0): fold webhooks into trust surface (signed webhooks + webhook idempotency)

Webhooks now a full citizen of the 3.0 trust surface — signing unified on the
RFC 9421 profile (baseline-required for sellers) and every webhook payload
carries a required idempotency_key. Also picks up several spec-hardening and
governance-tightening PRs that landed post-merge.

CHANGELOG.md — Adds to trust-surface minor changes:
- Unify webhook signing on RFC 9421 profile (#2423)
- Require idempotency_key on every webhook payload (#2416, #2417)
- check_governance required on every spend-commit (#2403, #2419)
- Experimental status mechanism + custom pricing escape hatch (#2422)

Plus patch entries for:
- submitted branch on create_media_buy + ai_generated_image right-use (#2425)
- time semantics + activate_signal idempotency (#2407)
- known-limitations/privacy-considerations/why-not FAQs + platform-agnostic lint (#2427)
- scope-truthfulness pass on three audited claims (#2385, #2404)

release-notes.mdx — Renames #1 item to "Trust Surface: Idempotency, Request
Signing, Signed Governance, and Signed Webhooks" and promotes webhooks to a
first-class bullet. Updates intro paragraph to mention webhook signing + payload
idempotency. Adds 4 new rows to the breaking changes table (webhook signing,
webhook idempotency_key, revocation-notification.notification_id rename, plus
MediaBuy.pending_approval placement). New webhook migration bullet at the top
of the rc.3 adopter list.

whats-new-in-v3.mdx — Expands the trust surface section with two new paragraphs
covering webhook signing under the 9421 profile and required payload
idempotency across all five webhook payload schemas.

prerelease-upgrades.mdx — 4 new breaking-change rows (webhook signing, webhook
payload idempotency, notification_id → idempotency_key, etc.) + 8 new additive
bullets covering webhook signing, webhook idempotency, check_governance on
spend-commit, experimental status mechanism, submitted branch, time semantics,
and the new reference pages.

migration/index.mdx — Adds webhook signing and webhook idempotency rows to the
v2→v3 migration checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(3.0): expert-review fixes — accurate trust-surface primitives, adcp_use placement, no protocol default on TTL

Addresses three rounds of expert review (product, protocol, copy) on the 3.0
release-docs PR.

**Accuracy fixes (protocol expert)**:
- idempotency_key TTL: "default 24h" was wrong — schema has no default, 24h
  is only recommended. Clients MUST NOT assume one. Fixed in CHANGELOG,
  release-notes, whats-new, prerelease-upgrades.
- adcp_use placement: webhook-signing JWK carries adcp_use:"webhook-signing"
  on the JWK inside the JWKS document at jwks_uri — NOT as a field on the
  brand.json agents[] entry. Clarified in all four files.
- kid uniqueness: kid values MUST be unique across adcp_use purposes within
  a JWKS. Added to whats-new and prerelease-upgrades.
- check_governance qualifier: restored "when a governance agent is
  configured on the plan" — was previously unconditional MUST. Added
  PERMISSION_DENIED as the rejection code.
- UUID v4: spec allows ^[A-Za-z0-9_.:-]{16,255}$; UUID v4 is an AdCP Verified
  requirement, not schema-enforced. Clarified across docs.
- Legacy HMAC opt-in: named the actual field (push_notification_config.
  authentication.credentials) where 3.x buyers opt into HMAC fallback.
- 4.0 removal scope: the entire `authentication` object is removed in 4.0,
  not just HMAC.
- webhook_signature_* reworded to "typed reason codes defined in the
  Security guide" — they are not enum values in error-code.json.

**Narrative fixes (product + copy)**:
- Primitive count reconciled: release-notes headline said 4, list had 5+1,
  whats-new said 3. Now consistently 4, grouped by symmetry (requests:
  idempotency + signing; webhooks: signing + idempotency; governance JWS
  as capstone). Universal security storyboard moved to item #2
  (specialisms/storyboards) where it belongs as verification, not a
  primitive.
- Opener paragraph tightened. "AdCP 3.0 makes agent-to-agent ad buying
  cryptographically verifiable and retry-safe." leads — the
  "safe for real money" overreach is softened throughout.
- Connective tissue added: "Trust primitives define the bar; storyboards
  test it; AdCP Verified certifies it." in item #2.
- Item #5 (IO approval) merged into item #12 (media buy lifecycle) —
  they described the same change. Numbered items now 1-14 clean.
- Release notes #14 consolidated brand-schema + operating-an-agent +
  cadence + experimental-status + known-limitations into one "Operating
  an Agent, Release Cadence, CHARTER" item; brand-schema extensions
  pointer to CHANGELOG.
- Long 14/15-step checklists deduplicated — release-notes and
  whats-new now link to the Security guide rather than restating them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(3.0): remove preview specialisms, fill brand baseline, thread trust surface

- Remove sales-streaming-tv, sales-exchange, sales-retail-media, and
  measurement-verification from the 3.0 specialism enum and catalog. They
  were causing confusion about what's stable at GA. Tracking reintroduction
  with authoritative storyboards in 3.1: #2511.
- Fill the brand protocol baseline storyboard (identity-only); rights
  lifecycle remains in the experimental brand-rights specialism.
- Rewrite quickstart step 3 to teach idempotency_key + replay semantics
  and step 4 to replace HMAC-SHA256 webhooks with the RFC 9421 webhook
  profile (JWKS-anchored, typed webhook_signature_* reason codes).
- Add a trust-surface callout to the intro walkthrough so Alex's team
  encounters signing, idempotency replay, and signed governance JWS
  together at the media-buy execution moment.
- Clean up cross-references in compliance-catalog, specialist learning
  modules (governance, media-buy), platform track, migration guide, and
  what's-new page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(3.0): apply external rc.3 review fixes — versioning clarity, SI experimental flags

- versioning: empty 3.1/3.2 Notes column removed; table now points at
  GitHub milestones for current candidate scope (no fixed commitments).
- versioning: retire "architecture committee led by Brian O'Kelley"
  phrasing; cross-cutting decisions happen in working-group forums and
  public GitHub issues. Brian's role remains named in FAQ and CHARTER.
- whats-new: flag Sponsored Intelligence as (experimental) in the
  protocol-scope comparison rows and implementer checklist, matching
  treatment already present on the dedicated SI section, FAQ, and
  governance overview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(3.0): defensibility review P0 fixes + cutover checklist (#2538)

- known-limitations:47 tense fix. Art 22 / Annex III schema invariant
  shipped via #2310 on 2026-04-18.
- release-notes trust-framing. RFC 9421 request signing is optional in
  3.0 (required only for AdCP Verified). Reframed as "retry-safe and
  auditable, with optional end-to-end request signing."
- whats-new Verified self-attestation note. Agents publish their own
  runner-output.json; AAO does not audit or gate issuance.
- policy-registry CSBS provenance. Formal IP donation instrument is
  tracked in #2314 (Evergreen, legal-gated). CSBS ships under AAO
  custodianship until signed assignment is on file.

Launch-day cutover tracked in #2538.

* docs(3.0): defensibility review P1 fixes

* docs(governance): vendor Bylaws + Membership Agreement into repo (closes #2440)

* docs(governance): replace HTML-comment mirror headers with MDX-safe blockquotes

* docs(governance): absolute GitHub URL for governance README pointers (Mintlify broken-link fix)

* chore(husky): pin mintlify to 4.2.500 in pre-push (useState crash in 4.2.515+)

* docs(3.0): flip to GA — FAQ maturity, AAMP comparison, industry landscape (closes #2538 docs)

* docs(3.0): Verified program launches with 3.1 — set expectations now

* fix(ci): restore multi-platform optional deps in lockfile + axios types

Regenerating the lockfile during the main-merge resolution was done on
macOS-arm64, which only recorded darwin-arm64 entries for platform-
specific optional deps (@rolldown/binding-*, @img/sharp-*). Linux CI
then failed with 'Cannot find module @rolldown/binding-linux-x64-gnu'
(TypeScript Build) and 'Could not load the sharp module using the
linux-x64 runtime' (broken-links).

Regenerated the lockfile with npm install --os=linux --cpu=x64 after
nuking node_modules, which populated all platform optional deps
(darwin/linux/win32/android/freebsd/wasm bindings for rolldown; full
sharp platform matrix including the nested favicons/node_modules/sharp
copy). Reinstalling locally after on macOS works fine — npm picks up
only the darwin-arm64 bindings at runtime.

Also fixes a latent typecheck error in server/src/adagents-manager.ts:
axios 1.15.2 narrowed AxiosHeaders to string | number | true |
string[] | AxiosHeaders | undefined, breaking the .includes() call on
response.headers['content-type']. Wrap in String() to coerce safely.
The lockfile regeneration bumped axios from an older 1.13.x resolve
to 1.15.2 which surfaced this (existing bug, would have hit anyone
regenerating the lockfile on main).

* fix(ci): drop tests for two SDK-owned compliance assertions

server/tests/unit/compliance-assertions.test.ts still imported spec from
context-no-secret-echo.ts and idempotency-conflict-no-payload-leak.ts,
both of which were deleted in the 5.8 @adcp/client upgrade (the SDK now
ships both as built-in default invariants and owns the tests).

Removed the two describe blocks and their imports. Kept the
governance.denial_blocks_mutation tests — that assertion is still
repo-local.

Missed locally because npm run test:unit only scans tests/, not
server/tests/ — CI catches both via test:server-unit.

* docs(3.0): add item 17 (schema presence tightenings) + fold in x-annotations

Two additions to the 3.0.0 release notes for work that landed on main
over the last 30h but wasn't yet in our section:

- Item 17 covers #2612: check-governance-response now enforces
  conditions/findings/expires_at presence via if/then, and
  sync-catalogs-response requires item_count on created/updated/
  unchanged actions. Conformant agents unchanged; non-conformant ones
  now fail at response_schema validation instead of downstream
  field_present checks.
- Brand schema extensions summary paragraph now also names the three
  non-normative x- annotations that shipped over the last 48h:
  x-entity (#2660 phases 1-4 complete), x-mutates-state (#2675), and
  the governance_policy registry/inline split (#2685). All x-*
  annotations — agents don't validate them; they're tooling hints for
  the storyboard context-entity lint.

* docs(3.0): fold envelope-replayed schema fix and audience-status enum into item 11

Two schema-touch commits from the training-agent sprint that are protocol-visible:

- #2839 (2ee6ac7): envelope-level `replayed` flag now accepted on 15 mutating response schemas (property-list, collection-list, governance). Previously replay responses on these tools failed schema validation.
- #2836 (8ed0d4c): formal `audience-status` enum with explicit lifecycle transitions, paralleling other lifecycle-bearing resource types.

Both roll into item 11 (Error Codes and Schema Consistency). No breaking-changes-table additions — the `replayed` fix is permissive, the enum formalization just elevates an implicit contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 25, 2026
* docs(conformance, catalog): refresh universal-storyboards tables

Both docs/building/conformance.mdx and docs/building/compliance-catalog.mdx
had stale universal-storyboards tables. static/compliance/source/universal/
now contains 9 graded storyboards; the docs listed 5–7. Drift accumulated
as new storyboards landed without back-filling the index pages.

- Add webhook-emission (universal since #2417 / 3.0; never indexed)
- Add pagination-integrity (recently landed; missing from both)
- Add idempotency to compliance-catalog (had it in conformance.mdx only)
- Add signed-requests to compliance-catalog (had it in conformance.mdx only,
  added there in #3077)

Framing fix in compliance-catalog: the lead-in claimed every agent runs
every universal storyboard regardless of claims — true in scope, but
confusing on capability-gated rows. Reworded to acknowledge the gating
(deterministic-testing, signed-requests) plus a closing paragraph that
gated storyboards can't be partially implemented (advertise false rather
than ship a partial surface).

Both index pages now reflect the same 9 graded universal storyboards in
the same order, matching the published /compliance/{version}/universal/
directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(compliance): lint universal-storyboard doc tables match filesystem (#3102)

* chore(compliance): lint that universal-storyboard doc tables match the filesystem

Prevents the drift #3099 just fixed from re-accumulating. Every graded
universal storyboard MUST appear in both docs/building/conformance.mdx
and docs/building/compliance-catalog.mdx; every backtick-quoted slug in
those tables MUST resolve to a real graded storyboard on disk.

Catches:
- New universal storyboard ships without a doc-table row (forward).
- Doc keeps a row for a renamed/deleted storyboard (reverse).
- Either index page loses its "Universal" heading.

Wiring:
- scripts/lint-universal-storyboard-doc-parity.cjs — new module exporting
  lint({ sourceDir, repoRoot }) plus helpers, with a CLI entrypoint.
- scripts/build-compliance.cjs — calls lint inside generateIndex, so the
  compliance build fails loudly on drift.
- tests/lint-universal-storyboard-doc-parity.test.cjs — 10 tests:
  source-tree guard, clean fixture, non-graded fixtures filtered out,
  forward parity (both docs), reverse parity (both docs), missing
  heading, helper unit tests.
- package.json — test:storyboard-doc-parity wired into the umbrella
  test target alongside other storyboard lints; CI picks it up
  automatically via npm run test.

"Graded" = YAML has a phases:[] array. Filters out the three non-graded
fixtures (storyboard-schema.yaml, runner-output-contract.yaml,
fictional-entities.yaml) that live alongside graded storyboards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(compliance): apply DX-review nice-to-haves to doc-parity lint

Two cheap improvements from the DX-expert review:

1. Forward-parity error message now mentions the slug form (kebab-case
   filename slug for compliance-catalog, snake_case YAML id for
   conformance). Parallels the reverse error which already had this hint.
   Helps a contributor adding a brand-new storyboard pick the right
   form without comparing existing rows.

2. JSX comment above each universal-storyboards table pointing at the
   lint script. Discoverability for contributors editing the docs
   directly — they see the rule before the build fails.

Tests still pass 10/10; build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3.0: Add required idempotency_key to webhook payload schemas

1 participant