Skip to content

feat(server): BuyerAgentRegistry Phase 1 Stage 1 — types + factories (#1269)#1293

Merged
bokelley merged 2 commits into
mainfrom
bokelley/buyer-agent-registry-stage-1
May 2, 2026
Merged

feat(server): BuyerAgentRegistry Phase 1 Stage 1 — types + factories (#1269)#1293
bokelley merged 2 commits into
mainfrom
bokelley/buyer-agent-registry-stage-1

Conversation

@bokelley

@bokelley bokelley commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 Stage 1 of #1269 — durable buyer-agent identity surface, types and factory functions only. Pure additive, no runtime wiring; later stages thread the registry through the dispatcher.

What's in

  • BuyerAgent interface with readonly fields, set-valued billing_capabilities, optional default_account_terms, allowed_brands, aliases (rotation reservation).
  • AdcpCredential discriminated union — kind: 'api_key' | 'oauth' | 'http_sig'. The http_sig variant carries the cryptographically-verified agent_url (per spec(accounts): buyer-agent identity model + billing error-code coverage for sync_accounts adcp#3831's derivation rule); the bearer variants don't.
  • BuyerAgentStatus ('active' | 'suspended' | 'blocked') and BuyerAgentBillingMode (re-export of wire BillingParty).
  • BuyerAgentRegistry Protocol + three factory functions:
    • signingOnly({ resolveByAgentUrl }) — bearer kinds resolve to null; only http_sig routes to the resolver.
    • bearerOnly({ resolveByCredential }) — all credential kinds route through the adopter's mapping.
    • mixed({ resolveByAgentUrl, resolveByCredential })http_sig routes to the signed path, bearer kinds to the credential path; signed preferred when both present.
  • BuyerAgentResolveInput — minimal input shape for resolve(), decoupled from ResolvedAuthInfo to keep the registry surface stable across the migration cycle.

What's NOT in (later Stage)

  • Resolve seam wiring in the dispatcher → Stage 2.
  • ResolvedAuthInfo.credential field with two-minor deprecation → Stage 3.
  • Status enforcement (suspended/blocked → 403), multi-credential conflict resolution, credential redaction → Stage 4.
  • CachingBuyerAgentRegistry decorator + conformance fixtures → Stage 5.
  • Framework-level billing_capability enforcement + AdCP-3.1 error-code emission → Phase 2 (feat(server): BuyerAgentRegistry Phase 2 — wire billing-capability enforcement (AdCP 3.1) #1292), gated on SDK 3.1 cutover.

Why types-first

Three reasons to land Stage 1 alone:

  1. The shape is up for review — adopters and the Python team see the API before any wiring locks it in.
  2. Stage 2 (the dispatcher seam) is a more sensitive change; landing the types separately keeps the diffs reviewable.
  3. The Python v3-identity-bundle-design.md round-2 RFC has BuyerAgent / BuyerAgentRegistry as its spine — cross-language shape parity benefits from the JS types being concrete and merged.

Test plan

  • npm run typecheck clean.
  • test/lib/buyer-agent-registry.test.js — 19/19 pass:
    • signingOnly: routes http_sig, returns null for api_key/oauth, returns null for absent credential, throws on missing resolver, propagates resolver throws.
    • bearerOnly: routes all kinds (including http_sig), returns null for absent credential, throws on missing resolver.
    • mixed: signed path takes precedence; bearer falls through; null on absent.
    • BuyerAgent shape: readonly semantics survive serialization; Set-membership works.
  • npm run format:check clean.
  • Regression — adjacent test files (server-decisioning-from-platform, server-decisioning-to-wire-account, new file): 150/150 pass.

Cross-links

🤖 Generated with Claude Code

…nctions (#1269)

Adds the durable buyer-agent identity surface: `BuyerAgent` record, kind-
discriminated `AdcpCredential`, and `BuyerAgentRegistry` with three factory
functions (`signingOnly`, `bearerOnly`, `mixed`) that encode the implementer
posture at construction.

The credential answers "who signed?" / "who holds this token?"; the
`BuyerAgent` answers "who is this counterparty in our books?" — a durable
commercial relationship distinct from the per-request credential. Same
pattern as an SSP's `buyer_id` row keyed to a DSP regardless of credential
type.

This stage ships the types and factories only. Stage 2 wires the resolve
seam in the dispatcher; Stage 3 ships the `ResolvedAuthInfo` migration
shim; Stage 4 adds status enforcement / multi-credential conflict / log
redaction; Stage 5 adds the caching decorator and conformance fixtures.
All Phase 1 stages are no-wire-emission and ship in 3.0.x.

Phase 2 (#1292) — framework-level `billing_capability` enforcement and
the AdCP-3.1 error-code emission from adcontextprotocol/adcp#3831 — is
gated on the SDK's 3.1 cutover.

19 unit tests cover factory contracts (kind-routing, null on unhonored
kinds, construction-time validation, resolver-throw propagation),
`BuyerAgent` shape semantics, and `BillingMode` Set-membership.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_sig credentials

Three fixes from expert review on PR #1293 (no blockers, all in-scope for
Stage 1):

1. Doc-clarify that the legacy `ResolvedAuthInfo` synthesis is Stage 3's
   job, not Stage 1's. The previous comment promised behavior the code
   didn't implement.

2. `ResolveBuyerAgentByCredential` JSDoc now requires implementations to
   switch on `credential.kind` and reject unrecognized kinds. Closes the
   foot-gun where a `WHERE token = $1` lookup against an api-key table
   could mis-resolve when handed an `http_sig` credential whose `keyid`
   collides with an existing api-key value — the credential variants share
   no key namespace.

3. Runtime guard against malformed `http_sig` credentials. A misbehaving
   authenticator could produce `kind: 'http_sig'` without a non-empty
   `agent_url`; without the guard, `signingOnly` and `mixed` would
   silently pass `undefined` to the resolver. `signingOnly` now returns
   null on malformed; `mixed` rejects rather than falling through to the
   bearer path (which would make `mixed` strictly weaker than
   `signingOnly` — a malformed http_sig could otherwise bypass signed-
   path enforcement by routing through the bearer table).

Test coverage extended to 22 cases (+3): malformed-http_sig refusal in
both `signingOnly` and `mixed`; empty-agent_url refusal.

Deferred items (Stages 2, 3, Phase 2) recorded on #1269 / #1292.

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

bokelley commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

Review fixes pushed (b7ac472)

Three-expert review (ad-tech-protocol / code-reviewer / security-reviewer): no blockers. Three small fixes addressed in this commit; four deferred items recorded for later stages.

Fixed in this PR

  1. Stale doc comment. BuyerAgentResolveInput.credential? previously promised "synthesized oauth-shaped credential built from legacy clientId/scopes/token" — but that synthesis is Stage 3's job, not Stage 1's. Updated to point at Stage 3.

  2. bearerOnly foot-gun (security). Tightened ResolveBuyerAgentByCredential JSDoc: "Implementations MUST switch on credential.kind and reject (return null) on any kind they don't explicitly recognize." Prevents a WHERE token = $1 lookup against an api-key table from mis-resolving when handed an http_sig credential whose keyid happens to collide with an existing api-key value — the credential variants share no key namespace.

  3. Malformed http_sig runtime guard (security). Added isVerifiedHttpSigPayload helper rejecting kind: 'http_sig' credentials with missing/empty agent_url. Three new tests: signingOnly rejects malformed http_sig; signingOnly rejects http_sig with empty agent_url; mixed rejects malformed http_sig rather than falling through to the bearer path (otherwise mixed would be strictly weaker than signingOnly — an authenticator producing an http_sig-shaped credential without a verified agent_url could bypass signed-path enforcement).

Deferred to later stages (recorded)

  • Stage 2: Object.freeze on resolved BuyerAgent values at the dispatcher seam. Cheap defense-in-depth.
  • Stage 3: Widen VerifiedSigner (src/lib/signing/types.ts:58-62) with jwks_uri + agents_index per adcp#3831's normative agent_url derivation rule. Plumb through from brand-jwks.ts:209-211.
  • Stage 3: Verifier-as-sole-constructor for http_sig credentials via non-enumerable provenance tag (Symbol.for('adcp.verified')) — defense against custom authenticate() returning a hand-rolled { kind: 'http_sig', agent_url: 'attacker.com' }.
  • Phase 2 (feat(server): BuyerAgentRegistry Phase 2 — wire billing-capability enforcement (AdCP 3.1) #1292): suggestBilling(agent.billing_capabilities, rejected): BillingMode | undefined helper composing the registry's set against the spec's single error.details.suggested_billing.

Cross-linked in #1269 and #1292.

Validation

  • npm run typecheck clean.
  • test/lib/buyer-agent-registry.test.js — 22/22 pass (+3 new cases).
  • npm run format:check clean.
  • Regression — adjacent test files: 153/153 pass.

Confirmed non-issues from review

  • BillingParty re-export over parallel enum: correct call (matches the dominant pattern in src/lib/server/decisioning/).
  • BuyerAgent.allowed_brands AND-composing with future BrandAuthorizationResolver: legitimate; no spec conflict.
  • aliases rotation: spec is silent on rotation overlap; registry-side modeling is in-bounds.
  • bearerOnly letting http_sig through to resolveByCredential: intentional — bearerOnly is the "trust adopter mapping for any kind" posture. JSDoc tightened (above) addresses the foot-gun.
  • Type-only PR has zero log/error/metric sinks → no Stage 1 credential-leak vector.

🤖 Generated with Claude Code

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