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
22 changes: 22 additions & 0 deletions .changeset/4890-aao-include-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
---

feat(aao): re-introduce `?include=properties` on `GET /v1/agents/{agent_url}/publishers` so SDK divergence detectors can run full set-diff, not just count comparison (#4890).

**Why.** The directory's per-publisher `properties_authorized` count is a false-negative trap for divergence detection. Count-equality is not set-equality — a publisher rotating N properties leaves the count unchanged while the underlying set is entirely different. The SDK divergence detector ([adcp-client-python#752](https://github.com/adcontextprotocol/adcp-client-python/pull/752)) currently has to short-circuit to "no divergence" on count match, missing routine publisher rotations against managed-network parent files (the cafemedia ~6,800-publisher shape).

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

1. **Query parameter** (`docs/aao/directory-api.mdx`). `?include=properties` — repeated-key form, same encoding rule as `status`. Default off (preserves the current envelope and payload size). Unknown values return `400`.

2. **Schema delta** (`static/schemas/source/aao/agent-publishers.json`). `PublisherEntry.property_ids: array of string`, present iff request included `include=properties`. Same population `properties_authorized` counts, surfaced as IDs. Per-publisher scope; never network-wide. Order unspecified — consumers treat as a set.

3. **Cost framing.** Roughly per-publisher-property-count × ~16 bytes per ID. On a managed-network parent file (~6,800 publishers × avg 1 property each ≈ 7 KB additional). Opt-in via the include flag so the default page payload is unchanged.

4. **Recommended workflow.** Divergence detectors SHOULD request `include=properties` and compare directory `property_ids[]` against a federated fetch as a set, not as a count.

**Out of scope.** Full property objects inline (not just IDs — consumers fetch detail via existing per-domain primitives). Pagination of very-large-per-publisher property lists (defer to v3 if it becomes a real shape).

**Follow-ups.**
- Server implementation in `server/src/routes/registry-api.ts` + `server/src/db/federated-index-db.ts` — wire `include=properties` through to a SQL projection that surfaces the resolved `property_ids[]` (already computed during `properties_authorized` count derivation per #4836). Update the Zod response schema (`AgentPublishersEntrySchema`) and regenerate `static/openapi/registry.yaml`. Integration test coverage parallel to the existing detail-row tests.
- SDK companion: upgrade `detect_publisher_properties_divergence` to full set-diff (`PublisherDivergence.missing_in_inline` / `.missing_in_federated` populated from `property_ids`, not `None`). Mirror in adcp-client (TS/JS) and adcp-go.
31 changes: 30 additions & 1 deletion docs/aao/directory-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ GET https://{aao_directory}/v1/agents/{agent_url}/publishers
| `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` | string, repeated | `authorized` | Filter by lifecycle status. v1: `authorized`, `revoked`. Repeat the key once per value (OpenAPI `style: form, explode: true`). Comma-separated single-value form (`status=authorized,revoked`) is **not** accepted; directories MUST return `400` with an explanation pointing to the repeated-key form. |
| `limit` | int (1–1000) | 200 | Max publishers per page. |
| `include` | string, repeated | unset | Opt into expanded per-row fields. v1: `properties` — each `PublisherEntry` carries the canonical `property_ids[]` list under that publisher (lets consumers run full set-diff against a federated fetch, not just count comparison). Repeated-key form, same encoding rule as `status`. Unknown values return `400`. |

#### Worked example: filtering by multiple status values

Expand Down Expand Up @@ -72,6 +73,16 @@ The OpenAPI fragment for the `status` parameter:

Repeated-key was chosen because (a) it is what `URLSearchParams.append()` and OpenAPI's default `explode: true` produce, (b) it composes cleanly with future values that might contain a comma, and (c) it leaves no parser ambiguity at the directory.

#### Worked example: requesting `?include=properties` for full set-diff

```
GET /v1/agents/https%3A%2F%2Fsales-agent.example.com%2F/publishers?include=properties
```

The default response carries `properties_authorized` as a count only. Count-equality is **not** set-equality: a publisher rotating three properties leaves the count unchanged but the set entirely different, which a count-based divergence detector cannot see. `?include=properties` adds a `property_ids: list[string]` field per `PublisherEntry` — the canonical IDs the agent's selectors resolve to under that publisher — so consumers can run full set-diff against a federated `fetch_agent_authorizations` result and detect rotation, not just delta-in-magnitude.

The flag is opt-in to keep the default page payload small. Inline IDs add roughly the per-publisher property count × ~16 bytes per ID; on a managed-network parent file (~6,800 publishers × avg 1 property each ≈ 7 KB of additional IDs), the overhead is small but non-zero. Pagination semantics are unchanged.

### Response

```json
Expand Down Expand Up @@ -114,6 +125,22 @@ Repeated-key was chosen because (a) it is what `URLSearchParams.append()` and Op
}
```

With `?include=properties`, each `PublisherEntry` additionally carries `property_ids`:

```json
{
"publisher_domain": "recipeswithessentialoils.com",
"discovery_method": "ads_txt_managerdomain",
"manager_domain": "cafemedia.com",
"properties_authorized": 3,
"properties_total": 3,
"property_ids": ["p-001", "p-002", "p-003"],
"signing_keys_pinned": false,
"status": "authorized",
"last_verified_at": "2026-05-19T08:00:00Z"
}
```

## Field reference

### Envelope
Expand All @@ -134,6 +161,7 @@ Repeated-key was chosen because (a) it is what `URLSearchParams.append()` and Op
| `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. |
| `property_ids` | conditional | Present iff request included `?include=properties`. Canonical list of `property_id`s under this publisher that the agent's selectors resolve to — the same population `properties_authorized` counts, surfaced as IDs so consumers can run full set-diff (not just count comparison) against a federated fetch. Per-publisher scope; never network-wide. Treat as a set; order is unspecified. |
| `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`. |
Expand Down Expand Up @@ -197,6 +225,7 @@ 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.
4. Operators running a divergence detector (catching cases where the directory and the publisher's live `adagents.json` disagree) SHOULD request `?include=properties` and compare the directory's `property_ids[]` against a federated fetch as a set, not as a count. A publisher rotating N properties produces equal counts on both sides; only set-comparison catches it.

## Relationship to `publisher_properties` inline resolution

Expand All @@ -207,7 +236,7 @@ On managed-network-shape parent files (per [adcp#4825 inline resolution rule](/d
- **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.
- **Full property objects inline.** `?include=properties` returns the resolved `property_ids[]` only — not the property objects themselves. Consumers with the IDs can fetch detail via existing per-domain primitives.

## See also

Expand Down
5 changes: 5 additions & 0 deletions static/schemas/source/aao/agent-publishers.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
"minimum": 0,
"description": "Count of properties under THIS `publisher_domain` only — total inventory the publisher's file declares. Never a network-wide count. On managed-network-shape parent files (per adcp#4825 inline resolution), this is the count of inline `properties[]` entries whose `publisher_domain` field matches this row's domain."
},
"property_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Canonical list of `property_id`s under THIS `publisher_domain` that the agent's selectors resolve to. Present iff the request included `?include=properties`; absent otherwise. The set is the same population `properties_authorized` counts, surfaced as IDs so consumers can run full set-diff against a federated fetch (count-equality is not set-equality — a publisher rotating N properties leaves the count unchanged but the set entirely different). Per-publisher scope; never network-wide. Order is unspecified; consumers should treat as a set."
},
"signing_keys_pinned": {
"type": "boolean",
"description": "Whether the publisher's adagents.json entry for this agent pins `signing_keys[]`. When true, the agent's signed responses MUST verify against the pinned key set regardless of the agent's own JWKS. Operators should treat true as a signal that their published JWKS must match the publisher's pin."
Expand Down
Loading