Skip to content

fix(protocols): normalize adcp_version to release-precision on the wire#1807

Merged
bokelley merged 2 commits into
mainfrom
bokelley/adcp-3.1-beta-cache
May 17, 2026
Merged

fix(protocols): normalize adcp_version to release-precision on the wire#1807
bokelley merged 2 commits into
mainfrom
bokelley/adcp-3.1-beta-cache

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

  • Adds toReleasePrecisionWire(bundleKeyOrVersion) next to resolveBundleKey in schema-loader.ts.
  • Wires it into buildVersionEnvelope at the single emit site so prerelease pins emit spec-valid wire values.
  • Exports it publicly for storyboard fixtures + conformance harnesses that build their own wire envelopes.

Why this matters

AdCP 3.1's core/version-envelope.json constrains the adcp_version field to release-precision strings via pattern ^\d+\.\d+(-[a-zA-Z0-9.-]+)?$. The schema's own description carries the normalization rule:

SDKs that read full-semver values from bundle metadata (e.g. ComplianceIndex.published_version = "3.1.0-beta.1") MUST normalize to release-precision ("3.1-beta.1") before emitting on the wire — meta-field values are NOT valid wire values.

Before this fix, buildVersionEnvelope emitted the bundle key verbatim, so a 3.1.0-beta-pinned client would send adcp_version: "3.1.0-beta.0" and sellers would AJV-reject the request body with a pattern mismatch.

The bug is latent today — COMPATIBLE_ADCP_VERSIONS doesn't include any 3.1.x release, so no public-pin path can hit it. But a caller manually overriding adcpVersion past the compat gate, or the SDK's eventual 3.1 enablement, would silently break. Found while staging a local 3.1.0-beta cache from spec PR adcontextprotocol/adcp#3307 and exercising buildVersionEnvelope against the real envelope schema.

How it works

toReleasePrecisionWire:

  • Stable bundle keys ('3.0', '3.1') pass through unchanged.
  • Prerelease semver collapses the PATCH segment, preserving the prerelease tag: '3.1.0-beta.0''3.1-beta.0'.
  • Full stable semver collapses to minor: '3.0.11''3.0'.
  • Legacy aliases ('v3') pass through (gated out of the wire field by bundleSupportsAdcpVersionField anyway).
  • Rejects unrecognized shapes loudly rather than silently emitting non-spec values.

Verification

End-to-end check: emitted envelopes for bundle = '3.0' | '3.1' | '3.1.0-beta.0' | '3.0.11' all validate against core/version-envelope.json from a locally-built 3.1.0-beta.0 cache.

Test plan

  • 7 unit cases covering stable / prerelease / release-precision / legacy / garbage inputs.
  • Cross-check that every non-legacy output matches the spec's wire pattern.
  • All 28 existing version-envelope tests still pass.
  • npm run format:check clean.
  • npm run typecheck clean.
  • CI green.

🤖 Generated with Claude Code

