Skip to content

feat(registry): measurement-vendor discovery on /api/registry/agents (#3613)#3726

Merged
bokelley merged 4 commits into
mainfrom
bokelley/measurement-vendor-discovery
May 1, 2026
Merged

feat(registry): measurement-vendor discovery on /api/registry/agents (#3613)#3726
bokelley merged 4 commits into
mainfrom
bokelley/measurement-vendor-discovery

Conversation

@bokelley

@bokelley bokelley commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the AAO server-side half of per-metric catalog discovery — the buyer-facing complement to the AdCP schema work that landed in #3652.

The crawler now ingests each measurement agent's get_adcp_capabilities.measurement block and stores it in the existing agent_capabilities_snapshot table (new measurement_capabilities_json JSONB column). The public /api/registry/agents endpoint gains three filters:

Param Match Notes
metric_id=attention_units Exact, repeatable JSONB containment on metrics[].metric_id
accreditation=MRC Exact, repeatable verified_by_aao always false — vendor-asserted, AAO does not independently verify
q=attention Substring on metric_id v1 scope; max 64 chars; SQL wildcards rejected outright

All three imply type=measurement when present; explicit non-measurement type returns 400. When ?capabilities=true, the response folds in capabilities.measurement_capabilities next to the existing creative_capabilities and signals_capabilities.

Design choices

Three expert reviews shaped the shape:

  • Buy-side (adtech-product-expert): added accreditation= as a first-class filter (was originally only metric_id + q). Kept fuzzy q as secondary filter since planner agents handle synonym retries when the closed taxonomy was rejected. Stayed on /api/registry/agents rather than splitting /measurement-vendors — every type would otherwise need its own endpoint.
  • DX: auto-scope to type=measurement when filters are present, 400 only on explicit conflict. sources counts align with filtered set (sum(sources) === count). Flat sibling under capabilities.* (don't rename to protocol_capabilities — backward compat + agent buildability).
  • Security: must-fix per-field caps + 256KB DB CHECK + scriptish/URI scheme rejection at write time + ESCAPE '\\' ILIKE pattern (mirrors catalog-db.ts, not the brand-db.ts variant that omits it).

What's NOT in this PR

  • q= fuzzy search across description / standard_reference / accrediting_body (v1 is metric_id only — see DX note in OpenAPI description; expand once we see real query patterns).
  • Cache-key collision audit on CapabilityDiscovery.cache (security L2).
  • Dashboard innerHTML audit (security M1 follow-up).
  • rel="noopener noreferrer" recommendation for downstream renderers (security M2).

These are tracked as follow-ups; not blockers for this PR.

Closes #3613. Closes #3614 (direct-call vs AAO-index decision matrix folded into docs/registry/index.mdx).

Test plan

  • vitest run server/tests/unit/measurement-capabilities-sanitize.test.ts — 17 sanitizer tests covering happy path, all caps, scriptish/URI rejection, control-char strip, verified_by_aao always false
  • Existing registry-agents and openapi tests still pass (32/32)
  • tsc --noEmit clean
  • Migration 461 applies cleanly on staging
  • Smoke test ?metric_id= and ?accreditation= against staging crawl data
  • Confirm ?capabilities=true response shape in OpenAPI docs preview

bokelley and others added 4 commits April 30, 2026 22:52
…3613)

Crawler ingests each measurement agent's get_adcp_capabilities.measurement
block (AdCP 3.x, schema #3652) and the public /api/registry/agents endpoint
gains three filters:

- metric_id=attention_units (exact, repeatable, JSONB containment)
- accreditation=MRC (exact, repeatable; verified_by_aao always false)
- q=attention (substring on metric_id; v1 scope only)

All three imply type=measurement; explicit non-measurement type returns 400.
Filtering at SQL level so no live fan-out per request. sources counts
recomputed against filtered set (sum(sources) === count invariant).

Crawler calls get_adcp_capabilities on agents that expose the tool, parses
the measurement block, and persists to new measurement_capabilities_json
JSONB column. 10s timeout. A measurement fetch failure does not fail the
whole discovery — other capability blocks still land normally.

Per security review:
- Per-field caps at write time (metrics ≤500, description ≤2000,
  metric_id ≤256, URI ≤2048, accreditations/metric ≤32). Reject — don't
  silently truncate — so failure is visible via discovery_error.
- Belt-and-braces 256KB DB CHECK on the column.
- Strip C0 controls + DEL (keep \t, \n).
- Reject <script / javascript: / data:text/html / inline event handlers
  after NFKC normalization.
- URI fields https-only in production.
- q rejects %/_ outright (substring search, not pattern); remaining ILIKE
  escaping uses ESCAPE '\\' (mirrors catalog-db.ts:353 pattern, not
  brand-db.ts which omits the explicit ESCAPE clause).

Closes #3613, closes #3614 (direct-call vs index doc folded into
docs/registry/index.mdx).

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

Two migrations landed on main with prefix 459 (PR #3672 09:14 UTC,
PR #3567 22:31 UTC) without renumbering. Subsequent PRs trip:

- `No duplicate migration numbers` workflow check
- `migrate.test.ts > has no duplicate migration version numbers on disk`
- `loadMigrations` validation in `Server integration tests` and
  `Built migrations against Postgres`

Renumber the later one (`459_create_type_reclassification_log` → 462)
since 460 (identities) and 461 (measurement_capabilities, this branch)
are already taken. Filename reference in
`type-reclassification-log-db.ts` updated to match.

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

# Conflicts:
#	server/src/db/migrations/459_create_type_reclassification_log.sql
#	server/src/db/migrations/461_create_type_reclassification_log.sql
#	server/src/db/migrations/462_create_type_reclassification_log.sql
…vation test

Reviewer noticed the comment claimed "\r is stripped" but the regex
explicitly preserves 0x0D — exact intent was to keep all whitespace
controls (\t, \n, \r), strip everything else in C0 plus DEL. Comment
now matches code; test asserts \r survives.

Co-Authored-By: Claude Opus 4.7 (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.

Buyer-agent direct-call vs AAO-index pattern for measurement capabilities AAO crawler + index for measurement-agent capabilities

1 participant