Repository navigation
feat(website-browser): the relay's guard on every CDP command - #487
Merged
Merged
Conversation
An agent's Browser.close, Browser.crash or Browser.crashGpuProcess is
answered {} and ends only its own connection, with 1000, so the browser
stays up for the env and every other connection. Target.exposeDevToolsProtocol,
a browser context with its own proxy, and a navigation or new tab to the
browser's own pages (file:, chrome:, chrome-extension:, chrome-untrusted:,
view-source:, devtools: and about: but blank and srcdoc, read as the
browser reads a URL) get a CDP error saying agentenv refuses them.
Download behavior is sent on as agentenv's own, the browser's default into
~/Downloads, keeping only the context and whether to send events, so
Playwright's connect still works. When the deploy answers dialogs, an
agent's answer is answered {} and not sent on. Each connection's record
entry lists the first 100 commands the guard refused, changed, answered
or closed it on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rmed ones Target.sendMessageToTarget carries a whole command inside its message, so a file: navigation wrapped in it reached the browser unchecked. The relay serves flattened sessions only, so it's refused. A command whose params weren't an object, or whose method or sessionId wasn't a string, raised in the guard and got no answer, leaving its client to time out. It now gets a CDP error with code -32600, and so does a message that isn't an object. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its connection The relay sends nothing on once a connection starts closing, so when the test ended the connection before the relay had read its Hang command, Hang never reached the pipe and there was nothing to give up. CI's unit job hit that ordering. The test now waits until Hang is on the pipe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
The CDP relay from #482 now checks every command an agent sends before it reaches the browser's pipe. M8.17 hands these addresses to agents next. Without a guard, one agent's
Browser.closeends the browser for every connection and for the env. An agent could also route its traffic around the site proxy, save downloads outside the env's folder, or open the browser's own pages. This is FD-4954 (M8.7). Its five decisions are recorded there; this PR follows them as decided.What the guard does (in
browser_cdp.guard, a pure function the relay calls per command):Browser.close,Browser.crashandBrowser.crashGpuProcessare answered{}. Then that connection alone closes with 1000 (decision 2). To the client it looks like a real close. Its sessions are detached, and the tabs it opened stay open.{"code": -32000, "message": "agentenv refuses <method>: <why>"}(decision 1). Playwright and Puppeteer raise it with the reason, and the connection stays open.Target.exposeDevToolsProtocol. It gives a page's own script a browser-level CDP channel, which would get around the guard.Target.sendMessageToTarget. It carries a whole command inside itsmessage, where the guard can't see it, and the relay serves flattened sessions only.Target.createBrowserContextwithproxyServerorproxyBypassList.Page.navigateandTarget.createTargetto the browser's own pages. The URL is read the way the browser reads it: tabs and newlines removed, spaces and controls trimmed from its ends, the scheme in any case.file:,chrome:,chrome-extension:,view-source:anddevtools:.chrome-untrusted:, andabout:pages other thanabout:blankandabout:srcdoc, since the browser rewritesabout:settingstochrome://settings.Browser.setDownloadBehaviorand the olderPage.setDownloadBehaviorare sent on as agentenv's own setting:behavior: "default", with only the agent'sbrowserContextIdandeventsEnabledkept, so files land in~/Downloadsunder their real names;connectOverCDPsets download behavior while connecting (confirmed below), and its call still succeeds.dialogs: acceptordismiss), an agent'sPage.handleJavaScriptDialogis answered{}and not sent on. The deploy's answer is always the one Chromium gets, and the dialog record stays right. With the default,agent, nothing changes.cdp_connectionsentry gainsguarded: the first 100 commands the guard refused, changed, answered or closed it on, each with its method, what agentenv did and when. There's no action-log hook yet. M8.19 adds it together with the action log, its first consumer.It's a guardrail, not the boundary: the sandbox's network is the boundary. The README's relay section lists all of it.
Also:
method, and objectparamsand a stringsessionIdif any.Not in this PR:
Size: 206 lines added, 85 of them tests.
How it was tested
ruff check: clean.Protocol tests: 1057 passed at 831c670. The new ones in
test_browser_cdp.py:guard()on a table of commands.https:andabout:blankpass, whileabout:settings," \tFI\nLE:///…",chrome://settingsandView-Source:are refused. A context withproxyBypassListis refused, but one without a proxy is sent. An agent's dialog answer is sent when the deploy leaves dialogs to agents, andBrowser.crashcloses.exposeDevToolsProtocol, the answered dialog and the close never reach the pipe.{}when the deploy answers dialogs.Browser.closeis answered{}and closes only that connection, with 1000. The other connection keeps getting answers.guardedentries are checked.Real Chrome 156, locally, through the relay and Playwright 1.64.1 and Puppeteer 24.43.1:
http_urlwhile Puppeteer, byws_url, callsbrowser.close(). That returns cleanly, Chromium stays up, and Playwright then loads the site.page.goto("file:///etc/hostname")fails withProtocol error (Page.navigate): agentenv refuses Page.navigate: file: URLs open the browser's own files, settings and tools.Browser.setDownloadBehavior: changed, soconnectOverCDPdoes set download behavior, and the pinned call keeps it working.ws_url) still passes through the guard.Integration, on agent-environments-admin VMs (amd64 c6i.2xlarge, arm64 c7g.2xlarge): the browser computer's CDP test gains the same guard check. It also checks that the record shows Puppeteer's close, Playwright's refused navigation and its changed download call. The four suites,
browser_computer_test.py,computer_app_test.py,chromium_app_test.pyandwebsite_browser_test.py, passed 18 of 18 on each arch at dd20437. The guard check passed on both: Puppeteer's close returned cleanly, Chromium stayed up, Playwright loaded the site, and itsfile:navigation came back refused.Review fixes (Greptile, 3/5 on dd20437), at 831c670, each with a test that fails without it:
Target.sendMessageToTargetlet a wrappedfile:navigation past the guard. It's now refused.params, or a non-stringmethodorsessionId, raised in the guard and got no answer. On dd20437 the relay test hangs waiting for it. Such commands, and messages that aren't objects, now get a CDP error with code -32600.These change only the relay's command handling, covered by the protocol tests. The VM run is at dd20437.
CI on 831c670:
unitfailed in feat(website-browser): per-user CDP addresses, discovery and a WebSocket relay #482's relay test,assert [] == ['Hang']. The relay now sends nothing once a connection starts closing, and on CI the test ended the connection before the relay had read itsHangcommand, soHangnever reached the pipe. It passed 48 of 48 here even under load. A scratch test that ends the connection before the relay readsHangreproduces it. 0386620 makes the test wait untilHangis on the pipe.integration-local-slow (4/8)couldn't pullmirror.gcr.io/library/registry:2to start its local registry, so none of its tests ran.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge.Summary
The PR adds a guard to the browser’s CDP relay. Since the previous review, only the relay test changed.
Reviews (3) · Last reviewed commit: "test: wait for the unanswered command to..." · Reviewed by Greptile