Skip to content

feat(registry): endorsed_by_publisher_member on discovered agents — option C from #3547#3565

Merged
bokelley merged 3 commits into
mainfrom
EmmaLouise2018/registry-publisher-endorsement
May 1, 2026
Merged

feat(registry): endorsed_by_publisher_member on discovered agents — option C from #3547#3565
bokelley merged 3 commits into
mainfrom
EmmaLouise2018/registry-publisher-endorsement

Conversation

@EmmaLouise2018

Copy link
Copy Markdown
Contributor

Closes #3547. Refs #3538 Problem 6.

Three live discovered agents (`agent.mamamia.com.au`, `gatavocom.sales-agent.setupad.ai`, `wheelrandom.sales-agent.setupad.ai`) return `member: null` even though their `discovered_from.publisher_domain` is claimed by an AAO member. The publisher's `adagents.json` endorsement carries trust signal — response builder threw it away.

This PR ships option C from #3547: option A as the new field, option B as the collapse verification.

Option A — `endorsed_by_publisher_member` field

New optional field on `/api/registry/agents` response. Populated only when:

  • agent's `source = 'discovered'`
  • `discovered_from.publisher_domain` is claimed by a `member_profiles` row whose `publishers[]` contains that domain with `is_public = true`

Mutually exclusive with `member` (registered agent never has both). Schema in `server/src/schemas/registry.ts` carries the `.openapi({ description })` so docs explain the relationship. Lookup happens in `server/src/federated-index.ts` over the same `memberDb.listProfiles({ is_public: true })` call we already make for registered agents — no extra DB round-trip.

Example:

```json
{
"agent_url": "agent.mamamia.com.au",
"source": "discovered",
"member": null,
"discovered_from": { "publisher_domain": "mamamia.com.au" },
"endorsed_by_publisher_member": {
"slug": "mamamia",
"display_name": "Mamamia",
"publisher_domain": "mamamia.com.au"
}
}
```

Agent stays `source: 'discovered'` — the agent itself didn't opt in. The endorsement is a publisher-side signal, not an agent claim.

Option B — collapse verification

(verified) registered row already wins when `agent_url` is also discovered. `server/src/federated-index.ts:73-75` skips a discovered row when `registeredAgents` already has the same `agent_url`. Inline comment expanded at the merge site documenting the existing behaviour, and the new test case `agent_url claimed by both registered + discovered collapses to registered (option B)` pins it so a future refactor can't quietly regress it.

What changed

  • `server/src/schemas/registry.ts` — new `EndorsedByPublisherMember` Zod schema with description, attached to `FederatedAgentWithDetailsSchema` as optional
  • `server/src/types.ts` — matching `endorsed_by_publisher_member` field on `FederatedAgent` interface
  • `server/src/federated-index.ts` — builds publisher-domain → member-ref index from already-loaded profiles, populates the field on each discovered row when the linkage exists
  • `server/src/routes/registry-api.ts` — passes the field through the response mapping; description prefers member name, then endorsement display_name, then bare publisher_domain
  • `static/openapi/registry.yaml` — mechanical regen via `npm run build:openapi` (separate commit)
  • `server/tests/unit/registry-discovered-endorsement.test.ts` — 6 tests covering populate / omit / is_public=false / registered-no-field / option-B collapse / authorization-overrides-source-domain

Backwards compatibility

Non-breaking on the wire. Adds an optional field. Consumers ignoring unknown fields are unaffected. Consumers that previously read `member: null` and assumed "no member relationship" now have the additional `endorsed_by_publisher_member` signal available — strictly more information, never less.

Test plan

  • `vitest run server/tests/unit/registry-discovered-endorsement.test.ts` — 6/6 pass
  • `vitest run server/tests/unit/openapi-coverage.test.ts` — still 3/3
  • `tsc --noEmit -p server/tsconfig.json` — clean
  • Pre-commit hooks green (no `--no-verify`)
  • Staging spot-check: the three known cases (mamamia, gatavo, wheelrandom) render with `endorsed_by_publisher_member` populated when their publisher_domain is owned by a member; absent otherwise

Stack ordering

Independent of #3540, #3541, #3542, #3543. Lands any time. Recommended after #3542 so the schema annotations from that PR are already in main.

Out of scope

  • UI treatment of `endorsed_by_publisher_member` — separate PR, depends on what segmentation lands in Problem 3 P1.
  • `?source=` query param + UI tabs (Problem 3 P1) — separate PR.
  • Self-attestation registration for non-members (Problem 5 option B) — needs product call.

@bokelley

Copy link
Copy Markdown
Contributor

Issue #3573 proposes agent_url canonicalization (trailing-slash strip, lowercase scheme+host, reject fragments/query strings) before the federated-index.ts:73-75 registered/discovered collapse — same surface as this PR; consider folding before merge or confirm follow-up.


Generated by Claude Code

Closes #3547 (option C from the decision issue). Refs #3538 Problem 6.

Discovered agents whose discovered_from.publisher_domain is claimed by an
AAO member (member_profiles row with publishers[] entry where
is_public=true) now surface a new optional endorsed_by_publisher_member
field on /api/registry/agents. Mutually exclusive with member —
registered agents never carry it.

Three live cases that motivated this — agent.mamamia.com.au,
gatavocom.sales-agent.setupad.ai, wheelrandom.sales-agent.setupad.ai —
returned member: null even though the publisher that endorsed them via
adagents.json is a member. The publisher-side trust signal was being
thrown away by the response builder.

Option B from #3547 (collapse to registered when agent_url is also
discovered) was already correct. server/src/federated-index.ts:73-75
skips a discovered row when registeredAgents has the same agent_url —
verified by the new agent_url claimed by both registered + discovered
collapses to registered (option B) test. Inline comment added at the
merge site documenting the existing behaviour.

OpenAPI regen lands in a separate commit per repo hygiene.
…name required + describe

Pre-review nit: slug + display_name were marked .optional() in
EndorsedByPublisherMemberSchema, but the populating code at
federated-index.ts:127-131 always sets them from non-optional
member_profiles.slug / .display_name. Optional schema was a footgun for
clients rendering display_name. Tighten to .required() and add .openapi
descriptions per the same pattern as publisher_domain.
…ber field

Mechanical regen via npm run build:openapi after rebase on main. Picks up
the EndorsedByPublisherMember schema definition + reference + the updated
member description that points to the new endorsement signal instead of
referencing #3538 Problem 6 as a placeholder.
@EmmaLouise2018 EmmaLouise2018 force-pushed the EmmaLouise2018/registry-publisher-endorsement branch from c157038 to 5a756d9 Compare April 30, 2026 04:23

@bokelley bokelley 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.

Reviewed as part of registry-stack triage session (refs #3538). Scope is tight, tests cover behavior, changesets present, openapi regen is mechanical. Approving.

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.

decision(registry): how should discovered agents surface member-publisher endorsement (#3538 Problem 6)

2 participants