How to run Node tests in a capsule
A coding capsule that changes a JavaScript project should prove its change by running the project's
tests, and it should do that inside the same boundary as everything else it runs. This guide builds
a scoped capsule that installs a project's dependencies with npm ci and runs its
vitest suite, with only the grants Node needs: the npm package tree, the
system library folder native addons load from, OpenSSL's configuration file, and an address-space
limit V8 can run under. The
capsule's /proc and the files that resolve localhost come with scoped and need no grant.
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.network.allow | The hosts the capsule's commands may reach |
| capabilities.resources.memory_bytes | The address-space limit on each command |
| capabilities.resources.cgroup_memory_bytes | The memory the capsule's whole command tree may use |
| lifecycle.done_check | The command that decides whether a task is done |
Step 1 — find the three host paths
Node's grants name three paths on your host.
| Path | Holds | How to find it |
|---|---|---|
| The npm package tree | npm's own JavaScript, which the npm command runs |
echo "$(npm root -g)/npm" |
| The system library folder | The shared libraries a native addon loads, such as rollup's librt.so.1 |
ldd node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node after a local npm ci, or ldconfig -p \| grep librt.so.1 |
| OpenSSL's configuration file | The file node reads at startup |
The path node names in node: OpenSSL configuration error: … calling fopen(<path>, rb) when it is refused |
On an x86-64 Debian or Ubuntu host with Node installed through nvm, they are
/home/<you>/.nvm/versions/node/<version>/lib/node_modules/npm, /usr/lib/x86_64-linux-gnu and
/etc/ssl/openssl.cnf.
Step 2 — write the manifest
Create a murmur.yaml file next to your project's package.json and package-lock.json, with
your three paths in place of the ones below:
name: node-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
- env
- node
- npm
interpreter_runtime:
- binary: npm
dirs:
- path: /home/you/.nvm/versions/node/v22.23.2/lib/node_modules/npm
list_dir: true
- binary: node
dirs:
- path: /usr/lib/x86_64-linux-gnu
list_dir: false
- path: /etc/ssl/openssl.cnf
list_dir: false
network:
allow:
- https://registry.npmjs.org
resources:
memory_bytes: 68719476736
cgroup_memory_bytes: 8589934592
inference:
transport: http
model: o3-mini-high
driver:
artifact: murmur/driver-openai
lifecycle:
done_check:
command: "bash -c 'npm ci --ignore-scripts && node node_modules/vitest/vitest.mjs run'"
limit: 0
What each grant is for:
| Grant | Why |
|---|---|
env in shell.allow |
npm is a script whose first line is #!/usr/bin/env node |
interpreter_runtime on npm |
npm reads its own package tree, outside the workdir |
interpreter_runtime on node: the library folder |
A native addon in node_modules loads system libraries the sandbox does not know about until it loads — vitest's rollup fails with librt.so.1: cannot open shared object file without it; see Native addons. This grant carries execute and draws W-SEC-009 |
interpreter_runtime on node: openssl.cnf |
node reads OpenSSL's configuration file at startup and exits with code 13, node: OpenSSL configuration error, when it is refused. One file, not its folder |
memory_bytes: 68719476736 |
memory_bytes limits address space, and V8 reserves far more of it than it uses: below 32 GiB vitest fails with Cannot allocate Wasm memory, and W-RUN-010 says so |
cgroup_memory_bytes |
The real cap on the memory the command tree uses, here 8 GiB |
Nothing grants /proc, /etc/hosts or /etc/nsswitch.conf. On scoped, each command has
its own /proc, where os.cpus() and vitest's worker
pool find what they read, and reads the two name-resolution files
that resolve localhost.
Step 3 — run the tests the way scoped allows
scoped does not run files from the workdir, and every file under node_modules is in the workdir.
That decides the two commands:
| Command | Instead of | Why |
|---|---|---|
npm ci --ignore-scripts |
npm ci |
An install script runs a file from the workdir |
node node_modules/vitest/vitest.mjs run |
npm test, npx vitest, node_modules/.bin/vitest |
Each of those executes a file under node_modules; node reading vitest's entry point is node running, which shell.allow permits |
A toolchain that spawns a native binary from node_modules — esbuild for TypeScript, swc — needs
capabilities.filesystem.workdir_exec: true,
which caps the capsule at advisory. A plain-JavaScript project with no vite or vitest config
needs neither.
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 prints no W-RUN-010 for this manifest, and its AppArmor / user namespaces block
reads private proc: created (…). mur run --workdir . --explain-scope shows the same view in
its Process view block:
Process view
/proc: private (pid-namespace) — the session's own processes and the kernel's world-readable files; no host process
grant: read and list (scoped, sealed); never execute or write
Step 5 — run the capsule
Create a task.md file asking for the change you want, then run it with the project folder as the
workdir, so the commands run where package.json is:
mur run --workdir . --task task.md
murmur: url localhost:52222
session: ses_019ed2af53da75c2aefee84ee10c34af
status: ok
When the agent stops, the runtime runs the done check in the work folder. The session's trace
records each run as a done_check event: outcome: passed and exit_code: 0 when every test file
passed. With limit above 0, a failing run reopens the task and hands the agent what the
command printed.
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; each command gets its own /proc and reads /etc/hosts and /etc/nsswitch.conf |
interpreter_runtime on npm |
Grants npm's package tree, read and list |
interpreter_runtime on node |
Grants the system library folder native addons load from and OpenSSL's configuration file; fires W-SEC-009 |
memory_bytes: 68719476736 |
An address-space limit V8 runs under |
cgroup_memory_bytes |
Caps the memory the command tree uses |
npm ci --ignore-scripts, node node_modules/vitest/vitest.mjs run |
Install and test without executing a file from the workdir |
lifecycle.done_check |
Runs the install and the tests when the agent stops; limit: 0 records the result without reopening the task |