fix(governance): define adagents.json discovery redirect policy (#5450)#5472
Merged
Conversation
The initial /.well-known/adagents.json fetch now follows same-registrable-domain redirects (apex↔www, HTTPS-preserving, ≤3 hops, SSRF re-validated per hop, anchored on the originally-requested domain and using the PSL private section so shared-hosting tenants like *.github.io are distinct registrants) so standard apex→www managed hosting resolves instead of being silently reported unauthorized. Cross-registrable-domain redirects are refused — declare delegation with authoritative_location instead — and the authoritative_location dereference refuses all redirects. Reconciles the reference validator with the spec: the /.well-known fetch was following redirects to any public host and the authoritative_location dereference likewise; both are now policy-correct. Adds cross-SDK conformance vectors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Approving. The right fix for a documented three-way contradiction — and it lands the policy as a bounded same-registrable-domain carve-out, not a blanket loosening, so the SSRF posture tightens rather than regresses.
Things I checked
- Origin anchoring is correct.
originRegistrableDomainis computed once fromparsedUrl.hostnamebefore the redirect loop (url-security.ts:190) and every per-hop check compares against that frozen value, nevercurrentUrl. Thesame → crosstwo-hop chain (pub.example → www.pub.example → attacker.example) is refused at hop 2 — covered by both theanchored-on-originunit test and vectorwell-known-same-then-cross-refused-anchored-on-origin. - Fails closed on every null/edge case.
if (!originRegistrableDomain || targetRegistrableDomain !== originRegistrableDomain) throw— IP-literal origin, bare-suffix target, both-null all refuse the redirect. No fail-open path.security-reviewerconfirmed no High/Medium/Low. - Per-hop SSRF re-validation intact.
validateRedirectTarget→validateFetchUrl→ reserved-range + DNS-rebind pin still fires on every hop before any dial; the same-site/HTTPS gates layer on top, not in place of it. Widening to same-eTLD+1 does not relax reserved-range defense. - HTTPS preservation. Asserted on hop 0 (
url-security.ts:179) and per-hop before the domain check, so anhttps→httpsame-domain hop is still refused (vectorwell-known-https-downgrade-refused). allowPrivateDomains:trueis load-bearing. Without itgetDomaincollapsesvictim.github.ioandattacker.github.iotogithub.ioand a cross-tenant redirect is wrongly trusted. With it they're distinct registrants — refused. Both vectors + unit tests cover the private-suffix boundary.- Authoritative-location hop refuses all redirects.
maxRedirects: 0(adagents-manager.ts:117) returns the raw 3xx, then the new branch maps[301,302,303,307,308]to anauthoritative_locationrefuse error (adagents-manager.ts:136-138). Hop-cap exceeded on the well-known fetch returns the terminal 301, whichresponse.status !== 200classifies as not-valid — matches vectorwell-known-hop-cap-exceeded-refused. - Downstream trust stays pinned right.
security-reviewerconfirmed authorization (hasExplicitPublisherScope,authorized_agents/properties) evaluates againstnormalizedDomain— the originally-requested publisher domain — neverresolved_url. Lettingwww.publisher.comanswer forpublisher.comcannot let any other registrant author the publisher's authorization, because the body can only come from the same eTLD+1. - No schema drift.
static/schemas/source/adagents.json:19authoritative_locationdescription ("refuse redirects on the fetch") scopes the second-hop dereference, which the new policy still enforces. Leaving it untouched is correct —ad-tech-protocol-expert: sound. - Changeset present,
minoris right. Document shape unchanged; this only widens/narrows which redirects a validator follows.tldtsadded topackage.json+package-lock.jsonat the existing resolved7.0.30. - Test-plan honesty. PR claims 11 redirect unit tests + 154 url-security/adagents tests pass, typecheck clean. No unchecked manual-verification boxes describing the changed path.
Follow-ups (non-blocking — file as issues)
- Sibling SDKs are unaligned until their PRs land. Until
adcp-client-pythonflipsfollow_redirects=Falseandadcp-go/adcp-clientadopt the same-domain-follow / authoritative-refuse behavior against the shared vector, a correctly-configured apex→www publisher is still reported unauthorized by those validators. The vector file is the contract; the cross-SDK PRs are the deliverable. - Hop-cap vector wording.
well-known-hop-cap-exceeded-refusedexpectsreason: too_many_redirects, but the TS reference returns the terminal 3xx and lets the caller reject it rather than throwing. Not a contradiction (result: refusedmeans "MUST NOT resolve to a usable 200"), but a sibling author reading only the vector could implement a throw. Worth a one-line note thatrefusedis a validator-layer outcome, not a transport-primitive throw.
Minor nits (non-blocking)
- DNS lookup precedes the refusal.
url-security.ts:409—validateRedirectTarget(which resolves DNS) runs before the cheap synchronous same-site/scheme checks. A cross-domain hop is still refused with no connection, but reordering the same-site/scheme guards ahead ofvalidateRedirectTargetwould avoid a needless lookup of the attacker-named host and tighten the "MUST NOT dial the off-domain hop" intent. - Refusal message bucketing.
classifySafeFetchErrorfiles thecross-registrable-domainthrow under thenetworkbucket and surfaces it verbatim. Hosts are publisher-controlled so it's not the internal-topology leak the function guards against — but a dedicated pattern would read more consistently with the other AAO buckets.
code-reviewer: clean. security-reviewer: sound, not an SSRF regression. ad-tech-protocol-expert: sound-with-caveats (the hop-cap wording note above).
Safe to merge.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
This was referenced Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves #5450. Defines the redirect policy for
adagents.jsondiscovery and reconciles the reference validator with it.The current state was a three-way disagreement that silently broke correctly-configured publishers:
security.mdxSSRF step 4) had a blanket "refuse all redirects on counterparty-controlled fetches," whilemanaged-networks.mdxsaid redirects on the.well-knownfetch were "not prohibited" — a self-contradiction.301s towww(the managed-hosting/CDN default) is reported unauthorized..well-knownfetch and theauthoritative_locationdereference — the latter directly violating the spec's "refuse redirects" rule for that hop.Policy (two hops, deliberately different)
/.well-known/adagents.jsonauthoritative_locationinstead.authoritative_locationdereference (2nd hop)This mirrors the existing bounded redirect carve-out for
brand.jsonresolution ("one redirect, no chains"), so it's a precedented exception, not a new posture.Registrable-domain matching uses the PSL including its private section (
tldts.getDomain(..., { allowPrivateDomains: true })) so shared-hosting tenants likevictim.github.ioandattacker.github.ioare distinct registrants — without that, a cross-tenant redirect on*.github.io/*.pages.dev/ etc. would be wrongly trusted.Changes
managed-networks.mdx"Why not HTTP redirects?" rewritten into the precise two-part rule;security.mdxSSRF step 4 + outbound-TLS bullet get the bounded same-domain exception. Schemaadagents.jsondescription untouched — its "refuse redirects" caveat is correct for theauthoritative_locationhop.url-security.tsaddssameSiteRedirectsOnlytosafeFetch/safeFetchAxiosLike;adagents-manager.tswires the well-known fetch to{ sameSiteRedirectsOnly: true, maxRedirects: 3 }and theauthoritative_locationfetch to{ maxRedirects: 0 }.static/test-vectors/adagents-discovery-redirects.json— cross-SDK vectors covering 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.tldtspromoted to a direct dependency (resolution unchanged at7.0.30);minorchangeset.Wire impact
None — the
adagents.jsondocument shape is unchanged. This only widens/narrows which redirects a validator follows, so it's wire-compatible (minor). Targets the 3.1 line.Follow-up (sibling SDK PRs)
The reference validator here is aligned; coordinated PRs to bring
adcp-client,adcp-client-python(flipfollow_redirects=False), andadcp-goto the same same-domain-follow / authoritative-refuse behavior against the shared test-vector are tracked separately.Review
Passed an adversarial security + correctness review (found and fixed: the
allowPrivateDomainsshared-hosting bypass, missing PSL/hop-cap test coverage, and a stale branch base). 11 redirect unit tests + 154 url-security/adagents tests pass; typecheck clean.🤖 Generated with Claude Code