Skip to main content

capsule_runtime/
workdir_writes.rs

1//! What the runtime itself writes inside the capsule's workdir, declared rather than discovered.
2//!
3//! A consumer for whom the workdir *is* the deliverable — a benchmark harness diffing it to score
4//! a patch, a `git-worktree` workspace whose diff becomes a pull request — has to answer "what did
5//! the *capsule* change?". Without a declaration that answer is a hand-maintained exclusion list
6//! that goes stale the moment the runtime gains an entry, and silently: nothing tells the consumer
7//! the set grew. [`runtime_writes`] is that declaration, carried by
8//! [`crate::containment::ScopeReport`] and therefore printed by `mur run --explain-scope` and
9//! recorded verbatim in `trace.jsonl`'s `session_start.effective_grants`.
10//!
11//! # Path form
12//!
13//! Every [`RuntimeWriteReport::path`] is relative to the **accessible workdir** — the directory a
14//! `--workdir` run hands the capsule, and the directory a consumer diffs — and carries the
15//! **literal** segment `<session-id>` ([`SESSION_ID_PLACEHOLDER`]) wherever a real session id
16//! appears. `mur run --explain-scope` answers before a session exists, so there is no id to
17//! substitute; keeping the placeholder in both renderings is what lets a consumer capture the
18//! declaration ahead of a run and compare it byte for byte with the one the trace carries.
19//! `session_start.session_id` supplies the substitution.
20//!
21//! Without `--workdir` there is no separate deliverable: the accessible workdir *is* the session
22//! directory, under `<manifest dir>/workdir/<session id>/`, and the runtime owns all of it. The
23//! declaration describes the `--workdir` layout in both cases, so that it is one fixed answer a
24//! consumer can compare against rather than two.
25//!
26//! # What is filtered and what is labelled
27//!
28//! [`RuntimeWriteCondition`] names when a path is present. Only [`RuntimeWriteCondition::Sealed`]
29//! filters the set: the enforcement tier is host state the report has already probed, so a report
30//! that omitted the sealed rows on a host that will use them — or listed them on a host that
31//! cannot — would be describing a different launch than the one it is about. Every other condition
32//! turns on what the session does after it starts, which no report can know, so those rows are
33//! always declared and the condition says when the path appears.
34//!
35//! # Adding an entry
36//!
37//! Add a row to [`DECLARED_WRITES`]. `every_workdir_write_is_declared_or_exempted` reads the
38//! runtime's own source text and fails until a new `workdir.join(...)` is either declared here or
39//! listed in that test's exempt tables with a written reason.
40
41use serde::Serialize;
42
43use crate::sandbox::EnforcementTier;
44
45/// The literal segment a declared path carries in place of a real session id.
46///
47/// Substituted by the consumer, from `session_start.session_id`. Never expanded here: see the
48/// module docs for why the two renderings have to stay identical.
49pub const SESSION_ID_PLACEHOLDER: &str = "<session-id>";
50
51/// Whether a declared entry is a single file or a directory whose whole subtree the runtime owns.
52///
53/// A consumer subtracting the declaration treats [`Self::Directory`] as a path prefix and
54/// [`Self::File`] as an exact match.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
56#[serde(rename_all = "kebab-case")]
57pub enum RuntimeWriteKind {
58    File,
59    Directory,
60}
61
62impl RuntimeWriteKind {
63    /// The wire name, identical to what [`Serialize`] emits.
64    #[must_use]
65    pub fn wire_name(self) -> &'static str {
66        match self {
67            Self::File => "file",
68            Self::Directory => "directory",
69        }
70    }
71}
72
73/// Which of the two workdirs an entry lands in.
74///
75/// The distinction only exists under `--workdir`, where [`Self::Session`] paths sit under
76/// `.murmur/<session-id>/` inside the accessible workdir. Both are reported relative to the
77/// accessible workdir, so a consumer that excludes the single prefix `.murmur/` covers every
78/// [`Self::Session`] row without reading their paths at all.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
80#[serde(rename_all = "kebab-case")]
81pub enum RuntimeWriteScope {
82    /// Directly inside the directory the capsule was asked to produce.
83    Accessible,
84    /// Inside this session's own bookkeeping directory, `.murmur/<session-id>/`.
85    Session,
86}
87
88impl RuntimeWriteScope {
89    /// The wire name, identical to what [`Serialize`] emits.
90    #[must_use]
91    pub fn wire_name(self) -> &'static str {
92        match self {
93            Self::Accessible => "accessible",
94            Self::Session => "session",
95        }
96    }
97}
98
99/// When a declared entry is present on disk.
100///
101/// [`Self::Sealed`] is the one value that also decides whether the row appears in the report at
102/// all — see the module docs.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
104#[serde(rename_all = "kebab-case")]
105pub enum RuntimeWriteCondition {
106    /// Written by every session, before the capsule runs.
107    Always,
108    /// Written only when the launch was given `--workdir`.
109    WorkdirProvided,
110    /// Written by the agent loop, so absent from a script capsule's session.
111    AgentSession,
112    /// Written by a script capsule, whose WASI preopen `.` is the accessible workdir itself.
113    ScriptSession,
114    /// Written when a shell or native tool call runs.
115    Shell,
116    /// Written when a peer handle is redeemed, which needs `capabilities.peer_fetch.allow`.
117    PeerFetch,
118    /// Written when this capsule launches a sub-capsule, which needs `capabilities.spawn.allow`.
119    Spawn,
120    /// Written by a capsule that was itself launched as a delegated child, into its own workdir.
121    Delegated,
122    /// Written only where the enforcement tier composes a sealed root.
123    Sealed,
124}
125
126impl RuntimeWriteCondition {
127    /// The wire name, identical to what [`Serialize`] emits.
128    #[must_use]
129    pub fn wire_name(self) -> &'static str {
130        match self {
131            Self::Always => "always",
132            Self::WorkdirProvided => "workdir-provided",
133            Self::AgentSession => "agent-session",
134            Self::ScriptSession => "script-session",
135            Self::Shell => "shell",
136            Self::PeerFetch => "peer-fetch",
137            Self::Spawn => "spawn",
138            Self::Delegated => "delegated",
139            Self::Sealed => "sealed",
140        }
141    }
142}
143
144/// One path the runtime writes inside the capsule's workdir.
145///
146/// Field names are the JSON keys verbatim: `path`, `kind`, `scope`, `condition`.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148pub struct RuntimeWriteReport {
149    /// Relative to the accessible workdir, carrying the literal [`SESSION_ID_PLACEHOLDER`].
150    pub path: String,
151    pub kind: RuntimeWriteKind,
152    pub scope: RuntimeWriteScope,
153    pub condition: RuntimeWriteCondition,
154}
155
156/// One row of [`DECLARED_WRITES`]: an entry named relative to its own scope's root.
157///
158/// The `.murmur/<session-id>/` prefix a [`RuntimeWriteScope::Session`] entry carries in the report
159/// is composed by [`DeclaredWrite::report`] rather than written into each row, so the placeholder
160/// appears in one place and a row states only the part that is about the entry itself.
161struct DeclaredWrite {
162    entry: &'static str,
163    kind: RuntimeWriteKind,
164    scope: RuntimeWriteScope,
165    condition: RuntimeWriteCondition,
166}
167
168impl DeclaredWrite {
169    fn report(&self) -> RuntimeWriteReport {
170        let path = match self.scope {
171            RuntimeWriteScope::Accessible => self.entry.to_string(),
172            RuntimeWriteScope::Session => {
173                format!(".murmur/{SESSION_ID_PLACEHOLDER}/{}", self.entry)
174            }
175        };
176        RuntimeWriteReport {
177            path,
178            kind: self.kind,
179            scope: self.scope,
180            condition: self.condition,
181        }
182    }
183
184    /// The first path segment of this entry — the name a `workdir.join(...)` in the runtime's own
185    /// source has to resolve to for the entry to count as declared.
186    #[cfg(test)]
187    fn first_segment(&self) -> &'static str {
188        self.entry.split('/').next().unwrap_or(self.entry)
189    }
190}
191
192/// Every path the runtime writes inside a capsule's workdir, in report order.
193///
194/// The single source of truth: `mur run --explain-scope` reads it, `trace.jsonl` carries what that
195/// produced, and `every_workdir_write_is_declared_or_exempted` fails the suite when the runtime's
196/// source grows a workdir entry this table does not name.
197const DECLARED_WRITES: &[DeclaredWrite] = &[
198    // ── The accessible workdir's own top level ────────────────────────────────
199    DeclaredWrite {
200        // The one prefix a consumer excludes. Created by `stage_session` before anything else.
201        entry: ".murmur/<session-id>",
202        kind: RuntimeWriteKind::Directory,
203        scope: RuntimeWriteScope::Accessible,
204        condition: RuntimeWriteCondition::Always,
205    },
206    DeclaredWrite {
207        // The task input the capsule reads by relative path, rewritten on every incoming A2A
208        // message and removed again under `lifecycle.task_acceptance: queue`.
209        entry: "task.md",
210        kind: RuntimeWriteKind::File,
211        scope: RuntimeWriteScope::Accessible,
212        condition: RuntimeWriteCondition::Always,
213    },
214    DeclaredWrite {
215        // Copied from the manifest directory so the agent can reference it from `.`; skipped when
216        // the destination already exists, which is why a `--workdir` holding its own manifest is
217        // never overwritten.
218        entry: "murmur.yaml",
219        kind: RuntimeWriteKind::File,
220        scope: RuntimeWriteScope::Accessible,
221        condition: RuntimeWriteCondition::WorkdirProvided,
222    },
223    DeclaredWrite {
224        // `logs/shell-<ms>.log` for a foreground command whose output was truncated, and
225        // `logs/<work-id>.log` for a demoted one. Stays at the top level because the shell tool
226        // returns this path, workdir-relative, in the result the model reads.
227        entry: "logs",
228        kind: RuntimeWriteKind::Directory,
229        scope: RuntimeWriteScope::Accessible,
230        condition: RuntimeWriteCondition::Shell,
231    },
232    DeclaredWrite {
233        // Where a redeemed peer handle's bytes land. A documented resource-plane path.
234        entry: "peer-in",
235        kind: RuntimeWriteKind::Directory,
236        scope: RuntimeWriteScope::Accessible,
237        condition: RuntimeWriteCondition::PeerFetch,
238    },
239    DeclaredWrite {
240        // One directory per sub-capsule launch, each its own accessible workdir. Under `.murmur/`
241        // already, but beside the session directory rather than inside it: a child outlives no
242        // session but is named for the capsule, not for the parent's id.
243        entry: ".murmur/children",
244        kind: RuntimeWriteKind::Directory,
245        scope: RuntimeWriteScope::Accessible,
246        condition: RuntimeWriteCondition::Spawn,
247    },
248    DeclaredWrite {
249        // A script capsule's WASI preopen `.` is the accessible workdir, so its `out/result.txt`
250        // lands here rather than in the session directory the agent loop writes to.
251        entry: "out",
252        kind: RuntimeWriteKind::Directory,
253        scope: RuntimeWriteScope::Accessible,
254        condition: RuntimeWriteCondition::ScriptSession,
255    },
256    DeclaredWrite {
257        // A delegated child's own record of how it ended, read by the launcher's watcher.
258        entry: "completion.json",
259        kind: RuntimeWriteKind::File,
260        scope: RuntimeWriteScope::Accessible,
261        condition: RuntimeWriteCondition::Delegated,
262    },
263    // ── The per-session directory ─────────────────────────────────────────────
264    DeclaredWrite {
265        entry: "tools",
266        kind: RuntimeWriteKind::Directory,
267        scope: RuntimeWriteScope::Session,
268        condition: RuntimeWriteCondition::Always,
269    },
270    DeclaredWrite {
271        entry: "trace.jsonl",
272        kind: RuntimeWriteKind::File,
273        scope: RuntimeWriteScope::Session,
274        condition: RuntimeWriteCondition::Always,
275    },
276    DeclaredWrite {
277        // Trace bodies too large to inline, addressed by their own sha256.
278        entry: "blobs",
279        kind: RuntimeWriteKind::Directory,
280        scope: RuntimeWriteScope::Session,
281        condition: RuntimeWriteCondition::Always,
282    },
283    DeclaredWrite {
284        // `bootstrap.log`, `otel.log` and one `hook-<name>.log` per failing hook.
285        entry: "logs",
286        kind: RuntimeWriteKind::Directory,
287        scope: RuntimeWriteScope::Session,
288        condition: RuntimeWriteCondition::AgentSession,
289    },
290    DeclaredWrite {
291        // `result.txt`, `result_<task-id>.txt` and `compaction-summaries.jsonl`.
292        entry: "out",
293        kind: RuntimeWriteKind::Directory,
294        scope: RuntimeWriteScope::Session,
295        condition: RuntimeWriteCondition::AgentSession,
296    },
297    DeclaredWrite {
298        entry: "MURMUR.md",
299        kind: RuntimeWriteKind::File,
300        scope: RuntimeWriteScope::Session,
301        condition: RuntimeWriteCondition::AgentSession,
302    },
303    DeclaredWrite {
304        // One `plan-<n>.json` per submitted plan.
305        entry: "plans",
306        kind: RuntimeWriteKind::Directory,
307        scope: RuntimeWriteScope::Session,
308        condition: RuntimeWriteCondition::AgentSession,
309    },
310    DeclaredWrite {
311        // `$HOME` for every shell and native tool subprocess. Reached only through the variable;
312        // nothing outside the runtime names the literal path.
313        entry: ".capsule-home",
314        kind: RuntimeWriteKind::Directory,
315        scope: RuntimeWriteScope::Session,
316        condition: RuntimeWriteCondition::Shell,
317    },
318    DeclaredWrite {
319        // The bind source the composed root mounts at `/tmp`. The capsule sees `/tmp` and never a
320        // workdir path.
321        entry: ".mur-tmp",
322        kind: RuntimeWriteKind::Directory,
323        scope: RuntimeWriteScope::Session,
324        condition: RuntimeWriteCondition::Sealed,
325    },
326    DeclaredWrite {
327        // The bind sources for the composed root's synthetic `/etc/passwd` and `/etc/group`.
328        entry: ".mur-etc",
329        kind: RuntimeWriteKind::Directory,
330        scope: RuntimeWriteScope::Session,
331        condition: RuntimeWriteCondition::Sealed,
332    },
333];
334
335/// Every path the runtime writes inside this launch's workdir, in declaration order.
336///
337/// Pure and filesystem-free: `mur run --explain-scope` returns before a workdir exists, so a
338/// declaration that touched one would make the diagnostic create the thing it describes.
339///
340/// `tier` is the tier the session would actually install — [`crate::sandbox::applied_tier`], the
341/// host's own tier capped by the declared floor — never the declared class on its own. A capsule
342/// declaring `scoped` on a sealed-capable host composes no root; under an unmet `sealed` floor the
343/// launch refuses anyway. Either way the honest answer is the set the mechanism that will really
344/// run writes.
345pub(crate) fn runtime_writes(tier: EnforcementTier) -> Vec<RuntimeWriteReport> {
346    let sealed = tier == EnforcementTier::KernelSealed;
347    DECLARED_WRITES
348        .iter()
349        .filter(|declared| sealed || declared.condition != RuntimeWriteCondition::Sealed)
350        .map(DeclaredWrite::report)
351        .collect()
352}
353
354/// The `Runtime writes` block of `mur run --explain-scope`'s human rendering.
355///
356/// Ends with a newline. Shared with nothing else today, but kept beside the declaration for the
357/// reason [`crate::containment::render_read_only`] is kept beside its own: a second renderer is a
358/// second place for the wording to drift from the data.
359#[must_use]
360pub fn render_runtime_writes(writes: &[RuntimeWriteReport]) -> String {
361    let mut out = String::new();
362    out.push_str(
363        "  paths are relative to the accessible workdir; `<session-id>` is a literal segment, \
364         replaced by the run's own session id\n",
365    );
366    if writes.is_empty() {
367        out.push_str("  runtime writes: <none>\n");
368        return out;
369    }
370    out.push_str("  runtime writes:\n");
371    for write in writes {
372        out.push_str(&format!(
373            "    - {} ({}, {}, {})\n",
374            write.path,
375            write.kind.wire_name(),
376            write.scope.wire_name(),
377            write.condition.wire_name()
378        ));
379    }
380    out
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    /// The source files carrying a production `workdir.join(...)`, each scanned by
388    /// [`every_workdir_write_is_declared_or_exempted`].
389    ///
390    /// `plan.rs` has no production occurrence today and is scanned anyway, so a plan step that
391    /// grows one is covered without anybody remembering to widen this list.
392    const SCANNED: &[(&str, &str)] = &[
393        ("agent.rs", include_str!("agent.rs")),
394        ("agent/inventory.rs", include_str!("agent/inventory.rs")),
395        ("agent/process.rs", include_str!("agent/process.rs")),
396        ("child_launch.rs", include_str!("child_launch.rs")),
397        ("delegation.rs", include_str!("delegation.rs")),
398        ("hooks.rs", include_str!("hooks.rs")),
399        ("murmur_md.rs", include_str!("murmur_md.rs")),
400        ("otel.rs", include_str!("otel.rs")),
401        ("plan.rs", include_str!("plan.rs")),
402        ("protected_paths.rs", include_str!("protected_paths.rs")),
403        ("recipes.rs", include_str!("recipes.rs")),
404        ("resource_plane.rs", include_str!("resource_plane.rs")),
405        ("runtime.rs", include_str!("runtime.rs")),
406        ("sandbox.rs", include_str!("sandbox.rs")),
407        ("sealed.rs", include_str!("sealed.rs")),
408        ("shell.rs", include_str!("shell.rs")),
409        ("tool_annotations.rs", include_str!("tool_annotations.rs")),
410        ("trace.rs", include_str!("trace.rs")),
411        ("trace_blobs.rs", include_str!("trace_blobs.rs")),
412    ];
413
414    /// Constant names a `workdir.join(...)` may name, paired with the constant itself.
415    ///
416    /// The pairing is the point: the scanner reads the identifier out of source text, and this
417    /// table is what turns that text into the value it stands for, so renaming the constant or
418    /// changing what it holds is caught rather than silently accepted.
419    const CONSTANTS: &[(&str, &str)] = &[
420        ("MANIFEST_FILENAME", murmur_artifact::MANIFEST_FILENAME),
421        ("COMPLETION_FILE", crate::delegation::COMPLETION_FILE),
422        ("BLOB_DIR_NAME", crate::trace_blobs::BLOB_DIR_NAME),
423        ("SEALED_TMP_DIR_NAME", crate::sealed::SEALED_TMP_DIR_NAME),
424        (
425            "SEALED_ETC_STAGING_DIR_NAME",
426            crate::sealed::SEALED_ETC_STAGING_DIR_NAME,
427        ),
428        (
429            "SYNTHETIC_HOME_DIR_NAME",
430            crate::shell::SYNTHETIC_HOME_DIR_NAME,
431        ),
432    ];
433
434    /// Entry names the scanner resolves that are deliberately not declared, each with the reason.
435    const EXEMPT: &[(&str, &str)] = &[
436        (
437            "input.txt",
438            "Read, never written: the fallback name `read_task` tries after `task.md`. Source \
439             text cannot tell a read from a write, so a path the runtime only ever opens for \
440             reading is exempted here rather than declared as something the runtime produces.",
441        ),
442        (
443            "package.json",
444            "Read, never written: the recipe detector opens it to learn which scripts a workdir \
445             already declares. Exempt on `input.txt`'s terms.",
446        ),
447    ];
448
449    /// `workdir.join(...)` calls whose argument is not a literal or a named constant, each with
450    /// the reason no entry name can be resolved from the source text.
451    const DYNAMIC_EXEMPT: &[(&str, &str, &str)] = &[
452        (
453            "protected_paths.rs",
454            "&rule.relative",
455            "A `capabilities.filesystem.read_only` path from the manifest, resolved to decide \
456             whether a call is refused. The runtime reads it and writes nothing.",
457        ),
458        (
459            "protected_paths.rs",
460            "&lexical",
461            "The lexically normalised form of a path a tool call named, resolved for the same \
462             refusal decision.",
463        ),
464        (
465            "recipes.rs",
466            "dir",
467            "A directory name from the recipe detector's own candidate list, opened to see \
468             whether the workdir has it.",
469        ),
470        (
471            "resource_plane.rs",
472            "declared_root",
473            "The `exports.files` / `exports.peer_files` root a manifest declares. The plane \
474             serves what the capsule put there; it creates nothing of its own.",
475        ),
476        (
477            "resource_plane.rs",
478            "&export.root",
479            "The same declared root, resolved when a request is served. Exempt on \
480             `declared_root`'s terms.",
481        ),
482        (
483            "runtime.rs",
484            "&relative",
485            "A resource path a tool call named, resolved to check it is inside the workdir \
486             before the file is read.",
487        ),
488        (
489            "runtime.rs",
490            "&stored_path",
491            "Where a redeemed peer handle's bytes go, composed by `peer_handoff::stored_path_for` \
492             under `peer-in/`. Declared through that directory's own entry, which is the name a \
493             consumer excludes.",
494        ),
495        (
496            "shell.rs",
497            "&output_path",
498            "A demoted command's own log path, `logs/<work-id>.log`, composed by \
499             `detached::output_path_for` and joined here only to measure the file's size. \
500             Declared through the `logs` entry.",
501        ),
502        (
503            "delegation.rs",
504            "format!( \".{COMPLETION_FILE}.{}.tmp\", uuid::Uuid::now_v7().simple() )",
505            "The temporary file `completion.json` is written through and renamed from. It exists \
506             only between two syscalls in the same function, so a consumer diffing a finished \
507             run never sees it.",
508        ),
509    ];
510
511    /// Production text of `source`: everything above its test module.
512    ///
513    /// A test fixture writing `workdir.join("scratch")` says nothing about what the runtime
514    /// produces, so scanning below this marker would fill the tables with names no capsule ever
515    /// sees.
516    fn production_text(source: &str) -> &str {
517        match source.find("#[cfg(test)]\nmod tests {") {
518            Some(index) => &source[..index],
519            None => source,
520        }
521    }
522
523    /// Every `workdir.join(<arg>)` in `source`, as the argument's source text.
524    ///
525    /// The receiver is matched on its `workdir` suffix rather than by name, which is what makes
526    /// `accessible_workdir.join(`, `session_workdir.join(`, `child_workdir.join(` and
527    /// `parent_accessible_workdir\n    .join(` all one case.
528    fn workdir_join_arguments(source: &str) -> Vec<String> {
529        let mut found = Vec::new();
530        let bytes = source.as_bytes();
531        let mut cursor = 0;
532        while let Some(offset) = source[cursor..].find(".join(") {
533            let start = cursor + offset;
534            cursor = start + ".join(".len();
535            if !source[..start].trim_end().ends_with("workdir") {
536                continue;
537            }
538            let mut depth = 1usize;
539            let mut index = cursor;
540            while index < bytes.len() && depth > 0 {
541                match bytes[index] {
542                    b'(' => depth += 1,
543                    b')' => depth -= 1,
544                    _ => {}
545                }
546                index += 1;
547            }
548            assert!(
549                depth == 0,
550                "unbalanced parentheses after a join into a workdir"
551            );
552            // Collapsed to one line: an argument's line breaks and indentation are `rustfmt`'s
553            // business, and a table row that had to reproduce them would fail on a reflow.
554            found.push(
555                source[cursor..index - 1]
556                    .split_whitespace()
557                    .collect::<Vec<_>>()
558                    .join(" "),
559            );
560        }
561        found
562    }
563
564    /// The entry name a `workdir.join(...)` argument stands for, or `None` when the source text
565    /// cannot say — a variable, a field access, a formatted string.
566    fn resolve_entry_name(argument: &str) -> Option<String> {
567        if let Some(rest) = argument.strip_prefix('"') {
568            return rest.split_once('"').map(|(literal, _)| literal.to_string());
569        }
570        let last = argument.rsplit("::").next().unwrap_or(argument);
571        let is_constant_name = !last.is_empty()
572            && last
573                .chars()
574                .all(|character| character.is_ascii_uppercase() || character == '_');
575        if !is_constant_name {
576            return None;
577        }
578        Some(
579            CONSTANTS
580                .iter()
581                .find(|(name, _)| *name == last)
582                .unwrap_or_else(|| {
583                    panic!(
584                        "a workdir.join({argument}) names the constant {last}, which this test's \
585                         CONSTANTS table does not pair with a value. Add \
586                         (\"{last}\", <path to the constant>) so a rename or a value change is \
587                         caught here rather than silently accepted."
588                    )
589                })
590                .1
591                .to_string(),
592        )
593    }
594
595    /// Every entry the runtime writes into a workdir is declared by [`DECLARED_WRITES`] or
596    /// exempted here with a written reason.
597    ///
598    /// The input is the runtime's own source text rather than a list somebody maintains, so a
599    /// developer who adds a `workdir.join("...")` fails this test until they decide which it is.
600    #[test]
601    fn every_workdir_write_is_declared_or_exempted() {
602        let declared: Vec<&str> = DECLARED_WRITES
603            .iter()
604            .map(DeclaredWrite::first_segment)
605            .collect();
606
607        let mut scanned = 0usize;
608        for (file, source) in SCANNED {
609            for argument in workdir_join_arguments(production_text(source)) {
610                scanned += 1;
611                let Some(entry) = resolve_entry_name(&argument) else {
612                    assert!(
613                        DYNAMIC_EXEMPT
614                            .iter()
615                            .any(|(exempt_file, exempt_argument, _)| exempt_file == file
616                                && *exempt_argument == argument),
617                        "{file} writes workdir.join({argument}) into a workdir, and the argument \
618                         is neither a string literal nor a named constant, so this test cannot \
619                         say what it names. Add ({file:?}, {argument:?}, <written reason>) to \
620                         this test's DYNAMIC_EXEMPT table."
621                    );
622                    continue;
623                };
624                assert!(
625                    declared.contains(&entry.as_str())
626                        || EXEMPT.iter().any(|(name, _)| *name == entry),
627                    "{file} writes `{entry}` into a workdir, and no --explain-scope report \
628                     declares it. Either add an entry for `{entry}` to DECLARED_WRITES in \
629                     workdir_writes.rs, so a consumer diffing the workdir can subtract it, or add \
630                     it to this test's EXEMPT table with a written reason — a path the runtime \
631                     only reads is the usual one."
632                );
633            }
634        }
635
636        // A scanner that silently stopped matching would otherwise pass by finding nothing.
637        assert!(
638            scanned >= 50,
639            "the workdir.join scan found only {scanned} occurrences across {} files, far below \
640             what the runtime has — the scanner is broken, not the runtime",
641            SCANNED.len()
642        );
643
644        for (name, _) in EXEMPT {
645            assert!(
646                !declared.contains(name),
647                "`{name}` is listed as exempt and is also declared — remove one"
648            );
649        }
650    }
651
652    /// Every source file with a production `workdir.join(...)` is in `SCANNED`.
653    ///
654    /// Without this, a new module writing into a workdir would be invisible to the scan above and
655    /// the declaration would go quietly stale, which is the exact failure this whole module
656    /// exists to close.
657    #[test]
658    fn every_source_file_that_joins_a_workdir_is_scanned() {
659        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
660        let mut stack = vec![root.clone()];
661        let mut checked = 0usize;
662        while let Some(dir) = stack.pop() {
663            let entries = std::fs::read_dir(&dir)
664                .unwrap_or_else(|error| panic!("reading {}: {error}", dir.display()));
665            for entry in entries {
666                let path = entry.unwrap().path();
667                if path.is_dir() {
668                    stack.push(path);
669                    continue;
670                }
671                if path.extension().is_some_and(|extension| extension == "rs") {
672                    checked += 1;
673                    // This module's own prose and failure messages quote the pattern being
674                    // searched for; it declares the writes rather than performing any.
675                    if path
676                        .file_name()
677                        .is_some_and(|name| name == "workdir_writes.rs")
678                    {
679                        continue;
680                    }
681                    let source = std::fs::read_to_string(&path).unwrap();
682                    if workdir_join_arguments(production_text(&source)).is_empty() {
683                        continue;
684                    }
685                    let relative = path
686                        .strip_prefix(&root)
687                        .unwrap()
688                        .to_string_lossy()
689                        .replace('\\', "/");
690                    assert!(
691                        SCANNED.iter().any(|(file, _)| *file == relative),
692                        "src/{relative} writes into a workdir but is not in \
693                         every_workdir_write_is_declared_or_exempted's SCANNED list, so nothing \
694                         checks its entries against the declaration. Add \
695                         (\"{relative}\", include_str!(\"{relative}\")) to it."
696                    );
697                }
698            }
699        }
700        assert!(
701            checked >= 40,
702            "walked only {checked} source files under src/ — the walk is broken"
703        );
704    }
705
706    /// Sealed is the one condition that changes the reported set, and it adds nothing a consumer
707    /// diffing the accessible workdir's top level would see.
708    #[test]
709    fn the_sealed_tier_adds_exactly_the_two_sealed_session_entries() {
710        let scoped = runtime_writes(EnforcementTier::KernelFull);
711        let sealed = runtime_writes(EnforcementTier::KernelSealed);
712
713        assert!(
714            scoped.iter().all(|write| sealed.contains(write)),
715            "the sealed declaration must be a superset of the scoped one"
716        );
717        let added: Vec<&RuntimeWriteReport> = sealed
718            .iter()
719            .filter(|write| !scoped.contains(write))
720            .collect();
721        assert_eq!(
722            added
723                .iter()
724                .map(|write| write.path.as_str())
725                .collect::<Vec<_>>(),
726            vec![
727                ".murmur/<session-id>/.mur-tmp",
728                ".murmur/<session-id>/.mur-etc"
729            ]
730        );
731        for write in added {
732            assert_eq!(write.condition, RuntimeWriteCondition::Sealed);
733            assert_eq!(write.scope, RuntimeWriteScope::Session);
734            assert_eq!(write.kind, RuntimeWriteKind::Directory);
735        }
736    }
737
738    /// Every non-sealed tier reports the same set, so a host's Landlock or seccomp availability
739    /// cannot quietly change what a consumer subtracts.
740    #[test]
741    fn only_the_sealed_tier_changes_the_declared_set() {
742        let baseline = runtime_writes(EnforcementTier::EnvironmentOnly);
743        assert_eq!(baseline, runtime_writes(EnforcementTier::KernelSeccompOnly));
744        assert_eq!(baseline, runtime_writes(EnforcementTier::KernelFull));
745        assert!(baseline
746            .iter()
747            .all(|write| write.condition != RuntimeWriteCondition::Sealed));
748    }
749
750    /// Serialized rows carry exactly the four documented keys, in declaration order, with the
751    /// kebab-case wire values.
752    ///
753    /// The order is asserted off `to_string` rather than off `to_value`: `serde_json`'s `Value`
754    /// holds a map that sorts its keys, so a round trip through it would prove nothing about the
755    /// bytes `--explain-scope --json` and `trace.jsonl` actually carry.
756    #[test]
757    fn a_serialized_row_carries_exactly_path_kind_scope_and_condition() {
758        for write in runtime_writes(EnforcementTier::KernelSealed) {
759            let line = serde_json::to_string(&write).unwrap();
760            assert_eq!(
761                line,
762                format!(
763                    r#"{{"path":"{}","kind":"{}","scope":"{}","condition":"{}"}}"#,
764                    write.path,
765                    write.kind.wire_name(),
766                    write.scope.wire_name(),
767                    write.condition.wire_name()
768                )
769            );
770        }
771    }
772
773    /// A session-scoped entry is reported under the one prefix a consumer excludes, and an
774    /// accessible one never is.
775    #[test]
776    fn session_scoped_paths_carry_the_murmur_prefix_and_the_literal_session_id() {
777        for write in runtime_writes(EnforcementTier::KernelSealed) {
778            match write.scope {
779                RuntimeWriteScope::Session => {
780                    let prefix = format!(".murmur/{SESSION_ID_PLACEHOLDER}/");
781                    assert!(
782                        write.path.starts_with(&prefix),
783                        "{} is session-scoped but not under {prefix}",
784                        write.path
785                    );
786                }
787                RuntimeWriteScope::Accessible => assert!(
788                    !write
789                        .path
790                        .starts_with(&format!(".murmur/{SESSION_ID_PLACEHOLDER}/")),
791                    "{} is accessible-scoped but sits inside the session directory",
792                    write.path
793                ),
794            }
795            assert!(
796                !write.path.contains("ses_"),
797                "{} carries a real session id where the placeholder belongs",
798                write.path
799            );
800        }
801    }
802
803    /// No two rows name the same path, so a subtraction cannot silently depend on which one it
804    /// matched.
805    #[test]
806    fn no_declared_path_is_reported_twice() {
807        let writes = runtime_writes(EnforcementTier::KernelSealed);
808        let mut paths: Vec<&str> = writes.iter().map(|write| write.path.as_str()).collect();
809        paths.sort_unstable();
810        let count = paths.len();
811        paths.dedup();
812        assert_eq!(paths.len(), count, "a path is declared twice");
813    }
814
815    #[test]
816    fn the_rendering_names_every_declared_path_and_the_placeholder_rule() {
817        let writes = runtime_writes(EnforcementTier::KernelSealed);
818        let rendered = render_runtime_writes(&writes);
819        assert!(rendered.contains("`<session-id>` is a literal segment"));
820        for write in &writes {
821            assert!(
822                rendered.contains(&write.path),
823                "{} is missing from the rendering",
824                write.path
825            );
826        }
827        assert!(rendered.ends_with('\n'));
828    }
829}