How to run a Playwright suite in a capsule
A capsule that changes a web front end should prove its change by running the project's browser
tests, inside the same boundary as everything else it runs. This guide builds a scoped capsule
that runs a Playwright suite against a headless Chrome on your host, with
only the grants Node and Chrome need: Chrome's own folder, the fonts it draws with, the system
library folder it loads modules from, the PCI device tree its GPU process reads, and an
address-space limit V8 can run under. The suite's commands go through the capsule's shell, so
Chrome runs as a shell command's descendant,
under the same containment as every other program the capsule runs.
The relevant manifest options are:
| Option | Controls |
|---|---|
| capabilities.containment | The containment floor the capsule refuses to launch below |
| capabilities.shell.allow | Which binaries the capsule's commands may run |
| capabilities.shell.interpreter_runtime | Host folders an allowlisted binary reads and loads from outside the workdir |
| capabilities.resources.memory_bytes | The address-space limit on each command |
| capabilities.resources.cgroup_pids_max | How many processes the capsule's whole command tree may hold |
| capabilities.resources.cgroup_memory_bytes | The memory the capsule's whole command tree may use |
Step 1 — find the browser
Playwright's chromium-headless-shell is a single folder holding the chrome-headless-shell
binary and the libraries it loads beside it. Install it once on the host, outside the capsule:
npx playwright install chromium-headless-shell
The folder is ~/.cache/ms-playwright/chromium_headless_shell-<build>/chrome-headless-shell-linux64.
The guide uses build 1208 and a home folder of /home/you; put your own path in each place it
appears.
capabilities.shell.allow names a binary by name, so the folder must be on the PATH that
mur run starts with:
export PATH="/home/you/.cache/ms-playwright/chromium_headless_shell-1208/chrome-headless-shell-linux64:$PATH"
Step 2 — point Playwright at that browser
Create a playwright.config.mjs file next to your project's package.json:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
launchOptions: {
executablePath: '/home/you/.cache/ms-playwright/chromium_headless_shell-1208/chrome-headless-shell-linux64/chrome-headless-shell',
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
},
},
});
| Line | Why |
|---|---|
executablePath |
The binary the manifest grants. Playwright's own download location is not granted, and nothing downloads inside the capsule |
--no-sandbox |
A capsule command runs with no_new_privs set and no capabilities, which Chrome's own sandbox cannot start under; the capsule is the boundary |
--disable-dev-shm-usage |
A capsule command cannot write /dev/shm; Chrome keeps its shared memory in the temporary folder instead |
--disable-gpu |
The manifest grants no GPU device; Chrome renders in software |
Step 3 — write the manifest
Create a murmur.yaml file next to playwright.config.mjs, with your browser folder in place of
the one below:
name: playwright-tests
version: "0.1.0"
artifacts:
- name: murmur/driver-openai
version: "0.20.0"
runtime: driver
gateway:
endpoint: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
capabilities:
containment: scoped
shell:
allow:
- bash
- node
- chrome-headless-shell
interpreter_runtime:
- binary: node
dirs:
- path: /etc/ssl/openssl.cnf
list_dir: false
- binary: chrome-headless-shell
dirs:
- path: /home/you/.cache/ms-playwright/chromium_headless_shell-1208/chrome-headless-shell-linux64
list_dir: true
- path: /proc
list_dir: true
- path: /etc/fonts
list_dir: true
- path: /usr/share/fonts
list_dir: true
- path: /usr/lib/x86_64-linux-gnu
list_dir: false
- path: /sys/bus/pci
list_dir: true
- path: /sys/devices
list_dir: true
resources:
memory_bytes: 274877906944
cgroup_pids_max: 1024
cgroup_memory_bytes: 4294967296
inference:
transport: http
model: o3-mini-high
driver:
artifact: murmur/driver-openai
What each grant is for:
| Grant | Why |
|---|---|
node in shell.allow |
Runs Playwright's test runner, and Playwright starts Chrome from it |
chrome-headless-shell in shell.allow |
Chrome is executed by node, and every program a capsule command executes must be allowed, at any depth |
/etc/ssl/openssl.cnf on node |
node reads OpenSSL's configuration file at startup and exits with code 13 when it is refused; see Native addons |
The browser folder on chrome-headless-shell |
Chrome's data files, locales and the graphics libraries it loads from its own folder |
/etc/fonts and /usr/share/fonts |
The fonts Chrome draws pages with |
/usr/lib/x86_64-linux-gnu |
Chrome loads its NSS crypto modules from the system library folder with dlopen, so they are not among the libraries the binary names; without them an https page stops Chrome. This grant carries execute and draws W-SEC-009 |
/sys/bus/pci and /sys/devices |
With the library folder granted, Chrome's GPU process loads libpci, which reads the PCI device tree and exits the process when it is refused; see A library a grant brings in reach |
memory_bytes: 274877906944 |
memory_bytes limits address space, and V8 reserves tens of GiB of it in every Chrome and Node process |
cgroup_pids_max and cgroup_memory_bytes |
The real caps on the command tree: Chrome runs dozens of processes per page |
Without the two /sys grants, Chrome starts and stops at once, and Playwright reports:
Error: browserContext.newPage: Target page, context or browser has been closed
with Chrome's log naming the cause:
pcilib: Cannot open /sys/bus/pci/devices
ERROR:content/browser/gpu/gpu_process_host.cc] GPU process exited unexpectedly: exit_code=256
FATAL:content/browser/gpu/gpu_data_manager_impl_private.cc] GPU process isn't usable. Goodbye.
Step 4 — check the manifest
mur install
Different ways to install artifacts
mur install fetches an artifact from the source of its scope. murmur/ artifacts come from
the built-in source, murmur-nexus/default-artifacts, with no configuration.
Option A — name the source of a scope in ~/.murmur/config.yaml:
registry:
sources:
- repo: <owner>/<repo>
token: "${GITHUB_TOKEN}"
Then install by full name, with or without a version:
mur install <owner>/<artifact-name>@<version>
Option B — pass a full GitHub reference and skip configuration entirely:
mur install github:<owner>/<repo>@<tag>
See Installing artifacts to learn more.
mur doctor
mur doctor checks the manifest against this host before any model call.
Step 5 — run the suite the way scoped allows
scoped does not run files from the workdir, and every file under node_modules is in the workdir.
Run Playwright's test runner through node rather than through npx playwright or
node_modules/.bin/playwright, each of which executes a file under node_modules:
node node_modules/@playwright/test/cli.js test
Install the project's dependencies on the host before the run, or with npm ci --ignore-scripts
inside the capsule as How to run Node tests in a capsule shows.
Create a task.md file asking for the change you want and naming that command as the way to
check it, then run the capsule with the project folder as the workdir:
mur run --workdir . --task task.md
murmur: url localhost:52222
session: ses_019ed2af53da75c2aefee84ee10c34af
status: ok
mur trace show
Different ways to identify a session
mur trace show with no argument reads the most recent session:
mur trace show
To name another one, pass an ordinal counting back from the newest (@2), the last 4 or more
characters of its ID (3e4b), the full ID, or a path to its trace.jsonl:
mur trace show @2
mur trace show 3e4b
mur trace show ses_6801f81dd28b4a9daf434e8324c4793e
mur trace show path/to/trace.jsonl
Use --workdir <path> if your session directories are not under ./workdir. Every command
that names a session takes the same addresses — see
Session addresses.
Other trace exploration commands
mur trace has four subcommands for exploring session output:
mur trace show — print the full trace for a session to the terminal.
mur trace steps — show a turn-by-turn summary of what the agent did in a session. Pass --verbose to include a truncated summary of each tool's input.
mur trace diff — compare the traces of two sessions side by side, or with no arguments the two most recent. Useful for spotting behavioural regressions between runs.
mur trace report — generate a structured summary report from a session's trace. Covers token usage, tool calls, latency, and other session-level metrics.
Summary
| Feature / setting | How it works |
|---|---|
capabilities.containment: scoped |
Landlock and seccomp over the host filesystem; Chrome runs under the same containment as the shell command that started it |
executablePath in playwright.config.mjs |
Starts the granted chrome-headless-shell instead of a Playwright download |
--no-sandbox, --disable-dev-shm-usage, --disable-gpu |
Start Chrome without its own sandbox, without /dev/shm and without a GPU device |
interpreter_runtime on chrome-headless-shell |
Grants the browser folder, the fonts, the system library folder and the PCI device tree |
interpreter_runtime on node |
Grants OpenSSL's configuration file |
memory_bytes, cgroup_pids_max, cgroup_memory_bytes |
An address-space limit V8 runs under, and caps on the whole command tree |
node node_modules/@playwright/test/cli.js test |
Runs the suite without executing a file from the workdir |