Skip to content

feat(server): add spec_compat_hooks() for pre-v3 / pre-4.4 buyer compatibility#648

Merged
bokelley merged 3 commits into
mainfrom
claude/issue-646-spec-compat-hooks
May 11, 2026
Merged

feat(server): add spec_compat_hooks() for pre-v3 / pre-4.4 buyer compatibility#648
bokelley merged 3 commits into
mainfrom
claude/issue-646-spec-compat-hooks

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Closes #646

Summary

Ships a built-in spec_compat_hooks() factory in adcp.server that eliminates ~150 LOC of boilerplate every Python seller writes independently when accepting pre-v3 / pre-4.4 buyers. Returns a dict[str, Callable] compatible with serve(pre_validation_hooks=...).

from adcp.server import spec_compat_hooks, serve

# Standalone:
serve(handler, pre_validation_hooks=spec_compat_hooks())

# Combined with adopter-specific hooks:
serve(handler, pre_validation_hooks={**spec_compat_hooks(), "create_media_buy": my_hook})

# Selective opt-out:
serve(handler, pre_validation_hooks=spec_compat_hooks(exclude={"sync_creatives"}))

# Private creative-format registry:
serve(handler, pre_validation_hooks=spec_compat_hooks(creative_agent_url="https://creative.example.com"))

Four spec-mandated hooks included:

Hook What it does Spec reference
get_products Defaults buying_mode='brief' when absent Spec: sellers receiving pre-v3 requests without buying_mode SHOULD default to 'brief'
sync_creatives (1) Wraps bare format_id string as {agent_url, id} adcp 4.4 changed format_id from string to structured object
sync_creatives (2) Infers asset_type from exact key match or field presence adcp 4.4 added explicit asset_type discriminator
sync_creatives (3) Demotes asset_type='image''url' when width/height absent but url present adcp 4.4 image variant requires dims

Design decisions (from expert review):

  • asset_type inference uses exact key match only"hero_image" is an asset ID, not a type hint; substring matches are intentionally excluded
  • Hook 4 (image→url demotion) only fires when url is present; structurally invalid assets (no url either) are left unchanged for schema validation to report
  • sync_creatives hooks 2+3+4 are always bundled under one key (one callable per tool name) — adopters needing granular control copy logic from adcp.server.spec_compat
  • Opt-in by design — sellers who want strict rejection of pre-4.4 clients leave this off

Also exports:

  • PreValidationHooks: TypeAlias — for adopter type annotations
  • CANONICAL_CREATIVE_AGENT_URL — for test fixtures / private-registry overrides

What was tested

  • pytest tests/test_spec_compat_hooks.py -v35 passed (all four hooks, exact-key vs substring inference, partial-dims strip, image→url safety, factory composition, integration through create_tool_caller)
  • pytest tests/ -q --ignore=tests/integration -k "not test_real_tls"4350 passed, 0 new failures
  • ruff check src/adcp/server/spec_compat.pyall checks passed
  • mypy: no new errors in spec_compat.py or __init__.py

Nits surfaced by pre-PR review (not fixed — left for reviewer discretion):

  • serve.py/ServeConfig use a looser Callable[..., Any] annotation than PreValidationHooks; aligning all three is a follow-up
  • __all__ ordering: # Spec compatibility section currently follows # Test controller; could move to after # MCP integration for discoverability
  • exclude with unknown names is silently ignored; a warnings.warn could help with typos

Pre-PR review

  • code-reviewer: approved — no blockers; one nit (partial-dims test gap, now fixed)
  • dx-expert: approved — no blockers; two highest-impact nits fixed (routerhandler in docstring, TypeAlias annotation added)

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01FmPS6TP29PNk9PuedFDTbm


Generated by Claude Code

claude added 3 commits May 11, 2026 06:32
Ships a built-in `spec_compat_hooks()` factory in `adcp.server` that
returns `dict[str, Callable]` compatible with `serve(pre_validation_hooks=...)`.
Eliminates ~150 LOC of boilerplate that every adopter accepting legacy
buyers currently writes independently.

Four spec-mandated coercions:
- get_products: default buying_mode='brief' when omitted (pre-v3)
- sync_creatives: wrap bare format_id string as {agent_url, id} (pre-4.4)
- sync_creatives: infer asset_type from key (exact match) or field presence
- sync_creatives: demote image→url when width/height absent but url present

Closes #646

https://claude.ai/code/session_01FmPS6TP29PNk9PuedFDTbm
…rl demotion

Covers the case where an asset has lone width (no height) — the demotion
still fires and strips the stray dim before changing asset_type to 'url'.

https://claude.ai/code/session_01FmPS6TP29PNk9PuedFDTbm
…nused params

Pre-PR review nits:
- Rename `router` to `handler` in module docstring examples (correct param name)
- Annotate PreValidationHooks with TypeAlias for IDE hover ergonomics
- Add noqa ARG001 to tool_name params that are required by the hook signature
  but intentionally unused (hooks are registered per-tool in the dict)

https://claude.ai/code/session_01FmPS6TP29PNk9PuedFDTbm
@bokelley bokelley marked this pull request as ready for review May 11, 2026 07:15
@bokelley bokelley merged commit 30690e5 into main May 11, 2026
16 checks passed
@bokelley bokelley deleted the claude/issue-646-spec-compat-hooks branch May 11, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(server): ship built-in spec_compat_hooks() for pre-v3 / pre-4.4 buyer compatibility

2 participants