Skip to content

feat(computer): a browser alone on the open web: every connection logged, every wait bounded (FD-4969, M8.21) - #588

Merged
earakely-scale merged 7 commits into
mainfrom
edgararakelyan/fd-4969-open-web-hosts
Nov 10, 2026
Merged

earakely-scale merged 7 commits into
mainfrom
edgararakelyan/fd-4969-open-web-hosts

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

M8.21 (FD-4969), the first step toward Online-Mind2Web. A browser running alone loads any public site reliably, and each connection it makes is logged. Design note: Open-web hosts in the site proxy (M8.21, FD-4969); the note is settled once you've reviewed it.

Revised in review: the proxies refuse nothing, as upstream Harbor's public network mode refuses nothing. The sandbox's network policy decides where a browser alone can go, as it did before this PR. The internal-address check, the own_loopback setting and the probe's override for it are gone (the note's decisions 2, 3 and 7, withdrawn).

Before this PR: a browser alone with no site routing goes out through the headless base's stand-in proxy on 3128, which already forwarded every host, but:

What changes

The stand-in (ubuntu-24.04-headless/site-proxy.py)

  • Relays with a selector (epoll on Linux), not select().
  • Waits: 30 s for the lookup and every connection attempt together; an idle connection is kept 300 s, and a write to a peer that stopped reading ends then too.
  • Answers a failure itself: a name that doesn't resolve, or a host that doesn't answer, is a 502 with X-Agentenv-Site-Proxy: failed.

Both proxies log the same JSON lines

  • connection, when one ends: host, port, address, bytes each way, start, duration.
  • failed, for one that couldn't open: method, host, port, reason (doesn't resolve or no answer) and the error.
  • Every line is JSON: the stand-in's startup line, and the shared proxy's refusals (refused), unexpected connection errors (error) and restarts (restarted), which were plain text.
  • The stand-in's log is /var/log/agentenv/site-proxy.log; the shared proxy's is /etc/agentenv/sites/site-proxy.log.
  • They can't drift: the two proxies can't share code (the stand-in is baked into the base image and starts before the deploy installs site_proxy.py), so one test sends the same success and failures through both and checks the records match.

Unchanged: HTTPS is still tunnelled with CONNECT and never decrypted; plain HTTP is forwarded. A synthetic site still wins its names.

Docs: the headless base in computers/README.md, and the website env entry in AGENTS.md.

Test plan

  • Unit:

    • test_computer_stand_in_proxy.py (the stand-in, one fake-network fixture for every case):
      • a public host tunnelled and forwarded, each connection logged;
      • nothing refused: the computer's own loopback and an internal address are reached alike;
      • a name that doesn't resolve answered with a logged 502;
      • a name's addresses tried in turn: the next one after a refusal, and all of them within one deadline when the first hangs;
      • a connection whose sockets are numbered past 1023 (it fails on the old select() relay);
      • a tunnel whose server stopped reading, ended within the idle timeout instead of waiting for ever.
    • test_computer_site_proxy.py:
      • a live site's host tunnelled and forwarded with each connection logged; one that doesn't answer in time is a logged 502;
      • refusals of hosts that are no site are JSON lines;
      • both proxies log a connection, a name that doesn't resolve and a host that doesn't answer in the same records.
    • Unit tiers: tst/unit/env, cli and the protocol tests pass here, apart from the Mac-only Windows disk-recipe test, which fails on main on this Mac too.
  • Integration (CI green on 80601db, 32 checks, the remote CodeBuild tier included): test_alone_on_the_open_web_it_loads_a_public_site_through_its_proxy_and_an_agents_tab_and_logs_each_connection, a browser computer with no site routing:

    • https://github.com/ loads through the stand-in and in a CDP agent's own tab;
    • a name that doesn't resolve is a 502;
    • the log holds the github.com connection and the failure.
    • The browser-computer tests that serve their pages on the computer's own loopback pass with no setting again.
    • The site-routing test reads the shared proxy's refusals as JSON lines.
  • VM e2e (c6i.4xlarge, amd64, local Docker), on d9e6e0a, before the log-format commit 80601db (its integration change, the routing test reading JSON refusals, runs in CI): 6 tests passed in 41 min (most of it waiting on archive.ubuntu.com, which was degraded):

  • VM chaos, one browser computer alone, passed:

    Phase Result
    The stand-in stopped a CDP agent's navigation gets net::ERR_PROXY_CONNECTION_FAILED: no fallback to a direct connection
    Started again github.com loads
    Killed with kill -9 supervisord starts it again; github.com loads, and a name that doesn't resolve is still a 502 at once
    A host that never answers (10.255.255.1:81) a 502 after 30 s, the connect deadline, never a hang
    localtest.me, a public name for 127.0.0.1 reaches a server on the computer's own loopback: nothing is refused
    20 fetches of github.com at once all 200
    A tunnel idle for 90 s, past the old 60 s still carries a TLS request: 200 Connection established, then 200 OK
    The log 22 github.com connections, localtest.me at 127.0.0.1, and both failures with their reasons

    The VM is torn down: instance terminated, security group and key pair deleted.

