Skip to content

docs(specs): RFC for the SDK fact-contribution surface#5782

Open
bokelley wants to merge 1 commit into
mainfrom
docs/sdk-fact-contribution-rfc
Open

docs(specs): RFC for the SDK fact-contribution surface#5782
bokelley wants to merge 1 commit into
mainfrom
docs/sdk-fact-contribution-rfc

Conversation

@bokelley

@bokelley bokelley commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

How participants send us facts — the client-side counterpart to the catalog-graduation RFC. Follows the property-alignment / bind-on-verify work (#5750, #5752, #5761).

The gap

The server already exposes the whole fact surface — POST /api/registry/resolve (identifiers + provenance → property_rids), POST /api/registry/catalog/disputes, and the claim/verify-origin bind-on-verify endpoints. But @adcp/sdk's RegistryClient surfaces only saveProperty. This RFC defines the client surface, identical in TS (@adcp/client) and Python (adcp).

Key idea: resolving is contributing

A buyer resolving its campaign list to get property_rids for targeting is simultaneously asserting "these identifiers exist and are demanded, from agency_allowlist." Contribution is a byproduct of the caller's own workflow; provenance is the trust envelope. The footgun — the write path looks like a lookup — is closed by naming the method reportIdentifiers() (not resolve) and making mode a required argument.

Surface (flat, for TS/Python parity)

  • Community claims: reportIdentifiers(), disputeFact() / getDispute()
  • Consume: browseCatalog() (cursor), syncCatalog(since) (timestamp)
  • Origin authority: claimDomain() / verifyOrigin(), saveProperty() (exists)

Principles: provenance structurally required on writes; the write is never implicit; property_rid documented as a non-authorization handle; spec-once + a shared conformance vector (given the TS/Python divergence in adcontextprotocol/adcp-client#2301).

Review

Reviewed by the agentic-product-architect against the live endpoints. It caught a fabricated 429/tiering contract (/resolve has no limiter), a dropped required summary.not_found, and the wrong dispute response shape ({dispute_id, action_taken, reason}) — all corrected. Error contract now matches the routes (401 all writes, 403 claimDomain org membership, 429 only on claim/verify/save, verifyOrigin transient retryable).

Proposal for WG/SDK-team discussion — no wire/schema change. Implementation lands in the separate adcp-client / adcp-client-python repos.

🤖 Generated with Claude Code

The server already exposes the full "send us facts" surface (POST
/api/registry/resolve with provenance, /catalog/disputes, and the #5752
claim/verify-origin), but @adcp/sdk's RegistryClient surfaces only saveProperty.
This RFC defines the client surface — the same shape in @adcp/client (TS) and
adcp (Python) — so participants contribute facts as a byproduct of resolving
their own identifier lists.

- reportIdentifiers() (wraps /resolve) as the primary funnel: resolving IS
  contributing; mode is required and the method is named for the write so a
  lookup-looking call can't silently create catalog rows; provenance required
  in resolve mode; property_rid documented as a non-authorization handle.
- disputeFact() / getDispute(), browseCatalog() / syncCatalog() (two distinct
  pagination cursors), claimDomain() / verifyOrigin() (origin-rooted authority),
  saveProperty() (exists, identity-only).
- Flat surface (no registry.facts.* sub-client) for clean TS/Python parity;
  trust boundary named in methods + docs. Spec once + shared conformance vector,
  given the adcp-client#2301 resolver divergence.
- Error contract grounded in the live routes (401 all writes, 403 claimDomain
  org membership, 429 only on claim/verify/save — /resolve has no limiter,
  verifyOrigin transient is the retryable case).

Reviewed by the agentic-product-architect against the live endpoints; it caught
a fabricated 429/tiering contract, a dropped summary.not_found field, and the
wrong dispute response shape — all corrected. Client-side counterpart to the
catalog-graduation RFC; no wire/schema change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@aao-release-bot aao-release-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Draft RFC for the client-side fact-contribution surface — no wire or schema change, so this ships on the strength of whether it describes the live routes honestly. It does.

Things I checked

  • /resolve auth claim is accurate. catalog-api.ts:169 gates auth on mode === 'resolve' && !req.user → 401; lookup needs no auth. The RFC's 401-table matches the route, not the wish.
  • "/resolve has no rate-limiter today" is correct. The /resolve handler mounts no saveMiddleware; the claim/verify/save family does. The RFC explicitly tells both SDKs not to build a 429/tiering contract against /resolve — the right call, because the earlier draft's fabricated 429 was the exact bug the agentic-product-architect pass caught.
  • browseCatalog shape matches. GET /catalog returns { entries, total, next_cursor } (catalog-api.ts:220-224) — the opaque-cursor mechanism, distinct from syncCatalog's timestamp watermark. The RFC keeps them as two separate pagination surfaces instead of conflating them.
  • Referenced specs resolve. property-catalog-graduation.md, property-definition-alignment.md, registry-authorization-model.md all exist under specs/. No dangling links.
  • The core design move is the right one. Naming the write reportIdentifiers() instead of resolve(), making mode required, and structurally requiring provenance in resolve mode closes the footgun the RFC names itself: the write path looks like a lookup. Fail-closed beats fail-open on a method that silently creates catalog rows and demand signal.

Follow-ups (non-blocking — file as issues)

  • The RFC leans hard on a shared conformance vector asserting the full wire shape (summary.not_found, dispute action_taken, the verifyOrigin reason union) to prevent the adcp-client#2301 resolver drift from freezing into both SDKs. That vector is the load-bearing artifact here and it lives in another repo. Track it as a hard dependency of the implementation PRs, not a nice-to-have — a spec-once that ships twice without the vector is just two drifts with a shared parent.
  • resolve() as a "documented alias" for reportIdentifiers() reopens the footgun by name. If it ships, it should carry the same required mode/provenance discriminated union — worth stating in the implementation spec so the alias can't become the soft-write escape hatch.

Minor nits (non-blocking)

  1. crawl provenance exclusion. The RFC notes crawl is server-internal and not offered to callers. Good — but the SDK's local provenance.type validation (the 400-preempt list) should enumerate the seven allowed values and reject crawl explicitly, so a caller copying a server-side value gets a clean local error instead of a round-trip.

Grounded in the actual routes, and honest about what's built versus what's still the SDK team's work. Approving on the strength of the endpoint-accuracy pass plus the closed write-path footgun.

LGTM. Follow-ups noted below.

@mintlify

mintlify Bot commented Jul 1, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
adcp 🟢 Ready View Preview Jul 1, 2026, 8:31 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

bokelley added a commit that referenced this pull request Jul 1, 2026
…nts (#5787)

The catalog fact surface (POST /api/registry/resolve, /catalog/disputes) is live
and serves real traffic, but catalog-api.ts registered zero OpenAPI paths — the
whole router was absent from static/openapi/registry.yaml. Since SDK clients
generate their types from the published OpenAPI (adcp-client's types.generated.ts
sources https://agenticadvertising.org/openapi/registry.yaml), the "send us facts"
surface could not be typed. This makes it part of the contract.

- New server/src/schemas/catalog-openapi.ts (mirrors the member-agents-openapi.ts
  pattern — standalone so the generator imports it without route-factory deps).
  Registers: resolveIdentifiers (POST /api/registry/resolve), fileCatalogDispute
  (POST /api/registry/catalog/disputes), getCatalogDispute (GET .../disputes/{id}).
- Schemas mirror the Zod validators + result types in catalog-api.ts /
  catalog-db.ts / catalog-governance.ts exactly: the provenance enum, the
  identifiers[1..10000] cap, ResolveResponse.summary's five fields (incl
  not_found), and the dispute TriageResult ({dispute_id, action_taken, reason}).
- property_rid is documented as a non-authoritative join/match handle, never an
  authorization credential (mirrors the #5750 identity-not-authorization lesson).
- Wired into scripts/generate-openapi.ts + a "Property Catalog" tag;
  regenerated static/openapi/registry.yaml.

Prerequisite for the SDK fact-contribution surface (specs/sdk-fact-contribution.md,
#5782): once this publishes, @adcp/client and adcp regenerate types and add the
reportIdentifiers()/disputeFact() methods. Read-side (browse/sync) OpenAPI is a
fast follow-on. No behavior change — documentation of live endpoints only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Jul 1, 2026
…5790)

Completes the property-catalog OpenAPI surface started in #5787. The catalog
read/consume endpoints are live but were absent from static/openapi/registry.yaml,
so the SDK's read-side (browseCatalog/syncCatalog — the second half of the
resolve→sync→build-lists loop from RFC #5782) couldn't be typed.

- browseCatalog — GET /api/registry/catalog: filtered, cursor-paginated browse;
  each entry carries the property's identifiers.
- syncCatalog — GET /api/registry/catalog/sync: delta since a `server_timestamp`
  watermark (capped 10,000/page), distinct from browse's opaque cursor.

Schemas mirror the route handlers + catalog-db result types (browse entries
carry identifiers[]; sync entries are raw CatalogProperty with timestamps).
property_rid documented as a non-authoritative handle, consistent with #5787.

Once published, adcp-client regenerates types and adds browseCatalog()/
syncCatalog() alongside the shipped resolveIdentifiers/fileCatalogDispute/
claim/verify surface (@adcp/sdk 9.7.0). No behavior change — documentation of
live endpoints only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant