From a77f566d87ecd09ed69d90aeed341a39b8447686 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 16 Oct 2026 02:13:57 +0000 Subject: [PATCH] chore(deps): update @adcp/sdk to 15.9.2 and drop external-smoke SDK workarounds SDK 15.9.2 fixes the two bugs that #8208 worked around in the external smoke check (adcp-client#3278, #3279). Capability discovery now validates get_adcp_capabilities against the echoed adcp_version when it is in the pin's major and bundled. The protocol/tool cross-check now keys comply_test_controller under compliance_testing. Removed from scripts/external-smoke: the compliance cross-check waiver, and the served-release re-pin with its second discovery (ReleaseProbe, RepinProbe, releasePrecision, servedRelease, negotiateServedRelease, repinFor). Kept on top of the single 15.9.2 discovery: OAuth 401 classification, the echoed release must share the pin's major and be listed in supported_versions, and storyboards resolve from the served release's hosted bundle (new resolverFor option). The "same release when re-pinned" check needed the second probe and is dropped. SDK 15.9.0 adds pattern/transport to adcp_reporting_destination_ authorizations. PostgresReportingManagedDeliveryStore uses them in authorizeDestination, installBinding and claimRevocation (every worker pass), and its probe does not check them. Migration 624 adds them verbatim from REPORTING_MANAGED_DELIVERY_MIGRATION, plus the 15.9 tombstone retry columns for ledger parity. The disposable-PostgreSQL suite passes 8/8 with it and fails 6/8 without it. Moves the exact current-version pins to 15.9.2 (SDK doc, Addie certification text with a CODE_VERSION bump, buyer evidence, provenance tests). Re-records the TypeScript conformance baseline: all 125 cases, same 3.2.1 digest, counts and finding; only the SDK version changed. Removes the two request-signing SDK_KNOWN_GAPS todos, which now pass. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/building/by-layer/L4/choose-your-sdk.mdx | 16 +- docs/runbooks/training-agent-gcs-reporting.md | 5 +- package-lock.json | 8 +- package.json | 2 +- scripts/external-smoke/README.md | 30 ++-- scripts/external-smoke/check.ts | 115 +++++--------- scripts/external-smoke/run.ts | 21 +-- scripts/sdk-response-conformance/README.md | 2 +- .../known-findings.json | 2 +- scripts/training-gcs-buyer.mjs | 2 +- server/src/addie/config-version.ts | 2 +- server/src/addie/mcp/certification-tools.ts | 4 +- ...ing_gcs_reporting_destination_contract.sql | 92 ++++++++++++ .../registry-api-agent-refresh.test.ts | 4 +- ...stry-api-compliance-verdict-source.test.ts | 2 +- .../training-gcs-reporting-postgres.test.ts | 2 +- .../tests/unit/compliance-publication.test.ts | 2 +- server/tests/unit/external-smoke.test.ts | 141 +++++++----------- ...quest-signing-profile-3.2-vectors.test.cjs | 12 +- 19 files changed, 236 insertions(+), 228 deletions(-) create mode 100644 server/src/db/migrations/624_training_gcs_reporting_destination_contract.sql diff --git a/docs/building/by-layer/L4/choose-your-sdk.mdx b/docs/building/by-layer/L4/choose-your-sdk.mdx index 5a5d70bf2f9..af741250ae8 100644 --- a/docs/building/by-layer/L4/choose-your-sdk.mdx +++ b/docs/building/by-layer/L4/choose-your-sdk.mdx @@ -23,17 +23,17 @@ For the layered model behind this — what each layer contains and what an SDK a What "shipped" means at each layer is the [L0–L3 checklist](/docs/building/cross-cutting/sdk-stack#what-an-sdk-at-each-layer-should-provide). -*Last updated: 2026-10-12.* +*Last updated: 2026-10-16.* | SDK | Current package | 3.2 support | L0 | L1 | L2 | L3 | |---|---|---|:-:|:-:|:-:|:-:| -| **`@adcp/sdk`** (TypeScript) | `15.8.1` | Full 3.2 support | ✅ | ✅ | ✅ | ✅ | +| **`@adcp/sdk`** (TypeScript) | `15.9.2` | Full 3.2 support | ✅ | ✅ | ✅ | ✅ | | **`adcp`** (Python) | `8.0.0` | Exact `3.2.1` schemas and types; higher-level helpers remain partial | ✅ | ⚠️ | ⚠️ | ⚠️ | | **`adcp/v3`** (Go) | `v3.2.1` | Embeds `3.2.0-rc.1` generated schemas and types and defaults to the `"3.1"` wire pin; final 3.2 support is planned as a follow-up | ✅ | ⚠️ | ⚠️ | ⚠️ | Legend: ✅ shipped · ⚠️ partial / role-dependent · ❌ not yet covered. The L0–L3 columns describe the current package, not a 3.2 support claim. -`@adcp/sdk@15.8.1` is on the npm `latest` dist-tag and includes the seller-side reporting modules (`@adcp/sdk/reporting/source` and `@adcp/sdk/reporting/ledger`) and the buyer-side consumer module (`@adcp/sdk/reporting/consumer`). The production composer (`@adcp/sdk/reporting/service`) and private GCS adapter (`@adcp/sdk/reporting/gcs`) require application-specific provisioning and rollout validation. Python `adcp==8.0.0` embeds the final 3.2 schemas. Final 3.2 support for Go is planned as a follow-up release; check the [adcp-go releases](https://github.com/adcontextprotocol/adcp-go/releases). +`@adcp/sdk@15.9.2` is on the npm `latest` dist-tag and includes the seller-side reporting modules (`@adcp/sdk/reporting/source` and `@adcp/sdk/reporting/ledger`) and the buyer-side consumer module (`@adcp/sdk/reporting/consumer`). The production composer (`@adcp/sdk/reporting/service`) and private GCS adapter (`@adcp/sdk/reporting/gcs`) require application-specific provisioning and rollout validation. Python `adcp==8.0.0` embeds the final 3.2 schemas. Final 3.2 support for Go is planned as a follow-up release; check the [adcp-go releases](https://github.com/adcontextprotocol/adcp-go/releases). SDK 15 tightens OAuth issuer binding and provider/storage defaults. Before upgrading an existing integration, follow the [SDK 15 OAuth migration and owner recovery guide](https://github.com/adcontextprotocol/adcp-client/blob/68614aa0ce84046c78d76dda9721ffbf9328639a/docs/migration-oauth-issuer-bindings.md). @@ -98,7 +98,7 @@ documents coordinator options, continuation storage, and supported projections. [![npm version](https://img.shields.io/npm/v/@adcp/sdk)](https://www.npmjs.com/package/@adcp/sdk) ```bash -npm install @adcp/sdk@15.8.1 +npm install @adcp/sdk@15.9.2 ``` ```javascript @@ -210,11 +210,11 @@ Both SDKs share the same positional shape: `adcp [tool] [payload]`. The ### JavaScript CLI ```bash -npx @adcp/sdk@15.8.1 --help -npx @adcp/sdk@15.8.1 --save-auth my-agent https://sales.example.com/mcp -npx @adcp/sdk@15.8.1 my-agent list_products '{"adcp_version":"3.2","account":{"account_id":"account_123"}}' +npx @adcp/sdk@15.9.2 --help +npx @adcp/sdk@15.9.2 --save-auth my-agent https://sales.example.com/mcp +npx @adcp/sdk@15.9.2 my-agent list_products '{"adcp_version":"3.2","account":{"account_id":"account_123"}}' # or against the built-in public test agent: -npx @adcp/sdk@15.8.1 test-mcp list_products '{"adcp_version":"3.2"}' +npx @adcp/sdk@15.9.2 test-mcp list_products '{"adcp_version":"3.2"}' ``` The `adcp_version` in the payload is a wire pin the CLI passes through, so it diff --git a/docs/runbooks/training-agent-gcs-reporting.md b/docs/runbooks/training-agent-gcs-reporting.md index 174e47f114e..067e50a1cf1 100644 --- a/docs/runbooks/training-agent-gcs-reporting.md +++ b/docs/runbooks/training-agent-gcs-reporting.md @@ -71,7 +71,10 @@ Migration 620 installs the published production composer's Core, finality writer fence, Managed Delivery, notification/outbox/activity, object inventory and installation-authority SQL in `training_reporting_gcs`. Migration 621 adds the obligation tombstone table that the SDK 15.8.1 Core ledger reads before -planning any period; it has the same namespace prerequisite. Core and Managed +planning any period; it has the same namespace prerequisite. Migration 624 adds +the destination authorization `pattern`/`transport` columns that the SDK 15.9.2 +Managed Delivery store reads on provisioning, binding installs and every worker +pass, plus the 15.9 tombstone retry columns; same prerequisite. Core and Managed use the **same** pool on the application primary. Host tables freeze source facts, the destination, principal, bucket, namespace and trusted reader grants. The SDK's installation UUID survives migration replay and process reconstruction. diff --git a/package-lock.json b/package-lock.json index 0b134dce89c..6feed8d5ec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "adcontextprotocol", "version": "3.2.1", "dependencies": { - "@adcp/sdk": "15.8.1", + "@adcp/sdk": "15.9.2", "@anthropic-ai/sdk": "^0.127.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.9.7", @@ -136,9 +136,9 @@ } }, "node_modules/@adcp/sdk": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-15.8.1.tgz", - "integrity": "sha512-Nrvviq/U+0VVweHAxdfVEP3vtj1kcisfW9ggHCBK6BtHEOUyiXvqWaT96bhckM2BQkprqYffxM7msNYKOFkFbw==", + "version": "15.9.2", + "resolved": "https://registry.npmjs.org/@adcp/sdk/-/sdk-15.9.2.tgz", + "integrity": "sha512-xIUjXaT/HIag7CdlVtaFJGX6bgOJ5fjGHeENqi0fY6jdparDjwdUN4BggznUMaJ6A6IH5p2Boh1lZSjpgjlYxA==", "license": "Apache-2.0", "workspaces": [ ".", diff --git a/package.json b/package.json index adf881678a2..644f77ecf2a 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "eval:addie-matched-v4-authorized": "tsx --import dotenv/config server/tests/manual/matched-v4-authorized-execution.ts" }, "dependencies": { - "@adcp/sdk": "15.8.1", + "@adcp/sdk": "15.9.2", "@anthropic-ai/sdk": "^0.127.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.9.7", diff --git a/scripts/external-smoke/README.md b/scripts/external-smoke/README.md index 1f2a1304640..e64ffd6b20e 100644 --- a/scripts/external-smoke/README.md +++ b/scripts/external-smoke/README.md @@ -10,25 +10,23 @@ Each authenticated/anonymous probe makes at most two discovery attempts, each with a fresh AbortSignal and fetch closure, a 10s total budget, and explicit 4s SDK transport timeout. Only categorized transient transport failures retry; schema, assertion, auth and unknown errors do not. -Authenticated discovery runs once per negotiated release per attempt, and its -profile is reused for local storyboard resolution. Anonymous success meets the -same checks, or failed discovery must supply concrete authentication error -evidence (including the SDK's OAuth challenge, "requires OAuth authorization"). +Authenticated discovery runs once per attempt, and its profile is reused for +local storyboard resolution. Anonymous success meets the same checks, or failed +discovery must supply concrete authentication error evidence (including the +SDK's OAuth challenge, "requires OAuth authorization"). The probe pins the default hosted line (3.0). A 3.0 pin cannot carry `adcp_version`, so the SDK sends only `adcp_major_version: 3`, and the agent -may serve and echo its highest 3.x release. `@adcp/sdk` validates the response -against the 3.0 pin regardless. The versioning spec says buyers validate -against the echoed release. When the echo differs from the pin, the smoke check -therefore requires the echo to be listed in `adcp.supported_versions`, -discovers again pinned to that release, validates strictly against it, and -resolves storyboards from that release's hosted bundle. An echo with no hosted -bundle, a different major, or a different release on the pinned probe fails. - -SDK 15.8.1 also keys `comply_test_controller` as protocol `compliance` in its -tool cross-check, while it parses the `compliance_testing` block as -`compliance_testing`. The check waives only that exact cross-check message, -and only when the response carries the `compliance_testing` block. +may serve and echo its highest 3.x release. Since 15.9.2, `@adcp/sdk` +validates the response against its bundled schema for the echoed release, as +the versioning spec requires, and falls back to the pin when the echo is +missing, from another major, or not bundled. (An external schema root bound to +the pin would win over the echo; `testCapabilityDiscovery` does not register +the probe's `schemaRoot`, so none applies here.) When the agent echoes a release, +the smoke check also requires the echo to be in the pin's major and listed in +`adcp.supported_versions`, and resolves storyboards from that release's hosted +bundle in this checkout (the 3.0 bundle does not know later specialisms such +as `sales-dooh`). An echo with no hosted bundle fails. SDK 14.0.0-rc.35 does not pass the signal to its second capability call. The local `trustedFetchFn` composes the attempt signal with the transport signal diff --git a/scripts/external-smoke/check.ts b/scripts/external-smoke/check.ts index a807df44f6d..1531aa9b32e 100644 --- a/scripts/external-smoke/check.ts +++ b/scripts/external-smoke/check.ts @@ -88,28 +88,10 @@ export async function boundedDiscovery( } } -// @adcp/sdk 15.8.1 (and 15.9.1) parses a `compliance_testing` capability -// block as protocol `compliance_testing`, but its tool cross-check keys -// comply_test_controller under `compliance`. Every agent that correctly -// declares the block therefore fails the step with exactly this message. -// Waive only that exact message, and only when the agent's response carries -// the block (the SDK also adds the protocol when the block is missing). Any -// other cross-check issue still fails. -const SDK_COMPLIANCE_CROSS_CHECK_STEP = 'Cross-validate protocols and tools'; -const SDK_COMPLIANCE_CROSS_CHECK_ERROR = 'Tools suggest unreported protocols: compliance (has: [comply_test_controller])'; - -function isSdkComplianceCrossCheckFalsePositive(step: DiscoveryResult['steps'][number], result: DiscoveryResult): boolean { - return step.step === SDK_COMPLIANCE_CROSS_CHECK_STEP - && step.error === SDK_COMPLIANCE_CROSS_CHECK_ERROR - && result.profile?.supported_protocols?.includes('compliance_testing') === true - && typeof (result.profile.raw_capabilities as { compliance_testing?: unknown } | undefined)?.compliance_testing === 'object'; -} - function assertDiscovery(result: DiscoveryResult): void { const schemaIssues = result.profile?.capabilities_schema_issues; if (schemaIssues?.length) throw new SmokeFailure('semantic', `Capability schema errors: ${JSON.stringify(schemaIssues)}`); - const failed = result.steps.filter(step => !step.passed && !step.skipped - && !isSdkComplianceCrossCheckFalsePositive(step, result)); + const failed = result.steps.filter(step => !step.passed && !step.skipped); const failures = failed.map(step => ({ kind: step.task === 'getAgentInfo' || step.task === 'get_adcp_capabilities' ? failureKind(step.error) : 'semantic' as FailureKind, @@ -136,71 +118,45 @@ function assertDiscovery(result: DiscoveryResult): void { type ResolverOptions = Parameters[1]; -/** Probe and storyboard-resolution options pinned to one release. */ -export interface ReleaseProbe { - options: TestOptions; - resolverOptions: ResolverOptions; -} +/** Storyboard-resolution options for the release the agent echoed. */ +export type ResolverForRelease = (release: string) => ResolverOptions; -/** Builds a probe pinned to the release the agent said it served. */ -export type RepinProbe = (release: string) => ReleaseProbe; - -const RELEASE_RE = /^([1-9]\d*)\.(\d+)(?:\.\d+)?(?:-([0-9A-Za-z.-]+))?$/; - -/** Release-precision wire value (`3.0`, `3.2-rc.7`) for a pin or an echo. */ -export function releasePrecision(version: unknown): string | undefined { - const match = typeof version === 'string' ? version.match(RELEASE_RE) : null; - return match ? `${match[1]}.${match[2]}${match[3] ? `-${match[3]}` : ''}` : undefined; -} - -/** The release the agent echoed on `get_adcp_capabilities`, if any. */ -function servedRelease(result: DiscoveryResult): string | undefined { - const raw = result.profile?.raw_capabilities as { adcp_version?: unknown } | undefined; - return releasePrecision(raw?.adcp_version); -} +const MAJOR_RE = /^([1-9]\d*)\./; /** - * The SDK validates capabilities against the caller's pin, not the served - * release. A 3.0 pin cannot carry `adcp_version`, so the SDK sends only - * `adcp_major_version: 3` and the agent may serve its highest 3.x release - * (docs/reference/versioning.mdx, "Server resolves"). The spec says buyers - * validate against the echoed release, not their pin ("Server echoes"). - * - * When the echo differs from the pin, the agent must advertise the echoed - * release. The probe then discovers again pinned to it, so the SDK validates - * strictly against that release, and resolves storyboards from that - * release's bundle. The pinned probe must keep serving the same release. + * A 3.0 pin cannot carry `adcp_version`, so the SDK sends only + * `adcp_major_version: 3` and the agent may serve and echo its highest 3.x + * release (docs/reference/versioning.mdx, "Server resolves"). The SDK + * validates the capabilities against that echo. This checks that the agent + * may serve it, and resolves storyboards from the served release's bundle: + * the pin's bundle does not know specialisms that later releases added. */ -async function negotiateServedRelease( - url: string, - probe: ReleaseProbe, +function servedResolverOptions( result: DiscoveryResult, - discover: Discovery, - repin: RepinProbe | undefined, -): Promise<{ probe: ReleaseProbe; result: DiscoveryResult }> { - const served = servedRelease(result); - const pinned = releasePrecision(probe.options.wireAdcpVersion ?? probe.options.adcpVersion); - if (!served || !pinned || served === pinned || !repin) return { probe, result }; + options: TestOptions, + resolverOptions: ResolverOptions, + resolverFor: ResolverForRelease | undefined, +): ResolverOptions { + const echoed = (result.profile?.raw_capabilities as { adcp_version?: unknown } | undefined)?.adcp_version; + if (echoed === undefined) return resolverOptions; + // The SDK falls back to the pin for an echo it cannot validate against, so + // its schema issues are context for the failures below. const issues = result.profile?.capabilities_schema_issues; - const context = issues?.length ? `; issues against the ${pinned} pin: ${JSON.stringify(issues)}` : ''; - if (served.split('.')[0] !== pinned.split('.')[0]) { - throw new SmokeFailure('semantic', `Agent served AdCP ${served} to a ${pinned} pin; majors must agree${context}`); + const context = issues?.length ? `; capability schema errors: ${JSON.stringify(issues)}` : ''; + const pinned = options.wireAdcpVersion ?? options.adcpVersion; + const pinMajor = pinned?.match(MAJOR_RE)?.[1]; + if (typeof echoed !== 'string' || (pinMajor !== undefined && echoed.match(MAJOR_RE)?.[1] !== pinMajor)) { + throw new SmokeFailure('semantic', `Agent served AdCP ${JSON.stringify(echoed)} to a ${pinned} pin; majors must agree${context}`); } - if (!result.profile?.adcp_supported_versions?.includes(served)) { - throw new SmokeFailure('semantic', `Agent served AdCP ${served}, which its supported_versions do not list${context}`); + if (!result.profile?.adcp_supported_versions?.includes(echoed)) { + throw new SmokeFailure('semantic', `Agent served AdCP ${echoed}, which its supported_versions do not list${context}`); } - let servedProbe: ReleaseProbe; + if (!resolverFor) return resolverOptions; try { - servedProbe = repin(served); + return resolverFor(echoed); } catch (error) { - throw new SmokeFailure('semantic', `Agent served AdCP ${served}, but no hosted bundle is available to validate it: ${error instanceof Error ? error.message : String(error)}${context}`); - } - const servedResult = await discover(url, servedProbe.options); - const reserved = servedRelease(servedResult); - if (reserved !== undefined && reserved !== served) { - throw new SmokeFailure('semantic', `Agent served AdCP ${served} to a ${pinned} pin, but ${reserved} when pinned to ${served}`); + throw new SmokeFailure('semantic', `Agent served AdCP ${echoed}, but no hosted bundle is available to resolve its storyboards: ${error instanceof Error ? error.message : String(error)}`); } - return { probe: servedProbe, result: servedResult }; } export async function checkProbe( @@ -211,15 +167,16 @@ export async function checkProbe( { discover = (endpoint, opts) => boundedDiscovery(endpoint, opts), resolve = resolveStoryboardsForCapabilities, - repin, - }: { discover?: Discovery; resolve?: Resolver; repin?: RepinProbe } = {}, + resolverFor, + }: { discover?: Discovery; resolve?: Resolver; resolverFor?: ResolverForRelease } = {}, ): Promise<'public' | 'authenticated' | 'auth-required'> { for (let attempt = 1; ; attempt++) { let result: DiscoveryResult; - let probe: ReleaseProbe = { options, resolverOptions }; + let served: ResolverOptions; try { - // One discovery per negotiated release per attempt; reuse its profile. - ({ probe, result } = await negotiateServedRelease(url, probe, await discover(url, options), discover, repin)); + // One discovery per attempt; reuse its profile. + result = await discover(url, options); + served = servedResolverOptions(result, options, resolverOptions, resolverFor); assertDiscovery(result); } catch (error) { const kind = failureKind(error); @@ -233,7 +190,7 @@ export async function checkProbe( specialisms: profile.specialisms ?? [], major_versions: profile.adcp_major_versions, supported_versions: profile.adcp_supported_versions, - }, probe.resolverOptions); + }, served); assert(resolved.storyboards.length > 0, 'No applicable storyboards'); assert(resolved.bundles.length > 0, 'No applicable bundles'); return anonymous ? 'public' : 'authenticated'; diff --git a/scripts/external-smoke/run.ts b/scripts/external-smoke/run.ts index 4025a402174..a4dd85abe03 100644 --- a/scripts/external-smoke/run.ts +++ b/scripts/external-smoke/run.ts @@ -1,29 +1,22 @@ -import type { TestOptions } from '@adcp/sdk/testing'; import { PUBLIC_TEST_AGENT } from '../../server/src/config/test-agent.js'; import { hostedComplianceTarget, hostedComplianceOptions, withHostedTestOptions, } from '../../server/src/services/hosted-compliance-version.js'; -import { checkProbe, type RepinProbe } from './check.js'; +import { checkProbe, type ResolverForRelease } from './check.js'; const url = process.env.ADCP_SMOKE_CHECK_URL || PUBLIC_TEST_AGENT.url; const token = process.env.ADCP_SMOKE_CHECK_TOKEN || PUBLIC_TEST_AGENT.token; const target = hostedComplianceTarget(); const resolverOptions = hostedComplianceOptions(target); -// When the agent serves a different release than the default pin, validate -// and resolve against this checkout's hosted bundle for the served release. -const repinFor = (base: Partial): RepinProbe => release => { - const served = hostedComplianceTarget(release); - return { options: withHostedTestOptions(base, served), resolverOptions: hostedComplianceOptions(served) }; -}; +// Resolve storyboards from this checkout's hosted bundle for the release the +// agent served, not the default pin's. +const resolverFor: ResolverForRelease = release => hostedComplianceOptions(hostedComplianceTarget(release)); -const authenticated: Partial = { auth: { type: 'bearer', token } }; -await checkProbe(url, withHostedTestOptions(authenticated, target), resolverOptions, false, { - repin: repinFor(authenticated), +await checkProbe(url, withHostedTestOptions({ auth: { type: 'bearer', token } }, target), resolverOptions, false, { + resolverFor, }); console.log('Authenticated discovery and storyboard resolution passed.'); -const anonymous = await checkProbe(url, withHostedTestOptions({}, target), resolverOptions, true, { - repin: repinFor({}), -}); +const anonymous = await checkProbe(url, withHostedTestOptions({}, target), resolverOptions, true, { resolverFor }); console.log(`Anonymous discovery: ${anonymous}.`); diff --git a/scripts/sdk-response-conformance/README.md b/scripts/sdk-response-conformance/README.md index ca3f0289071..14773ea77fe 100644 --- a/scripts/sdk-response-conformance/README.md +++ b/scripts/sdk-response-conformance/README.md @@ -40,7 +40,7 @@ The report exits **1 for findings or skipped cases**, **2 for a harness failure* ## Initial findings and regression policy -The baseline was first recorded with `@adcp/sdk@14.0.0-rc.35`, `adcp==8.0.0b14`, and Go `adcp/v3@v3.2.1` against RC.1. The current evidence pairs TypeScript `@adcp/sdk@15.8.1` with the 3.2.1 GA release while retaining the explicitly pinned Python and Go packages; each baseline entry records its exact protocol artifact and package version. The TypeScript probe was rerun under SDK 15.8.1: all 125 cases dispatched, with the same artifact digest, counts and existing schema finding; only the recorded SDK version changed. Python and Go entries retain their earlier pinned evidence. There are two catalog cases, one case for every published error default, and three overrides per language. Go skips native listing. The report records: +The baseline was first recorded with `@adcp/sdk@14.0.0-rc.35`, `adcp==8.0.0b14`, and Go `adcp/v3@v3.2.1` against RC.1. The current evidence pairs TypeScript `@adcp/sdk@15.9.2` with the 3.2.1 GA release while retaining the explicitly pinned Python and Go packages; each baseline entry records its exact protocol artifact and package version. The TypeScript probe was rerun under SDK 15.9.2: all 125 cases dispatched, with the same artifact digest, counts and existing schema finding; only the recorded SDK version changed. Python and Go entries retain their earlier pinned evidence. There are two catalog cases, one case for every published error default, and three overrides per language. Go skips native listing. The report records: - TypeScript: error defaults and overrides agree; `list_products` gains `adcp_version`, which the raw pinned response schema forbids. This is a schema/envelope inconsistency, related to the prior [SDK normalization fix #2594](https://github.com/adcontextprotocol/adcp-client/issues/2594); it is not a claim that the fixed SDK client still rejects the response. The probe deliberately retains the raw artifact result instead of silently stripping fields or patching an immutable schema. - Python: 84 default recovery classifications differ from the published metadata; native listing gains `status: completed`, rejected both by the raw pinned schema and the MCP client's advertised-output validator. diff --git a/scripts/sdk-response-conformance/known-findings.json b/scripts/sdk-response-conformance/known-findings.json index aaae69a5287..e3605473dce 100644 --- a/scripts/sdk-response-conformance/known-findings.json +++ b/scripts/sdk-response-conformance/known-findings.json @@ -1,6 +1,6 @@ [ { - "sdk": {"package":"@adcp/sdk","version":"15.8.1"}, + "sdk": {"package":"@adcp/sdk","version":"15.9.2"}, "protocol": { "version": "3.2.1", "wire_selector": "3.2", diff --git a/scripts/training-gcs-buyer.mjs b/scripts/training-gcs-buyer.mjs index 2aab8525f58..d9c7e61c3d2 100644 --- a/scripts/training-gcs-buyer.mjs +++ b/scripts/training-gcs-buyer.mjs @@ -133,7 +133,7 @@ async function main() { if (values.init) { await pool.query('CREATE SCHEMA IF NOT EXISTS training_reporting_buyer'); for (const sql of persistence.migrations.all) await pool.query(sql); } await persistence.probe(); const result = await verifyTrainingGcsBuyer({ grant, expected, storage, client, persistence, consumerScope: `${base}:${grant.principal_id}`, authorize }); - const evidence = { sdk: '15.8.1', protocol: 'mcp', checked_at: new Date().toISOString(), grant, + const evidence = { sdk: '15.9.2', protocol: 'mcp', checked_at: new Date().toISOString(), grant, definitive: result.definitive, receipt_batches: receiptBatches, status_batches: statusBatches, receipts: result.submittedReceipts, materializations: result.ledger.materializations, obligations: result.obligations, failed_consumer_statuses: result.failedConsumerStatuses }; diff --git a/server/src/addie/config-version.ts b/server/src/addie/config-version.ts index 964b89ea953..1f84e43e855 100644 --- a/server/src/addie/config-version.ts +++ b/server/src/addie/config-version.ts @@ -30,7 +30,7 @@ import { loadRules, loadResponseStyle } from './rules/index.js'; * Format: YYYY.MM.N where N is incremented for multiple changes in a month * Example: 2025.01.1, 2025.01.2, 2025.02.1 */ -export const CODE_VERSION = '2026.10.6'; +export const CODE_VERSION = '2026.10.7'; // Types export interface ConfigVersion { diff --git a/server/src/addie/mcp/certification-tools.ts b/server/src/addie/mcp/certification-tools.ts index 48f67712002..c6cdfcb8df7 100644 --- a/server/src/addie/mcp/certification-tools.ts +++ b/server/src/addie/mcp/certification-tools.ts @@ -3076,7 +3076,7 @@ Tell ${codingTool}: "Build a buyer agent using @adcp/sdk that connects to the pu The SDK handles protocol details — the learner focuses on orchestration logic. -Use the current stable \`3.2\` wire pin with @adcp/sdk@15.8.1 for the targeting-aware discovery portion. Decompose one messy request into brief plus criteria.offer_filters, criteria.targeting_overlay, and criteria.required_overlay_support; verify that unsupported future-selection requirements filter products; review any targeting_resolution.modifications before purchase; and verify effective package targeting on readback. Treat the get_products compatibility facade's equivalent fields as compatibility evidence, not as proof that the compact tasks work. +Use the current stable \`3.2\` wire pin with @adcp/sdk@15.9.2 for the targeting-aware discovery portion. Decompose one messy request into brief plus criteria.offer_filters, criteria.targeting_overlay, and criteria.required_overlay_support; verify that unsupported future-selection requirements filter products; review any targeting_resolution.modifications before purchase; and verify effective package targeting on readback. Treat the get_products compatibility facade's equivalent fields as compatibility evidence, not as proof that the compact tasks work. Reference: ${SDKS_URL} @@ -3091,7 +3091,7 @@ Validate in two parts. 1. Run the compatibility buying workflow against the public test agent and share the output. Use the \`adcp\` CLI: \`\`\` -npx @adcp/sdk@15.8.1 test-mcp get_products '{"adcp_version":"3.2","buying_mode":"brief","brief":""}' +npx @adcp/sdk@15.9.2 test-mcp get_products '{"adcp_version":"3.2","buying_mode":"brief","brief":""}' \`\`\` Replace \`\` with your actual brief. Then run the full buying flow: get_products (select a canonical \`format_options[]\` entry) → create_media_buy → get_adcp_capabilities on the chosen creative endpoint → sync_creatives with \`format_kind\` and optional \`format_option_ref\`. diff --git a/server/src/db/migrations/624_training_gcs_reporting_destination_contract.sql b/server/src/db/migrations/624_training_gcs_reporting_destination_contract.sql new file mode 100644 index 00000000000..75191537e52 --- /dev/null +++ b/server/src/db/migrations/624_training_gcs_reporting_destination_contract.sql @@ -0,0 +1,92 @@ +-- SDK 15.9.2 reporting migration deltas over the SQL in migrations 620 and 621. +-- PostgresReportingManagedDeliveryStore.authorizeDestination, installBinding and +-- claimRevocation (run by every managed delivery worker pass) read and write the +-- destination authorization pattern/transport columns unconditionally, so the +-- GCS reporting runtime needs them even with nothing to revoke. The tombstone +-- retry columns keep REPORTING_LEDGER_MIGRATION parity; only period retention, +-- which the training runtime does not enable, reads them. Both blocks are +-- verbatim from the SDK, additive and idempotent in the same schema. +DO $schema_precondition$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'training_reporting_gcs') THEN + RAISE EXCEPTION 'Provision training_reporting_gcs for the application role before migration 624'; + END IF; + IF NOT has_schema_privilege(current_user, 'training_reporting_gcs', 'USAGE') + OR NOT has_schema_privilege(current_user, 'training_reporting_gcs', 'CREATE') THEN + RAISE EXCEPTION 'Application role requires USAGE and CREATE on training_reporting_gcs'; + END IF; +END; +$schema_precondition$; +SET LOCAL search_path = training_reporting_gcs, pg_catalog; +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '60s'; + +-- Delivery pattern and transport recorded on the authorization generation, so +-- a revocation can be routed to the transport that holds the provider grant. +-- Both are immutable for a generation (a new delivery contract mints a new +-- destination_ref generation) and NULL for a generation whose contract is not +-- known. The block runs once, in the same statement batch that adds the +-- columns: it backfills a row only when every binding on that exact +-- (account_id, destination_ref, generation) agrees on the method and carries +-- the same transport, and otherwise leaves the row NULL. A later re-run finds +-- the columns present and does nothing, so a generation that was left NULL +-- stays NULL rather than being re-decided by whichever binding arrived since. +DO $pattern_transport$ +DECLARE + unresolved BIGINT; +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = current_schema() + AND table_name = 'adcp_reporting_destination_authorizations' + AND column_name = 'transport' + ) THEN + ALTER TABLE adcp_reporting_destination_authorizations ADD COLUMN IF NOT EXISTS pattern TEXT; + ALTER TABLE adcp_reporting_destination_authorizations ADD COLUMN IF NOT EXISTS transport TEXT; + WITH agreed AS ( + SELECT authz.account_id, authz.destination_ref, authz.generation, + MIN(binding.data->>'method') AS pattern, + MIN(binding.data->>'transport') AS transport + FROM adcp_reporting_destination_authorizations authz + JOIN adcp_reporting_managed_bindings binding + ON binding.account_id = authz.account_id + AND binding.destination_ref = authz.destination_ref + AND binding.authorization_generation = authz.generation + WHERE authz.pattern IS NULL AND authz.transport IS NULL + GROUP BY authz.account_id, authz.destination_ref, authz.generation + HAVING COUNT(*) = COUNT(binding.data->>'method') + AND COUNT(DISTINCT binding.data->>'method') = 1 + AND COUNT(*) = COUNT(binding.data->>'transport') + AND COUNT(DISTINCT binding.data->>'transport') = 1 + ) + UPDATE adcp_reporting_destination_authorizations authz + SET pattern = agreed.pattern, + transport = agreed.transport, + data = authz.data || jsonb_build_object('pattern', agreed.pattern, 'transport', agreed.transport) + FROM agreed + WHERE authz.account_id = agreed.account_id + AND authz.destination_ref = agreed.destination_ref + AND authz.generation = agreed.generation; + SELECT COUNT(*) INTO unresolved FROM adcp_reporting_destination_authorizations WHERE transport IS NULL; + IF unresolved > 0 THEN + RAISE NOTICE 'adcp_reporting_destination_authorizations: % generation(s) left without pattern/transport; list them with PostgresReportingManagedDeliveryStore.listDestinationAuthorizationsWithoutContract()', unresolved; + END IF; + END IF; +END +$pattern_transport$; + +DO $tombstone_retry$ +DECLARE + tombstones regclass := to_regclass('adcp_reporting_obligation_tombstones'); +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = tombstones AND attname = 'attempts' AND NOT attisdropped) THEN + ALTER TABLE adcp_reporting_obligation_tombstones ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = tombstones AND attname = 'next_attempt_at' AND NOT attisdropped) THEN + ALTER TABLE adcp_reporting_obligation_tombstones ADD COLUMN next_attempt_at TIMESTAMPTZ; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = tombstones AND attname = 'last_failure' AND NOT attisdropped) THEN + ALTER TABLE adcp_reporting_obligation_tombstones ADD COLUMN last_failure TEXT; + END IF; +END +$tombstone_retry$; diff --git a/server/tests/integration/registry-api-agent-refresh.test.ts b/server/tests/integration/registry-api-agent-refresh.test.ts index c479118f3d2..61a9fe8deb5 100644 --- a/server/tests/integration/registry-api-agent-refresh.test.ts +++ b/server/tests/integration/registry-api-agent-refresh.test.ts @@ -451,7 +451,7 @@ describe('POST /api/registry/agents/:encodedUrl/refresh (integration)', () => { expect(res.status).toBe(200); expect(res.body).toMatchObject({ completeness, is_authoritative: false, badge_eligible: false, badge_eligible_adcp_versions: [], diagnostics: [], - provenance: { sdk_version: '15.8.1', test_session_id: expect.any(String), agent_build_version: null }, + provenance: { sdk_version: '15.9.2', test_session_id: expect.any(String), agent_build_version: null }, }); expect(complyMock.mock.calls[0][1].test_session_id).toBe(res.body.provenance.test_session_id); const db = new ComplianceDatabase(); @@ -1242,7 +1242,7 @@ describe('POST /api/registry/agents/:encodedUrl/refresh (integration)', () => { currentUserId = STATIC_ADMIN_USER_ID; const diagnostics = await request(app).get(`/api/registry/agents/${encodeURIComponent(agentUrl)}/compliance/diagnostics`); expect(diagnostics.body).toMatchObject({ completeness: 'timed_out', is_authoritative: false, - diagnostics_visibility: 'owner_or_operator', provenance: { sdk_version: '15.8.1', agent_build_version: null } }); + diagnostics_visibility: 'owner_or_operator', provenance: { sdk_version: '15.9.2', agent_build_version: null } }); } } finally { revokeBadges.mockRestore(); diff --git a/server/tests/integration/registry-api-compliance-verdict-source.test.ts b/server/tests/integration/registry-api-compliance-verdict-source.test.ts index 9175de2f068..03d98f91f03 100644 --- a/server/tests/integration/registry-api-compliance-verdict-source.test.ts +++ b/server/tests/integration/registry-api-compliance-verdict-source.test.ts @@ -755,7 +755,7 @@ describe('GET /api/registry/agents/:encodedUrl/compliance — owner-scope gate ( const response = await request(app).get(`/api/registry/agents/${encodeURIComponent(AGENT_URL)}/compliance/diagnostics?run_id=${run.id}`); expect(response.status).toBe(200); expect(response.body).toMatchObject({ run_id: run.id, completeness: 'timed_out', is_authoritative: false, - provenance: { compliance_bundle_version: '3.1.20', sdk_version: '15.8.1', agent_build_version: 'build-immutable-42', agent_library_version: 'seller-sdk-2' }, + provenance: { compliance_bundle_version: '3.1.20', sdk_version: '15.9.2', agent_build_version: 'build-immutable-42', agent_library_version: 'seller-sdk-2' }, diagnostics_visibility: 'owner_or_operator' }); expect(response.body.diagnostics[0].error_text).toBe('Expected products array'); expect(JSON.stringify(response.body)).not.toContain('fixture-secret-value'); diff --git a/server/tests/integration/training-gcs-reporting-postgres.test.ts b/server/tests/integration/training-gcs-reporting-postgres.test.ts index 902e31ba1c4..2f6ef1cd115 100644 --- a/server/tests/integration/training-gcs-reporting-postgres.test.ts +++ b/server/tests/integration/training-gcs-reporting-postgres.test.ts @@ -83,7 +83,7 @@ describe.skipIf(!testUrl)('training GCS reporting against disposable PostgreSQL' await client.query('BEGIN'); await client.query(`DROP SCHEMA IF EXISTS ${TRAINING_REPORTING_SCHEMA} CASCADE`); await client.query(`CREATE SCHEMA ${TRAINING_REPORTING_SCHEMA}`); - for (const migration of ['620_training_gcs_reporting.sql', '621_training_gcs_reporting_obligation_tombstones.sql']) { + for (const migration of ['620_training_gcs_reporting.sql', '621_training_gcs_reporting_obligation_tombstones.sql', '624_training_gcs_reporting_destination_contract.sql']) { await client.query(await readFile(new URL(`../../src/db/migrations/${migration}`, import.meta.url), 'utf8')); } const legacy = await readFile(new URL('../../src/db/migrations/575_reporting_reliability_curriculum.sql', import.meta.url), 'utf8'); diff --git a/server/tests/unit/compliance-publication.test.ts b/server/tests/unit/compliance-publication.test.ts index d2ade3f0e77..c904fa3b516 100644 --- a/server/tests/unit/compliance-publication.test.ts +++ b/server/tests/unit/compliance-publication.test.ts @@ -181,7 +181,7 @@ describe('hosted compliance publication policy', () => { it('records exact runner and agent provenance, leaving absent agent builds null', () => { const run = result([{ passed: true }]); expect(complianceResultToDbInput(run, agentUrl, 'production').provenance_json).toMatchObject({ - compliance_bundle_version: '3.1.20', sdk_version: '15.8.1', + compliance_bundle_version: '3.1.20', sdk_version: '15.9.2', agent_build_version: 'test-build', agent_library_version: 'seller-sdk-1', }); delete run.agent_profile.adcp_build_version; diff --git a/server/tests/unit/external-smoke.test.ts b/server/tests/unit/external-smoke.test.ts index 006d64423da..a839d674061 100644 --- a/server/tests/unit/external-smoke.test.ts +++ b/server/tests/unit/external-smoke.test.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'; import { parseDocument } from 'yaml'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { testCapabilityDiscovery, type TestOptions } from '@adcp/sdk/testing'; -import { boundedDiscovery, checkProbe, MAX_ATTEMPTS, releasePrecision, SmokeFailure, type ReleaseProbe } from '../../../scripts/external-smoke/check.js'; +import { boundedDiscovery, checkProbe, MAX_ATTEMPTS, SmokeFailure } from '../../../scripts/external-smoke/check.js'; const root = fileURLToPath(new URL('../../../', import.meta.url)); const url = 'https://smoke.example.com/mcp'; @@ -182,126 +182,101 @@ describe('external smoke discovery', () => { }); }); -describe('external smoke served-release negotiation', () => { +describe('external smoke served release', () => { // A 3.0 pin cannot carry adcp_version, so the SDK sends only - // adcp_major_version: 3 and validates against 3.0. The agent may serve its - // highest 3.x release and echo it; the spec says to validate against that. + // adcp_major_version: 3. The agent may serve its highest 3.x release and + // echo it; the SDK validates against that echo. const pinned: TestOptions = { ...options, adcpVersion: '3.0.27' }; const pinnedResolver = { version: '3.0.27' } as Parameters[2]; - const servedProbe: ReleaseProbe = { - options: { ...options, adcpVersion: '3.2.4' }, - resolverOptions: { version: '3.2.4' } as ReleaseProbe['resolverOptions'], - }; - const served = (echo: string | undefined, issues: Array<{ pointer: string; message: string }> = []): Result => { + const servedResolver = { version: '3.2.4' } as Parameters[2]; + const served = (echo: unknown, issues: Array<{ pointer: string; message: string }> = []): Result => { const result = success(); - result.profile!.adcp_supported_versions = ['3.0', '3.1', '3.2']; + result.profile!.adcp_supported_versions = ['3.0', '3.1', '3.2-rc.7', '3.2']; result.profile!.raw_capabilities = echo === undefined ? {} : { adcp_version: echo }; if (issues.length) result.profile!.capabilities_schema_issues = issues; return result; }; + // What the SDK reports when it falls back to validating a 3.2 response + // against the 3.0 pin. const threeTwoShapes = [ { pointer: '/media_buy/features/bidding_policy', message: 'must be boolean' }, { pointer: '/compliance_testing/scenarios/4', message: 'must be one of: "force_creative_status"' }, ]; - it('validates and resolves against the echoed release instead of the 3.0 pin', async () => { - const discover = vi.fn() - .mockResolvedValueOnce(served('3.2', threeTwoShapes)) - .mockResolvedValueOnce(served('3.2')); - const repin = vi.fn(() => servedProbe); + it.each(['3.2', '3.2-rc.7', '3.0'])('resolves storyboards from the bundle of echoed release %s after one discovery', async echo => { + const discover = vi.fn().mockResolvedValue(served(echo)); + const resolverFor = vi.fn(() => servedResolver); const resolve = vi.fn(() => resolved); - await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, resolve, repin })) + await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, resolve, resolverFor })) .resolves.toBe('authenticated'); - expect(repin).toHaveBeenCalledExactlyOnceWith('3.2'); - expect(discover).toHaveBeenNthCalledWith(1, url, pinned); - expect(discover).toHaveBeenNthCalledWith(2, url, servedProbe.options); - expect(resolve).toHaveBeenCalledExactlyOnceWith(expect.anything(), servedProbe.resolverOptions); + expect(discover).toHaveBeenCalledExactlyOnceWith(url, pinned); + expect(resolverFor).toHaveBeenCalledExactlyOnceWith(echo); + expect(resolve).toHaveBeenCalledExactlyOnceWith(expect.anything(), servedResolver); }); - it('re-validates at the served release even when the pin validation passed', async () => { - const discover = vi.fn() - .mockResolvedValueOnce(served('3.2')) - .mockResolvedValueOnce(served('3.2', [{ pointer: '/adcp', message: 'invalid' }])); - await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, repin: () => servedProbe })) - .rejects.toMatchObject({ kind: 'semantic', message: expect.stringContaining('/adcp') }); - expect(discover).toHaveBeenCalledTimes(2); + it('resolves from the pin bundle when the agent omits the echo', async () => { + const discover = vi.fn().mockResolvedValue(served(undefined)); + const resolverFor = vi.fn(() => servedResolver); + const resolve = vi.fn(() => resolved); + await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, resolve, resolverFor })) + .resolves.toBe('authenticated'); + expect(resolverFor).not.toHaveBeenCalled(); + expect(resolve).toHaveBeenCalledExactlyOnceWith(expect.anything(), pinnedResolver); }); - it.each([ - ['echoes the pinned release', '3.0'], - ['omits the echo', undefined], - ])('keeps pin validation when the agent %s', async (_label, echo) => { - const discover = vi.fn().mockResolvedValue(served(echo, threeTwoShapes)); - const repin = vi.fn(() => servedProbe); - await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, repin })) - .rejects.toMatchObject({ kind: 'semantic', message: expect.stringContaining('bidding_policy') }); + it('rejects schema issues the SDK reports against the served release', async () => { + const discover = vi.fn().mockResolvedValue(served('3.2', [{ pointer: '/adcp', message: 'invalid' }])); + const resolve = vi.fn(() => resolved); + await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, resolve, resolverFor: () => servedResolver })) + .rejects.toMatchObject({ kind: 'semantic', message: expect.stringContaining('/adcp') }); expect(discover).toHaveBeenCalledTimes(1); - expect(repin).not.toHaveBeenCalled(); + expect(resolve).not.toHaveBeenCalled(); }); it.each([ ['a release its supported_versions omit', '3.3', /supported_versions do not list/], ['a different major', '4.0', /majors must agree/], - ])('rejects an agent that serves %s', async (_label, echo, message) => { + ['a non-string version', 3, /majors must agree/], + ])('rejects an agent that serves %s, with the SDK schema issues as context', async (_label, echo, message) => { const discover = vi.fn().mockResolvedValue(served(echo, threeTwoShapes)); - const repin = vi.fn(() => servedProbe); - const rejection = checkProbe(url, pinned, pinnedResolver, false, { discover, repin }); + const resolverFor = vi.fn(() => servedResolver); + const rejection = checkProbe(url, pinned, pinnedResolver, false, { discover, resolverFor }); await expect(rejection).rejects.toMatchObject({ kind: 'semantic', message: expect.stringMatching(message) }); await expect(rejection).rejects.toThrow(/bidding_policy/); expect(discover).toHaveBeenCalledTimes(1); - expect(repin).not.toHaveBeenCalled(); - }); - - it('fails when no hosted bundle exists for the served release', async () => { - const discover = vi.fn().mockResolvedValue(served('3.2', threeTwoShapes)); - const repin = () => { throw new Error('No npm-published compliance version is registered for 3.2'); }; - await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, repin })) - .rejects.toMatchObject({ kind: 'semantic', message: expect.stringMatching(/no hosted bundle.*3\.2.*bidding_policy/) }); - expect(discover).toHaveBeenCalledTimes(1); + expect(resolverFor).not.toHaveBeenCalled(); }); - it('fails when the agent serves another release once pinned to the echoed one', async () => { - const discover = vi.fn() - .mockResolvedValueOnce(served('3.2', threeTwoShapes)) - .mockResolvedValueOnce(served('3.1')); - await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, repin: () => servedProbe })) - .rejects.toMatchObject({ kind: 'semantic', message: expect.stringContaining('but 3.1 when pinned to 3.2') }); + it('checks the echo even without a resolver for the served release', async () => { + const discover = vi.fn().mockResolvedValue(served('3.3')); + await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, resolve: () => resolved })) + .rejects.toMatchObject({ kind: 'semantic', message: expect.stringMatching(/supported_versions do not list/) }); }); - it('normalizes pins and echoes to release precision', () => { - expect(releasePrecision('3.0.27')).toBe('3.0'); - expect(releasePrecision('3.2')).toBe('3.2'); - expect(releasePrecision('3.2.0-rc.7')).toBe('3.2-rc.7'); - expect(releasePrecision('3.2-rc.7')).toBe('3.2-rc.7'); - expect(releasePrecision('v3')).toBeUndefined(); - expect(releasePrecision(3)).toBeUndefined(); + it('fails when no hosted bundle exists for the served release', async () => { + const discover = vi.fn().mockResolvedValue(served('3.2')); + const resolverFor = () => { throw new Error('No npm-published compliance version is registered for 3.2'); }; + await expect(checkProbe(url, pinned, pinnedResolver, false, { discover, resolverFor })) + .rejects.toMatchObject({ kind: 'semantic', message: expect.stringMatching(/AdCP 3\.2, but no hosted bundle.*No npm-published/) }); + expect(discover).toHaveBeenCalledTimes(1); }); }); -describe('external smoke SDK cross-check and auth evidence', () => { - const complianceFalsePositive = 'Tools suggest unreported protocols: compliance (has: [comply_test_controller])'; - const withCrossCheck = (error: string, protocols: string[], declaresBlock = true): Result => { +describe('external smoke cross-check and auth evidence', () => { + it.each([ + // SDK 15.9.2: a declared compliance_testing block requires the controller. + "Protocol 'compliance_testing' reported but no matching tools found", + 'Tools suggest unreported protocols: compliance_testing (has: [comply_test_controller])', + 'Tools suggest unreported protocols: signals (has: [get_signals])', + ])('fails on any SDK protocol/tool cross-check issue: %s', async error => { const result = success(); - result.profile!.supported_protocols = protocols; - result.profile!.raw_capabilities = declaresBlock ? { compliance_testing: { scenarios: ['force_creative_status'] } } : {}; result.steps.push({ step: 'Cross-validate protocols and tools', passed: false, duration_ms: 0, error }); - return result; - }; - - it('waives the SDK compliance cross-check false positive when compliance_testing was declared', async () => { - const discover = vi.fn().mockResolvedValue(withCrossCheck(complianceFalsePositive, ['media_buy', 'compliance_testing'])); - await expect(checkProbe(url, options, {}, false, { discover, resolve: () => resolved })).resolves.toBe('authenticated'); - }); - - it.each([ - ['compliance_testing was not declared', complianceFalsePositive, ['media_buy'], false], - ['the SDK synthesized compliance_testing without a declared block', complianceFalsePositive, ['media_buy', 'compliance_testing'], false], - ['the step reports another issue', `Protocol 'signals' reported but no matching tools found; ${complianceFalsePositive}`, ['media_buy', 'compliance_testing']], - ['another protocol is unreported', 'Tools suggest unreported protocols: signals (has: [get_signals])', ['media_buy', 'compliance_testing']], - ])('still fails the cross-check when %s', async (_label, error, protocols, declaresBlock = true) => { - const discover = vi.fn().mockResolvedValue(withCrossCheck(error, protocols, declaresBlock)); - await expect(checkProbe(url, options, {}, false, { discover, resolve: () => resolved })) - .rejects.toMatchObject({ kind: 'semantic' }); + const discover = vi.fn().mockResolvedValue(result); + const resolve = vi.fn(() => resolved); + await expect(checkProbe(url, options, {}, false, { discover, resolve })) + .rejects.toMatchObject({ kind: 'semantic', message: expect.stringContaining(error) }); + expect(discover).toHaveBeenCalledTimes(1); + expect(resolve).not.toHaveBeenCalled(); }); it('treats an SDK OAuth challenge as anonymous auth-required evidence', async () => { diff --git a/tests/request-signing-profile-3.2-vectors.test.cjs b/tests/request-signing-profile-3.2-vectors.test.cjs index 10a167097da..bb4b3d50134 100644 --- a/tests/request-signing-profile-3.2-vectors.test.cjs +++ b/tests/request-signing-profile-3.2-vectors.test.cjs @@ -189,15 +189,6 @@ async function sdkVerify(vector, id) { ); } -// @adcp/sdk 14.2.0 in adcpVersion '3.2' mode converts a raw IDN U-label to its A-label instead of -// rejecting it (docs/reference/url-canonicalization.mdx: the comparer MUST reject raw non-ASCII), so it -// verifies the single-fault 026 mirror, and it derives @authority from the URL rather than the Host header, so -// it verifies 031. Both are sibling-repo (adcp-client) fixes; remove an entry when the SDK rejects the vector. -const SDK_KNOWN_GAPS = { - '026-non-ascii-host': '@adcp/sdk 14.2.0 (adcpVersion 3.2) verifies a raw U-label host instead of rejecting it', - '031-malformed-host-authority': '@adcp/sdk 14.2.0 (adcpVersion 3.2) derives @authority from the URL and ignores the malformed Host header', -}; - describe('AdCP 3.2 request-signing corpus covers content-digest (adcp#7733)', () => { it('mirrors every legacy positive vector under the same number and slug', () => { const files = new Set(profilePositive.map(e => e.file)); @@ -377,8 +368,7 @@ describe('AdCP 3.2 request-signing vectors verify independently', () => { assert.equal(cryptoVerify(keys.find(k => k.kid === kid), base, bytes), true); }); - const sdkTest = SDK_KNOWN_GAPS[key] ? { todo: SDK_KNOWN_GAPS[key] } : {}; - it(`${id}: @adcp/sdk 3.2 verifier returns the expected outcome`, sdkTest, async () => { + it(`${id}: @adcp/sdk 3.2 verifier returns the expected outcome`, async () => { const outcome = vector.expected_outcome; if (outcome.success) { const result = await sdkVerify(vector, id);