feat: platform browser multi-tab, navigation allowlist, close cleanup, egress filter, allowlist worker wiring
- Multi-tab: browser_new_page / browser_list_pages / browser_switch_page / browser_close_page in playwright_mcp.py; each new page gets a fresh Playwright context, so cookies and storage are isolated between pages. Single-page tools act on the current page; switching to a missing index is an error, not a crash; closing the last page is an error (use browser_close). - Navigation allowlist: BROWSER_ALLOWED_HOSTS (comma list, leading-dot suffix match, case/port-insensitive) checked in _check_allowed_url before launching Chromium; unset (or empty) means no check, the old behaviour. file:// always allowed; localhost/127.0.0.1 have no special treatment. - Close cleanup: browser_close closes all pages and best-effort removes cwd browser-live.png / browser-live.json; custom screenshot files are untouched. list_pages needs no launch. - Egress filter: 10 programming-source Tinyproxy BASIC rules (python.org, nodejs.org, go.dev, pkg.go.dev, rust-lang.org, developer.mozilla.org, crates.io, stackoverflow.com, cve.mitre.org, nvd.nist.gov); already-present github.com / pypi.org / npmjs.org not duplicated; searx.diante.me stays exact-only, no diante.me wildcard. - Allowlist worker wiring: _docker_task_context now passes a set, non-empty BROWSER_ALLOWED_HOSTS from the worker env into [mcp_servers.playwright.env] in the tenant config, so the MCP process actually sees it (container -e alone does not reach MCP subprocesses, 2026-10-30). Unset/empty writes nothing and never overwrites; the value never goes into logs; it is not added to the websearch env block or the container env. ensure_playwright_mcp_env now returns False without rewriting when the result would be identical (canonical join: one blank line before the block), so a second prep leaves an unchanged tenant config alone; other sections untouched. - Skill + docs: browser-automation SKILL.md multi-tab/allowlist section; docs/platform-browser.md comparison doc (new file). - Tests (mock/fake only, no network, no real Chromium, no real credentials): cloud.tests.test_playwright + cloud.tests.test_websearch_worker, 48 tests OK, including 6 new wiring tests (switch off passes no variable, switch on reaches MCP env verbatim, empty never written, old-tenant merge idempotent + no-rewrite, ensure no-op when identical, allowlist enforces only after wiring). - NOT done (see PR body): no persistent profile, no credential vault, no approval card, no CAPTCHA bypass, in-page link navigation does not pass the allowlist, no real-Chromium live verification, host tinyproxy filter not reloaded.
This commit is contained in:
1 parent
27f48fe1ed
commit
da7a9950fb
8 files changed
+1050
-12
No files matched your search
+64
-1
@@ -16,7 +16,11 @@ proxy vars and PLAYWRIGHT_BROWSERS_PATH arrive via the MCP env
|
||||
block in config.toml, never via tool arguments.
|
||||
|
||||
One Browser instance lives for the whole MCP process: navigate
|
||||
once, then snapshot/click/fill/screenshot against the same page.
|
||||
once, then snapshot/click/fill/screenshot against the current
|
||||
page. browser_new_page / browser_list_pages /
|
||||
browser_switch_page / browser_close_page manage multiple pages;
|
||||
each new page gets a fresh Playwright context, so cookies are
|
||||
isolated between pages.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -217,6 +221,57 @@ _TOOLS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "browser_new_page",
|
||||
"description": (
|
||||
"Open a new page (tab) and make it current. Every "
|
||||
"new page gets a fresh Playwright context, so "
|
||||
"cookies and storage are isolated from other pages. "
|
||||
"Later navigate/snapshot/click/fill/screenshot act "
|
||||
"on this page until browser_switch_page."
|
||||
),
|
||||
"inputSchema": {"type": "object", "properties": {}},
|
||||
},
|
||||
{
|
||||
"name": "browser_list_pages",
|
||||
"description": (
|
||||
"List all open pages with index, URL, title and "
|
||||
"which one is current. Does not launch a browser "
|
||||
"when none is open."
|
||||
),
|
||||
"inputSchema": {"type": "object", "properties": {}},
|
||||
},
|
||||
{
|
||||
"name": "browser_switch_page",
|
||||
"description": (
|
||||
"Make page ``index`` (from browser_list_pages) "
|
||||
"current. Later single-page tools act on it. "
|
||||
"Switching to a missing index is an error, not a "
|
||||
"crash; refs from the previous page are dropped."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"index": {"type": "integer"},
|
||||
},
|
||||
"required": ["index"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "browser_close_page",
|
||||
"description": (
|
||||
"Close one page (``index``, or the current page "
|
||||
"when omitted). Closing the last page is an error; "
|
||||
"use browser_close for the whole browser. Closing "
|
||||
"the current page switches to the previous page."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"index": {"type": "integer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "browser_close",
|
||||
"description": "Close the browser page. Safe to call twice.",
|
||||
@@ -337,6 +392,14 @@ def handle_request(request, browser=None):
|
||||
elif name == "browser_screenshot":
|
||||
result = target.screenshot(
|
||||
args.get("output_filename"))
|
||||
elif name == "browser_new_page":
|
||||
result = target.new_page()
|
||||
elif name == "browser_list_pages":
|
||||
result = target.list_pages()
|
||||
elif name == "browser_switch_page":
|
||||
result = target.switch_page(args.get("index"))
|
||||
elif name == "browser_close_page":
|
||||
result = target.close_page(args.get("index"))
|
||||
elif name == "browser_close":
|
||||
result = target.close()
|
||||
else:
|
||||
|
||||
Reference in new issue
Block a user