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, or carries a scope |
E-BLD-001 |
E-BLD-002 |
requires_files: entry is unsafe (absolute, .., symlink) or collides inside the archive, or a skill folder holds a symlink, two skill.md spellings or a murmur-build.json |
E-BLD-002 |
E-BLD-003 |
Packed entry set is not a launchable wasm payload | E-BLD-003 |
E-BLD-004 |
Artifact manifest declares inference_auth:, the former name of upstream_auth: |
E-BLD-004 |
E-BLD-005 |
A driver's models: table is malformed — a max_input_tokens above its context_window included — or models: is declared on an artifact that is not a driver |
E-BLD-005 |
E-BLD-006 |
An artifact's provider_tool: block is malformed, or declared on a driver, hook or skill artifact |
E-BLD-006 |
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, or a declared provider tool needs sealed and the effective floor is below it |
E-CAP-003 |
E-CAP-004 |
capabilities.shell.staged_runtime is declared below an effective sealed containment floor |
E-CAP-004 |
E-CAP-005 |
Creating a network namespace for the capsule's native subprocess tree — shell commands, spawned capsules and native tools — failed on this host, 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 work folder | 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's tool name is 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 WASM component |
E-CAP-016 |
E-CAP-017 |
A gateway: is declared on a native tool, whose requests never pass the credential gateway |
E-CAP-017 |
E-CAP-018 |
A gateway: names an endpoint but binds no api_key and does not declare keyless: true |
E-CAP-018 |
E-CAP-019 |
A process driver requires a variable capabilities.env.allow does not declare |
E-CAP-019 |
E-CAP-020 |
Two declared artifacts share a tool name, the part of their names after the / |
E-CAP-020 |
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-CFG-003 |
registry: in a config file cannot be used: a removed default: key, a source's name:, type: or url:, a repo that is not <owner>/<repo>, a reserved scope claimed, or two sources for one scope |
E-CFG-003 |
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, or no eval-run record for the run mur eval diff --run names |
mur eval |
E-EVAL-003 |
A dataset case's start or check.files is missing, is not a directory, or holds a symbolic link or special file; its check.command is empty; or its check.timeout_secs is 0. Names the case and the path or field |
mur eval run |
E-EVAL-004 |
A case's check binary is not in an arm's capabilities.shell.allow (names the case, the arm and the binary), or the baseline's and the variant's inference.model differ (names both) |
mur eval run |
E-IDX-001 |
A file in the directory given to mur index cannot be indexed, or the directory holds no .mur.zip |
E-IDX-001 |
E-IDX-002 |
An artifact index is not a schema 2 index this mur reads: schema 1, another schema, not JSON, or invalid; or a source's latest release has no index |
E-IDX-002 |
E-IDX-003 |
mur index was not given a usable source: --repo or --release is unusable, or --merge names an index of another scope |
E-IDX-003 |
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), including a write to standard output that failed for a reason other than its reader going away | Piped output |
E-IO-004 |
HOME is unset, relative, or names a directory in which ~/.murmur cannot be created, so there is nowhere to keep a session's runtime and records directories |
E-IO-004 |
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), a removed inference.endpoint or inference.api_key, a gateway.endpoint with userinfo, ${ or another rejected shape, a gateway: that writes both api_key and keyless: true, a malformed network.authentication, a credential scope that names a retired door method, a network.authentication declared beside a non-empty capabilities.spawn.allow, a lifecycle.queue_depth: 0 under task_acceptance: queue, a lifecycle.keep_working block with a missing or blank message or a missing or zero limit, a lifecycle.done_check block with a missing or blank command, a command whose program capabilities.shell.allow does not list, a missing limit or a zero timeout_secs, a top-level mur_version, which is pinned as deploy.mur_version, a gateway.api_key: ${NAME} that neither credentials.NAME in ~/.murmur/config.yaml nor the environment variable NAME holds, an artifact name that is not <scope>/<name> where a full name is required, a scope on a source: entry, two capabilities.spawn.allow entries with one bare part, or a top-level registry: key |
E-MAN-003 |
E-NEW-001 |
The generator agent produced no out/murmur.yaml |
mur new |
E-NEW-002 |
The name given to mur new --roster, or a capsule name derived from it, is not an artifact name |
mur new --roster |
E-NEW-003 |
The directory mur new --roster would write already exists |
mur new --roster |
E-NEW-004 |
inference.provider in ~/.murmur/config.yaml names a provider mur new --roster has no driver for |
mur new --roster |
E-NEW-005 |
mur new was given --index local, the global store, which is not a published artifact index |
mur new |
E-NEW-006 |
mur new could not read the artifact index it takes default-artifact versions from |
E-NEW-006 |
E-NEW-007 |
The artifact index mur new read lists no version of a default artifact that fits this mur |
E-NEW-007 |
E-REG-001 |
The scope's index lists no such name, no such version, or no payload of it for the host platform, or no version that fits this mur; or a local/ artifact is not in the store, and no source serves the local scope |
E-REG-001 |
E-REG-002 |
Artifact bytes do not match the sha256 recorded for them or listed in the source's index, or an artifact's packed name: or version: is not the one it is installed as |
E-REG-002 |
E-REG-003 |
An artifact of that name and version is already published | mur publish |
E-REG-004 |
Reserved version string (latest, stable, edge), given to mur publish or mur install |
mur install |
E-REG-005 |
A registry-resolved artifact's version or hash, or a roster member's, disagrees with the murmur.lock entry |
Lockfile |
E-REG-006 |
A source did not answer an artifact lookup — rate limited, refused or unreachable — so whether it publishes the artifact is not known | E-REG-006 |
E-REG-007 |
An artifact name on the command line is not <scope>/<name>, or a github: owner claims a reserved scope |
E-REG-007 |
E-REG-008 |
No source serves the artifact's scope | E-REG-008 |
E-REG-009 |
A source's index states a scope the source does not own | E-REG-009 |
E-REG-010 |
A scope's GitHub owner id is not the one recorded for it in murmur.lock, in the stored payload, or compiled in for murmur |
E-REG-010 |
E-REG-011 |
mur install or mur publish was handed a payload whose packed murmur.yaml ships a malformed models: table |
E-REG-011 |
E-ROS-001 |
roster.yaml is missing, unreadable, or does not have the roster shape, or names a member's capsule by a bare name |
E-ROS-001 |
E-ROS-002 |
A roster marks no member entry: true, or more than one |
E-ROS-002 |
E-ROS-003 |
Two roster members share one name |
E-ROS-003 |
E-ROS-004 |
A reachability rule names a member the roster does not have |
E-ROS-004 |
E-ROS-005 |
A reachability rule calls a member that does not declare exports.peer_tasks.accept: true |
E-ROS-005 |
E-ROS-006 |
A roster with peer traffic has a member with no network.authentication |
E-ROS-006 |
E-ROS-007 |
A roster member is not installed at its version, or its packed murmur.yaml cannot be read |
E-ROS-007 |
E-ROS-008 |
A reachability rule lists the entry member in to |
E-ROS-008 |
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 |
A lock_version other than 3, a lock entry not named <scope>/<name>, missing lock entry, a lock entry with no hash for this host's platform, or a malformed origin / session pair |
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 a transport: process harness binary was not found |
E-RUN-006 |
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 |
The work folder, $HOME and $TMPDIR together grew past capabilities.resources.workdir_max_bytes |
Size ceiling |
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, or MURMUR_SPAWNER_LIFELINE to something that is not this session's spawner lifeline |
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 or formation id 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 or formation named could not be ended and is still running, or mur trace rm named a session that is still running |
E-RUN-024 |
E-RUN-025 |
An artifact whose entry declares gateway: has no usable upstream_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-RUN-029 |
A transport: process driver exports no version of the process interface, or a driver imports a murmur:stream interface that does not exist at the served version |
E-RUN-029 |
E-RUN-030 |
The transport: http inference driver exports the process driver interface |
E-RUN-030 |
E-RUN-032 |
A process driver could not be loaded with no grants, or described itself unusably | E-RUN-032 |
E-RUN-033 |
A transport: process turn failed — the harness reported it, or the runtime ended it at the turn limit |
E-RUN-033 |
E-RUN-034 |
A call into the process driver refused, crashed or ran out of time | E-RUN-034 |
E-RUN-035 |
A transport: process harness went silent and was killed |
E-RUN-035 |
E-RUN-036 |
The harness could not find the session it was asked to continue | E-RUN-036 |
E-RUN-037 |
--resume-mode compact under inference.transport: process |
E-RUN-037 |
E-RUN-038 |
A spend ceiling is set against a process driver that reports no usage | E-RUN-038 |
E-RUN-039 |
A forget was asked of a capsule that keeps no harness session | E-RUN-039 |
E-RUN-040 |
The session ran and its task did not complete: it failed, spent inference.max_turns, hit a spend ceiling, or was canceled |
E-RUN-040 |
E-RUN-041 |
A capsule declaring control: could not write its control token beside its running record, and did not launch |
E-RUN-041 |
E-RUN-042 |
mur control named a session with no control surface, or the control surface refused the request |
E-RUN-042 |
E-RUN-043 |
A murmur.lock pin written by manage.pull() is declared as runtime: hook, runtime: driver, with gateway:, or bound as inference.system_prompt_artifact |
E-RUN-043 |
E-RUN-044 |
MURMUR_FORMATION_ID is set to something that is not a formation id |
E-RUN-044 |
E-RUN-045 |
A formation member did not come up, so mur run --roster launched nothing and stopped every member it had started |
E-RUN-045 |
E-RUN-046 |
mur run --roster was run inside a formation member, a member's formation channel cannot be read, MURMUR_FORMATION_PEERS is set in mur run's environment, or MURMUR_FORMATION_LIFELINE does not name this member's lifeline |
E-RUN-046 |
E-RUN-047 |
An installed capsule's bytes no longer hash to the sha256 roster admission bound its member to | E-RUN-047 |
E-RUN-048 |
mur token named a session whose door is public, which minted no token |
E-RUN-048 |
E-RUN-049 |
mur token --credential named a credential the session did not mint |
E-RUN-049 |
E-RUN-050 |
The runtime met a symbolic link, or the wrong kind of file, inside the workdir — while staging the session, or during the task mur run ran — or mur trace rm met one on the way to a directory it removes |
E-RUN-050 |
E-RUN-051 |
A delegated child's --workdir is not the directory its parent created; nothing was written |
E-RUN-051 |
E-RUN-052 |
A driver choice has no output cap — its driver lists no limits for its model and the manifest sets no inference.max_tokens — or its driver ships a malformed models: table, a max_input_tokens above its context_window included |
E-RUN-052 |
E-RUN-053 |
The session reached its deadline, own or inherited, and was ended; a formation's --deadline fell before its entry member started; or a launch was handed a deadline it cannot enforce |
E-RUN-053 |
E-RUN-054 |
inference.compaction.max_tokens is above the output ceiling the primary driver's models: table declares for the compaction model |
E-RUN-054 |
E-RUN-055 |
A driver choice's output cap, or inference.compaction.max_tokens, is at or above its context window, leaving its calls no input budget |
E-RUN-055 |
E-RUN-056 |
The running session a session address named is a script capsule, which binds no door to connect to | E-RUN-056 |
E-RUN-057 |
A capsule declares a computer tool and a transport: http driver choice runs a model its driver marks vision: false |
E-RUN-057 |
E-RUN-058 |
A stored tool artifact ships a malformed provider_tool: block, or one on an artifact that is not a tool |
E-RUN-058 |
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, empty file); unknown event types are silently skipped. mur trace show reads a trace with no session_end, and skips a torn final line; mur trace diff and mur trace report refuse both, and a formation member's missing session_end names its formation and mur trace show <formation-id>. Also a mur trace show --body selector that names no recorded hash, or a hash whose body was never stored, a --turn that names several turns, --body or --turn with a formation id, and mur trace steps with a formation id, which names the mur trace show <formation-id> that lists its members and calls |
trace.jsonl schema, mur trace show --body, Listing a formation, mur trace steps |
E-TRC-002 |
No session found in the workdir, or a session selector matched none or several. Also a frm_ argument to mur trace show that is not a formation id, or a formation no session under any searched root belongs to, and a literal path given to mur trace rm |
mur trace, Listing a formation, mur trace rm |
E-WIT-LEVEL-001 |
A driver a launch stages, an artifact version mur install names, or a version a runtime pull resolves, does not fit the WIT level this mur serves |
E-WIT-LEVEL-001 |
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-DEPLOY-001 |
A deploy's target compiled none, or not all, of the uploaded artifacts; those compile on the capsule's first launch | W-DEPLOY-001 |
W-DEPLOY-002 |
A deploy's target runs under a different MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES from the deploying machine |
W-DEPLOY-002 |
W-REG-001 |
An installed native artifact has no recorded platform | W-REG-001 |
W-REG-002 |
A capsule in the spawn closure could not be inspected | W-REG-002 |
W-REG-003 |
An installed artifact does not fit the WIT level this mur serves |
W-REG-003 |
W-ROS-001 |
A roster lets more members call a member than its lifecycle holds at once |
W-ROS-001 |
W-RUN-001 |
A turn stopped at the inference.max_tokens output cap |
W-RUN-001 |
W-RUN-002 |
A transport: process harness reports a version its process driver was not tested against |
W-RUN-002 |
W-RUN-003 |
An inference.alternates driver choice's credential was found nowhere at launch, so the choice is unavailable |
W-RUN-003 |
W-RUN-004 |
A tool's input_schema is malformed where the required-field check reads it, so its calls run unchecked |
W-RUN-004 |
W-RUN-007 |
A session carries a formation id but no lifeline, so it does not wind down when its formation ends | W-RUN-007 |
W-RUN-008 |
A formation member the roster lets call another reaches no member's door with its capabilities.network.allow |
W-RUN-008 |
W-RUN-009 |
A driver choice's driver lists no limits for its model, so it runs on inference.max_tokens with no context window |
W-RUN-009 |
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 — any but network, filesystem, state, task_io, conversation and limits.deadline_seconds |
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-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-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-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 |
A capabilities.network.allow entry names an artifact's gateway.endpoint host — the gateway does not use the entry, and it grants direct reach to that host without the key |
W-SEC-025 |
W-SEC-027 |
An artifact's gateway.api_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 a gateway.api_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 |
W-SEC-030 |
An artifact reaches its upstream through an unmetered credential gateway, which the spend ceilings do not cover | W-SEC-030 |
W-SEC-031 |
A transport: process harness session reports it is authenticating as something other than a subscription |
W-SEC-031 |
W-SEC-032 |
The A2A door is exposed off loopback and network.authentication is not declared |
W-SEC-032 |
W-SEC-033 |
The session's runtime or records directory lies inside the workdir, so the capsule's commands can change the files the runtime owns or the records it keeps | W-SEC-033 |
Manifest errors
E-MAN-003 — a manifest value the runtime rejects
A field in murmur.yaml has the wrong type, or a value of the right type the runtime refuses. The
message names the field and the value; the cases are listed in the index, and a
gateway.api_key: ${NAME} that nothing holds is explained under
Where ${NAME} is read from.
A credential scope that is a retired name of a door method — an A2A 0.3 method name, or an older name of a murmur method — is refused, and the hint names the scope to write in its place:
error[E-MAN-003]: murmur.yaml: invalid network config for 'network.authentication.credentials.watcher.scopes': 'tasks/get' is a retired name of a door method, and the door answers only the names its card lists
hint: write 'GetTask' in place of 'tasks/get'
error[E-MAN-003]: murmur.yaml: invalid network config for 'network.authentication.credentials.watcher.scopes': 'stream/watch' is a retired name of a door method, and the door answers only the names its card lists
hint: write 'murmur.WatchSession' in place of 'stream/watch'
agent/getAuthenticatedExtendedCard has no replacement scope, because every valid token may call
GetExtendedAgentCard:
hint: remove 'agent/getAuthenticatedExtendedCard': every authenticated caller may call GetExtendedAgentCard, so a credential lists no scope for it
The scopes are listed under
network.authentication.
An artifacts: entry, a capabilities.spawn.allow entry and a capabilities.install entry name
an artifact by its full name, <scope>/<name>. A bare name is refused, and the hint names its
scoped forms — see Artifact names:
error[E-MAN-003]: murmur.yaml: invalid artifact name in 'artifacts[0].name': 'tool-edit' is not a full artifact name: an artifact is named <scope>/<name>
hint: name it with its scope: 'murmur/tool-edit' for a Murmur default artifact, '<owner>/tool-edit' for one published from github.com/<owner>, or 'local/tool-edit' for one installed from a .mur.zip file
| Manifest | Refused because |
|---|---|
A source: entry named ana/style |
A source: entry is named by its bare name; the hint is drop the scope: name it 'style' |
capabilities.spawn.allow: [local/worker, ana/worker] |
delegate-task offers each capsule to the model by its bare part, so two entries may not share one |
capabilities.install.tool: ["tool-*"] |
An install entry names its scope: ana/tool-*, ana/*, an exact full name, or * |
inference.driver.artifact: driver-anthropic beside a declared murmur/driver-anthropic |
inference.driver.artifact names the entry as written; the message names the declared entry to refer to |
A capabilities.shell.allow entry is a bare binary name, the one the session looks up on PATH.
An entry that is anything else is refused when the manifest is read, before anything is staged or
installed — see Shell allow:
error[E-MAN-003]: murmur.yaml: invalid capability config for 'capabilities.shell.allow[1]': '/opt/uv/bin/uv' is a path; an entry is a bare binary name
hint: name the binary bare (uv, not /opt/uv/bin/uv) and put its directory on PATH for mur run: PATH=/opt/uv/bin:$PATH mur run …
| Entry | Reason in the message |
|---|---|
/opt/uv/bin/uv, bin/uv |
is a path |
., .. |
names a directory |
"" |
is empty |
"a b" |
contains whitespace |
| An entry holding a NUL character | contains a NUL byte |
A top-level registry: key is refused by every command that reads a murmur.yaml, before any
other field is checked. Artifacts resolve from the stores and the sources in
~/.murmur/config.yaml; see Resolution order:
error[E-MAN-003]: murmur.yaml: 'registry' is not a manifest key
hint: remove it; artifacts resolve from the project store (.murmur/artifacts/), the global store (~/.murmur/artifacts/), and then the sources under registry.sources in ~/.murmur/config.yaml
A lifecycle.queue_depth of 0 under task_acceptance: queue leaves the queue no room for a
waiting task, and is refused before anything launches:
error[E-MAN-003]: murmur.yaml: invalid lifecycle config for 'lifecycle.queue_depth': 0 leaves task_acceptance: queue no room for a waiting task
hint: set lifecycle.queue_depth to 1 or more, or use lifecycle.task_acceptance: single to take one task at a time
mur run --lifecycle-task-acceptance queue over a manifest that declares queue_depth: 0 under
another acceptance is refused the same way:
error[E-MAN-003]: the launch sets lifecycle.task_acceptance to queue, and murmur.yaml declares lifecycle.queue_depth: 0, which leaves the queue no room for a waiting task
hint: set lifecycle.queue_depth to 1 or more in murmur.yaml, or launch without --lifecycle-task-acceptance queue
Under single and none, queue_depth has no effect, and any value is accepted. See
lifecycle.queue_depth.
A top-level mur_version key is refused whatever its value, including blank, and with or without a
deploy: block beside it:
error[E-MAN-003]: murmur.yaml: 'mur_version' is not a top-level key — the mur release a deploy installs is pinned as 'deploy.mur_version'
hint: move the pin under deploy: as deploy.mur_version — only mur deploy reads it, to choose the mur it installs on the target
See deploy.
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 work folder:
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 chosen by the cause:
| Cause | Hint |
|---|---|
| The host lacks the mechanism | lower the declared floor to '<achieved>' (capabilities.containment in murmur.yaml, containment in .murmur/config.yaml, or --containment), or run on a host that provides '<declared>' |
capabilities.filesystem.workdir_exec: true |
set capabilities.filesystem.workdir_exec to false, or lower the declared floor to 'advisory' (…the same three places) |
| The runtime or records directory lies inside the workdir | run with a --workdir that does not contain ~/.murmur, or lower the declared floor to 'advisory' (…the same three places) |
A declared provider tool needs sealed and the effective floor is below it |
set `capabilities.containment: sealed` in murmur.yaml (or pass `--containment sealed`); this host can back '<class>' and, when that class is below sealed, so the capsule also needs a host that provides 'sealed' |
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 composed root here" has several completely different causes. Whether sealed is refused is
decided by the namespace probe building the
composed root inside the capsule's network namespace, never by the AppArmor setting. When a probe
attempt failed, the reason has three parts: the class sentence below, the call the probe saw fail,
and the readings line E-CAP-005 ends with:
error[E-CAP-003]: declared containment class 'sealed' is not achievable on this host (achieved: 'scoped'): <class sentence and remedy> The probe's <call> failed with <ERRNO> (<description>). Host readings: kernel.apparmor_restrict_unprivileged_userns=<0|1>; AppArmor profile confining mur: <profile>.
When the capsule declares a provider tool, a sealed refusal
names each such tool on its own line under the message, before the hint:
error[E-CAP-003]: declared containment class 'sealed' is not achievable on this host (achieved: 'scoped'): <reason>
required by: tool 'computer' (provider_tool: computer)
hint: lower the declared floor to 'scoped' (capabilities.containment in murmur.yaml, containment in .murmur/config.yaml, or --containment), or run on a host that provides 'sealed'
A probe that could not run reads The probe could not run: <the runtime's message>. in place of
the failed call. A non-Linux host and a host without Landlock fail no attempt, and their reason is
the class sentence alone. 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. Reported only when the network namespace was refused at unshare or writing uid_map/gid_map with EPERM or EACCES, the setting reads 1 and mur is unconfined |
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 |
A user namespace 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 namespace was created and mounted inside, but pivot_root(2) was refused |
On an AppArmor host, check the loaded profile carries a pivot_root, rule and reload it; inside a container, pivot_root(2) is commonly absent from the runtime's seccomp allowlist, so establish the mount namespace outside the container |
sealed is built inside the capsule's network namespace, and this host could not create or enter it |
Fix the network namespace first: E-CAP-005 names the step that failed and its remedy |
The probe could not run, so whether this host can back sealed is unknown; refused rather than assumed |
Re-run once the process or descriptor limit or memory is relieved, or allow the probe's syscalls |
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 work folder, 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 second case names the workdir: ~/.murmur/sessions, where the session's
runtime directory is created, or the session's
records directory, lying inside the workdir the capsule's commands
write — HOME and --workdir the same directory, for one. The commands could then change the
task, MURMUR.md, a submitted plan, an installed tool or the session's trace, and the achieved
class is capped at advisory. The reason names the directory and the workdir:
error[E-CAP-003]: declared containment class 'scoped' is not achievable on this host (achieved: 'advisory'): the runtime directory /home/dev/.murmur/sessions and the records directory /home/dev/.murmur/records/home/dev/<session-id> lie inside the workdir /home/dev, so this capsule's commands can change the files the runtime owns — task.md, MURMUR.md, plans and installed tools — and the records it keeps about the session — its trace, logs, eval scores and delegation outcome, and no host can back a class above advisory for it; run with a --workdir that does not contain ~/.murmur
hint: run with a --workdir that does not contain ~/.murmur, or lower the declared floor to 'advisory' (capabilities.containment in murmur.yaml, containment in .murmur/config.yaml, or --containment)
Run with a --workdir that does not contain ~/.murmur. Below a scoped floor the launch runs
and prints W-SEC-033 instead.
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.
A provider tool below sealed names the manifest. A capsule declaring a tool whose artifact
carries a provider_tool: block needs the sealed floor whatever the
host can back. It is decided against the effective declared floor, after the artifacts are read
and before the session directory exists or any driver is called; the floor is never raised for it:
error[E-CAP-003]: tool 'computer' (provider_tool: computer) declares a provider tool, which runs only under the 'sealed' containment floor, and the effective containment floor is 'scoped'
hint: set `capabilities.containment: sealed` in murmur.yaml (or pass `--containment sealed`); this host can back 'sealed' — see https://docs.murmur.nexus/reference/containment/#provider-tools
Every such tool is named. Set the floor to sealed, or remove the tool from artifacts:.
E-IO-004 — HOME cannot hold ~/.murmur
Every launch keeps its runtime directory and its
records directory under ~/.murmur. A HOME that cannot hold it is
refused, and nothing is written anywhere:
| Cause | Message |
|---|---|
HOME is unset |
HOME is not set, so there is no ~/.murmur to hold this session's runtime and records directories |
HOME is a relative path |
HOME is '<value>', which is not an absolute path, so there is no ~/.murmur to hold this session's runtime and records directories |
~/.murmur, ~/.murmur/sessions or ~/.murmur/records is not a directory this user can write, or ~/.murmur is absent and HOME cannot be written |
~/.murmur (<path>) cannot hold this session's runtime and records directories: <reason> |
error[E-IO-004]: HOME is not set, so there is no ~/.murmur to hold this session's runtime and records directories
hint: set HOME to the absolute path of a directory you own, in which ~/.murmur exists or can be created as a 0700 directory
| Raised by | When |
|---|---|
mur run |
Before any artifact store, lockfile or registry is read; also under --explain-scope |
mur eval run |
When a case is staged |
mur trace show, steps, diff, report, list, rm, mur eval show, diff, mur run --resume |
When the address needs a records group, which list and rm always do; the path form needs none |
mur doctor |
In place of the runtime and records directory paths, counted as a failed check |
E-RUN-006 — a harness binary was not found
A transport: process capsule names an executable that is not installed on this host. mur run
refuses at staging, before the session directory exists.
error[E-RUN-006]: harness binary 'my-cli', named by the process driver's describe(), was not found on this host
hint: install the harness this capsule's process driver drives, or point inference.command at the executable to run
The name comes from one of two places, and the message says which:
| Named by | Resolved as |
|---|---|
inference.command |
A value containing / is used as a path and must be an executable file; a bare name is looked up on PATH |
The process driver's describe() |
Looked up on PATH |
The same code covers a transport: http inference driver artifact that is not installed in the
local artifact store.
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 https://docs.murmur.nexus/reference/cli/
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 https://docs.murmur.nexus/reference/cli/
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 nothing on disk to continue. 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 (no conversation record at /home/you/.murmur/conversations/shey/ctx_0193f2…/conversation.jsonl)
hint: a session is resumable only if its capsule kept something to continue it with: a conversation record under transport: http, or a harness-session.json under transport: process. Either way context.record: off keeps neither. Run `mur trace show <session>` to see what that session did, and omit --resume to start a fresh conversation — see https://docs.murmur.nexus/reference/cli/
What "nothing to continue" means is the one thing the transport decides:
| Transport | What is looked for |
|---|---|
http |
The context's conversation.jsonl |
process |
The context's harness-session.json, which holds the session id the harness answers to |
The other reasons apply to both: context.record: off, a capsule
with no inference: block, and a host whose home directory cannot be resolved. 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 https://docs.murmur.nexus/reference/cli/
--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 or spawner lifeline 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.
The same code refuses a MURMUR_SPAWNER_LIFELINE that does not name this session's
spawner lifeline, before any session directory exists. A child
that cannot hear its spawner would outlive it:
error[E-RUN-020]: MURMUR_SPAWNER_LIFELINE does not carry this session's spawner lifeline: descriptor 9 is a regular file, and a lifeline is a pipe's read end; a delegated child that cannot hear its spawner would keep running after the session that delegated to it has ended, so the launch is refused
hint: MURMUR_SPAWNER_LIFELINE is set by a parent capsule's runtime for each child it launches, beside MURMUR_SPAWNER, and is not for operators; unset it to run this capsule directly
| Reason | The value |
|---|---|
it is not a decimal descriptor number |
Holds anything but digits |
it is not a descriptor number this platform has |
Is too large to be a descriptor |
descriptor N is a standard stream, and a lifeline is 3 or above |
Is 0, 1 or 2 |
descriptor N is not open |
Names no open descriptor |
descriptor N is a character device / a regular file / a directory / a socket / not a pipe, and a lifeline is a pipe's read end |
Names something other than a pipe |
descriptor N is open for writing, and a child holds only a read end |
Names a pipe's write end |
this session was launched by a caller that does not own its process, and a lifeline ends the process |
Reached a launch embedded in another program rather than mur run |
A blank MURMUR_SPAWNER_LIFELINE is absent. The variable needs no MURMUR_SPAWNER and no
MURMUR_FORMATION_ID beside it.
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 '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>.
mur stop <formation-id> refuses with this code, and signals nothing,
when the formation id is malformed or names no running member:
error[E-RUN-022]: no member of formation frm_019f01a93ff27c1e9a3b5d0c4e8f2a61 is running on this machine
hint: `mur ps` lists the formations running here
| Message | Means |
|---|---|
'<arg>' is not a formation id: … |
The argument starts frm_ and is not a whole formation id |
no member of formation <id> is running on this machine |
No record names that formation |
formation <id> is not running: N member record(s) named processes that are gone, and were removed |
Every record naming it was stale |
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 suspended or the host overloaded; 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 process may be suspended, or the host too loaded to run it |
the capsule at … answers for session … |
Another session holds that address |
the agent card from … names no session |
The capsule was started by mur v0.4.0 or earlier, whose agent card this version cannot read a session from. It stays unreachable until it ends; mur stop still ends it |
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
| Raised by | When |
|---|---|
mur stop |
The session or formation could not be ended |
mur trace rm |
The session to remove is still running |
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.
mur stop <formation-id> reports a formation it could not end with
the same code, as formation <id> could not be ended: <reason>:
| Reason | Means |
|---|---|
its members record N different launchers (pid A, pid B) |
The members disagree about which process launched them; nothing was signalled. End each member with mur stop <session> |
SIGTERM to its launcher, pid N, was refused: … |
The launcher could not be confirmed or may not be signalled by this user; nothing was signalled and every member is still running |
SIGKILL to its launcher, pid N, was refused: … |
The same, at the escalation step |
its launcher, pid N, was still running 5 seconds after SIGKILL |
The kernel accepted the signal and the launcher has not gone |
N of its members were still running 25 seconds later: <session>, … |
Those members' processes outlived the wait; their records are kept |
A member started by hand that could not be ended is reported as error: … in the
report and counted among the members still running.
mur trace rm refuses a session that a running-capsule record names while
its process is alive, or alive with a start time that could not be read, and removes nothing:
error[E-RUN-024]: session ses_019f0193c7d871a5b2e30ff41a7c0ce1 is still running (pid 48213); nothing was removed
hint: stop it with `mur stop ses_019f0193c7d871a5b2e30ff41a7c0ce1` first, then remove it
E-RUN-025 — an artifact with a gateway declares no usable upstream_auth: block
An artifact entry declares gateway:, and the artifact's own
murmur.yaml does not say how its upstream takes the key. The runtime attaches the key to each of
the artifact's gateway requests itself, so without that declaration it cannot, and mur run
refuses at staging, before any component runs or any request is sent. The refusal applies whether
or not gateway.api_key is set, and to the inference driver, tools and hooks alike.
error[E-RUN-025]: artifact 'murmur/driver-anthropic@0.19.0' declares no usable upstream_auth: block
hint: the runtime presents the key itself and needs the artifact to say how; update the artifact to a version whose murmur.yaml declares upstream_auth:, or remove gateway: from its entry
A block that is present and malformed adds the reason in parentheses:
| Reason names | Means |
|---|---|
field 'inference_auth' was renamed to 'upstream_auth' |
The artifact declares the block under its former name. Nothing under that name is read; update the artifact to a version that declares upstream_auth: |
'upstream_auth' has invalid type |
The block is not a mapping |
missing required field 'upstream_auth.header' or 'upstream_auth.value' |
A child is absent |
'upstream_auth.header' has invalid type or 'upstream_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 |
cannot carry the key |
header names a header that routes or frames the request, or that upstreams commonly echo back, such as Host or Origin |
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 upstream_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 in the session's
records directory and to a task_failed
line with cause: "credential_rejected", and the task fails: mur run then also prints
E-RUN-040 carrying the same message, and exits 1.
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 gateway.api_key on the driver's entry 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,
mur stop, mur ps --json,
mur trace list and mur trace rm read the
running-capsule records under ~/.murmur/running/, and that
directory could not be created, held at 0700, or listed. mur ps --json, mur trace list and
mur trace rm only list it, and also report a HOME from which it cannot be located.
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, Not a directory |
A file is where the directory belongs |
Permission denied |
The directory exists and this user may not list it |
E-WIT-LEVEL-001 — an artifact does not fit the served WIT level
An artifact fits the WIT level this mur serves when every murmur:* interface
it names is at the version the level lists for its package. Two commands refuse one that does not:
| Command | Refuses |
|---|---|
mur install |
A version it resolves by name, before anything is fetched or written: <scope>/<name>@<version>, --all-platforms <scope>/<name>@<version>, and each version murmur.yaml pins when run with no argument, whether its payload would come from the project store, the global store or the scope's source |
mur run, mur eval, mur new |
A driver, at staging, before any driver runs |
mur install ./<file>.mur.zip, mur install github:<owner>/<repo>@<tag>, mur publish and
mur deploy install the bytes as given; a launch refuses a driver among them that does not fit. A
version left out of mur install <scope>/<name> is always the newest that fits.
A runtime pull — manage.pull() or the pull-artifact
tool — refuses a version that does not fit before anything is written, as a tool error or a
manage.pull error opening does-not-fit:. Its hint names the newest version that fits in the
places the pull may use, or says none does:
does-not-fit: [E-WIT-LEVEL-001] artifact 'murmur/tool-edit@2.0.0' does not fit WIT level 3, the level this mur serves: it imports murmur:tool-registry/invoke@9.9.9, and level 3 has murmur:tool-registry@0.2.0; murmur/tool-edit@1.0.0 is the newest version in the global store that fits level 3
| Situation | Hint |
|---|---|
| An allowed place holds a version that fits | <scope>/<name>@<V> is the newest version in <the project store \| the global store \| github:<owner>/<repo>> that fits level <N> |
| None does | no version of <scope>/<name> in <places> fits level <N> |
At staging:
| Driver | Refused when |
|---|---|
Every runtime: driver artifact the manifest declares, except a transport: process inference driver |
It imports any murmur:text interface, or a murmur:stream interface at another version |
The inference.driver artifact of a transport: process capsule |
It exports murmur:driver/process only at another version |
The message names the artifact, the served level, and one clause per interface that does not fit:
error[E-WIT-LEVEL-001]: artifact 'ana/tool-x@2.0.0' does not fit WIT level 3, the level this mur serves: it names murmur:tool/run@9.9.9, and level 3 has murmur:tool@0.2.0
hint: ana/tool-x@1.0.0 is the newest version in github:ana/artifacts's index that fits level 3; pin it in murmur.yaml and run `mur install`, or run `mur install ana/tool-x@1.0.0`
error[E-WIT-LEVEL-001]: artifact 'ana/process-driver@1.0.0' does not fit WIT level 3, the level this mur serves: it exports murmur:driver/process@0.3.0, and level 3 has murmur:driver@0.4.0
A clause reads it exports|imports <interface>, and level <N> has <package>@<version>, or
…, and level <N> has no version of <package> for a package the level does not list. When
mur install refuses a version from the source's index rather than from a stored payload, the
clause reads it names <interface>: the index lists a version's interfaces without saying which
it exports and which it imports.
The hint names the newest version of the artifact in its scope's source index that fits the served level. Finding it downloads no payload:
| Situation | Hint |
|---|---|
| The index lists a version that fits | <scope>/<name>@<V> is the newest version in <source>'s index that fits level <N>; pin it in murmur.yaml and run `mur install`, or run `mur install <scope>/<name>@<V>` |
| The index lists none that fits | no version of <scope>/<name> in <source>'s index fits level <N>; rebuild it against level <N>'s packages (`mur version` lists them) |
| The index could not be read | the index of <source> could not be read (<reason>), so no fitting version can be named; run `mur search <scope>/<name>` once it answers, or rebuild it against level <N>'s packages (`mur version` lists them) |
| No source serves the scope | no source serves the '<scope>' scope, so no index can name a version of <scope>/<name> that fits level <N>; add the repository it publishes from under registry.sources and run `mur search <scope>/<name>`, or rebuild it against level <N>'s packages (`mur version` lists them) |
The artifact was installed from a file (local/) |
local/<name> was installed from a file, so no source's index can name a version of it that fits level <N>; rebuild it against level <N>'s packages (`mur version` lists them), then run `mur install <file>` on the rebuilt file |
hint: ana/process-driver@1.1.0 is the newest version in github:ana/drivers's index that fits level 3; pin it in murmur.yaml and run `mur install`, or run `mur install ana/process-driver@1.1.0`
hint: no version of ana/process-driver in github:ana/drivers's index fits level 3; rebuild it against level 3's packages (`mur version` lists them)
hint: the index of github:ana/drivers could not be read (request failed: io: Connection refused), so no fitting version can be named; run `mur search ana/process-driver` once it answers, or rebuild it against level 3's packages (`mur version` lists them)
hint: no source serves the 'ana' scope, so no index can name a version of ana/process-driver that fits level 3; add the repository it publishes from under registry.sources and run `mur search ana/process-driver`, or rebuild it against level 3's packages (`mur version` lists them)
hint: local/process-driver was installed from a file, so no source's index can name a version of it that fits level 3; rebuild it against level 3's packages (`mur version` lists them), then run `mur install <file>` on the rebuilt file
mur doctor lists every installed artifact that does not fit, under its
WIT level block.
E-RUN-029 — the driver does not speak the driver interface
mur run refuses at staging, before any driver runs, when a driver names no interface it could
be served by at any level:
| Driver | Refused when | Hint |
|---|---|---|
The inference.driver artifact of a transport: process capsule |
It exports no version of murmur:driver/process, so it is not a process driver |
rebuild the driver against murmur:driver/process@0.4.0, or name a process driver |
Every runtime: driver artifact the manifest declares, except a transport: process inference driver |
It imports a murmur:stream interface at the served package version under a name the package does not define, such as murmur:stream/other@0.1.0 |
rebuild <name> against murmur:stream/events@0.1.0, the stream interface this mur serves; pin a release built against it in murmur.yaml and run `mur install` |
error[E-RUN-029]: artifact 'my-process-driver@1.0.0' is the transport: process driver but does not export murmur:driver/process@0.4.0; a process driver must be built against the process-driver world
hint: rebuild the driver against murmur:driver/process@0.4.0, or name a process driver
A driver at another version of either interface is
E-WIT-LEVEL-001, which is reported when a driver fails both checks.
E-RUN-030 — the http driver is a process driver
A transport: http manifest names an artifact under inference.driver that exports the process
driver interface. mur run refuses at staging, before the driver runs.
error[E-RUN-030]: artifact 'my-process-driver@1.0.0' is the transport: http inference driver but exports murmur:driver/process@0.4.0, the process driver interface; name it under transport: process, or name an http driver
hint: set inference.transport: process to use this driver, or name an http driver
E-RUN-032 — the process driver could not be loaded
A process driver is loaded with no grants: no environment, no files,
no network and no Murmur host interface. mur run refuses at staging when the driver:
- imports anything but WASI,
- fails or runs out of time while describing itself, or
- names a required variable that is empty or contains
=.
The message ends with the reason; for an import, it names the interface.
error[E-RUN-032]: process driver 'my-process-driver@1.0.0' could not be loaded with no grants: component imports instance `murmur:stream/events@0.1.0`, but a matching implementation was not found in the linker
hint: the driver must export murmur:driver/process@0.4.0 and import nothing but WASI
E-RUN-033 — a harness turn failed
A transport: process turn ended in failure. The kind names what went wrong, and the message is
the harness's own.
error[E-RUN-033]: the harness turn failed (auth): not signed in on this host
hint: see `mur trace show` for the turn; auth means the harness is not signed in on this host, quota means wait or raise the plan's limit, and max-turns means raise inference.max_turns
| Kind | Means |
|---|---|
auth |
The harness could not authenticate |
quota |
The harness ran out of quota, or was rate limited |
max-turns |
The turn limit was reached — the harness's own, or inference.max_turns |
canceled |
The turn was interrupted |
harness-error |
The harness reported an error of its own |
other |
Anything else the harness reported |
The trace carries the same failure as a harness_failed event, whose source says whether the
harness reported it (harness) or the runtime ended the turn itself (runtime, which is what a
turn past inference.max_turns is). mur trace show prints it as one line.
E-RUN-034 — a process driver call failed
A call into the process driver refused, crashed, or ran out of time. The message names the WIT function and what came back.
error[E-RUN-034]: process driver 'my-process-driver@1.0.0' failed in launch: launch refused by config
hint: the driver refused or failed to translate for its harness; check inference.driver.config, or use a driver release built for this harness version
| Call | Made |
|---|---|
launch |
Once per run, to plan the harness's arguments, environment, files and stdin |
parse |
For each batch of complete stdout lines |
classify-exit |
When the harness's output ended without a terminal event |
A driver that runs past its 30-second per-call deadline, or whose launch plan names an unusable environment variable or file, fails the same way.
E-RUN-035 — the harness went silent
A transport: process run went 600 seconds with neither a line of harness output nor a tool call
through the bridge, so the harness was killed.
error[E-RUN-035]: the harness produced neither a line of output nor a tool call for 600s and was killed
hint: the harness was waiting on something that never came; check `mur trace show` for the last thing it did
There is no limit on how long a run may take: any output, and any tool call, starts the window
again. Output on stderr does not. The trace's harness_exit event records cause: "inactivity".
The 600 seconds is fixed, as is the 10-second interrupt grace a cancelled harness gets. Neither is a manifest setting.
E-RUN-036 — the harness could not find the session
A turn on transport: process was launched to continue the
session this context's harness session map holds, and the harness
failed it without ever reporting that session. The conversation is gone from the harness's own
store:
error[E-RUN-036]: the harness 'claude-code' could not continue session 0199c7d4-1f60-7c31-9a6e-0f2b9c1d4e55, which context 'ctx_0193f2…' names: no conversation found with session id 0199c7d4-1f60-7c31-9a6e-0f2b9c1d4e55 (that id is recorded in /home/you/.murmur/conversations/shey/ctx_0193f2…/harness-session.json)
hint: the harness no longer holds that conversation. murmur left the id where it is, so the next task in this context fails the same way rather than answering from nothing. Ask for it to be dropped and this context starts a new conversation: `mur run --context <id> --forget-session`, or `"forgetSession": true` under the murmur door extension's key in the next SendMessage or SendStreamingMessage message's metadata — see https://docs.murmur.nexus/reference/agent-card/#forget-session
The map entry is left exactly as it was. Starting a new conversation instead would be silent memory
loss: the next message would be answered as if nothing had been said. Dropping the entry is a
person's decision, asked for on the request that starts the next turn — with
mur run --forget-session or the door's
forgetSession message field, both described under
what removes an entry. A forget asked of a capsule on any other
transport is E-RUN-039.
The turn must have been launched to continue a session, the harness must never have reported one,
and the failure kind must be harness-error or other. A turn that started a new conversation had
nothing to lose, and auth, quota, max-turns and canceled each name a cause of their own —
an expired login is not a missing conversation — so all of those stay
E-RUN-033.
E-RUN-037 — --resume-mode compact under transport: process
--resume-mode compact runs the capsule's on-compaction hook over the conversation and continues
from its summary. On transport: process the harness holds the
conversation, so there is no history to hand that hook:
error[E-RUN-037]: --resume-mode compact is not available under inference.transport: process; the harness holds this conversation and murmur has no history to compact
hint: use --resume-mode full, which hands the harness the session id this context already has and lets it answer from everything it was told — see https://docs.murmur.nexus/reference/cli/
--resume-mode full is what this transport resumes with: the harness is launched on the session id
the map holds and answers from everything it was told before.
E-RUN-038 — a spend ceiling against a driver that reports no usage
A transport: process spend ceiling is enforced against the token
counts the harness reports and its driver relays. A driver whose describe() sets
reports-usage: false relays none, so a ceiling set against it could never be reached. mur run
refuses at staging, before anything is spawned and before a session directory exists:
error[E-RUN-038]: process driver 'my-process-driver@1.0.0' reports no usage — its describe() sets reports-usage: false — so inference.max_session_tokens and spend.machine_tokens_per_day could never be enforced
hint: use a driver release that reports its harness's token usage, or remove the ceiling — a ceiling murmur can never measure against would let the run go on for ever
The message names whichever ceilings are in effect:
inference.max_session_tokens from the manifest,
spend.machine_tokens_per_day from the effective config, or both. The same
capsule with neither in effect runs normally — a driver that reports no usage is a supported
driver, and its turns simply carry no token counts.
E-RUN-039 — nothing to forget on this transport
--forget-session and the door's forgetSession message field
drop the harness session a context names. Only
transport: process has one — on every other transport the
runtime holds the conversation itself, in a
conversation record. The CLI refuses at staging, before the
launch creates anything:
error[E-RUN-039]: --forget-session forgets the harness session a context names, and only inference.transport: process has one; this capsule's transport is 'http'
hint: a harness owns the conversation only under inference.transport: process, so that is the only transport with a session to forget; every other one keeps its conversation record here, which `mur conversation rm` removes — see https://docs.murmur.nexus/reference/cli/
The door refuses forgetSession: true on SendMessage and SendStreamingMessage with JSON-RPC
-32602, and starts no task — see forgetSession.
E-RUN-040 — the task did not complete
The session ran, shut down cleanly, and the task it ran for ended without completing. mur run
prints the task's exit status on its status: line, prints this error and exits 1:
status: failed
error[E-RUN-040]: the task ended failed: {"error":"driver: failed to parse Anthropic response JSON: EOF while parsing a string at line 1 column 38","stop_reason":"error"}
hint: the task's result text is in out/result.txt in the session's records directory, and `mur trace show <session>` shows the turn it ended on and why
| Exit status | Reason printed |
|---|---|
failed |
The reason of the run's task_failed line, whose cause names the kind of failure |
max_turns_reached |
That the task used every turn inference.max_turns allows |
spend_ceiling_reached |
That a spend ceiling refused the next inference call. The spend_ceiling_reached line names which ceiling and the numbers |
canceled |
That the task was canceled, by CancelTask, murmur.StopSession, mur stop, SIGTERM or the end of the session that delegated it. A task canceled by its formation's end is no error: the member ends formation_ended |
For canceled the hint says there is no result to read:
status: canceled
error[E-RUN-040]: the task ended canceled: the task was canceled
hint: the task was canceled before it completed, so out/result.txt in the session's records directory holds no result; `mur trace show <session>` shows the turn it was canceled on and why
A launch reports the first task that did not complete; a later run that completed does not replace
it. See Status and exit code. Under --json no status: line is printed,
and the error and exit code are the same.
E-RUN-041 — the control token could not be written
A capsule whose manifest declares control: writes its control token
to ~/.murmur/running/<session_id>.control before its door is announced. When that file cannot be
written, the capsule does not run:
error[E-RUN-041]: this capsule declares control: and its control token could not be written: <path>: <reason>
hint: make ~/.murmur/running a directory this user can write, or remove control: from murmur.yaml — a capsule whose declared controllability no controller could reach does not run
No inference call is made. The message names the path and the failure, never the token.
E-RUN-042 — no control surface, or a refused control request
mur control prints this in two cases:
| Case | Message |
|---|---|
The session holds no control token, because its manifest declares no control: block |
<session_id> has no control surface: it holds no control token, because its murmur.yaml declares no control: block |
The control surface answered anything but 200 |
the control surface of <session_id> refused <METHOD> <path>: HTTP <status>: <reason> |
What is controllable is fixed at launch, so the first case is answered by declaring control: and
restarting the capsule. Neither message carries the token or a secret value.
E-RUN-043 — a runtime pull declared in an operator-only role
A murmur.lock entry with origin: runtime was written by a capsule's
manage.pull(), not by an operator command. Such a pin stages only as a tool or a skill without a
gateway: block, reached by the model as a marked tool-array entry.
mur run refuses the launch, before any session directory exists, when murmur.yaml gives it one
of these roles. When more than one applies, the first in the table is named:
| Named in the message | Where murmur.yaml declares it |
Hint's second remedy |
|---|---|---|
runtime: hook |
The artifact's entry | remove runtime: hook from its murmur.yaml entry |
runtime: driver |
The artifact's entry | remove runtime: driver from its murmur.yaml entry |
gateway: |
The artifact's entry | remove gateway: from its murmur.yaml entry |
inference.system_prompt_artifact |
inference:, naming the skill as the system prompt |
stop binding it as inference.system_prompt_artifact in murmur.yaml |
error[E-RUN-043]: murmur.lock pins 'ana/some-tool@1.2.3' from a runtime pull by session ses_0190a1b2c3d4..., and murmur.yaml declares it with gateway:, which only an operator-declared pin may carry
hint: run `mur install ana/some-tool@1.2.3` to adopt the pin as operator-declared, or remove gateway: from its murmur.yaml entry
error[E-RUN-043]: murmur.lock pins 'ana/some-skill@1.0.0' from a runtime pull by session ses_0190a1b2c3d4..., and murmur.yaml declares it with inference.system_prompt_artifact, which only an operator-declared pin may carry
hint: run `mur install ana/some-skill@1.0.0` to adopt the pin as operator-declared, or stop binding it as inference.system_prompt_artifact in murmur.yaml
A mur run --system-prompt override replaces the binding, so an overridden launch stages the skill
as an ordinary marked entry.
mur eval run refuses the same pin per case: the case is recorded as stage_failed with this
message.
mur install <name>@<version> rewrites the entry as origin: operator and the next launch stages
it. mur doctor reports the same refusal ahead of a run.
E-RUN-044 — the formation id could not be read
A session launched as a member of a formation is handed
MURMUR_FORMATION_ID, and it hands the same value to every
child it delegates to. A formation id is frm_ followed by 32 lowercase hex digits. Any other
value refuses the launch before a session directory exists:
error[E-RUN-044]: MURMUR_FORMATION_ID does not carry a formation id: it does not start with 'frm_'; a session that cannot tell which formation it belongs to would be grouped with the wrong one, so the launch is refused
hint: MURMUR_FORMATION_ID is set by the runtime that launches a formation's members and by a member's runtime for the children it spawns; unset it to run this capsule on its own
The reason names what is wrong — the prefix, the number of digits, or a character outside lowercase hex — and never the value itself.
An unset or blank MURMUR_FORMATION_ID is not this error: it is the ordinary case of a session in
no formation.
E-RUN-045 — a formation member did not come up
mur run --roster starts every peer and waits for each one's door to
answer. A peer that does not come up refuses the whole launch. Every member already started is
stopped, the entry member is never started, and no formation line is printed:
error[E-RUN-045]: the formation could not be launched
'broken' (broken@0.1.0): its process exited with status 1 before reporting; its last stderr lines were:
[capsule-runtime] warning[W-SEC-027]: artifact 'murmur/driver-anthropic' gateway.api_key is written literally in murmur.yaml, 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.<NAME> <key>` to have it re-read (https://docs.murmur.nexus/reference/diagnostics/#w-sec-027)
error[E-RUN-008]: missing artifacts: murmur/driver-anthropic@9.9.9
hint: run `mur install` to install all manifest dependencies
stopped: coder (pid 529088), reviewer (pid 529091)
hint: the formation was not launched, and nothing it started is still running; fix the member named above and launch again. `mur run --capsule <capsule> --capsule-version <version>` runs one member on its own and shows its whole output
Each failed member is named as '<name>' (<capsule>@<version>), in roster order, with one reason:
| Reason | Means |
|---|---|
its process could not be started: <why> |
The mur binary could not be executed, or <why> is its directory <dir> could not be made: …: the peer's directory could not be made, or something is already at its path |
its process exited with <status> before reporting |
The member's mur run refused or crashed before its readiness line. Its last stderr lines follow, and carry the member's own error code |
its process reported and then exited with <status> before its door answered |
The member reported a door and ended before it answered |
it did not report within 180s |
No readiness line within the deadline |
its first line is not a readiness line |
The member printed something other than a JSON readiness line first |
it reported no door |
The readiness line names no URL |
it reported formation <x> instead of <minted>, or it reported no formation instead of <minted> |
The member is not running in the formation it was launched into |
its door at <url> did not answer as session <ses> within 180s |
Nothing answered at the reported address, or something else did. The last probe's error follows |
stopped: lists the members that were running when the launch was refused, as name (pid N), or
none. Every one of them has been stopped and reaped.
E-RUN-046 — not a formation's launcher, or an unreadable formation channel or lifeline
A process that already carries MURMUR_FORMATION_ID is a formation member, and a member cannot
launch a formation:
error[E-RUN-046]: MURMUR_FORMATION_ID is set, so this process is already a member of a formation, and a formation member cannot launch a formation
hint: a member joins the formation it was launched into and never starts one; unset MURMUR_FORMATION_ID to launch this roster as a formation of its own
The same code refuses a member whose formation channel cannot be read, before a session directory exists. The reason names the problem and never quotes the line:
error[E-RUN-046]: MURMUR_FORMATION_CHANNEL does not carry this member's formation credentials: descriptor 987 is not open; a member that cannot read what its launcher handed it could neither prove who it is nor call anyone, so the launch is refused
hint: MURMUR_FORMATION_CHANNEL is set by `mur run --roster` alone, for each member it starts, beside MURMUR_FORMATION_ID; launch the formation with `mur run --roster`, or unset both to run this capsule on its own
| The reason says | Means |
|---|---|
it is set, but MURMUR_FORMATION_ID is not |
MURMUR_FORMATION_CHANNEL without a formation id |
it does not name a file descriptor by number |
The variable is not a descriptor number |
descriptor N is standard input, output or error, never a channel |
The variable names 0, 1 or 2 |
descriptor N is not open |
Nothing is open at that descriptor |
descriptor N is neither a pipe nor a file |
The descriptor is a socket, a terminal or a directory |
the channel ended before its first line arrived |
The channel was closed empty |
no first line arrived within 10s |
The channel stayed open and silent for 10 seconds |
its first line is not JSON, … is not a JSON object, … has no string <field>, … has no calls array |
The first line is not a member's credentials |
its first line's formation_id is not a formation id: … |
As E-RUN-044 for the id |
its first line's member is not a member name: … |
The name does not match ^[a-z][a-z0-9-]{0,31}$ |
its first line's verify_key is not a formation key: … |
The key is not 32 bytes of base64url |
its first line's calls[N].name …, calls[N] names a member a second time |
A callee name is not a member name, or is listed twice |
its first line's calls[N].token was not issued to this member for that callee under the line's own key |
A token does not verify as this member's for that callee |
its first line names another formation than MURMUR_FORMATION_ID |
The channel belongs to another formation |
MURMUR_FORMATION_PEERS in mur run's own environment is refused the same way. The runtime sets
it inside a member's components, never in a process environment:
error[E-RUN-046]: MURMUR_FORMATION_PEERS cannot be used: it is set in this process's environment, so the launch is refused
hint: MURMUR_FORMATION_PEERS is set by a formation member's runtime inside its own WASM components, never in a process environment; unset it to run this capsule
It also refuses a mur run whose MURMUR_FORMATION_LIFELINE does not
name the member's end of its lifeline, before a session directory exists:
error[E-RUN-046]: MURMUR_FORMATION_LIFELINE does not carry this member's formation lifeline: descriptor 9 is a character device, and a lifeline is a pipe's read end; a member that cannot hear its lifeline would keep running after its formation ends, so the launch is refused
hint: MURMUR_FORMATION_LIFELINE is set by `mur run --roster` for each member it launches, beside MURMUR_FORMATION_ID, and is not for operators; unset it to run this capsule on its own
| The reason says | Means |
|---|---|
it is not a decimal descriptor number |
The value is not digits only. A blank value is absent, not refused |
descriptor N is a standard stream |
The value is 0, 1 or 2 |
descriptor N is not open |
The process holds no descriptor N |
descriptor N is a character device, a regular file, a directory, a socket |
Descriptor N is not a pipe |
descriptor N is open for writing |
Descriptor N is a pipe's write end |
it is set, but MURMUR_FORMATION_ID is not |
A lifeline belongs to a formation member |
it was already read by this process |
The lifeline is read once per process |
a script capsule opens no door |
Only an agent capsule can be a formation member |
Unset the variable to run the capsule on its own.
E-RUN-047 — the capsule changed since admission
Every member mur run --roster starts is handed the sha256 of the artifact admission resolved for
it. A member whose installed artifact now hashes differently — reinstalled or republished between
admission and launch — refuses before staging, rather than run other bytes:
error[E-RUN-047]: capsule 'local/coder@1.2.0' resolved to bytes with sha256 7bec909458461f9f56fca830b2c939a63eb79f2041c758e7390c1a9704a0167f, but it was admitted with sha256 0000000000000000000000000000000000000000000000000000000000000000
hint: the installed artifact changed after the roster was admitted; launch the formation again so admission reads what is installed now
Under mur run --roster it reaches the operator inside E-RUN-045, quoted from the
member's stderr. Launching again admits what is installed now.
E-RUN-048 — the session's door is public
mur token named a running session whose capsule declares no
network.authentication. Its door takes no token, so
none was minted:
error[E-RUN-048]: ses_019f01a940ce7761854e768ecbe3d399 has a public door: its capsule declares no network.authentication, so it minted no token
Call the door without one, or check that the address names the session you meant: @1 is the most
recent running session on the machine, whichever capsule it is.
E-RUN-049 — the session minted no such credential
mur token --credential named a credential the session's manifest does not declare. The message
lists every name it did mint, operator first:
error[E-RUN-049]: ses_019f01a940ce7761854e768ecbe3d399 minted no credential 'admin'; it has: operator, reader, watcher
Pass one of the listed names. A credential added to network.authentication.credentials after
launch is minted by the next launch, not this one.
E-RUN-050 — a link inside the workdir
The runtime met a symbolic link, or the wrong kind of file, inside the work folder, the records
directory or the runtime directory, refused to write, read, create or remove through it, and wrote
nothing outside them. mur run reports this code in two cases:
| When | Typical paths |
|---|---|
| Staging the session, before the capsule starts | The tools, skills and MURMUR.md staging writes in the runtime directory |
During the task mur run ran, after the capsule created a link |
The operator's task.md read when the session starts, a script capsule's out/result.txt, peer-in/ |
The message names the path and the part of it that is the link. A path in the work folder is named relative to it; a path in the runtime or records directory, by its absolute path:
error[E-RUN-050]: refused to create directory peer-in: peer-in is a symbolic link inside the workdir
hint: the runtime never follows a link inside the workdir; replace peer-in with a regular file or directory, or remove it, and run again
mur trace rm raises it too, for a link or a file that is not a directory
in place of the work folder or records directory it removes. Nothing is removed, and nothing the
link points at is touched. The message names the directory the path is relative to:
error[E-RUN-050]: refused to remove ses_019f0193c7d871a5b2e30ff41a7c0ce1: ses_019f0193c7d871a5b2e30ff41a7c0ce1 is a symbolic link (in /home/me/proj/workdir)
hint: the runtime never follows a link inside the workdir; replace /home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1 with a regular file or directory, or remove it, and run again
The hint names where the path is:
| Path | Hint |
|---|---|
| In the workdir | the runtime never follows a link inside the workdir; replace <path> with a regular file or directory, or remove it, and run again |
Under ~/.murmur/records, mur's records directory — mur trace rm's session records directory, or one a run writes to |
<path> is in mur's records directory, where mur never follows a link; remove it, or replace it with the directory it stands in for, and run again |
error[E-RUN-050]: refused to remove ses_019f0193c7d871a5b2e30ff41a7c0ce1: ses_019f0193c7d871a5b2e30ff41a7c0ce1 is a symbolic link (in /home/me/.murmur/records/home/me/proj/workdir)
hint: /home/me/.murmur/records/home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1 is in mur's records directory, where mur never follows a link; remove it, or replace it with the directory it stands in for, and run again
Remove the link, or replace it with the file or directory it stands in for. A refusal during
a task also writes a workdir_path_refused trace
line naming the path, and an A2A task that meets one ends failed with the refusal as its status
message; see Links inside the work folder.
E-RUN-051 — a delegated child's workdir is not the one its parent created
A capsule that delegates creates the child's work folder under children/ in its own
records directory and launches the child with that directory's
identity. The child compares it with the --workdir it opens, before writing anything there, and
refuses when they differ: something replaced the directory under children/, with a link or
another directory, between its creation and the launch:
error[E-RUN-051]: the workdir /home/dev/.murmur/records/home/dev/project/ses_019f01a9c3e47d1a9b2c3d4e5f6a7b8c/children/worker-3f9c2a1b7e4d5c60 is not the directory the delegating capsule created (expected 2049:1311021, found 2049:1311042)
hint: the delegating capsule's child work folder in children/ changed between the directory's creation and this launch; nothing was written
The delegation fails at the child's start, as any child that cannot launch does. Nothing was written under either directory.
E-RUN-052 — a driver choice has no output cap
Each driver choice of a transport: http capsule takes its
output cap from inference.max_tokens, or else from the
models: entry its driver declares for its model. With neither, the
launch is refused before any request leaves:
error[E-RUN-052]: driver choice 'primary' has no output cap: driver murmur/driver-openai@1.0.0 declares no limits for model 'gpt-5.6-luna' and the manifest sets no inference.max_tokens
hint: set inference.max_tokens to the model's output cap, or use a release of the driver whose murmur.yaml lists the model under models:
The choice is primary or an inference.alternates entry's name. The model id is matched by
exact string equality, so a dated id and its alias are separate entries. Set inference.max_tokens
in the capsule's manifest, or install a release of the driver that lists the model.
mur doctor reports the same condition as a fix.
A driver whose own models: table is malformed refuses the launch under the same code, naming the
driver and the field. An entry's max_input_tokens above its context_window is one such table:
error[E-RUN-052]: driver local/driver-glm@0.1.0 ships a malformed models: table: murmur.yaml: field 'models.glm-5.3.max_output_tokens' must be an integer from 1 to 4294967295
hint: the driver's own murmur.yaml is at fault; use a release of the driver whose models: table is well formed
mur install and mur publish refuse such a payload with E-REG-011, so the launch
meets one only in a store written some other way.
E-RUN-053 — the session reached its deadline
mur run --deadline <SECONDS> ends the session that many seconds after
mur run started. Reached, the running task is cancelled — or, when
lifecycle.keep_working is keeping it working, ends ok with
its last turn end's answer — the session's teardown runs, and the run exits 1:
status: deadline_reached
error[E-RUN-053]: the session reached its --deadline of 600 seconds; the runtime stopped the running task and ended the session
hint: raise --deadline, or set lifecycle.deadline_notice_secs so the agent is told sooner; `mur trace show <session>` shows where the task was when the deadline fell
The trace records deadline_reached and ends with session_end.exit_status: "deadline_reached".
A session runs under the earliest deadline placed on it. When that is
the one it inherited, from its formation's --deadline or from the capsule that delegated to it,
the message names the moment instead:
error[E-RUN-053]: the session reached the deadline it inherited, at 2026-10-20T14:00:00Z, from the formation or the capsule that launched it; the runtime stopped the running task and ended the session
hint: raise the --deadline the formation or the delegating capsule was launched with; `mur trace show <session>` shows where the task was when the deadline fell
A formation launched with mur run --roster --deadline exits 1 with
this code when its entry member ends at the deadline. When the deadline falls before the entry
member has started, the launcher stops every member it started and names them:
error[E-RUN-053]: the formation reached its --deadline of 60 seconds before its entry member started
stopped: coder (pid 41871), reviewer (pid 41872)
hint: raise --deadline so it covers the members' launch as well as the task; nothing the formation started is still running
The same code refuses a session staged with a deadline that its launch cannot enforce: one launched
by a program embedding the runtime rather than by mur run, or a script capsule.
E-RUN-054 — the compaction cap is above the model's ceiling
inference.compaction.max_tokens caps the calls a hook makes while
it serves on-compaction. The primary driver serves those calls, so a value above the
max_output_tokens its models: table declares for the compaction
model — inference.compaction.model, else inference.model — refuses the launch before any request
leaves:
error[E-RUN-054]: inference.compaction.max_tokens (9000) is above the output ceiling of 8192 that driver local/murmur-driver-openai@0.1.0 declares for compaction model 'gpt-5'
hint: lower inference.compaction.max_tokens to at most 8192, or remove it to use the driver's ceiling
A value equal to the ceiling is accepted. A compaction model the table does not list has no ceiling
to check against, and the value is used as written. mur doctor reports the
same condition as a fix.
E-RUN-055 — a driver choice has no input budget
Every inference call's input budget is the input ceiling, or the context window less the output cap the call sends, whichever is smaller. An output cap at or above the window leaves no input tokens, and the launch is refused before any request leaves:
error[E-RUN-055]: driver choice 'primary' has no input budget: its context window of 200000 (manifest) less its output cap of 200000 (manifest) leaves no input tokens; input ceiling none (model 'test-model', driver local/murmur-driver-anthropic@0.1.0)
hint: lower inference.max_tokens below the context window, or raise context.max_tokens above the output cap
| Field | Value |
|---|---|
| Choice | primary or an inference.alternates entry's name. Each choice is checked on its own window |
| Window, cap | Each followed by its source: manifest or driver |
| Input ceiling | The driver's max_input_tokens for the model, or none |
The same code refuses an inference.compaction.max_tokens at or
above the compaction model's window — context.max_tokens, else the context_window the primary
driver's models: table declares for that model:
error[E-RUN-055]: compaction calls have no input budget: the context window of 100000 (manifest) for compaction model 'gpt-5.6' less inference.compaction.max_tokens of 100000 leaves no input tokens; input ceiling 922000 (driver local/driver-openai@0.1.0)
hint: lower inference.compaction.max_tokens below 100000
A compaction model with neither window is not checked. mur doctor reports
both conditions as a fix.
E-RUN-056 — a script capsule has no door
mur watch, mur cancel and mur control connect to a capsule's door. The address named a
running script capsule, which binds none, so nothing is
connected to:
error[E-RUN-056]: ses_019f01a940ce7761854e768ecbe3d399 is a script capsule: it binds no door, so there is nothing to connect to
hint: mur ps lists it and mur stop ends it
mur stop ends a script capsule with a signal, and its
trace records what it did.
E-RUN-057 — a computer tool on a model without vision
A computer tool returns screenshots, so every transport: http driver choice of a capsule that
declares one — the primary and each alternate — must run a model
that reads images. A choice whose driver's models: table marks its
model vision: false refuses the launch before the session directory exists:
error[E-RUN-057]: tool 'computer' is a computer (provider_tool: computer) but model 'probe-model' of driver choice 'primary' has no vision: driver local/driver-example@1.0.0 declares vision: false for it
hint: run that driver choice on a model that reads images, or remove the computer tool from artifacts: — see https://docs.murmur.nexus/reference/diagnostics/#e-run-057
The first such tool and the first such choice, primary first, are named. A model whose entry has
no vision key, or that the table does not list, is not refused. A transport: process capsule
is not checked. A pull-artifact of a computer tool into such a session is refused the same way.
E-RUN-058 — a malformed provider_tool: block
A stored tool artifact's provider_tool: block does not parse, or
sits on an artifact whose runtime is driver, hook or skill:
error[E-RUN-058]: artifact 'local/computer@1.0.0' ships a malformed provider_tool: block: murmur.yaml: field 'provider_tool.display_width' must be an integer from 1 to 65535
hint: provider_tool: holds kind: computer with display_width and display_height, each an integer from 1 to 65535; install a version of the artifact that ships a valid block, or remove it from artifacts: — see https://docs.murmur.nexus/reference/diagnostics/#e-run-058
mur build refuses the same block with E-BLD-006.
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 where that namespace cannot be created 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: <failed call> failed with <ERRNO> (<description>). <likely cause and remedy> Host readings: kernel.apparmor_restrict_unprivileged_userns=<0|1>; AppArmor profile confining mur: <profile>.
mur decides by creating the namespace: a short-lived child runs exactly the construction a
launch runs, and its result is the answer. AppArmor's unprivileged-userns restriction
(kernel.apparmor_restrict_unprivileged_userns) and the profile confining mur are read only to
explain a refusal. A container confined by its own AppArmor profile, on a host whose restriction
reads 1, is not refused when the namespace can be created inside it.
The refusal names the failed call, its errno and the host readings. Together they identify the
host condition:
Failed call and errno |
Readings line | Host condition | What to do |
|---|---|---|---|
writing uid_map/gid_map or unshare(CLONE_NEWUSER \| CLONE_NEWNET), EPERM |
=1; profile unconfined or not reported |
AppArmor's restriction applies to mur, and no mur-sealed profile confines it. Ubuntu lets the unshare through and refuses the identity map that follows, so the failed call is usually writing uid_map/gid_map |
Install and load the AppArmor profile shipped with mur; for a checkout build, run scripts/install-dev-apparmor.sh. The refusal gives both commands. Setting the restriction to 0 host-wide is the last resort |
Any call, usually unshare(…) with EPERM |
Profile is another name, such as docker-default |
A container confined by its own AppArmor profile; that profile, the container runtime's seccomp filter and its capabilities refused the namespace. The restriction setting does not govern it | Run the container with an AppArmor profile and seccomp policy that permit unshare(CLONE_NEWUSER), or with --cap-add SYS_ADMIN, or create the network namespace outside the container and run mur inside it |
Any call, usually unshare(…) with EPERM |
=0, or AppArmor not enabled |
A container runtime's seccomp filter or a missing CAP_SYS_ADMIN, or another security module such as SELinux |
The container remedies in the row above; on a bare host, the policy of the security module in use |
| Any call | Profile mur-sealed |
The mur-sealed profile confines mur and the namespace still failed: the loaded profile may be out of date |
sudo apparmor_parser -r /etc/apparmor.d/mur-sealed, then compare the installed profile in mur doctor |
writing uid_map/gid_map, bringing lo up or installing the local default route |
Any other | The namespace was created and then could not be owned (uid_map) or configured (lo, route) — a confinement that withholds capabilities inside the namespace |
As the row matching the readings line |
unshare(…), ENOSPC, EINVAL, ENOSYS or EUSERS |
Any | The kernel does not provide the mechanism: user.max_user_namespaces is 0 or exhausted (ENOSPC), the kernel is built with CONFIG_USER_NS=n (EINVAL, ENOSYS), or the nesting limit for user namespaces is reached (EUSERS) |
Raise user.max_user_namespaces, run on a kernel built with CONFIG_USER_NS=y, or run mur fewer user namespaces deep |
No call: the runtime's own message, such as fork() for the namespace creator failed or the namespace creator exited without handing anything over (killed by signal …) |
Any | The namespace creator could not run, or stopped before reporting: a process or descriptor limit or memory ran out, or a policy killed it | Re-run once the limit is relieved; if the message names a signal, allow the creator's syscalls in the policy that killed it |
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.
A capsule can spawn a subprocess through any of the routes below, and mur run checks each at a
different point:
| Route | Checked |
|---|---|
capabilities.shell.allow or capabilities.spawn.allow is non-empty |
At staging, before any artifact is pulled |
capabilities.install.tool is non-empty: a pattern can match a native tool a pull installs |
At staging, before any artifact is pulled |
A declared runtime: tool artifact ships a native implementation |
At launch, once the artifacts are staged and before any component is instantiated |
A native tool pulled into the session by manage.pull or pull-artifact |
At that tool's first call, which fails with E-CAP-005 and the same reason |
Every check point and mur doctor print the same reason. mur doctor reports the answer as the
network namespace: line of its AppArmor / user namespaces block, for every project, beside the
mount namespace: and sealed root: lines. How the three lines and the readings tell host
conditions apart: Telling host conditions apart.
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.
When the capsule declares a provider tool, the refusal names each
such tool on its own line under the message, before the hint:
required by: tool 'computer' (provider_tool: computer).
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
work folder. 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 https://docs.murmur.nexus/reference/resource-plane/
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 https://docs.murmur.nexus/reference/resource-plane/
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 https://docs.murmur.nexus/reference/workdir/#state-store
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 https://docs.murmur.nexus/reference/workdir/#state-store
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 https://docs.murmur.nexus/reference/manifest/#artifact-config
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 work folder or records directory 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 https://docs.murmur.nexus/reference/manifest/#field-context
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 work folder:
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, submit-plan, switch-driver, call-member,
end-without-answer, pull-artifact, remove-artifact, session-status and
background-command are answered by the runtime itself, so
no artifact may have one of them as its tool name, the part of its full name after the /.
ana/pull-artifact is refused as pull-artifact would be:
error[E-CAP-013]: artifact 'ana/pull-artifact' has the tool name 'pull-artifact', which collides with a tool the runtime provides itself; the reserved names are share-file, fetch-peer-file, delegate-task, submit-plan, switch-driver, call-member, end-without-answer, pull-artifact, remove-artifact, session-status, background-command
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 https://docs.murmur.nexus/reference/runtime-provided-tools/
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 pull of one of these names, by manage.pull() or by
pull-artifact, is refused the same way, and the 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 |
artifacts[].gateway.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
gateway.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 (artifacts.murmur/driver-anthropic.gateway.api_key). This is one of the two Environment requirements 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 the spawn closure 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 WASM
component's 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 presents it itself through the artifact's gateway.api_key — see https://docs.murmur.nexus/reference/diagnostics/#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 or third-party API key, set
gateway.api_key on the entry of the artifact that uses it: the
runtime presents the key itself. For a variable a component 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.
E-CAP-017 — a gateway on a native tool
A runtime: tool entry declares gateway:, and the artifact it
names ships a native implementation.
error[E-CAP-017]: artifact 'my-native-tool' declares 'gateway:' but ships a native implementation, whose requests never pass the credential gateway
hint: a native tool's requests leave through the egress proxy and never pass the credential gateway, so no key could be attached; remove gateway: from its entry, or use a WASM build of the tool
mur run refuses at staging, before a session directory is created. A native tool runs as a host
subprocess whose requests the runtime cannot attach a key to.
E-CAP-018 — a gateway that binds no credential
An artifact entry's gateway: names an endpoint but has no
non-blank api_key and does not write keyless: true. An api_key that is absent, "",
whitespace or null counts as none, as does keyless: false.
error[E-CAP-018]: murmur.yaml: invalid artifact declaration at index 0: artifact 'local/tool-web-search' declares gateway.endpoint 'https://api.tavily.com' but binds no credential
hint: set gateway.api_key: ${NAME} on this entry to have the runtime present a key, or write gateway.keyless: true if this upstream takes no key — the runtime never infers keylessness from the address
Refused when the manifest is parsed, by mur run, mur doctor and a roost reading a child's
manifest alike, before a session directory is created or a request is sent. The endpoint's address
plays no part: a loopback endpoint is refused the same way.
| Remedy | When |
|---|---|
api_key: ${NAME} on the entry, with the key stored by mur config set -g credentials.NAME <key> |
The upstream takes a key |
keyless: true on the entry |
The upstream takes no key, such as a local model server — see Keyless upstreams |
E-CAP-019 — a process driver needs an undeclared variable
A process driver requires variables in its CLI's environment, and
capabilities.env.allow does not declare every one of them. The CLI sees only the variables the
manifest declares. mur run refuses at staging and names each missing variable.
error[E-CAP-019]: process driver 'my-process-driver@1.0.0' requires MY_PROFILE in its environment, which capabilities.env.allow does not declare
hint: add each variable to capabilities.env.allow — the harness sees only variables the manifest declares
E-CAP-020 — two artifacts share a tool name
A session offers each declared artifact to the model by its tool name, the part of its full name
after the /, and stages it under tools/<tool name>/ in the session's
runtime directory. Two declared artifacts with one
tool name are refused, whatever their scopes:
error[E-CAP-020]: artifacts 'murmur/tool-edit' and 'ana/tool-edit' share the tool name 'tool-edit'; a capsule offers each artifact to the model by its tool name, so no two may share one
hint: declare one of them: the model calls each artifact by the part of its name after the '/', so two scopes cannot both serve one tool name in a capsule — see https://docs.murmur.nexus/reference/diagnostics/#e-cap-020
A source: entry counts by its bare name, so - name: tool-edit with source: beside
murmur/tool-edit is refused the same way. The check runs before any artifact is resolved, so
nothing is resolved and no session directory is created. Declare one of the two.
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
show, rm and truncate name one context id, optionally narrowed with --record. Nothing
matching it is refused naming what was looked for and where. For show, a context directory that
holds neither conversation.jsonl nor harness-session.json matches nothing:
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 show, 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: is its bare name. It becomes a filename (<name>-<version>.mur.zip), the tool
name the model calls 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. A / is refused on its
own, in name: and in mur build --skill <name> alike, because the scope belongs to the source
the artifact is installed from — see Artifact names:
error[E-BLD-001]: invalid artifact name 'ana/tool-x': an artifact's own name carries no scope, so it may not contain '/'
hint: the scope belongs to the source the artifact is published from, not to the artifact: name it 'tool-x', and it installs as <owner>/tool-x from github.com/<owner> or as local/tool-x from its .mur.zip file
name: my-tool # ok
name: My Tool # E-BLD-001
name: my_tool # E-BLD-001
name: -my-tool # E-BLD-001
name: ana/my-tool # E-BLD-001
E-BLD-002 — unsafe or colliding packed 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>'
error[E-BLD-002]: requires_files entry 'murmur-build.json' packs as the archive entry 'murmur-build.json', which is reserved for mur build
error[E-BLD-002]: <path> is a symlink; a skill folder packs regular files only
error[E-BLD-002]: <folder> holds both SKILL.md and skill.md; a skill has one skill.md
error[E-BLD-002]: <folder>/murmur-build.json packs as the archive entry 'murmur-build.json', which is reserved for mur build
error[E-BLD-002]: skill source refused: <one of the three skill-folder messages above>
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. -
No
murmur-build.jsonat the archive root.mur buildwrites that entry itself, with the artifact's built-at WIT level. The same name in amur build --skillinput is refused too.
A skill folder built with no murmur.yaml (see mur build skill packaging),
or installed as a local source: directory, follows the same rules over every file it holds: no
symlink anywhere, one skill.md at the root in whichever casing, and no root murmur-build.json.
The skill source refused: form is mur run refusing a local source: directory before any
workdir is created.
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.
E-BLD-004 — manifest declares a retired block name
error[E-BLD-004]: murmur.yaml: field 'inference_auth' was renamed to 'upstream_auth'; this murmur reads only the new name
hint: rename the block to upstream_auth: — an artifact published with inference_auth: is refused at launch with E-RUN-025
The artifact's murmur.yaml declares inference_auth:, the former name of the
upstream_auth: block. mur build refuses before writing the
.mur.zip, including when upstream_auth: is also declared. Rename the block; its fields are
unchanged.
E-BLD-005 — malformed models: table
error[E-BLD-005]: murmur.yaml: field 'models.glm-5.3.max_output_tokens' must be an integer from 1 to 4294967295
hint: models: is a driver's table of model id to max_output_tokens and context_window, and optionally max_input_tokens (at most context_window), each an integer from 1 to 4294967295, and vision, a boolean; a driver shipped with a malformed table is refused at launch with E-RUN-052
mur build refuses before writing the .mur.zip, naming the field:
models: |
Field named |
|---|---|
| Not a mapping | models |
| An empty model id | models |
Declared on an artifact whose runtime is not driver |
models |
| An entry that is not a mapping | models.<id> |
An entry missing max_output_tokens or context_window |
models.<id>.<key> |
A value that is not an integer from 1 to 4294967295 |
models.<id>.<key> |
A max_input_tokens above the entry's context_window |
models.<id>.max_input_tokens |
A vision that is not a boolean |
models.<id>.vision |
| An entry holding any other key | models.<id>.<key> |
The block's shape is in models: block.
E-BLD-006 — malformed provider_tool: block
error[E-BLD-006]: murmur.yaml: field 'provider_tool.display_width' must be an integer from 1 to 65535
hint: provider_tool: goes on a runtime: tool artifact and holds kind: computer with display_width and display_height, each an integer from 1 to 65535; a tool shipped with a malformed block is refused at launch with E-RUN-058
mur build refuses before writing the .mur.zip, naming the field:
provider_tool: |
Field named |
|---|---|
Declared on an artifact whose runtime is driver, hook or skill |
provider_tool |
| Not a mapping | provider_tool |
No kind, a kind that is not a string, or a kind other than computer (the message lists the known kinds) |
provider_tool.kind |
A display_width or display_height missing, not an integer, or outside 1 to 65535 |
provider_tool.<key> |
| Any other key | provider_tool.<key> |
The block's shape is in provider_tool: block.
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.
Deploy warnings
mur deploy run prints these on stderr after it has asked the target to
compile the uploaded artifacts. The deploy continues, the capsule starts, and mur deploy run exits
0. See Precompile on the target.
W-DEPLOY-001 — the target did not precompile
Two forms:
warning[W-DEPLOY-001]: the target did not precompile (error: unrecognized subcommand 'precompile'); the capsule compiles its artifacts on its first launch
warning[W-DEPLOY-001]: the target could not precompile tool-echo@1.0.0; the capsule compiles it on its first launch
| Form | Cause | Fix |
|---|---|---|
did not precompile (<error>) |
The target's mur printed no mur precompile report. The parenthesis quotes the first line it printed on stderr: unrecognized subcommand 'precompile' means the target runs a mur release older than the command; an SSH error means the command never ran |
Set deploy.mur_version in the manifest to a release that has mur precompile, or pass --mur-binary |
could not precompile <name@version>, … |
The report names those artifacts failed: the payload is not a .mur.zip, has no murmur.yaml that parses, or its WASM does not compile on the target |
Rebuild or re-publish the artifact. An artifact whose WASM does not compile also fails the capsule's launch |
The capsule's first launch compiles what the target did not, taking as long as a launch with
--no-precompile.
W-DEPLOY-002 — the target's decompression ceiling differs
warning[W-DEPLOY-002]: the artifact decompression ceiling is 524288000 bytes on this machine and 600000000 bytes on the target; the capsule runs, warm, under the target's value. To align them, pass --env MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES=524288000
The target's MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES — the value in the deploy's .env, or the
default — is not this machine's. The target compiled and the capsule runs under the target's value,
so the first launch is still warm, but an artifact that extracts here may be refused there, or the
reverse. To run under this machine's value, pass the --env the warning gives.
Config errors
E-CFG-003 — a config file's registry: section cannot be used
mur install and mur deploy build each scope's source
from the registry: section of every config file they read, and refuse a section they cannot use,
naming the file. Nothing is requested or written. Other commands do not read registry:.
error[E-CFG-003]: /home/ana/.murmur/config.yaml sets registry.default, which this mur no longer reads: each scope has exactly one source, so there is no first source to choose
hint: delete registry.default; a source is one registry.sources entry, `- repo: <owner>/<repo>` with an optional `token:`; it serves the scope named by its owner, and the murmur scope is always served by murmur-nexus/default-artifacts
| Message names | Fix |
|---|---|
sets registry.default |
Delete the key |
registry.sources[N] sets name:, type: or url: |
Delete those keys and keep repo: and token: |
registry.sources[N] names no repo |
Add repo: <owner>/<repo> |
repo '…' is not a GitHub repository written <owner>/<repo> |
Write the repository as <owner>/<repo> |
repo '…' claims the reserved scope 'local' or 'murmur' |
Remove the entry. Only murmur-nexus/default-artifacts serves murmur, and an entry naming it may only set its token: |
repo '…' names no usable scope |
The owner is not a valid scope; name another repository |
registry.sources[N] (…) and registry.sources[M] (…) both serve scope '…' |
Keep one of them |
Index errors
mur index writes an artifact index and mur search reads one. A refused
mur index writes nothing and leaves an existing output file as it was.
E-NEW-006 — mur new could not read the artifact index
mur new writes each default artifact at the newest version that fits this mur in a published
artifact index — see mur new. With no index to read it refuses and writes
nothing. The message names the index and why it was not read:
error[E-NEW-006]: could not read the artifact index of github:murmur-nexus/default-artifacts: request failed: io: Connection refused
hint: pass --index <PATH|URL> to take versions from an artifact index file or http(s) URL instead
| Index | Message names |
|---|---|
Omitted --index: the latest default-artifacts release |
the artifact index of github:murmur-nexus/default-artifacts and what the source answered. A rate limit adds the token advice mur install gives to the hint |
An --index URL |
the artifact index at <URL> and the request's failure or HTTP status |
An --index file |
the artifact index file <PATH> and the read error |
Restore the network or the source, or pass --index naming a copy of an index you hold.
E-NEW-007 — no fitting version of a default artifact
The artifact index mur new read is of another scope, does not list a default artifact it writes,
or lists only versions that do not fit this mur. The message names the artifact and the index,
and for versions that do not fit, the newest one and why:
error[E-NEW-007]: the artifact index github:murmur-nexus/default-artifacts v1.0.0 lists no version of murmur/driver-anthropic that fits this mur; the newest it lists, 1.1.0, needs murmur:driver/process@9.9.9, this mur serves murmur:driver@0.4.0
hint: upgrade mur, or pass --index <PATH|URL> naming an artifact index that lists a version of murmur/driver-anthropic this mur fits
A version fits when this mur serves its WIT level and one of its payloads runs on this platform
— see WIT levels.
E-IDX-001 — a file cannot be indexed
mur index refuses the whole directory when one .mur.zip in it cannot be indexed, naming the
file and the reason:
error[E-IDX-001]: release/tool-editor-0.7.0.mur.zip cannot be indexed: its file name does not match its packed name 'tool-editor' and version '0.6.0': expected tool-editor-0.6.0.mur.zip, or tool-editor-0.6.0-<platform>.mur.zip with <platform> one of darwin-aarch64, darwin-x86_64, linux-aarch64, linux-x86_64
| Reason | Fix |
|---|---|
it is not a zip archive, it holds no packed murmur.yaml, its packed murmur.yaml is invalid |
Rebuild the artifact with mur build |
its packed name '<name>' … |
Give the artifact a valid name: — see Artifact names |
its packed version '<v>' is not a semantic version, … is reserved |
Set version: to a semantic version such as 1.0.0 |
its file name does not match its packed name … and version … |
Rename the file to <name>-<version>.mur.zip, or rebuild it |
a native payload names its platform |
Name the file <name>-<version>-<platform>.mur.zip |
a wasm payload runs on every platform, a static payload … |
Drop the platform from the file name |
its wasm payload cannot be read as a component: it has no root .wasm entry …, … its root .wasm entries are ambiguous: … |
A wasm payload carries its component as capsule.wasm or as its one root *.wasm; rebuild it with mur build |
its wasm payload cannot be read as a component: its root .wasm entry <file> is a core module, not a component |
Build the payload as a component (--target wasm32-wasip2), then mur build |
its wasm payload cannot be read as a component: its root .wasm entry <file> does not parse as a component: … |
The file is not wasm, or is truncated; rebuild it |
its wasm payload cannot be read as a component: its root .wasm entry <file> is over the <N>-byte decompression cap |
The component is larger than MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES allows |
it packs runtime '<a>' …, and <file> of the same <name>@<version> packs runtime '<b>' |
Build every platform's payload of one version from the same murmur.yaml |
its packed murmur-build.json is not JSON …, … sets no built_at_level, … sets built_at_level to <value>, which is not a non-negative integer |
Rebuild the artifact with mur build, which writes the file |
its packed murmur-build.json sets built_at_level <a>, and <file> of the same <name>@<version> sets built_at_level <b> |
Build every platform's payload of one version with the same mur |
A payload whose packaging is wasm must carry a component mur index can read, because the index
records the interfaces it names, and an empty list would fit every mur:
error[E-IDX-001]: release/tool-w-0.1.0.mur.zip cannot be indexed: its wasm payload cannot be read as a component: its root .wasm entry capsule.wasm is a core module, not a component
A native payload and a skill carry no component and are indexed with no interfaces.
A directory with no .mur.zip directly inside it is refused too:
error[E-IDX-001]: release holds no .mur.zip file to index
E-IDX-002 — not a schema 2 artifact index
mur search, mur new --index and mur index --merge read only a schema 2 artifact index.
A schema 1 index, whose schema_version is 1 as a string or a number, is refused with a hint
for its publisher:
error[E-IDX-002]: artifact index https://github.com/ana/my-artifacts/releases/latest/download/artifacts-index.json is a schema 1 index
hint: this is a schema 1 artifact index, which this mur does not read; its publisher regenerates it with `mur index <dir> --repo <owner>/<repo> --release <tag>`
| Message ends with | Cause |
|---|---|
is a schema 1 index |
The index's publisher has not regenerated it with mur index |
has schema_version <value>, and this mur reads only schema 2 |
Another schema version |
is not valid JSON: … |
The file is not JSON, or is truncated |
is not a valid schema 2 index: <location> <reason> |
A field breaks the format; <location> names it, such as artifacts[0].versions[1].assets is missing |
mur install reads a source's index from its latest release, so a release with no
artifacts-index.json, or one carrying a schema 1 index, is refused too, naming the artifact being
resolved. Its publisher writes and uploads one — see
How to publish your own artifact source:
error[E-IDX-002]: github:ana/artifacts publishes no artifacts-index.json on its latest release v2, so ana/tool-git@1.0.0 cannot be looked up
hint: its publisher writes the index with `mur index <dir> --repo ana/artifacts --release v2` and uploads artifacts-index.json to release v2
E-IDX-003 — mur index was not given a usable source
--repo must be a GitHub repository <owner>/<repo> whose owner is a valid scope, and
--release a tag with no whitespace. The scope comes from --repo by the rule
mur install github:<owner>/<repo>@<tag> uses, so the reserved scopes local and murmur are
refused, except murmur for murmur-nexus/default-artifacts:
error[E-IDX-003]: --repo 'murmur/tools' would index under 'murmur', a reserved scope
hint: 'local' names artifacts installed from a .mur.zip file and 'murmur' names those from murmur-nexus/default-artifacts; no GitHub owner can claim either
An index given to --merge must have the scope --repo names:
error[E-IDX-003]: --merge previous.json indexes scope 'bob', and --repo ana/tools names scope 'ana'
Registry errors
E-REG-001 — the source does not list the artifact
The scope's source answered, and its index lists no artifact of that exact name, no such version, or no payload of the version for this platform:
error[E-REG-001]: could not resolve ana/tool-git@2.0.0: the index of github:ana/artifacts lists ana/tool-git at 1.0.0, 0.9.0
hint: name one of those versions, or omit the version to install the newest that fits this mur: mur install ana/tool-git
| Message | Fix |
|---|---|
the index of <source> (release <tag>) lists no <scope>/<name> |
Check the name. A name matches exactly: ana/tool-git is not ana/tool-git-validate |
lists <scope>/<name> at <versions> |
Name one of the listed versions, or leave the version out |
has no payload of it for <platform>; it publishes … |
Build it on this host with mur build and install the file, or ask the publisher to release that platform's asset |
no version of <scope>/<name> in <source> fits WIT level <N>, the level this mur serves; the newest is … |
The reference named no version and no version fits; the hint links the WIT levels page, which lists the mur release serving each level |
the index of <source> lists it as <file> on release <tag>, and … |
The index and the releases disagree; the publisher regenerates the index |
An explicit version that is listed but does not fit is refused with
E-WIT-LEVEL-001.
A local/ artifact missing from the store is refused with the same code; no source serves the
local scope:
error[E-REG-001]: artifact local/tool-gi@1.0.0 is not in the store, and no source serves the 'local' scope
hint: a local/ artifact is installed from its .mur.zip file: mur install ./tool-gi-1.0.0.mur.zip
A launch whose murmur.yaml names an artifact neither store holds is refused at staging:
error[E-REG-001]: artifact ana/tool-gi@1.0.0 is not in the store
hint: install it: mur install ana/tool-gi@1.0.0
E-REG-002 — an artifact's bytes or name do not match
Installed bytes that do not hash to the sha256 murmur.lock
records for them are refused:
error[E-REG-002]: artifact integrity check failed for ana/tool-x@1.0.0
hint: artifact on disk does not match murmur.lock — re-publish or delete the lock
A payload fetched from a source is checked against the source's index before anything is stored.
A digest or a packed version: other than the index lists is refused:
error[E-REG-002]: could not resolve ana/tool-git@1.0.0: tool-git-1.0.0.mur.zip from github:ana/artifacts has sha256 bbe05ded0b5b42985748292a0d20b482c73d5e7f346a275c80f0d45b4386a038, and its index lists sha256 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
hint: the payload is not the one the index describes, so nothing was stored; its publisher regenerates the index, or the download was altered on the way
error[E-REG-002]: tool-git-1.0.0.mur.zip from github:ana/artifacts packs version '1.0.1', and its index lists it as ana/tool-git@1.0.0
hint: the asset was built before its version was set, so nothing was stored; its publisher rebuilds it and regenerates the index
An artifact is filed under the bare name its own murmur.yaml declares — see
Store layout. Installing it under a full name whose bare
part differs is refused, and nothing is written to the store:
error[E-REG-002]: artifact ana/tool-x@1.0.0 declares name 'tool-y' in its murmur.yaml, so it cannot be filed as ana/tool-x
hint: an artifact is filed under the name its own murmur.yaml declares; install it as ana/tool-y
E-REG-006 — a source did not answer the lookup
mur install, mur install --all-platforms and mur deploy run ask the
source of the artifact's scope. When that source fails
to answer a request, the install fails with E-REG-006 rather than
E-REG-001: the source may publish the artifact. The message gives what the source
returned:
error[E-REG-006]: could not look up murmur/driver-anthropic@1.0.0: github:murmur-nexus/default-artifacts did not answer — rate limited by GitHub (HTTP 403, x-ratelimit-remaining: 0): API rate limit exceeded for 203.0.113.7. — resets in about 41 minutes
hint: set GITHUB_TOKEN to any GitHub token, for example `export GITHUB_TOKEN=$(gh auth token)`, or point the source's `token:` in config.yaml at a variable you already export, such as `token: ${GH_TOKEN}` — GitHub allows 60 unauthenticated API requests an hour and the first artifact of a scope can spend five; see https://docs.murmur.nexus/reference/installing-artifacts/
The parenthesis after rate limited names what identified the rate limit:
| Shown | Identified by |
|---|---|
(HTTP 429) |
The status alone |
(HTTP 403, x-ratelimit-remaining: 0) |
GitHub's rate-limit header |
(HTTP 403, retry-after header) |
A retry-after header, sent with GitHub's secondary rate limits |
(HTTP 403, response body says rate limit) |
Only the response text; no rate-limit header was present |
An explicit mur install github:<owner>/<repo>@<tag> reports a rate limit the same way.
After did not answer — |
Meaning | Fix |
|---|---|---|
rate limited by GitHub, requests sent without a token |
The anonymous limit for this IP address is spent | Export GITHUB_TOKEN, or set token: on the source — see Rate limits |
rate limited by GitHub, requests sent with a token |
The token's own limit is spent | Retry after the reset the message shows |
HTTP 401 or HTTP 403 without rate limited |
GitHub refused the token — for example, an organization's SAML enforcement, or a revoked token | Authorize or replace the token |
HTTP 5xx, request failed: … |
GitHub or the network did not answer | Retry |
E-REG-007 — a command-line artifact name is not <scope>/<name>
mur install <ref> and mur run --capsule take an artifact's full name, <scope>/<name> — see
Artifact names. A bare name is refused with a hint naming
its scoped forms:
error[E-REG-007]: 'tool-edit' is not a full artifact name: an artifact is named <scope>/<name>
hint: name it with its scope: 'murmur/tool-edit' for a Murmur default artifact, '<owner>/tool-edit' for one published from github.com/<owner>, or 'local/tool-edit' for one installed from a .mur.zip file
A name with more than one /, or a part the name rule refuses, is refused with the reason and no
hint:
error[E-REG-007]: 'a/b/c' is not a valid artifact name: expected exactly one '/', between the scope and the name
local and murmur are reserved scopes, so a github: reference whose owner is either is
refused:
error[E-REG-007]: github:local/repo cannot be installed: 'local' is a reserved scope
hint: 'local' names artifacts installed from a .mur.zip file and 'murmur' names those from murmur-nexus/default-artifacts; no GitHub owner can claim either
E-REG-008 — no source serves the scope
Each scope has one source. murmur is built in; any
other scope needs a registry.sources entry naming a repository its owner publishes from. A scope
with none is refused before any request:
error[E-REG-008]: no source serves the 'ana' scope, so ana/tool-x@1.0.0 cannot be looked up
hint: add the GitHub repository 'ana' publishes from to ~/.murmur/config.yaml or the project's .murmur/config.yaml:
registry:
sources:
- repo: ana/<repository>
E-REG-009 — a source's index names another scope
A source answers only for the scope of its owner, so the scope of its
index must be that scope. An index naming another one is refused, and nothing
is downloaded beyond it:
error[E-REG-009]: could not resolve ana/tool-x@1.0.0: github:ana/claims-bob publishes an index for scope 'bob', and it serves scope 'ana'
hint: a source answers only for the scope of its owner, so nothing was installed; its publisher regenerates the index with `mur index <dir> --repo ana/claims-bob --release <tag>`
Only murmur-nexus/default-artifacts may publish an index for murmur:
error[E-REG-009]: could not resolve murmur-nexus/tool-x@1.0.0: github:murmur-nexus/other publishes an index for scope 'murmur', which only murmur-nexus/default-artifacts serves; this source serves scope 'murmur-nexus'
E-REG-010 — a scope's owner has changed
A scope is tied to the numeric id of the GitHub account that owns it — see Owner identity. An install whose source answers with another id than the one recorded for the scope is refused before anything is downloaded, written or pinned:
error[E-REG-010]: could not resolve ana/tool-git@1.0.0: the owner of scope 'ana' has changed: github:ana/artifacts is now owned by GitHub account id 2222, and murmur.lock records owner id 1111 (entry ana/tool-git)
hint: the GitHub account that owned the scope was renamed and its name registered again by someone else, or the repository changed hands; if you trust the new owner, delete the 'ana' entries from murmur.lock and install again
| Recorded in | Message |
|---|---|
murmur.lock |
… murmur.lock records owner id <id> (entry <name>) |
The .meta.json of a payload the global store serves to a project install |
<name>@<version> in the global store was installed from GitHub account id <id>, and murmur.lock records owner id … |
mur itself, for murmur |
… scope 'murmur' is owned by GitHub account id 190196424, the owner of murmur-nexus/default-artifacts, and github:murmur-nexus/default-artifacts answered with owner id <id> |
To trust the new owner, delete the scope's entries from murmur.lock and install again. The
murmur scope's id cannot be changed: a different one means the request did not reach GitHub's own
murmur-nexus/default-artifacts, for example through MUR_GITHUB_API_BASE.
E-REG-011 — a payload ships a malformed models: table
mur install — from a file, GitHub, the global store or a manifest's dependencies — and
mur publish read the payload's packed murmur.yaml before anything is stored. A
models: table E-BLD-005 would refuse is refused here
too, and neither the store nor murmur.lock is written:
error[E-REG-011]: local/murmur-driver-anthropic@0.1.0 cannot be stored: its murmur.yaml ships a malformed models: table: murmur.yaml: field 'models.test-model.max_input_tokens' is 1050001, above this entry's context_window of 1050000; an input ceiling is at most the window
hint: the driver's own murmur.yaml is at fault; use a release of the driver whose models: table is well formed — mur build refuses the same table with E-BLD-005
A payload with no readable murmur.yaml declares no table, and is stored.
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, and none of them changes
the exit code of mur doctor.
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/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 the spawn closure could not be inspected
warning[W-REG-002]: 1 capsule in the spawn closure 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/reference/diagnostics/#w-reg-002)
mur doctor walks a capsule's capabilities.spawn.allow closure to report
what every capsule in it 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 the spawn closure:
- local/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.
W-REG-003 — an installed artifact does not fit the WIT level this mur serves
installed:
ana/stale-hook@0.3.0 global built at none (derived) does not fit level 3
⚠ ana/stale-hook@0.3.0 global exports murmur:hook/lifecycle@0.8.0 — this mur serves murmur:hook@0.9.0
mur run refuses these at launch (warning[W-REG-003], https://docs.murmur.nexus/reference/diagnostics/#w-reg-003)
An artifact is built against one version of each Murmur interface it uses, and this mur serves
the one set of versions its WIT level lists. An installed artifact that does not
fit that level does not load. The common cause is upgrading mur past an interface change while
the store still holds artifacts built for the older mur.
Reinstalling the same version fetches the same build. Install a release of the artifact built
against the version this mur serves, and pin it in murmur.yaml if the project declares it.
mur doctor prints this warning for every stale artifact in
the project and global stores that this project does not declare. A stale artifact the project
does declare fails the checklist instead, because the next mur run refuses it.
Roster errors
A roster.yaml is admitted whole or refused with one of these codes. Every message
names roster.yaml. Every code except E-ROS-001 names the member at fault, apart from an
E-ROS-002 for a roster with no entry member, where no single member is at fault. The order
admission checks them in is in Admission order.
E-ROS-001 — the roster is missing or malformed
The message names the key path, such as members[1].version, and the member's name when the fault
is inside a member entry:
error[E-ROS-001]: roster.yaml: members[1].version (member 'coder'): 'latest' is a reserved alias, not an exact version; pin the version the member runs
| Fault | Fix |
|---|---|
| YAML syntax error | Fix the YAML at the line and column shown |
unknown key |
Remove or correct the key. A misspelt reachability is refused rather than read as no reachability |
is required, or a wrong type |
Write the field with the type in Fields |
roster_version other than 1 |
Write roster_version: 1 |
members empty |
List at least one member |
A name that does not match ^[a-z][a-z0-9-]{0,31}$ |
Use lowercase letters, digits and -, starting with a letter, at most 32 characters |
A capsule that is a bare name, such as crew-lead |
Name the capsule by its full name, as mur list shows it: local/crew-lead for one installed from its .mur.zip file |
A capsule that is not a valid artifact name |
Name the capsule as mur list shows it |
A version that is blank, latest, stable or edge |
Write the exact installed version |
A reachability string other than all |
Write all or a list of rules |
A rule with an empty to, or one listing its own from |
List at least one other member |
E-ROS-002 — no entry member, or several
Exactly one member has entry: true. With none, the message says so; with several, it names
every member that claims it. Mark the member that receives the formation's task, and only that
one. See The entry member.
E-ROS-003 — two members share a name
Give each member its own name. Two members may run the same capsule under different names.
This is checked before the entry member, so a roster with a duplicate is refused with E-ROS-003
whatever its entry lines say.
E-ROS-004 — a rule names no member
The message names the rule's from and the name that matches no member, which is from itself
when the rule's from is unknown. Add the member to members, or correct the name in the rule.
E-ROS-005 — a rule calls a member that does not serve peers
Every member in a rule's to must declare
exports.peer_tasks.accept: true in its own murmur.yaml.
Declare it and rebuild and reinstall the capsule, or remove the member from every rule's to.
reachability: all never pairs a member that does not serve peers, so it never raises this code.
E-ROS-006 — a public door in a roster with peer traffic
When the roster has at least one edge, every member declares
network.authentication, including members on no
edge. Declare it in the member's murmur.yaml and rebuild and reinstall the capsule, or remove the
roster's reachability. A capsule that declares network.authentication cannot also declare
capabilities.spawn.allow (E-MAN-003). See Authentication.
E-ROS-007 — a member cannot be resolved
The message names the member and its capsule@version. The capsule is in neither the project
store nor the global store at that version, its bytes do not match the hash the store recorded for
them, or its packed murmur.yaml cannot be read or parsed. Install the capsule at that version
with mur install <capsule>@<version>, or change the member's version to one mur list shows.
E-ROS-008 — a rule calls the entry member
error[E-ROS-008]: roster.yaml: the reachability rule from 'coder' lists the entry member 'planner' in `to`, but the entry member is never called by a peer: it takes the formation's tasks from the launch or from a client at its door
hint: remove 'planner' from that rule's `to` in roster.yaml. For 'coder' to report to 'planner', write the edge the other way — `- from: planner` with `to: [coder]` — so 'planner' hands 'coder' its work with call-member and 'coder''s answer comes back into 'planner''s own task; 'coder' then needs exports.peer_tasks.accept: true in its murmur.yaml
The message names the rule's from and the entry member. The entry member is never called by a
peer: its tasks come from the launch's --task or from a client at its door, and no member is
issued a credential for it. See The entry member.
Turn the edge around: remove the entry member from the rule's to, and write a rule from: the
entry member with the caller in to. The entry member then hands the caller its work with
call-member, and the caller's answer comes back into the
entry member's own task. The caller, as a callee, then needs
exports.peer_tasks.accept: true in its murmur.yaml.
reachability: all never pairs a member with the entry member as its callee, so it never raises
this code.
Roster warnings
mur run --roster prints these on stderr after admitting the roster and
before the formation launches. mur doctor prints them in its Roster block
on stdout and counts each as a warning. Each one carries a W-ROS-NNN code and a link back to its
section on this page. None of them refuses the roster or changes an exit code.
W-ROS-001 — more members may call a member than it holds
warning[W-ROS-001]: roster.yaml lets 10 members call 'reviewer' (w01, w02, w03, w04, w05, w06, w07, w08, w09, w10), but it holds 2 tasks at once — one running and lifecycle.queue_depth: 1 waiting — so a call that arrives while it is full waits and is offered again, and is rejected only if it stays full until the call's deadline; lifecycle.queue_depth: 9 would hold them all (https://docs.murmur.nexus/reference/diagnostics/#w-ros-001)
A member holds a fixed number of tasks at once, set by its own
lifecycle. A call that arrives while it holds that many waits:
the calling member's call-member offers the task again
with backoff until the member takes it, and ends the call rejected only if the member stays full
until the call's deadline. A member the table below gives 0 tasks never takes one, so every call
to it ends rejected at its deadline. See A busy member.
lifecycle.task_acceptance |
Tasks held at once |
|---|---|
queue |
queue_depth + 1: one running and queue_depth waiting |
single |
1 |
none |
0 |
Callers are counted from the roster's expanded reachability, all
included, one per calling member: a member holds at most one unanswered call to a given member.
The warning names the member, how many members may call it and which, what it holds, and the
queue_depth that holds every caller.
Fix it with one of:
- raise
lifecycle.queue_depthin the called member'smurmur.yamlto the value the warning names; - under
singleornone, settask_acceptance: queuewith thatqueue_depth; - narrow the roster's rules so fewer members may call it.
A member's own background work — a demoted shell command's completion, a delegation's outcome — takes a queue slot too, and is not counted here.
Nothing is refused, because the calls may never overlap: a roster whose callers take turns runs without a rejection.
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 and was cut off; the task goes on with another turn
(https://docs.murmur.nexus/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 inference.max_tokens, the
driver's models: entry for the model, or a controller's change.
The warning prints once per capped turn, worded by what the turn does to the task:
| Outcome | Warning body |
|---|---|
| The task goes on | this turn stopped at the inference.max_tokens output cap of <N> tokens and was cut off; the task goes on with another turn |
The task's last allowed turn under inference.max_turns |
this turn stopped at the inference.max_tokens output cap of <N> tokens, so out/result.txt holds a fragment rather than a finished answer |
| The second turn in a row with no text and no tool call | this turn stopped at the inference.max_tokens output cap of <N> tokens with no text and no tool call, the second in a row, so the task ends failed |
What each outcome leaves behind:
| Surface | The task goes on | Last allowed turn | Second empty turn in a row |
|---|---|---|---|
out/result.txt in the session's records directory, and out/result_<task-id>.txt under mode: threaded |
Untouched by this turn; the turn that ends the task writes it | The model's partial text, then a marker naming inference.max_tokens and the cap |
error: and the failure reason |
The task's response artifact that GetTask reports, and the task output an on-task-end hook reads |
— | The same marked text | The failure |
| The conversation record | The turn's thinking and text, with "truncated": true, then the runtime's user message stating the cut |
The turn's thinking and text, with "truncated": true |
The turn's thinking and text, with "truncated": true, when it had any |
The trace's inference event |
"decision": "capped", "stop_reason": "max_tokens" |
The same | The same |
The trace's task_end |
capped_turns counts the turn |
The same; exit_status "ok" |
The same; exit_status "failed", after a task_failed with cause output_cap |
A tool call in a capped turn is never run, in any outcome: its arguments may be cut. The user message names it, and the record keeps only the turn's thinking and text. A turn cut at the output cap gives the message's exact text.
inference.max_turns: 1 makes every capped turn the last: the task ends ok with the fragment.
The output_cap failure's reason names both turns and the cap:
two turns in a row (turn 0 and turn 1) stopped at the inference.max_tokens output cap of 256 tokens with no text and no tool call
Raising inference.max_tokens gives a turn room to finish. A model that spends the whole cap
reasoning, with no text and no tool call, is the case the output_cap failure stops: lower its
reasoning effort, or raise the cap.
W-RUN-002 — an untested harness version
[capsule-runtime] warning[W-RUN-002]: the harness reports version 'my-cli 0.9.0', which this process driver was not tested against (tested against 1.0.0)
(https://docs.murmur.nexus/reference/diagnostics/#w-run-002)
Before each run, mur run runs the harness binary with the version arguments the
process driver's describe() names, and compares what it printed
against the versions that driver release was tested against. A harness whose flags or output format
have moved since may be driven wrongly in ways that only show up mid-run.
The version could also not be read at all, in which case the warning says why:
| Reason | Message ends |
|---|---|
| The binary exited non-zero | it exited non-zero |
| It did not answer within 10 seconds | it did not answer within 10s |
| It printed nothing | it printed nothing |
| It could not be run | it could not be run: <error> |
Nothing is refused: the run proceeds on the driver as written. The trace carries the same text as a
harness_warning event, and harness_start.version_tested records the verdict.
W-RUN-003 — an unavailable driver choice
[capsule-runtime] warning[W-RUN-003]: driver choice 'gpt' (driver murmur/driver-openai, model gpt-5) is unavailable: its credential OPENAI_API_KEY was found in neither the global config's credentials: nor the environment, so a switch to it will be refused; the session runs on the primary
(https://docs.murmur.nexus/reference/diagnostics/#w-run-003)
The gateway of an inference.alternates driver is keyed by a
${NAME} that neither credentials.<NAME> nor the launching environment
holds. The primary's credential is what the launch needs, so the launch goes on:
| Where | What it shows |
|---|---|
session_start.inference_choices |
The choice with available: false and credential_source: "none" |
mur control show |
The choice marked unavailable |
| A switch to it | Refused 409 credential_unresolvable — see Control surface |
Set the credential and restart the capsule to make the choice available. A credential supplied at
run time goes in control.secrets instead of the environment.
W-RUN-004 — a malformed input_schema
[capsule-runtime] warning[W-RUN-004]: the tool 'my-tool' declares an input_schema whose `required` is not an array of strings, so its calls are dispatched without the required-field check (https://docs.murmur.nexus/reference/diagnostics/#w-run-004)
A tool's input_schema cannot be read where the runtime checks a
call's required fields. The tool stays callable and its calls run unchecked; the warning prints the
first time the tool is called in a session, and not again.
| The message says the schema's | Means |
|---|---|
root is not a JSON object |
The schema is an array, a string, a number or null rather than {...} |
`required` is not an array of strings |
required is a string, an object or null, or one of its entries is not a string |
Fix the schema in the tool's own murmur.yaml: required is a JSON array of property names. A
field the tool supplies a default for belongs out of required — a listed field is refused when it
is missing.
W-RUN-007 — a formation member with no lifeline
warning[W-RUN-007]: this session is a member of formation frm_0192a5b3c4d57e6f8a9b0c1d2e3f4a5b but was handed no lifeline, so it will not wind down when that formation ends; end it with `mur stop` (https://docs.murmur.nexus/reference/diagnostics/#w-run-007)
A mur run launched with MURMUR_FORMATION_ID set and no MURMUR_FORMATION_LIFELINE, other than
a delegated child, is a formation member started by hand. It runs as any other mur run does, for
as long as its lifecycle keeps it, and appears in mur ps. Nothing ends it when
its formation ends; end it with mur stop. The warning prints once, at launch.
Nothing is refused. See How a formation ends.
W-RUN-008 — a caller that cannot reach a member's door
[capsule-runtime] warning[W-RUN-008]: roster.yaml lets 'lead' call 'worker', but its capabilities.network.allow reaches no loopback http door at an unpinned port, so every call-member call will be refused; declare "localhost" (https://docs.murmur.nexus/reference/diagnostics/#w-run-008)
A member that roster.yaml lets call another has the
call-member tool, but a roster rule grants a name and a
credential, never network access. Each call is checked against the member's own
capabilities.network.allow at the called member's real door,
which a formation serves on loopback http at a port chosen at launch. Only an entry that matches
localhost over http at every port reaches it:
| Entry | Reaches a member's door |
|---|---|
localhost |
Yes |
http://localhost |
No: a URL entry with no port means the scheme's default port, 80 |
localhost:8080, http://localhost:8080 |
No: the door's port is chosen at launch |
127.0.0.1 |
No: a door's address names localhost |
The warning prints once, at staging, to stderr and the member's logs/bootstrap.log. Nothing is
refused: the member launches and may still be called, and each call-member call it makes fails
in the same turn, naming capabilities.network.allow. Declare localhost in the member's
capabilities.network.allow. See Giving a member work.
W-RUN-009 — a driver lists no limits for the model
[capsule-runtime] warning[W-RUN-009]: driver murmur/driver-openai@1.0.0 declares no limits for model 'gpt-5.6-luna' (driver choice 'primary'), so the session uses inference.max_tokens (16384) as its output cap and knows no context window: compaction does not run and no seed budget applies; set context.max_tokens to give it one (https://docs.murmur.nexus/reference/diagnostics/#w-run-009)
The choice's driver has no models: entry for its model, the
manifest sets inference.max_tokens, and it sets no context.max_tokens. The launch goes ahead on
the manifest's output cap, and for that choice:
| Without a context window | Effect |
|---|---|
| Compaction | Does not run |
An on-task-start seed, when the choice is the primary |
Refused with reason: "no_budget" |
The warning prints once per such choice, at staging, to stderr and logs/bootstrap.log, and
mur doctor prints the same text. A manifest that sets both
inference.max_tokens and context.max_tokens needs nothing from the table and draws no warning.
Set context.max_tokens to the model's context window, or install a release of the driver that
lists the model.
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/reference/diagnostics/#w-sec-001)
Where a warning is written depends on whether the session's records directory exists yet:
| Warning | Written to |
|---|---|
W-SEC-001, W-SEC-002, W-SEC-003, W-SEC-005, W-SEC-010, W-SEC-021, W-SEC-023 — decided at launch |
stderr and the session's 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-027, W-SEC-028, W-SEC-029, W-SEC-030 — decided at staging, before the records directory exists |
stderr |
W-SEC-004 — from mur build |
stderr |
W-SEC-031 — decided mid-session, when the harness reports its session |
stderr |
W-SEC-032 — decided at staging by mur run, by mur doctor, and by mur deploy run once the port is published |
stderr from mur run and mur deploy run; stdout from mur doctor |
W-SEC-033 — decided at staging by mur run, and by mur doctor |
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
(outbound HTTP from tool and 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: a shell step that relies on root's usual "can read
anything" posture fails under it.
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.memory_bytes, limits.table_elements, limits.instances, resources or
containment. Per-hook grants (see artifacts[].capabilities)
read only network, filesystem, state, task_io, conversation and
limits.deadline_seconds — the others are structurally accepted but nothing enforces them
per-hook. Each is named by its full key:
[capsule-runtime] warning[W-SEC-006]: hook 'compactor' declares capabilities.limits.memory_bytes which the runtime does not apply per-hook — only capabilities.network, capabilities.filesystem, capabilities.state, capabilities.task_io, capabilities.conversation and capabilities.limits.deadline_seconds govern a hook (https://docs.murmur.nexus/reference/diagnostics/#w-sec-006)
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, the memory,
table and instance 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 declares anycapabilities:sub-block other thanstate. A native tool runs under the capsule-widecapabilities.filesystem,capabilities.network.allow,capabilities.network.unix_socketsandcapabilities.shell.allow, enforced by the same sandbox as a shell command; a per-artifact block does not narrow them.capabilities.stateis the one sub-block that applies to a native tool (A native tool's own config and store), so a block that holds onlystateprints nothing.
[capsule-runtime] warning[W-SEC-008]: artifact 'local/tool-fixture' declares capabilities.network but ships a native implementation — capabilities.state is the one sub-block that applies to a native tool, and the rest run under the capsule-wide capabilities, enforced by the same sandbox as shell commands, which this block does not narrow (https://docs.murmur.nexus/reference/diagnostics/#w-sec-008)
Why it matters: a declared-but-unapplied grant reads like a scoped artifact. In the native case the tool keeps the whole capsule ceiling despite an entry that looks like it narrowed it.
What to do: for an inert sub-key, remove it and declare it in the top-level capabilities:
block instead. For a native tool, keep state if it needs a store, narrow the capsule-wide block
itself, or ship the tool as WASM if it needs grants narrower than the rest of the capsule's.
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. A CPython or Perl interpreter needs no grant for its own import
path: mur derives those directories from the
interpreter itself. For any other interpreter the grant is what lets it run, 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: for an allowlisted CPython or Perl interpreter, delete the grant unless it names a
directory the interpreter does not report on its own import path, such as a virtual environment's
site-packages; mur run --explain-scope lists what is derived under derived interpreter
runtime. A grant naming a trust-store directory such as /etc/ssl is not needed when the capsule
declares capabilities.network.allow: mur grants the host's trust store
then, so delete it. Otherwise, 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 work folder. 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 per launch, on stderr:
| Command | Prints it |
|---|---|
mur run |
Once, at staging |
mur run --roster |
Once, from the launcher, after the roster is admitted and before any member starts |
| A formation member, or a sub-capsule a capsule delegates to or a plan step starts | Never: the runtime that starts it sets MURMUR_HOST_WARNINGS_REPORTED=1 in its environment |
mur doctor |
Always |
A mur run started with MURMUR_HOST_WARNINGS_REPORTED=1 does not print it. Only the value 1
silences it, and only on stderr: session_start still records the grant.
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 directory.
The capsule's own component 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/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 — retired
Nothing raises this code and nothing reuses it. A native tool receives its entry's config: block
as MURMUR_ARTIFACT_CONFIG, as a WASM tool does — see
A native tool's own config and store.
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 component 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/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: local/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/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/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.
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/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/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 release the
manifest was written for — and upgrade rather than edit the manifest.
W-SEC-020 — retired
Nothing raises this code and nothing reuses it. A delegation's outcome is delivered into the task
that made it on every lifecycle, and that task ends any sub-capsule still running when it ends —
see How the outcome arrives.
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 and pids.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/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 and pids.max stay enforced on
the scope and stay fatal on failure. The message names the limits in force: it reads
memory.max, pids.max and cpu.max when
capabilities.resources.cgroup_cpu_percent sets a CPU cap, and
memory.max and pids.max when it does not. 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 — retired
Nothing raises this code and nothing reuses it. A shell command that moves to the background
returns a work id on every lifecycle, and the agent reads the command's state, waits for it or
stops it by that id with background-command.
W-SEC-023 — the session is not addressable
Fires when: a session's running-capsule record could not be
written: an agent session's once its A2A door is open, a
script capsule's once its session starts. 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 — and `mur token` cannot read the tokens of an authenticated door: failed to create the directory: Permission denied (os error 13) (https://docs.murmur.nexus/reference/diagnostics/#w-sec-023)
A script capsule's warning says what it loses instead:
[capsule-runtime] warning[W-SEC-023]: this session's record under ~/.murmur/running/ could not be written, so `mur ps` cannot list it and `mur-roost` cannot count it: failed to create the directory: Permission denied (os error 13) (https://docs.murmur.nexus/reference/diagnostics/#w-sec-023)
Why it matters: for a script capsule, the record is what lets mur ps list it
and a mur-roost started while it runs count it. For an agent session, 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. It is also where mur token reads a token: a capsule declaring
network.authentication whose record was not written has no token anyone can read, so its door
refuses every caller.
What the runtime does about it: nothing is refused and no exit code changes. An agent session
binds its port, serves its door and runs its tasks exactly as it would have; a script capsule runs
its run to the end.
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 component 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 component 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 WASM component, 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/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/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 a gateway's upstream
Fires when: an entry in the capsule-wide
capabilities.network.allow matches any artifact's
gateway.endpoint, or an entry in an artifact's own
capabilities.network.allow matches that artifact's gateway.endpoint. Once per matching entry,
naming the entry and the artifact, 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 gateway.endpoint host of artifact 'murmur/driver-anthropic'; the gateway does not use it — the runtime reaches that upstream itself — so the entry only grants tools, subprocesses and artifacts direct reach to that host without the key (https://docs.murmur.nexus/reference/diagnostics/#w-sec-025)
For an entry on an artifact's own capabilities:, the warning begins
artifact '<name>' capabilities.network.allow entry.
The runtime reaches the upstream for the artifact and attaches the key there, so the gateway works with or without the entry. The entry still grants direct access to that host, without the key. Remove it unless something other than the gateway needs that host.
W-SEC-026 — retired
Nothing raises this code and nothing reuses it. spend.machine_tokens_per_day
counts a transport: process capsule's turns from the token
counts the harness reports and its driver relays. Two codes cover what happens around that:
| Code | Covers |
|---|---|
E-RUN-038 |
A spend ceiling set against a driver that reports no usage |
W-SEC-031 |
A harness billing against something other than the subscription |
W-SEC-027 — a gateway key is read only at launch
Fires when: an artifact's gateway.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 such key, naming the artifact, on stderr, from mur run (including
mur run --explain-scope) and from mur doctor.
[capsule-runtime] warning[W-SEC-027]: artifact 'murmur/driver-anthropic' gateway.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/reference/diagnostics/#w-sec-027)
For a literal, the warning names artifact '<name>' gateway.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 keyed request. This capsule keeps the key it launched with, so revoking that key
breaks it until it restarts — for the driver's key, 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
gateway.api_key: ${NAME} on the artifact's entry. 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 capsule reads a gateway.api_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 — under formations/, down to each member's directory only |
Once per path |
[capsule-runtime] warning[W-SEC-028]: the 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/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/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/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.
W-SEC-030 — an unmetered credential gateway
Fires when: an artifact entry declares gateway: and is not the
transport: http driver inference.driver.artifact names. Once per such gateway, on stderr, from
mur run (including mur run --explain-scope) and from mur doctor. Never for the configured
driver.
[capsule-runtime] warning[W-SEC-030]: artifact 'local/tool-web-search' reaches api.tavily.com through the credential gateway, unmetered — murmur neither counts nor limits what its calls spend, and inference.max_session_tokens and spend.machine_tokens_per_day do not cover it (https://docs.murmur.nexus/reference/diagnostics/#w-sec-030)
Why it matters: only the configured driver's calls count toward the spend ceilings. This artifact's calls count toward neither, so a capsule that stops at its token ceiling can keep spending on this upstream.
What the runtime does about it: nothing is refused. session_start.gateways
records the gateway with metered: false.
What to do: bound the artifact's spend with the upstream's own controls, such as a key with its own quota.
W-SEC-031 — a harness that is not on a subscription
[capsule-runtime] warning[W-SEC-031]: the harness session reports auth 'api-key', not 'subscription' — this run's spend may be billed to an API key rather than to the subscription this transport exists for (https://docs.murmur.nexus/reference/diagnostics/#w-sec-031)
Why it matters: a transport: process capsule runs on a
harness the operator is already signed in to, and the point of that is spend on that plan. A harness
billing another way spends money against a key instead. The ceilings still count it —
inference.max_session_tokens and
spend.machine_tokens_per_day are enforced against whatever the harness
reports, however it is billing — but the bill arrives somewhere the operator did not choose.
What the runtime does about it: nothing is refused. The value is whatever the harness reported
through its process driver, compared only against subscription. The trace carries the same text as
a harness_warning event, beside the harness_session event that reported it.
What to do: sign the harness in on the plan the run should spend from, or bound the key's spend with the provider's own controls.
W-SEC-032 — a public door off loopback
Fires when: a capsule's A2A door is exposed off this host and its manifest declares no
network.authentication. Once per invocation, from:
| Command | Exposure |
|---|---|
mur run --bind <ADDR> |
ADDR is off loopback |
mur doctor --bind <ADDR> |
ADDR is off loopback |
mur deploy run |
Always: it publishes the port at the VM's public address |
Loopback is an IPv4 address in 127.0.0.0/8, ::1 or localhost; 0.0.0.0, :: and every other
address or hostname are off it. A capsule with no inference: block serves no door and never
warns. mur run and mur doctor print the same bytes for the same manifest and address.
warning[W-SEC-032]: the door is bound to 0.0.0.0 and network.authentication is not declared — any caller that reaches it can call SendMessage, SendStreamingMessage, murmur.WatchSession, GetTask, ListTasks, CancelTask, SubscribeToTask, murmur.StopSession, and the agent card publishes to any A2A client the session id, tools [bash], shell: true, network: true and planes [files] (https://docs.murmur.nexus/reference/diagnostics/#w-sec-032)
Why it matters: a public door answers every caller that reaches the port. Anyone on the network
can start tasks, read and watch every task, and cancel every task at once with
murmur.StopSession, and the agent card tells them
the session id, the tools, and whether the capsule holds shell and network grants. The methods and
the disclosure in the line are read from the manifest.
What the runtime does about it: nothing is refused. The capsule binds and serves as it would on loopback.
What to do: declare network.authentication, hand
callers the tokens mur run prints, and put a TLS terminator in front of the
door, which speaks plain HTTP. Or bind to loopback.
W-SEC-033 — the runtime or records directory lies inside the workdir
Fires when: ~/.murmur/sessions, where every session's
runtime directory is created, or the session's
records directory, lies inside the work folder:
| From | When |
|---|---|
mur run |
Once per launch, when the declared containment is advisory |
mur doctor |
For <manifest-dir>/workdir, whatever the declared containment |
[capsule-runtime] warning[W-SEC-033]: the runtime directory /home/dev/.murmur/sessions and the records directory /home/dev/.murmur/records/home/dev/<session-id> lie inside the workdir /home/dev, so this capsule's commands can change the files the runtime owns — task.md, MURMUR.md, plans and installed tools — and the records it keeps about the session — its trace, logs, eval scores and delegation outcome; run with a --workdir that does not contain ~/.murmur (https://docs.murmur.nexus/reference/diagnostics/#w-sec-033)
Why it matters: the capsule's commands can rewrite the task, MURMUR.md, a submitted plan
before it runs, an installed native tool before its next call, or the trace and eval scores every
later reader trusts.
What the runtime does about it: the launch runs, records achieved_containment: advisory, and
reports read_only_to_commands: false for the directory that lies inside. A scoped or sealed floor is refused with
E-CAP-003 instead.
What to do: run with a --workdir that does not contain ~/.murmur.