diff --git a/CLAUDE.md b/CLAUDE.md index 2aac70468..704e26c26 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 Fortress (a stealth-patched Chromium fork), not Playwright's own managed Chromium -- pass `executable_path="/opt/fortress/tilion-fortress/tilion"` 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.fortress` -- 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/fortress/tilion-fortress/tilion", args=["--no-sandbox"])`. See `libs/bootstrap/README.md` for the full deferral contract. # Always remember these guidelines: diff --git a/dev/changelog/deferred-install-fortress-arm64.md b/dev/changelog/deferred-install-fortress-arm64.md new file mode 100644 index 000000000..91626b41b --- /dev/null +++ b/dev/changelog/deferred-install-fortress-arm64.md @@ -0,0 +1,5 @@ +`scripts/deferred_install.sh` now fetches the Fortress stealth Chromium fork +per-arch on first container boot instead of vanilla Playwright Chromium: x64 +from Fortress's official `v151.0.7908.0` release, arm64 from a fork build +(minhtrinh-imbue/fortress, for the aarch64-support PR tiliondev/fortress#29 +until it merges upstream). Both tarballs are SHA256-verified before install. diff --git a/libs/browser/browser_test.py b/libs/browser/browser_test.py index e61245af1..ddf6aa3a0 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.fortress" + monkeypatch.setattr(bsession, "_FORTRESS_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/changelog/deferred-install-fortress-arm64.md b/libs/browser/changelog/deferred-install-fortress-arm64.md new file mode 100644 index 000000000..d3bcecaf5 --- /dev/null +++ b/libs/browser/changelog/deferred-install-fortress-arm64.md @@ -0,0 +1,6 @@ +The browser fleet's Chromium engine is now Fortress (a from-source, BSD-3, +stealth-patched Chromium fork) instead of Playwright's own managed Chromium. +`session.py` and the agents' direct Playwright calls both launch the same +Fortress binary at `/opt/fortress/tilion-fortress/tilion`, so the whole +workspace runs one engine. The fleet gates browser launches on the Fortress +deferred-install marker. diff --git a/libs/browser/src/browser/session.py b/libs/browser/src/browser/session.py index d0ee2d6b2..73f445ca7 100644 --- a/libs/browser/src/browser/session.py +++ b/libs/browser/src/browser/session.py @@ -105,10 +105,15 @@ # tab feels snappier. Slightly more bandwidth than skipping frames. _SCREENCAST_EVERY_NTH_FRAME = 1 -# Deferred-install marker (see scripts/deferred_install.sh). Chromium installs +# Deferred-install marker (see scripts/deferred_install.sh). Fortress 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") +_FORTRESS_MARKER = Path("/var/lib/minds/deferred-install/done.fortress") + +# Fortress's fixed install path (see scripts/deferred_install.sh's +# _install_fortress). A stealth, C++-patched Chromium fork -- replaces +# vanilla Chromium as the engine for every browser the fleet launches. +_FORTRESS_EXECUTABLE = "/opt/fortress/tilion-fortress/tilion" # 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 @@ -329,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 _FORTRESS_MARKER.exists(): return False, "Chromium is still installing in this workspace; try again in a minute." return True, "ready" @@ -542,7 +547,10 @@ async def start( """ self._playwright = playwright self._input_enabled.set() - chromium_path = playwright.chromium.executable_path + # Fixed Fortress path, not playwright.chromium.executable_path -- the + # fleet's engine is Fortress, not Playwright's own managed Chromium + # (which vanilla Playwright calls elsewhere in this image still use). + chromium_path = _FORTRESS_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..97a4215be 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, "_FORTRESS_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..237a2c8bb 100755 --- a/scripts/deferred_install.sh +++ b/scripts/deferred_install.sh @@ -66,35 +66,72 @@ _recover_interrupted_dpkg() { fi } -_install_playwright() { +# Fortress (tiliondev/fortress) stealth Chromium engine, replacing vanilla +# Playwright-managed Chromium. x64 from the official release; arm64 has no +# official release yet (tiliondev/fortress#29, open as of 2026-07-23) so this +# points at a fork build in the meantime -- swap _FORTRESS_ARM64_URL/_SHA256 +# to the official tiliondev/fortress release once that PR merges. +# Fork build: https://github.com/minhtrinh-imbue/fortress/releases/tag/linux-arm64-151.0.7908.0-debian12 +# PR: https://github.com/tiliondev/fortress/pull/29 +readonly _FORTRESS_X64_URL="https://github.com/tiliondev/fortress/releases/download/v151.0.7908.0/tilion-fortress-linux-x64.tar.gz" +readonly _FORTRESS_X64_SHA256="243238b2b8a8b944b7ba2b63533d2b917da7d569dcb290ce96bf28151294b873" +readonly _FORTRESS_ARM64_URL="https://github.com/minhtrinh-imbue/fortress/releases/download/linux-arm64-151.0.7908.0-debian12/tilion-fortress-linux-arm64-debian12.tar.gz" +readonly _FORTRESS_ARM64_SHA256="da6965af8fa8e995d137bcabdca8d163fde7f32ba483eaf5e029223995f19ada" +readonly _FORTRESS_INSTALL_DIR="/opt/fortress" + +_install_fortress() { local marker - marker="$(_marker_for playwright)" + marker="$(_marker_for fortress)" if [ -f "$marker" ]; then - _log "playwright: marker present at $marker, skipping" + _log "fortress: 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` apt-installs the shared libs Chromium needs (libnss3, + # libgbm, etc.) -- Fortress is a Chromium build too, same requirement. + # Recover any interrupted dpkg state first so a bake-interrupted install + # 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 - touch "$marker" - _log "playwright: install complete, marker written to $marker" - else - _log "playwright: install FAILED; marker not written so the next boot retries" + _log "fortress: installing apt system libs" + if ! (cd "$REPO_ROOT" && uv run playwright install-deps chromium); then + _log "fortress: apt install FAILED; marker not written so the next boot retries" + return 1 + fi + + local url sha256 + case "$(uname -m)" in + x86_64) url="$_FORTRESS_X64_URL"; sha256="$_FORTRESS_X64_SHA256" ;; + aarch64) url="$_FORTRESS_ARM64_URL"; sha256="$_FORTRESS_ARM64_SHA256" ;; + *) _log "fortress: unsupported architecture $(uname -m)"; return 1 ;; + esac + _log "fortress: downloading $url" + local tmp_dir + tmp_dir="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '$tmp_dir'" RETURN + local asset="$tmp_dir/fortress.tar.gz" + if ! curl -fsSL -o "$asset" "$url"; then + _log "fortress: download FAILED; marker not written so the next boot retries" + return 1 + fi + if [ "$(sha256sum "$asset" | awk '{print $1}')" != "$sha256" ]; then + _log "fortress: SHA256 mismatch -- refusing to install" + return 1 + fi + rm -rf "$_FORTRESS_INSTALL_DIR" + mkdir -p "$_FORTRESS_INSTALL_DIR" + if ! tar xzf "$asset" -C "$_FORTRESS_INSTALL_DIR"; then + _log "fortress: extract FAILED; marker not written so the next boot retries" return 1 fi + chmod +x "$_FORTRESS_INSTALL_DIR/tilion-fortress/tilion" + touch "$marker" + _log "fortress: install complete (${_FORTRESS_INSTALL_DIR}/tilion-fortress/tilion), marker written to $marker" } main() { mkdir -p "$MARKER_DIR" local rc=0 - _install_playwright || rc=$? + _install_fortress || rc=$? if [ "$rc" -eq 0 ]; then _log "all deferred installs complete" else