Skip to content

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