Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/4823-aao-directory-inverse-lookup-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
---

feat(aao): define directory inverse-lookup endpoint — `GET /v1/agents/{agent_url}/publishers` returns the set of publishers whose adagents.json authorizes a given agent_url, with provenance, per-publisher property counts, and lifecycle status.

**Why.** The push primitive (`verify_agent_authorization`) and the pull-with-list primitive (`fetch_agent_authorizations`) both presuppose the operator already knows the publisher set. At managed-network scale (Raptive: ~6,800 domains delegated to cafemedia.com) that presupposition fails. The AAO directory at `agenticadvertising.org` already indexes publisher adagents.json files — this RFC exposes the inverse map as a public HTTP endpoint so sales-agent operators can answer "what publishers have authorized my agent?" at sync time, without crawling the open web themselves.

**What this changeset defines** (spec-only — server implementation tracked separately, see Follow-ups).

1. **Endpoint shape** (`docs/aao/directory-api.mdx`). `GET /v1/agents/{agent_url}/publishers` with `since`, `cursor`, `status`, `limit` query parameters. Canonicalizes the `agent_url` lookup key the same way `verify_agent_authorization` does. Cursor-based pagination; opaque cursors.

2. **Response envelope schema** (`static/schemas/source/aao/agent-publishers.json`). Returns `{ agent_url, directory_indexed_at, publishers[], next_cursor }`. Each `PublisherEntry` carries `publisher_domain`, `discovery_method`, optional `manager_domain`, `properties_authorized`, `properties_total`, optional `signing_keys_pinned`, `status`, `last_verified_at`.

