Skip to content

feat(schemas): regen for AdCP 3.0 GA — custom pricing + experimental_features#210

Merged
bokelley merged 2 commits intomainfrom
bokelley/adcp-3-ga-regen
Apr 20, 2026
Merged

feat(schemas): regen for AdCP 3.0 GA — custom pricing + experimental_features#210
bokelley merged 2 commits intomainfrom
bokelley/adcp-3-ga-regen

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Closes #204. Regenerates Pydantic models from the upstream AdCP 3.0 GA protocol bundle (adcontextprotocol/adcp#2422).

Summary

  • New custom variant in vendor pricing discriminated union (VendorPricing, VendorPricingOption). Requires description and metadata (non-empty), optional currency. Consumers dispatching on .root.model should add a "custom" branch — recommended default is operator review, not auto-select.
  • per_unit pricing variant (added in an earlier 3.0 rc) is now in the generated models alongside custom.
  • New optional experimental_features: list[str] | None on GetAdcpCapabilitiesResponse. Dot-namespaced ids (brand.rights_lifecycle, governance.campaign, trusted_match.core) declared by sellers that implement experimental surfaces.
  • x-status: experimental annotations on Brand Rights, Campaign Governance, and TMP — metadata only; datamodel-code-generator ignores unknown x-* extensions, so type output is unchanged.
  • Idempotency now carries a required supported: bool (pulled in via the latest bundle). Four test fixtures updated to pass supported=True.

Wire format

No breaks. All changes are additive.

Test plan

  • make regenerate-schemas succeeds
  • ruff check src/ passes
  • mypy src/adcp/ passes (671 source files)
  • pytest tests/ passes (1696 passed, 14 skipped)
  • Verified VendorPricing5/VendorPricingOption11 carry model: Literal["custom"] with required description + metadata
  • Verified experimental_features field on GetAdcpCapabilitiesResponse in both protocol/ and bundled/protocol/

🤖 Generated with Claude Code

bokelley and others added 2 commits April 19, 2026 22:32
…features

Regenerates from upstream protocol bundle (PR adcontextprotocol/adcp#2422):

- New `custom` variant in VendorPricing/VendorPricingOption unions
  (model: "custom", required description + metadata, optional currency).
  Consumers dispatching on `.root.model` must add a "custom" branch;
  recommended default is operator review, not auto-select.
- `per_unit` pricing variant picked up from an earlier 3.0 rc.
- New optional `experimental_features: list[str] | None` on
  GetAdcpCapabilitiesResponse for dot-namespaced feature ids
  (brand.rights_lifecycle, governance.campaign, trusted_match.core).
- x-status: experimental metadata on Brand Rights, Campaign Governance,
  and TMP schemas (type output unchanged — metadata only).
- Idempotency now carries a required `supported: bool`. Test fixtures
  updated to pass supported=True.

Closes #204

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

The AdCP 3.0 GA schema promoted `adcp.idempotency.supported` to a required
bool with explicit semantics: `supported=false` means the seller does NOT
dedupe and a naive retry WILL double-process. The strict-idempotency gate
was checking only `replay_ttl_seconds`, so a seller declaring
`supported=false, replay_ttl_seconds=86400` (contradictory but type-valid)
passed the gate silently.

_ensure_idempotency_capability now raises IdempotencyUnsupportedError when:
- adcp.idempotency is missing
- supported=false
- supported=true but replay_ttl_seconds is None

The exception message carries a reason string so the specific mode is
visible in logs. Added three targeted tests covering each failure mode
plus the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley
Copy link
Copy Markdown
Contributor Author

Expert review follow-up

Ran the PR past three expert reviewers. Summary of findings and actions:

Addressed in 9ff08c9 (follow-up commit)

[must-fix from code-reviewer] _ensure_idempotency_capability in src/adcp/client.py ignored the new required supported field. A seller declaring supported=false, replay_ttl_seconds=86400 (contradictory but type-valid) passed the strict gate silently. Fixed to raise IdempotencyUnsupportedError on any of:

  • adcp.idempotency missing entirely
  • supported=false (seller does not dedupe — naive retry double-processes)
  • supported=true but replay_ttl_seconds is None (seller is spec-incomplete)

Updated IdempotencyUnsupportedError to carry a reason string so logs show which mode tripped. Added three tests covering each failure mode plus the happy path.

Not addressed (acknowledged, scoped out of this PR)

  • Metadata.minProperties: 1 not enforced on custom pricing (ad-tech-protocol-expert, python-expert). The JSON Schema says the metadata object on custom-pricing variants must be non-empty, but datamodel-codegen flattens additionalProperties: true + optional summary_for_operator to a model that accepts {}. This is a codegen limitation, not something this regen introduced. Worth a follow-up as a post_generate_fixes.py hook or a hand-written model_validator that survives regen.
  • Idempotency conditional if supported then require ttl not enforced at the model level (ad-tech-protocol-expert). Same class of codegen gap (draft-07 if/then isn't emitted). The client-side gate fix above compensates for the buyer path; a seller building capabilities responses should still own spec conformance.
  • VendorPricing / VendorPricingOption / ExperimentalFeature / capability-response Idempotency not re-exported via adcp.types public API (python-expert). Tests in test_capabilities.py reach into adcp.types.generated_poc.protocol.get_adcp_capabilities_response directly — this pre-existed this PR. Adding public re-exports + semantic aliases (CustomPricing, etc.) is worth doing but is a distinct DX improvement, not a regen correctness fix.

Confirmed correct / no action needed

  • Custom-variant fields match spec: model, description (min_length=1), metadata required; currency (ISO-4217 pattern) and ext optional. extra="allow" preserves escape-hatch semantics for vendor-specific metadata keys.
  • experimental_features field correctly propagates the dot-namespace pattern and is optional (matches wire contract — seller absence = "implements none").
  • Making Idempotency.supported required matches the spec posture ("clients MUST NOT assume a default").
  • Multiple-inheritance subclasses on VendorPricingOption7..11 dispatch deterministically via the model Literal discriminant under Pydantic v2. No MRO pitfall.
  • No SDK-internal dispatch on pricing .model exists (verified via grep) — consumers are responsible for adding a custom branch per the issue's guidance.
  • aliases.py intentionally omits per-variant signal-pricing aliases (see existing comment at lines 1062-1067); adding CustomPricing here alone would be inconsistent with the established pattern.

@bokelley bokelley merged commit 4dfaffe into main Apr 20, 2026
9 checks passed
bokelley added a commit that referenced this pull request Apr 20, 2026
…se, webhook re-exports

Three P0 blockers surfaced by round-6 storyboard validation on the
AdCP 3.0 GA rebase.

1. IdempotencyStore.capability() now emits the required supported field
---------------------------------------------------------------------
Upstream PR #210 (adcp 3.0 GA regen) made ``adcp.idempotency.supported``
REQUIRED on the capabilities response. ``IdempotencyStore.capability()``
at src/adcp/server/idempotency/store.py:78 was still returning
``{"replay_ttl_seconds": N}`` only, so every agent using the documented
``capabilities_response(idempotency=store.capability())`` pattern
silently emitted a schema-invalid capabilities block. Now returns
``{"supported": True, "replay_ttl_seconds": N}``. Four existing tests
updated to match.

2. adcp-keygen --purpose for webhook-signing keys
-------------------------------------------------
The webhook verifier enforces ``adcp_use == "webhook-signing"`` on the
JWK, but ``adcp-keygen`` hardcoded ``adcp_use: "request-signing"`` with
no override. A user following keygen → publish JWKS → emit webhook got
``webhook_signature_key_purpose_invalid`` on first delivery — the exact
failure mode round-6 DX exploration flagged as a blocker for
discoverability of the new 9421 path. Added ``--purpose
{request-signing,webhook-signing}`` CLI flag (default request-signing
for back-compat) and threaded the value through generate_ed25519 /
generate_es256.

3. Top-level adcp re-exports the new 9421 webhook surface
---------------------------------------------------------
``adcp/__init__.py`` re-exported the deprecated legacy
``get_adcp_signed_headers_for_webhook`` but NOT the new 9421 entry
points. A coding agent scanning ``dir(adcp)`` for webhook primitives
saw only legacy. Added: ``sign_webhook``, ``WebhookReceiver``,
``WebhookReceiverConfig``, ``WebhookVerifyOptions``,
``WebhookDedupStore``, ``MemoryBackend``, ``LegacyHmacFallback``,
``generate_webhook_idempotency_key``. Also promoted the MemoryBackend /
WebhookDedupStore imports in adcp.webhooks to explicit ``as``
re-exports so mypy treats them as public.

BREAKING CHANGE: IdempotencyStore.capability() return shape changes
from ``{"replay_ttl_seconds": N}`` to ``{"supported": True,
"replay_ttl_seconds": N}``. Callers that byte-compared against the old
shape will need to update their expected value. Required to emit
schema-valid AdCP 3.0 capabilities responses.

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

Three P0 blockers surfaced by round-6 storyboard validation on the
AdCP 3.0 GA rebase.

1. IdempotencyStore.capability() now emits the required supported field
---------------------------------------------------------------------
Upstream PR #210 (adcp 3.0 GA regen) made ``adcp.idempotency.supported``
REQUIRED on the capabilities response. ``IdempotencyStore.capability()``
at src/adcp/server/idempotency/store.py:78 was still returning
``{"replay_ttl_seconds": N}`` only, so every agent using the documented
``capabilities_response(idempotency=store.capability())`` pattern
silently emitted a schema-invalid capabilities block. Now returns
``{"supported": True, "replay_ttl_seconds": N}``. Four existing tests
updated to match.

2. adcp-keygen --purpose for webhook-signing keys
-------------------------------------------------
The webhook verifier enforces ``adcp_use == "webhook-signing"`` on the
JWK, but ``adcp-keygen`` hardcoded ``adcp_use: "request-signing"`` with
no override. A user following keygen → publish JWKS → emit webhook got
``webhook_signature_key_purpose_invalid`` on first delivery — the exact
failure mode round-6 DX exploration flagged as a blocker for
discoverability of the new 9421 path. Added ``--purpose
{request-signing,webhook-signing}`` CLI flag (default request-signing
for back-compat) and threaded the value through generate_ed25519 /
generate_es256.

3. Top-level adcp re-exports the new 9421 webhook surface
---------------------------------------------------------
``adcp/__init__.py`` re-exported the deprecated legacy
``get_adcp_signed_headers_for_webhook`` but NOT the new 9421 entry
points. A coding agent scanning ``dir(adcp)`` for webhook primitives
saw only legacy. Added: ``sign_webhook``, ``WebhookReceiver``,
``WebhookReceiverConfig``, ``WebhookVerifyOptions``,
``WebhookDedupStore``, ``MemoryBackend``, ``LegacyHmacFallback``,
``generate_webhook_idempotency_key``. Also promoted the MemoryBackend /
WebhookDedupStore imports in adcp.webhooks to explicit ``as``
re-exports so mypy treats them as public.

BREAKING CHANGE: IdempotencyStore.capability() return shape changes
from ``{"replay_ttl_seconds": N}`` to ``{"supported": True,
"replay_ttl_seconds": N}``. Callers that byte-compared against the old
shape will need to update their expected value. Required to emit
schema-valid AdCP 3.0 capabilities responses.

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.

Regen for AdCP 3.0 GA: new custom pricing variant + experimental_features[]

1 participant