Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/building/by-layer/L4/choose-your-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -210,11 +210,11 @@ Both SDKs share the same positional shape: `adcp <agent> [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
Expand Down
5 changes: 4 additions & 1 deletion docs/runbooks/training-agent-gcs-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 14 additions & 16 deletions scripts/external-smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 36 additions & 79 deletions scripts/external-smoke/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -136,71 +118,45 @@ function assertDiscovery(result: DiscoveryResult): void {

type ResolverOptions = Parameters<Resolver>[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(
Expand All @@ -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);
Expand All @@ -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';
Expand Down
21 changes: 7 additions & 14 deletions scripts/external-smoke/run.ts
Original file line number Diff line number Diff line change
@@ -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<TestOptions>): 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<TestOptions> = { 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}.`);
Loading
Loading