Skip to content

feat: AdCP 3.0 webhook support (signing + idempotency)#64

Merged
bokelley merged 3 commits into
mainfrom
bokelley/pr-2417-review
Apr 20, 2026
Merged

feat: AdCP 3.0 webhook support (signing + idempotency)#64
bokelley merged 3 commits into
mainfrom
bokelley/pr-2417-review

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Implements publisher + subscriber helpers for the two AdCP 3.0 webhook baselines:

Adds a new adcp/webhook package plus a new skills/build-webhook-publisher skill that the 6 agent-builder skills cross-reference. All existing signing/idempotency tests continue to pass — zero-value Profile preserves back-compat.

What's new

Core (adcp/signing, adcp/webhook)

  • signing.Profile{Tag, AdcpUse, ErrorPrefix} bundle. Exported vars ProfileRequestSigning (default) and ProfileWebhookSigning.
  • signing.GenerateKeyForProfile — generates JWKs scoped for either profile via adcp_use.
  • Error.WireCode(profile) — emits webhook_signature_* vs request_signature_* in WWW-Authenticate based on profile; Go-level ErrorCode constants stay stable.
  • webhook.Marshal / Deliver / Publisher — sender primitives + production retry layer. Publisher retries 5xx/408/429 with exponential backoff + jitter, honors Retry-After, bounded by MaxAttempts (5) + MaxElapsed (1h). Terminates on 2xx / 4xx-non-retryable.
  • webhook.HTTPHandler — receiver middleware composing RFC 9421 verification (ProfileWebhookSigning + DigestRequired) with idempotency-based dedup scoped per authenticated sender.
  • webhook.DecodeConfig — typed accessor that pulls the subscriber URL out of push_notification_config (currently any on generated request types).
  • 5 typed webhook payload types (MCPWebhookPayload, CollectionListChangedWebhook, PropertyListChangedWebhook, ArtifactWebhookPayload, RevocationNotification) with IdempotencyKey string as a required field. IdempotencyKeyPtr() methods auto-generated from WEBHOOK_SCHEMAS — future additions get methods for free.

Safety gates

  • HTTPHandler panics when Verification is nil unless AllowUnverified=true AND a custom Sender is supplied. Blocks the "unsigned receiver + attacker-controlled scope" misconfiguration the security reviewer flagged.
  • webhook.NewSigner force-overrides Profile = ProfileWebhookSigning so a webhook signer cannot accidentally emit a request-signing signature.
  • JSON syntax errors in the body → 400 (not 500) so senders stop retrying malformed payloads.
  • Deliver / Publisher shallow-clone the caller's *http.Client and install CheckRedirect = http.ErrUseLastResponse when unset — signed requests MUST NOT follow redirects.

Skills

  • New skills/build-webhook-publisher/SKILL.md — canonical guide for signed webhook emission in Go agents.
  • Cross-references added to build-seller-agent, build-generative-seller-agent, build-retail-media-agent, build-collection-agent, build-signals-agent, and build-creative-agent at each skill's async/notification section.

Validation

  • 25 webhook tests including end-to-end HTTP round-trips over httptest.NewServer, cross-profile rejection, content-digest enforcement, dedup scope disjoint from request-side, retry classification, Retry-After parsing, MaxElapsed + context cancellation, and the profile-specific wire error prefix.
  • 4 expert reviews (code, security, protocol conformance, DX). All HIGH findings addressed:
    • Security: the Verification=nil panic gate.
    • Code: Deliver now actually installs CheckRedirect; returns DeliverResult carrying the key.
    • Protocol: conformance clean on all 9 spec checks.
    • DX: rewrote Marshal mutation semantics, added ErrMissingURL documentation, removed import gaps from the Quickstart, clarified NewSigner's Profile override.
  • Generation cycle: a coding agent given only these skills produced a working seller with webhook notifications that compiled on first pass and hit zero of the five flagged footguns.

Test plan

  • go test ./... — all 4 packages pass (adcp, idempotency, signing, webhook)
  • go vet ./... — clean
  • gofmt -l — clean on all new/modified files
  • TestSkillSkeletonsCompile — all 7 skills (including new webhook one) compile against the current SDK

Breaking changes

None for existing callers. Zero-value Profile resolves to ProfileRequestSigning everywhere it's read. Existing signing.NewSigner, signing.Middleware, and signing.VerifyRequestSignature callers continue to work without any code changes.

🤖 Generated with Claude Code

bokelley and others added 3 commits April 19, 2026 22:19
Implements publisher + subscriber helpers for the two AdCP 3.0 webhook
baselines:

  - PR #2417: idempotency_key required on every webhook payload so receivers
    have a single canonical dedup field.
  - PR #2423: RFC 9421 webhook-signing profile (adcp/webhook-signing/v1)
    with distinct tag + adcp_use from request-signing, content-digest
    required.

adcp/signing: parameterize existing RFC 9421 implementation with a Profile
struct (Tag + AdcpUse + ErrorPrefix). Zero-value Profile preserves
back-compat as ProfileRequestSigning. ProfileWebhookSigning enables the
webhook variant without forking the signer. Error.WireCode(profile) emits
the profile-specific prefix in WWW-Authenticate so receivers see
webhook_signature_* vs request_signature_*.

adcp/webhook: new package. Marshal generates UUIDv4 keys and freezes
wire bytes. Deliver is the one-shot signed POST. Publisher adds retry
with exponential backoff, Retry-After, bounded by MaxAttempts + MaxElapsed.
HTTPHandler composes signature verification (DigestRequired) with
idempotency-based dedup scoped per authenticated sender. DecodeConfig
extracts the subscriber URL from push_notification_config.

Safety gates: HTTPHandler panics when Verification is nil unless
AllowUnverified is explicitly set (prevents the "custom Sender + no
verification = attacker-controlled dedup scope" misconfiguration).
NewSigner force-overrides Profile so webhook signers cannot emit
request-signing signatures.

Auto-generation: IdempotencyKeyPtr methods on each webhook payload type
are emitted by generate.py alongside the struct. Adding a 6th webhook
schema to WEBHOOK_SCHEMAS automatically extends the set of types that
implement webhook.Payload.

Test coverage: 25 webhook tests including end-to-end HTTP round-trips,
cross-profile rejection, content-digest enforcement, dedup scope
disjoint from request-side, retry classification, Retry-After parsing,
MaxElapsed + context cancellation, panic gates, and the webhook-signing
wire error prefix.

Reviewed by code-reviewer, security-reviewer, ad-tech-protocol-expert,
dx-expert. All HIGH findings addressed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t from 6 existing skills

Canonical source for emitting AdCP 3.0 webhooks: signer setup,
DecodeConfig usage (with the three-case return table),
Publisher-vs-Deliver decision, 5 gotchas (mutation, CheckRedirect,
re-marshal on retry, profile separation, key persistence), retry tuning,
and a Common Mistakes error→fix table.

Each of the 6 existing agent-builder skills (build-seller-agent,
build-generative-seller-agent, build-retail-media-agent,
build-collection-agent, build-signals-agent, build-creative-agent) now
points to build-webhook-publisher from its async / notification section,
so updates to webhook guidance propagate automatically.

Validated by a generation cycle: a coding agent given only these skills
and the task "build a seller that emits task-status webhooks" produced
code that compiled on first pass and hit zero of the five flagged
footguns. Gen-cycle code was kept under .context/ (gitignored).

The Quickstart code block is compile-checked by the existing
TestSkillSkeletonsCompile in adcp/skill_compile_test.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 79156f8 into main Apr 20, 2026
5 checks passed
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.

1 participant