Not in this PR

  • chromium-open-web itself: headful, network allow-all, alone (M8.24).
  • The full request log of the traffic the proxy decrypts (M8.20, for WebArena-Verified).
  • One proxy instead of two: replacing the stand-in with the shared proxy means holding the browser's launch until the deploy installs it.
  • An immediate error for a host the sandbox's allowlist drops (M8.24).

🤖 Generated with Claude Code

…ernal address (FD-4969, M8.21)

A browser running alone goes out only through a proxy, and neither of its proxies checked where it connected: the
headless base's stand-in on 3128 forwarded every host, and the shared site proxy's forward port took a live site's
hosts to whatever their names resolved to.

- Both now resolve each name themselves and connect only to an address they checked (`allowed`): one that isn't
  globally routable, or is multicast, is refused at once with a 403, an IPv4-mapped, 6to4 or NAT64 address judged as
  the IPv4 address it carries. So loopback, private, link-local (the metadata service), shared and reserved addresses
  and their IPv6 equivalents are refused whether a page or a CDP agent names them, by address or by a name.
- HTTPS is still tunnelled with CONNECT, never decrypted; plain HTTP is forwarded.
- Each connection is a JSON line in the proxy's log when it ends (host, port, address, bytes each way, start,
  duration), and each refusal one with its reason.
- The stand-in waits 30 s to connect and keeps an idle connection 300 s (it dropped one at 60 s).
- The headless base's `own_loopback` setting, false by default, lets the browser reach the computer's own loopback,
  as the browser computer's tests, which serve their pages inside the computer, now set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale and others added 2 commits November 9, 2026 19:06
…ture on its own loopback

The probe serves its conformance fixture on the computer's loopback, which the headless base's stand-in now refuses,
so gate 3 left P3 to P9 unverified on every browser computer. A probe boots with the base's own_loopback on, where the
base declares it; a deploy still boots at the base's default, off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It waited on its two sockets with select(), which refuses a descriptor past 1023, so a proxy with that many files
open dropped every connection it opened after. It waits with a selector now, epoll on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale marked this pull request as ready for review November 10, 2026 03:51
@earakely-scale
earakely-scale requested a review from a team as a code owner November 10, 2026 03:51
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

@greptileai review

earakely-scale and others added 3 commits November 9, 2026 19:58
…and one deadline to connect

- The stand-in cleared its upstream socket's timeout once connected, so a write to a server that stopped reading
  could wait for ever. Both of a connection's sockets keep IDLE_S now, so that write ends and the tunnel closes.
- Each proxy gave every address its own connect timeout, and the shared one resolved outside any: a host of three
  addresses that never answer took three times as long to fail. The lookup and every attempt now share one deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or's public network does

The sandbox's network policy is what limits a browser alone, as it did before:
the stand-in proxy and the shared proxy's direct tunnel go back to taking any
host, the computer's own loopback and internal addresses included, so the
`own_loopback` setting and the probe's override for it go too. What stays: the
stand-in's selector relay, its one connect deadline and 300 s idle timeout, and
each proxy's JSON line for every connection and every one that fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale earakely-scale changed the title feat(computer): a browser alone reaches the open web but never an internal address (FD-4969, M8.21) feat(computer): a browser alone on the open web: every connection logged, every wait bounded (FD-4969, M8.21) Nov 10, 2026
The stand-in and the shared site proxy's live-host tunnel now write a failed
connection the same way (reason "doesn't resolve" or "no answer", plus the
error), and every line either proxy writes is JSON: the stand-in's startup
line, the shared proxy's refusals, unexpected connection errors and keep's
restarts included. One test sends the same success and failures through both
and checks the records match, so the two can't drift again.

Also: the shared proxy counts relayed bytes with a small Count instead of a
list and an index, through one relay loop; the stand-in's _open returns the
address it connected to instead of leaving it on the handler; the stand-in's
tests share one fake-network fixture and are renamed for what they cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 0376f42 into main Nov 10, 2026
36 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/fd-4969-open-web-hosts branch November 10, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant