1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
//! Registry of `W-SEC-*` codes for non-fatal, security-relevant runtime warnings.
//!
//! Mirrors the `E-<CATEGORY>-NNN` convention `murmur-cli`'s `CliError` uses for fatal errors
//! (see `murmur_cli::error`), but for warnings: printed and the session continues. Each code
//! has a matching `#w-sec-nnn` anchor on the diagnostics reference page, so callers can
//! append [`security_warning_link`] to the message instead of re-explaining the issue inline.
/// `capabilities.shell.allow` is non-empty but the host has no kernel-level subprocess
/// sandboxing primitive (Landlock/seccomp are Linux-only) — enforcement is environment-only.
pub const W_SEC_001: &str = "W-SEC-001";
/// `capabilities.shell.allow` is non-empty on a Linux host without Landlock (kernel <5.13) —
/// exec/network are seccomp-enforced, `socket(AF_UNIX)` is refused unless
/// `capabilities.network.unix_sockets` is declared (and `AF_NETLINK`/`AF_PACKET` always are, with
/// no opt-in), and the shell child still drops every Linux capability before `execve` — but
/// filesystem scope is not enforced at all. The socket-domain rule is pure seccomp, so it is
/// identical on this tier and on `W_SEC_005`'s; the fixed capsule device set is the opposite —
/// pure Landlock, so on this tier it does not apply and every device under `/dev` is reachable.
pub const W_SEC_002: &str = "W-SEC-002";
/// `capabilities.shell.allow` includes `"bash"` and `capabilities.network.allow` is non-empty,
/// but nothing on this host constrains bash's own outbound network access.
pub const W_SEC_003: &str = "W-SEC-003";
/// A manifest field that looks credential-shaped (`api_key`, `token`, `secret`, `password`)
/// holds a literal value instead of a `${VAR_NAME}` reference.
pub const W_SEC_004: &str = "W-SEC-004";
/// The host resolved to a Linux kernel-enforcement tier (Landlock/seccomp). Landlock now grants a
/// narrow, derived read+execute scope outside the workdir (the allowlisted binaries, their loader,
/// and their shared libraries — nothing writable), so allowlisted programs can actually run; on top
/// of that it grants a fixed, non-manifest-derived device set — `/dev/null` read+write (the sole
/// writable path outside the workdir), `/dev/zero` and `/dev/urandom` read-only, every other device
/// including `/dev/random` denied; the
/// workdir's own grant withholds character-device, block-device and unix-socket creation, so a
/// capsule cannot `mknod` a raw disk node inside it; a classic seccomp rule on `socket(2)`'s
/// `domain` refuses `AF_UNIX` unless `capabilities.network.unix_sockets` is declared and refuses
/// `AF_NETLINK`/`AF_PACKET` unconditionally, so a capsule cannot reach a host daemon socket such as
/// `/var/run/docker.sock`; and the forked shell child drops every Linux
/// capability and sets `no_new_privs` before `execve`. On both tiers it also grants read-only
/// `/etc/hosts` and `/etc/nsswitch.conf`, and commands run in the session's own PID namespace with
/// a private, read-and-list `/proc` that lists only the session's processes and the kernel's
/// world-readable files. Fires on both Linux kernel tiers, the sealed one with its own notice:
/// there `/proc` is a bind of that private procfs. Where the host cannot mount a private procfs,
/// `/proc` is the host's on both tiers and Landlock refuses every open beneath it; what still
/// reaches outside the capsule then is `stat(2)` on `/proc/<pid>` (existence, owner, start time).
pub const W_SEC_005: &str = "W-SEC-005";
/// A `runtime: hook` artifact entry declares `capabilities.shell`/`.spawn`/`.env`/`.limits`,
/// but a per-hook grant only reads `network`, `filesystem` and `task_io` — the other
/// sub-blocks are structurally accepted and silently inert.
pub const W_SEC_006: &str = "W-SEC-006";
/// A `runtime: tool`/`runtime: driver` artifact entry declares a `capabilities.network.allow`
/// entry the capsule-wide ceiling does not itself allow. Narrowing can only subtract, so the
/// entry is dropped from that artifact's effective grant rather than granted.
pub const W_SEC_007: &str = "W-SEC-007";
/// A `runtime: tool`/`runtime: driver` artifact entry declares `capabilities.shell`/`.spawn`/
/// `.env`/`.limits`, but per-artifact narrowing only reads `network` and `filesystem` — the
/// other sub-blocks are structurally accepted and silently inert.
pub const W_SEC_008: &str = "W-SEC-008";
/// `capabilities.shell.interpreter_runtime` grants an allowlisted binary specific host
/// directories outside the workdir. This couples the capsule to a specific host
/// distro/interpreter-version layout (e.g. `/usr/lib/python3.11` breaks the moment the host
/// ships Python 3.12); the durable fix is the still-unbuilt staged runtime bind-mount, which
/// this grant only bridges until. Fires once per declared grant, at staging.
pub const W_SEC_009: &str = "W-SEC-009";
/// The capsule can spawn subprocesses on a host where no cgroup v2 scope can exist
/// (macOS, or any non-Linux target). Per-process `rlimit(2)` ceilings still apply, but every
/// *aggregate* bound does not: nothing caps memory, pid count or CPU across the subprocess tree
/// as a whole. `RLIMIT_NPROC` is not a substitute — it is per-**uid**, so a fork bomb of
/// distinct, short-lived processes evades it. Permanent on this platform, like [`W_SEC_001`].
pub const W_SEC_010: &str = "W-SEC-010";
/// `capabilities.filesystem.workdir_exec: true` keeps the Landlock `Execute` right on the session
/// workdir, so anything the capsule writes there can run regardless of `capabilities.shell.allow`.
/// The allowlist is still applied to what the *agent asks* to run, but it stops being an
/// enforceable property of the capsule: a binary compiled, downloaded or renamed inside the workdir
/// executes on its own. Because of that the capsule can never achieve
/// `ContainmentClass::Scoped` — it reports `advisory` on every host, including a Landlock-capable
/// one — and pairing it with `capabilities.containment: scoped` refuses the launch with `E-CAP-003`.
/// Fires once, at staging, only for a capsule that declared it.
pub const W_SEC_011: &str = "W-SEC-011";
/// A `sealed` capsule's `capabilities.shell.allow` names a known compiler driver (`cc`, `gcc`,
/// `g++`, `c++`) whose helper subprocess — `cc1`, `cc1plus`, `as`, `ld`, `collect2` — has no grant
/// carrying the Landlock `Execute` right. The driver forks and execs those helpers itself; they
/// are separate binaries outside its own `DT_NEEDED` closure, so nothing derives them, and they
/// live under the fixed sealed runtime tree (`/usr`, `/bin`, …), which is bind-mounted read-only
/// and granted `list_dir: true, executable: false` — present and readable, but not runnable. The
/// driver therefore starts, answers `--version`, and then fails partway through the first real
/// compile. The fix is an `interpreter_runtime` or `staged_runtime` grant naming the helper's
/// containing directory, both of which carry `Execute`. A warning rather than a refusal because
/// the `<driver> -print-prog-name=<helper>` probe behind it is a heuristic about one driver
/// family: a hard refusal built on it could block a capsule that would in fact have worked. Fires
/// once per uncovered helper, at staging, only under a declared `sealed` floor. A driver that could
/// not be asked at all is `W_SEC_029`, not silence.
pub const W_SEC_012: &str = "W-SEC-012";
/// AppArmor is enabled on this host and `kernel.apparmor_restrict_unprivileged_userns` is off, so
/// unprivileged user namespaces are unrestricted for *every* binary on the machine rather than for
/// `mur` alone. That is what makes `capabilities.containment: sealed` and the capsule network
/// namespace work here, and it is not the configuration murmur ships: the shipped `mur-sealed`
/// AppArmor profile grants the same permission to one binary. A legitimate operator choice and on
/// some hosts the only one, so it never refuses a run and never changes an exit code — but a
/// `sealed` result obtained this way and one obtained through the profile must not read the same.
/// Fires once, from the host probe, at staging and from `mur doctor`.
pub const W_SEC_013: &str = "W-SEC-013";
/// A `capabilities.state` block was declared somewhere nothing reads it, so no durable store was
/// created and no `state/` preopen exists.
///
/// The grant is applied per *artifact*, on the tool, driver or hook entry that will actually run
/// with the second preopen. A capsule-wide, top-level `capabilities.state` reaches no such entry:
/// the capsule's own guest is built with no artifact grant at all. Structurally valid and
/// therefore warned rather than refused, on the same terms as `W-SEC-006`/`W-SEC-008` — but stated
/// plainly, because an operator who believes their capsule has durable state and finds an empty
/// directory has no other signal that the declaration went nowhere.
/// Fires at staging, before any session workdir exists.
pub const W_SEC_014: &str = "W-SEC-014";
// W-SEC-015 is retired and its code is not reused. The diagnostics page keeps the `#w-sec-015`
// anchor.
/// A `capabilities.conversation` block was declared in the capsule-wide `capabilities:` block,
/// where nothing reads it, so no artifact was granted `murmur:conversation/read`.
///
/// The grant is applied per *artifact*, on the `runtime: hook` entry whose component imports the
/// interface. The capsule's own guest holds no artifact grant and compiles against a world that
/// has no such import, so a top-level declaration reaches nothing. Structurally valid and
/// therefore warned rather than refused, on the same terms as `W-SEC-014`.
/// Fires at staging, before any session workdir exists.
pub const W_SEC_016: &str = "W-SEC-016";
/// `capabilities.filesystem.read_only` is declared alongside an allowlisted interpreter, so the
/// declaration is advisory for that binary.
///
/// The dispatch-time write-intent analyser reads a shell call's argv and `-c` script text. An
/// interpreter's own file I/O is not in either: `python3 -c "open(p,'w').write(x)"` is one opaque
/// argument, and nothing in it names a redirection or a write verb the analyser recognizes. The
/// declaration still holds for every call the analyser *can* read, and it still holds against the
/// tool path — this names the one route around the shell half rather than leaving it implied.
/// Fires once per allowlisted interpreter, at staging, only for a capsule that declared
/// `read_only`.
pub const W_SEC_017: &str = "W-SEC-017";
/// `capabilities.filesystem.read_only` is declared and an installed tool's `input_schema` names a
/// path-shaped or destination-shaped property without saying which of its inputs are filesystem
/// destinations, so its calls are judged by key name.
///
/// The dispatch-time write-intent analyser guesses from property names when a tool tells it
/// nothing: an input pairing a path-shaped key with a content-shaped key is refused, wherever in
/// the input that pair sits. The guess is wrong in both directions — a note a tool merely stores
/// is refused as a write, and a destination under an unrecognized name is never checked. A tool
/// silences this by annotating its schema: `"format": "murmur-destination"` on a destination
/// string, `"format": "murmur-opaque"` on an object or array it only stores. Fires once per
/// installed tool, at staging, only for a capsule that declared `read_only`.
pub const W_SEC_018: &str = "W-SEC-018";
/// A key in `murmur.yaml` that this build does not recognize, captured during parse instead of
/// dropped.
///
/// Every `Raw*` deserialization struct carries a `#[serde(flatten)]` overflow map, so a key no
/// field claims lands there rather than vanishing. Fires once per captured key, naming the key,
/// the dotted path of the block that held it, and — when one is near enough by edit distance —
/// the recognized key of that block it most resembles. A near match is worded as a spelling
/// problem; no near match is worded as a key this build does not know, which a newer `mur` may.
///
/// Never a refusal: `#[serde(deny_unknown_fields)]` is deliberately set nowhere in the manifest
/// parser, because refusing an unknown key makes a manifest written for a newer `mur` unloadable
/// by an older one. Fires from `mur run` and from `mur doctor`, in the same words.
pub const W_SEC_019: &str = "W-SEC-019";
// W-SEC-020 is retired and its code is not reused. 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. The diagnostics page keeps the `#w-sec-020` anchor.
/// The capsule was given a cgroup scope and the declared
/// `capabilities.resources.cgroup_io_bytes_per_sec` ceiling did not apply to it, so the capsule's
/// subprocess tree runs with no I/O bandwidth bound.
///
/// `io.max` is the one cgroup limit this runtime treats as non-fatal: the backing device of a path
/// cannot always be resolved to a `MAJ:MIN` the block layer accepts (tmpfs, overlayfs, btrfs
/// subvolumes and device-mapper stacks all break the assumption), and a capsule that saturates
/// disk bandwidth is slow where one that exhausts memory or pids is fatal. `memory.max` and
/// `pids.max` stay enforced on the scope and stay fatal on failure, and so does `cpu.max` when
/// `capabilities.resources.cgroup_cpu_percent` sets a cap. Fires once per
/// launch, and carries the same reason `--explain-scope`'s and `session_start`'s `io_max.reason`
/// carry.
pub const W_SEC_021: &str = "W-SEC-021";
// W-SEC-022 is retired and its code is not reused. A demoted shell command's result reaches the
// agent on every lifecycle, through the work id its shell call returns and the
// `background-command` tool. The diagnostics page keeps the `#w-sec-022` anchor.
/// A session opened its A2A door and its record under `~/.murmur/running/` could not be written,
/// so nothing on the machine can look the session's address up.
///
/// The record is what makes `mur watch` and `mur cancel` reachable by session address rather than
/// by the URL one terminal printed once. A home directory that cannot be resolved or written
/// leaves the session running and serving exactly as it would have, addressable only by that URL.
/// Never a refusal: a capsule that serves is worth more than a capsule that refused to start
/// because a convenience could not be recorded. Fires once per launch, only for a session that
/// binds a door.
pub const W_SEC_023: &str = "W-SEC-023";
/// A `capabilities.env.allow` entry is credential-shaped by name (`api_key`, `token`, `secret`,
/// `password`), so the grant hands a capsule a host secret murmur does not broker.
///
/// Two arms, chosen by asking the credential backstop whether it would drop the name. A name the
/// backstop keeps reaches every WASM guest the capsule runs, and murmur has no way to withdraw it
/// once granted; a name the backstop drops delivers nothing, so the entry is inert and the capsule
/// never sees the value. A kept name on a capsule whose resolved `lifecycle.after_task` is `sleep`
/// gets a further sentence: the capsule outlives the task that launched it while still holding the
/// value.
///
/// Never a refusal, and `sleep` does not escalate it into one: `capabilities.env.allow` exists
/// precisely because capsules need secrets murmur knows nothing about, and a long-lived worker
/// holding an operator-granted database password is an ordinary shape. What the warning removes is
/// the silence — without it nothing tells an operator which of their capsules holds what. Fires
/// once per distinct credential-shaped entry, in declaration order.
pub const W_SEC_024: &str = "W-SEC-024";
/// A `capabilities.network.allow` entry — capsule-wide, or on an artifact's own entry — names the
/// host of an artifact's `gateway.endpoint`.
///
/// The runtime reaches that upstream itself, through the artifact's credential gateway, and
/// attaches the key there; a gateway-addressed request never consults the allow-list. The entry
/// still grants tools, subprocesses and artifacts direct reach to that host, without the key, so
/// the warning says what it grants. Never a refusal.
/// Fires once per matching entry, before any session workdir exists.
pub const W_SEC_025: &str = "W-SEC-025";
// W-SEC-026 is retired and its code is not reused. The machine ceiling covers a
// `transport: process` capsule, and `E-RUN-038` refuses one set against a driver that reports no
// usage. The diagnostics page keeps the `#w-sec-026` anchor.
/// An artifact's `gateway.api_key` can only be read at launch: it is a literal in the manifest, or
/// a `${NAME}` the global config's `credentials:` map does not hold and the launching shell's
/// environment supplies.
///
/// A rotated key reaches every capsule whose key comes from the config on its next request; this
/// one keeps the key it launched with until it is restarted. Accepted rather than refused, because
/// exporting the key is how CI runs are keyed. Fires once per launch, before any session workdir
/// exists, and never names the value.
pub const W_SEC_027: &str = "W-SEC-027";
/// A file under `~/.murmur` that holds a secret, or the config file a capsule is resolving a
/// `gateway.api_key` from, has a mode that lets other accounts on the host read it.
///
/// Never a refusal. Fires once at staging when a `credentials.<NAME>` is read from a config file
/// granting any group or other bit, and from `mur doctor` once for every owner-only entry, or
/// path beneath one, that is wider than expected. Names the path, its mode and the `chmod` that
/// fixes it, never a value.
pub const W_SEC_028: &str = "W-SEC-028";
/// Under a declared `sealed` floor, a `capabilities.shell.allow` entry names a known compiler
/// driver (the `W_SEC_012` table), but its `<driver> -print-prog-name=<helper>` probe could not be
/// run: the spawn failed with an OS error (`EACCES`, `ETXTBSY`, `EAGAIN`, `ENOMEM`, …) or a signal
/// terminated the driver before it answered. `W_SEC_012` was therefore not evaluated for that
/// driver's remaining helpers, which may have no `Execute` grant under the composed root. Fires
/// once per driver entry, at staging and from `mur doctor`, naming the entry, the resolved driver
/// path, the unchecked helpers and the error or signal. A warning rather than a refusal because
/// `W_SEC_012`, the check it stands in for, never refuses a launch either.
pub const W_SEC_029: &str = "W-SEC-029";
/// An artifact reaches its upstream through a credential gateway that is not the inference
/// gateway, so nothing meters it.
///
/// Only the configured `transport: http` driver's gateway is admitted against the spend meter;
/// every other artifact's keyed request is sent without an admission and counts toward neither
/// `inference.max_session_tokens` nor `spend.machine_tokens_per_day`. Never a refusal; stated so an
/// operator does not read the spend ceilings as covering it. Fires once per such gateway, at launch and
/// from `mur doctor`, before any session workdir exists, and never names the key.
pub const W_SEC_030: &str = "W-SEC-030";
/// A `transport: process` harness reported that it is authenticating as something other than a
/// subscription.
///
/// The process transport exists so a capsule can run on a harness the operator is already signed
/// in to, with the spend on that plan. A harness billing against an API key instead spends money
/// murmur neither holds nor counts: `inference.max_session_tokens` and
/// `spend.machine_tokens_per_day` do not cover it. Fires once per run, from the session the
/// harness itself reports, and never names a credential.
pub const W_SEC_031: &str = "W-SEC-031";
/// The A2A door is exposed off loopback and the manifest declares no `network.authentication`, so
/// any caller that reaches the port can drive the capsule and read what its agent card publishes.
///
/// Exposed means bound to an address outside `127.0.0.0/8`, `::1` and `localhost`, or published
/// to the network by `mur deploy`. The warning names every method a stranger can call and what the
/// card discloses. Never a refusal: a public door on a private network is a legitimate choice, and
/// the warning is what makes it one. Fires once per launch from `mur run`, from
/// `mur doctor --bind`, and from `mur deploy`.
pub const W_SEC_032: &str = "W-SEC-032";
/// `~/.murmur/sessions`, where every session's runtime directory is created, lies inside the
/// workdir the capsule's commands write, so they can change `task.md`, `MURMUR.md`, plans and
/// installed tools. Fires once per launch below a `scoped` floor, which records
/// `achieved_containment: advisory`; a `scoped` or `sealed` floor refuses the launch with
/// `E-CAP-003` instead. `mur doctor` prints it for `<manifest-dir>/workdir`.
pub const W_SEC_033: &str = "W-SEC-033";
/// A capsule declares a `scoped` or `sealed` floor and runs code on a route that class does not
/// confine: the `inference.transport: process` harness and its own tools, which run as children
/// outside the class. WASM tools, drivers and hooks run in guest workers under the class and are
/// never named. One warning names every such route present, and the artifacts on it. Never a
/// refusal. Fires once per launch, from `mur run` (stderr and the bootstrap log) and from
/// `mur doctor`.
pub const W_SEC_034: &str = "W-SEC-034";
/// A `host_paths` entry in `~/.murmur/config.yaml` is, lies under, or contains a credential
/// location that exists on this host (`~/.ssh`, `~/.aws`, `~/.config/gh`, `~/.murmur`, …), so every
/// scoped or sealed capsule on the host can read it, and change it when the entry is
/// `read-write`. Never a refusal: lending such a folder is the operator's call. Fires once per entry
/// from `mur run` and from `mur doctor`, naming the entry and the location.
pub const W_SEC_035: &str = "W-SEC-035";
/// The capsule runs unconfined: its shell commands, plan shell steps, the programs its tools run
/// through `murmur:process` and its `transport: process` harness run as the operator, with their
/// `HOME`, their whole environment (credential variables included), the host filesystem and
/// network, and no `no_new_privs`, so `sudo` works wherever the operator's sudoers allows it.
/// Chosen only by the operator, with `mur run --unconfined` or `containment: unconfined` in
/// `~/.murmur/config.yaml`. Fires exactly once per launch, after staging succeeds;
/// `--explain-scope` does not print it.
pub const W_SEC_036: &str = "W-SEC-036";
/// Builds the doc link for a `W-SEC-*` code, e.g. `.../diagnostics/#w-sec-001`.