feat(catalog-sync): v3.1 catalog sync cluster (#4761, #4762, #4763)#4767
Merged
Conversation
Three companion v3.1 proposals for catalog mirroring between AdCP agents and consumers. Independent and complementary — agents MAY adopt any subset. #4762 — get_signals wholesale mode - discovery_mode enum on get-signals-request (brief default | wholesale) - oneOf gate so wholesale bans signal_spec/signal_ids and brief still requires one (replaces existing anyOf) - incomplete[] on get-signals-response (scopes: signals, pricing, catalog) - signals.discovery_modes capability declaration - Docs: wholesale section with auth/provenance, pricing scope, capability probe, refinement table addition #4761 — catalog_version conditional fetch (ETag-style) - if_catalog_version + if_pricing_version on get-products-request and get-signals-request - catalog_version, pricing_version, unchanged on both responses - oneOf preserves the required-payload contract while allowing unchanged: true responses to omit products[] / signals[] - Docs: Catalog versioning section in both task references with unchanged example and pagination-interaction rule #4763 — Per-agent catalog change feed - specs/catalog-change-feed.md modeled on specs/registry-change-feed.md: UUID-v7 cursor feed, denormalized payloads, catalog.bulk_change fast-forward, optional webhook subscriptions - catalog_change_feed top-level capability stanza (supported, retention_window_days >=7, webhooks_supported, event_types[]) Additive across the board — no breaking changes; safe in a minor release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's audit-oneof check flags new undiscriminated oneOf entries (#3917). The three new conditional shapes added in this PR express the same constraints without oneOf: - get-signals-request: discovery_mode=wholesale bans signal_spec/signal_ids; brief mode requires one of them - get-products-response, get-signals-response: when unchanged=true, the catalog payload is omitted and catalog_version is required; otherwise the payload is required Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. if_pricing_version requires if_catalog_version (schema-level via dependencies block + explicit evaluation order in docs and field descriptions). Pricing has no structural baseline of its own. 2. Port the three load-bearing security sections from the registry feed spec into specs/catalog-change-feed.md: advisory event payloads, re-fetch coalescing, feed-event content signing on the 4.0 track. 4. Replace the soft >100-entities example for catalog.bulk_change with a SHOULD: emit when an operation affects >5% of catalog OR >100 entities, whichever is smaller. Prevents flood-attack via deliberately-high threshold on small agents. 5. Document the X-AdCP-Catalog-* vs X-Registry-* header asymmetry — the catalog feed lives on each agent's origin (shares HTTP space with other AdCP surfaces), the registry feed lives on the central registry origin. Subscribers dispatch on header namespace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses concerns raised by ad-tech-protocol-expert, security-reviewer, adtech-product-expert, and docs-expert on the catalog-sync cluster (#4761, #4762, #4763). Cache layering (the big addition) Adds cache_scope: "public" | "account" to get_products and get_signals responses. REQUIRED when the request includes `account`; structurally "public" otherwise. catalog_version and pricing_version tokens are now explicitly scope-keyed — buyers cache (cache_scope, version) pairs and present the matching token on the next request. Sellers MAY downgrade an account from "account" back to "public" by returning the public version on a previously-account-scoped tuple. The change feed declares applies_to.scope on *.priced and *.updated events to invalidate the right cache layer. Removes the silent-stale-mirror failure mode in the prior shape, where a public-layer mirror over N accounts would either miscache wrong prices or refetch per-account on every event. Most accounts at most sellers land in "public" and share one cache entry. New `Cache layering` doc sections in both task references and full event side at specs/catalog-change-feed.md §"Cache layering and event scoping". Security blockers - Per-caller scope filtering MUST apply at event-emission time, not just authentication (closes multi-tenant leak between brand A and brand B on a shared agent) - Anti-replay: webhook envelope now signs X-AdCP-Catalog-Timestamp + X-AdCP-Catalog-Delivery-Seq into the HMAC; receivers reject skew >5min and out-of-order deliveries - SSRF guards on POST /catalog/subscriptions: HTTPS-only, DNS rebinding defense at registration AND each delivery, metadata-service IPs blocked, delivery-target challenge SHOULD - Subscription CRUD scoped to creator principal (GET/DELETE/PATCH); per- principal cap (suggest 10); secret rotation via PATCH with overlap - Jittered coalescing 60-300s to prevent catalog.bulk_change thundering- herd amplification - effective_at in the future is a pre-announce; consumers MAY warm cache but MUST NOT bind, MUST re-verify post-effective, seller MAY retract - Honest posture text: re-verify defends transport tamper, NOT operator compromise (the agent re-confirms its own lie); operator-compromise defense lives in signed create_media_buy and the R-1 4.0 track Protocol blockers - RETENTION_EXPIRED added to enums/error-code.json with enumDescriptions + enumMetadata per the dual-surface convention from #3738 - media_buy.buying_modes capability added symmetric with new signals.discovery_modes (closes the asymmetry buyers couldn't probe against) - unchanged: false explicitly documented as permitted no-op-affirmation but MUST carry the payload; absent unchanged is equivalent to false - catalog_version MUST be returned on every paginated page (not only the first) when feed is declared; SHOULD otherwise. Buyer MUST restart pagination on mid-stream version change. Protocol tighten - *.removed events carry optional removal_reason (withdrawn | cancellation | depublication | policy_takedown); downstream UX differs - filters canonicalization rule: keys sorted lexicographically, defaults treated identically, set-semantic arrays sorted. Closes silent stale- mirror bugs from buyer SDKs hashing equivalent filters differently. Docs - Fixed orphaned "This tells buyers..." prose that my prior change moved away from extensions_supported - Fixed "Returning 312 products" example that paired with products: [] - Discoverability note in get_adcp_capabilities pointing to the if_catalog_version tokens on the task pages - catalog-changed example mirrored into get_signals.mdx; pagination + canonicalization rules unified across both task pages Refs review threads from four expert agents. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
Auto-merge handled get-products-response.json (main added `extensions` property, this branch added cache_scope/catalog_version/etc — disjoint). Manual resolution in static/schemas/source/enums/error-code.json: keep my RETENTION_EXPIRED alongside main's seven new FORMAT_*/PIXEL_* codes in the enum list, enumDescriptions, and enumMetadata blocks. Validation green: schemas, json-schema, oneof-discriminators, examples, composed, error-codes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EmmaLouise2018
previously approved these changes
May 19, 2026
This was referenced May 19, 2026
Closes the pre-flight discoverability gap on conditional fetch — agents that support catalog_version probes but not the full change feed had no capability declaration, so buyers building a session across N agents couldn't fast-path which agents to bother caching versions for. New top-level catalog_versioning capability: - supported (required boolean) — whether catalog_version is returned and if_catalog_version is honored - pricing_version_separate (boolean) — whether pricing_version is tracked independently. When false, sending if_pricing_version is wasted bytes. - cache_scope_account (boolean) — whether the agent publishes per-account overlays. When false, all responses are cache_scope: public regardless of whether account was provided. Symmetric with catalog_change_feed — independent stanzas, agents MAY declare either, both, or neither. Refs #4767, follow-ups #4787 (storyboards), #4788 (training agent), #4789 (SDK). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The error-code-drift lint requires every code present in source but missing from origin/3.0.x to carry a disposition entry. RETENTION_EXPIRED is new in 3.1 (catalog change-feed, #4763) and was added to the enum without the matching disposition row. CI caught it on the prior push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Protocol: - Canonicalize signals scoping tuple between request and response (was (agent, account, filters, discovery_mode) on request vs the correct (agent, discovery_mode, filters, destinations, countries [+account_id]) on response — implementers reading request side would silently miscache when destinations/countries varied) - Mirror the products-request scoping description to match the response- side scope-keyed model - Add `expired` to removal_reason enum (flight-end / seasonal retirement) - Clarify policy_takedown covers regulator/legal takedowns too - Specify unchanged: true behavior mid-pagination (whole-catalog-vs-cached, not per-page) - Add consumer-precedence note in capabilities (feed > versioning > poll) - Clarify *.created MUST carry applies_to: account when introducing an account-only entity (preventing accidental public-cache leak) Security: - Independent per-(subscription_id, event_id) jitter — closes the timing- oracle on withheld account_ids (deterministic seed would let collusive observers triangulate the affected set) - Explicit TLS floor: TLS 1.2+ (1.3 RECOMMENDED), cert chain validation against system trust store, SNI match, reject self-signed/expired - Bind challenge-flow POST to the same URL constraints (challenge is not a carve-out from anti-SSRF rules) - delivery_seq MUST NOT reset on secret rotation (closes anti-replay gap during overlap window) - Retraction effective_at semantics: MUST be <= original effective_at; retraction identified by latest-event-for-entity, not payload match - Note that cache_scope_account: true is a small market-posture signal; agents preferring confidentiality MAY omit and detect-on-call Docs: - get_signals.mdx now documents cache_scope, catalog_version, pricing_version, unchanged in a Response Metadata table parallel to get_products.mdx (closes the symmetry break the docs reviewer flagged) - Fix intra-doc anchor: #catalog_versioning → #catalog-versioning (Mintlify slugifies underscores) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds discriminated event-payload schemas so /catalog/events response is schema-validated, not just markdown-described. Closes the conformance gap the protocol reviewer flagged on the second-pass review (#4767). - static/schemas/source/core/catalog-event.json — discriminated on event_type with 9 oneOf branches: product.{created,updated,priced, removed}, signal.{created,updated,priced,removed}, catalog.bulk_change. Each variant declares required+const event_type so the audit-oneof discriminator check passes. - static/schemas/source/core/catalog-events-response.json — feed-poll response wrapper (events[], next_cursor, has_more, retention_window_days). - Reusable $defs: - appliesTo — discriminated on scope (public | account); account variant carries optional account_ids - removalReason — enum: withdrawn | cancellation | expired | depublication | policy_takedown - specs/catalog-change-feed.md cites both schemas from the GET /catalog/events section as the conformance contract. The registry feed has the same gap (markdown-only event payloads); the parallel registry-event schema work tracked in #4792. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TypeScript Build CI step regenerates static/openapi/registry.yaml from Zod source and fails on any git diff. PR #4771 added 685 lines of brand-registry endpoint documentation directly to registry.yaml because those routes (brand.json, brand-logos, brand-ownership, brand-wiki) are docs-only — the Express routes exist but were never given Zod schemas. That hand-edit fails the freshness lint on every PR, and main itself is currently red on the same step. Two ways to fix: (a) write Zod schemas for the 7 brand-registry paths (real engineering, ~9 operations with multipart upload + full error matrices), or (b) make the generator merge-preserve hand-authored content. This PR takes (b) — generator now reads the on-disk yaml and unions its tracked output with anything already there. Paths, component schemas, and tag descriptors are all preserved when the generator doesn't own them; generator output wins on conflicts so Zod-backed paths remain authoritative. Brand Logos and Brand Wiki tag descriptions moved into TAG_DESCRIPTIONS so they emit in their documented position (between Brand Resolution and Property Resolution) rather than appended at the tag list's tail. registry.yaml carries a 2-line whitespace normalization from YAML re- serialization — quoted strings collapse to unquoted where YAML permits, semantically identical. After this PR: build:openapi && git diff --exit-code is clean on main and the lint stops blocking PRs that haven't touched openapi at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeQL flagged scripts/generate-openapi.ts:128 (existsSync followed by readFileSync). The file could in principle change between the check and the read. Trivial in this generator's actual environment, but the fix is a single-syscall pattern: readFileSync directly, treat ENOENT as "no prior yaml," rethrow anything else. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge-feeds # Conflicts: # static/openapi/registry.yaml
Three quiet-failure paths the spec prose acknowledged but schemas didn't enforce, plus the minor copy items. 1. cache_scope now REQUIRED on every response (was: required-when-account in the prose only). Schema couldn't see the request, so the safer formulation is unconditional: every response declares its cache layer. When the request had no account, MUST be 'public'. When the request had account, seller declares 'public' (this account prices off the public rate card — buyer dedupes) or 'account' (custom overrides). Without this, a seller silently omitting cache_scope on an account- scoped response would cause buyers to mis-key the cache and serve account-overlay payloads to other accounts — the canonical safety invariant of the entire two-layer model. Both products and signals if/then/else `else` and `then` branches now require cache_scope. 2. incomplete[].scope on get-products-response gains 'catalog' for parity with signals — sellers in buying_mode: 'wholesale' that can't enumerate the full catalog in time_budget previously had no precise scope to declare. 3. applies_to now REQUIRED on *.created events (was: optional with default-public). Same class of bug as #1 — a forgotten applies_to on an account-only created event leaks the new entity into every consumer's public cache. Schema-required explicit declaration prevents the quiet-failure path. Sellers MUST emit { scope: 'public' } explicitly for public-layer additions rather than rely on a default. 4. retracts_event_id added (optional) to product.priced and signal.priced. Lets consumers detect pre-announce retraction deterministically by event_id lookup rather than maintaining per-entity 'latest-event' state. When present, retraction effective_at MUST be <= retracted event's effective_at. 5. RETENTION_EXPIRED description tightened — UUID v7 carries no agent identity, so 'cursor I never issued' and 'cursor I retired' collapse to the same not-in-log lookup. Spelled out: any well-formed cursor the agent cannot locate returns this code; remediation is identical. 6. event_id description notes the format: uuid schema constraint accepts any UUID version while the spec MUSTs v7 — schema validators alone won't catch v4-vs-v7 confusion. 7. catalog.bulk_change.recommendation enum description explains the single-value-enum-not-boolean choice as forward-compat affordance for future recommendation vocabulary (cursor_advance, subscribe_to_subscope). 8. /.well-known/adcp-catalog-webhook-challenge callout: AdCP-defined, intentionally not in IANA registry, scoped to this protocol. Examples in get_signals.mdx updated to include cache_scope: 'public' (7 example blocks) since the schema now requires it. External-review thread on PR #4767. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The catalog-sync cluster (#4761 conditional fetch + #4762 wholesale signals + #4763 per-agent change feed, landed via #4767) is one of the larger additive surfaces in 3.1 and was missing from the what's-new overview. Adds: - New "you can mirror catalogs without burning bandwidth" bullet in Why upgrade - Three rows in the At a glance table (catalog mirroring, wholesale signals, per-agent change feed) - Full Headline feature section (Catalog mirroring — conditional fetch, wholesale signals, per-agent change feed) positioned between Webhook foundation and Canonical creative formats, covering the three mechanisms, cache layering, honest security posture, and capability declarations + new event-payload JSON Schemas - Three rows in the Adopter action table (mirror-maintaining consumer, signals agent, sales/signals agent serving mirrors at scale) - Description meta updated to mention catalog-sync Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ixture The bundled-schema validation fixture in composed-schema-validation.test.cjs exercised the get-products-response bundle without cache_scope. After the external-review fix making cache_scope schema-required on every response, the fixture failed the bundle's `else` branch. Local `test:composed` passed because it loads the source schema (which re-resolves $refs at runtime); CI's bundled-schema path loads the dist artifact, which is where the required: ["products", "cache_scope"] branch lives self-contained. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six spec items and one nit from the pre-merge review.
1. Version-pin cache_scope. Schema stays REQUIRED on every response (the
safety invariant of the two-layer cache depends on it), but the
description and changeset now explicitly document the validator
obligation: SDKs MUST select the validator based on the server-
declared adcp_version. For 3.0.x-declared responses, the 3.1 cache_
scope-required constraint MUST be relaxed. This is a tightening
within 3.1, not a 3.0 break — but adopter SDKs that hardcode the 3.1
schema without version-pinned validation will reject correct 3.0
traffic, so the obligation is normative.
2. next_cursor always echoes the request cursor when events[] is empty.
Removes the dual state machine (was: "absent OR echoes the request
cursor") so SDKs do unconditional `cursor = response.next_cursor`.
Only absent on the initial poll (no cursor was sent and no events
returned).
3. Drop unchanged: false. Tightened to const: true | absent — the
absence of the field IS the "response carries products" signal.
Removes the footgun where some sellers emit { unchanged: false,
products: [...] } and some emit { products: [...] } for the same
state. unchanged: false is now a schema error.
4. types= ignore-unsupported. GET /catalog/events?types=signal.priced
against a products-only agent returns an empty events[], not
INVALID_REQUEST (HTTP Accept semantics). Consumers SHOULD pre-filter
against the agent's declared event_types[] capability.
5. filters canonicalization forward-compat. New filter fields in 3.x
minor versions MUST declare set-vs-sequence semantics; absent an
explicit declaration, the rule defaults to set-semantics (sort
before hashing). Prevents silent drift between sellers and SDKs as
new filters land.
6. Webhook as go-poll signal. Receivers MUST NOT consume the webhook
body as a source of state — they MUST fetch via feed_url to apply
events. Collapses the SDK's idempotency surface from two paths to
one, prevents the footgun where webhook-arrival order != cursor
order due to coalescing jitter.
Nit (already symmetric in current state, confirmed): incomplete[].items
on both get-products-response and get-signals-response use
additionalProperties: false. No change needed.
Examples in get_products.mdx and get_signals.mdx updated to drop
`unchanged: false` lines from the catalog-changed examples (the field
is now schema-prohibited at that value).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 tasks
bokelley
added a commit
to adcontextprotocol/adcp-client
that referenced
this pull request
May 19, 2026
…g-sync cluster) (#1879) * feat(types, validation): opt-in support for AdCP 3.1.0-beta.1 (catalog-sync cluster) Adopters pin `adcpVersion: '3.1.0-beta.1'` (or '3.1-beta') to validate against the beta schemas including the catalog-sync cluster from adcontextprotocol/adcp#4767: `if_catalog_version` / `if_pricing_version` conditional fetch on `get_products` / `get_signals`, wholesale `discovery_mode` for signals, the `catalog_change_feed` capability stanza, and the new `core/catalog-event.json` + `core/catalog-events-response.json`. The SDK's primary pin stays at the GA `ADCP_VERSION` — this is a side-bundle, not a default move. The `latest` symlink in `schemas/cache/` continues to point at the GA pin. Plumbing: - scripts/sync-3-1-beta-schemas.ts wraps syncSchemas() with `latest` symlink restoration + HEAD-restore of tracked side-effect paths (`schemas/registry/registry.yaml`, protocol-managed skills) so an opt-in beta sync never bumps GA-pinned check-in surfaces. - scripts/generate-3-1-beta-types.ts emits the parallel `src/lib/types/v3-1-beta/tools.generated.ts` surface, mirroring the v2.5 codegen pipeline with two additions: intra-schema `$ref` reseating (`#/oneOf/...` → `#/definitions/<Name>/oneOf/...`) for the schemas that self-reference inside their own tree, and index-signature widening on inline anonymous objects so optional named properties don't collide with the index type (TS2411). - COMPATIBLE_ADCP_VERSIONS extended via `COMPATIBLE_PREFIX`. The major/minor gate in `buildCompatibleVersions` stays closed for primary-pin moves — opt-in betas land in the prefix list, GA bumps still require an explicit range extension. - package.json `./types/v3-1-beta` export + typesVersions entry mirror the v2.5 pattern. CatalogSync client (the consumer-facing change-feed mirror) lands in a follow-on PR. Verified: - tsc --noEmit clean (0 errors) - format:check clean - npm run build:lib clean; dist/lib/schemas-data/3.1.0-beta.1/ ships alongside 3.0 and v2.5 - 21/21 schema-loader-per-version + v2-5-types-import tests pass; 21/21 validation tests pass - End-to-end: resolveBundleKey('3.1-beta') resolves; getValidator ('get_products', 'sync', '3.1.0-beta.1') compiles; a beta-shaped response (unchanged: true, catalog_version, cache_scope: 'public') validates against the beta schema Closes adcontextprotocol/adcp#4794 (the SDK-side schema/types prerequisite — CatalogSync client lands separately). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(types, codegen): address expert convergence on PR #1879 Five fixes from the parallel DX/protocol/code/security expert reviews of the catalog-sync opt-in PR. Convergent blockers from three of the four reviewers; security findings were inherited from `sync-schemas.ts` and filed for follow-up rather than this PR. 1. Protocol-safety on CatalogChangeEvent (the most important fix). `core/catalog-event.json` declares `payload` as root-required but lists only the discriminator (`event_type`) in each branch's local `required`. `json-schema-to-typescript` emitted `payload?: BranchShape` in each union branch, so intersection with the wrapper's `payload: {}` produced `payload: {}` — load-bearing branch-specific shape lost at the type level. Added `propagateRootRequiredIntoOneOfBranches` to the codegen pipeline: for any schema with `oneOf` + root-level `required`, lift the root required fields into every branch's required array before handing to jsts. Each branch now emits `payload: BranchShape` (non-optional), the intersection narrows correctly, and discriminated-union safety survives codegen. Ajv runtime validation already enforced the spec semantics (Ajv reads the original schema with `required` intact); this fixes only the TS surface adopters see. 2. WIRE-VERSION-COMPAT.md playbook now describes both compat shapes. Added a "Two compat patterns — pick the right one" comparison table at the top and a full "Recipe: opt-in side-bundle" section covering the newer-than-pin pattern: tarball-not-SHA, latest-symlink restore, RESTORE_PATHS from HEAD, COMPATIBLE_PREFIX extension, the three preprocessing passes, and the upgrade story for beta.N to beta.N+1. 3. Changeset now recommends `'3.1-beta'` (release-precision) as the canonical pin. Full-semver `'3.1.0-beta.1'` is now a trailing sentence for bit-fidelity in cross-version interop tests. Adds the namespace-import recommendation (`import * as V31Beta`) since flat-named imports will collide with the GA surface. Includes a 6-line poll-loop snippet so adopters can use this surface today without waiting for CatalogSync. 4. Removed dead `readdirSync` import + `void readdirSync` footer from `generate-3-1-beta-types.ts`. The defensive comment was factually wrong — `loadStandaloneCoreSchemas` uses only `existsSync` + `readFileSync`. 5. Runtime-validator test added at `test/lib/schema-loader-per-version.test.js`. Exercises `getValidator('get_products', 'request', '3.1.0-beta.1')` against an `if_catalog_version`-bearing payload (must validate) AND an `if_pricing_version`-without-`if_catalog_version` payload (must reject). The second assertion confirms Ajv still enforces the spec's `dependencies` constraint at runtime even though codegen strips it for TS-surface generation. Also exercises the release-precision `'3.1-beta'` resolution path. Verified: tsc --noEmit clean, format:check clean, 19/19 schema-loader-per-version tests pass (up from 18), build clean. Deferred to separate PRs per security reviewer's classification as inherited from `sync-schemas.ts`: - Cosign fail-closed in NODE_ENV=production publish path (medium) - `assertSafeVersion` on `sync()` version parameter (low) - `path.resolve` + boundary check in `refToCachePath` defense-in-depth (low) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three companion v3.1 proposals for catalog mirroring between AdCP agents and consumers (storefronts, federated marketplaces, registries). Independent and complementary — agents MAY adopt any subset.
catalog_versionconditional fetch (ETag-style probe)get_signalswholesale discovery mode (symmetric withget_productswholesale)specs/catalog-change-feed.md)Additive across the board — new optional fields, new conditional schemas, new capability stanzas, new spec doc. No breaking changes; safe in a minor release.
#4762 —
get_signalswholesale modesignals/get-signals-request.json:discovery_modeenum (briefdefault,wholesale); replaced existinganyOfgate with aoneOfso wholesale banssignal_spec/signal_idsand brief still requires one.signals/get-signals-response.json:incomplete[]with scopessignals/pricing/catalog— partial completion signalled inline, not via async/Submitted handoff.protocol/get-adcp-capabilities-response.json:signals.discovery_modes. Agents not declaring"wholesale"MAY returnINVALID_REQUESTfor wholesale calls.#4761 —
catalog_versionconditional fetchif_catalog_version+if_pricing_versionrequest fields added to bothget-products-requestandget-signals-request.catalog_version,pricing_version,unchangedresponse fields added to both responses.oneOfso the unchanged response (withproducts/signalsomitted) is schema-valid without breaking the standard required-payload contract.catalog_versionon each page; consumers SHOULD restart fromcursor: nullon a mid-pagination version change.#4763 — Per-agent catalog change feed
specs/catalog-change-feed.mdmodeled onspecs/registry-change-feed.md: UUID-v7 cursor feed, denormalized payloads, optional webhook subscriptions.product.{created,updated,priced,removed},signal.{created,updated,priced,removed},catalog.bulk_change(fast-forward for rate-card sweeps).protocol/get-adcp-capabilities-response.json: top-levelcatalog_change_feedstanza (supported,retention_window_days≥7,webhooks_supported,event_types[]).GET /catalog/events,POST /catalog/subscriptions) live on the agent itself, not the registry. Authorization scope mirrors wholesale enumeration.How they compose
A consumer mirroring a stable catalog uses
catalog_versionalone for cheap "anything new?" probes.A consumer needing near-real-time tracking bootstraps via wholesale enumeration (
get_productsorget_signals), persists the returnedcatalog_version, then steady-states on the change feed for event-level deltas.catalog.bulk_changeis the fast-forward back to wholesale resync.Test plan
npm run test:schemas— all 7 suites pass (548 schemas)npm run test:json-schema— 270 passed, 0 failed (all$schema-tagged JSON blocks in docs)npm run test:examples— 36 passednpm run test:composed— 43 passednpm run test:pagination-invariant— 16 passednpm run test:version-envelope— passingnpm run build:schemas— cleantest-agent.adcontextprotocol.org— not caused by these changes (verified by stashed baseline)Refs #4761, #4762, #4763.
🤖 Generated with Claude Code