feat(browser): verify Chromium headless shell across platforms (#154396)

* docs(browser): audit engine licenses and non-AGPL alternatives

Preserve the reviewed browser-stack change while incorporating the landed prerequisites.

Original revision: 8f41f08a1e92c079ad19ab9f3b849be26cf1559b

* feat(browser): verify Chromium headless shell across platforms

Preserve the reviewed browser-stack change while incorporating the landed prerequisites.

Original revision: b11dd7173db731e026b7abae9d3b4064ac30d300

* test(gateway): complete prepared placement read fixture

Supply the required empty policyConfig in the placement lifecycle read fixture. This repairs the inherited check-test-types-core-4 failure while keeping the production browser-stack changes unchanged.
This commit is contained in:
Vincent Koc 2026-09-24 00:12:40 +08:00 • committed by GitHub
parent f7dc8adeee
commit f5cf1b31e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 52 additions and 9 deletions

View file

@ -232,6 +232,42 @@ Engine startup, CDP connectivity, task completion, and full OpenClaw integration
are separate checks. A running container or a successful `Browser.getVersion`
does not prove that snapshots, references, and actions work through OpenClaw.
### Chromium headless shell baseline
For an alternative without Lightpanda's AGPL engine, first test Chromium's
headless shell through the existing Chromium profile. It retains Chromium's
third-party license obligations; this is not an MIT-only binary. It does not
require another automation daemon or an OpenClaw engine adapter.
Use the repository-pinned Playwright installer rather than an unpinned wrapper:
```sh
node node_modules/playwright-core/cli.js install chromium-headless-shell
node node_modules/playwright-core/cli.js install --dry-run chromium-headless-shell
```
The second command prints the selected version, platform download, and install
directory. Locate `chrome-headless-shell` (or `chrome-headless-shell.exe` on
Windows) in that directory. Linux also needs the browser's system libraries and
fonts; see [Linux troubleshooting](/tools/browser-linux-troubleshooting).
Run from the repository root, quoting paths that contain spaces:
```sh
node --import ./scripts/tsx.mjs extensions/browser/scripts/bench-lightweight.ts --headless-shell "/path/to/chrome-headless-shell" --iterations 10 --output headless-shell-benchmark.json
```
The report labels the requested distribution separately from its Chromium
protocol engine and the observed browser version. `--headless-shell` selects
the benchmark executable only: it does not install a production browser,
change a profile, or establish binary provenance. Preserve its complete
distribution and `LICENSE.headless_shell` when reviewing deployment. The installer
also downloads platform helper assets, including FFmpeg; review and retain their
own notices separately. Use
separate invocations for the full Chromium and headless-shell comparisons;
memory or startup savings must be measured, not inferred from download size.
### Native engine comparison
Run the opt-in synthetic route benchmark from the repository root after
installing development dependencies:
@ -239,7 +275,7 @@ installing development dependencies:
node --import ./scripts/tsx.mjs extensions/browser/scripts/bench-lightweight.ts --lightpanda /path/to/lightpanda --chromium /path/to/chrome --iterations 10 --output lightweight-benchmark.json
```
Either binary flag can be used alone. The script creates isolated OpenClaw
Any binary flag can be used alone. The script creates isolated OpenClaw
state and browser data, serves a local form, then verifies navigation, the default
efficient AI snapshot, reference-based typing/clicking, exactly one form
submission, waiting, and text extraction through the browser route dispatcher.

View file

@ -11,12 +11,16 @@ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
type Engine = "chromium" | "lightpanda";
type Run = { engine: Engine; executable: string } | { engine: Engine; endpoint: string };
type Distribution = "chromium" | "chromium-headless-shell" | "lightpanda";
type Run =
| { engine: Engine; distribution: Distribution; executable: string }
| { engine: Engine; endpoint: string };
const { values } = parseArgs({
options: {
lightpanda: { type: "string" },
chromium: { type: "string" },
"headless-shell": { type: "string" },
endpoint: { type: "string" },
engine: { type: "string" },
"fixture-bind": { type: "string", default: "127.0.0.1" },
@ -33,7 +37,7 @@ assert(
const runs: Run[] = [];
if (values.endpoint) {
assert(
!values.lightpanda && !values.chromium,
!values.lightpanda && !values.chromium && !values["headless-shell"],
"Use either native binaries or an external --endpoint, not both.",
);
assert(
@ -47,18 +51,19 @@ if (values.endpoint) {
runs.push({ engine: values.engine, endpoint: values.endpoint });
} else {
assert(!values.engine, "--engine requires --endpoint.");
for (const [engine, executable] of [
["chromium", values.chromium],
["lightpanda", values.lightpanda],
for (const [engine, distribution, executable] of [
["chromium", "chromium", values.chromium],
["chromium", "chromium-headless-shell", values["headless-shell"]],
["lightpanda", "lightpanda", values.lightpanda],
] as const) {
if (executable) {
runs.push({ engine, executable: path.resolve(executable) });
runs.push({ engine, distribution, executable: path.resolve(executable) });
}
}
}
assert(
runs.length > 0,
"Pass --lightpanda <binary>, --chromium <binary>, or --endpoint <url> --engine <engine>.",
"Pass --lightpanda <binary>, --chromium <binary>, --headless-shell <binary>, or --endpoint <url> --engine <engine>.",
);
async function freePort() {
@ -265,7 +270,7 @@ async function main() {
let startupMs: number | null = null;
let engineVersion: string | null = null;
if ("executable" in spec) {
const engineHome = path.join(scratchDir, engine);
const engineHome = path.join(scratchDir, spec.distribution);
await fs.mkdir(engineHome);
const args =
engine === "lightpanda"
@ -513,6 +518,7 @@ async function main() {
const sorted = tasks.map((item) => item.durationMs).toSorted((a, b) => a - b);
return {
engine,
distribution: "distribution" in spec ? spec.distribution : null,
engineVersion,
connectionMode: proc ? "spawned" : "external",
warmIterations: iterations,
@ -586,6 +592,7 @@ async function main() {
workload:
"Local synthetic form through OpenClaw routes, no LLM. One first task includes initial page open/attachment; each measured warm task includes navigation, default efficient AI snapshot (Lightpanda selects aria refs), typing, exactly one submission, wait and text extraction. Capability/session checks run after measurement.",
engineOrder: runs.map((spec) => spec.engine),
distributionOrder: runs.map((spec) => ("distribution" in spec ? spec.distribution : null)),
results,
},
null,