Skip to content

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

@bokelley

Description

@bokelley

Problem

adcp 5.0 (PR #629, closes #614) shipped serve(pre_validation_hooks=...) — the mechanism is great, but every Python adopter that accepts pre-v3 / pre-4.4 buyers needs the same handful of spec-mandated default + shape-migration hooks. Each adopter writes (and maintains) the same code.

In bokelley/salesagent we just migrated our 273-LOC SpecDefaultsMiddleware to ~150 LOC of pre_validation_hooks. Looking at what we wrote, none of it is salesagent-specific — every line is a universal compatibility shim:

Hook What it does Why universal
get_products.buying_mode='brief' Default the field when omitted Spec says: "Sellers receiving requests from pre-v3 clients without buying_mode SHOULD default to 'brief'." Every seller, by the spec text.
sync_creatives.creatives[*].format_id string → {agent_url, id} Wrap bare string format_id as the structured 4.4 reference adcp 4.4 changed format_id from string to structured. Pre-4.4 buyers send a string; any seller accepting them needs the same wrap. The default agent_url is the canonical https://creative.adcontextprotocol.org/ registry.
sync_creatives.creatives[*].assets[*].asset_type inference Infer the missing asset_type from the asset key (image/video/...) or from content/url/width+height presence adcp 4.4 added explicit asset_type to make AssetVariant discriminated; pre-4.4 buyers used the key as the implicit type. Universal compat.
sync_creatives.creatives[*].assets[*] image → url demote When asset_type='image' but width/height missing, demote to url variant The strict 4.4 image variant requires dims; pre-4.4 buyers frequently send image-shaped URLs without them. Universal compat.

We're carrying this code as if it were adopter logic. It's not — it's the SDK's compat layer for migrating buyers across spec major versions.

Proposed shape

Ship a built-in spec_compat_hooks() (or similarly named) in adcp.server that adopters can wire into pre_validation_hooks:

from adcp.server import spec_compat_hooks, serve

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

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

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

Opt-in by design — adopters who want strict-4.4 rejection of pre-v3 / pre-4.4 buyers leave it off. Adopters who accept the long tail of legacy clients (most production sellers) wire it once and forget about it.

What's in / out of scope

In: The four hooks above. Each maps to a spec versioning event (pre-v3 buying_mode, pre-4.4 format_id shape, pre-4.4 asset_type discriminator, pre-4.4 image dim requirement).

Out: Adopter-specific defaulting (e.g. salesagent's removed account: auth-chain placeholder — that was an adopter bug, not a spec migration). Adopters write those themselves via the same pre_validation_hooks mechanism, layered on top.

Why this matters beyond us

These hooks are exactly the kind of glue every adopter has to write when migrating from a pre-4.4 SDK to a 5.0 SDK. Shipping them in the SDK:

  1. Saves each adopter ~150 LOC of mechanical-but-spec-sensitive code.
  2. Centralises the canonical agent_url (https://creative.adcontextprotocol.org/), the asset-type inference table, the image-dim demotion rule — adopters who roll their own will silently diverge.
  3. Provides a single PR target when the spec adds the next pre-major-version compat shim (adcp 6.0 → 7.0 someday) instead of every adopter re-discovering it.

Source

Our core/spec_default_hooks.py is what we just wrote — happy to send the contents as a starter PR if there's interest in shipping it directly. The file is intentionally pure-Python (no transport / framework deps) so dropping it into adcp.server should be drop-in.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions