Skip to content

feat(computer): the website browser's control connection and its record of tabs, downloads, dialogs and launches - #459

Merged
earakely-scale merged 12 commits into
mainfrom
edgararakelyan/fd-4939-m510-the-browsers-control-connection-tab-and-download-record
Nov 3, 2026
Merged

earakely-scale merged 12 commits into
mainfrom
edgararakelyan/fd-4939-m510-the-browsers-control-connection-tab-and-download-record

Conversation

@earakely-scale

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

Copy link
Copy Markdown
Collaborator

What and why

This is M5.10 (FD-4939): the website browser's control connection, and the record it keeps. It was built on #453. #453 has merged (v0.9.1419), and this branch is now rebased onto main with only M5.10's commits. The decisions are in the design note. Edgar reviewed all seven on Nov 2, and 1be6c524 applies the outcome.

  • Events on the pipe. DevToolsPipe hands each event (a message with no id) to its listeners, sessionId included, and tells its closers when the browser closes the pipe.
  • BrowserControl (website_browser.py, self.control). At each launch, before the start tabs the command line can't open:
    • Target.setDiscoverTargets and a browser-level Target.setAutoAttach (flattened, pages only), with Page.enable on every page's session;
    • Browser.setDownloadBehavior with events, its behavior left at Chrome's default. Downloads still go unattended into ~/Downloads, where the managed policies send them, and a name downloaded again is kept beside the first (report (1).bin), not written over it.
  • What it records:
    • open tabs in opened order, each with active: the tab whose page has focus, else the first visible one, read when asked;
    • closed_tabs, each with closed_at and closed_by: agentenv if it closed the tab itself, unknown otherwise (CDP doesn't say who; M8.6's relay can name agents later), or exit or stop for tabs that went with the browser, as soon as its pipe closes;
    • downloads, with state and, once completed, the path Chrome reports (filePath), size and sha256, hashed in a thread;
    • dialogs, with type, message, answer and who answered.
  • dialogs, a new BrowserSettings field moved here from M5.9: agent (the default), accept or dismiss. agentenv answers on the dialog's own page session, and a prompt() gets its default text. Under accept and dismiss alike, a beforeunload ("leave this page?") is always answered Leave, so none of agentenv's answers blocks a navigation. The shipped chromium's [settings] spells it out.
  • hidden_page(): a page of agentenv's own (hidden: true) for reading state. It's attached, closed after use, and never in the record. M5.11 reads local storage through it.
  • The launch log is per boot and kept by the app runtime for every app, so a restarted server keeps it:
    • launch_app(cause) takes a cause: boot, server_restart, launch, data/add or data/reset;
    • each launch and each stop the server makes is appended to .agentenv/launches in the app's folder, beside its pid, which a boot starts fresh from the image;
    • a website browser logs an exit when its pipe closes without the server stopping it, with the exit code or the signal that ended it. Nothing relaunches it: in a computer the agent can, and alone it's an infra failure (M8.5).
  • browser/v1 record (GET) returns all of it, with launches and since, when this server started recording. Like every route but MCP and the card, it's framework-only. The card's browser/v1 params are unchanged; they now come from extension_params, beside the operation.
  • A fix in the data plane: data/get leaves out a file that a running app removes after it's listed and before it's read. Chromium does this to its metrics files, and it failed CI's gate 3 probe.

