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
25 changes: 25 additions & 0 deletions .changeset/derived-upstream-managed-namespace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@adcp/sdk': major
---

**Breaking (correction inside the unreleased v14):** `AccountStore.resolution: 'derived'` is now an upstream-managed account-id namespace, matching the account-reference model settled in [adcp#5062](https://github.com/adcontextprotocol/adcp/pull/5062). The previous wire semantics were inverted and unusable for the adapters the mode exists for — buyers who called `list_accounts` got ids that every subsequent call rejected.

Wire contract:

- `{ account_id }` is the durable reference for `'derived'` and is now **accepted** (it was refused with `INVALID_REQUEST`); the `{ brand, operator }` natural-key arm is now **refused** with `INVALID_REQUEST` (`field: 'account.brand'`) and a `list_accounts` suggestion.
- `list_accounts` is **required** for `'derived'` — `createAdcpServerFromPlatform` throws `PlatformConfigError` when neither `accounts.list` nor `opts.accounts.listAccounts` is wired. It is the discovery contract for a namespace the agent doesn't own; credential-bound singletons expose one row.
- `sync_accounts` is not categorically blocked: natural-key provisioning entries fail per-row with `UNSUPPORTED_PROVISIONING`, natural-key _references_ fail per-row with `INVALID_REQUEST`, and `account: { account_id }` settings-update entries pass through.
- A declared `'derived'` resolution projects `account.require_operator_auth: true` on `get_adcp_capabilities`, like a declared `'explicit'`.

Tenant-isolation hardening (`'derived'` only):

- `accounts.resolve` must verify buyer-supplied ids; the framework backstops it — a resolved account whose `id` isn't the one the buyer named is refused with `ACCOUNT_NOT_FOUND` (with a dev-mode warning), so an un-migrated resolver that ignores `ref` fails closed instead of cross-serving.
- `sync_accounts` and `sync_governance` entries — the two account references that never passed through `accounts.resolve` — are now resolved against the caller's reachable set before any write, on both the platform and merge-seam wirings.
- The `comply_test_controller` sandbox gate no longer accepts a wire `sandbox: true` claim alongside an `account_id` the resolver refused.

API:

- `createDerivedAccountStore` verifies buyer-supplied `account_id` against what the caller's credential can reach (fail closed → `ACCOUNT_NOT_FOUND`), auto-selects the account on ref-less tools only when exactly one is reachable, and wires a filtered, paged `list_accounts`. New `listAccounts` / `lookupAccount` options support credentials that reach many accounts; `toAccount` keeps its signature for the single-account case.
- Added `AccountResolutionMode` / `CanonicalAccountResolutionMode` types and the `normalizeAccountResolution()`, `isAccountResolutionMode()`, `refHasNaturalKey()` helpers. An unrecognized `resolution` value is now a `PlatformConfigError` rather than silently inheriting another mode's enforcement. No alias spelling is introduced — `'derived'` stays the single name.

Migration: [13 → 14 § derived account resolution](https://github.com/adcontextprotocol/adcp-client/blob/main/docs/migration-13-to-14.md#derived-account-resolution-is-now-an-upstream-managed-account-id-namespace). Fixes #1647 and #1628.
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,13 @@ Pick the specialisms you want to claim in `get_adcp_capabilities`. Each maps to

**Adding wire-version compat (e.g. SDK pin moves to v4, or a new legacy seller version)?** See [`docs/development/WIRE-VERSION-COMPAT.md`](./docs/development/WIRE-VERSION-COMPAT.md) — playbook for the schema cache, codegen, adapter registry, validation pinning, conformance fixtures, and smoke harness. Reference whenever you touch `src/lib/adapters/legacy/`, `src/lib/types/v*-*/`, or `schemas/cache/<version>/`.

**Migrating from 13 to 14 (prerelease)?** See [`docs/migration-13-to-14.md`](./docs/migration-13-to-14.md). One account-model breaking change to audit: **`accounts.resolution: 'derived'` is now an upstream-managed account-id namespace** (adcp-client#1647, upstream adcp#5062). It accepts `{ account_id }` and refuses the `{ brand, operator }` arm — the exact inverse of the 6.7-era behavior described further down this list. `accounts.list` (or `opts.accounts.listAccounts`) is required for the mode, `accounts.resolve` must verify the buyer-supplied id against what the caller's credential can reach (the framework fails a mismatch closed), `sync_accounts` natural-key provisioning entries fail per-row with `UNSUPPORTED_PROVISIONING` while `account: { account_id }` settings-update entries pass through after a reachability check, and a declared `'derived'` projects `account.require_operator_auth: true`. `createDerivedAccountStore` does the verification, the paging, and the `list_accounts` wiring for you — `toAccount` for one account per credential, `listAccounts` (+ optional `lookupAccount`) for a roster.

**Migrating from 7.9 to 7.10?** See [`docs/migration-7.9-to-7.10.md`](./docs/migration-7.9-to-7.10.md) — **pure additive walkthrough; no breaking recipes.** Headlines: `AgentClient.getProducts()` auto-augments responses with V2 `format_options[]` (read-side mental model), `packageRefsForCapabilities(product, [capId])` returns `{capability_ids, format_ids?}` to spread into a `PackageRequest` (write-side, dual emission per adcp#4844 in 3.1.0-beta.2), `fetchFormatSchema` + `resolveSchemaRefs` for the `format_schema` URI+digest contract (HTTPS+SSRF+digest+bounded `$ref`), and `extractPublisherFormats` / `scopePublisherFormats` / `resolveCapabilityId` on top of `validateAdAgents()` for the `adagents.json#/formats` AAO 3.1 catalog. V1-only adopters do nothing — `format_ids[]` preserved on every product, `legacy*` helpers (semantic narrowing, not deprecation) cover the single-target v1 write path. Opt-in 3.1.0-beta.2 schemas via `adcpVersion: '3.1-beta'` / `'3.1.0-beta.2'`; primary pin stays at the GA `ADCP_VERSION`. `DEFAULT_MIRROR_HOSTS` collapsed to `creative.adcontextprotocol.org` per adcp#4866 (legacy host was never provisioned).

**Migrating from 6.7 to 6.9?** See [`docs/migration-6.7-to-6.9.md`](./docs/migration-6.7-to-6.9.md) — **skips past 6.8.0** (deprecated same-day; 6.7 → 6.9 is the supported path). Two breaking recipes to audit before bumping: **#1** the framework now auto-wires the sandbox-authority gate inside `createAdcpServerFromPlatform` for `comply_test_controller` (Phase 2 of #1435 — resolved-account `mode` is the trust boundary, not buyer-supplied `account.sandbox`), and **#2** `npm run compliance:skill-matrix` is removed in favour of `compliance:fork-matrix` (~10 s deterministic vs ~50 min LLM-variance). Thirteen additive recipes covering `Account.mode` helpers (with `Account.sandbox` server-side `@deprecated`), `createAdcpServer.instructions` async, `BuyerAgentRegistry.extra` forwarding, `composeMethod` variadic, `ComplyControllerConfig.force` extension + `queryUpstreamTraffic` adapter, `createDerivedAccountStore` Shape D, `RosterAccountStoreOptions.resolveWithoutRef`, `ConformanceClient` Socket Mode, AdCP 3.0.6 schema bump + `tasks/get` slash registration + `AgentClient.executor`, storyboard runner symmetric account resolution, codegen `asset_type` discriminator on `Individual*Asset` slots, skill prose collapse + `skills/cross-cutting.md`, five new worked references / mock-servers. Self-grade checklist with grep-based audits.

**Migrating from 6.6 to 6.7?** See [`docs/migration-6.6-to-6.7.md`](./docs/migration-6.6-to-6.7.md) — seventeen adopter recipes plus two breaking changes to audit before bumping: **#10** `accounts.resolution: 'implicit'` now actually refuses inline `{account_id}` references (was aspirational pre-6.7), **#10b** same refusal extended to `'derived'` mode (recipe backfilled in 6.9 via #1492 — runtime shipped in 6.7 via #1475), and **#11** `SalesPlatform` split into `SalesCorePlatform & SalesIngestionPlatform` (all methods individually optional, self-announcing under `tsc --noEmit`). The other fifteen recipes are additive: `definePlatform` family, `refAccountId`, `composeMethod`, `accounts.resolve` security presets, `accounts.upsert/list/syncGovernance(ctx)`, typed errors, `issues[].hint`, `BuyerAgentRegistry`, `createTenantRegistry`, `createOAuthPassthroughResolver` (Shape B), `createRosterAccountStore` (Shape C), `createDerivedAccountStore` (Shape D — single-tenant `'derived'`), `createTenantStore` (multi-tenant w/ built-in security gate), `MEDIA_BUY_TRANSITIONS` + `assertMediaBuyTransition` lifecycle helpers, `createMediaBuyStore` for `targeting_overlay` echo. Worked diff + self-grade checklist included.
**Migrating from 6.6 to 6.7?** See [`docs/migration-6.6-to-6.7.md`](./docs/migration-6.6-to-6.7.md) — seventeen adopter recipes plus two breaking changes to audit before bumping: **#10** `accounts.resolution: 'implicit'` now actually refuses inline `{account_id}` references (was aspirational pre-6.7), **#10b** same refusal extended to `'derived'` mode (recipe backfilled in 6.9 via #1492 — runtime shipped in 6.7 via #1475; **reversed in SDK 14 by #1647**, where `'derived'` accepts `account_id` and refuses the natural key instead), and **#11** `SalesPlatform` split into `SalesCorePlatform & SalesIngestionPlatform` (all methods individually optional, self-announcing under `tsc --noEmit`). The other fifteen recipes are additive: `definePlatform` family, `refAccountId`, `composeMethod`, `accounts.resolve` security presets, `accounts.upsert/list/syncGovernance(ctx)`, typed errors, `issues[].hint`, `BuyerAgentRegistry`, `createTenantRegistry`, `createOAuthPassthroughResolver` (Shape B), `createRosterAccountStore` (Shape C), `createDerivedAccountStore` (Shape D — `'derived'`; reworked in SDK 14 from single-tenant to upstream-managed account-id namespace), `createTenantStore` (multi-tenant w/ built-in security gate), `MEDIA_BUY_TRANSITIONS` + `assertMediaBuyTransition` lifecycle helpers, `createMediaBuyStore` for `targeting_overlay` echo. Worked diff + self-grade checklist included.

**Migrating from 4.x?** See [`docs/migration-4.x-to-5.x.md`](./docs/migration-4.x-to-5.x.md) for the full 4.x → 5.x path. Covers framework shape (5.0's `TaskResult` discriminated union + `createAdcpServer`), exports cleanup (5.1's `platform_type` removal + storyboard-tarball move), AdCP 3.0 GA alignment (5.2's `authority_level` → `human_review_required`, `inventory-lists` → `property-lists`, `idempotency_key` requirement, `serve({ authenticate })` surface), downstream ergonomics (5.3–5.4 `AdcpServer` return type + `dispatchTestRequest`), signed-requests composition (5.5–5.6 `requireSignatureWhenPresent` + `capabilities.overrides`), conformance runner wiring (5.7 `createExpressAdapter`), conformance defaults (5.8 `createComplyController`), A2A session continuity + typed errors (5.9), OAuth client credentials + strict validation defaults (5.10), and the pin to AdCP 3.0.0 GA (5.13: `ADCP_VERSION` switched from `latest` to the published `3.0.0` release; `validate_property_delivery` response is now wired to its generated schema). Includes a wire-interop matrix for mixed-version (beta.3 / rc.2 / GA) traffic.

Expand Down
15 changes: 9 additions & 6 deletions docs/guides/BUILD-AN-AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,11 @@ import { definePlatform, refAccountId, AccountNotFoundError } from '@adcp/sdk/se
const platform = definePlatform({
capabilities: { specialisms: ['sales-non-guaranteed'] as const, /* ... */ },
accounts: {
// 'explicit' (default), 'implicit' (sync_accounts-first), or 'derived' (single-tenant).
// 'implicit' adopters: framework refuses inline {account_id} references with INVALID_REQUEST
// *before* reaching your resolver (post-6.7).
// 'explicit' (default), 'implicit' (sync_accounts-first), or 'derived'
// (upstream-managed account-id namespace, discovered via list_accounts).
// The framework enforces each mode's durable reference shape *before*
// reaching your resolver: 'implicit' refuses inline {account_id};
// 'derived' refuses the {brand, operator} natural key.
resolution: 'explicit',
resolve: async (ref, ctx) => {
const id = refAccountId(ref);
Expand All @@ -463,7 +465,7 @@ Three resolution modes:

- **`'explicit'`** (default) — buyer passes `{account_id}` inline on every request. Snap, Meta, GAM-style sellers. The framework calls `resolve(ref, ctx)` with the inline ref.
- **`'implicit'`** — buyer must call `sync_accounts` first; subsequent requests resolve from the auth-principal linkage your `upsert` populated. LinkedIn-shaped sellers. The framework refuses inline `{account_id}` references with `INVALID_REQUEST` (post-6.7 — pre-6.7 the docstring claimed this but nothing checked it). Use [`InMemoryImplicitAccountStore`](https://github.com/adcontextprotocol/adcp-client/blob/main/src/lib/adapters/implicit-account-store.ts) for the reference shape.
- **`'derived'`** — single-tenant agents where the auth principal alone identifies the tenant. Self-hosted broadcasters, retail-media operators in proxy mode. `resolve(undefined, ctx)` returns the singleton.
- **`'derived'`** — an upstream-managed account-id namespace: you front a platform that owns the roster (Meta / Snap ad accounts, AudioStack workspaces, a retail-media proxy), or the credential is bound to a single account. Buyers discover ids via `list_accounts` and pass `{account_id}`; the framework refuses the `{brand, operator}` arm, `accounts.list` is required, and `resolve` must verify the buyer-supplied id against what the caller's credential can reach. Use [`createDerivedAccountStore`](https://github.com/adcontextprotocol/adcp-client/blob/main/src/lib/adapters/derived-account-store.ts), which does both. `resolve(undefined, ctx)` auto-selects when exactly one account is reachable. **Changed in SDK 14** — see the [migration guide](../migration-13-to-14.md#derived-account-resolution-is-now-an-upstream-managed-account-id-namespace).

**Stateless BYOK provider adapters.** For single-account API-key or
bearer-token BYOK, the provider credential can be the AdCP request credential
Expand All @@ -474,8 +476,9 @@ derives the account from request auth, and uses the same request-local token
for upstream provider calls. No SDK-managed OAuth flow, refresh-token store,
provider-token store, or callback route is required when the caller owns the
provider credential lifecycle. If the provider credential can see multiple
upstream accounts, use an explicit account roster pattern such as
`createOAuthPassthroughResolver` instead of `'derived'`. Handlers with a
upstream accounts, stay in `'derived'` and supply `listAccounts` so buyers
can discover and name one, or use `createOAuthPassthroughResolver` under
`'explicit'` when you own the id namespace. Handlers with a
resolved account should read the active token from
`ctx.account.authInfo?.token`; refresh hooks update `account.authInfo`.
Handlers without a resolved account can read the request token from
Expand Down
Loading
Loading