Diagnostics
mur reports every problem it finds with a code. An E-* code is a failure that stops the
command; a W-* code is a warning that lets it continue. The index lists every code with the
section that explains it.
Index
| Code | Meaning | Details |
|---|---|---|
E-BLD-001 |
Manifest name: is not a valid artifact identifier |
E-BLD-001 |
E-BLD-002 |
requires_files: entry is unsafe (absolute, .., symlink) or collides inside the archive |
E-BLD-002 |
E-BLD-003 |
Packed entry set is not a launchable wasm payload | E-BLD-003 |
E-CAP-001 |
A capabilities.network.allow entry could not be parsed |
E-CAP-001 |
E-CAP-002 |
A filesystem.scope is not relative to the workdir, or escapes it via .. |
E-CAP-002 |
E-CAP-003 |
Declared containment floor (advisory|scoped|sealed) is not achievable on this host |
E-CAP-003 |
E-CAP-004 |
capabilities.shell.staged_runtime is declared below an effective sealed containment floor |
E-CAP-004 |
E-CAP-005 |
This host cannot give the capsule's native subprocess tree its own network namespace, so capabilities.network.allow cannot be enforced for it |
E-CAP-005 |
E-CAP-006 |
Nothing declared makes an allowlisted interpreted entrypoint's package tree reachable inside a sealed composed root |
E-CAP-006 |
E-CAP-007 |
an export root resolves outside the accessible workdir | E-CAP-007 |
E-CAP-008 |
a persistent capsule declares exports.peer_files without a short enough max_ttl |
E-CAP-008 |
E-CAP-009 |
capabilities.state.store does not name a usable durable store |
E-CAP-009 |
E-CAP-010 |
An artifact entry's config: block cannot be delivered as MURMUR_ARTIFACT_CONFIG |
E-CAP-010 |
E-CAP-011 |
context.record_store, or mur run --context, does not name one conversation record directory |
E-CAP-011 |
E-CAP-012 |
A capabilities.filesystem.read_only entry is not a usable workdir subpath |
E-CAP-012 |
E-CAP-013 |
An artifact claims the name of a tool the runtime provides itself | E-CAP-013 |
E-CAP-014 |
A variable this project needs is set by nothing in the environment | E-CAP-014 |
E-CAP-015 |
A capsule declares a capabilities.env.allow entry the capsule that spawns it does not hold |
E-CAP-015 |
E-CAP-016 |
A capabilities.env.allow entry names a variable the credential backstop strips from every guest |
E-CAP-016 |
E-CNV-001 |
No such record store or context id under ~/.murmur/conversations/ |
E-CNV-001 |
E-CNV-002 |
A context id is present under more than one record store | E-CNV-002 |
E-CNV-003 |
mur conversation truncate --keep is not a usable number of messages to keep |
E-CNV-003 |
E-CFG-001 |
No inference provider configured and wizard cannot run in non-interactive mode | mur new |
E-CFG-002 |
mur config set given an unsupported dotted key |
mur config |
E-DEPLOY-001 |
No --host given, or an --env value is not KEY=VALUE |
mur deploy run |
E-DEPLOY-003 |
SSH connection or remote command failed | mur deploy run |
E-DEPLOY-004 |
Capsule did not emit usable startup JSON within 120s | mur deploy run |
E-DEPLOY-006 |
The pinned mur release could not be fetched from GitHub |
mur deploy run |
E-EVAL-001 |
Eval file parse error (malformed JSON, unknown record_type, missing required field); message includes :line: number |
eval.jsonl schema |
E-EVAL-002 |
No eval file found for the session named on the command line | mur eval |
E-IO-001 |
File or directory not found | — |
E-IO-002 |
Permission denied on a host path (the host's own permissions, not a capsule capability) | — |
E-IO-003 |
General I/O error (read/write failure) | — |
E-MAN-001 |
Missing required manifest field | — |
E-MAN-002 |
YAML syntax error in manifest | — |
E-MAN-003 |
Field type mismatch in manifest, or a structurally valid value the runtime rejects (artifact entry, inference config, capability config), or an inference.api_key: ${NAME} that neither credentials.NAME in ~/.murmur/config.yaml nor the environment variable NAME holds |
Where ${NAME} is read from |
E-NEW-001 |
The generator agent produced no out/murmur.yaml |
mur new |
E-REG-001 |
Artifact not found in registry, or found in a release that publishes no asset for the host platform | mur install |
E-REG-002 |
Installed artifact bytes do not match the sha256 recorded for them | Lockfile |
E-REG-003 |
An artifact of that name and version is already published | mur publish |
E-REG-004 |
Reserved version string (latest, stable, edge) |
mur publish |
E-REG-005 |
A registry-resolved artifact's version or hash disagrees with the murmur.lock entry |
Lockfile |
E-RUN-001 |
Capsule crashed, compile failure, missing component export, execution deadline exceeded (capabilities.limits.deadline_seconds), or resource limit exceeded (capabilities.limits.memory_bytes/table_elements) |
Execution limits |
E-RUN-002 |
Missing WASI import (linker error) | — |
E-RUN-003 |
Unsupported lock_version, missing lock entry, or a lock entry with no hash for this host's platform |
Lockfile |
E-RUN-004 |
Capsule WASM not found at expected path | — |
E-RUN-005 |
Inference driver not configured in manifest | Inference configuration |
E-RUN-006 |
Inference driver artifact not installed, or inference.command is not on PATH |
Inference configuration |
E-RUN-007 |
Agent loop failed at runtime | — |
E-RUN-008 |
Required artifact not installed locally | mur run |
E-RUN-009 |
inference.system_prompt_file (or the compaction system-prompt file) could not be read |
inference.system_prompt |
E-RUN-010 |
network.internal_port is already bound |
— |
E-RUN-011 |
A native subprocess was killed for exceeding a capabilities.resources limit |
Which limit a subprocess hit |
E-RUN-012 |
The capsule can spawn native subprocesses but no cgroup v2 scope could be delegated to bound them (Linux only) | Platform behavior |
E-RUN-013 |
Session workdir grew past capabilities.resources.workdir_max_bytes |
Host resource limits |
E-RUN-014 |
A sealed session cleared the host probe at launch but its composed root could not be built for a subprocess |
Containment class |
E-RUN-015 |
mur run --resume and --context were both given |
E-RUN-015 |
E-RUN-016 |
The session --resume named records no task_start carrying a context id |
E-RUN-016 |
E-RUN-017 |
The context --resume resolved to kept no conversation record |
E-RUN-017 |
E-RUN-018 |
--resume-mode compact with no hook bound to on-compaction |
E-RUN-018 |
E-RUN-019 |
A session that can delegate could not register with mur-roost |
E-RUN-019 |
E-RUN-020 |
MURMUR_SPAWNER is set to something that is not a spawner handle |
E-RUN-020 |
E-RUN-021 |
A staged native tool's binary is built for another operating system or CPU architecture | E-RUN-021 |
E-RUN-022 |
A session address names no capsule running on this machine | E-RUN-022 |
E-RUN-023 |
The capsule a session address named is running and did not answer | E-RUN-023 |
E-RUN-024 |
The session named could not be ended and is still running | E-RUN-024 |
E-RUN-025 |
The transport: http inference driver declares no usable inference_auth: block |
E-RUN-025 |
E-RUN-026 |
spend.machine_tokens_per_day is set and the spend ledger under ~/.murmur/spend cannot be used |
E-RUN-026 |
E-RUN-027 |
The provider kept rejecting the inference credential after it was re-read | E-RUN-027 |
E-RUN-028 |
The running-capsule records under ~/.murmur/running/ could not be read |
E-RUN-028 |
E-TOP-001 |
Tempo endpoint unreachable, or invalid --window format |
mur topology |
E-TOP-002 |
Tempo HTTP query failed (search or trace fetch) | mur topology |
E-TOP-003 |
Tempo response JSON parse failure | mur topology |
E-TRC-001 |
Trace file parse error (malformed JSON, missing required session_start/session_end, empty file); unknown event types are silently skipped. Also a mur trace show --body selector that names no recorded hash, or a hash whose body was never stored |
trace.jsonl schema, mur trace show --body |
E-TRC-002 |
No session found in the workdir, or a session selector matched none or several | mur trace |
W-BLD-001 |
A declaration names an archive entry the packer already fills | W-BLD-001 |
W-BLD-002 |
capsule.wasm shadows another root *.wasm |
W-BLD-002 |
W-BLD-003 |
A compiled artifact packages build inputs | W-BLD-003 |
W-REG-001 |
An installed native artifact has no recorded platform | W-REG-001 |
W-REG-002 |
A capsule in a formation could not be inspected | W-REG-002 |
W-RUN-001 |
A turn stopped at the inference.max_tokens output cap |
W-RUN-001 |
W-SEC-001 |
No kernel-level subprocess sandbox on this platform | W-SEC-001 |
W-SEC-002 |
Linux host without Landlock — filesystem scope and exec unenforced | W-SEC-002 |
W-SEC-003 |
network.allow doesn't constrain bash's own outbound connections |
W-SEC-003 |
W-SEC-004 |
Literal secret value found in a manifest field | W-SEC-004 |
W-SEC-005 |
Linux kernel enforcement is in force — what it covers, and the one key that re-widens it | W-SEC-005 |
W-SEC-006 |
A hook's capabilities: block declares a sub-key that is inert on hooks |
W-SEC-006 |
W-SEC-007 |
A tool/driver narrowed to a host the capsule-wide ceiling does not allow — the entry was dropped | W-SEC-007 |
W-SEC-008 |
A tool/driver capabilities: block declares something per-artifact narrowing does not apply |
W-SEC-008 |
W-SEC-009 |
capabilities.shell.interpreter_runtime couples the capsule to a specific host interpreter-version layout |
W-SEC-009 |
W-SEC-010 |
No cgroup on this platform — the subprocess tree has no aggregate memory/pids/cpu bound | W-SEC-010 |
W-SEC-011 |
An executable workdir makes capabilities.shell.allow advisory |
W-SEC-011 |
W-SEC-012 |
A compiler driver's helper binaries have no Execute grant under sealed |
W-SEC-012 |
W-SEC-013 |
Unprivileged user namespaces are unrestricted host-wide, not granted to mur by the shipped AppArmor profile |
W-SEC-013 |
W-SEC-014 |
A capsule-wide capabilities.state block grants nothing — a durable store is granted per artifact |
W-SEC-014 |
W-SEC-015 |
A config: block on a native tool delivers nothing — config reaches WASM tools, drivers and hooks |
W-SEC-015 |
W-SEC-016 |
A capsule-wide capabilities.conversation block grants nothing — the grant is per hook |
W-SEC-016 |
W-SEC-017 |
capabilities.filesystem.read_only is advisory for an allowlisted interpreter |
W-SEC-017 |
W-SEC-018 |
An installed tool's input_schema leaves a path-shaped property undeclared, so calls naming it are judged by key name |
W-SEC-018 |
W-SEC-019 |
A key in murmur.yaml this build does not recognize was parsed and ignored |
W-SEC-019 |
W-SEC-020 |
The capsule can delegate, and its lifecycle block cannot receive a delegation's outcome |
W-SEC-020 |
W-SEC-021 |
A cgroup scope was created and the declared cgroup_io_bytes_per_sec ceiling did not apply to it |
W-SEC-021 |
W-SEC-022 |
The capsule can run shell commands, and its lifecycle block cannot receive a background command's completion |
W-SEC-022 |
W-SEC-023 |
A session opened its door and its running-capsule record could not be written | W-SEC-023 |
W-SEC-024 |
capabilities.env.allow names a credential-shaped variable — the grant hands the capsule a secret murmur does not broker |
W-SEC-024 |
W-SEC-025 |
capabilities.network.allow names the inference endpoint — inference does not use the entry, and it grants direct reach to that host without the key |
W-SEC-025 |
W-SEC-026 |
spend.machine_tokens_per_day is set and the capsule uses transport: process, whose spend murmur neither counts nor limits |
W-SEC-026 |
W-SEC-027 |
The inference key is read only at launch — from the environment or a literal in murmur.yaml — so a rotated key does not reach the running capsule |
W-SEC-027 |
W-SEC-028 |
A file under ~/.murmur that holds a secret, or the config file an inference key was read from, is readable by other accounts |
W-SEC-028 |
W-SEC-029 |
A compiler driver could not be run to ask where its helper binaries live, so W-SEC-012 was not evaluated for it |
W-SEC-029 |
Capability errors
E-CAP-001 — invalid network.allow entry
An entry in a capabilities.network.allow list could not be parsed, so the capsule is refused
before any registry pull, artifact compile or workdir creation:
error[E-CAP-001]: invalid network allow entry '<entry>': <reason>
The accepted forms are a full URL, a bare host, and a host with a port. A path, query or fragment
on the entry, and any scheme other than http/https, are rejected — see
Network allow entries.
E-CAP-002 — invalid filesystem.scope
A filesystem.scope value is not a usable subdirectory of the session workdir:
error[E-CAP-002]: invalid filesystem scope '<scope>': scope must be relative to the workdir
error[E-CAP-002]: invalid filesystem scope '<scope>': scope cannot escape the workdir via '..'
The same two rules apply wherever a scope is declared: the capsule-wide
capabilities.filesystem.scope, a per-hook grant, and a per-tool or per-driver narrowing. Like
E-CAP-001, the check runs before any registry pull, artifact compile or workdir creation.
scope: "." is the explicit "whole workdir" grant. See
Filesystem scope.
E-CAP-003 — declared floor not achievable on this host
If the achieved class is weaker than the effective declared floor, mur run refuses to launch
before any registry pull, artifact compile, or workdir creation. The message names both classes
and the reason; the hint is always the same pair of remedies — lower the declared floor
(capabilities.containment in murmur.yaml, containment in .murmur/config.yaml, or
--containment), or run on a host that provides the declared class.
error[E-CAP-003]: declared containment class 'scoped' is not achievable on this host (achieved: 'advisory'): scoped requires Landlock filesystem mediation (Linux 5.13+ with a usable Landlock ABI); this host provides no kernel filesystem mediation, so paths outside the workdir are constrained by convention only
hint: lower the declared floor to 'advisory' (capabilities.containment in murmur.yaml, containment in .murmur/config.yaml, or --containment), or run on a host that provides 'scoped'
For sealed the reason names the specific missing piece and the command that fixes it, because
"no mount namespace here" has several completely different causes. The runtime never falls back to
a weaker class, whichever one it reports:
| Reason reported | What to do |
|---|---|
| This platform has no mount namespace and never will | Nothing — macOS and every other non-Linux host stay at advisory permanently |
AppArmor's unprivileged-userns restriction is active while the mur-sealed profile is not confining this binary |
Install and load the profile shipped with mur: sudo install -m 644 packaging/apparmor/mur-sealed /etc/apparmor.d/mur-sealed && sudo apparmor_parser -r /etc/apparmor.d/mur-sealed, or re-run the mur installer as root. Running a checkout build out of ./target, which no shipped profile attaches to: sudo scripts/install-dev-apparmor.sh. Last resort, only where a profile genuinely cannot be loaded: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0, which removes unprivileged-userns hardening from every program on the machine and reports W-SEC-013 |
unshare(CLONE_NEWUSER \| CLONE_NEWNS) was refused — the usual answer inside a container, where CAP_SYS_ADMIN is absent or the container's own seccomp filter blocks unshare(2) |
Add --cap-add SYS_ADMIN to the container invocation, or establish the mount namespace outside the container and run mur inside it |
The namespace was created but its identity uid_map/gid_map could not be written, so the process cannot own it |
Check that /proc/sys/user/max_user_namespaces is non-zero, that no LSM policy blocks uid_map writes for this binary, and that mur is not already running inside an unmapped user namespace |
The namespace was created but mount(2) inside it was refused — a confinement that permits userns_create and then withholds CAP_SYS_ADMIN |
Load the shipped profile with sudo apparmor_parser -r /etc/apparmor.d/mur-sealed; inside a container, add --cap-add SYS_ADMIN, or establish the mount namespace outside it |
The kernel provides no unprivileged user namespaces at all (CONFIG_USER_NS=n, or user.max_user_namespaces=0) |
sudo sysctl -w user.max_user_namespaces=10000 if the sysctl is merely zeroed, otherwise run on a kernel built with CONFIG_USER_NS=y |
No usable Landlock ABI, which sealed keeps inside the composed root as defence in depth |
Run on Linux 5.13+ — a host that cannot back scoped cannot back sealed either |
One case names the manifest rather than the host, because no host can satisfy it:
capabilities.filesystem.workdir_exec: true declared
alongside capabilities.containment: scoped (or sealed). An executable workdir keeps the
Landlock Execute right on the session workdir, so a binary the capsule compiles, downloads or
renames inside it runs regardless of capabilities.shell.allow — the allowlist stops being an
enforceable property of the capsule, and the achieved class is capped at advisory on every host.
Remove workdir_exec, or lower the declared floor to advisory.
A sealed host that clears the probe at launch and then fails to build the composed root for a
particular subprocess is a different event and gets its own code, E-RUN-014. Two causes reach it,
and the message names the failing step and its errno:
- A
capabilities.shell.staged_runtimegrant whosesource_pathdoes not exist on this host. Each grant is planned as a required bind ahead ofpivot_root, so themount(2)fails withENOENTand the session aborts. Fix the path, or drop the grant. - Something moved underneath the runtime mid-session — an AppArmor profile reloaded, a container
policy changed. Re-probe with
mur run --explain-scope.
Neither means the declared floor was wrong; E-CAP-003 covers that.
E-RUN-015 — --resume and --context together
mur run --resume <session> resolves a session address to the context id that
session ran under, and then runs exactly as --context <id> would. The two name the same thing two
ways, so passing both is refused rather than resolved by precedence:
error[E-RUN-015]: --resume and --context name the same thing two ways
hint: --resume <session> resolves that session's context id for you; --context <id> names one directly. Pass whichever you have, not both — see docs/content/reference/cli.md
Refused before the session address is resolved and before anything is staged, so no session directory appears.
E-RUN-016 — the resumed session ran no task
--resume reads the context id off the named session's trace.jsonl, from the first
task_start line. A session that never
reached a task carries none, and there is no conversation to continue:
error[E-RUN-016]: cannot resume session ses_0193f2…: its trace.jsonl records no task_start carrying a context id
hint: only a session that actually ran a task has a conversation to continue. Run `mur trace show <session>` to see what it did, and resume one that reached a task — see docs/content/reference/cli.md
Run mur trace show <session> to see what that session did.
E-RUN-017 — the resumed context kept no record
The context id --resume resolved has no
conversation record on disk. Resuming it would start a fresh
conversation while reporting success, so it is refused instead, naming the session, the context and
which of the reasons applies:
error[E-RUN-017]: cannot resume session ses_0193f2…: context 'ctx_0193f2…' has no conversation record (the capsule declares inference.transport: process, whose CLI owns its own conversation, and kept no conversation record)
hint: a session is resumable only if its capsule kept a conversation record: an http-transport capsule that did not declare context.record: off. Run `mur trace show <session>` to see what that session did, and omit --resume to start a fresh conversation — see docs/content/reference/cli.md
The reasons: context.record: off,
inference.transport: process — whose CLI owns its own
conversation — a capsule with no inference: block, a host whose home directory cannot be
resolved, and a record path that resolves but holds no file. Refused at staging, before this
launch's session directory is created.
E-RUN-018 — --resume-mode compact with no compaction hook
--resume-mode compact runs the capsule's on-compaction hook over the loaded record and
continues from its summary. With nothing bound to that event there is nothing to produce the
summary, and quietly serving full instead would give the operator a mode they did not ask for:
error[E-RUN-018]: --resume-mode compact needs a hook bound to on-compaction; this capsule declares none
hint: declare a hook artifact whose binding is on-compaction (or all) with commit_policy: replace-context, or use --resume-mode full, which loads the record verbatim and needs no hook — see docs/content/reference/cli.md
--resume-mode full is often the cheaper mode anyway: a verbatim reload can hit the provider's
prompt cache, while compaction changes the prefix from the first altered token, guarantees a cache
miss, and costs an extra inference call to produce the summary.
E-RUN-019 — the session could not register with mur-roost
A capsule whose manifest declares capabilities.spawn.allow is refereed by mur-roost every time
it asks to spawn something, and the daemon can only referee a session whose grants it holds. The
session announces itself at launch (see
POST /register); a registration it cannot complete refuses the
launch rather than leaving a capsule that can delegate running outside the knowledge of the daemon
that bounds it:
error[E-RUN-019]: failed to register this session with mur-roost at http://127.0.0.1:7700: failed to connect to 127.0.0.1:7700: Connection refused (os error 111); a capsule declaring capabilities.spawn.allow must be known to the daemon that referees its spawns, so the launch is refused rather than run unrefereed
hint: start the daemon the capsule registers with — `mur-roost --port 7700 --registry-path <store>` — and set MURMUR_ROOST_URL to its base URL, or drop capabilities.spawn.allow from the manifest if this capsule spawns nothing
MURMUR_ROOST_URL unset or blank reports the same code with MURMUR_ROOST_URL is not set as the
reason. The message names the daemon and the reason, never a token.
mur doctor reports the same three states — mur-roost not installed, MURMUR_ROOST_URL not set,
and nothing answering at the URL it names — for a capsule declaring capabilities.spawn.allow, so
the refusal is visible before a run meets it. See
Start the daemon for how to obtain mur-roost and start it.
A capsule that declares no spawn capability never reaches this — it opens no connection to the
daemon at all, and mur run succeeds with nothing listening.
E-RUN-020 — the spawner handle could not be read
A capsule launched as a delegated child is handed MURMUR_SPAWNER, which names where its outcome
is reported and under which delegation id (see
The completion path). A child that cannot read it can tell
nobody that it finished, so the launch is refused before a session is staged:
error[E-RUN-020]: MURMUR_SPAWNER does not carry a readable spawner handle: it is not JSON: expected value at line 1 column 1; a delegated child must be able to tell its spawner that it finished, so the launch is refused rather than run unreportable
hint: MURMUR_SPAWNER is injected by a parent capsule's runtime at launch; unset it to run this capsule directly
The reason names what about the value could not be read — that it is not JSON, that a field is
missing, or that trust is not a trust class — and never the value itself, which names another
capsule's address and session.
An unset or blank MURMUR_SPAWNER is not this error: it is the ordinary case of a capsule nobody
delegated, which reports to nobody and runs exactly as it would have.
E-RUN-021 — the native binary is built for another platform
A native tool artifact carries a host executable at bin/<name>. Staging compares the platform
that binary was built for against this host's, before any of the session's native binaries is
written to the workdir:
error[E-RUN-021]: native tool 'murmur-tool-git' cannot run on this host: its binary is built for darwin-aarch64, this host is linux-x86_64
hint: the installed artifact holds a binary for another platform — reinstall it on this host with `mur install <name>@<version>`, or publish a build for this host's platform
The check reads the binary's header and recognises these formats:
| Format | Identified from | Platforms |
|---|---|---|
| ELF64 | e_machine |
linux-x86_64, linux-aarch64 |
| Mach-O 64-bit | cputype |
darwin-x86_64, darwin-aarch64 |
| Fat Mach-O | 0xCAFEBABE magic |
any darwin-* host |
Architecture is compared as strictly as operating system: an x86_64 ELF on an aarch64 Linux
host is refused on the same terms as a Mach-O on Linux.
Refusal requires a positive identification of both sides. A payload in a format the check does not recognise — a shell script, a WASM module, a 32-bit image — stages and runs, and so does any payload on a host outside the four platform targets.
When a capsule declares several native tools, one unrunnable binary refuses all of them: a refused session leaves no tool binaries in its workdir.
mur doctor reads the same header of the same installed bytes and fails that
artifact's line, so the mismatch is reportable without launching a session.
E-RUN-022 — the address names no running capsule
mur watch and mur cancel resolve a
session address against the
running-capsule records on this machine. This is the refusal when
none of them names a capsule that is still there.
error[E-RUN-022]: ses_019f01a940ce7761854e768ecbe3d399 is not running: no process holds pid 48213
hint: its record has been removed; start the capsule again to make the address resolve
The record naming a session whose process is gone is removed by the read that reported this, so a second attempt reports that nothing is running rather than naming the same session twice:
error[E-RUN-022]: no capsule is running on this machine
hint: start a capsule with `mur run`; it is addressable for as long as it serves
The reason names which of the two local checks the record failed.
| Reason | Means |
|---|---|
no process holds pid N |
Nothing is running under that process id |
pid N is held by a process that started at another time |
The process id was handed out again to something unrelated |
The same code covers an address that is not a session address at all — a path, a host:port, or
fewer than four characters:
error[E-RUN-022]: 'localhost:41235' is not a session address
hint: name a running session with @1, a ses_ id, or a 4-character suffix of one
To reach a capsule by address rather than by name, pass --url <host:port>.
E-RUN-023 — the capsule did not answer
The address named a capsule whose process is alive, and the capsule's agent card did not come back:
error[E-RUN-023]: ses_019f01a940ce7761854e768ecbe3d399 is running but its capsule did not answer at localhost:41235: failed to connect to localhost:41235: Connection refused (os error 111)
hint: the process holding the door is alive — it may be mid-turn; its record is kept
E-RUN-022 removes the record; this one keeps it. The process is running, so the record still
names something real, and a capsule that was slow to answer must not lose the only handle anyone
has on it. Try again, or read what the session is doing with
mur trace show in its workdir.
| Reason | Means |
|---|---|
failed to connect to …, or a read timeout |
The door did not answer in time — the capsule may be mid-turn |
the capsule at … answers for session … |
Another session holds that address |
pid N's start time could not be read |
A process holds the process id and the host would not say when it started, so whether it is the capsule is unknown |
E-RUN-024 — the session could not be ended
mur stop could not end the process a running-capsule
record names: a signal was refused, or the process is still there
afterwards.
error[E-RUN-024]: ses_019f01a940ce7761854e768ecbe3d399 could not be ended: SIGTERM to pid 48213 was refused: Operation not permitted (os error 1)
hint: the capsule is still running and its record is kept; the process may belong to another user
The message names which signal was refused and why.
| Reason | Means |
|---|---|
SIGTERM to pid N was refused: pid N's start time could not be read |
A process holds the process id and the host would not say when it started, so it could not be confirmed as the capsule and was not signalled |
SIGTERM to pid N was refused: … with an OS error |
The process is there and this user may not signal it — usually another user's capsule |
SIGKILL to pid N was refused: … |
The same two, at the escalation step |
pid N was still running 5 seconds after SIGKILL |
The kernel accepted the signal and the process has not gone, which an uninterruptible wait can cause |
The record is not removed. Unlike E-RUN-022, the capsule it names is still
running, so unlinking the record would remove the only handle anyone has on it. Every task the
session held was cancelled before the signalling started, so the capsule is idle even though it is
still there.
E-RUN-025 — the inference driver declares no usable inference_auth: block
A transport: http capsule names a driver whose own murmur.yaml does not say how its provider
takes the API key. The runtime attaches the key to each driver request itself, so without that
declaration it cannot authenticate the driver, and mur run refuses at staging, before any
component runs or any request is sent. The refusal applies whether or not inference.api_key is
set.
error[E-RUN-025]: inference driver 'murmur-driver-anthropic@1.0.0' declares no usable inference_auth: block
hint: the runtime presents the provider key itself and needs the driver to say how; update the driver to a version whose murmur.yaml declares inference_auth:
A block that is present and malformed adds the reason in parentheses:
| Reason names | Means |
|---|---|
'inference_auth' has invalid type |
The block is not a mapping |
missing required field 'inference_auth.header' or 'inference_auth.value' |
A child is absent |
'inference_auth.header' has invalid type or 'inference_auth.value' has invalid type |
A child is not a string |
is not a valid HTTP header name |
header cannot be sent as a header |
must contain {key} exactly once |
value has no {key}, or more than one |
contains characters an HTTP header value cannot hold |
value has a control character such as a newline |
The block's shape is in Driver inference_auth: block.
E-RUN-026 — the spend ledger cannot be used
spend.machine_tokens_per_day is set, and the shared ledger the machine ceiling
is counted in cannot be used: HOME is unset, ~/.murmur/spend cannot be created or held as a
0700 directory, or today's file cannot be opened. mur run refuses a transport: http capsule
at staging, before the session directory exists and before any provider request.
error[E-RUN-026]: spend ledger at /home/me/.murmur/spend is unavailable: failed to create the directory: File exists (os error 17)
hint: spend.machine_tokens_per_day is set in config.yaml, and the machine spend ceiling cannot be kept without its ledger; make ~/.murmur/spend a directory this user can write, or remove spend.machine_tokens_per_day
E-RUN-027 — the provider rejected the inference credential
The provider answered a transport: http inference request with 401, and re-reading the
credential did not cure it. When a request is rejected, the runtime re-reads
credentials.<NAME> at once. If the value there has changed, the request
is sent once more with it. The task ends with this error when the value is unchanged, or when that
single resend is rejected too. It replaces the driver's own error text. The same message, without
the code, is written to out/result.txt, and the session fails.
error[E-RUN-027]: the provider rejected the inference credential credentials.ANTHROPIC_API_KEY in /home/me/.murmur/config.yaml (HTTP 401)
hint: replace it with `mur config set -g credentials.ANTHROPIC_API_KEY <key>`; running capsules use it on their next call
What the message names depends on where the key came from:
| Source | Message names | Hint |
|---|---|---|
credentials.<NAME> in the global config |
The entry and the config file path | Replace the entry; running capsules use it on their next call |
The environment variable <NAME> |
The variable, and that it is read once at launch | Restart the capsule with a valid key, or store the key as credentials.<NAME> |
A literal inference.api_key in murmur.yaml |
The manifest literal, and that it is read once at launch | As for the environment |
When the single resend was also rejected, the message adds that the credential was re-read and its
new value was rejected too. The session trace records the rejection as an
inference_credential event with
change: "rejected". Any status other than 401, including 403, reaches the driver unchanged
and is reported as the driver reports it.
E-RUN-028 — the running-capsule records could not be read
mur ps, mur watch, mur cancel and
mur stop read the running-capsule records
under ~/.murmur/running/, and that directory could not be created, held at 0700, or listed.
Which capsules are running is unknown, so nothing is listed, nothing is removed and nothing is
signalled.
error[E-RUN-028]: the running-capsule records could not be read: /home/me/.murmur/running: failed to create the directory: File exists (os error 17)
hint: nothing was listed and nothing was removed; check that ~/.murmur/running is a directory this user owns
The message ends with the path and the operating system's error.
| Error | Means |
|---|---|
File exists |
A file is where the directory belongs |
Permission denied |
The directory exists and this user may not list it |
E-CAP-004 — staged runtime below the sealed floor
A staged_runtime grant is staged into a composed root, and a composed root is built only for a
capsule that asked for sealed (see
A weaker declaration is never silently upgraded). A capsule
declaring staged_runtime below an effective sealed floor is therefore refused before any
registry pull, artifact compile or workdir creation:
error[E-CAP-004]: capabilities.shell.staged_runtime is declared for python3 but the effective containment floor is 'scoped' — staging a runtime tree requires the 'sealed' floor, because there is no composed root to bind-mount it into below that
hint: set `capabilities.containment: sealed` in murmur.yaml (or pass `--containment sealed`) so the capsule gets a composed root to stage the runtime into, or remove the capabilities.shell.staged_runtime grant.
The check reads the declared floor alone, so it fires identically on a host that could deliver
sealed. That is what separates it from E-CAP-003, whose remedy points the opposite way:
E-CAP-003 |
E-CAP-004 |
|
|---|---|---|
| What went wrong | the host is too weak for what the capsule declared | the capsule declared too little for what it asked for |
| Remedy | lower the floor, or move hosts | raise the floor, or drop the grant |
mur doctor surfaces the same condition as a warning ahead of a run.
E-CAP-005 — no network namespace for the subprocess tree
capabilities.network.allow is enforced for a native subprocess by putting its whole tree in a
network namespace of its own, whose only way out is an egress proxy in the runtime process. A
Linux host that cannot create that namespace refuses the launch:
error[E-CAP-005]: this host cannot give the capsule's subprocess tree its own network namespace, so capabilities.network.allow cannot be enforced for it: <reason>
Two host conditions produce it, and the refusal text names which one:
| Reason reported | What to do |
|---|---|
The kernel provides unprivileged user namespaces but this host withholds them — AppArmor's restrict_unprivileged_userns is on and the shipped profile is not confining mur, unshare was refused outright (the container case), or the namespace could not be owned or configured |
Install and load the AppArmor profile shipped with mur, or run outside the container restriction. The refusal text names the exact command for the host it printed on |
The kernel does not provide the mechanism at all — CONFIG_USER_NS=n, or user.max_user_namespaces=0 |
Enable user namespaces on the host |
This applies to every Linux capsule that can spawn a subprocess, including one whose
capabilities.network.allow is empty: with the namespace missing, an empty allowlist would mean
unrestricted egress rather than none. There is no path that continues at reduced enforcement.
It is not part of the containment ladder — every class needs the namespace equally — so neither
raising nor lowering a declared floor changes this answer. mur doctor reports the same condition
ahead of a run.
E-CAP-006 — interpreted entrypoint unreachable under sealed
Staging a shell.allow binary's own ELF dependency closure into the composed root is what makes
that binary runnable, and for an interpreted entrypoint it stages nothing useful. A console script
such as pip at ~/.local/bin/pip is a #! script, not an ELF image, so its dependency closure
is empty — and the package it needs (~/.local/lib/python3.12/site-packages/pip) is a different
directory nothing derives. The capsule would launch cleanly and fail deep into a run, so under a
declared sealed floor this is decided at staging, before any registry pull, artifact compile or
workdir creation.
mur run refuses unless one of the following holds: the script already resolves under a fixed
sealed runtime path (/usr, /bin, /sbin, /lib, /lib32, /lib64, /libx32), or it lives
inside a directory a staged_runtime/interpreter_runtime grant already names, or some such grant
names the script or its shebang interpreter:
error[E-CAP-006]: capabilities.shell.allow grants 'pip' (/home/dev/.local/bin/pip, a script run by 'python3') under the 'sealed' containment floor, but nothing declared makes the interpreted entrypoint's own package tree reachable inside the composed root — ...
hint: if this command also has a module form, prefer it: allowlist the interpreter and invoke the module through it — `python3 -m pytest` in place of `pytest` — which runs the same code and needs no grant at all when the interpreter is the distro's ...
Two remedies answer this refusal, and which one you want depends on the command:
| Situation | Remedy |
|---|---|
The command has a module form — python3 -m pytest runs what pytest runs, and node, ruby and perl have the same form |
Allowlist the interpreter and invoke the module through it. Under a distro interpreter this needs no grant at all, because /usr/bin is already a fixed sealed runtime path, and it stays correct across base images. murmur does not check whether a module form exists, so confirm one from the command's own documentation |
| The command has no module form — a wrapper script with no module entry point, or a case where that exact script must run | Declare capabilities.shell.interpreter_runtime (or staged_runtime) for the interpreter, naming the directories its import machinery reads, measured with strace -f -e trace=openat,getdents64 <the command>. Those directories are host-specific — /opt/venv/lib/python3.11/site-packages on one image, a distro dist-packages on the next — so declaring them ties the capsule to one image's layout |
The name match is deliberately loose, and the guarantee is correspondingly narrow: declaring
interpreter_runtime for python3 satisfies every python3 script, whatever directories the
grant names. murmur does not derive an interpreted program's import closure — sys.path, .pth
files and whatever the script does at runtime make that undecidable in general — so this check
verifies you declared something, never that the directory you declared is the right one.
Measuring it is still yours to do, which is why the hint names the strace invocation.
The check reads only the declared floor, never a host probe, and is inert at scoped and
advisory: below sealed there is no composed root, and the host filesystem is simply the host
filesystem. mur doctor surfaces the same condition ahead of a run, as a warning.
Two neighbouring codes are easy to confuse with this one:
| Code | Fires when | Say |
|---|---|---|
E-CAP-004 |
a grant exists at too low a floor | raise the floor |
E-CAP-006 |
the floor is already sealed and no grant exists |
add a grant |
W-SEC-012 |
an allowlisted compiler driver's helpers have no Execute grant |
name the helper's directory in a grant |
E-CAP-007 — export root outside the workdir
exports.files.root and
exports.peer_files.root each name a subtree of the
accessible workdir. When one of those paths already exists and resolves somewhere
else — because it is a symlink pointing out of the workdir — mur run refuses at staging, before
the workdir is created and before any session runs:
error[E-CAP-007]: exports.files.root 'out/' resolves to '/srv/elsewhere', which is outside the capsule workdir '/home/dev/project'
hint: point the export root at a directory inside the capsule workdir. A root that already exists as a symlink out of the workdir is refused whole rather than followed — see docs/content/reference/resource-plane.md
The root is refused whole rather than followed one file at a time: a per-request check would let the first file leave before anyone noticed. A root that does not exist yet is accepted — the agent may create it during a task.
This is not a containment shortfall, and no change to capabilities.containment affects it. An
export is a disclosure the operator makes, and the path named simply is not inside the capsule. See
Resource plane and
Containment and disclosure.
E-CAP-008 — a persistent capsule needs a short handle lifetime
A peer-file handle is bounded by the minting capsule's own
lifetime: the key it is verified with is generated in memory at launch and destroyed at teardown,
so an ephemeral capsule (lifecycle.after_task: exit, the default) needs no declared ceiling at
all.
lifecycle.after_task: sleep withdraws that bound deliberately. The capsule stays alive, its
instance key stays alive, and a handle sitting in persisted A2A message history stays redeemable —
so the declared lifetime becomes the only one there is. It must therefore be declared, and it must
be at most 15m:
error[E-CAP-008]: exports.peer_files with lifecycle.after_task: sleep requires exports.peer_files.max_ttl to be declared and at most 900s (declared 1800s); a handle's lifetime is not a durability mechanism
hint: declare `exports.peer_files.max_ttl: 15m` or shorter, or drop `lifecycle.after_task: sleep` so teardown bounds every handle instead. A consumer that needs these bytes after the capsule is gone should have the operator relaunch the runtime against the still-present workdir and request again — see docs/content/reference/resource-plane.md
mur run refuses at staging, before the workdir is created and before any session runs, so no
trace.jsonl appears.
A handle's lifetime is not a durability mechanism, and the remedy is never a longer one. Workdirs persist past teardown: a consumer that needs the bytes after the capsule is gone should have the operator relaunch the runtime against the same workdir and request again — see The minting key.
E-CAP-009 — capabilities.state.store does not name a usable store
A durable state store is one directory under ~/.murmur/state/, so its
name is one path segment: non-empty, no /, not . or .., not beginning with ., and not
absolute. Anything else names either a directory tree or somewhere outside the state root, and
neither is a store:
error[E-CAP-009]: invalid state store name '../escape': a store name is a single path segment and must not contain '/'
hint: capabilities.state.store names one directory under ~/.murmur/state/, so it must be a single path segment: no '/', no '.' or '..', not absolute, and not starting with a dot. Omit `store:` to use the capsule name — see docs/content/reference/workdir.md
The same code covers a well-formed name whose directory this host cannot supply — an unresolvable
home directory, or a ~/.murmur/state/ that cannot be created as a 0700 directory:
error[E-CAP-009]: state store 'shey' is unavailable at /home/dev/.murmur/state: failed to create the directory: File exists (os error 17)
hint: a capsule declaring capabilities.state needs a resolvable home directory: run with HOME set to an absolute path, and make sure ~/.murmur/state/ can be created as a 0700 directory — see docs/content/reference/workdir.md
Both are decided at staging, before any registry pull, workdir creation or component
instantiation, so nothing is created under ~/.murmur/state/ and no trace.jsonl appears.
mur run --explain-scope refuses the same declarations with the same code, so a manifest that
would not launch does not pass the diagnostic either.
Neither is a containment shortfall, and no floor change fixes one: the remedy is the store name or
the host's home directory, never capabilities.containment.
E-CAP-010 — an artifact's config: block cannot be delivered
config: on an artifact entry travels to that artifact as one
environment variable holding JSON, so the block has to be a string-keyed mapping that serializes to
JSON within 65536 bytes. Each rule refuses by name, quoting what the entry declared:
error[E-CAP-010]: invalid config for artifact 'murmur-tool-corpus': 'config:' must be a mapping of keys to values, but this entry declares a sequence
hint: config: on an artifact entry must be a mapping with string keys that serializes to at most 65536 bytes of JSON; it is delivered to that artifact alone as MURMUR_ARTIFACT_CONFIG. Omit the key entirely to deliver no variable, and keep secrets out of it — see docs/content/reference/manifest.md
An oversized block is refused rather than truncated, and the message names the size it serialized to alongside the limit:
error[E-CAP-010]: invalid config for artifact 'murmur-tool-corpus': 'config:' serializes to 70011 bytes of JSON, over the 65536-byte limit for MURMUR_ARTIFACT_CONFIG
config: written with no value under it is an empty block, refused on the same terms. Omit the key
to deliver no variable at all.
The refusal is decided at staging, before any registry pull, workdir creation or component
instantiation, so no session workdir appears and no trace.jsonl is written.
mur run --explain-scope refuses the same blocks with the same code.
The runtime checks the shape and not the meaning: which keys a given artifact requires is that artifact's own business, and a missing one surfaces as that artifact's error rather than this one.
E-CAP-011 — a conversation record path segment is not usable
The conversation record lives at
~/.murmur/conversations/<record>/<context-id>/, and both segments come from the operator:
context.record_store and
mur run --context. Each must be a single path segment, and a value that is not
refuses the launch, quoting what was written:
error[E-CAP-011]: invalid context.record_store 'a/b': must be a single path segment: no '/', no '.' or '..', not absolute, and not starting with a dot
hint: context.record_store names one directory under ~/.murmur/conversations/, and --context names one directory beneath that, so each must be a single path segment. Omit context.record_store to use the capsule name, and omit --context to get a fresh id per task — see docs/content/reference/manifest.md
The refusal is decided at staging, before any registry pull, workdir creation or component
instantiation, so nothing is created under ~/.murmur/conversations/.
Distinct from E-CAP-009, which is the same shape rule applied to
capabilities.state.store and points at a different key and a different directory.
A contextId an A2A client sends is not an operator value and never refuses a launch: a task whose
context id is not a usable segment simply goes unrecorded, reported once to stderr and to
logs/bootstrap.log.
E-CAP-012 — invalid filesystem.read_only entry
A capabilities.filesystem.read_only entry is not a usable subdirectory of the session workdir:
error[E-CAP-012]: invalid read-only path '/etc': read-only path must be relative to the workdir
error[E-CAP-012]: invalid read-only path '../outside': read-only path cannot escape the workdir via '..'
The same two rules E-CAP-002 applies to filesystem.scope, applied to each entry
of the read-only list. The check runs at staging, before any registry pull, workdir creation or
component instantiation, so no session directory is created and no call is ever checked against a
rule the runtime could not build. An empty or whitespace-only entry is dropped at manifest parse
rather than refused. See Read-only paths.
E-CAP-013 — an artifact claims a runtime-provided tool name
share-file, fetch-peer-file, delegate-task and submit-plan are answered by the runtime
itself, so an artifact cannot be declared under any of them:
error[E-CAP-013]: artifact 'delegate-task' collides with a tool the runtime provides itself; the reserved names are share-file, fetch-peer-file, delegate-task, submit-plan
hint: the runtime answers these names itself, so an artifact under one of them would be shadowed at dispatch whatever the tool allowlist said. Rename the artifact, or drop the dependency if the runtime-provided tool is what you wanted — see docs/content/reference/runtime-provided-tools.md
The check runs at staging, ahead of every artifact in the manifest, so no artifact is resolved,
pulled or hash-verified and no session directory is created. mur run --explain-scope refuses the
same manifest by the same code. An in-session manage.pull() of one of these names is refused the
same way, and the running capsule receives that refusal as an error string.
Shell binary names are not reserved: they come from capabilities.shell.allow. See
Runtime-provided tools.
E-CAP-014 — a variable nothing sets
A variable this project needs is set by neither this shell nor the workspace .env. Two manifest
keys name one:
| Key | Read from |
|---|---|
capabilities.env.allow |
Any capsule in the capabilities.spawn.allow closure mur doctor walked |
inference.api_key |
The project manifest, where the value is written ${VAR} |
error[E-CAP-014]: this project needs 1 variable nothing in this environment sets: WORKER_TOKEN
Every name is copied from the launching shell at the moment of the spawn, so an unset one reaches the capsule that declared it as absent — export it, or declare it in the workspace .env.
Reported only by mur doctor. A capsule declaring no
capabilities.spawn.allow has no closure to walk and is reported only for a variable its own
manifest references. What mur run does with the same name depends on the key: an unset
capabilities.env.allow name is omitted from the launched environment, and an
inference.api_key reference it cannot resolve is refused with
E-MAN-003.
Names only: no value is read into the report or printed. A name set to the empty string counts as
set, because the runtime copies it through as-is. The matching stdout line is
✗ NAME unset — <capsule>@<version>, naming every capsule that needs the name. A name reached
through a field other than capabilities.env.allow carries that field in brackets, as
✗ NAME unset — solo@0.0.1 (inference.api_key). This is one of the two formation findings
that make mur doctor exit non-zero.
E-CAP-015 — a declaration mur-roost will refuse
A capsule declares a capabilities.env.allow entry the capsule that spawns it does not hold:
error[E-CAP-015]: 1 declaration in this formation exceeds the envelope the capsule spawning it holds, and mur-roost refuses a spawn that widens one.
A child may declare no more than its parent does on capabilities.env.allow.
A spawn may narrow its parent's envelope, never widen it, so mur-roost refuses this delegation at
the moment the parent reaches for it. mur doctor predicts that refusal
offline, naming both capsules and the axis:
declarations mur-roost will refuse:
✗ worker@0.1.0 declares capabilities.env.allow 'WORKER_ONLY', which its parent root-capsule@0.0.1 does not
Add the name to the parent's capabilities.env.allow, or drop it from the child's.
E-CAP-016 — an env.allow entry the credential backstop strips
A capabilities.env.allow entry names a variable the credential backstop removes from every guest
environment:
error[E-CAP-016]: capabilities.env.allow names 'GITHUB_TOKEN' (credential backstop pattern 'GITHUB_TOKEN'), 'MY_SERVICE_SECRET' (capabilities.shell.strip_env pattern '*_SERVICE_SECRET'); these entries are removed from every guest environment before any guest is built, so the grant would deliver nothing
hint: remove these entries from capabilities.env.allow. No manifest setting exempts a name from the credential backstop, and a provider key never belongs in env.allow: the runtime reaches the provider itself through inference.api_key — see docs/content/reference/diagnostics.md#e-cap-016
Fires when: a name matches one of these patterns. The error names every matching entry once, in declaration order, with the pattern that matched and where that pattern comes from.
| Pattern source | Patterns |
|---|---|
credential backstop |
The built-in list: GITHUB_TOKEN, AWS_*, DOCKER_*, *_API_KEY among them |
capabilities.shell.strip_env |
The patterns the manifest declares |
A built-in pattern covers names that carry no secret too, such as AWS_REGION and DOCKER_HOST.
The verdict comes from the name alone, so a host that has the variable set and one that does not get the same result, and no value ever appears in the message. It is reported by:
| Surface | Outcome |
|---|---|
mur run, mur new, mur eval, and every capsule mur-roost launches |
Refused at staging, before any artifact is resolved or pulled and before a session directory is created |
mur run --explain-scope |
Refused, by the same code |
mur doctor |
warning[E-CAP-016], followed by `mur run` will refuse this capsule; the rest of the checklist still runs |
Why it is a refusal: the backstop removes these names whatever the manifest declares, so the grant would deliver nothing, and a capsule that launched with it would look granted and hold nothing.
What to do: remove the entries. For a provider key, set
inference.api_key instead: the runtime reaches the provider
itself. For a variable a guest needs, give it a host name no pattern matches. A name the backstop
keeps but that looks like a credential is reported by W-SEC-024.
Conversation record errors
mur conversation acts on the
record store directly. All three refusals happen before
anything is read or written.
E-CNV-001 — no such record or context
rm and truncate name one context id, optionally narrowed with --record. Nothing matching it
is refused naming what was looked for and where:
error[E-CNV-001]: no context 'ctx_nowhere' in any record under ~/.murmur/conversations/
hint: `mur conversation ls` lists every record and context on this host
E-CNV-002 — a context id is ambiguous across record stores
A context id is unique inside one record store and nowhere else: two capsules can be handed the
same one, and two capsules can be pointed at one
context.record_store. An id present under more than one store is
refused naming every store it is in, rather than guessing which conversation to delete or rewrite:
error[E-CNV-002]: context 'ctx_fixed' is present under 2 record stores: store-a, store-b
hint: pass --record <NAME> to say which one, e.g. --record store-a
E-CNV-003 — invalid --keep
mur conversation truncate --keep 0 is refused, because truncating a record to nothing is
mur conversation rm:
error[E-CNV-003]: --keep must be at least 1
hint: truncating a record to nothing is `mur conversation rm ctx_fixed`
Build lints
mur build checks the file set it is about to pack before it writes a byte of zip. What it
finds is reported two ways:
E-BLD-NNNerrors stop the build. No.mur.zipis written — not a partial one, not an empty one.W-BLD-NNNwarnings go to stderr and the build completes. They flag a packaging mistake that still produces a working artifact.
Both classes are about packaging: what ends up inside the archive, and whether the runtime
will be able to launch it. They are distinct from the W-SEC-NNN
warnings, which are about capability and enforcement posture.
E-BLD-001 — invalid artifact name
error[E-BLD-001]: invalid artifact name '<name>': <reason>
An artifact name: becomes a filename (<name>-<version>.mur.zip), a registry key and a
directory in the local store, so it is held to the lowest common denominator of all three:
- non-empty, at most 100 characters
- ASCII lowercase letters, digits and
-only - no leading or trailing
-
Uppercase letters, spaces, /, \ and _ are rejected by the character rule. This is a
format check only — it reserves no prefix and no namespace. murmur-hook-compact and
murmur-tool-git are ordinary valid names; who may publish a given name is a registry
question, not a packaging one.
name: my-tool # ok
name: My Tool # E-BLD-001
name: my_tool # E-BLD-001
name: -my-tool # E-BLD-001
E-BLD-002 — unsafe or colliding requires_files: entry
error[E-BLD-002]: requires_files entry '<entry>' has an unsafe path: <reason>
error[E-BLD-002]: requires_files entry '<entry>' is a symlink (<path>); declare the file it points to instead
error[E-BLD-002]: requires_files entries '<a>' and '<b>' both pack as the archive entry '<name>'
Every requires_files: entry must be a plain relative path to a real file inside the source
directory, and the entries must not collide once they are inside the archive:
- No absolute paths.
/etc/hostsjoined onto the source directory is/etc/hosts. - No
..components. This is the same rule a.mur.zipreader applies to an entry name it unpacks, applied at authoring time. - No symlinks. The link is not followed: packing it would ship whatever it resolves to — plausibly from outside the source tree — under the declared name.
- No two declarations claiming one archive entry. The archive name is a rewrite of the
declared path (
\→/), so two distinct files can otherwise land in the same slot, where one silently overwrites the other on unpack.
A requires_files: entry that redundantly names murmur.yaml is not a collision — it is
dropped before packing, and reported as W-BLD-001 instead.
E-BLD-003 — packed entry set is not a launchable payload
error[E-BLD-003]: missing root .wasm file (expected capsule.wasm or one root *.wasm)
error[E-BLD-003]: multiple root .wasm files found: <names>
The artifact's runtime:/execution: resolve to a wasm artifact, but the entry set about
to be packed does not contain exactly one payload the runtime could select. mur build never
compiles anything, so a .wasm must already exist on disk and be declared in
requires_files:.
The rule and the message are the runtime's own, so a build that passes this check cannot fail
payload selection at mur run time:
- exactly one root
*.wasmentry → selected - a root
capsule.wasm→ always selected, however many other root*.wasmentries exist (which is why that case is a warning, not an error — seeW-BLD-002) - zero root
*.wasmentries →missing root .wasm file - two or more, none named
capsule.wasm→multiple root .wasm files found
A *.wasm in a subdirectory is not a root entry and does not count. This check applies only to
wasm artifacts — native (implementation: native) and static (runtime: skill,
execution: static) artifacts are packed without it.
W-BLD-001 — declaration names a reserved archive entry
warning[W-BLD-001]: requires_files entry '<entry>' names the reserved archive entry '<name>', which mur build already packs
Two root entries have a fixed meaning inside a .mur.zip: murmur.yaml is seeded by the
packer itself, and capsule.wasm is the payload the runtime prefers over every other root
*.wasm. Declaring murmur.yaml in requires_files: packs nothing extra — the entry is
deduplicated away and the artifact is byte-identical without it.
Remove the declaration.
W-BLD-002 — capsule.wasm shadows another root payload
warning[W-BLD-002]: root 'capsule.wasm' is always selected as the payload, so <names> ships but never runs
The artifact carries a root capsule.wasm and at least one other root *.wasm. The build
succeeds — capsule.wasm makes payload selection unambiguous — but the other file is shipped
and never executed.
Keep exactly one root *.wasm: drop capsule.wasm, or rename the payload you meant to run.
W-BLD-003 — compiled artifact packages build inputs
warning[W-BLD-003]: compiled artifact packages build inputs: <names>
A wasm or native artifact declares an obvious build input in requires_files: — Cargo.toml,
Cargo.lock, a *.rs file, or anything under a target/ path. A compiled artifact ships its
payload, not the sources it was built from, so this is almost always a stray declaration.
Static artifacts (runtime: skill) are exempt: their files are their content.
Registry warnings
mur run and mur doctor print non-fatal warnings about what an artifact store holds. Each one
carries a W-REG-NNN code and a link back to its section on this page. The artifact still resolves
and the session still runs, and mur doctor still exits 0.
W-REG-001 — installed native artifact with no recorded platform
warning[W-REG-001]: murmur-tool-git@0.4.2 is a native artifact with no recorded platform — it resolved from the generic store path, where every host resolves the same payload
Fix: mur install murmur-tool-git@0.4.2
(https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-reg-001)
A native tool is a different binary per platform, so the store files it at
<name>-<version>-<platform>.mur.zip with a metadata sidecar of the same name. This artifact is at
the untagged <name>-<version>.mur.zip instead, which is where an install that recorded no platform
puts it. Two consequences:
- Every host resolves that one payload, whichever platform it was built for.
- Installing a second platform into the same version directory would overwrite it rather than sit beside it.
Reinstalling refiles the payload under this host's platform tag:
mur install <name>@<version>. There is no separate migration command, and nothing rewrites a
store in place — mur install overwrites what is there.
mur doctor marks that artifact's checklist line, prints the same Fix: line, and counts it as a
warning rather than an error, so a store holding an untagged native payload does not fail a CI
pre-flight check.
W-REG-002 — a capsule in a formation could not be inspected
warning[W-REG-002]: 1 capsule in this formation could not be inspected, so what it declares is absent from the report above: ghost-worker
The walk not being able to read a capsule is not evidence that a run fails, so this does not change the exit code.
(https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-reg-002)
mur doctor walks a capsule's capabilities.spawn.allow closure to report
what the whole formation needs from the operator's environment. A capsule it cannot open
contributes nothing to that report, so the report is incomplete. The matching stdout block names
each one and why:
could not inspect 1 of 3 capsules in this formation:
- ghost-worker (declared by root-capsule@0.0.1): not installed in the project or global store
Three reasons:
| Reason | What it means |
|---|---|
| not installed in the project or global store | Neither store holds any version of that name |
| N versions installed (…) and nothing pins which one a run would get | More than one version is installed and no murmur.lock entry pins one; no version is chosen, because there is no latest alias and guessing would report the wrong capsule's requirement as fact |
| installed but unreadable | A version resolved, but its archive or packed murmur.yaml could not be read |
Install the capsule, or pin its version in murmur.lock, and mur doctor folds its declarations
into the report. The exit code is unchanged either way: the walk not being able to read a capsule
is not evidence that a run fails.
Runtime warnings
A session prints non-fatal warnings about what a turn did while the loop was running. Each one
carries a W-RUN-NNN code and a link back to its section on this page. The session still produces
a result, still ends ok, and mur run still exits 0.
These are decided mid-session, so they go to stderr alone. logs/bootstrap.log covers the launch,
which is over by the time a turn happens.
W-RUN-001 — a turn stopped at the output cap
[capsule-runtime] warning[W-RUN-001]: this turn stopped at the inference.max_tokens output cap of 256 tokens, so out/result.txt holds a fragment rather than a finished answer
(https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-run-001)
The provider reported stop_reason: "max_tokens": it generated the whole budget
inference.max_tokens allowed and stopped mid-reply. The cap
named in the warning is the value that was in force — the manifest's, or the 8192 default.
What the turn leaves behind:
| Surface | What it holds |
|---|---|
out/result.txt, and out/result_<task-id>.txt under mode: threaded |
The model's partial text, then a marker naming inference.max_tokens and the cap |
The terminal A2A status event's response, and the task output an on-task-end hook reads |
The same marked text |
| The conversation record's assistant message | The model's own bytes, with "truncated": true on the message envelope |
The trace's inference event |
"stop_reason": "max_tokens", beside a decision of "text" |
The turn is a result, not a failure: the session ends ok, the A2A task reaches completed, and
the runtime attempts no continuation turn. Raising inference.max_tokens gives the next run room to
finish; a task whose answer is genuinely long is better split across turns the agent drives itself.
Security warnings
mur run, mur build and mur doctor print non-fatal warnings about capability and enforcement
gaps in a manifest or host. Each one carries a W-SEC-NNN code and a link back to its section on
this page:
[capsule-runtime] warning[W-SEC-001]: capabilities.shell.allow is non-empty but this platform
has no kernel-level subprocess sandbox (Landlock/seccomp are Linux-only) — enforcement is
environment-only (synthetic HOME + credential env-stripping). This is permanent on this
platform. (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-001)
Where a warning is written depends on whether a session workdir exists yet:
| Warning | Written to |
|---|---|
W-SEC-001, W-SEC-002, W-SEC-003, W-SEC-005, W-SEC-010, W-SEC-020, W-SEC-021, W-SEC-022, W-SEC-023 — decided at launch |
stderr and workdir/<session_id>/logs/bootstrap.log |
W-SEC-006 to W-SEC-009, W-SEC-011 to W-SEC-019, W-SEC-024, W-SEC-025, W-SEC-026, W-SEC-027, W-SEC-028, W-SEC-029 — decided at staging, before the workdir exists |
stderr |
W-SEC-004 — from mur build |
stderr |
W-SEC-001 — No kernel sandbox on this platform
Fires when: capabilities.shell.allow is non-empty and the host resolves to the
Environment-only tier — see Subprocess enforcement tiers for which
hosts those are and why it is permanent there.
Why it matters: shell subprocesses on this host get environment-level protection only — a
synthetic HOME and credential env-stripping. No kernel enforcement constrains what they can read,
execute, or reach on the network.
What to do: treat capabilities.shell.allow and capabilities.network.allow as advisory on
this platform, not a security boundary. For kernel enforcement, run the capsule on a host that
resolves to an enforcing tier (see
Subprocess enforcement tiers). If the capsule ingests untrusted
external content, use the
data/action phase-separation pattern instead of relying on the
allowlists to contain a compromised subprocess.
W-SEC-002 — Landlock unavailable, filesystem scope unenforced
Fires when: capabilities.shell.allow is non-empty and the host resolves to the Seccomp-only
tier — see Subprocess enforcement tiers.
Why it matters: filesystem reads and writes outside the capsule workdir are not kernel-enforced
at all on this tier. Nor is exec: capabilities.shell.allow is enforced by granting the
Landlock Execute right on exactly the allowlisted binaries, so without Landlock there is no exec
mediation here and a shell subprocess can run any binary its uid can reach.
The fixed capsule device set is a Landlock rule list, so it
does not apply here either — and the direction of that gap is worth being explicit about. Without
Landlock there is nothing to grant and nothing to deny: a capsule here can open /dev/null,
/dev/zero and /dev/urandom, but equally /dev/random, /dev/mem and any raw block device its
uid can reach.
What survives on this tier is everything that needs no Landlock — the shell child's capability drop
and the socket(2) domain denial, both described under
What every Linux tier grants. A host stuck below
kernel 5.13 is therefore not exposed to the /var/run/docker.sock escape.
What to do: upgrade the host kernel to move to the Full tier. Until then, treat filesystem scope
and exec scope as advisory on this host — neither has a mechanism here. This is also why this tier
cannot reach the scoped containment class.
W-SEC-003 — bash bypasses the network allowlist
Fires when: capabilities.shell.allow contains the literal entry "bash" and
capabilities.network.allow is non-empty, on a host that resolves to the Environment-only tier
(see W-SEC-001). On the enforcing tiers a bash subprocess's own outbound
connections are constrained by the same allowlist, so this warning does not fire there.
Why it matters: capabilities.network.allow constrains requests the runtime itself makes
(WASI HTTP calls from tool/driver components). It does not constrain a bash subprocess's own
outbound connections on this tier — bash can reach any host regardless of what
network.allow declares.
Maximum-risk combination: bash in shell.allow combined with any external-fetch
capability (network.allow, or a tool/driver artifact that fetches independently) gives a
capsule both exposure to untrusted content and unchecked shell authority to act on it — see the
threat model for the full picture alongside prompt
injection.
What to do: run on a host that resolves to an enforcing tier (see
Subprocess enforcement tiers), or avoid pairing bash with a
non-empty network.allow on platforms without one. Declaring sh or zsh instead only silences
the warning: the check matches the literal string bash, and the exposure is the same.
W-SEC-004 — Literal secret in manifest
Fires when: mur build scans murmur.yaml and finds a credential-shaped field — one whose
key contains api_key, token, secret or password — holding a string that is not a
${VAR_NAME} reference and is either longer than 8 characters or begins with a known API-key
prefix (sk-, sk-ant-).
Why it matters: a literal secret in murmur.yaml ships inside the built artifact and is easy
to accidentally commit to version control.
What to do: replace the literal value with a ${VAR_NAME} reference and inject the real
value via environment at run time. The build still succeeds — this is a warning, not a blocker —
but the artifact should not be published or committed until the literal is removed.
W-SEC-005 — Linux kernel enforcement is in force
Fires when: capabilities.shell.allow is non-empty and the host resolves to the Full or
the Sealed tier (see
Subprocess enforcement tiers). The strongest tiers
warn rather than stay silent, because each is a scope with documented exceptions and silence would
imply it has none.
What the message says. On the Full tier it names what the Landlock scope, the fixed device set,
the socket(2) domain deny and the default-deny syscall allowlist cover — see
Subprocess enforcement tiers for the full grant. On
the Sealed tier it names what the composed root adds and states its one exception: /proc is a bind
of the host's rather than a masked private procfs, because mounting one unprivileged needs a PID
namespace this tier does not create, so host process metadata stays visible inside the root exactly
as it is under scoped.
The one documented exception a manifest controls.
capabilities.network.unix_sockets: true re-widens the
AF_UNIX half of the domain deny. Nothing else on the list has an opt-out:
socket(2) domain |
Default | Can a manifest widen it? |
|---|---|---|
AF_UNIX |
denied (EACCES) |
yes — capabilities.network.unix_sockets: true |
AF_NETLINK |
denied (EACCES) |
no |
AF_PACKET |
denied (EACCES) |
no |
AF_INET, AF_INET6, everything else |
unaffected | governed by capabilities.network.allow — TCP and UDP by IP destination |
That key is coarse on purpose: it is a whole address family, not a per-socket-path allowlist, so
declaring it true re-exposes every unix socket the process can reach — /var/run/docker.sock,
which is host root, included. Landlock cannot substitute for it at any ABI, because ABI v6's
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET scopes abstract unix sockets only and docker.sock is a
pathname socket. Declare it only when a shell tool genuinely needs a local daemon socket. AF_UNIX
is also how glibc's NSS reaches nscd and how syslog(3) reaches /dev/log, so the default deny
is the mechanism on this list most likely to interfere with an existing workload.
Side effect worth knowing about: the shell child's capability drop also removes
CAP_DAC_OVERRIDE from a root-run capsule's subprocess, so it no longer bypasses ordinary
file-permission checks. That is intended, but it is a real behavior change for root deployments
whose shell steps relied on root's usual "can read anything" posture.
What to do: keep shell.allow, network.allow and filesystem.scope as narrow as the task
genuinely needs, and don't run mur run as root if you can avoid it — a non-root capsule never had
the CAP_MKNOD the workdir device-node restriction exists to backstop.
W-SEC-006 — Inert sub-key in a hook's capabilities: block
Fires when: a runtime: hook artifact entry's capabilities: block declares shell,
spawn, env, limits, resources or containment. Per-hook grants (see
artifacts[].capabilities) only read network, filesystem,
state and task_io — the other sub-blocks are structurally accepted but nothing enforces them
per-hook.
Why it matters: an operator who declares, say, capabilities.shell.allow on a hook entry
expecting it to scope that hook's shell access would otherwise have no signal that the runtime
never reads it there — it is silently inert rather than rejected.
What to do: remove the inert sub-key from the hook's entry. Shell, spawn, env, limits,
resources and the containment floor are capsule-wide concerns — declare them in the top-level
capabilities: block instead. See Hook capabilities for
the full rules on what a per-hook grant does and does not cover.
W-SEC-007 — Per-artifact network entry outside the capsule ceiling
Fires when: a runtime: tool or runtime: driver entry's capabilities.network.allow names
an entry the capsule-wide top-level capabilities.network.allow does not itself allow. Per-artifact
capabilities narrow (see Tool and driver capabilities):
the effective grant is declaration ∩ ceiling, so the uncovered entry is dropped from that
artifact's grant rather than granted. Staging continues.
A bare host (api.example.com) is broader than a scheme-bound ceiling entry
(https://api.example.com) because it spans both schemes and every port, so it is not covered
and will be dropped — this is the most common way to hit this warning by accident.
Why it matters: the artifact ends up with less access than the entry asked for. Nothing is widened, so this is never an escalation — but a tool that silently cannot reach a host it was "granted" fails in a way that is hard to trace back to the manifest.
What to do: either add the host to the capsule-wide capabilities.network.allow (if the whole
capsule should be able to reach it), or make the per-artifact entry at least as specific as the
ceiling entry it should sit under, or delete it if the drop was what you actually wanted.
W-SEC-008 — Unapplied per-artifact grant on a tool or driver
Fires when: either
- a
runtime: tool/runtime: driverentry'scapabilities:block declaresshell,spawn,env,limits,resourcesorcontainment— a per-artifact grant only readsnetwork,filesystemandstate(W-SEC-006is the hook-side twin of this case, wheretask_iois honored as well); or - a
runtime: toolentry with a native (non-WASM) implementation declarescapabilities:at all. A native tool runs as a host subprocess under the capsule-wide shell/sandbox machinery, not through the WASI tool path narrowing is applied on, so the whole block is inert.
Why it matters: a declared-but-unenforced grant reads like a scoped artifact. In the native case in particular, the tool keeps the full capsule ceiling despite an entry that looks like it locked it down.
What to do: for an inert sub-key, remove it and declare it in the top-level capabilities:
block instead. For a native tool, scope it through capabilities.shell.* on the capsule-wide
block, or ship the tool as WASM if you need per-artifact narrowing.
W-SEC-009 — Interpreter-runtime grant couples the capsule to a host layout
Fires when: the capsule's top-level capabilities.shell.interpreter_runtime declares one or
more grants. Fires once per grant, from both mur run (at staging) and mur doctor.
Why it matters: an interpreter_runtime grant widens an already-allowlisted binary's Landlock
scope to specific host directories outside the workdir so a path-based interpreter (e.g. CPython)
can reach its standard library. That makes the grant necessary to run such an interpreter, and it
couples the capsule to a specific host distro/interpreter-version layout: a grant naming
/usr/lib/python3.11 stops resolving the moment the host ships Python 3.12, and a capsule that
runs on Debian may not run on Alpine. The durable fix is
capabilities.shell.staged_runtime, which bind-mounts a
pinned runtime tree into the capsule's own composed root instead of reaching out to the host's; it
requires an effective sealed floor, while interpreter_runtime works at scoped too.
What it grants, exactly: one Landlock rule per named directory, and nothing else. Each directory
carries its own required list_dir:
list_dir |
Rights granted | Effect |
|---|---|---|
true |
Execute + ReadFile + ReadDir |
The directory's own entries are enumerable — what CPython's FileFinder needs for a sys.path entry |
false |
Execute + ReadFile |
Files inside can still be opened by exact name, but the directory itself cannot be listed |
There is no field that accepts a prefix and expands it, and ReadDir is never inferred — it is
granted only where an author wrote list_dir: true, and only on that one directory, never on its
parent or siblings. A directory not named in the manifest receives no rule at all.
What to do: name the narrowest set of directories that actually works — measure the real
requirement with strace -f -e trace=openat,getdents64 <interpreter> -c "import ..." rather than
guessing, and set list_dir: false on any directory you only open known files inside. Accept
that the capsule is now pinned to this host's interpreter layout, or switch to
capabilities.shell.staged_runtime if the capsule can run at an effective sealed floor — it
carries no host-layout coupling and fires no W-SEC-009.
Parse-time rejections. A malformed interpreter_runtime fails mur run/mur doctor with
E-MAN-003 at manifest parse time, naming the offending value:
- a
binarynot present in the same block'sshell.allow— this mechanism narrows filesystem access alongside an exec grant that already exists, and never itself grants exec - a
dirs[].paththat is not absolute (does not start with/) - a
dirs[]entry that omitslist_dir— enumerability is never inferred - an
interpreter_runtime[]entry with an emptydirslist
W-SEC-010 — No aggregate bound on the subprocess tree
Fires when: the capsule can spawn a native subprocess by any route
(capabilities.shell.allow, capabilities.spawn.allow, or a native-implementation artifact) and
the host is not Linux — so no cgroup v2 scope can exist for its process tree.
Why it matters: capabilities.resources bounds subprocesses with two independent mechanisms,
and only the weaker one survives on this platform. Per-process setrlimit(2) ceilings still
apply, and still apply as hard limits the capsule cannot raise. But every aggregate bound —
cgroup_memory_bytes, cgroup_pids_max, cgroup_cpu_percent, cgroup_io_bytes_per_sec — is a
cgroup v2 feature, and cgroups are Linux-only. Nothing caps the subprocess tree's total memory,
task count, or CPU.
RLIMIT_NPROC is not a substitute, and this is the specific reason cgroups are required. It
is a per-uid ceiling, not a per-tree one: a fork bomb of distinct, short-lived processes that
fork and exit faster than the count is observed slips past it in practice even when it is set
correctly. Only a cgroup's pids.max bounds the tree as a whole. Two further gaps on macOS
specifically: it has no RLIMIT_AS, and its RLIMIT_DATA is present in the headers but not
enforceable (the kernel rejects any finite value with EINVAL), so
capabilities.resources.memory_bytes has no effect there either — a subprocess's memory is
bounded neither per-process nor in aggregate.
The failure mode is denial of service. A capsule that exhausts host resources has not read, written, or reached anything outside its granted scope.
What to do: run capsules that spawn subprocesses on a Linux host with systemd user cgroup
delegation configured, where the aggregate bounds are real — see
Verification for how those bounds are checked by hand. On this platform,
treat capabilities.resources' cgroup_* fields as declared-but-inert and do not rely on them.
This is permanent on this platform, exactly like W-SEC-001: a kernel without
cgroups cannot grow them.
Note the asymmetry with Linux, which is deliberate: there, the same condition (can spawn a
subprocess, no cgroup scope available) is a refused launch with E-RUN-012, not a warning —
on Linux a missing scope is a host misconfiguration an operator can fix, while here it is a
property of the platform.
W-SEC-011 — Executable workdir makes shell.allow advisory
Fires when: the manifest declares
capabilities.filesystem.workdir_exec: true. Once, at
staging, on stderr — from both mur run and mur doctor.
Why it matters: capabilities.shell.allow is enforced by granting the Landlock Execute right
on exactly the allowlisted binaries' own paths and withholding it everywhere the capsule can write
— above all its own session workdir. workdir_exec: true gives that right back to the workdir. From
then on, a binary the agent compiles, downloads, unpacks or renames inside its workdir executes
regardless of what the allowlist says. There is no name check to defeat, because there is no name
check: the kernel is granting Execute on the path, and the path is inside the granted directory.
This is a stated trade, not a defect. Compile-and-run workloads — a capsule that runs
gcc/cargo build in its workdir and then executes the artefact — need it, and there is no
narrower form of the grant that distinguishes "a binary this capsule compiled" from "a binary this
capsule downloaded". What the runtime refuses to do is let the trade be silent.
What the runtime does about it, beyond this warning:
- the capsule's achieved containment class is
advisory, on every host — including a Landlock-capable one that would otherwise reportscopedorsealed; mur run --explain-scopeprintsworkdir exec: truenext to thatadvisory;trace.jsonl'ssession_startevent carriesworkdir_exec: true, so a completed session's record shows which guarantee was in force;- declaring it alongside
capabilities.containment: scoped(orsealed) refuses the launch withE-CAP-003, before any registry pull, artifact compile or workdir creation. The refusal text names the manifest key rather than a host mechanism, because no host can satisfy the pair.
What to do: remove workdir_exec if the capsule does not genuinely need to run something it
produced — that is the default, and it makes capabilities.shell.allow a boundary the kernel
enforces rather than a convention. If the capsule does need it, keep the declared containment floor
at advisory and treat shell.allow as documentation of intent, not as containment: use the
data/action phase-separation pattern to bound what the capsule
can be induced to build and run, and give it no network allowlist entry it does not need.
W-SEC-012 — A compiler driver's toolchain has no Execute grant
Fires when: the capsule's declared containment floor is
sealed and capabilities.shell.allow names a known
compiler driver — cc, gcc, g++, c++ — whose helper binaries have no grant carrying the
Landlock Execute right. Once per uncovered helper, at staging, on stderr, before any registry
pull or workdir creation. Also printed by mur doctor, which never launches anything.
Why it matters: cc compiles nothing. It forks and execs a front end (cc1 for C, cc1plus
for C++), an assembler (as), a linker (ld) and a linker wrapper (collect2). Those are
separate binaries, and none of them appears in cc's own DT_NEEDED closure — so the derivation
that stages an allowlisted binary's shared libraries into the composed root stages none of them.
They are still present inside the root, because they live under /usr, which
sealed bind-mounts read-only into every composed root.
What they do not have is permission to run: that fixed tree is granted ReadFile + ReadDir and
deliberately not Execute, because it is the one grant covering whole host trees the manifest
never named — it has to make them readable without making them runnable. The result is a capsule
where cc --version prints a version and the first real compile dies partway through, with an
exec failure on a path that is demonstrably right there.
Example. A capsule declaring containment: sealed and shell.allow: [cc] on a Debian-family
host gets, at staging:
[capsule-runtime] warning[W-SEC-012]: capabilities.shell.allow grants the compiler driver 'cc',
but its helper 'cc1' at /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 has no grant carrying the
Landlock Execute right under the 'sealed' composed root — ...
What to do: declare an
interpreter_runtime (or
staged_runtime) grant for the driver, naming the
directories its helpers live in. Both grant shapes carry Execute, which is exactly what is
missing. Measure the directories on the host rather than guessing them:
cc -print-prog-name=cc1 # /usr/libexec/gcc/x86_64-linux-gnu/13/cc1
cc -print-prog-name=as # `as` — deferred to PATH, i.e. /usr/bin/as
The warning itself names the helper's containing directory, so the common case is a copy-paste.
A driver that cannot be run to answer those questions gets W-SEC-029 instead,
and no W-SEC-012 for the helpers it was not asked about.
Why this warns where E-CAP-006 refuses
Both fire only under a declared sealed floor, and both are about a shell.allow binary that
starts and then cannot finish. They differ in what is known:
E-CAP-006 (refusal) |
W-SEC-012 (warning) |
|
|---|---|---|
| Trigger | an allowlisted #! script with no covering grant |
an allowlisted compiler driver with an uncovered helper |
| Evidence | categorical — a script's ELF closure is empty, so staging it stages nothing it imports | heuristic — a -print-prog-name= probe over a fixed table of four driver names |
| Failure it predicts | a missing-module error inside the root, not a denial | an exec failure partway through a compile |
| Outcome | launch refused before any registry pull | launch proceeds, operator warned |
A capsule may compile successfully without every probed helper — a link-only workload never reaches
cc1 — and a driver family outside the table is not probed at all, so refusing a launch on this
evidence would block capsules that would have worked.
W-SEC-013 — Unprivileged user namespaces are unrestricted host-wide
Fires when: AppArmor is enabled on this host and
kernel.apparmor_restrict_unprivileged_userns is 0. Once, at staging, on stderr — from both
mur run and mur doctor.
Why it matters: capabilities.containment: sealed and the capsule network namespace both need
an unprivileged user namespace, and on an AppArmor host something has to permit it. This warning is
about which of the two permitting postures the host is in — they differ enormously in blast radius:
| Grant | What it permits |
|---|---|
profile_confining |
mur alone creates unprivileged user namespaces; every other binary stays restricted |
restriction_disabled_host_wide |
every program on the machine creates them |
The other two values, and the line that prints them, are in Where the user namespace comes from.
Ubuntu 23.10 and later ship the restriction on precisely because unprivileged user namespaces are a
recurring local privilege-escalation surface. Switching it off works, and on a host where no profile
can be loaded it is the right answer — but it is not the configuration murmur ships, and a sealed
result obtained that way is a different security statement from one obtained through the profile.
What the runtime does about it, beyond this warning: nothing is refused, and no exit code
changes — mur doctor on a project whose artifacts all check out still exits 0. The grant is
recorded in the three places it can be read back:
mur doctorprints anAppArmor / user namespacesblock naming the grant;mur run --explain-scopeprintsuserns grant:in its Containment block, and--explain-scope --jsoncarriesuserns_grant;trace.jsonl'ssession_startevent carriesuserns_grantnext tocontainment_achieved, so a finished session's record shows which of the two mechanisms was in force.
What to do: if the narrow grant is available to you, take it —
# 1. restore the hardening, removing any drop-in that disables it
sudo rm -f /etc/sysctl.d/*-userns.conf
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
# 2. install and load the profile shipped with mur
sudo install -m 644 packaging/apparmor/mur-sealed /etc/apparmor.d/mur-sealed
sudo apparmor_parser -r /etc/apparmor.d/mur-sealed
Running a build out of a checkout, where the binary is ./target/debug/mur or
./target/release/mur and no shipped profile attaches to it, use
sudo scripts/install-dev-apparmor.sh — it generates and loads the same grant for those two paths,
so a from-source developer never needs the host-wide sysctl. Then re-run mur doctor and expect
userns grant: profile_confining.
If the host genuinely cannot load a profile, keep the sysctl and keep this warning: it is a record of the posture, not a defect to silence.
The installed-profile comparison
mur doctor also compares /etc/apparmor.d/mur-sealed byte-for-byte against the profile the
running mur build ships, and prints both SHA-256 digests when they differ. That is a file
finding and nothing more: AppArmor loads from the kernel's policy cache, so a file can be edited
without apparmor_parser -r ever running, and a loaded profile can outlive the file it came from.
The userns grant line is the behavioural answer and stays the source of truth — the comparison
never changes a containment class and never changes an exit code.
Local customisation belongs in /etc/apparmor.d/local/mur-sealed, which both shipped profiles
include if exists and which is deliberately not hashed.
W-SEC-014 — a capsule-wide capabilities.state block grants nothing
Fires when: the capsule's own top-level capabilities: block declares state. Once, at
staging, on stderr.
Why it matters: a durable state store is granted per artifact — it
is the runtime: tool, runtime: driver or runtime: hook entry that receives the second preopen.
The capsule's own guest is built with no artifact grant at all, so a top-level declaration reaches
nothing: no directory is created and no state path exists for anybody. Without this warning the
only signal is a store that never appears.
[capsule-runtime] warning[W-SEC-014]: capsule-wide capabilities.state is declared, but a durable state store is granted per artifact — nothing reads a top-level declaration, so no store was created and no 'state' preopen exists. Move the block onto the tool, driver or hook entry that needs it (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-014)
What the runtime does about it: nothing is refused and no exit code changes. The block is
structurally valid, exactly as an inert shell block on a hook entry is
(W-SEC-006), so it is reported rather than rejected.
What to do: move the block onto the artifact entry that needs the store —
artifacts:
- name: murmur-tool-corpus
version: 0.1.0
runtime: tool
capabilities:
state:
store: shey
Two artifacts that need the same store each declare it, with the same store: name. There is no
capsule-wide form: sharing a store is written once per artifact, so reading any one entry tells you
what that artifact reaches.
W-SEC-015 — a config: block on a native tool delivers nothing
Fires when: a runtime: tool entry declares config: and the
artifact ships a native (non-WASM) implementation. Once per artifact, at staging, on stderr.
Why it matters: config arrives in the environment the runtime builds for one WASM component.
A native tool runs as a host subprocess under the capsule-wide shell environment, which is shared
rather than per-artifact, so no MURMUR_ARTIFACT_CONFIG is delivered anywhere. Without this
warning the only signal is a tool that behaves as though it were never configured.
[capsule-runtime] warning[W-SEC-015]: artifact 'murmur-tool-fixture' declares 'config:' but ships a native implementation — a native tool runs as a host subprocess and reads no per-artifact config, so no MURMUR_ARTIFACT_CONFIG is delivered (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-015)
What the runtime does about it: nothing is refused and no exit code changes, exactly as for a
capabilities: block on the same entry (W-SEC-008). mur run --explain-scope
still lists the artifact under artifact config:, because that reports what the manifest declares.
What to do: move the settings into whatever the native tool already reads — its command-line
arguments or a file it is pointed at — or drop the block. config: on a runtime: tool entry
backed by a WASM component, on a runtime: driver entry, or on a runtime: hook entry is
delivered normally.
W-SEC-016 — a capsule-wide capabilities.conversation block grants nothing
Fires when: the capsule's own top-level capabilities: block declares conversation. Once, at
staging, on stderr.
Why it matters: the murmur:conversation/read grant is per hook — it is the runtime: hook
entry whose component imports the interface that receives it. The capsule's own guest holds no
artifact grant and compiles against a world with no such import, so a top-level declaration reaches
nothing and no artifact can read the conversation record.
[capsule-runtime] warning[W-SEC-016]: capsule-wide capabilities.conversation is declared, but the murmur:conversation/read grant is per artifact — nothing reads a top-level declaration, so no artifact can read the conversation record. Move the block onto the hook entry that needs it (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-016)
What the runtime does about it: nothing is refused and no exit code changes, on the same terms
as W-SEC-014.
What to do: move the block onto the hook entry that reads the record —
artifacts:
- name: murmur-hook-recall
version: 0.1.0
runtime: hook
capabilities:
conversation:
read: true
W-SEC-017 — read_only is advisory for an allowlisted interpreter
Fires when: capabilities.filesystem.read_only is non-empty and capabilities.shell.allow
names an interpreter — a shell (bash, sh, zsh, fish, dash, ksh) or a general-purpose
one (python, python3, perl, ruby, node, deno, bun, php, awk, gawk, mawk,
lua, tclsh, Rscript). Once per such binary, at staging, on stderr.
Why it matters: the dispatch-time check reads a shell call's argv and its -c script body, and
flags what it can positively identify as a write — a redirection, or an argument in a write-target
position of a binary it knows. An interpreter's own file I/O is in neither:
python3 -c "open(p,'w').write(x)" is one opaque argument that names no redirection and no
recognized verb. The interpreter can construct a write into a declared read-only subtree, and it
will not be refused.
[capsule-runtime] warning[W-SEC-017]: capabilities.filesystem.read_only is declared and capabilities.shell.allow includes 'python3', an interpreter that can construct a write the dispatch check cannot read — the declaration is advisory for that binary. It still holds for every tool call and for every shell command whose write the dispatch check can identify (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-017)
What the runtime does about it: nothing is refused and no exit code changes. The declaration
still applies in full to every tool call and to every shell command whose write the dispatch check can
identify, and every refusal is still recorded as
protected_path_denied.
What to do: treat the declaration as advisory for the named binary and decide whether that is
acceptable for this capsule. Removing the interpreter from capabilities.shell.allow closes the
gap; keeping it does not weaken any other part of the declaration. See
Read-only paths.
W-SEC-018 — a tool's input schema does not say where it writes
Fires when: capabilities.filesystem.read_only is non-empty and an installed tool's
input_schema declares a path-shaped property (path, file_path, filepath, filename,
file) or a destination-shaped one (dest, dest_path, destination, destination_path,
target_path, output_path, out_path, new_path, to) that the key-name rules still judge.
Once per such tool, at staging, on stderr, naming every one of those properties.
The decision is per property. Annotating one property answers for that property alone, so a tool
that has declared some of its destinations keeps being asked about the rest. A schema-root
murmur-destinations list, including the empty one, answers for the whole tool. A property inside
a subtree declared murmur-opaque is never named, because the key-name rules do not descend there
either.
murmur-opaque answers for a property only where the schema declares that property an object or an
array. On a string property the dispatch check ignores it and keeps judging the value by key name,
so that property is still named here.
Why it matters: with nothing declared, the dispatch-time check guesses which of a tool's inputs
are filesystem destinations from those property names. The guess is wrong in both directions: a
note the tool merely stores, carrying a {file, text} pair, is refused as a write, and a
destination under a name no table carries is never checked.
[capsule-runtime] warning[W-SEC-018]: capabilities.filesystem.read_only is declared and the tool 'guessed-tool' declares the property 'file_path' with no murmur annotation in effect — calls naming it are judged by key name. Annotate a destination property with "format": "murmur-destination", any object the tool only stores with "format": "murmur-opaque", and a destination derived from another property — or the absence of any — with the schema-root "murmur-destinations" list (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-018)
With more than one such property the line names them all: declares the properties 'file', 'path'
with no murmur annotation in effect — calls naming them are judged by key name.
What the runtime does about it: nothing is refused and no exit code changes. The named properties keep the key-name rules.
What to do: say in the tool's input_schema what each named property is. A tool usually needs
more than one of these:
| Declaration | Says |
|---|---|
"format": "murmur-destination" on a string property |
That property's value is a file the tool writes |
"format": "murmur-opaque" on an object or array property |
That subtree is payload the tool stores, not filesystem intent |
"murmur-destinations": ["{repo_path}/.murmur"] at the schema root |
The tool writes a fixed path under another input's value |
"murmur-destinations": [] at the schema root |
The tool writes nothing named by or derived from its input |
A property that is a destination under some input values and a read source under others — a repo
written under checkout and read under log — cannot be declared either way. It falls back on its
name: if the name is in one of the tables above it keeps the key-name rules and keeps firing this
warning; repo is in neither, so a tool with that shape is contained there only by
capabilities.filesystem.scope.
The warning is the tool author's to answer, not the operator's: a capsule cannot annotate a tool it installs, and there is no way to suppress it for one. The entry grammar is in Read-only paths.
W-SEC-019 — an unrecognized key in murmur.yaml
Fires when: murmur.yaml carries a key no field of the block it sits in claims. Once per such
key, on stderr, from mur run and from mur doctor in identical words. One further line is added
when mur_version pins a major.minor.patch version higher than the running binary's.
Why it matters: the manifest parser sets deny_unknown_fields nowhere, so a key it does not
recognize is accepted and applied to nothing. Without this warning a misspelled
capabilities.filesystem.read-only and a correctly spelled read_only produce the same silent,
successful launch — with opposite protections in force.
The wording distinguishes the two causes. A key within edit distance of one the block does recognize is a spelling problem:
[murmur-artifact] warning[W-SEC-019]: unrecognized key 'read-only' in capabilities.filesystem — did you mean 'read_only'? The manifest has a spelling problem here: the key was parsed and ignored, so whatever it declared is not in effect. Correct it in murmur.yaml (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-019)
A key with no near neighbour is one this build does not know, which a newer mur may:
[murmur-artifact] warning[W-SEC-019]: unrecognized key 'quantum_teleport' at the top level — this build of mur does not recognize it and no key it does recognize there is close to it, so the key may come from a newer mur. It was parsed and ignored; nothing here says the manifest is misspelled (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-019)
A higher mur_version pin names the cause directly rather than leaving it to be inferred from
unfamiliar key names:
[murmur-artifact] warning[W-SEC-019]: this manifest pins mur 99.0.0, you are running 0.2.0; 2 keys in it are not recognized by this build (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-019)
| Part of the line | What it names |
|---|---|
'read-only' |
The key, spelled as the manifest spells it |
in capabilities.filesystem |
The block that held it, dotted. A per-artifact block carries its index: artifacts[0].capabilities.shell. A top-level key reads at the top level |
did you mean 'read_only'? |
The nearest key that block does recognize. Absent when none is near |
What the runtime does about it: nothing is refused and no exit code changes. The key is
reported and dropped; every other key in its block still parses and still applies. Refusing would
make a manifest written for a newer mur unloadable by an older one, which turns every optional-key
addition into a breaking change across a fleet.
What to do: correct the spelling when a suggestion is offered. When none is, check whether the
key belongs to a newer mur than the one running — mur --version against the mur_version pin —
and upgrade rather than edit the manifest. A version-gap line answers that question outright.
The pin is separate from this warning and unchanged by it: mur_version is compared for equality
by mur run, which warns on any difference in either direction, and only a pin parsing as a
numeric triple higher than the running version adds the third line above.
W-SEC-020 — a delegating capsule cannot receive an outcome
Fires when: capabilities.spawn.allow is non-empty and the resolved lifecycle block cannot
take an inbound completion — lifecycle.after_task is exit,
or lifecycle.task_acceptance is anything but queue.
Once per launch, on stderr and in the session's logs/bootstrap.log.
[capsule-runtime] warning[W-SEC-020]: this capsule declares capabilities.spawn.allow, but its lifecycle block cannot receive a delegation's outcome: delegate-task returns as soon as the sub-capsule is running, and what the sub-capsule did arrives afterwards as a background task. Declare lifecycle.task_acceptance: queue with lifecycle.after_task: sleep, and a lifecycle.queue_depth covering how many delegations one turn issues, or every outcome this capsule delegates for will be posted to a session that has already exited. (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-020)
Why it matters: delegate-task returns as soon as the
sub-capsule is running and holding its task. What the sub-capsule did arrives afterwards, as a
completion-origin task in the bg lane. Under the default lifecycle the session ends with the
task that made the delegation, so the sub-capsule runs, finishes, posts its outcome to an address
nothing answers on, and records the failed delivery in its own completion.json — where nobody is
looking. The work happens and the result is lost.
What the runtime does about it: nothing is refused and no exit code changes. The delegation still runs and the sub-capsule still does its work; only the report has nowhere to land.
What to do: declare a lifecycle that outlives the delegating task.
lifecycle:
task_acceptance: queue
queue_depth: 4
after_task: sleep
queue_depth covers how many delegations one turn issues, because each outcome in flight occupies
a slot. A capsule that delegates and deliberately does not wait — one that hands work off and exits
— is a legitimate shape, which is why this is a warning and not a refusal; silence it by not
declaring capabilities.spawn.allow on a capsule that does not delegate.
W-SEC-021 — a declared I/O ceiling did not apply
Fires when: the session was given a cgroup scope and the io.max write for
capabilities.resources.cgroup_io_bytes_per_sec against that
scope did not succeed. Once per launch, on stderr and in the session's logs/bootstrap.log.
[capsule-runtime] warning[W-SEC-021]: the declared capabilities.resources.cgroup_io_bytes_per_sec ceiling did not apply to this session's cgroup scope, so this capsule's native subprocess tree has no I/O bandwidth bound; memory.max, pids.max and cpu.max are still enforced on the scope (declared 104857600 bytes/s): the filesystem mounted at /dev/shm is backed by `tmpfs`, which is not a block device, so no io.max ceiling can name one (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-021)
Why it matters: io.max names a block device by MAJ:MIN, and a filesystem with no block
device behind it — tmpfs, overlayfs, FUSE and network mounts — has none to name. Because a default
ceiling is always applied, every capsule that gets a cgroup scope has a declared I/O ceiling, and
this warning is what says the ceiling is not on the scope.
What the runtime does about it: nothing is refused and no exit code changes. io.max is the
one cgroup limit this runtime treats as non-fatal; memory.max, pids.max and cpu.max stay
enforced on the scope and stay fatal on failure. See
Whether the I/O ceiling applied.
What to do: the same reason this warning carries is in io_max.reason, in
mur run --explain-scope --json and in session_start.effective_grants in trace.jsonl. Move the
project onto a block-device-backed filesystem if an I/O bound matters for this capsule; otherwise
the warning is a report and the session is bounded by memory, pids and CPU as declared.
W-SEC-022 — a shell-running capsule cannot receive a completion
Fires when: capabilities.shell.allow is non-empty and the resolved lifecycle block cannot
take an inbound completion — lifecycle.after_task is exit,
or lifecycle.task_acceptance is anything but queue.
Once per launch, on stderr and in the session's logs/bootstrap.log.
[capsule-runtime] warning[W-SEC-022]: this capsule declares capabilities.shell.allow, but its lifecycle block cannot receive a background command's completion: a shell command that outruns lifecycle.shell_grace_secs is demoted to the background, and its exit code and output path arrive afterwards as a background task. Declare lifecycle.task_acceptance: queue with lifecycle.after_task: sleep, or every command this capsule demotes will be discarded at session end and reported to the operator instead of to the agent. (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-022)
Why it matters: every shell command starts in the foreground, and one that outruns
lifecycle.shell_grace_secs is demoted: the turn gets a
wrk_ handle, and the exit code and output path arrive afterwards as a completion-origin task in
the bg lane. Under the default lifecycle the session ends with the task that started the command,
so the command keeps running with nothing reading its result. The compute is spent and the agent
never learns the answer.
What the runtime does about it: nothing is refused and no exit code changes. The command still runs and still writes its output; only the completion has nowhere to land. At session end the discard is stated to the operator — see When a session ends with a command still running.
What to do: declare a lifecycle that outlives the task which started the command.
lifecycle:
task_acceptance: queue
queue_depth: 4
after_task: sleep
The warning does not read lifecycle.shell_grace_secs, and no value of it silences the warning:
0 demotes on the first check after the spawn, so a low grace makes the discard more likely rather
than less. A capsule that fires a command and deliberately does not wait — one that starts work and
exits — is a legitimate shape, which is why this is a warning and not a refusal; silence it by not
declaring capabilities.shell.allow on a capsule that runs no commands.
W-SEC-023 — the session is not addressable
Fires when: a session opened its A2A door and its
running-capsule record could not be written. Once per launch, on
stderr and in the session's logs/bootstrap.log.
[capsule-runtime] warning[W-SEC-023]: this session's record under ~/.murmur/running/ could not be written, so `mur watch` and `mur cancel` cannot reach it by session address — only by the URL it announces: failed to create the directory: Permission denied (os error 13) (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-023)
Why it matters: the record is what lets mur watch and
mur cancel name this session from a terminal that never saw its URL. Without
one, the address mur run printed is the only way back to the capsule, and it is gone as soon as
that terminal is.
What the runtime does about it: nothing is refused and no exit code changes. The capsule binds its port, serves its door and runs its tasks exactly as it would have.
What to do: the message carries the reason the write failed, usually a ~/.murmur that is not
writable or a home directory the host would not resolve. Restore write access to ~/.murmur, or
set HOME to a directory this user owns, and the next launch records itself.
W-SEC-024 — capabilities.env.allow names a credential-shaped variable
Fires when: an entry in capabilities.env.allow reaches every
WASM guest and its name is credential-shaped. Either rule makes a name credential-shaped:
- It contains
api_key,token,secretorpassword, in any case. - One of its segments, split on every character that is not a letter or a digit, is
KEY,KEYS,PASS,PASSWD,PASSPHRASE,CREDENTIAL,CREDENTIALS,CREDS,DSN,AUTH,PATorCOOKIE, in any case.
A name the credential backstop strips never reaches a guest and is refused with
E-CAP-016 instead. The warning prints once per distinct entry, in declaration order,
on stderr, from mur run (including mur run --explain-scope) and from mur doctor in identical
words.
The rule is measured against a fixed list of 26 names that carry credentials and 16 that do not:
| Result | Names |
|---|---|
| Caught: 22 of 26 | Rule 1 alone catches 6 of 26, including DATABASE_PASSWORD, SLACK_BOT_TOKEN and STRIPE_API_KEY. Rule 2 adds 16, including PRIVATE_KEY, SSH_KEY, CREDENTIALS, SENTRY_DSN, SMTP_PASS and GH_PAT |
| Missed | DATABASE_URL, REDIS_URL, DB_PWD, CONNECTION_STRING |
| Warned about but carry no credential | TOKENIZERS_PARALLELISM, CACHE_KEY_PREFIX |
| Never warned about | PATH, PWD, AUTHOR_NAME, KEYBOARD_LAYOUT, PASSENGER_COUNT and the other ordinary names on the list |
A name the rule misses is still delivered to every guest, so review a connection-string entry such
as DATABASE_URL yourself.
A credential-shaped name:
[capsule-runtime] warning[W-SEC-024]: capabilities.env.allow names 'DATABASE_PASSWORD', a credential-shaped variable the credential backstop does not drop — every WASM guest this capsule runs observes the host's value. murmur does not broker this secret and cannot withdraw it: for as long as the capsule runs, the capsule holds it (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-024)
A credential-shaped name on a capsule whose resolved lifecycle.after_task
is sleep gets one further sentence, because the capsule holds the value after the task that
launched it is gone:
[capsule-runtime] warning[W-SEC-024]: capabilities.env.allow names 'DATABASE_PASSWORD', a credential-shaped variable the credential backstop does not drop — every WASM guest this capsule runs observes the host's value. murmur does not broker this secret and cannot withdraw it, and lifecycle.after_task: sleep keeps this capsule alive past the task that launched it, so it holds that value with nothing left waiting on it (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-024)
Why it matters: capabilities.env.allow is the one grant whose value murmur never issues, sees
or revokes — an operator names a host variable, and the runtime passes it through. The line is how
you tell which of your capsules holds which secret.
What the runtime does about it: nothing is refused and no exit code changes. The grant is as effective as it would be unwarned. The judgment is made from the variable's name alone: the line reads the same on a host that has the variable set and one that does not, and no value ever appears in it.
What to do: decide whether this capsule should hold that secret for its whole life, and point the variable at the narrowest credential that does the job — murmur cannot rotate or withdraw it for you.
A capsule that holds an operator-granted secret for its whole life is a legitimate shape, which is
why this is a warning and not a refusal — including with after_task: sleep.
lifecycle.after_task is not a trigger on its own: with no
credential-shaped name declared there is nothing being held.
W-SEC-025 — capabilities.network.allow names the inference endpoint
Fires when: a transport: http capsule's capabilities.network.allow
has an entry that matches its inference.endpoint. Once per matching
entry, on stderr, from mur run (including mur run --explain-scope) and from mur doctor.
[capsule-runtime] warning[W-SEC-025]: capabilities.network.allow entry 'https://api.anthropic.com' names the inference endpoint; inference no longer uses it — the runtime reaches the provider itself — so the entry now only grants tools, subprocesses and the driver direct reach to that host without the key (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-025)
The runtime reaches the provider for the driver and attaches the key there, so inference works with or without the entry. The entry still grants every tool, shell subprocess and the driver direct access to that host, without the key. Remove it unless something other than inference needs that host.
W-SEC-026 — a machine spend ceiling does not cover a transport: process capsule
Fires when: spend.machine_tokens_per_day is set in the effective config and
the capsule's inference.transport is process. Once, on stderr,
from mur run (including mur run --explain-scope) and from mur doctor.
[capsule-runtime] warning[W-SEC-026]: spend.machine_tokens_per_day is set and this capsule uses transport: process — the CLI reaches its provider with its own credentials, so murmur neither counts nor limits this capsule's spend (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-026)
The CLI that transport: process drives holds its own credentials, so none of its requests pass
the runtime, and the capsule runs with no ledger lines and no machine refusal. Bound that CLI's
spend with its provider's own controls.
W-SEC-027 — the inference key is read only at launch
Fires when: a transport: http capsule's inference.api_key
can only be read at launch. That is either a literal value in murmur.yaml, or a ${NAME} that
credentials.<NAME> does not hold and the launching shell's environment
supplies. It fires once per launch, on stderr, from mur run (including mur run --explain-scope)
and from mur doctor.
[capsule-runtime] warning[W-SEC-027]: inference.api_key: ${ANTHROPIC_API_KEY} is read from the environment variable ANTHROPIC_API_KEY, because credentials.ANTHROPIC_API_KEY is not set in the global config, so the key is read once at launch and this capsule cannot pick up a rotated key until it is restarted; store the key with `mur config set -g credentials.ANTHROPIC_API_KEY <key>` to have it re-read (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-027)
For a literal, the warning names inference.api_key is written literally in murmur.yaml and never
prints the value.
Why it matters: a key stored with mur config set -g credentials.<NAME> reaches every running
capsule on its next inference request. This capsule keeps the key it launched with, so revoking
that key breaks it until it restarts, with E-RUN-027.
What the runtime does about it: nothing is refused. Exporting the key is how CI runs are keyed.
What to do: store the key with mur config set -g credentials.<NAME> <key> and write
inference.api_key: ${NAME} in the manifest. A ${NAME} that neither the config nor the
environment holds does not warn: the launch is refused with E-MAN-003.
W-SEC-028 — a file holding a secret is readable by other accounts
Fires when: a path under ~/.murmur that is expected to be owner-only has a mode wider than
that. It fires from two places, on stderr:
| Source | Fires for | How often |
|---|---|---|
mur run |
The config file a transport: http capsule reads its key from, as credentials.<NAME>, when its mode grants any group or other permission |
Once per launch. A key from the environment or a literal never fires it, and neither does a re-read while the capsule runs |
mur doctor |
Every owner-only entry in the ~/.murmur modes table wider than its mode, and every directory wider than 0700 or file wider than 0600 beneath one |
Once per path |
[capsule-runtime] warning[W-SEC-028]: the inference credential credentials.ANTHROPIC_API_KEY is read from /home/alice/.murmur/config.yaml, which is mode 0644 and readable by other accounts on this host; run `chmod 600 /home/alice/.murmur/config.yaml` (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-028)
[capsule-runtime] warning[W-SEC-028]: /home/alice/.murmur/deploy_keys/dep_x/id_ed25519 holds SSH private keys and is mode 0644, which other accounts on this host can read; run `chmod 600 /home/alice/.murmur/deploy_keys/dep_x/id_ed25519` (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-028)
Why it matters: any account on the host can read the provider key, deploy key or record the path holds.
What the runtime does about it: nothing is refused and no mode is changed. murmur sets these modes on every write, so a wide path was loosened by hand, restored from a backup, or written by another tool.
What to do: run the chmod the warning names. Running mur config set -g again also rewrites
config.yaml at 0600.
W-SEC-029 — a compiler driver could not be asked where its toolchain lives
Fires when: the capsule's declared containment floor is
sealed, capabilities.shell.allow names a compiler driver
that W-SEC-012 checks, and running that driver to ask where a helper lives fails
before the driver answers. It fires once per shell.allow entry, on stderr, at staging and from
mur doctor.
| Failure | Reported as |
|---|---|
The driver could not be started — for example Permission denied, Text file busy, or a resource error |
The operating system's error message |
| The driver was killed by a signal before it answered | terminated by signal N |
[capsule-runtime] warning[W-SEC-029]: capabilities.shell.allow grants the compiler driver 'cc', but running /home/alice/bin/cc -print-prog-name=<helper> failed (Permission denied (os error 13)), so W-SEC-012 was not evaluated for its helpers [cc1, cc1plus, as, ld, collect2] and they may have no Execute grant under the 'sealed' composed root; check that /home/alice/bin/cc can be executed by this user and re-run `mur doctor` (https://docs.murmur.nexus/murmur-nexus/murmur/reference/diagnostics/#w-sec-029)
The bracketed list names the helpers that were not checked. Helpers checked before the failure
are reported with W-SEC-012 as usual.
Why it matters: the helpers in the bracketed list were not checked, so the capsule may have a
toolchain it cannot run under sealed even though no W-SEC-012 names it.
What the runtime does about it: nothing is refused, and the driver is not run again.
What to do: make the driver named in the warning runnable by the user launching the capsule,
then run mur doctor. A Text file busy or resource error that clears on its own is gone on
the next mur doctor.