Not in this PR: typed browser/v1 operations (M5.11), the data plane (M5.13), per-agent closed_by (M8.6's relay), relaunch on a crash (M8.5), and per-context records (M8.15).

How it was tested

  • ruff check: clean.

  • Protocol and unit tests: 1101 passed (packages/agentenv-protocol/tests, tst/unit/env/envs/test_chromium_app.py, test_computer_env.py). New ones cover:

    • the record's tabs in opened order, closed_by for browser, agentenv and stop, a hidden page left out, and the attach calls in order;
    • accept, dismiss and agent dialogs: the answer sent on the page's session, and who answered;
    • a completed download's path, size and sha256;
    • the active tab, from focus and then visibility;
    • over a fake browser on the real pipe: an event reaching the record; the launch log's causes, its stop, and an exit when the browser is killed; and the killed browser's tab closed by exit at once.
  • Integration, slow tier, on agent-environments-admin VMs (amd64 c6i.2xlarge, arm64 c7g.2xlarge): all 10 passed on each, with every feat(computer): [settings] defaults for computer apps, and chromium on WebsiteBrowserEnvironment #453 test. The new chromium_app_test.py test deploys chromium with dialogs = accept on a loopback page and checks:

    • every run of the page's confirm() is accepted and recorded as answered by agentenv;
    • every run's download is completed with the right size and sha256;
    • a tab an agent opens and closes with Control+T and Control+W is in closed_tabs with closed_by: unknown, and the page's tab stays active;
    • killing Chromium logs an exit with signal: SIGKILL after the boot launch;
    • the menu launcher brings it back with a launch, and it's ready again;
    • killing its app server logs a server_restart.

    Chrome reloads an error page by itself once its server answers, so the page can run more than once. The first VM run showed two dialogs and two downloads, both correctly recorded, and the test now checks each run. The first run also put the launch log beside /run/agentenv/apps/'s launch marks, which broke two computer_app_test.py tests that list that folder; it's now in .agentenv/.

  • Greptile, round 1: three findings, all fixed in df66bef7:

    • a gone browser's tabs now close as soon as its pipe closes (exit or stop);
    • data launch causes are data/add and data/reset;
    • the two literal numbers are named.
  • After the review's decisions (965cfa34, before the rebase): all 10 integration tests passed on fresh amd64 and arm64 VMs again. The rebase onto main left every file this PR touches unchanged.

  • Greptile, round 2 (on the rebased head): session kept every finished process's reaping, and the test's waits used literal numbers. Both are fixed in 8fd4b81f.

  • CI on the rebased head failed one slow shard: M1.6's tab test, whose Control+Tab never reached Chromium on that runner, even with three presses. The window opened at boot didn't have the keyboard. The test now clicks into Chromium's window first, as an agent would, which 8fd4b81f also covers.

  • Keyboard focus on CI runners: after 8fd4b81f, my record test's Control+T and Control+W also missed Chromium on a runner. Main's own CI timed out the same way once in feat(computer): WebsiteBrowserEnvironment, the base class for website browsers #449's website_browser_test. Both browser tests now share one key helper (website_browser_test._keys), which clicks into Chromium's window before pressing anything (55958daf, c30e4219). session.stop() also drops a stopped process's exit (Greptile's follow-up).

  • Final head c30e4219: all 10 integration tests passed on fresh amd64 and arm64 VMs, and CI is 24/24. One slow shard needed a rerun: gate 3's input probe landed 10 of 16 simultaneous clicks on a busy runner. That's a computer-driver probe this PR doesn't touch, and it has flaked on main too.

  • Two checks added after the concerns review (659d6133):

    • dialogs = agent, chromium's default: Chrome shows the page's confirm(), and the record holds it unanswered. The test presses Enter as an agent would, and the record then shows it accepted with by: agent.
    • The same name downloaded once per run of the page.
  • Same-name downloads: the second check failed at first. Every run's download had the first file's path and hash.

    • 2e183beb guessed the saved file from its name and size. Greptile flagged that guess twice (P1s), and rightly.
    • A verbose VM run then showed the real cause. Chrome 156 does send filePath, but under setDownloadBehavior allow it writes a repeated name over the earlier file. Chrome's own behavior numbers it instead.
    • 72f36d07 keeps Chrome's default behavior, takes each path from filePath, and removes the guess. The test checks that the paths are distinct and that their hashes match what the page served.
  • On 72f36d07: all 11 integration tests passed on amd64 and arm64 VMs. That's the 10 from before, plus the agent-dialog test.

  • data/get on a running app (6969e500): CI's slow shard then failed at gate 3. Its probe reads chromium's profile with data/get while chromium runs, and Chrome deleted a BrowserMetrics file between the listing and the read, so the whole export failed with ENOENT. The race was already on main. This PR doesn't touch the data plane, but it showed up here. A file gone by the time it's read is now left out, inline or uploaded, with a protocol test for each path.

  • On 6969e500: all 11 integration tests passed again on both VMs.

  • Greptile, round 3: its two P1s were on the download guess, which 72f36d07 removed. Its waits finding was only half fixed in 8fd4b81f: the chromium test's _record still used literal numbers. They're named in c568d9c0, a test-only change.

  • Final head c568d9c0: CI is 24/24, and Greptile is 5/5 with no open findings.

  • Every VM, its security group and key, and the CodeArtifact token are gone.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge, with no new actionable issues found.

Summary

The PR adds a browser control connection, browser records, and a launch log that survives app-server restarts.

  • The website browser keeps a record of its tabs, downloads, and launches.
  • Browser dialogs follow a setting and record who answered.
  • App launches, stops, and exits appear in a per-boot log.
  • Data exports skip files that disappear while they are being read.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Chromium -->|events| DevToolsPipe
  DevToolsPipe --> BrowserControl
  BrowserControl -->|tabs, downloads, dialogs| Record
  AppRuntime -->|launches, stops, exits| Log[Launch log]
  Log --> Record
  Record --> Route["browser/v1 record"]
Loading

Reviews (12) · Last reviewed commit: "test(computer): name the chromium record..." · Reviewed by Greptile

@earakely-scale
earakely-scale requested a review from a team as a code owner November 3, 2026 00:42
Base automatically changed from edgararakelyan/fd-4938-m59-chromium-on-websitebrowserenvironment to main November 3, 2026 01:27
earakely-scale and others added 4 commits November 2, 2026 18:58
…rd of tabs, downloads, dialogs and launches

agentenv's pipe to a website browser now carries events. At each launch its BrowserControl attaches to every page with
flattened sessions and records the open tabs in opened order and which is active, the tabs closed and who closed them,
downloads with their size and sha256, and JavaScript dialogs, which the new dialogs setting has agentenv accept or
dismiss. The app runtime logs each launch with its cause, each stop it made and each exit it didn't, per boot, and the
browser serves all of it as browser/v1 record. hidden_page() gives agentenv a page of its own for reading state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…data causes as data/add and data/reset

The record kept a killed browser's tabs open until the next launch; they now close as soon as its pipe does, by exit
or by its server's stop. A launch after a data call logs the public data/add or data/reset, the launch log moves into
the app's .agentenv folder beside its pid, and the active-tab check's wait and the hash's chunk size are named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is recorded alike

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and when the record starts

From the review of the design: a tab agentenv didn't close is closed_by unknown; an exit is logged with its code or
signal, and nothing relaunches the browser; agentenv's dialog answers always leave a page asking before it's left, so
dismiss never blocks a navigation; and the record says since when it covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale force-pushed the edgararakelyan/fd-4939-m510-the-browsers-control-connection-tab-and-download-record branch from 965cfa3 to 1be6c52 Compare November 3, 2026 02:59
Comment on lines +432 to +435
for _ in range(100):
if await browser.devtools_answers():
return True
await asyncio.sleep(0.05)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test waits lack names

The new _answering helper uses 100 retries and a 0.05-second delay without descriptive names. _record in tst/integration/computer/chromium_app_test.py repeats this with 60 and 1.

The repository requires descriptive names for magic numbers. Name these limits and delays so their purpose and units are clear. This requirement must be satisfied before merging.

Rule Used: Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. (source)

Learned From
scaleapi/scaleapi#126388

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/tests/test_website_browser.py
Line: 432-435

Comment:
**Test waits lack names**

The new `_answering` helper uses `100` retries and a `0.05`-second delay without descriptive names. `_record` in `tst/integration/computer/chromium_app_test.py` repeats this with `60` and `1`.

The repository requires descriptive names for magic numbers. Name these limits and delays so their purpose and units are clear. This requirement must be satisfied before merging.

**Rule Used:** Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. ([source](https://app.greptile.com/-/switch-tenant?tenant=scale-ai&next=%2Fcustom-context%3Fmemory%3D002e0051-41ad-46c1-9098-47433c580150))

**Learned From**
[scaleapi/scaleapi#126388](https://github.com/scaleapi/scaleapi/pull/126388)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8fd4b81: the test waits use WAIT_S and POLL_S, and the kill uses signal.SIGKILL.

@earakely-scale earakely-scale Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8fd4b81f only named the protocol test's waits; _record in chromium_app_test.py still used 60 and 1. Fixed in c568d9c0: RECORD_WAIT_S and RECORD_POLL_S for the record, SERVE_START_S for the page server's start, and TAB_PRESSES for _tab_to.

earakely-scale and others added 5 commits November 2, 2026 19:15
…omium before its tab test's Control+Tab

session kept every launched process's reaping until its exit was asked for; it now drops all but the latest 16 no one
asked about. The tab test clicks into Chromium's window before pressing Control+Tab, since on a CI runner the window
opened at boot didn't have the keyboard and no press reached it; the test's waits are named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tests click into its window before every key press

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…window first, for website_browser_test too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-name downloads are each hashed at their own path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch Chrome 156 doesn't report

Chrome 156 sends no filePath with a completed download, so the record fell back to ~/Downloads/<suggested name> and
gave every later download of the same name the first file's path and hash. A completed download with no path now
takes the newest file named as Chrome names repeats (report.bin, report (1).bin, ...) that no other download holds and
that was written since it began, of the size it received; it's chosen as the download completes, so two can't claim
one file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale and others added 3 commits November 2, 2026 21:31
…wnload's path from Chrome

The control connection set Browser.setDownloadBehavior to allow, under which Chrome overwrites a download of a name it
already saved; on its own Chrome keeps both, report.bin and report (1).bin. It now asks only for the events, with
behavior default, so downloads go where the managed policies send them, as Chrome saves them. Chrome reports each
completed download's filePath, which the record takes; with none it records no path rather than a guess, so the
name-matching fallback is gone, and BrowserControl no longer takes a downloads folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e it's read

data/get lists the files under its paths and then reads or uploads each, while the app keeps running. Chromium
deletes a BrowserMetrics file soon after it starts, and CI's gate 3 probe read the profile just then, so the whole
export failed with ENOENT. A file gone by the time it's read is now left out, inline or uploaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale merged commit 9b59f18 into main Nov 3, 2026
27 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/fd-4939-m510-the-browsers-control-connection-tab-and-download-record branch November 3, 2026 06:31
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