murmur-artifact 0.11.0

The shared artifact model for Murmur capsules: manifest schema, .mur.zip packing and unpacking, the registry trait, refs, lockfile, and inference config.
Documentation
//! 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`.
pub fn security_warning_link(code: &str) -> String {
    crate::diagnostic_link(code)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Every code is distinct and spelled in the `W-SEC-NNN` shape the diagnostics page anchors
    /// on. A duplicated or misspelled constant would silently point two warnings at one anchor.
    #[test]
    fn every_code_is_unique_and_well_formed() {
        let codes = [
            W_SEC_001, W_SEC_002, W_SEC_003, W_SEC_004, W_SEC_005, W_SEC_006, W_SEC_007, W_SEC_008,
            W_SEC_009, W_SEC_010, W_SEC_011, W_SEC_012, W_SEC_013, W_SEC_014, W_SEC_016, W_SEC_017,
            W_SEC_018, W_SEC_019, W_SEC_021, W_SEC_023, W_SEC_024, W_SEC_025, W_SEC_027, W_SEC_028,
            W_SEC_029, W_SEC_030, W_SEC_031, W_SEC_032, W_SEC_033, W_SEC_034, W_SEC_035, W_SEC_036,
        ];
        for code in codes {
            assert!(code.starts_with("W-SEC-"), "malformed code: {code}");
            assert_eq!(code.len(), 9, "malformed code: {code}");
        }
        let mut unique = codes.to_vec();
        unique.sort_unstable();
        unique.dedup();
        assert_eq!(unique.len(), codes.len(), "two W-SEC codes are equal");
    }

    #[test]
    fn link_lowercases_the_code_into_the_anchor() {
        assert_eq!(
            security_warning_link(W_SEC_001),
            "https://docs.murmur.nexus/reference/diagnostics/#w-sec-001"
        );
    }
}