Skip to content

feat(server): implement /v1/agents/{agent_url}/publishers directory endpoint #4836

Description

@bokelley

Implementation of the AAO directory inverse-lookup endpoint specified in #4823 and PR #4828.

Endpoint

GET /v1/agents/{agent_url}/publishers

Response shape per schema and docs.

Why this is its own ticket

The spec PR (#4828) is small (schema + docs). The server implementation is non-trivial:

  1. Discovery method surfacing. The existing agent_publisher_authorizations table tracks source (adagents_json | agent_claim) — the "evidence" lineage. The spec's discovery_method (direct | authoritative_location | adagents_authoritative | ads_txt_managerdomain) is a different concept: how this publisher → agent edge was reached in the discovery graph. Requires either a new column or a derived field set by the crawler at index time.

  2. Manager domain. Not stored on the existing row. Crawler knows which manager file the edge came from; needs to be persisted alongside the publisher_domain → agent_url edge.

  3. Per-publisher property counts. properties_authorized and properties_total are scoped to the row's publisher only, never network-wide. Computed by resolving the publisher's selectors via existing expandPublisherPropertiesToIdentifiers (already in federated-index.ts:6897). For managed-network-shape parent files, depends on the feat(adagents): clarify publisher_properties resolution — managed-network pattern needs inlined-properties path #4825 inline resolution rule.

  4. signing_keys_pinned lookup. Boolean — does the publisher's authorized_agents[] entry for this agent carry signing_keys[]? Cheap derived field at indexing time.

  5. Revoked tombstones. When a publisher's adagents.json newly lists a publisher_domain in revoked_publisher_domains[], emit one row with status: revoked on the next sync after revocation lands, then drop it.

Where it lands

  • Route: server/src/routes/registry-api.ts — new router.get("/v1/agents/:encodedUrl/publishers", ...). Sits alongside the existing /registry/lookup/agent/:agentUrl/domains (bare-bones inverse lookup that returns only domain strings, no provenance). The existing endpoint is kept; the new endpoint is the spec-compliant richer surface.
  • DB: server/src/db/federated-index-db.ts — extend getDomainsForAgent shape OR new getPublishersForAgentWithDetail that joins the necessary fields.
  • Crawler: surface discovery_method + manager_domain at index time in server/src/registry-sync/authorization-index.ts.

Dependencies

Out of scope (v1)

  • Authentication. Public endpoint, anonymous rate limiting (bucket key: agent_url + IP).
  • ?include=properties for inline property detail. Counts-only v1.
  • Cross-directory federation.

Companion SDK work

  • adcontextprotocol/adcp-client-python — fetch_agent_authorizations_from_directory(agent_url, directory_url) wrapper
  • adcontextprotocol/adcp-client (TS/JS) — mirror
  • adcontextprotocol/adcp-go — mirror

Acceptance

  • Endpoint returns spec-conformant response for direct, authoritative_location, adagents_authoritative, ads_txt_managerdomain discovery methods
  • Per-publisher property counts compute correctly on cafemedia-shape parent files (requires feat(adagents): clarify publisher_properties resolution — managed-network pattern needs inlined-properties path #4825)
  • signing_keys_pinned surfaces accurately for publishers that pin keys (e.g., WSJ)
  • revoked tombstones propagate within one refresh cycle of the publisher's revoked_publisher_domains[] update
  • Cursor pagination stable across refresh cycle
  • Conditional GET (If-None-Match) returns 304 when nothing changed
  • Rate limiting bucket keyed on agent_url + IP, returns 429 with Retry-After

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions