From d4eccd8345371519a9fee2e3f08b875331dd28fa Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 24 Oct 2026 15:20:21 +0800 Subject: [PATCH] feat(browser): support lightweight Docker browser installs (#154608) * refactor(browser): share engine adapters for Chromium and Lightpanda Refresh signed stack identities without changing any source tree. Previous signed head: bed85c558d64369dca51d6a6e484097730506f2f PR06 CI tested stale synthetic merge22409995, whose main base80c3c742 predates Peter Steinberger's canonical static-validation fix82411a34. Fresh synchronization must check the repaired merge context; neither old failed CI nor old passing runtime proof is relabeled. Preserve every source/test/dependency blob and previously qualified registry/catalog fixture correction. * improve(browser): default managed Chromium to efficient headless launches Refresh signed stack identities without changing any source tree. Previous signed head: b6cda11dd86f3653de9a4c087ca557a9aff176fd PR06 CI tested stale synthetic merge22409995, whose main base80c3c742 predates Peter Steinberger's canonical static-validation fix82411a34. Fresh synchronization must check the repaired merge context; neither old failed CI nor old passing runtime proof is relabeled. Preserve every source/test/dependency blob and previously qualified registry/catalog fixture correction. * improve(browser): trim Docker browser images and harden Lightpanda sidecar Refresh signed stack identities without changing any source tree. Previous signed head: e6d320e30aa0e7d902a3c11cd6c2895143e03c59 PR06 CI tested stale synthetic merge22409995, whose main base80c3c742 predates Peter Steinberger's canonical static-validation fix82411a34. Fresh synchronization must check the repaired merge context; neither old failed CI nor old passing runtime proof is relabeled. Preserve every source/test/dependency blob and previously qualified registry/catalog fixture correction. * test(agents): retain canonical restored-run settlement fixture * test(agents): preserve upstream requester-disposal completion ordering * fix(browser): preserve shared Chromium discovery and Lightpanda lifecycle * fix(browser): align lightweight image integration Preserve the current shared browser discovery helper and renderer fixtures after baseline integration. Leave Lightpanda resource ceilings with operators and keep selected-image proof wording version neutral. --- Dockerfile | 27 +++++++----- deploy/lightpanda/README.md | 16 ++++++-- deploy/lightpanda/compose.yaml | 24 ++++++++++- docs/install/docker.md | 41 +++++++++++++++++-- docs/install/docker/environment-variables.md | 2 +- docs/tools/browser/lightweight.md | 29 ++++++++++--- .../browser/scripts/bench-lightweight.ts | 1 + extensions/diffs/src/browser.runtime.test.ts | 36 +++++++++++++++- extensions/diffs/src/browser.runtime.ts | 8 ++++ scripts/sandbox-browser-entrypoint.sh | 10 +++-- test/scripts/test-install-sh-docker.test.ts | 5 ++- 11 files changed, 170 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 035ba7584821..33df26896bc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -357,22 +357,27 @@ RUN --mount=type=cache,id=openclaw-trixie-apt-cache,target=/var/cache/apt,sharin python3 -m pip install --no-cache-dir --break-system-packages $OPENCLAW_IMAGE_PIP_PACKAGES; \ fi -# Optionally install Chromium and Xvfb for browser automation. -# Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ... -# Adds ~300MB but eliminates the 60-90s Playwright install on every container start. +# Optionally install Chromium distributions for browser automation. +# Legacy 1 keeps full Chromium and headless shell; explicit chromium or +# headless-shell installs only that distribution; managed shell use requires an +# explicit executablePath on a separate headless browser profile. # Must run after node_modules COPY so playwright-core is available. ARG OPENCLAW_INSTALL_BROWSER="" ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright RUN --mount=type=cache,id=openclaw-trixie-apt-cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,id=openclaw-trixie-apt-lists,target=/var/lib/apt,sharing=locked \ - if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \ - apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \ - install -d -m 0755 -o node -g node "$(dirname "$PLAYWRIGHT_BROWSERS_PATH")" && \ - mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" && \ - node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \ - chown -R node:node "$PLAYWRIGHT_BROWSERS_PATH"; \ - fi + set -eu; \ + case "$OPENCLAW_INSTALL_BROWSER" in \ + ""|0) exit 0 ;; \ + 1) set -- ;; \ + chromium) set -- --no-shell ;; \ + headless-shell) set -- --only-shell ;; \ + *) echo 'OPENCLAW_INSTALL_BROWSER must be empty, 0, 1, chromium, or headless-shell' >&2; exit 1 ;; \ + esac; \ + install -d -m 0755 -o node -g node "$(dirname "$PLAYWRIGHT_BROWSERS_PATH")" && \ + mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" && \ + node /app/node_modules/playwright-core/cli.js install --with-deps "$@" chromium && \ + chown -R node:node "$PLAYWRIGHT_BROWSERS_PATH" # Optionally install Docker CLI for sandbox container management. # Build with: docker build --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1 ... diff --git a/deploy/lightpanda/README.md b/deploy/lightpanda/README.md index 66961b350457..524611ba680c 100644 --- a/deploy/lightpanda/README.md +++ b/deploy/lightpanda/README.md @@ -6,10 +6,15 @@ They do not add an engine binary, container layer, or npm dependency to OpenClaw ## License boundary -OpenClaw's adapter is MIT-licensed. Lightpanda 1.0.2 is -**AGPL-3.0-or-later**, as stated in its +OpenClaw's adapter is MIT-licensed. Lightpanda 1.0.2 is **AGPL**, with an +upstream declaration inconsistency at its selected source commit: its +[licensing document](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/LICENSING.md) +says `AGPL-3.0-only`, while its [source header](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/src/main.zig#L1) -and [license](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/LICENSE). +allows version 3 or later. Preserve both declarations during redistribution +review; neither makes the engine permissively licensed. The +[license text](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/LICENSE) +is AGPL version 3. The engine is independently installed and communicates with OpenClaw over CDP; OpenClaw does not link, vendor, or relicense its implementation. This is not an MIT-only stack or a certification that every deployment is legally compliant. @@ -78,6 +83,11 @@ Linux ARM64, and both macOS architectures remain unverified by this update. The other three native binary pins were checked against release API digests, without download or execution. +The retained adapter observations above do not qualify this version's Compose +WebSocket-readiness probe, independent probe-session close, crash restart, or +SIGTERM/stop-grace behavior. Those container-specific checks require execution +against the exact pinned image; historical 0.4.1 results are not proof for the selected image. + ## Existing JavaScript clients The following client inspection records the **2026-09-21** audit. Current client diff --git a/deploy/lightpanda/compose.yaml b/deploy/lightpanda/compose.yaml index a115bf31b647..cc586fa2c401 100644 --- a/deploy/lightpanda/compose.yaml +++ b/deploy/lightpanda/compose.yaml @@ -2,7 +2,8 @@ # the repository's docker-compose.yml for an unpublished container-only endpoint. services: lightpanda: - # Optional external AGPL-3.0-or-later engine, not an MIT-only image. + # Optional external AGPL engine, not an MIT-only image. Upstream declarations + # disagree on -only vs -or-later; see README.md for the source evidence. # See README.md before mirroring, bundling, or modifying this distribution. # Official 1.0.2 multi-platform index: linux/amd64 and linux/arm64. image: docker.io/lightpanda/browser:1.0.2@sha256:5d1d724c476957d8c2f8393a51fd089477523b0e5aa9c783c48688785d934fb9 @@ -14,5 +15,26 @@ services: - ALL security_opt: - no-new-privileges:true + # This probe requires Bash /dev/tcp in the pinned distribution. Test the CDP + # WebSocket upgrade, not a separate `lightpanda version` process. This probe + # opens/closes its own session and does not touch an agent's active session. + healthcheck: + test: + - CMD + - /bin/bash + - -ec + - >- + exec 3<>/dev/tcp/127.0.0.1/9222; + printf 'GET / HTTP/1.1\r\nHost: 127.0.0.1:9222\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n' >&3; + IFS= read -r response <&3; + [[ "$$response" == *" 101 "* ]] + interval: 30s + timeout: 5s + start_period: 10s + retries: 3 + # Override the upstream image's SIGKILL so Tini can forward graceful stop. + stop_signal: SIGTERM + stop_grace_period: 10s # The official image already runs as its lightpanda user under tini. + # Docker restarts exited processes, not a still-running unhealthy process. restart: unless-stopped diff --git a/docs/install/docker.md b/docs/install/docker.md index a5c938e8cd0f..b7a64ef55d00 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -147,8 +147,43 @@ For a new installation from a local source build, bake Chromium into the image: OPENCLAW_IMAGE=openclaw:local OPENCLAW_INSTALL_BROWSER=1 ./scripts/docker/setup.sh ``` -This build-time option installs Chromium and Xvfb; setting it on -an already-built container does not install a browser. +`OPENCLAW_INSTALL_BROWSER=1` installs full Chromium and headless shell, preserving +existing Playwright scripts that use default headless launches. Choose `chromium` +to install only full Chromium, or `headless-shell` for a smaller, headless-only image: + +```bash +OPENCLAW_IMAGE=openclaw:local OPENCLAW_INSTALL_BROWSER=headless-shell ./scripts/docker/setup.sh +``` + +All modes use the repository-pinned Playwright installer, including its system +libraries, fonts, and FFmpeg helper. Empty or `0` omits the browser. These are +build-time choices; setting them on an already-built container does not install +or replace a browser. Headless shell cannot provide a headed browser window or +Chrome extensions. Keep full Chromium for those workflows and validate your sites +before switching. Neither browser distribution is MIT-only; inventory the actual +image and preserve all third-party license notices. + +OpenClaw automatically discovers full Chromium only. A `headless-shell` image +requires an explicit shell executable path on a separate managed profile. Find +the installed path inside the container (the revision directory changes with +Playwright updates): + +```bash +find /home/node/.cache/ms-playwright -type f -name chrome-headless-shell +``` + +Set `browser.profiles.shell.executablePath` to that path, give the profile its own +`cdpPort`, and keep it headless. Select that profile when starting the browser. +Do not reuse an existing full Chromium profile: switching distributions can +discard localStorage. See [Browser configuration](/tools/browser/configuration). + +With the explicit `chromium` mode, +your own Playwright scripts must use `channel: "chromium"` for headless launches; +Playwright's default headless launch expects the separate shell binary. Keep `1` +to retain both binaries without changing existing scripts. +The bundled Diffs PNG/PDF renderer supports all image modes. After explicit and +system-browser choices, it selects Playwright's full Chromium when installed and +otherwise retains Playwright's default headless-shell selection. The image supplies Chromium, not a replacement for your browser configuration: @@ -158,7 +193,7 @@ The image supplies Chromium, not a replacement for your browser configuration: - OpenClaw auto-detects the image's Playwright-managed Chromium on Linux. An explicit `browser.executablePath` or profile executable path must point to a binary inside the container; a path from your laptop will not work there. -- A headless container needs headless browser operation. Check explicit +- Managed browsers run headless by default. Check explicit `browser.headless`, profile headless settings, and `OPENCLAW_BROWSER_HEADLESS` overrides if startup reports a missing display. See [Browser configuration](/tools/browser-control). - Connect with `operator.admin` access to a Gateway advertising `browser.request`. diff --git a/docs/install/docker/environment-variables.md b/docs/install/docker/environment-variables.md index b99559851629..3882757d9467 100644 --- a/docs/install/docker/environment-variables.md +++ b/docs/install/docker/environment-variables.md @@ -23,7 +23,7 @@ Optional variables accepted by `scripts/docker/setup.sh` (and, for the gateway c | `OPENCLAW_DOCKER_BUILD_NODE_OPTIONS` | Override the local source-build Node options (default `--max-old-space-size=8192`) | | `OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB` | Override the local source-build tsdown heap in MB | | `OPENCLAW_DOCKER_BUILD_SKIP_DTS` | Skip declaration output during runtime-only local image builds (default `1`) | -| `OPENCLAW_INSTALL_BROWSER` | Bake Chromium + Xvfb into the image at build time | +| `OPENCLAW_INSTALL_BROWSER` | Bake both Chromium distributions (`1`), only `chromium`, or only `headless-shell` into the image; empty or `0` omits the browser | | `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) | | `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume | | `OPENCLAW_TZ` | Set the gateway and CLI container timezone to an IANA name (default `UTC`) | diff --git a/docs/tools/browser/lightweight.md b/docs/tools/browser/lightweight.md index 914477599ee6..e0175e984764 100644 --- a/docs/tools/browser/lightweight.md +++ b/docs/tools/browser/lightweight.md @@ -38,9 +38,12 @@ the Lightpanda engine. Existing profiles and engine configuration are unchanged. ## Licensing and distribution OpenClaw's adapter remains MIT-licensed. The optional Lightpanda engine is -**AGPL-3.0-or-later**, not MIT: see its +**AGPL**, not MIT. Version 1.0.2 has inconsistent declarations: its +[licensing document](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/LICENSING.md) +says `AGPL-3.0-only`, while its [pinned source notice](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/src/main.zig#L1) -and [license](https://github.com/lightpanda-io/browser/blob/c5f7bb29b127040c899d4e181b5709f76fc71a6f/LICENSE). +allows later versions. Record that discrepancy during your SBOM review; neither +interpretation makes the engine permissively licensed. The existing Playwright client is Apache-2.0; the existing `ws` client is MIT. Their licenses and third-party notices still apply. @@ -121,7 +124,8 @@ the official container image has Linux amd64 and arm64 variants. From the repository root: ```sh -docker compose -f deploy/lightpanda/compose.yaml -f deploy/lightpanda/compose.host.yaml up -d +docker compose -f deploy/lightpanda/compose.yaml -f deploy/lightpanda/compose.host.yaml up -d --wait +docker compose -f deploy/lightpanda/compose.yaml -f deploy/lightpanda/compose.host.yaml ps docker compose -f deploy/lightpanda/compose.yaml -f deploy/lightpanda/compose.host.yaml exec lightpanda /bin/lightpanda version ``` @@ -130,6 +134,21 @@ another host port, and update the profile URL to match. The image is pinned by i multi-platform digest, so Docker selects the host architecture without pulling a moving `latest` or `nightly` version. +The healthcheck opens an independent local CDP WebSocket session and checks its +handshake. This proves listener readiness, not successful browsing or OpenClaw +integration. The sample leaves memory, CPU, and PID limits unset. Set limits for +your measured workload in your deployment's Compose configuration, or use +[`docker update`](https://docs.docker.com/reference/cli/docker/container/update/) +with `--memory`, `--cpus`, and `--pids-limit` for an existing container. +`restart: unless-stopped` restarts a crashed engine but does not restart a +still-running unhealthy container. Monitor unhealthy status and replace failed +workers through your deployment's supervisor. Restarting loses that worker's +page/login state; do not automatically replay clicks or submissions. + +The stop signal is `SIGTERM`, with a 10-second grace period, overriding the +upstream image's immediate `SIGKILL`. This is a single-worker example, not a +high-availability service or transparent failover guarantee. + CDP gives a client control over the browser; the sample does not add CDP authentication. Do not change the loopback binding to a public address. Use an authenticated tunnel for access from another host. @@ -145,7 +164,7 @@ docker compose -f deploy/lightpanda/compose.yaml -f deploy/lightpanda/compose.ho Merge the sidecar into the repository's existing Compose project: ```sh -docker compose -f docker-compose.yml -f deploy/lightpanda/compose.yaml up -d lightpanda +docker compose -f docker-compose.yml -f deploy/lightpanda/compose.yaml up -d --wait lightpanda ``` Configure the OpenClaw Gateway with `cdpUrl: "ws://lightpanda:9222"` in the profile @@ -371,7 +390,7 @@ container/binary version separately with the report. A Windows Node controller can use the same external-engine interface, but Windows Docker runtime behavior has not been verified for this sample. -Compare the same fixed tasks with a pinned Chromium baseline. Record +Compare the same deterministic tasks with a pinned Chromium baseline. Record task completion before reporting speed or memory improvements; unsupported or failed work must not be counted as a successful fast result. Report warm and cold runs separately, engine versions, host OS/architecture, client overhead, diff --git a/extensions/browser/scripts/bench-lightweight.ts b/extensions/browser/scripts/bench-lightweight.ts index 9f065e62db97..0c2ec30cc6cf 100644 --- a/extensions/browser/scripts/bench-lightweight.ts +++ b/extensions/browser/scripts/bench-lightweight.ts @@ -450,6 +450,7 @@ async function main() { const snapshot = await request("GET", "/snapshot", undefined, { targetId, format: "ai", + mode: "efficient", }); const text = String(snapshot.snapshot); const nameRef = /textbox[^\n]*\[ref=((?:f\d+)?e\d+)\]/.exec(text)?.[1]; diff --git a/extensions/diffs/src/browser.runtime.test.ts b/extensions/diffs/src/browser.runtime.test.ts index 5b9d5f6a7c7c..0505596dd783 100644 --- a/extensions/diffs/src/browser.runtime.test.ts +++ b/extensions/diffs/src/browser.runtime.test.ts @@ -19,8 +19,9 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vites import { registerDiffsPlugin } from "./plugin.js"; import { createTempDiffRoot } from "./test-helpers.js"; -const { launchMock } = vi.hoisted(() => ({ +const { launchMock, playwrightExecutablePathMock } = vi.hoisted(() => ({ launchMock: vi.fn(), + playwrightExecutablePathMock: vi.fn(), })); const toolContext = { agentId: "main", @@ -38,6 +39,7 @@ type ScreenshotParams = Parameters< vi.mock("playwright-core", () => ({ chromium: { launch: launchMock, + executablePath: playwrightExecutablePathMock, }, })); @@ -63,6 +65,7 @@ describe("PlaywrightDiffScreenshotter", () => { ({ rootDir, cleanup: cleanupRootDir } = await createTempDiffRoot("openclaw-diffs-browser-")); outputPath = path.join(rootDir, "preview.png"); launchMock.mockReset(); + playwrightExecutablePathMock.mockReset().mockReturnValue(path.join(rootDir, "chromium")); }); afterEach(async () => { @@ -125,10 +128,41 @@ describe("PlaywrightDiffScreenshotter", () => { "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe", "C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe", + playwrightExecutablePathMock(), ]); expect(candidates.every((candidate) => path.win32.isAbsolute(candidate))).toBe(true); }); + it.each([true, false])( + "renders with Playwright-only installation when full Chromium is present=%s", + async (hasFullChromium) => { + vi.stubEnv("PATH", ""); + vi.stubEnv("OPENCLAW_BROWSER_EXECUTABLE_PATH", ""); + vi.stubEnv("BROWSER_EXECUTABLE_PATH", ""); + vi.stubEnv("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", ""); + const managedPath = path.join(rootDir, "playwright", "chromium", "chrome"); + playwrightExecutablePathMock.mockReturnValue(managedPath); + vi.spyOn(fs, "access").mockImplementation(async (candidate) => { + if (!hasFullChromium || candidate !== managedPath) { + throw new Error("ENOENT"); + } + }); + + launchMock.mockResolvedValue(createMockBrowser([])); + const screenshotter = new PlaywrightDiffScreenshotter({ config: {} }); + await screenshotter.screenshotHtml(screenshotParams({ image: { scale: 1 } })); + const [launchOptions] = expectDefined(launchMock.mock.calls[0], "browser launch"); + if (hasFullChromium) { + // Playwright's default headless launch requests the absent shell binary. + expect(launchOptions.executablePath).toBe(managedPath); + } else { + // A shell-only installation must retain Playwright's default selection. + expect(launchOptions).not.toHaveProperty("executablePath"); + } + await expect(fs.readFile(outputPath, "utf8")).resolves.toBe("png"); + }, + ); + it("shares a browser across parallel renders and closes it after the idle window", async () => { const { pages, browser, screenshotter } = await createScreenshotterHarness(); diff --git a/extensions/diffs/src/browser.runtime.ts b/extensions/diffs/src/browser.runtime.ts index e660e908032e..12a6a5ac911d 100644 --- a/extensions/diffs/src/browser.runtime.ts +++ b/extensions/diffs/src/browser.runtime.ts @@ -321,6 +321,14 @@ async function resolveBrowserExecutablePath(config: OpenClawConfig): Promise {