Skip to content

feat(server): route validation by wire adcp_version (stage 3)#664

Merged
bokelley merged 5 commits into
mainfrom
claude/versioned-schemas-stage-3
May 11, 2026
Merged

feat(server): route validation by wire adcp_version (stage 3)#664
bokelley merged 5 commits into
mainfrom
claude/versioned-schemas-stage-3

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Re-opened with main as base after #659 merged (the original #660 auto-closed on base-branch deletion).

Summary

Stage 3 of the versioned-schema-validation port. create_tool_caller now detects the buyer's claimed version off the request envelope and threads it through to the per-version validator added in Stage 2 (#659).

Detection order (mirrors the TS SDK's applyVersionEnvelope and the spec text on every *-request schema):

  1. adcp_version (3.1+ release-precision string) — normalized to release precision (\"3.0.7\"\"3.0\"); must be in COMPATIBLE_ADCP_VERSIONS.
  2. adcp_major_version (legacy integer) — maps to the highest supported minor for that major.
  3. Neither field set — falls through to the SDK pin (existing behaviour).

Default-permissive gate (per downstream-impact audit)

The dispatcher rejects unsupported versions with VERSION_UNSUPPORTED only when ADCP_STRICT_VERSION_ENVELOPE=1 is set. Default behaviour logs a WARNING with a migration hint and falls through to SDK-pin validation. Strict becomes the default in 5.3. This decouples the additive adapter feature from the subtractive version gate so adopters with test fixtures using placeholder versions keep working through the migration window.

Review status

  • code-reviewer: LAND_AS_IS (re-review confirmed all prior fixes)
  • downstream-impact audit: recommended the soft-gate, which is now wired

Test plan

  • pytest — all green
  • 19 unit/integration tests across test_validation_envelope and test_dispatcher_version_routing (8 envelope, 11 dispatcher)
  • New test_unsupported_version_permissive_falls_through covers the default-permissive path

🤖 Generated with Claude Code

bokelley and others added 5 commits May 11, 2026 05:35
``get_validator``, ``validate_request``, and ``validate_response`` now
accept an optional ``version=`` kwarg. ``None`` defaults to the SDK's
compile-time pin (existing behaviour, byte-identical for current call
sites). A wire-version string (``"3.0.7"``, ``"2.5"``,
``"3.1.0-beta.1"``) routes to the per-bundle-key cache laid down by
Stage 1 — each version's file index, compiled validators, and core
registry live in their own ``_LoaderState`` so cross-version traffic
doesn't share compilation state.

The dispatcher call sites are unchanged today; Stage 3 will detect the
buyer's ``adcp_major_version`` off the wire and thread it through.

Also widens ``resolve_bundle_key`` to accept bare ``MAJOR.MINOR``
(already a bundle key — matches the wire envelope's release-precision
``adcp_version`` field, so the dispatcher can pass it straight through).

Tests:
* ``test_schema_loader_per_version`` lays down a synthetic
  ``schemas/cache/2.5/`` fixture, asserts validators for the synthetic
  legacy tool and the real SDK-pin tools coexist independently, and
  pins ``version=None`` behaviour to the SDK pin.
* ``test_validation_version`` extended with the new pass-through and
  rejection cases.

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

Review feedback on PR #659 (Stage 2). Two nits:

* Fixture used to write into the repo's real ``schemas/cache/2.5/``,
  which means CI runs against an installed wheel would silently miss
  the synthetic bundle (packaged ``_schemas/`` wins) and the assertions
  would be vacuous. Move to ``tmp_path`` + ``monkeypatch.setattr`` on
  the loader's resolver instead.
* Teardown used a sequence of ``rmdir`` calls that fail if anything else
  lands in the directories mid-run. ``shutil.rmtree(..., ignore_errors=
  True)`` is robust.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 3 of the versioned-schema-validation port. ``create_tool_caller``
now detects the buyer's claimed version off the request envelope and
threads it through to the per-version validator added in Stage 2.

Detection order (mirrors the TS SDK's ``applyVersionEnvelope`` and the
spec text on every ``*-request`` schema):

1. ``adcp_version`` (3.1+ release-precision string) — normalized to
   release precision (``"3.0.7"`` → ``"3.0"``); must be in
   ``COMPATIBLE_ADCP_VERSIONS`` or ``VERSION_UNSUPPORTED`` is raised.
2. ``adcp_major_version`` (legacy integer) — maps to the highest
   supported minor for that major. Unknown major raises
   ``VERSION_UNSUPPORTED`` with structured details.
3. Neither field set — falls through to the SDK pin (existing behaviour).

New module ``adcp.validation.envelope`` owns the detection logic;
``UnsupportedVersionError`` carries the wire value and supported set so
the dispatcher can echo both into the error envelope's ``details``.

Existing ``test_spec_compat_hooks`` payloads used
``adcp_major_version=4`` as a wire-shape placeholder — updated to ``3``
now that the dispatcher actually validates the field.

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

Review feedback on PR #660.

* String ``adcp_major_version`` (e.g. ``"3"``) used to fall through to
  ``None`` and silently get SDK-pin validation. A buyer that
  JSON-stringified the field deserves a loud
  ``VERSION_UNSUPPORTED``, not a quiet behaviour swap. Same for ``< 1``
  values (below the spec's ``minimum: 1`` bound).
* ``claimed_version`` in error details used to be ``str(exc.wire_value)``,
  which converted the int field to ``"4"``. Pass through verbatim so
  buyer telemetry sees the same type they sent.
* Rename ``test_no_version_field_validator_uses_sdk_pin`` to actually
  test that claim — pass a ``ValidationHookConfig`` so the validator
  fires, and assert ``version=None`` is threaded. Split the original
  test's "no validation config means no validator runs" assertion into
  its own regression test.

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

Downstream-impact audit on PR #660 flagged that switching from "wire
schema accepts adcp_major_version 1-99" to "dispatcher rejects unknown
versions" in one release would break test fixtures using ``4`` as a
sentinel and any buyer claiming an unsupported version that happened to
pass schema validation.

Decouple legacy adapter routing (additive, ships now) from
version-envelope strictness (subtractive, ships under a gate).

* Default (``ADCP_STRICT_VERSION_ENVELOPE`` unset or ``"0"``): an
  unsupported wire version is logged at WARNING with a migration hint
  pointing at the env var. The dispatcher falls through to SDK-pin
  validation — same as 5.1.0 behaviour for that payload.
* Strict (``ADCP_STRICT_VERSION_ENVELOPE=1``): raise the
  ``VERSION_UNSUPPORTED`` envelope the spec prescribes, before any
  handler dispatch. This becomes the default in 5.3.

Existing strict-mode tests gain a ``strict_version_envelope`` fixture
so the spec-prescribed behaviour stays covered. New
``test_unsupported_version_permissive_falls_through`` exercises the
default-permissive path and asserts the migration hint shows up in
logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit d2ffac7 into main May 11, 2026
17 checks passed
@bokelley bokelley deleted the claude/versioned-schemas-stage-3 branch May 11, 2026 10:14
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