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
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ from adcp.testing import build_asgi_app, build_test_client, make_request_context

## Property Lists and Identifier Resolution

For publisher manifest sync, resolve the agent's selectors with
`get_properties_by_agent` or `resolve_properties_for_agent`, then call
`properties_for_publisher_domain(properties, publisher_domain)` to attribute
inventory. It keeps publisher subdomains and supports wildcard domain inventory.
Pass `include_unattributed=False` to exclude inventory without a publisher or
domain anchor. When using bare-entry permissive resolution, pass
`publisher_domain=` from the trusted fetch origin to require a matching domain
identifier. Use `await verify_cross_domain_properties(properties, source_domain,
agent_url)` before trusting external publisher claims; it follows secure
publisher discovery and checks strict inventory authorization and revocations.
All three helpers are exported from `adcp`. See `docs/adagents-discovery.md`.

Import `MCPPropertyListFetcher`, `ProductPropertyPolicy`, and
`check_property_list_reference` from `adcp`. The default seller fetcher resolves
lists with MCP `get_property_list`, follows pagination, and caches complete
Expand Down
117 changes: 117 additions & 0 deletions docs/adagents-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,120 @@ Malformed sibling entries and non-string or empty URL values are skipped during
lookup. `validate_adagents_structure(document)` reports invalid URL types through
its existing `missing_url` diagnostic. The listing helper returns matching entry
objects in source order and does not modify the document.

## Attributing properties to a publisher

After resolving the agent's selectors, use `properties_for_publisher_domain`
to select a publisher's inventory from a shared manifest:

```python
from adcp import get_properties_by_agent, properties_for_publisher_domain

authorized = get_properties_by_agent(document, agent_url)
properties = properties_for_publisher_domain(authorized, "www.example.org")
```

An explicit property `publisher_domain` takes precedence over identifiers and
must name the requested publisher. Otherwise, a `type="domain"` or
`type="subdomain"` identifier must
name that publisher or any subdomain. For example, `shop.example.org` and
`ctv.example.org` both belong to `example.org`. Host case, trailing dots, IDNA
and a leading `www.` alias within the same registrable domain are normalized.
The alias rule respects both public and private suffixes, so `www.com` cannot
become `com` and `www.github.io` cannot become `github.io`. Domain matching uses
`domain_matches`, including an explicit `*.example.org` pattern.
Wildcard domain inventory such as `*.example.org` belongs to `example.org`;
publisher-domain arguments must be concrete hostnames.

Properties with neither a publisher anchor nor a domain identifier, such as
bundle-only apps, are kept by default. Pass `include_unattributed=False` to
exclude them. Invalid domain claims are excluded. The helper returns original
objects in input order without changing the document. Attribution trusts the
manifest's claims; it does not establish cross-domain authority.

## Gating bare-entry resolution

When using permissive resolution, supply the domain from the fetch origin:

```python
from adcp import fetch_adagents, resolve_properties_for_agent

document = await fetch_adagents(domain, validate_structure=False)
properties = resolve_properties_for_agent(
document, agent_url, mode="permissive", publisher_domain=domain
)
```

For bare-entry fallback, each property must have a domain identifier naming
the publisher or a subdomain. The property's own `publisher_domain` cannot
satisfy this gate. Apps, venues and other inventory without a matching domain
identifier are excluded. Typed authorization selectors keep their existing
behavior in both modes. Omitting `publisher_domain` preserves the existing
permissive behavior; strict resolution remains the default.

## Verifying cross-domain claims

Use `verify_cross_domain_properties` on resolved inventory before trusting
claims about other domains:

```python
from adcp import (
fetch_adagents,
get_properties_by_agent,
properties_for_publisher_domain,
verify_cross_domain_properties,
)

source_domain = "network.example"
document = await fetch_adagents(source_domain)
authorized = get_properties_by_agent(document, agent_url)
verified = await verify_cross_domain_properties(
authorized, source_domain, agent_url
)
publisher_properties = properties_for_publisher_domain(verified, "publisher.example")
```

The source domain must come from the trusted fetch origin. For every external
publisher anchor and domain or subdomain identifier, the helper fetches that domain's own
manifest and strictly resolves the same agent's selectors. The remote
authorization must cover the claimed inventory and be attributable to the
checked domain. A matching agent URL alone is insufficient. All claimed typed
identifiers must match an authorized property of the same type, using
`identifiers_match`; a matching property ID cannot override different domain
or bundle identifiers. When identifiers are absent, a publisher-scoped
property ID is used.

A verified publisher anchor also covers that publisher's subdomain identifiers.
Parent domains and unrelated domains require their own verification. Wildcard
inventory is verified at its base hostname, and Unicode and IDNA domain
identities compare equivalently. The permissive gate continues to require
`type="domain"`; attribution and verification also recognize `type="subdomain"`.

The default fetcher is `fetch_adagents`, which applies the existing URL/DNS,
response size and redirect protections, follows `authoritative_location`, and
discovers ads.txt `MANAGERDOMAIN` delegation. Delegation still requires the
resolved manifest to authorize inventory attributable to the checked publisher;
an unanchored app in a shared manager file is insufficient. Remote revocations
override authorization. A missing, invalid or unavailable manifest excludes
that domain's claims and logs the failure at debug level. Each external domain
is fetched once per call, sequentially, without recursively verifying its
other claims. There is no persistent verification cache, so later calls can
observe revocations.

Local inventory, including subdomains of the source, passes through. Inventory
with no domain identifier or publisher anchor also passes through because
there is no remote domain to check. Use `include_unattributed=False` during
attribution to exclude it, and use the domain gate above for bare entries.

For custom timeouts, deterministic tests or caller-controlled discovery, pass
an async `fetcher(domain) -> dict`. It must provide secure discovery and follow
publisher delegation. For example:

```python
async def fetch_publisher(domain: str) -> dict:
return await fetch_adagents(domain, timeout=5.0)

verified = await verify_cross_domain_properties(
authorized, source_domain, agent_url, fetcher=fetch_publisher
)
```
8 changes: 7 additions & 1 deletion docs/adagents-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ from adcp import fetch_adagents, validate_adagents_structure, resolve_properties
async def inspect_publisher(domain: str, agent_url: str):
document = await fetch_adagents(domain, validate_structure=False)
report = validate_adagents_structure(document)
properties = resolve_properties_for_agent(document, agent_url, mode="permissive")
properties = resolve_properties_for_agent(
document, agent_url, mode="permissive", publisher_domain=domain
)
return report, properties
```

The domain gate restricts bare-entry fallback to properties with a matching
domain identifier. See [publisher agent discovery](adagents-discovery.md) for
publisher attribution and cross-domain verification.

The opt-out preserves JSON-object and endpoint checks, authorization-array shape,
renderer catalog trust checks, URL/DNS SSRF gates, response size caps, and redirect
constraints. `fetch_adagents_with_cache` and `validate_adagents_domain` accept the
Expand Down
6 changes: 6 additions & 0 deletions src/adcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,13 @@ def _resolve_version() -> str:
"get_properties_by_agent",
"identifiers_match",
"parse_managerdomains",
"properties_for_publisher_domain",
"resolve_properties_for_agent",
"validate_adagents_domain",
"validate_adagents_structure",
"verify_agent_authorization",
"verify_agent_for_property",
"verify_cross_domain_properties",
),
"adcp.canonical_formats": (
"format_is_supported",
Expand Down Expand Up @@ -1600,10 +1602,12 @@ def get_adcp_version() -> str:
"domain_matches",
"identifiers_match",
"parse_managerdomains",
"properties_for_publisher_domain",
"get_all_properties",
"get_all_tags",
"get_properties_by_agent",
"resolve_properties_for_agent",
"verify_cross_domain_properties",
# Test helpers
"test_agent",
"test_agent_a2a",
Expand Down Expand Up @@ -1851,11 +1855,13 @@ def get_adcp_version() -> str:
get_properties_by_agent,
identifiers_match,
parse_managerdomains,
properties_for_publisher_domain,
resolve_properties_for_agent,
validate_adagents_domain,
validate_adagents_structure,
verify_agent_authorization,
verify_agent_for_property,
verify_cross_domain_properties,
)
from adcp.canonical_formats import (
format_is_supported,
Expand Down
Loading
Loading