Repository navigation
fix(discovery): discover preview pins unpinned and reach probe-gated sellers anonymously - #3694
Conversation
…sellers anonymously A registration check on the 3.3 beta SDK reported a 3.0-only seller as "not an AdCP agent": get_adcp_capabilities went out pinned to 3.3-beta.5, the seller answered VERSION_UNSUPPORTED with supported_versions ["3.0"], and nothing retried. - getAdcpCapabilities() and the internal probe share one rule (capabilityDiscoveryEnvelope): a preview pin is discovered unpinned, and a refused stable pin is retried once unpinned. A caller-chosen pin (params or explicit versionEnvelope) is sent as-is; the refusal keeps its supported_versions. - Testing discovery follows the same rule. The storyboard runner no longer sets an explicit 'auto' envelope for AdCP 3 runs, so comply reaches its version check (CapabilityResolutionError unsupported_adcp_version) instead of "Agent unreachable — VERSION_UNSUPPORTED". - A client with no credential falls back to legacy MCP initialize when a seller 401/403s only server/discover. If that also fails, the seller's challenge still reaches the caller. OAuth-provider clients are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n retry - Authored get_adcp_capabilities storyboard steps send the configured pin again; only the SDK's profile discovery goes out unpinned. - capabilityDiscoveryEnvelope evaluates the release actually sent (wireAdcpVersion ?? adcpVersion), so a stable wire override is sent. - Skip the unpinned retry when the caller set an operationId, and keep the pinned refusal unless the retry succeeds. - Without a credential, keep the server/discover challenge when the legacy initialize fails for a non-auth reason; log the probe refusal (status only). - Document the discovery rule in WIRE-VERSION-COMPAT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — discovery-pinning fix verified clean, no blocking or medium findings.
What I checked:
- Witness-not-translator: the failed unpinned retry returns the real pinned refusal (VERSION_UNSUPPORTED) with no fabrication — witness preserved. No mock/fallback data injected.
capabilityDiscoveryEnvelopebehavior confirmed (preview→'none', stable→undefined, explicit wins);executeCapabilityDiscoveryretry gating correctly skips for callerPinned/operationId/'none'.- mcp-modern.ts fallback is fail-closed: legacy auth/abort/throttle supersede, OAuth providers excluded, skipProbe bounds it to one legacy attempt. Uses official MCP client path.
- Operational readiness: unpinned retry preserves deadline/signal via
childTaskOptions; get_adcp_capabilities idempotent; cost is one extra round-trip. - Changeset present (
patch) and defensible for an additive-helper bugfix — no removed/renamed exports or response-shape change that would understate impact. - Test coverage strong: prerelease-capability-discovery.test.js asserts discoveryCalls deepEqual, pinning retry ordering ['3.1', undefined] and no preview-pin leak.
Gate checks: high_risk is true only via src/lib/protocols/mcp-modern.ts (modified), but with no medium-or-higher finding the modification is presumed safe (row 5 does not fire). gated_paths is false. No no-auto-approve team match. review_decision REVIEW_REQUIRED does not block since gated_paths is false. No findings → rows 1–8 do not fire; falls through to row 9 = approve.
…nAPI The published registry OpenAPI gained the brand logo asset endpoints (getBrandLogoAsset, getBrandLogoLegacy) and logo background docs, so the "Generated, Docs & Formatting" check failed on every open PR. Additive only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — purely additive registry snapshot refresh, no blocking or medium findings.
What I checked:
- Delta pass over 3 files: a
patchchangeset, a vendored registry OpenAPI snapshot (schemas/registry/registry.yaml) adding two brand-logo endpoints (getBrandLogoAsset,getBrandLogoLegacy) plusbackground-field documentation, and the regeneratedtypes.generated.ts. - Changeset type (
patch) and impact are correct — these are registry endpoint types, not agent-response reshaping, so no witness/wire-shape concerns. high_risk: trueis driven by(modified)entries onschemas/registry/registry.yamlandsrc/lib/protocols/mcp-modern.ts, but the reviewer found no medium-or-higher concerns, so the modifications are presumed safe (no row-5 trigger).gated_paths: false; no no-auto-approve team match.- Prior decision was
approvewith no findings; this run also surfaces none — net clean.
No critical/high/medium findings. Rows 1–8 do not fire (review_decision REVIEW_REQUIRED does not block since gated_paths is false; high_risk alone with no medium finding does not escalate). Falls through to row 9.
… refused too An uncredentialed client against a seller that 401s everything got the legacy initialize's bare "Error POSTing to endpoint" instead of the probe's 401. Keep the probe error unless the legacy attempt was throttled or cancelled, so the caller sees exactly what it saw before the retry existed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Subsequent review: the prior decision was a clean approve with no findings. The delta is a single non-generated file (src/lib/protocols/mcp-modern.ts, ~47 lines) consisting of comment rewrites plus narrowing legacyFailureSupersedesProbe to drop isNegotiationAuthFailure, so a second 401 on the legacy initialize attempt defers to the probe's original challenge rather than replacing it. The logic is consistent with the surrounding no-credential fallback and preserves witness behavior (surfaces the seller's real 401 + WWW-Authenticate chain, no fabrication). Covered by the updated 401-legacy-fallback tests, and a changeset is present.
Decision-table walk: no critical/high findings (row 1 n/a); gated_paths is false (row 2 n/a); high_risk is true but reasons are (modified), not (deleted) (row 3 n/a); no medium findings at all (rows 4, 5, 8 n/a — the high-risk (modified) paths carry no medium concern); prior decision was approve, not escalate (row 6 n/a); no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.
Problem
Reported in AdCP Slack. A registration check on the 3.3 beta SDK kept saying a 3.0-only seller "does not look like an AdCP agent". The check called
get_adcp_capabilitiespinned toadcp_version: "3.3-beta.5". The seller correctly answeredVERSION_UNSUPPORTEDwithsupported_versions: ["3.0"], and nothing retried. We reproduced it against17.0.0-beta.22with a local 3.0-only seller.Rule implemented here: a buyer sends a preview pin only to a peer that advertises that exact release, and preview pins never negotiate down. Discovery goes out unpinned, and on
VERSION_UNSUPPORTEDthe buyer retries unpinned.Changes
1. Public discovery (
SingleAgentClient)capabilityDiscoveryEnvelope()inadapters/version/negotiate.ts.getAdcpCapabilities()and the internal probe both dispatch throughexecuteCapabilityDiscovery().wireAdcpVersion ?? adcpVersion) goes out unpinned.adcp_version/adcp_major_versionin params, or an explicitversionEnvelope. The refusal comes back withadcpError.details.supported_versions.operationIdis not retried, so the returned task keeps that id.2. Testing and storyboard discovery
applyAdcpVersionRunOptions()/applyStoryboardVersionOptions()leaveversionEnvelopeunset for AdCP 3 runs. That is the SDK default: ordinary calls still sendauto. Before, they set an explicitauto, which pinned profile discovery.get_adcp_capabilitiesstoryboard steps still send the configured pin (storyboardCapabilityStepEnvelope). Theuniversal/version-negotiationstoryboard grades the seller's answer to that pin.comply()then reaches its version check and fails withCapabilityResolutionError(unsupported_adcp_version,supportedVersions: ['3.0']), not "Agent unreachable — VERSION_UNSUPPORTED".versionEnvelopekeep pinned discovery.VERSION_UNSUPPORTED:classification incomply.tsis unchanged.testClientDiscoveryWireAdcpVersion).3. MCP
server/discover401 (mcp-modern.ts)initializeattempt when a seller 401s/403s onlyserver/discover, as a static-credential client already did. AdCP capability discovery is anonymous.Retry-After, and aborts still reach the caller as themselves.4. Registry types sync (unrelated, needed for green CI)
schemas/registry/registry.yamlandsrc/lib/registry/types.generated.ts, all additive, with its own changeset, the same as chore(registry): sync generated registry types with the published OpenAPI #3669.Tests
test/lib/prerelease-capability-discovery.test.js(new). A 3.0-only seller with a client on the3.3.0-beta.5pin:getAdcpCapabilities()/getCapabilities();supported_versions;operationIdskips the retry;discoverAgentProfilesucceeds;unsupported_adcp_version.test/lib/mcp-negotiation-401-legacy-fallback.test.js:server/discoveris discovered anonymously, with no credential sent and no OAuth walk;AuthenticationRequiredErrorwith theBasicchallenge, after one legacy attempt per URL;protocol-client-needs-auth(unchanged) caught that a bare legacy 401 replaced the probe's 401 for an uncredentialed caller. That is fixed by keeping the probe error.capabilities-probe-wire-version,storyboard-version-negotiation,storyboard-capability-discovery-terminal.Review
mcp-modern.ts): no High or Medium findings. Low items L1 (lost challenge on a non-auth legacy failure), L3 (debug log) and L4 (challenge-scheme assertion) are fixed here. L2 (an uncredentialed client against an everything-401 seller now sends one extrainitializeper candidate URL) is accepted: it's bounded, and it's what makes anonymous discovery work.operationIdon the retry; a failed retry hiding the refusal; a stable wire override on a preview client.initializeanonymously but 401stools/callgets a genericinvalid_tokenerror rather thanAuthenticationRequiredError. The same seller without theserver/discovergate already behaves that way onmain: the call path rethrows the raw 401 when the RFC 9728 walk can't be validated. Worth a follow-up.npm run review:codex -- --all, with the diff on stdin because codex's sandbox can't run shell commands on this host:autoas equal. Kept deliberately: both send the same envelope on ordinary calls and authored steps, and differ only in the reused client's own preview discovery probe, which follows that client's config. Making it strict would stop existing_clientreuse in harnesses built withauto.🤖 Generated with Claude Code