Skip to content
Closed
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
15 changes: 15 additions & 0 deletions .changeset/4825-adagents-inline-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"adcontextprotocol": minor
---

feat(adagents): permit parent-file inline resolution of `publisher_properties` selectors when matched top-level properties carry a `publisher_domain` matching the selector's addressed domains.

Federated per-child resolution remains the default and the trust root. Inline is a per-domain optimization: consumers SHOULD prefer it when the parent file's `properties[]` contains entries whose `publisher_domain` matches the selector's `publisher_domains[]` and whose tags satisfy the predicate. Production managed-network deployments inline properties (~6,800 represented domains in a single authoritative file) — strict federation requires N HTTP fetches per authorization check, infeasible at scale.

`by_id` selectors remain federated-only. Property IDs are publisher-scoped; fanning a fixed ID set across multiple publishers would silently authorize inventory at unintended publishers.

Divergence rule: if inline and federated resolutions disagree on the same `(publisher_domain, property_id)`, federated wins. Revocation: inline path MUST honor parent-file `revoked_publisher_domains[]`; consumers SHOULD also cross-check the child's own when federated.

Trust invariant: every matched inline property MUST carry an explicit `publisher_domain`. Properties missing this field MUST NOT be included in the inline resolution result set.

Companion: adcp-client-python needs a matching SDK update to fan `publisher_domains[]` into per-domain resolution and consult parent-file inline first when matching properties carry `publisher_domain`. Tracked in adcontextprotocol/adcp-client-python#746.
24 changes: 18 additions & 6 deletions docs/governance/property/adagents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,9 @@ When `https://{publisher}/.well-known/adagents.json` returns `404`, validators M

1. **Per-agent paths.** The agent entry directly carries the publisher domain under `publisher_properties[].publisher_domain`, inside `publisher_properties[].publisher_domains[]` (the compact managed-network form), or under `collections[].publisher_domain`.
2. **Property-level paths.** The agent entry references one or more top-level `properties[]` entries by ID (`property_ids`) or by tag (`property_tags`), and at least one resolved property carries a `publisher_domain` matching the source. This is the shape used in production by Mediavine and other managed networks where a property declares its `publisher_domain` once and many agents reference it indirectly.
3. **Publisher-properties inline path.** When the agent entry uses `authorization_type: "publisher_properties"` and the consumer resolves the selector via the parent-file inline path (see [Fan-out resolution semantics](#fan-out-resolution-semantics)), each matched inline property's `publisher_domain` field provides explicit naming of the publisher. The inline resolution precondition — requiring every matched property to carry a `publisher_domain` matching the selector's addressed domains — ensures the manager has positively typed the publisher domain in the same manifest. `by_id` selectors remain federated-only and do not benefit from this path.

What does **not** satisfy this rule: an `inline_properties` selector whose inline properties omit `publisher_domain`, or a top-level `property_tags` selector whose resolved top-level properties carry no matching `publisher_domain`. If no reachable `publisher_domain` field matches the source, fallback MUST fail closed.
What does **not** satisfy this rule: an `inline_properties` selector whose inline properties omit `publisher_domain`, a top-level `property_tags` selector whose resolved top-level properties carry no matching `publisher_domain`, or a `publisher_properties` inline-resolution match where the property entry lacks `publisher_domain` (such entries MUST be excluded from the inline result set). If no reachable `publisher_domain` field matches the source, fallback MUST fail closed.

The attack this protects against is implicit scoping — authorization that does not name the publisher anywhere the manager has to type the domain. Indirection through `properties[].publisher_domain` is safe because the manager has positively spelled the publisher's domain in the same manifest; the gate filters to properties whose `publisher_domain` matches the source before considering the reference.
- **No success by silence**: if the manager lookup fails, treat the publisher as missing `adagents.json` (same as no fallback).
Expand Down Expand Up @@ -493,12 +494,23 @@ Validators MUST reject any `publisher_properties[]` entry that includes both `pu

The above is semantically identical to repeating the singular-form entry once per domain. Use it when every represented publisher tags inventory the same way (typical of WordPress/managed-network deployments); fall back to one entry per publisher only when the selector predicate genuinely differs.

**Fan-out resolution semantics.** Each listed domain in `publisher_domains[]` is resolved **independently and in parallel** against that publisher's `adagents.json`:
### Fan-out resolution semantics

- Resolution is **consumer-side**, not publish-side. The manager file MUST NOT inline copies of each represented publisher's properties; consumers (validators, buyer agents indexing the file) fetch each listed publisher's own `adagents.json` to resolve the selector predicate. The manager file is the authorization declaration; the per-publisher files are the property catalog.
- If a listed publisher's `adagents.json` is unreachable (404, 5xx, timeout, fails its own validation), the selector resolves to the empty set **for that publisher only** — the entry remains valid for all other listed publishers. Consumers MUST NOT treat a single unreachable publisher as poisoning the rest of the compact entry.
- If a listed publisher's `adagents.json` carries no properties matching the predicate (no entries with the named tag), the selector resolves to the empty set for that publisher. Same partial-resolution rule applies.
- Resolution caching follows each publisher's own cache policy on their `adagents.json`, independently. A consumer SHOULD NOT extend or shorten one publisher's cache TTL based on observations of another publisher in the same compact entry.
Each listed domain in `publisher_domains[]` is resolved **independently and in parallel**. A `publisher_properties` selector resolves against properties in one of two ways:

1. **Federated resolution (default).** For each domain in `publisher_domain` or `publisher_domains[]`, fetch that publisher's `adagents.json` and apply the selector predicate against the publisher's own top-level `properties[]`. This is the trust-root path. Per-publisher error handling: if a listed publisher's `adagents.json` is unreachable (404, 5xx, timeout, fails its own validation), the selector resolves to the empty set **for that publisher only** — the entry remains valid for all other listed publishers. Consumers MUST NOT treat a single unreachable publisher as poisoning the rest of the compact entry. If a publisher's file carries no properties matching the predicate, the selector resolves to the empty set for that publisher. Resolution caching follows each publisher's own cache policy independently; a consumer SHOULD NOT extend or shorten one publisher's cache TTL based on observations of another publisher in the same compact entry.

2. **Parent-file inline resolution (managed-network optimization).** Consumers SHOULD prefer this path when the parent file's top-level `properties[]` contains entries whose `publisher_domain` field matches the selector's addressed domains and whose other fields satisfy the selector predicate. Consumers that prefer strict federation MAY ignore the inline path entirely. Inline resolution conditions:
- Every matched property MUST carry an explicit `publisher_domain` field whose value equals one of the selector's addressed domains. A `properties[]` entry without a `publisher_domain` field MUST NOT be included in the inline resolution result set for any `publisher_properties` selector, regardless of `selection_type`.
- For `selection_type: by_tag`: the property's `tags[]` MUST contain at least one of the selector's `property_tags[]`.
- For `selection_type: by_id`: inline resolution is **not available**; `by_id` selectors remain federated-only even when the parent file carries inline properties. Property IDs are scoped to the publisher that mints them; a `by_id` selector targets IDs in that publisher's namespace, not in the manager file's inline property catalog. Inline resolution cannot reliably answer whether a given ID exists at the addressed publisher.
- For `selection_type: all`: every parent-file `properties[]` entry with a `publisher_domain` matching the selector is selected.

Inline resolution is a **per-domain optimization**: a consumer MAY use inline for domains that have matching entries in the parent file and federated for the remainder. Inline resolution MUST honor `revoked_publisher_domains[]` on the parent file; a `publisher_domain` listed as revoked resolves to the empty set for that domain regardless of whether matching properties exist in `properties[]`. Consumers that also resolve federated SHOULD cross-check the child's own `revoked_publisher_domains[]`; first match (parent or child) revokes.

If inline and federated resolutions disagree on the same `(publisher_domain, property_id)`, the consumer MUST use the full federated property object and discard the inline object; the consumer SHOULD log the divergence as a publisher-side data-integrity warning.

**Why this is safe.** By requiring `publisher_domain` on each matched inline property and matching against the selector's addressed domains, the inline path preserves the bilateral verification invariant: the publisher whose inventory is being authorized must be explicitly named. A manager file cannot use inline resolution to authorize inventory for a publisher it doesn't list. This is the same invariant the [`managerdomain` fallback safety rule](#safety-rules-for-this-fallback) protects.

## Authorization Qualifiers

Expand Down
4 changes: 2 additions & 2 deletions static/schemas/source/core/publisher-property-selector.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"oneOf": [
{
"type": "object",
"description": "Select all properties from one publisher domain, or from each publisher domain when `publisher_domains` is used.",
"description": "Select all properties from one publisher domain, or from each publisher domain when `publisher_domains` is used. Resolves against each publisher's own adagents.json (federated path) or against matching entries in the parent file's top-level `properties[]` whose `publisher_domain` matches the selector's addressed domains (inline optimization — see adagents.json fan-out resolution semantics).",
"properties": {
"publisher_domain": {
"type": "string",
Expand Down Expand Up @@ -76,7 +76,7 @@
},
{
"type": "object",
"description": "Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json the common managed-network case where every represented site tags inventory with a shared label.",
"description": "Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json (federated path) or against matching entries in the parent file's top-level `properties[]` whose `publisher_domain` matches the selector's addressed domains and whose `tags[]` contains at least one of the selector's `property_tags[]` (inline optimization — see adagents.json fan-out resolution semantics).",
"properties": {
"publisher_domain": {
"type": "string",
Expand Down
Loading