3. **`discovery_method` enum** — `direct`, `authoritative_location`, `adagents_authoritative`, `ads_txt_managerdomain`. Distinguishes the four trust profiles. The directory verifies the [`managerdomain` fallback safety rule](/docs/governance/property/adagents#safety-rules-for-this-fallback) before returning a row with `ads_txt_managerdomain` — bilateral verification done once, for everyone.

4. **Per-publisher scoped counts.** `properties_authorized` and `properties_total` are scoped to the row's `publisher_domain` only, never network-wide. Avoids the "12/12 = full auth but really 12-of-6800-network" misread that a flat count would produce on managed-network rows.

5. **Lifecycle status enum** (v1) — `authorized` and `revoked`. `unbound`, `pending`, `unreachable`, `no_properties` deferred — directory does not have the crawler state to emit them honestly. `revoked` propagates parent-file `revoked_publisher_domains[]` (from the managed-network-scale changeset) on the next sync, then drops the tombstone.

6. **HTTP semantics** — `200` (success, MAY be empty), `400` (malformed), `404` (never indexed; distinct from 200 + empty), `429` (rate-limited), `5xx` (retry). `ETag` + `Cache-Control` set; `If-None-Match` for conditional GET.

7. **Authentication** — none in v1. Publishers are public; the inverse map is public. Rate-limiting keyed on `agent_url` + IP. Identity-bound limits via RFC 9421 request signing arrive in a separate RFC if needed.

**Dependency.** `properties_total` on managed-network-shape parent files (the cafemedia case) depends on the [adcp#4825](https://github.com/adcontextprotocol/adcp/issues/4825) inline resolution rule. Strict federation at managed-network scale requires N HTTP fetches per directory refresh per publisher — the same scale problem operators have, moved one layer up. With inline resolution endorsed, the directory computes per-publisher counts from the parent file's inline `properties[]` filtered by matching `publisher_domain`.

**Follow-ups.**
- Server implementation in `server/src/routes/registry-api.ts` (new endpoint), backed by `server/src/db/federated-index-db.ts` (extends existing `getDomainsForAgent` shape with `discovery_method`, `manager_domain`, count resolution, `signing_keys_pinned`). Tracked: `feat(server): implement /v1/agents/{agent_url}/publishers endpoint` — to be filed.
- SDK companion: `fetch_agent_authorizations_from_directory(agent_url, directory_url)` in adcp-client-python (adcp-client-python#746) and TS/Go/Java mirrors.
- `?include=properties` for inline property detail — out of scope for v1; add later if operators ask.
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,8 @@
"docs/aao/connect-addie",
"docs/aao/users",
"docs/aao/org-admins",
"docs/aao/addie-tools"
"docs/aao/addie-tools",
"docs/aao/directory-api"
]
},
{
Expand Down
175 changes: 175 additions & 0 deletions docs/aao/directory-api.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
---
title: AAO Directory API — Agent ↔ Publisher Inverse Lookup
description: HTTP API for sales-agent operators to discover which publishers authorize their agent, sourced from the AAO directory's index of publisher adagents.json files. Returns provenance, per-publisher property counts, and lifecycle status.
"og:title": "AdCP — AAO Directory Inverse Lookup API"
---

# AAO Directory API

The AAO directory at `agenticadvertising.org` indexes publisher `adagents.json` files across the open web. This API surfaces the **inverse map** every sales-agent operator needs at sync time:

> "What publishers have authorized my agent?"

Without this endpoint, operators have to either maintain the publisher domain list manually and call `fetch_agent_authorizations` against it, or crawl the open web themselves. Both are infeasible at managed-network scale ([cafemedia](https://cafemedia.com/.well-known/adagents.json) alone delegates ~6,800 publisher domains under a single manager file).

This endpoint is **discovery**, not **authorization**. The publisher's own `adagents.json` remains the trust root. The directory tells you which publishers to verify directly via the SDK's per-domain primitives (`verify_agent_authorization`, `fetch_agent_authorizations`).

## Endpoint

```
GET https://{aao_directory}/v1/agents/{agent_url}/publishers
```

`{agent_url}` MUST be percent-encoded. The directory canonicalizes the lookup key (lowercase host, default port stripped, trailing slash on path component normalized) using the same convention the SDK applies in `verify_agent_authorization`.

### Query parameters

| Parameter | Type | Default | Semantics |
|---|---|---|---|
| `since` | ISO 8601 | unset | Return only publishers whose `last_verified_at` ≥ `since`. Enables incremental sync. |
| `cursor` | opaque string | unset | Pagination cursor returned by a prior response. Stable across the directory's refresh cycle for the lifetime of the cursor. |
| `status` | comma-separated | `authorized` | Filter by lifecycle status. v1: `authorized`, `revoked`. |
| `limit` | int (1–1000) | 200 | Max publishers per page. |

### Response

```json
{
"agent_url": "https://sales-agent.example.com",
"directory_indexed_at": "2026-05-19T12:00:00Z",
"publishers": [
{
"publisher_domain": "recipeswithessentialoils.com",
"discovery_method": "ads_txt_managerdomain",
"manager_domain": "cafemedia.com",
"properties_authorized": 1,
"properties_total": 1,
"signing_keys_pinned": false,
"status": "authorized",
"last_verified_at": "2026-05-19T08:00:00Z"
},
{
"publisher_domain": "wsj.com",
"discovery_method": "direct",
"manager_domain": null,
"properties_authorized": 47,
"properties_total": 200,
"signing_keys_pinned": true,
"status": "authorized",
"last_verified_at": "2026-05-19T10:00:00Z"
},
{
"publisher_domain": "former-partner.example",
"discovery_method": "authoritative_location",
"manager_domain": "cafemedia.com",
"properties_authorized": 0,
"properties_total": 0,
"signing_keys_pinned": false,
"status": "revoked",
"last_verified_at": "2026-05-19T11:00:00Z"
}
],
"next_cursor": "eyJv..."
}
```

## Field reference

### Envelope

| Field | Required | Notes |
|---|---|---|
| `agent_url` | yes | Canonicalized echo of the lookup key. |
| `directory_indexed_at` | yes | Most recent per-publisher refresh in the result set. Provenance for the consumer's own cache. |
| `publishers` | yes | Array. Empty array is a valid response — the directory has indexed this agent but no current authorizations resolve. |
| `next_cursor` | optional | Opaque pagination cursor; absent or null on the terminal page. |

### `PublisherEntry`

| Field | Required | Notes |
|---|---|---|
| `publisher_domain` | yes | Publisher whose `adagents.json` authorizes the agent. |
| `discovery_method` | yes | `direct`, `authoritative_location`, `adagents_authoritative`, or `ads_txt_managerdomain`. See below. |
| `manager_domain` | conditional | Required when `discovery_method` ≠ `direct`. Null otherwise. |
| `properties_authorized` | yes | Count of properties under **this publisher_domain only** that the agent's selectors resolve to. Never a network-wide count. |
| `properties_total` | yes | Count of properties under **this publisher_domain only** in the publisher's file (or parent file's inline subset for that domain). Never a network-wide count. |
| `signing_keys_pinned` | optional | Whether the publisher pins `signing_keys[]` on this agent. When `true`, agent's signed responses MUST verify against the pinned set regardless of the agent's own JWKS. |
| `status` | yes | `authorized` or `revoked`. See below. |
| `last_verified_at` | yes | When the directory last fetched and validated this publisher's `adagents.json`. |

### `discovery_method` values

| Value | Meaning | Trust profile |
|---|---|---|
| `direct` | Agent listed in the publisher's own `/.well-known/adagents.json`. | Strongest — no delegation hops. |
| `authoritative_location` | Publisher's `/.well-known/adagents.json` declared `authoritative_location` pointing to a manager file that lists the agent. | Strong — publisher actively delegated. |
| `adagents_authoritative` | Discovered via the manager file's own `properties[]` carrying the publisher's domain (per [adcp#4825 inline resolution rule](https://github.com/adcontextprotocol/adcp/issues/4825)). | Medium — publisher named in manager file but didn't host the delegation themselves. |
| `ads_txt_managerdomain` | Discovered via the publisher's `ads.txt` `MANAGERDOMAIN=` directive pointing to the manager file. | Weakest — the [`managerdomain` fallback safety rule](/docs/governance/property/adagents#safety-rules-for-this-fallback) is the only positive cross-check. |

The directory verifies the `managerdomain` safety rule before returning a row with `discovery_method: ads_txt_managerdomain` — this is the directory's main value-add over a per-operator `ads.txt` crawl.

### `status` values

| Value | Meaning |
|---|---|
| `authorized` | Selector resolves to ≥ 1 property under this publisher_domain. The normal case. |
| `revoked` | Publisher previously authorized the agent and now lists this `publisher_domain` in `revoked_publisher_domains[]` of an authoritative file. Emitted as a tombstone on the first sync after revocation lands, then dropped. Lets operators propagate revocations without polling each publisher's cache TTL. |

`unbound`, `pending`, `unreachable`, and `no_properties` are **intentionally not part of v1**. The directory only indexes publishers whose `adagents.json` was successfully fetched and references the agent. If a publisher disappears, the directory drops it from results rather than returning a tombstone (consumers track membership via set-diff against prior pages).

## HTTP semantics

| Status | Meaning |
|---|---|
| `200 OK` | Lookup succeeded. Body MAY have empty `publishers[]`. |
| `400 Bad Request` | Malformed `agent_url`, invalid cursor, or unknown `status` value. |
| `404 Not Found` | Directory has never indexed any publisher referencing this `agent_url`. **Distinct from `200` + empty** (which means the directory has indexed this agent, but no current authorizations resolve). |
| `429 Too Many Requests` | Rate limit. `Retry-After` header set. Bucket key: `agent_url` (anonymous) plus IP (defense-in-depth). |
| `5xx` | Directory error. Consumer SHOULD retry with backoff. |

The endpoint sets `Cache-Control` and `ETag`. Conditional `GET` (`If-None-Match`) is the wire-level cache mechanism; `directory_indexed_at` in the body is the freshness anchor for consumer logic.

## Authentication

V1 is unauthenticated. Publisher `adagents.json` files are public; the inverse map is public. If rate-limiting graduates from IP-based to identity-based, the path is a separate RFC layering [RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421) request signing keyed off the agent's published JWKS — the agent proves it controls `agent_url` by signing the request. Out of scope for this RFC.

## Pagination

Cursors are opaque. Treat them as substitutable strings and pass them back verbatim. The directory MAY change cursor format without notice; consumers MUST NOT parse cursor contents.

A cursor remains valid for at least one directory refresh cycle. Past that, the directory MAY return `400` with `cursor_expired` or `200` with re-traversal from the start — both are conforming. Consumers SHOULD record the wall-clock time of the prior request and refuse to use cursors older than 24 hours.

## Relationship to other primitives

The AAO directory complements the existing SDK primitives:

| Question | Primitive | Direction |
|---|---|---|
| Is *this* agent listed in *this* publisher's `adagents.json`? | `verify_agent_authorization(adagents_data, agent_url)` | Push (publisher → agent) |
| Given a list of publishers, which authorize my agent? | `fetch_agent_authorizations(agent_url, publisher_domains)` | Pull, caller-supplied list |
| **Which publishers authorize my agent?** | **`GET /v1/agents/{agent_url}/publishers`** | **Pull, directory-supplied list** |

The first two answer questions where the operator already knows the publisher set. The directory endpoint answers the operator's actual sync-time question: "what's my publisher set?"

The recommended workflow:

1. Call `GET /v1/agents/{agent_url}/publishers` to discover the publisher set.
2. For each `publisher_domain` in the response, the operator MAY call `verify_agent_authorization` against the publisher's own `adagents.json` to re-confirm against the trust root. The directory's `last_verified_at` reduces but does not eliminate the need for per-domain verification on critical paths.
3. Use the response's `properties_authorized` / `properties_total` for operator-facing scope summaries, and the `signing_keys_pinned` flag to surface which agents must publish a JWKS matching the publisher's pin.

## Relationship to `publisher_properties` inline resolution

On managed-network-shape parent files (per [adcp#4825 inline resolution rule](/docs/governance/property/adagents#resolution-paths)), the directory computes `properties_total` from the parent file's inline `properties[]` filtered by `publisher_domain`. Strict federation at this scale would require N HTTP fetches per directory refresh per publisher — the same scale problem operators have, moved one layer up. The directory uses the inline resolution rule the spec endorses.

## Out of scope (v1)

- **Authentication.** Public endpoint, anonymous rate limiting. Identity-bound limits arrive in a separate RFC if needed.
- **Cross-directory federation.** Single directory. The endpoint shape is defined such that multiple AAO-compatible directories could implement it; discovery of which directory to query is configuration today.
- **Push notification of new authorizations.** Poll-based v1.
- **Inline property detail.** `properties_authorized` / `properties_total` are counts only; full property lists arrive via a future `?include=properties` if operators ask.

## See also

- [adagents.json Tech Spec](/docs/governance/property/adagents) — the trust root.
- [Managed Network Deployment](/docs/governance/property/managed-networks) — the canonical multi-publisher pattern this endpoint indexes.
- [adcp#4825](https://github.com/adcontextprotocol/adcp/issues/4825) — `publisher_properties` inline resolution rule the directory's count fields depend on.
Loading
Loading