Context
AdCP issue adcontextprotocol/adcp#5450 and merged spec PR adcontextprotocol/adcp#5472 define the redirect policy for adagents.json discovery. The reference validator in adcontextprotocol/adcp is now aligned; this SDK needs the same behavior so all AdCP implementations resolve discovery identically.
The bug this fixes: a publisher whose apex domain 301-redirects to www (the default on most managed hosting / CDNs) is silently reported unauthorized by validators that refuse all redirects on the discovery fetch.
Policy — two hops, deliberately different
| Fetch |
Redirect policy |
Initial https://{domain}/.well-known/adagents.json |
Follow same-registrable-domain redirects (apex↔www, HTTPS-preserving, ≤3 hops), re-validating SSRF (reject non-HTTPS, reject reserved/private IPs, pin connect) on every hop, with the same-domain comparison anchored on the originally-requested domain at every hop (not the previous hop). Refuse cross-registrable-domain redirects — that is an unscoped delegation signal; the publisher should declare delegation via authoritative_location instead. |
authoritative_location dereference (2nd hop) |
Refuse all redirects. The named URL is the declared authoritative location; a redirect away from it changes that declaration and MUST be treated as an error. |
⚠️ Security-critical: use the PSL private section
Registrable-domain comparison MUST use the Public Suffix List including its PRIVATE section. Without it, shared-hosting tenants collapse to one registrable domain — victim.github.io and attacker.github.io both reduce to github.io — and a cross-tenant redirect would be wrongly followed and trusted as authoritative. The same applies to *.pages.dev, *.web.app, *.herokuapp.com, *.wordpress.com, etc. The reference impl uses tldts.getDomain(host, { allowPrivateDomains: true }).
Conformance
Gate on the shared cross-SDK vectors: static/test-vectors/adagents-discovery-redirects.json. They cover apex↔www, subdomain, co.uk (ICANN multi-label), *.github.io (PSL private section), same→cross two-hop (origin-anchored), HTTPS downgrade, hop cap, and the authoritative-location refuse-all rule. The normative assertion per vector is result (resolved vs refused).
This SDK (adcp-client-python)
Per the #5450 report, the .well-known/adagents.json fetch currently passes follow_redirects=False (in adcp/adagents.py), which is exactly what breaks apex→www publishers.
.well-known fetch → replace follow_redirects=False with a manual same-registrable-domain redirect loop: follow only same-eTLD+1, HTTPS-preserving, ≤3 hops, re-validating SSRF on each hop, anchored on the originally-requested domain; refuse cross-domain.
authoritative_location dereference → keep redirects refused.
- PSL with private domains: e.g.
tldextract with include_psl_private_domains=True, or publicsuffixlist/publicsuffix2 with private rules — so victim.github.io ≠ attacker.github.io.
Acceptance: the discovery path passes every vector in adagents-discovery-redirects.json.
Context
AdCP issue adcontextprotocol/adcp#5450 and merged spec PR adcontextprotocol/adcp#5472 define the redirect policy for
adagents.jsondiscovery. The reference validator inadcontextprotocol/adcpis now aligned; this SDK needs the same behavior so all AdCP implementations resolve discovery identically.The bug this fixes: a publisher whose apex domain
301-redirects towww(the default on most managed hosting / CDNs) is silently reported unauthorized by validators that refuse all redirects on the discovery fetch.Policy — two hops, deliberately different
https://{domain}/.well-known/adagents.jsonauthoritative_locationinstead.authoritative_locationdereference (2nd hop)Registrable-domain comparison MUST use the Public Suffix List including its PRIVATE section. Without it, shared-hosting tenants collapse to one registrable domain —
victim.github.ioandattacker.github.ioboth reduce togithub.io— and a cross-tenant redirect would be wrongly followed and trusted as authoritative. The same applies to*.pages.dev,*.web.app,*.herokuapp.com,*.wordpress.com, etc. The reference impl usestldts.getDomain(host, { allowPrivateDomains: true }).Conformance
Gate on the shared cross-SDK vectors:
static/test-vectors/adagents-discovery-redirects.json. They cover apex↔www, subdomain,co.uk(ICANN multi-label),*.github.io(PSL private section), same→cross two-hop (origin-anchored), HTTPS downgrade, hop cap, and the authoritative-location refuse-all rule. The normative assertion per vector isresult(resolved vs refused).This SDK (adcp-client-python)
Per the #5450 report, the
.well-known/adagents.jsonfetch currently passesfollow_redirects=False(inadcp/adagents.py), which is exactly what breaks apex→www publishers..well-knownfetch → replacefollow_redirects=Falsewith a manual same-registrable-domain redirect loop: follow only same-eTLD+1, HTTPS-preserving, ≤3 hops, re-validating SSRF on each hop, anchored on the originally-requested domain; refuse cross-domain.authoritative_locationdereference → keep redirects refused.tldextractwithinclude_psl_private_domains=True, orpublicsuffixlist/publicsuffix2with private rules — sovictim.github.io≠attacker.github.io.Acceptance: the discovery path passes every vector in
adagents-discovery-redirects.json.