feat(registry): measurement-vendor discovery on /api/registry/agents (#3613)#3726
Merged
Conversation
…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>
This was referenced May 1, 2026
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
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.measurementblock and stores it in the existingagent_capabilities_snapshottable (newmeasurement_capabilities_jsonJSONB column). The public/api/registry/agentsendpoint gains three filters:metric_id=attention_unitsmetrics[].metric_idaccreditation=MRCverified_by_aaoalwaysfalse— vendor-asserted, AAO does not independently verifyq=attentionmetric_idAll three imply
type=measurementwhen present; explicit non-measurement type returns 400. When?capabilities=true, the response folds incapabilities.measurement_capabilitiesnext to the existingcreative_capabilitiesandsignals_capabilities.Design choices
Three expert reviews shaped the shape:
accreditation=as a first-class filter (was originally onlymetric_id+q). Kept fuzzyqas secondary filter since planner agents handle synonym retries when the closed taxonomy was rejected. Stayed on/api/registry/agentsrather than splitting/measurement-vendors— every type would otherwise need its own endpoint.type=measurementwhen filters are present, 400 only on explicit conflict.sourcescounts align with filtered set (sum(sources) === count). Flat sibling undercapabilities.*(don't rename toprotocol_capabilities— backward compat + agent buildability).ESCAPE '\\'ILIKE pattern (mirrorscatalog-db.ts, not thebrand-db.tsvariant 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).CapabilityDiscovery.cache(security L2).innerHTMLaudit (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_aaoalways falsetsc --noEmitclean?metric_id=and?accreditation=against staging crawl data?capabilities=trueresponse shape in OpenAPI docs preview