- 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.
Docker tasks do not inherit the worker process environment, they only
get extra_env (-e). SEARXNG_URL in the worker unit EnvironmentFile
therefore never reached codex -> MCP inside the container, so the
auto chain skipped SearXNG and only DuckDuckGo was tried (live e2e:
'all search providers failed: duckduckgo (DNS)').
_docker_task_context now merges any set SEARXNG_URL /
PERPLEXITY_API_KEY / EXA_API_KEY / TAVILY_API_KEY / BRAVE_API_KEY
from the worker environment into extra_env (values never logged;
paid keys follow the same extra_env path as gateway keys).
Tests: 2 new docker-env tests; websearch + tenant isolation = 25
passing; full cloud suite 1038 passing (4 skipped).
Worker --websearch-mcp (default off) seeds tenant codex-home with
websearch MCP on _tenant_dirs (container path); old tenants merge
on next task. Autoscaler unit gains the flag. Tinyproxy filter
adds Perplexity/Exa/Tavily/Brave/DuckDuckGo + exact
searx.diante.me (no diante.me wildcard, control plane stays out).
Tests: test_websearch_worker 4 + websearch_mcp 12 + tenant 7 = 23 OK.
Not pushed, not merged, not deployed; SearXNG host/DNS/nginx and
tinyproxy reload are host steps.