From 8a7179838a1e05779ab3fc3fd5faf35c00a6f8d8 Mon Sep 17 00:00:00 2001 From: Minh Trinh Date: Thu, 16 Jul 2026 17:07:04 -0700 Subject: [PATCH 1/2] Replace the agentic browser fleet's Chromium engine with CloakBrowser The fleet now launches CloakBrowser (a from-source C++/Blink/V8 stealth- patched Chromium fork) instead of Playwright's own managed Chromium -- session.py points browser_use at a fixed /opt/cloakbrowser/chrome instead of playwright.chromium.executable_path. deferred_install.sh installs it on first boot: apt-only playwright install-deps for shared libs, then a pinned, SHA256-verified per-arch (Linux x64/arm64) CloakBrowser release, unpacked to /opt/cloakbrowser/. Playwright's own managed-Chromium download is dropped entirely -- any agent's own direct Playwright calls also use CloakBrowser now (pass executable_path explicitly; see the updated CLAUDE.md guidance), so there's one Chromium binary in the image, not two. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 +- libs/bootstrap/README.md | 12 ++- libs/browser/README.md | 15 ++-- libs/browser/browser_test.py | 6 +- libs/browser/src/browser/session.py | 22 ++++-- libs/browser/test_browser_integration.py | 2 +- scripts/deferred_install.sh | 97 ++++++++++++++++++++---- 7 files changed, 122 insertions(+), 34 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 985c70835..d1e0b644d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,7 +82,7 @@ Only after doing all of the above should you begin writing code. # Important commands and conventions: - Never run `uv sync`, always run `uv sync --all-packages` instead -- For browser automation, Playwright's Python API is in the root venv (`from playwright.sync_api import sync_playwright`, run via `uv run python`). Chromium installs asynchronously on first container boot (the one-shot `deferred-install` program), so in a fresh workspace confirm it finished -- `supervisorctl status deferred-install` or the marker `/var/lib/minds/deferred-install/done.playwright` -- before launching, or the launch fails with a clear error. It runs as-is under the docker provider's gVisor runtime; if you hit a "No usable sandbox!" error on a runtime without unprivileged user namespaces, pass `chromium.launch(args=["--no-sandbox"])`. See `libs/bootstrap/README.md` for the full deferral contract. +- For browser automation, Playwright's Python API is in the root venv (`from playwright.sync_api import sync_playwright`, run via `uv run python`). The engine is CloakBrowser (a stealth-patched Chromium fork), not Playwright's own managed Chromium -- pass `executable_path="/opt/cloakbrowser/chrome"` explicitly to `chromium.launch(...)`, since Playwright's own browser-cache lookup only auto-discovers builds it downloaded itself. It installs asynchronously on first container boot (the one-shot `deferred-install` program), so in a fresh workspace confirm it finished -- `supervisorctl status deferred-install` or the marker `/var/lib/minds/deferred-install/done.cloakbrowser` -- before launching, or the launch fails with a clear error. It runs as-is under the docker provider's gVisor runtime; if you hit a "No usable sandbox!" error on a runtime without unprivileged user namespaces, pass `chromium.launch(executable_path="/opt/cloakbrowser/chrome", args=["--no-sandbox"])`. See `libs/bootstrap/README.md` for the full deferral contract. # Always remember these guidelines: diff --git a/libs/bootstrap/README.md b/libs/bootstrap/README.md index 3c70b81ff..72255f125 100644 --- a/libs/bootstrap/README.md +++ b/libs/bootstrap/README.md @@ -50,8 +50,16 @@ To add, change, or remove a service, edit `supervisord.conf` and run The `deferred-install` program in `supervisord.conf` runs `scripts/deferred_install.sh`, which installs packages that are too heavy to bake into the Docker image but aren't required by any boot-time service. -Currently it covers Playwright's Chromium browser + its apt system libraries -(`uv run playwright install --with-deps chromium`). +Currently it covers: + +- Chromium's apt system libraries (`uv run playwright install-deps chromium` + -- libs only, no browser download). +- CloakBrowser, a from-source C++ (Blink/V8) stealth-patched Chromium fork, + fetched from a pinned GitHub release + SHA256-verified per-arch (Linux + x64/arm64), unpacked to `/opt/cloakbrowser/`. This is the one Chromium + binary in the image -- both the agentic browser fleet and any agent's own + direct Playwright calls (`chromium.launch(executable_path=...)`) use it; + Playwright's own managed-Chromium download is not installed. It is a one-shot supervisord program (`autorestart=false`, `startsecs=0`, `exitcodes=0`): supervisord starts it once on boot and leaves it stopped after a diff --git a/libs/browser/README.md b/libs/browser/README.md index 226af7163..e9d709a78 100644 --- a/libs/browser/README.md +++ b/libs/browser/README.md @@ -8,12 +8,15 @@ agent, identified by its `MNGR_AGENT_ID`, or the human). thread-per-connection) that owns every browser. browser_use, Playwright (async), and the per-browser ownership state machine run on one background asyncio event loop, reached from the Flask threads through a single `run_coroutine_threadsafe` - bridge. Each browser is a headless Chromium driven by `browser_use.BrowserSession`, observed - over the same CDP endpoint to stream a live view (`Page.startScreencast` -> - base64 JPEG frames over a WebSocket) and inject human input. Each browser is - addressed by a random ~2-word english NAME (e.g. `alex-smith`), generated on - demand and never reused; the fleet starts empty and there is no default - browser. + bridge. Each browser is a headless CloakBrowser -- a from-source C++ + (Blink/V8) stealth-patched Chromium fork at `/opt/cloakbrowser/chrome` (see + `scripts/deferred_install.sh`), not Playwright's own managed Chromium -- + driven by `browser_use.BrowserSession` (`executable_path` pinned to that + fixed path), observed over the same CDP endpoint to stream a live view + (`Page.startScreencast` -> base64 JPEG frames over a WebSocket) and inject + human input. Each browser is addressed by a random ~2-word english NAME + (e.g. `alex-smith`), generated on demand and never reused; the fleet starts + empty and there is no default browser. - **Ownership** is one locked, compare-and-set state machine per browser. Agents never preempt each other -- a second agent waits in a FIFO queue (monitor-and-wait). The human can take control from the UI at any time, which diff --git a/libs/browser/browser_test.py b/libs/browser/browser_test.py index e61245af1..71431316a 100644 --- a/libs/browser/browser_test.py +++ b/libs/browser/browser_test.py @@ -76,11 +76,11 @@ def test_deferred_install_ready_gates_on_marker( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: monkeypatch.delenv("BROWSER_SKIP_INSTALL_CHECK", raising=False) - play = tmp_path / "done.playwright" - monkeypatch.setattr(bsession, "_PLAYWRIGHT_MARKER", play) + marker = tmp_path / "done.cloakbrowser" + monkeypatch.setattr(bsession, "_CLOAKBROWSER_MARKER", marker) ready, _ = bsession.deferred_install_ready() assert ready is False - play.write_text("") + marker.write_text("") ready, reason = bsession.deferred_install_ready() assert ready is True assert reason == "ready" diff --git a/libs/browser/src/browser/session.py b/libs/browser/src/browser/session.py index 3b20d5416..20727cd92 100644 --- a/libs/browser/src/browser/session.py +++ b/libs/browser/src/browser/session.py @@ -104,10 +104,16 @@ # tab feels snappier. Slightly more bandwidth than skipping frames. _SCREENCAST_EVERY_NTH_FRAME = 1 -# Deferred-install marker (see scripts/deferred_install.sh). Chromium installs -# asynchronously on first container boot; launching a browser before it exists -# fails, so callers gate on this. No Xvfb: CDP streaming/input are headless. -_PLAYWRIGHT_MARKER = Path("/var/lib/minds/deferred-install/done.playwright") +# Deferred-install marker (see scripts/deferred_install.sh). CloakBrowser +# installs asynchronously on first container boot; launching a browser before +# it exists fails, so callers gate on this. No Xvfb: CDP streaming/input are +# headless. +_CLOAKBROWSER_MARKER = Path("/var/lib/minds/deferred-install/done.cloakbrowser") + +# CloakBrowser's fixed install path (see scripts/deferred_install.sh's +# _install_cloakbrowser). A stealth, C++-patched Chromium fork -- replaces +# vanilla Chromium as the engine for every browser the fleet launches. +_CLOAKBROWSER_EXECUTABLE = "/opt/cloakbrowser/chrome" # Default model. browser-use's own default LLM is ChatBrowserUse (its hosted # model), so to drive with the user's Anthropic key we pass ChatAnthropic @@ -328,7 +334,7 @@ def deferred_install_ready() -> tuple[bool, str]: """Return ``(ready, reason)`` once Chromium is installed.""" if os.environ.get("BROWSER_SKIP_INSTALL_CHECK") == "1": return True, "ready" # host/CI testing without the deferred-install marker - if not _PLAYWRIGHT_MARKER.exists(): + if not _CLOAKBROWSER_MARKER.exists(): return False, "Chromium is still installing in this workspace; try again in a minute." return True, "ready" @@ -541,7 +547,11 @@ async def start( """ self._playwright = playwright self._input_enabled.set() - chromium_path = playwright.chromium.executable_path + # Fixed CloakBrowser path, not playwright.chromium.executable_path -- + # the fleet's engine is CloakBrowser, not Playwright's own managed + # Chromium (which vanilla Playwright calls elsewhere in this image + # still use). See _CLOAKBROWSER_EXECUTABLE. + chromium_path = _CLOAKBROWSER_EXECUTABLE profile_dir = _profile_dir(self.browser_id) profile_dir.mkdir(parents=True, exist_ok=True) _clear_stale_singleton(profile_dir) # a prior hard kill may have orphaned a lock diff --git a/libs/browser/test_browser_integration.py b/libs/browser/test_browser_integration.py index 6c5b8746c..f20d2124b 100644 --- a/libs/browser/test_browser_integration.py +++ b/libs/browser/test_browser_integration.py @@ -415,7 +415,7 @@ def test_http_release_requires_ownership(monkeypatch: pytest.MonkeyPatch) -> Non def test_http_new_browser_blocked_until_chromium_installed(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("BROWSER_SKIP_INSTALL_CHECK", raising=False) - monkeypatch.setattr(bsession, "_PLAYWRIGHT_MARKER", bsession.Path("/nonexistent/marker")) + monkeypatch.setattr(bsession, "_CLOAKBROWSER_MARKER", bsession.Path("/nonexistent/marker")) client = runner.application.test_client() resp = client.post("/browsers") assert resp.status_code == 503 diff --git a/scripts/deferred_install.sh b/scripts/deferred_install.sh index c1e1bf8ec..e4b5a7050 100755 --- a/scripts/deferred_install.sh +++ b/scripts/deferred_install.sh @@ -66,35 +66,102 @@ _recover_interrupted_dpkg() { fi } -_install_playwright() { +_install_playwright_deps() { local marker - marker="$(_marker_for playwright)" + marker="$(_marker_for playwright_deps)" if [ -f "$marker" ]; then - _log "playwright: marker present at $marker, skipping" + _log "playwright_deps: marker present at $marker, skipping" return 0 fi - # `playwright install --with-deps` shells out to apt; recover any - # interrupted dpkg state first so an install the bake interrupted can - # actually complete on retry. + # `install-deps` only apt-installs the shared system libraries a Chromium + # build needs (libnss3, libgbm, etc.) -- it does not download a browser + # binary. Both engines below are Chromium builds and need these libs, so + # this runs once, ahead of either. Recover any interrupted dpkg state + # first so an install the bake interrupted can actually complete on retry. _recover_interrupted_dpkg - _log "playwright: installing chromium + apt system libs (this may take a few minutes)" - # `--with-deps` apt-installs the system libraries chromium needs. - # `uv run` uses the workspace venv (the playwright Python wheel is - # already installed via the root pyproject.toml's pin). Subshell so - # the cwd change does not leak to other `_install_` functions. - if (cd "$REPO_ROOT" && uv run playwright install --with-deps chromium); then + _log "playwright_deps: installing apt system libs (this may take a few minutes)" + if (cd "$REPO_ROOT" && uv run playwright install-deps chromium); then touch "$marker" - _log "playwright: install complete, marker written to $marker" + _log "playwright_deps: install complete, marker written to $marker" else - _log "playwright: install FAILED; marker not written so the next boot retries" + _log "playwright_deps: install FAILED; marker not written so the next boot retries" return 1 fi } +# CloakBrowser is a from-source C++ (Blink/V8) stealth patch of Chromium -- +# used everywhere a Chromium binary is launched in this image (the agentic +# browser fleet, and any agent's own direct Playwright calls), replacing +# vanilla Chromium entirely. See libs/browser/README.md. +# +# Pinned to a specific free-tier release (not `latest`): CloakBrowser's +# newest major version is gated behind a paid tier, so `latest` would 404 on +# the asset for an unpaid image. Bump deliberately by updating these three +# vars together (and the SHA256s below) -- never silently drift on restart, +# matching every other deferred package's contract. +readonly _CLOAKBROWSER_VERSION="chromium-v146.0.7680.177.4" +readonly _CLOAKBROWSER_INSTALL_DIR="/opt/cloakbrowser" +readonly _CLOAKBROWSER_RELEASE_URL="https://github.com/CloakHQ/CloakBrowser/releases/download/${_CLOAKBROWSER_VERSION}" +# From that release's SHA256SUMS; recompute + update on every version bump. +readonly _CLOAKBROWSER_SHA256_ARM64="8b71ce53b4fd131327331a31fba3835d71882d19bfaabde78dd0f5390bd16f45" +readonly _CLOAKBROWSER_SHA256_X64="5af027faafb1fef9933eb784c094b764706de22a372a2cee84bc117fc4ab537f" + +_cloakbrowser_asset_for_arch() { + # Maps `uname -m` to CloakBrowser's release asset naming + pinned hash. + case "$(uname -m)" in + aarch64|arm64) + printf 'cloakbrowser-linux-arm64.tar.gz %s\n' "$_CLOAKBROWSER_SHA256_ARM64" + ;; + x86_64|amd64) + printf 'cloakbrowser-linux-x64.tar.gz %s\n' "$_CLOAKBROWSER_SHA256_X64" + ;; + *) + _log "cloakbrowser: unsupported architecture $(uname -m)" + return 1 + ;; + esac +} + +_install_cloakbrowser() { + local marker + marker="$(_marker_for cloakbrowser)" + if [ -f "$marker" ]; then + _log "cloakbrowser: marker present at $marker, skipping" + return 0 + fi + local asset expected_sha256 + read -r asset expected_sha256 < <(_cloakbrowser_asset_for_arch) || return 1 + _log "cloakbrowser: downloading ${_CLOAKBROWSER_VERSION}/${asset}" + local tmp_dir + tmp_dir="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '$tmp_dir'" RETURN + if ! curl -fsSL -o "$tmp_dir/$asset" "${_CLOAKBROWSER_RELEASE_URL}/${asset}"; then + _log "cloakbrowser: download FAILED; marker not written so the next boot retries" + return 1 + fi + local actual_sha256 + actual_sha256="$(sha256sum "$tmp_dir/$asset" | awk '{print $1}')" + if [ "$actual_sha256" != "$expected_sha256" ]; then + _log "cloakbrowser: SHA256 mismatch for $asset (expected $expected_sha256, got $actual_sha256) -- refusing to install" + return 1 + fi + rm -rf "$_CLOAKBROWSER_INSTALL_DIR" + mkdir -p "$_CLOAKBROWSER_INSTALL_DIR" + if ! tar xzf "$tmp_dir/$asset" -C "$_CLOAKBROWSER_INSTALL_DIR"; then + _log "cloakbrowser: extract FAILED; marker not written so the next boot retries" + return 1 + fi + chmod +x "$_CLOAKBROWSER_INSTALL_DIR/chrome" + touch "$marker" + _log "cloakbrowser: install complete (${_CLOAKBROWSER_INSTALL_DIR}/chrome), marker written to $marker" +} + main() { mkdir -p "$MARKER_DIR" local rc=0 - _install_playwright || rc=$? + _install_playwright_deps || rc=$? + _install_cloakbrowser || rc=$? if [ "$rc" -eq 0 ]; then _log "all deferred installs complete" else From b04d66457a6266ff54624aaec56d3b25fd868139 Mon Sep 17 00:00:00 2001 From: Minh Trinh Date: Fri, 17 Jul 2026 11:17:38 -0700 Subject: [PATCH 2/2] Document the CloakBrowser swap: source, placement, and how to update/replace it Co-Authored-By: Claude Sonnet 5 --- libs/browser/CLOAKBROWSER_ENGINE.md | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 libs/browser/CLOAKBROWSER_ENGINE.md diff --git a/libs/browser/CLOAKBROWSER_ENGINE.md b/libs/browser/CLOAKBROWSER_ENGINE.md new file mode 100644 index 000000000..1568a38cf --- /dev/null +++ b/libs/browser/CLOAKBROWSER_ENGINE.md @@ -0,0 +1,52 @@ +# The fleet's Chromium engine: CloakBrowser + +The browser fleet drives [CloakHQ/CloakBrowser](https://github.com/CloakHQ/CloakBrowser) +(a from-source C++/Blink/V8 stealth-patched Chromium fork), pulled from a pinned +GitHub release under their free "delayed release" tier (chosen over +`tiliondev/fortress` specifically because Fortress ships no Linux arm64 build, +which would break the desktop/Lima path on Apple Silicon; CloakBrowser does). +The binary lands at `/opt/cloakbrowser/chrome`, fetched and SHA256-verified by +`scripts/deferred_install.sh`'s `_install_cloakbrowser` on first container +boot (and pre-baked into the box image ahead of time on cloud slices, see +`slice_provider.py::_build_cloakbrowser_derived_image` in `mngr`). The only +code that changed to point at it: `_CLOAKBROWSER_MARKER` / +`_CLOAKBROWSER_EXECUTABLE` in `libs/browser/src/browser/session.py` (replacing +`_PLAYWRIGHT_MARKER` and `playwright.chromium.executable_path`), and the +matching `_CLOAKBROWSER_*` constants in `scripts/deferred_install.sh`. + +## Bumping the CloakBrowser version + +1. Pick a release tag from + with real (non-Pro-gated) `cloakbrowser-linux-x64.tar.gz` / + `-linux-arm64.tar.gz` assets -- check the release actually has downloadable + binaries, not just `SHA256SUMS` (their latest major is routinely paywalled; + the newest release *with* public binaries is the one to use). +2. Update `_CLOAKBROWSER_VERSION` and the two `_CLOAKBROWSER_SHA256_*` + constants in `scripts/deferred_install.sh` from that release's + `SHA256SUMS` file. +3. Update the matching `_CLOAKBROWSER_VERSION` / `_CLOAKBROWSER_SHA256_X64` in + `mngr`'s `slice_provider.py` (cloud is x64-only, no arm64 constant there) -- + these are two independently-pinned copies, kept in sync by hand, not code. + +Nothing else needs to change -- the binary always unpacks to the same +`/opt/cloakbrowser/chrome` path regardless of version. + +## Swapping to a different engine entirely (e.g. Fortress, once/if it ships arm64) + +Same two touch points, nothing architectural: + +1. In `scripts/deferred_install.sh`'s `_install_cloakbrowser` (rename it), + replace the release URL, asset name(s), and SHA256 pin(s) with the new + engine's. **Download and inspect the actual tarball first** to find the + real binary path inside it -- don't assume a name or directory structure. + (CloakBrowser's turned out to be a flat archive with the binary literally + named `chrome`; Fortress's quick-start docs implied a `tilion-fortress/` + wrapper dir with a `tilion` binary inside -- these differ per project.) +2. Update `_CLOAKBROWSER_EXECUTABLE` in `session.py` to that real path. +3. Mirror both changes in `mngr`'s `slice_provider.py` + (`_build_cloakbrowser_derived_image`) for the cloud pre-bake. + +Any Chromium-family fork that accepts standard launch flags (`--headless`, +`--user-data-dir`, `--remote-debugging-port`, `--no-sandbox`) and speaks CDP +satisfies the whole contract these two files rely on -- `browser_use` and the +CDP observer don't know or care which engine is behind `executable_path`.