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: bed85c558d

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: b6cda11dd8

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: e6d320e30a

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.
This commit is contained in:
Vincent Koc authored and GitHub committed 2026-10-24 15:20:21 +08:00
1 parent 1adbd71472
commit d4eccd8345
11 files changed
+170 -29

No files matched your search

+16 -11
View File
@@ -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 ...
+13 -3
View File
@@ -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
+23 -1
View File
@@ -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
+38 -3
View File
@@ -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`.
+1 -1
View File
@@ -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`) |
+24 -5
View File
@@ -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,
@@ -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];
+35 -1
View File
@@ -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();
+8
View File
@@ -321,6 +321,14 @@ async function resolveBrowserExecutablePath(config: OpenClawConfig): Promise<str
}
}
// Full-only images omit the shell selected by an unspecified headless launch.
// Keep discovery owned by the shared browser promise, using Playwright's public
// full-browser path; absence preserves its default shell-only selection.
const playwrightPath = chromium.executablePath();
if (await canAccess(playwrightPath, fsConstants.X_OK)) {
return playwrightPath;
}
return undefined;
}
+7 -3
View File
@@ -90,9 +90,13 @@ trap 'cleanup 143' TERM
mkdir -p "${HOME}" "${HOME}/.chrome" "${XDG_CONFIG_HOME}" "${XDG_CACHE_HOME}"
Xvfb :1 -screen 0 1280x800x24 -ac -nolisten tcp &
XVFB_PID=$!
echo "[sandbox] Xvfb started (PID: ${XVFB_PID})"
# Headless Chromium does not use a display server. Keep the headed/noVNC path
# unchanged, including when noVNC itself is disabled but Chromium is headed.
if [[ "${HEADLESS}" != "1" ]]; then
Xvfb :1 -screen 0 1280x800x24 -ac -nolisten tcp &
XVFB_PID=$!
echo "[sandbox] Xvfb started (PID: ${XVFB_PID})"
fi
if [[ "${CDP_PORT}" -ge 65535 ]]; then
CHROME_CDP_PORT="$((CDP_PORT - 1))"
+4 -1
View File
@@ -982,7 +982,10 @@ printf 'status=%s\\n' "$status"
[
"ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright",
'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
"node /app/node_modules/playwright-core/cli.js install --with-deps chromium",
'node /app/node_modules/playwright-core/cli.js install --with-deps "$@" chromium',
"1) set -- ;;",
"chromium) set -- --no-shell",
"headless-shell) set -- --only-shell",
],
],
] as const)("root Dockerfile preserves %s", (_contract, fragments) => {