Repository navigation
feat(gateway): site routes by real names (M8.12) - #627
Merged
earakely-scale merged 8 commits intoNov 12, 2026
Merged
Conversation
A site reached at one of its names through the gateway's /website/ route gets that name as its Host, so a site that builds its links and redirects from Host names itself as the browser reached it. Any other host leaves Host the site's internal address, so a forged one never reaches it; X-Forwarded-Host is unchanged. The names are the ones the site's env stores (WEBSITE_NAMES), else the ones its card declares, matched as a computer's site proxy matches them. WebSockets on the route follow the same rule. list_website_urls gives each site at its first name when the multi env's browser is a browser computer, which opens the sites there (WEBSITE_LISTED_BY_NAME), and at its internal address when it's playwright-mcp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AgentEnv-Site-Root that urlsplit can't parse (an unbalanced bracket) and a Host or X-Forwarded-Host that isn't printable ASCII, which httpx can't send upstream, each failed the request with a 500. Each now names nothing, as an empty one does: the request falls back to the next header and goes through. Found by M8.12's chaos fuzz; main answers the same 500s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names together The names a card declares were a side effect of the reserved-path read, and list_website_urls called that read only to fill them in. Now one cached read returns both, and each caller passes the names on explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#614's clock followers and M8.12's site names were added beside each other in the gateway's entrypoint and constructor; both are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and checks its own certificate The URI names the site, so websockets would default the port to 80 and verify TLS against the site's name; the upstream's port now defaults by its scheme, and a wss upstream is verified as the upstream. Also wraps two lines over 120 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ugh an environment proxy The relay's URI now names the site, so websockets' default proxy=True would decide no_proxy by the site's name and, with a proxy in the gateway's environment, tunnel to that name: the real domain, for a site named after one. Its upstream is always internal, so the relay connects directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y=None websockets 13 and 14 hand an unknown proxy argument to asyncio's create_connection, which raises TypeError, so every site WebSocket would fail there. The gateway image and the dev extra now ask for 15; the lock already resolves 17.2, so only its recorded floor changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
deleted the
edgararakelyan/fd-4959-m812-gateway-site-routes-the
branch
November 12, 2026 04:01
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.
What and why
M8.12 (FD-4959): the gateway routes sites by their real names.
Until now, a site behind the gateway's
/website/<name>/route always saw its internal address as itsHost,shop-website-backend:8000. A site that builds its links and redirects fromHost, as many real apps do, then sent chromium to an address chromium can't reach. For example,/loginredirected tohttps://shop-website-backend:8000/home.Sites built on agentenv-protocol's
WebsiteEnvironmentalready take their address fromX-Forwarded-Host, so they never showed this. Any other server did.Design note: M8.12: Gateway site routes by real names, with decisions 1a, 2a and 3a. It isn't settled until you've reviewed it.
What changes
Host. The host a client reached a site at is the one the gateway already forwards: a computer site proxy'sAgentEnv-Site-Rootorigin, else a front'sX-Forwarded-Host, else the request'sHost.Host. Case is ignored and the port is kept.Hostthe internal address, so a forged one never reaches the site.X-Forwarded-*is unchanged (decision 2a).websocketswritesHostfrom the URI, so the URI names the site and the TCP connection goes to the upstream./svc/<name>/is unchanged.WEBSITE_NAMES, else the ones its card declares.website/v1domains, read with the card the route already reads.@website(domains=...)sites declare their names only on the card, which is also where a computer's site table takes them from.*.name matches one label more, as the site proxy matches it.list_website_urlsfollows the run's browser (decision 1a).https://shop.test/, orhttp://for a.localhostname. The deploy names those sites inWEBSITE_LISTED_BY_NAME.playwright-mcp, or for a site deployed alone, sites keep their internal addresses, which is allplaywright-mcpcan reach.HEADalready worked, since Starlette adds it to any route that takesGET. A test now covers it.Decided while building, for your review
Host. A computer reaches such a site at a.localhostfallback name that depends on the run's other sites, which the gateway can't see. This is a known limitation.How it was tested
Hostrule:HEAD, case and port, a front'sX-Forwarded-Host, a wildcard, and forged origins, suffixes, ports and addresses;/svc/;list_website_urls: by name,.localhost, wildcard-only, card-only, and not listed.WEBSITE_NAMESparsing.test_docker_image_github_clone.py, which hangs here on a git credential helper. Neither touches this change.The fixture
tst/data/host_websiteis a plain Starlette server that redirects/logintohttps://<its Host>/home.Chromium, in a multi env, opens
https://shop.test/loginand lands onhttps://shop.test/home.list_website_urlsthrough the gateway's/mcpgives["https://shop.test/"].Asked straight at the gateway's route, the site sees
Shop.Testfor that origin, its internal address forhttps://evil.example, and its internal address with no origin named.X-Forwarded-Hostsays where each request came from.All of it holds again after a
docker restartof the gateway.Results:
browser_computer_test.py -k multi_envand M8.14's testmulti_env_computers_test.pygateway_test.py -k website(playwright-mcp, internal addresses)On amd64 the tiers ran again on the final code, after the second commit: 8 of 8 site and multi env browser tests, 4 of 4
multi_env_computers_test.py, 4 of 4gateway_test.py -k website. The third commit only names a constant.On arm64, two things the harness builds only for linux/amd64 couldn't run, since the VM has no emulation, and neither is touched here:
test_browser_connect_cdp_hands_an_agent_an_address_at_the_multi_envs_gateway_with_its_tools_hidden(tst/util/image_cache.pydefaults tolinux/amd64);gateway_test.py's gateway image (docker build --platform linux/amd64).Fuzz: 552 requests over raw sockets, each naming a host as
Host,X-Forwarded-HostorAgentEnv-Site-Root. The hosts included:SHOP.TEST,Shop.Test:443,shop.test:65535;%-escapes, non-ASCII and punycode names, 64-character labels and an 8000-character value;No answer was 5xx or hung. The site saw one of its names as
Hostonly when it was reached at that very name, and every good name came through as sent.Concurrency: 300 requests, 50 at a time, each naming a good name, a forged origin or none. Each one's
Hostwas its own.Site killed:
docker killon the site's container. Its route answered 502 at once, and the gateway kept listing it by name. Afterdocker start, the site was back in 1.2 s at its name, and a forged origin still got the internalHost.Gateway restarted under traffic: of about 120 requests, the roughly 10 sent during the restart got connection resets. None of the answered ones broke the rule. Afterwards chromium still landed on
https://shop.test/home, andlist_website_urlsstill gavehttps://shop.test/, including for the card-only site, whose card the restarted gateway read again.The fuzz first found 500s, and they were on main too. An
AgentEnv-Site-Rootthaturlsplitcan't parse (https://]x[) and a non-ASCIIHostorX-Forwarded-Host, which httpx can't send upstream, each failed the request.Fixed on the way
httpsupstream reached by name went to port 80 and checked the certificate against the site's name. It now defaults the port by the upstream's scheme, and verifies awssupstream as itself.proxy=None). With the site's name in the URI,websockets' defaultproxy=Truewould otherwise follow a proxy in the gateway's environment and tunnel to that name, which is the real domain for a site named after one. No agentenv deploy sets one today.proxy=Noneneeds websockets 15, so the gateway image and the dev extra now ask forwebsockets>=15. The lock already resolves 17.2, so only its recorded floor changes.SiteCard, its reserved paths and its names together, so the names are no longer a side effect of the reserved-path read.Not in scope
httplinks. Agents aren't affected.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; the previous WebSocket compatibility issue is fixed.Summary
The PR lets websites receive their declared names as
Hostand lists named URLs when the run uses a browser computer.Reviews (12) · Last reviewed commit: "Gateway: require websockets 15, the firs..." · Reviewed by Greptile