The adcp_version envelope field (AdCP 3.1, spec PR
adcontextprotocol/adcp#3493) is constrained by core/version-envelope.json
to release-precision strings via pattern ^\d+\.\d+(-[a-zA-Z0-9.-]+)?$.
Full-semver bundle keys like '3.1.0-beta.0' are explicitly NOT valid wire
values per the envelope schema's own normalization rule.

Before this fix, buildVersionEnvelope emitted the bundle key verbatim
for prerelease pins. A 3.1.0-beta-pinned client would send
adcp_version: "3.1.0-beta.0", which sellers AJV-reject with a pattern
mismatch. The bug was latent: COMPATIBLE_ADCP_VERSIONS doesn't include
any 3.1.x release today, so no public-pin path could hit it, but a
caller manually overriding adcpVersion past the compat gate (or the
SDK's eventual 3.1 enablement) would silently break.

Introduces toReleasePrecisionWire(bundleKeyOrVersion) next to
resolveBundleKey, applied at the single wire-emit site in
buildVersionEnvelope. Exported publicly so storyboard fixtures and
conformance harnesses produce the same wire shape the SDK emits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-1 multi-reviewer pass on PR #1807 surfaced two convergent JSDoc
gaps and one missing test category.

JSDoc additions:
- "**Idempotent.** Re-applying to an already-wire-shaped value is a no-op"
  — DX reviewer noted the test file states this but the contract should
  live in the JSDoc itself (cited by the conformance-test author who'll
  call the function defensively).
- "**Prerelease regex is stricter than the wire pattern by design.**"
  — protocol + code reviewers both noted that the regex
  `[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*` is tighter than the wire's
  `[a-zA-Z0-9.-]+`. The strictness is intentional (mirrors
  resolveBundleKey's path-traversal hardening + SemVer §9 compliance);
  the comment prevents a future reader from "fixing" it back to match
  the wire char-for-char.

Test additions:
- Trailing-dot rejection (`'3.1.0-beta.'`, `'3.0.'`) — confirms the
  stricter-than-wire-pattern note is enforced by code, not just doc.
- Leading-zero round-trip (`'03.01'` -> `'03.01'`) — code reviewer's
  precedence-story documentation: we don't normalize numeric
  components, only the patch-collapse.

Single-reviewer concerns not addressed in this commit (defer to
follow-up issues per the run-by-experts convergence rule):
- Naming asymmetry (`toReleasePrecisionWire` vs sibling
  `bundleSupportsAdcpVersionField`). DX preference; no convergence.
- `resolveBundleKey` doesn't accept release-precision pins
  (`'3.1-beta'` throws). Protocol-reviewer-flagged follow-up; out of
  scope for the wire-emit fix.
- Outgoing-envelope validation should name the helper in errors. DX
  follow-up.

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

Round 1 expert review — consensus: ship

Fired four reviewers in parallel per skills/run-by-experts/SKILL.md (protocol, code, security, dx). All four returned no blockers.

Convergent findings (2+ reviewers) — addressed in d3e6ad7:

  • JSDoc note that the prerelease regex is intentionally stricter than the wire pattern. Protocol + code reviewers both flagged that [0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)* (SemVer §9) is tighter than the wire's [a-zA-Z0-9.-]+. Strictness is intentional (mirrors resolveBundleKey's path-traversal hardening); added a JSDoc note so a future reader doesn't try to "fix" it back to match the wire char-for-char.

Single-reviewer must-fix (worth doing) — also in d3e6ad7:

  • JSDoc idempotency note (DX): added "Idempotent. Re-applying to an already-wire-shaped value is a no-op." Useful for conformance-test authors.
  • Trailing-dot rejection tests (code): '3.1.0-beta.', '3.0.' — confirms the stricter-than-wire-pattern note is enforced by code, not just doc.
  • Leading-zero round-trip test (code): '03.01''03.01' — documents that we don't normalize numeric components, only patch-collapse.

Single-reviewer suggestions — deferring to follow-up issues:

  • Naming asymmetry (DX): toReleasePrecisionWire vs sibling bundleSupportsAdcpVersionField. DX suggested toAdcpVersionWire; code reviewer raised the opposite question ("should this be @internal?"). No convergence; keeping current name. The name is precise (spec literally calls this "release-precision normalization") and the JSDoc anchors discoverability.
  • resolveBundleKey doesn't accept release-precision pins (protocol): a buyer reading seller supported_versions: ["3.1-beta"] and pinning to '3.1-beta' throws. Real gap, but scope-adjacent — the PR's job was wire-emit, not bundle resolution. Filed as a follow-up.
  • Outgoing-envelope validation should name the helper in errors (DX): the seller's AJV rejection won't help a buyer find this function. Worth a follow-up where the SDK's own request-validation pre-flight catches the bad shape and throws a helpful error pointing here.

Security: explicit ship-it. No path coupling, no ReDoS (linear up to 512KB inputs), no log-injection vector, safe to export.

Reviewer summaries:

  • protocol: "spec rule quoted verbatim in JSDoc. Single emit site. Public export for storyboard parity. Strict-mode test pattern cross-check is the right defense."
  • code: "no blockers. Pattern matches other tests — testing harness expects dist to be built. Convention is consistent. No generated files touched."
  • security: "Stateless, pure, throws on garbage, output bounded to the spec regex. Safe to export. Ship it."
  • dx: "Test as documentation: excellent. Seven scenarios + a cross-check against the actual wire regex. Verdict: Ship it."

@bokelley bokelley merged commit 0490e1c into main May 17, 2026
10 checks passed
@bokelley bokelley deleted the bokelley/adcp-3.1-beta-cache branch May 17, 2026 11:13
bokelley added a commit that referenced this pull request May 17, 2026
…ce (#1814)

* feat(wire-version): release-precision pins + wire validator + namespace

Three follow-ups surfaced during the expert review of #1807. Each is
small and additive; bundled because they share the wire-version helper
surface.

Issue #1: resolveBundleKey accepts release-precision pins.
AdCP 3.1's supported_versions capability advertises versions in
release-precision shape (["3.1-beta"]), and a buyer reading that off
the wire must be able to construct a client pinned to it. Before:
new AdcpClient({ adcpVersion: '3.1-beta' }) threw ConfigurationError.
Now: resolveBundleKey accepts MAJOR.MINOR-PRE and returns verbatim;
resolveSchemaRoot fuzzy-resolves to the highest cached prerelease dir
whose own release-precision form matches (so '3.1-beta' finds
schemas/cache/3.1.0-beta.0/, '3.1-beta.0' matches it exactly).

Issue #2: validateAdcpVersionWire public assertion.
Throws ConfigurationError with a hint pointing at toReleasePrecisionWire
when given a non-spec wire value. Use when constructing request
envelopes by hand (storyboard fixtures, conformance harnesses, custom
transports). Also wired as a defensive postcondition in
buildVersionEnvelope so future refactors can't silently emit non-spec
shapes.

Issue #3: wireVersion namespace.
Groups the three helpers (isSupported, normalize, validate) under one
barrel export. Stable target for future additions. Top-level exports
kept for back-compat — not deprecated.

Tests cover all three plus regression checks against the existing
test suites (47 tests pass across the wire-version surface).

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

* test(wire-version): make fuzzy-resolution test hermetic

CI failed on the release-precision-pinned-bundle test because it
relied on schemas/cache/3.1.0-beta.0/ existing — that directory is
gitignored and only present in dev workspaces that have synced a
3.1 prerelease cache. CI only syncs the SDK-pinned ADCP_VERSION.

Fix: create a synthetic 98.0.0-test.0/ fixture directory in before(),
exercise the fuzzy lookup against it (a pin of '98.0-test' should
resolve via the release-precision form '98.0-test.0'), and clean up
in after(). The fixture's major version (98) is far outside any
plausible real AdCP cache so a stray leftover can't conflict.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request May 17, 2026
bokelley added a commit that referenced this pull request May 17, 2026
Standalone design proposal for how the SDK should expose AdCP 3.1
(canonical formats, version envelope, expanded capabilities) without
forcing adopters to think about v1 vs v2 shapes.

The proposal lays out: auto-negotiation per agent via supported_versions;
a single dual-shape Product type that always carries format_ids and
populates format_options when projectable; bidirectional v1<->v2
projection with a structured Diagnostic surface (FORMAT_PROJECTION_FAILED,
FORMAT_DECLARATION_DIVERGENT, CUSTOM_FORMAT_FETCH_FAILED) that's never
logger-only per the spec's resolution-order amendment; a centralized
fetchFormatSchema utility implementing the normative custom-format fetch
contract; versioned codegen under src/lib/types/v3.0/ and v3.1/.

Migration path: 7.x defensive fixes (PR #1807 already shipped); 8.0
ships canonical-formats enablement; 8.x hardens; 9.0 removes v1 paths
when AdCP 4.0 lands.

Explicit non-goals and open questions documented for reviewer feedback.
Draft for review only — no code changes.

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.

1 participant