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//! # What is not here
36//!
37//! Every file the runtime writes for its own use — `MURMUR.md`, `task.md`, the manifest copy,
38//! `tools/` and `plans/` — is written into the session's runtime directory,
39//! `~/.murmur/sessions/<session-id>/runtime`, never into the workdir, and the synthetic `/etc`
40//! files and a `transport: process` harness's files go beside it. Every record it writes about
41//! the session — `trace.jsonl`, `blobs/`, `logs/bootstrap.log`, `logs/otel.log`,
42//! `logs/hook-<name>.log`, a delegated child's `completion.json` and the `hooks/` a records-granted
43//! hook writes in — goes into the session's records directory,
44//! `~/.murmur/records/<workdir root>/<session-id>`. None of them is a runtime write inside the
45//! workdir, so none is declared here; [`crate::runtime_dir`] and [`crate::records_dir`] describe
46//! them.
47//!
48//! # Adding an entry
49//!
50//! Add a row to [`DECLARED_WRITES`]. `every_workdir_write_is_declared_or_exempted` reads the
51//! runtime's own source text and fails until a new `workdir.join(...)` is either declared here or
52//! listed in that test's exempt tables with a written reason. A file the runtime writes for its
53//! own use goes through the runtime directory's root instead and is declared in that test's
54//! `RUNTIME_DIR_WRITES`; a record about the session goes through the records directory's root,
55//! `records_root`, and is declared in its `RECORDS_DIR_WRITES`.
56
57use serde::Serialize;
58
59use crate::sandbox::EnforcementTier;
60
61/// The literal segment a declared path carries in place of a real session id.
62///
63/// Substituted by the consumer, from `session_start.session_id`. Never expanded here: see the
64/// module docs for why the two renderings have to stay identical.
65pub const SESSION_ID_PLACEHOLDER: &str = "<session-id>";
66
67/// Whether a declared entry is a single file or a directory whose whole subtree the runtime owns.
68///
69/// A consumer subtracting the declaration treats [`Self::Directory`] as a path prefix and
70/// [`Self::File`] as an exact match.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "kebab-case")]
73pub enum RuntimeWriteKind {
74    File,
75    Directory,
76}
77
78impl RuntimeWriteKind {
79    /// The wire name, identical to what [`Serialize`] emits.
80    #[must_use]
81    pub fn wire_name(self) -> &'static str {
82        match self {
83            Self::File => "file",
84            Self::Directory => "directory",
85        }
86    }
87}
88
89/// Which of the two workdirs an entry lands in.
90///
91/// The distinction only exists under `--workdir`, where [`Self::Session`] paths sit under
92/// `.murmur/<session-id>/` inside the accessible workdir. Both are reported relative to the
93/// accessible workdir, so a consumer that excludes the single prefix `.murmur/` covers every
94/// [`Self::Session`] row without reading their paths at all.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "kebab-case")]
97pub enum RuntimeWriteScope {
98    /// Directly inside the directory the capsule was asked to produce.
99    Accessible,
100    /// Inside this session's own bookkeeping directory, `.murmur/<session-id>/`.
101    Session,
102}
103
104impl RuntimeWriteScope {
105    /// The wire name, identical to what [`Serialize`] emits.
106    #[must_use]
107    pub fn wire_name(self) -> &'static str {
108        match self {
109            Self::Accessible => "accessible",
110            Self::Session => "session",
111        }
112    }
113}
114
115/// When a declared entry is present on disk.
116///
117/// [`Self::Sealed`] is the one value that also decides whether the row appears in the report at
118/// all — see the module docs.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
120#[serde(rename_all = "kebab-case")]
121pub enum RuntimeWriteCondition {
122    /// Written by every session, before the capsule runs.
123    Always,
124    /// Written by the agent loop, so absent from a script capsule's session.
125    AgentSession,
126    /// Written by a script capsule, whose WASI preopen `.` is the accessible workdir itself.
127    ScriptSession,
128    /// Written when a shell or native tool call runs.
129    Shell,
130    /// Written when a peer handle is redeemed, which needs `capabilities.peer_fetch.allow`.
131    PeerFetch,
132    /// Written when this capsule launches a sub-capsule, which needs `capabilities.spawn.allow`.
133    Spawn,
134    /// Written only where the enforcement tier composes a sealed root.
135    Sealed,
136}
137
138impl RuntimeWriteCondition {
139    /// The wire name, identical to what [`Serialize`] emits.
140    #[must_use]
141    pub fn wire_name(self) -> &'static str {
142        match self {
143            Self::Always => "always",
144            Self::AgentSession => "agent-session",
145            Self::ScriptSession => "script-session",
146            Self::Shell => "shell",
147            Self::PeerFetch => "peer-fetch",
148            Self::Spawn => "spawn",
149            Self::Sealed => "sealed",
150        }
151    }
152}
153
154/// One path the runtime writes inside the capsule's workdir.
155///
156/// Field names are the JSON keys verbatim: `path`, `kind`, `scope`, `condition`.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct RuntimeWriteReport {
159    /// Relative to the accessible workdir, carrying the literal [`SESSION_ID_PLACEHOLDER`].
160    pub path: String,
161    pub kind: RuntimeWriteKind,
162    pub scope: RuntimeWriteScope,
163    pub condition: RuntimeWriteCondition,
164}
165
166/// One row of [`DECLARED_WRITES`]: an entry named relative to its own scope's root.
167///
168/// The `.murmur/<session-id>/` prefix a [`RuntimeWriteScope::Session`] entry carries in the report
169/// is composed by [`DeclaredWrite::report`] rather than written into each row, so the placeholder
170/// appears in one place and a row states only the part that is about the entry itself.
171struct DeclaredWrite {
172    entry: &'static str,
173    kind: RuntimeWriteKind,
174    scope: RuntimeWriteScope,
175    condition: RuntimeWriteCondition,
176}
177
178impl DeclaredWrite {
179    fn report(&self) -> RuntimeWriteReport {
180        let path = match self.scope {
181            RuntimeWriteScope::Accessible => self.entry.to_string(),
182            RuntimeWriteScope::Session => {
183                format!(".murmur/{SESSION_ID_PLACEHOLDER}/{}", self.entry)
184            }
185        };
186        RuntimeWriteReport {
187            path,
188            kind: self.kind,
189            scope: self.scope,
190            condition: self.condition,
191        }
192    }
193
194    /// The first path segment of this entry — the name a `workdir.join(...)` in the runtime's own
195    /// source has to resolve to for the entry to count as declared.
196    #[cfg(test)]
197    fn first_segment(&self) -> &'static str {
198        self.entry.split('/').next().unwrap_or(self.entry)
199    }
200}
201
202/// Every path the runtime writes inside a capsule's workdir, in report order.
203///
204/// The single source of truth: `mur run --explain-scope` reads it, `trace.jsonl` carries what that
205/// produced, and `every_workdir_write_is_declared_or_exempted` fails the suite when the runtime's
206/// source grows a workdir entry this table does not name.
207const DECLARED_WRITES: &[DeclaredWrite] = &[
208    // ── The accessible workdir's own top level ────────────────────────────────
209    DeclaredWrite {
210        // The one prefix a consumer excludes. Created by `stage_session` before anything else.
211        entry: ".murmur/<session-id>",
212        kind: RuntimeWriteKind::Directory,
213        scope: RuntimeWriteScope::Accessible,
214        condition: RuntimeWriteCondition::Always,
215    },
216    DeclaredWrite {
217        // `logs/shell-<ms>.log` for a foreground command whose output was truncated, and
218        // `logs/<work-id>.log` for a demoted one. Stays at the top level because the shell tool
219        // returns this path, workdir-relative, in the result the model reads.
220        entry: "logs",
221        kind: RuntimeWriteKind::Directory,
222        scope: RuntimeWriteScope::Accessible,
223        condition: RuntimeWriteCondition::Shell,
224    },
225    DeclaredWrite {
226        // Where a redeemed peer handle's bytes land. A documented resource-plane path.
227        entry: "peer-in",
228        kind: RuntimeWriteKind::Directory,
229        scope: RuntimeWriteScope::Accessible,
230        condition: RuntimeWriteCondition::PeerFetch,
231    },
232    DeclaredWrite {
233        // One directory per sub-capsule launch, each its own accessible workdir. Under `.murmur/`
234        // already, but beside the session directory rather than inside it: a child outlives no
235        // session but is named for the capsule, not for the parent's id.
236        entry: ".murmur/children",
237        kind: RuntimeWriteKind::Directory,
238        scope: RuntimeWriteScope::Accessible,
239        condition: RuntimeWriteCondition::Spawn,
240    },
241    DeclaredWrite {
242        // A script capsule's WASI preopen `.` is the accessible workdir, so its `out/result.txt`
243        // lands here rather than in the session directory the agent loop writes to.
244        entry: "out",
245        kind: RuntimeWriteKind::Directory,
246        scope: RuntimeWriteScope::Accessible,
247        condition: RuntimeWriteCondition::ScriptSession,
248    },
249    // ── The per-session directory ─────────────────────────────────────────────
250    DeclaredWrite {
251        // `result.txt`, `result_<task-id>.txt` and `compaction-summaries.jsonl`.
252        entry: "out",
253        kind: RuntimeWriteKind::Directory,
254        scope: RuntimeWriteScope::Session,
255        condition: RuntimeWriteCondition::AgentSession,
256    },
257    DeclaredWrite {
258        // `$HOME` for every shell and native tool subprocess. Reached only through the variable;
259        // nothing outside the runtime names the literal path.
260        entry: ".capsule-home",
261        kind: RuntimeWriteKind::Directory,
262        scope: RuntimeWriteScope::Session,
263        condition: RuntimeWriteCondition::Shell,
264    },
265    DeclaredWrite {
266        // The bind source the composed root mounts at `/tmp`. The capsule sees `/tmp` and never a
267        // workdir path.
268        entry: ".mur-tmp",
269        kind: RuntimeWriteKind::Directory,
270        scope: RuntimeWriteScope::Session,
271        condition: RuntimeWriteCondition::Sealed,
272    },
273];
274
275/// Every path the runtime writes inside this launch's workdir, in declaration order.
276///
277/// Pure and filesystem-free: `mur run --explain-scope` returns before a workdir exists, so a
278/// declaration that touched one would make the diagnostic create the thing it describes.
279///
280/// `tier` is the tier the session would actually install — [`crate::sandbox::applied_tier`], the
281/// host's own tier capped by the declared floor — never the declared class on its own. A capsule
282/// declaring `scoped` on a sealed-capable host composes no root; under an unmet `sealed` floor the
283/// launch refuses anyway. Either way the honest answer is the set the mechanism that will really
284/// run writes.
285pub(crate) fn runtime_writes(tier: EnforcementTier) -> Vec<RuntimeWriteReport> {
286    let sealed = tier == EnforcementTier::KernelSealed;
287    DECLARED_WRITES
288        .iter()
289        .filter(|declared| sealed || declared.condition != RuntimeWriteCondition::Sealed)
290        .map(DeclaredWrite::report)
291        .collect()
292}
293
294/// The `Runtime writes` block of `mur run --explain-scope`'s human rendering.
295///
296/// Ends with a newline. Shared with nothing else today, but kept beside the declaration for the
297/// reason [`crate::containment::render_read_only`] is kept beside its own: a second renderer is a
298/// second place for the wording to drift from the data.
299#[must_use]
300pub fn render_runtime_writes(writes: &[RuntimeWriteReport]) -> String {
301    let mut out = String::new();
302    out.push_str(
303        "  paths are relative to the accessible workdir; `<session-id>` is a literal segment, \
304         replaced by the run's own session id\n",
305    );
306    if writes.is_empty() {
307        out.push_str("  runtime writes: <none>\n");
308        return out;
309    }
310    out.push_str("  runtime writes:\n");
311    for write in writes {
312        out.push_str(&format!(
313            "    - {} ({}, {}, {})\n",
314            write.path,
315            write.kind.wire_name(),
316            write.scope.wire_name(),
317            write.condition.wire_name()
318        ));
319    }
320    out
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    /// The source files carrying a production `workdir.join(...)` or a [`WorkdirRoot`] write,
328    /// each scanned by [`every_workdir_write_is_declared_or_exempted`] and by
329    /// [`no_workdir_file_bypasses_the_no_follow_helper`].
330    ///
331    /// `plan.rs` and `delegation_plane.rs` have no production write of their own and are scanned
332    /// anyway: both read a workdir for a decision, so a raw call either grows is caught without
333    /// anybody remembering to widen this list.
334    ///
335    /// [`WorkdirRoot`]: crate::workdir_fs::WorkdirRoot
336    const SCANNED: &[(&str, &str)] = &[
337        ("agent.rs", include_str!("agent.rs")),
338        ("agent/inventory.rs", include_str!("agent/inventory.rs")),
339        ("agent/process.rs", include_str!("agent/process.rs")),
340        ("child_launch.rs", include_str!("child_launch.rs")),
341        ("delegation.rs", include_str!("delegation.rs")),
342        ("delegation_plane.rs", include_str!("delegation_plane.rs")),
343        ("hooks.rs", include_str!("hooks.rs")),
344        ("murmur_md.rs", include_str!("murmur_md.rs")),
345        ("otel.rs", include_str!("otel.rs")),
346        ("network_policy.rs", include_str!("network_policy.rs")),
347        ("plan.rs", include_str!("plan.rs")),
348        ("protected_paths.rs", include_str!("protected_paths.rs")),
349        ("recipes.rs", include_str!("recipes.rs")),
350        ("records_dir.rs", include_str!("records_dir.rs")),
351        ("resource_plane.rs", include_str!("resource_plane.rs")),
352        ("retention.rs", include_str!("retention.rs")),
353        ("runtime.rs", include_str!("runtime.rs")),
354        (
355            "runtime/artifact_change.rs",
356            include_str!("runtime/artifact_change.rs"),
357        ),
358        ("runtime_dir.rs", include_str!("runtime_dir.rs")),
359        ("sandbox.rs", include_str!("sandbox.rs")),
360        ("sealed.rs", include_str!("sealed.rs")),
361        ("shell.rs", include_str!("shell.rs")),
362        ("tool_annotations.rs", include_str!("tool_annotations.rs")),
363        ("trace.rs", include_str!("trace.rs")),
364        ("trace_blobs.rs", include_str!("trace_blobs.rs")),
365        ("types.rs", include_str!("types.rs")),
366    ];
367
368    /// Constant names a `workdir.join(...)` may name, paired with the constant itself.
369    ///
370    /// The pairing is the point: the scanner reads the identifier out of source text, and this
371    /// table is what turns that text into the value it stands for, so renaming the constant or
372    /// changing what it holds is caught rather than silently accepted.
373    const CONSTANTS: &[(&str, &str)] = &[
374        ("MANIFEST_FILENAME", murmur_artifact::MANIFEST_FILENAME),
375        ("COMPLETION_FILE", crate::delegation::COMPLETION_FILE),
376        ("BLOB_DIR_NAME", crate::trace_blobs::BLOB_DIR_NAME),
377        ("SEALED_TMP_DIR_NAME", crate::sealed::SEALED_TMP_DIR_NAME),
378        (
379            "SYNTHETIC_HOME_DIR_NAME",
380            crate::shell::SYNTHETIC_HOME_DIR_NAME,
381        ),
382        ("TASK_INPUT_FILE", crate::runtime::TASK_INPUT_FILE),
383        (
384            "LEGACY_TASK_INPUT_FILE",
385            crate::runtime::LEGACY_TASK_INPUT_FILE,
386        ),
387        ("TRACE_FILE_NAME", crate::trace::TRACE_FILE_NAME),
388        ("CHILDREN_DIR", crate::child_launch::CHILDREN_DIR),
389        ("HOOKS_SUBDIR", crate::records_dir::HOOKS_SUBDIR),
390    ];
391
392    /// Entry names the scanner resolves that are deliberately not declared, each with the reason.
393    const EXEMPT: &[(&str, &str)] = &[
394        (
395            "input.txt",
396            "Read, never written: the operator's task file `import_task_input` reads once when \
397             the workdir has no `task.md`. Source text cannot tell a read from a write, so a \
398             path the runtime only ever opens for reading is exempted here rather than declared \
399             as something the runtime produces.",
400        ),
401        (
402            "package.json",
403            "Read, never written: the recipe detector opens it to learn which scripts a workdir \
404             already declares. Exempt on `input.txt`'s terms.",
405        ),
406    ];
407
408    /// `workdir.join(...)` calls whose argument is not a literal or a named constant, each with
409    /// the reason no entry name can be resolved from the source text.
410    const DYNAMIC_EXEMPT: &[(&str, &str, &str)] = &[
411        (
412            "protected_paths.rs",
413            "&rule.relative",
414            "A `capabilities.filesystem.read_only` path from the manifest, resolved to decide \
415             whether a call is refused. The runtime reads it and writes nothing.",
416        ),
417        (
418            "protected_paths.rs",
419            "&lexical",
420            "The lexically normalised form of a path a tool call named, resolved for the same \
421             refusal decision.",
422        ),
423        (
424            "resource_plane.rs",
425            "declared_root",
426            "The `exports.files` / `exports.peer_files` root a manifest declares. The plane \
427             serves what the capsule put there; it creates nothing of its own.",
428        ),
429        (
430            "resource_plane.rs",
431            "&export.root",
432            "The same declared root, resolved when a request is served. Exempt on \
433             `declared_root`'s terms.",
434        ),
435        (
436            "runtime.rs",
437            "&stored_path",
438            "Where a redeemed peer handle's bytes go, composed by `peer_handoff::stored_path_for` \
439             under `peer-in/`. Declared through that directory's own entry, which is the name a \
440             consumer excludes.",
441        ),
442        (
443            "runtime.rs",
444            "rel",
445            "`write_staged_file`'s path, `tools/<name>/<file>` in the runtime directory, composed \
446             by each of its callers. Declared through RUNTIME_DIR_WRITES' `tools` entry.",
447        ),
448        (
449            "types.rs",
450            "rel",
451            "`StagedSession::write_workdir_file`'s path, named by the embedding caller for an \
452             input it stages for the capsule before launch. A launch writes nothing there of its \
453             own.",
454        ),
455        (
456            "shell.rs",
457            "&relative_path",
458            "A command's full-output log, `logs/<stem>.log`, composed on the line above. Declared \
459             through the `logs` entry.",
460        ),
461        (
462            "child_launch.rs",
463            "rel",
464            "One child's directory, `.murmur/children/<name>-<16 hex>`, composed by \
465             `child_workdir_rel`. Declared through the `.murmur/children` entry.",
466        ),
467        (
468            "child_launch.rs",
469            "child_workdir_rel(capsule_name)",
470            "The host path of the same child directory, for the child's `--workdir` argument. \
471             Declared through the `.murmur/children` entry.",
472        ),
473        (
474            "trace.rs",
475            "trace",
476            "A resumed-from session's `trace.jsonl`, relative to the records group every session \
477             of this workdir root keeps its records directory in, outside every workdir.",
478        ),
479        (
480            "runtime.rs",
481            "&name",
482            "The launched manifest's own file name, copied into the runtime directory through \
483             `runtime_root`. Declared through RUNTIME_DIR_WRITES' `murmur.yaml` entry, the \
484             project manifest's name.",
485        ),
486        (
487            "records_dir.rs",
488            "&rel",
489            "`<workdir root mirrored>/<session-id>` beneath `~/.murmur/records`, created at staging \
490             outside every workdir.",
491        ),
492    ];
493
494    /// Production text of `source`: everything above its test module.
495    ///
496    /// A test fixture writing `workdir.join("scratch")` says nothing about what the runtime
497    /// produces, so scanning below this marker would fill the tables with names no capsule ever
498    /// sees.
499    fn production_text(source: &str) -> &str {
500        match source.find("#[cfg(test)]\nmod tests {") {
501            Some(index) => &source[..index],
502            None => source,
503        }
504    }
505
506    /// The source text between the `(` that ends at `open` and its matching `)`.
507    fn parenthesised(source: &str, open: usize) -> &str {
508        let bytes = source.as_bytes();
509        let mut depth = 1usize;
510        let mut index = open;
511        while index < bytes.len() && depth > 0 {
512            match bytes[index] {
513                b'(' => depth += 1,
514                b')' => depth -= 1,
515                _ => {}
516            }
517            index += 1;
518        }
519        assert!(depth == 0, "unbalanced parentheses in a scanned call");
520        &source[open..index - 1]
521    }
522
523    /// `text` collapsed to one line: an argument's line breaks and indentation are `rustfmt`'s
524    /// business, and a table row that had to reproduce them would fail on a reflow.
525    fn one_line(text: &str) -> String {
526        text.split_whitespace().collect::<Vec<_>>().join(" ")
527    }
528
529    /// The first top-level argument of an argument list.
530    fn first_argument(arguments: &str) -> &str {
531        let mut depth = 0i32;
532        let mut in_string = false;
533        for (index, character) in arguments.char_indices() {
534            match character {
535                '"' => in_string = !in_string,
536                '(' | '[' | '{' if !in_string => depth += 1,
537                ')' | ']' | '}' if !in_string => depth -= 1,
538                ',' if !in_string && depth == 0 => return &arguments[..index],
539                _ => {}
540            }
541        }
542        arguments
543    }
544
545    /// Every `workdir.join(<arg>)` in `source`, as the argument's source text.
546    ///
547    /// The receiver is matched on its `workdir` suffix rather than by name, which is what makes
548    /// `accessible_workdir.join(`, `session_workdir.join(`, `child_workdir.join(` and
549    /// `parent_accessible_workdir\n    .join(` all one case.
550    fn workdir_join_arguments(source: &str) -> Vec<String> {
551        let mut found = Vec::new();
552        let mut cursor = 0;
553        while let Some(offset) = source[cursor..].find(".join(") {
554            let start = cursor + offset;
555            cursor = start + ".join(".len();
556            if !source[..start].trim_end().ends_with("workdir") {
557                continue;
558            }
559            found.push(one_line(parenthesised(source, cursor)));
560        }
561        found
562    }
563
564    /// The [`crate::workdir_fs::WorkdirRoot`] methods that create something beneath a root.
565    const HELPER_WRITES: &[&str] = &[
566        ".write_file(",
567        ".write_file_exact_mode(",
568        ".overwrite_in_place(",
569        ".open_append(",
570        ".create_dir_all(",
571    ];
572
573    /// The relative-path argument of every [`HELPER_WRITES`] call in `source`, as source text.
574    ///
575    /// A `.create_dir_all(` straight after `fs` is `std::fs`'s, a raw call
576    /// [`no_workdir_file_bypasses_the_no_follow_helper`] judges instead.
577    fn helper_write_arguments(source: &str) -> Vec<String> {
578        helper_calls(source, HELPER_WRITES)
579            .into_iter()
580            .map(|(_, argument)| argument)
581            .collect()
582    }
583
584    /// Every call of one of `methods` in `source`, as `(receiver, first argument)` source text,
585    /// skipping `std::fs` functions of the same name. The receiver is the identifier the call
586    /// chain ends in — `runtime_root` for `self.runtime_root\n    .write_file(` — or `""` when
587    /// the text before the call does not end in one.
588    fn helper_calls(source: &str, methods: &[&str]) -> Vec<(String, String)> {
589        helper_calls_at(source, methods)
590            .into_iter()
591            .map(|(_, receiver, argument)| (receiver, argument))
592            .collect()
593    }
594
595    /// [`helper_calls`], with the byte offset of each call.
596    fn helper_calls_at(source: &str, methods: &[&str]) -> Vec<(usize, String, String)> {
597        let mut found = Vec::new();
598        for method in methods {
599            let mut cursor = 0;
600            while let Some(offset) = source[cursor..].find(method) {
601                let start = cursor + offset;
602                cursor = start + method.len();
603                if source[..start].ends_with("fs") {
604                    continue;
605                }
606                let receiver: String = source[..start]
607                    .trim_end()
608                    .chars()
609                    .rev()
610                    .take_while(|character| character.is_ascii_alphanumeric() || *character == '_')
611                    .collect::<Vec<_>>()
612                    .into_iter()
613                    .rev()
614                    .collect();
615                found.push((
616                    start,
617                    receiver,
618                    one_line(first_argument(parenthesised(source, cursor))),
619                ));
620            }
621        }
622        found
623    }
624
625    /// The receivers that name the session's runtime directory, through which every file the
626    /// runtime owns is written and read.
627    const RUNTIME_DIR_ROOT: &str = "runtime_root";
628
629    /// The receivers that name the session's records directory, through which every record the
630    /// runtime writes about the session goes.
631    const RECORDS_DIR_ROOT: &str = "records_root";
632
633    /// Every entry the runtime writes into the records directory, with what it is.
634    const RECORDS_DIR_WRITES: &[(&str, &str)] = &[
635        (
636            "trace.jsonl",
637            "The session's trace: every appender, and a resumed session's `shell_lost` markers.",
638        ),
639        (
640            "blobs",
641            "Trace bodies too large to inline, by their sha256.",
642        ),
643        (
644            "logs",
645            "`bootstrap.log`, `otel.log` and one `hook-<name>.log` per failing hook.",
646        ),
647        (
648            "completion.json",
649            "A delegated child's record of how it ended, written by the child and by its \
650             parent's watcher.",
651        ),
652        (
653            "hooks",
654            "The `\".\"` of every hook declared `capabilities.filesystem.records: true`.",
655        ),
656    ];
657
658    /// The session's own log files, which share the `logs` name with the accessible workdir's
659    /// shell output logs and are told apart by the rest of their path.
660    const MOVED_LOG_PREFIXES: &[&str] = &["logs/bootstrap.log", "logs/otel.log", "logs/hook-"];
661
662    /// The literal text a path argument opens with — a string literal, or a `format!` string up
663    /// to its first `{` — or `None` when it opens with no literal.
664    fn literal_prefix(argument: &str) -> Option<&str> {
665        let argument = argument.trim_start_matches('&');
666        let argument = argument
667            .strip_prefix("format!(")
668            .map(str::trim_start)
669            .unwrap_or(argument);
670        let (literal, _) = argument.strip_prefix('"')?.split_once('"')?;
671        Some(literal.split('{').next().unwrap_or(literal))
672    }
673
674    /// Whether a write of `argument` names one of the session's own log files.
675    fn names_a_moved_log(argument: &str) -> bool {
676        literal_prefix(argument).is_some_and(|literal| {
677            MOVED_LOG_PREFIXES
678                .iter()
679                .any(|prefix| literal.starts_with(prefix))
680        })
681    }
682
683    /// The receivers that name a directory beside the runtime directory, outside every workdir:
684    /// `sealed-etc/`, `harness/` and the session directory holding them. No capsule command is
685    /// granted any of them, so what is written there is no runtime write inside a workdir.
686    const OUTSIDE_WORKDIR_ROOTS: &[&str] = &["sealed_etc_root", "harness_root", "session_dir_root"];
687
688    /// Every entry the runtime writes into the runtime directory, with what it is. The files a
689    /// capsule's commands read and cannot change.
690    const RUNTIME_DIR_WRITES: &[(&str, &str)] = &[
691        (
692            "tools",
693            "Every staged and pulled artifact's manifest, `skill.md` and native entrypoint, and \
694             every runtime-provided tool's manifest.",
695        ),
696        (
697            "MURMUR.md",
698            "The environment reference, written at staging and after port binding.",
699        ),
700        (
701            "task.md",
702            "The task: `mur run --task`, the import from the workdir, every A2A message and \
703             every reopen's rewrite.",
704        ),
705        (
706            "plans",
707            "One `plan-<n>.json` per submitted plan, read back when it runs.",
708        ),
709        (
710            "murmur.yaml",
711            "The project manifest, copied on every launch.",
712        ),
713    ];
714
715    /// Entry names of files the runtime owns and of the records it keeps: a write or read under
716    /// one of them reaches a runtime-directory or records-directory root, never a workdir root.
717    const MOVED_OUT_OF_THE_WORKDIR: &[&str] = &[
718        "tools",
719        "MURMUR.md",
720        "task.md",
721        "plans",
722        "murmur.yaml",
723        ".mur-etc",
724        ".process-driver",
725        "trace.jsonl",
726        "blobs",
727        "completion.json",
728        "eval.jsonl",
729    ];
730
731    /// The root a moved name belongs to: `records_root` for a record, `runtime_root` for every
732    /// other.
733    fn root_of(entry: &str) -> &'static str {
734        if RECORDS_DIR_WRITES.iter().any(|(name, _)| *name == entry) || entry == "eval.jsonl" {
735            RECORDS_DIR_ROOT
736        } else {
737            RUNTIME_DIR_ROOT
738        }
739    }
740
741    /// Functions that read the workdir's own copy of one of those names, each with the reason.
742    const MOVED_NAME_WORKDIR_READS: &[(&str, &str, &str)] = &[(
743        "runtime.rs",
744        "import_task_input",
745        "The one read of the operator's `task.md` from the workdir, at session start, copied \
746         into the runtime directory and never read again.",
747    )];
748
749    /// [`crate::workdir_fs::WorkdirRoot`] methods that read, list, inspect or remove.
750    const HELPER_READS: &[&str] = &[
751        ".read_file(",
752        ".read_to_string(",
753        ".list_dir(",
754        ".file_len(",
755        ".entry_kind(",
756        ".remove_file(",
757        ".remove_dir_all(",
758        ".open_beneath(",
759    ];
760
761    /// The entry name — the first path segment — a path argument stands for, or `None` when the
762    /// source text cannot say: a variable, a field access, a format string that opens with a
763    /// value rather than a name.
764    ///
765    /// A literal, a `format!` whose string opens with a literal segment or with a named constant
766    /// (`format!("{BLOB_DIR_NAME}/{sha}")`), and a named constant all resolve.
767    fn resolve_entry_name(argument: &str) -> Option<String> {
768        let argument = argument.trim_start_matches('&');
769        let argument = argument
770            .strip_prefix("format!(")
771            .map(str::trim_start)
772            .unwrap_or(argument);
773        let resolved = if let Some(rest) = argument.strip_prefix('"') {
774            let (literal, _) = rest.split_once('"')?;
775            match literal.strip_prefix('{') {
776                Some(inner) => resolve_constant(argument, inner.split_once('}')?.0)?,
777                None if literal.starts_with('{') => return None,
778                None => literal.split('{').next()?.to_string(),
779            }
780        } else {
781            resolve_constant(argument, argument.rsplit("::").next().unwrap_or(argument))?
782        };
783        let first = resolved.split('/').next()?.to_string();
784        (!first.is_empty()).then_some(first)
785    }
786
787    /// The value of the constant `name` in [`CONSTANTS`], or `None` when `name` is not a constant
788    /// name at all. Panics on a constant name the table does not pair with a value.
789    fn resolve_constant(argument: &str, name: &str) -> Option<String> {
790        let is_constant_name = !name.is_empty()
791            && name
792                .chars()
793                .all(|character| character.is_ascii_uppercase() || character == '_');
794        if !is_constant_name {
795            return None;
796        }
797        Some(
798            CONSTANTS
799                .iter()
800                .find(|(constant, _)| *constant == name)
801                .unwrap_or_else(|| {
802                    panic!(
803                        "a workdir write ({argument}) names the constant {name}, which this \
804                         test's CONSTANTS table does not pair with a value. Add \
805                         (\"{name}\", <path to the constant>) so a rename or a value change is \
806                         caught here rather than silently accepted."
807                    )
808                })
809                .1
810                .to_string(),
811        )
812    }
813
814    /// Every entry the runtime writes into a workdir is declared by [`DECLARED_WRITES`] or
815    /// exempted here with a written reason.
816    ///
817    /// The input is the runtime's own source text rather than a list somebody maintains: every
818    /// `workdir.join(...)` and the path of every [`HELPER_WRITES`] call. A developer who adds
819    /// either fails this test until they decide which it is.
820    #[test]
821    fn every_workdir_write_is_declared_or_exempted() {
822        let declared: Vec<&str> = DECLARED_WRITES
823            .iter()
824            .map(DeclaredWrite::first_segment)
825            .collect();
826
827        let mut scanned = 0usize;
828        let mut dynamic_rows_used = vec![false; DYNAMIC_EXEMPT.len()];
829        for (file, source) in SCANNED {
830            let text = production_text(source);
831            let arguments = workdir_join_arguments(text)
832                .into_iter()
833                .map(|argument| (String::new(), argument))
834                .chain(helper_calls(text, HELPER_WRITES));
835            for (receiver, argument) in arguments {
836                scanned += 1;
837                if OUTSIDE_WORKDIR_ROOTS.contains(&receiver.as_str()) {
838                    continue;
839                }
840                let runtime_dir = receiver == RUNTIME_DIR_ROOT;
841                let records_dir = receiver == RECORDS_DIR_ROOT;
842                assert!(
843                    records_dir || !names_a_moved_log(&argument),
844                    "{file} writes ({argument}) through `{receiver}`, a workdir root. The \
845                     session's own logs are records: they go through the records directory's root \
846                     (`records_root`), which no capsule command can write."
847                );
848                let Some(entry) = resolve_entry_name(&argument) else {
849                    let row =
850                        DYNAMIC_EXEMPT
851                            .iter()
852                            .position(|(exempt_file, exempt_argument, _)| {
853                                exempt_file == file && *exempt_argument == argument
854                            });
855                    assert!(
856                        row.is_some(),
857                        "{file} writes ({argument}) into a workdir, and the argument is neither \
858                         a string literal nor a named constant, so this test cannot say what it \
859                         names. Add ({file:?}, {argument:?}, <written reason>) to this test's \
860                         DYNAMIC_EXEMPT table."
861                    );
862                    dynamic_rows_used[row.unwrap()] = true;
863                    continue;
864                };
865                if runtime_dir {
866                    assert!(
867                        RUNTIME_DIR_WRITES.iter().any(|(name, _)| *name == entry),
868                        "{file} writes `{entry}` into the runtime directory, and \
869                         RUNTIME_DIR_WRITES does not declare it. Add (\"{entry}\", <what it is>)."
870                    );
871                    continue;
872                }
873                if records_dir {
874                    assert!(
875                        RECORDS_DIR_WRITES.iter().any(|(name, _)| *name == entry),
876                        "{file} writes `{entry}` into the records directory, and \
877                         RECORDS_DIR_WRITES does not declare it. Add (\"{entry}\", <what it is>)."
878                    );
879                    continue;
880                }
881                assert!(
882                    !MOVED_OUT_OF_THE_WORKDIR.contains(&entry.as_str()),
883                    "{file} writes `{entry}` through `{receiver}` ({argument}), a workdir root. \
884                     `{entry}` is a file the runtime owns or a record it keeps: it goes through \
885                     `{}`, which no capsule command can write.",
886                    root_of(&entry)
887                );
888                assert!(
889                    declared.contains(&entry.as_str())
890                        || EXEMPT.iter().any(|(name, _)| *name == entry),
891                    "{file} writes `{entry}` into a workdir, and no --explain-scope report \
892                     declares it. Either add an entry for `{entry}` to DECLARED_WRITES in \
893                     workdir_writes.rs, so a consumer diffing the workdir can subtract it, or add \
894                     it to this test's EXEMPT table with a written reason — a path the runtime \
895                     only reads is the usual one."
896                );
897            }
898        }
899
900        // A scanner that silently stopped matching would otherwise pass by finding nothing.
901        assert!(
902            scanned >= 40,
903            "the workdir write scan found only {scanned} occurrences across {} files, far below \
904             what the runtime has — the scanner is broken, not the runtime",
905            SCANNED.len()
906        );
907
908        for ((file, argument, _), used) in DYNAMIC_EXEMPT.iter().zip(dynamic_rows_used) {
909            assert!(
910                used,
911                "DYNAMIC_EXEMPT row ({file:?}, {argument:?}) matches no write in {file}; remove \
912                 it so the table names only what the runtime does"
913            );
914        }
915
916        for (name, _) in EXEMPT {
917            assert!(
918                !declared.contains(name),
919                "`{name}` is listed as exempt and is also declared — remove one"
920            );
921        }
922    }
923
924    /// No production read, listing, check or removal of a file the runtime owns goes through a
925    /// workdir root: every one reaches the runtime directory, whose files no capsule command can
926    /// change, apart from the rows of [`MOVED_NAME_WORKDIR_READS`].
927    #[test]
928    fn no_runtime_owned_file_is_reached_through_a_workdir_root() {
929        let mut checked = 0usize;
930        let mut rows_used = vec![false; MOVED_NAME_WORKDIR_READS.len()];
931        for (file, source) in SCANNED {
932            let text = production_text(source);
933            let methods: Vec<&str> = HELPER_READS.iter().chain(HELPER_WRITES).copied().collect();
934            for (start, receiver, argument) in helper_calls_at(text, &methods) {
935                let Some(entry) = resolve_entry_name(&argument) else {
936                    continue;
937                };
938                if !MOVED_OUT_OF_THE_WORKDIR.contains(&entry.as_str()) {
939                    continue;
940                }
941                checked += 1;
942                if receiver == root_of(&entry) {
943                    continue;
944                }
945                let function = enclosing_fn(text, start);
946                let row = MOVED_NAME_WORKDIR_READS
947                    .iter()
948                    .position(|(row_file, row_fn, _)| row_file == file && *row_fn == function);
949                let Some(row) = row else {
950                    panic!(
951                        "{file}: `{function}` reaches `{entry}` through `{receiver}` ({argument}), \
952                         a workdir root. `{entry}` is a file the runtime owns or a record it \
953                         keeps, and lives outside the workdir: go through `{}`.",
954                        root_of(&entry)
955                    );
956                };
957                rows_used[row] = true;
958            }
959        }
960        assert!(
961            checked >= 20,
962            "found only {checked} reaches of a runtime-owned file — the scanner is broken"
963        );
964        for ((file, function, _), used) in MOVED_NAME_WORKDIR_READS.iter().zip(rows_used) {
965            assert!(
966                used,
967                "MOVED_NAME_WORKDIR_READS row ({file:?}, {function:?}) matches no read; remove it"
968            );
969        }
970    }
971
972    /// Every source file with a production `workdir.join(...)` or [`HELPER_WRITES`] call is in
973    /// `SCANNED`.
974    ///
975    /// Without this, a new module writing into a workdir escapes the scans above.
976    #[test]
977    fn every_source_file_that_joins_a_workdir_is_scanned() {
978        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
979        let mut stack = vec![root.clone()];
980        let mut checked = 0usize;
981        while let Some(dir) = stack.pop() {
982            let entries = std::fs::read_dir(&dir)
983                .unwrap_or_else(|error| panic!("reading {}: {error}", dir.display()));
984            for entry in entries {
985                let path = entry.unwrap().path();
986                if path.is_dir() {
987                    stack.push(path);
988                    continue;
989                }
990                if path.extension().is_some_and(|extension| extension == "rs") {
991                    checked += 1;
992                    // This module's own prose and failure messages quote the patterns being
993                    // searched for, and `workdir_fs.rs` is the helper itself.
994                    if path
995                        .file_name()
996                        .is_some_and(|name| name == "workdir_writes.rs" || name == "workdir_fs.rs")
997                    {
998                        continue;
999                    }
1000                    let source = std::fs::read_to_string(&path).unwrap();
1001                    let text = production_text(&source);
1002                    if workdir_join_arguments(text).is_empty()
1003                        && helper_write_arguments(text).is_empty()
1004                    {
1005                        continue;
1006                    }
1007                    let relative = path
1008                        .strip_prefix(&root)
1009                        .unwrap()
1010                        .to_string_lossy()
1011                        .replace('\\', "/");
1012                    assert!(
1013                        SCANNED.iter().any(|(file, _)| *file == relative),
1014                        "src/{relative} writes into a workdir but is not in \
1015                         every_workdir_write_is_declared_or_exempted's SCANNED list, so nothing \
1016                         checks its entries against the declaration. Add \
1017                         (\"{relative}\", include_str!(\"{relative}\")) to it."
1018                    );
1019                }
1020            }
1021        }
1022        assert!(
1023            checked >= 40,
1024            "walked only {checked} source files under src/ — the walk is broken"
1025        );
1026    }
1027
1028    /// Every raw filesystem call in a scanned file, with the reason its path is not beneath the
1029    /// accessible workdir or the session directory, keyed by file and enclosing function.
1030    ///
1031    /// A row covers every listed call in that function. Anything a capsule's commands can reach
1032    /// goes through [`crate::workdir_fs::WorkdirRoot`] instead, and has no row here.
1033    const RAW_FS_EXEMPT: &[(&str, &str, &str)] = &[
1034        (
1035            "network_policy.rs",
1036            "resolve_scoped_dir",
1037            "Creates a declared `capabilities.filesystem.scope` beneath the root a WASI guest is \
1038             preopened at. The preopen itself is opened by path by `wasmtime-wasi`, so a root \
1039             here would hold nothing the guest then gets; the scope was validated as relative and \
1040             non-escaping at staging.",
1041        ),
1042        (
1043            "resource_plane.rs",
1044            "open_no_follow",
1045            "The resource plane's own reads of files the capsule exported, under its own \
1046             `SymlinkPolicy`; it resolves and confines every path itself and writes nothing.",
1047        ),
1048        (
1049            "resource_plane.rs",
1050            "list_export_files",
1051            "The resource plane's listing of a declared export root, on `open_no_follow`'s \
1052             terms.",
1053        ),
1054        (
1055            "retention.rs",
1056            "prune_formations",
1057            "A formation directory under `~/.murmur/formations` and its records subtree under `~/.murmur/records`, outside every workdir.",
1058        ),
1059        (
1060            "retention.rs",
1061            "prune_records",
1062            "A conversation record directory under `~/.murmur/conversations`, outside every workdir.",
1063        ),
1064        (
1065            "retention.rs",
1066            "remove_record",
1067            "A conversation record directory, on `prune_records`'s terms.",
1068        ),
1069        (
1070            "retention.rs",
1071            "list_records",
1072            "The conversation record root, on `prune_records`'s terms.",
1073        ),
1074        (
1075            "retention.rs",
1076            "child_dirs",
1077            "Directories beneath the conversation record root, on `prune_records`'s terms.",
1078        ),
1079        (
1080            "retention.rs",
1081            "locate_message",
1082            "A conversation record file, on `prune_records`'s terms.",
1083        ),
1084        (
1085            "retention.rs",
1086            "count_messages",
1087            "A conversation record file, on `prune_records`'s terms.",
1088        ),
1089        (
1090            "retention.rs",
1091            "stage_truncation",
1092            "A conversation record file being truncated, on `prune_records`'s terms.",
1093        ),
1094        (
1095            "retention.rs",
1096            "stage_with_mode",
1097            "`StagedRewrite`, the sibling-file rewrite of a file under `~/.murmur` — a conversation record or a private file — outside every workdir.",
1098        ),
1099        (
1100            "retention.rs",
1101            "commit",
1102            "`StagedRewrite`'s rename into place, on `stage_with_mode`'s terms.",
1103        ),
1104        (
1105            "retention.rs",
1106            "drop",
1107            "`StagedRewrite`'s removal of an uncommitted sibling file, on `stage_with_mode`'s terms.",
1108        ),
1109        (
1110            "runtime.rs",
1111            "check_resume_launchable",
1112            "A conversation record or harness-session file under `~/.murmur/conversations`, \
1113             outside every workdir.",
1114        ),
1115        (
1116            "runtime.rs",
1117            "copy_manifest_into_runtime_dir",
1118            "Reads the project's `murmur.yaml` from the manifest directory; the copy into the \
1119             runtime directory goes through `WorkdirRoot`.",
1120        ),
1121        (
1122            "runtime.rs",
1123            "load_local_skill_md",
1124            "A local skill source named relative to the manifest directory, read before any \
1125             workdir exists.",
1126        ),
1127        (
1128            "runtime.rs",
1129            "read_prompt_file",
1130            "A prompt file named relative to the manifest directory.",
1131        ),
1132        (
1133            "runtime.rs",
1134            "stage_session",
1135            "The operator's `--workdir` itself, checked by path before it is opened as the \
1136             accessible root, and `<manifest dir>/workdir/<session id>`, created before any root \
1137             exists. Nothing beneath either is touched by path.",
1138        ),
1139        (
1140            "sandbox.rs",
1141            "test_runtime_dir",
1142            "Test-only: creates a runtime directory's three subdirectories under a temporary \
1143             directory the test owns.",
1144        ),
1145        (
1146            "sandbox.rs",
1147            "build_sealed_root",
1148            "Host directories offered as the composed root's base, outside every workdir.",
1149        ),
1150        (
1151            "sandbox.rs",
1152            "open_o_path",
1153            "An `O_PATH` handle for a Landlock rule, opened from a host path the grant names; it \
1154             reads and writes nothing.",
1155        ),
1156        (
1157            "sandbox.rs",
1158            "resolve_landlock_grants_outside",
1159            "The ELF images of host binaries and libraries a shell grant names, read for their \
1160             dependencies.",
1161        ),
1162        (
1163            "sandbox.rs",
1164            "resolve_soname",
1165            "Host library search directories, outside every workdir.",
1166        ),
1167        (
1168            "sealed.rs",
1169            "inspect_installed_profile",
1170            "The host's installed AppArmor profile under `/etc/apparmor.d`.",
1171        ),
1172        (
1173            "sealed.rs",
1174            "inspect_profile_attachment",
1175            "`/proc/self/attr/current`, the runtime's own AppArmor attachment.",
1176        ),
1177        (
1178            "sealed.rs",
1179            "read_trimmed",
1180            "Host settings under `/sys` and `/proc`.",
1181        ),
1182    ];
1183
1184    /// `std::fs` / `tokio::fs` functions that touch a path, matched only straight after `fs::`
1185    /// so a [`crate::workdir_fs::WorkdirRoot`] method of the same name is never mistaken for one.
1186    const RAW_FS_FUNCTIONS: &[&str] = &[
1187        "write(",
1188        "copy(",
1189        "rename(",
1190        "create_dir_all(",
1191        "create_dir(",
1192        "set_permissions(",
1193        "remove_file(",
1194        "remove_dir_all(",
1195        "remove_dir(",
1196        "hard_link(",
1197        "symlink(",
1198        "read_to_string(",
1199        "read(",
1200    ];
1201
1202    /// Raw opens and existence checks, matched wherever they appear.
1203    const RAW_FS_CALLS: &[&str] = &[
1204        "File::create(",
1205        "File::options(",
1206        "OpenOptions::new(",
1207        "File::open(",
1208        "try_exists(",
1209    ];
1210
1211    /// Path predicates, matched unless the receiver is a metadata or file-type value, which has
1212    /// already been read from a descriptor or a directory entry.
1213    const RAW_FS_PREDICATES: &[&str] = &[".exists()", ".is_file()", ".is_dir()"];
1214
1215    /// The function the byte at `at` sits in: the nearest preceding `fn <name>`.
1216    fn enclosing_fn(source: &str, at: usize) -> &str {
1217        let before = &source[..at];
1218        let mut cursor = before.len();
1219        while let Some(index) = before[..cursor].rfind("fn ") {
1220            cursor = index;
1221            let boundary = index == 0
1222                || !before.as_bytes()[index - 1].is_ascii_alphanumeric()
1223                    && before.as_bytes()[index - 1] != b'_';
1224            let name: String = before[index + 3..]
1225                .chars()
1226                .take_while(|character| character.is_ascii_alphanumeric() || *character == '_')
1227                .collect();
1228            if boundary && !name.is_empty() {
1229                return &before[index + 3..index + 3 + name.len()];
1230            }
1231        }
1232        "<file level>"
1233    }
1234
1235    /// Every raw filesystem call in `source`, as `(enclosing fn, call)`.
1236    fn raw_fs_calls(source: &str) -> Vec<(String, String)> {
1237        let mut found = Vec::new();
1238        for function in RAW_FS_FUNCTIONS {
1239            let pattern = format!("fs::{function}");
1240            for (at, _) in source.match_indices(&pattern) {
1241                found.push((enclosing_fn(source, at).to_string(), pattern.clone()));
1242            }
1243        }
1244        for call in RAW_FS_CALLS {
1245            for (at, _) in source.match_indices(call) {
1246                found.push((enclosing_fn(source, at).to_string(), call.to_string()));
1247            }
1248        }
1249        for predicate in RAW_FS_PREDICATES {
1250            for (at, _) in source.match_indices(predicate) {
1251                let receiver = source[..at]
1252                    .trim_end()
1253                    .rsplit(|character: char| {
1254                        !(character.is_ascii_alphanumeric() || "_().".contains(character))
1255                    })
1256                    .next()
1257                    .unwrap_or_default();
1258                let last = receiver
1259                    .trim_end_matches("()")
1260                    .rsplit('.')
1261                    .next()
1262                    .unwrap_or_default();
1263                if matches!(last, "meta" | "metadata" | "file_type") {
1264                    continue;
1265                }
1266                found.push((enclosing_fn(source, at).to_string(), predicate.to_string()));
1267            }
1268        }
1269        found
1270    }
1271
1272    /// No scanned file touches a path with a raw filesystem call unless [`RAW_FS_EXEMPT`] says
1273    /// why that path is outside every workdir — and no row of that table outlives the call it
1274    /// excuses.
1275    ///
1276    /// A raw call resolves every component of its path, so a link the capsule planted anywhere on
1277    /// the way redirects it; [`crate::workdir_fs::WorkdirRoot`] is the one way the runtime
1278    /// touches a path beneath a workdir.
1279    #[test]
1280    fn no_workdir_file_bypasses_the_no_follow_helper() {
1281        let mut rows_used = vec![false; RAW_FS_EXEMPT.len()];
1282        for (file, source) in SCANNED {
1283            for (function, call) in raw_fs_calls(production_text(source)) {
1284                let row = RAW_FS_EXEMPT
1285                    .iter()
1286                    .position(|(exempt_file, exempt_fn, _)| {
1287                        exempt_file == file && *exempt_fn == function
1288                    });
1289                let Some(row) = row else {
1290                    panic!(
1291                        "{file}: `{function}` calls `{call}` directly. A path beneath the \
1292                         accessible workdir or the session directory goes through WorkdirRoot \
1293                         (crate::workdir_fs), which follows no link; a path outside every \
1294                         workdir gets a ({file:?}, {function:?}, <why it is not beneath a \
1295                         workdir>) row in RAW_FS_EXEMPT."
1296                    );
1297                };
1298                rows_used[row] = true;
1299            }
1300        }
1301        for ((file, function, _), used) in RAW_FS_EXEMPT.iter().zip(rows_used) {
1302            assert!(
1303                used,
1304                "RAW_FS_EXEMPT row ({file:?}, {function:?}) matches no raw filesystem call — \
1305                 the call is gone or moved; remove or re-key the row"
1306            );
1307        }
1308    }
1309
1310    #[test]
1311    fn the_raw_call_scanner_names_the_function_and_the_call() {
1312        let source = "fn staged() {\n    let _ = std::fs::write(self.workdir.join(\"plans\").join(\"x\"), b\"\");\n}\nfn helper(root: &WorkdirRoot) {\n    root.write_file(\"plans/x\", b\"\", 0o644);\n    if metadata.is_file() {}\n}\n";
1313        assert_eq!(
1314            raw_fs_calls(source),
1315            vec![("staged".to_string(), "fs::write(".to_string())]
1316        );
1317        assert_eq!(
1318            helper_write_arguments(source),
1319            vec!["\"plans/x\"".to_string()]
1320        );
1321        assert_eq!(resolve_entry_name("\"plans/x\"").as_deref(), Some("plans"));
1322        assert_eq!(
1323            resolve_entry_name("format!(\"{BLOB_DIR_NAME}/{sha256}\")").as_deref(),
1324            Some("blobs")
1325        );
1326        assert_eq!(
1327            resolve_entry_name("format!(\"tools/{name}/x\")").as_deref(),
1328            Some("tools")
1329        );
1330        assert_eq!(
1331            resolve_entry_name("CHILDREN_DIR").as_deref(),
1332            Some(".murmur")
1333        );
1334        assert_eq!(resolve_entry_name("&rel"), None);
1335        assert_eq!(resolve_entry_name("format!(\"{}/x\", a)"), None);
1336    }
1337
1338    /// Sealed is the one condition that changes the reported set, and it adds nothing a consumer
1339    /// diffing the accessible workdir's top level would see.
1340    #[test]
1341    fn the_sealed_tier_adds_exactly_the_tmp_store() {
1342        let scoped = runtime_writes(EnforcementTier::KernelFull);
1343        let sealed = runtime_writes(EnforcementTier::KernelSealed);
1344
1345        assert!(
1346            scoped.iter().all(|write| sealed.contains(write)),
1347            "the sealed declaration must be a superset of the scoped one"
1348        );
1349        let added: Vec<&RuntimeWriteReport> = sealed
1350            .iter()
1351            .filter(|write| !scoped.contains(write))
1352            .collect();
1353        assert_eq!(
1354            added
1355                .iter()
1356                .map(|write| write.path.as_str())
1357                .collect::<Vec<_>>(),
1358            vec![".murmur/<session-id>/.mur-tmp"]
1359        );
1360        for write in added {
1361            assert_eq!(write.condition, RuntimeWriteCondition::Sealed);
1362            assert_eq!(write.scope, RuntimeWriteScope::Session);
1363            assert_eq!(write.kind, RuntimeWriteKind::Directory);
1364        }
1365    }
1366
1367    /// The files the runtime owns are in the runtime directory, so the workdir declaration names
1368    /// none of them, on any tier.
1369    #[test]
1370    fn runtime_writes_lists_none_of_the_runtime_directory_files() {
1371        for tier in [
1372            EnforcementTier::KernelSealed,
1373            EnforcementTier::KernelFull,
1374            EnforcementTier::KernelSeccompOnly,
1375            EnforcementTier::EnvironmentOnly,
1376        ] {
1377            for write in runtime_writes(tier) {
1378                let last = write.path.rsplit('/').next().unwrap_or_default();
1379                assert!(
1380                    !MOVED_OUT_OF_THE_WORKDIR.contains(&last),
1381                    "{} is declared as a workdir write",
1382                    write.path
1383                );
1384            }
1385        }
1386    }
1387
1388    /// Every non-sealed tier reports the same set, so a host's Landlock or seccomp availability
1389    /// cannot quietly change what a consumer subtracts.
1390    #[test]
1391    fn only_the_sealed_tier_changes_the_declared_set() {
1392        let baseline = runtime_writes(EnforcementTier::EnvironmentOnly);
1393        assert_eq!(baseline, runtime_writes(EnforcementTier::KernelSeccompOnly));
1394        assert_eq!(baseline, runtime_writes(EnforcementTier::KernelFull));
1395        assert!(baseline
1396            .iter()
1397            .all(|write| write.condition != RuntimeWriteCondition::Sealed));
1398    }
1399
1400    /// Serialized rows carry exactly the four documented keys, in declaration order, with the
1401    /// kebab-case wire values.
1402    ///
1403    /// The order is asserted off `to_string` rather than off `to_value`: `serde_json`'s `Value`
1404    /// holds a map that sorts its keys, so a round trip through it would prove nothing about the
1405    /// bytes `--explain-scope --json` and `trace.jsonl` actually carry.
1406    #[test]
1407    fn a_serialized_row_carries_exactly_path_kind_scope_and_condition() {
1408        for write in runtime_writes(EnforcementTier::KernelSealed) {
1409            let line = serde_json::to_string(&write).unwrap();
1410            assert_eq!(
1411                line,
1412                format!(
1413                    r#"{{"path":"{}","kind":"{}","scope":"{}","condition":"{}"}}"#,
1414                    write.path,
1415                    write.kind.wire_name(),
1416                    write.scope.wire_name(),
1417                    write.condition.wire_name()
1418                )
1419            );
1420        }
1421    }
1422
1423    /// A session-scoped entry is reported under the one prefix a consumer excludes, and an
1424    /// accessible one never is.
1425    #[test]
1426    fn session_scoped_paths_carry_the_murmur_prefix_and_the_literal_session_id() {
1427        for write in runtime_writes(EnforcementTier::KernelSealed) {
1428            match write.scope {
1429                RuntimeWriteScope::Session => {
1430                    let prefix = format!(".murmur/{SESSION_ID_PLACEHOLDER}/");
1431                    assert!(
1432                        write.path.starts_with(&prefix),
1433                        "{} is session-scoped but not under {prefix}",
1434                        write.path
1435                    );
1436                }
1437                RuntimeWriteScope::Accessible => assert!(
1438                    !write
1439                        .path
1440                        .starts_with(&format!(".murmur/{SESSION_ID_PLACEHOLDER}/")),
1441                    "{} is accessible-scoped but sits inside the session directory",
1442                    write.path
1443                ),
1444            }
1445            assert!(
1446                !write.path.contains("ses_"),
1447                "{} carries a real session id where the placeholder belongs",
1448                write.path
1449            );
1450        }
1451    }
1452
1453    /// No two rows name the same path, so a subtraction cannot silently depend on which one it
1454    /// matched.
1455    #[test]
1456    fn no_declared_path_is_reported_twice() {
1457        let writes = runtime_writes(EnforcementTier::KernelSealed);
1458        let mut paths: Vec<&str> = writes.iter().map(|write| write.path.as_str()).collect();
1459        paths.sort_unstable();
1460        let count = paths.len();
1461        paths.dedup();
1462        assert_eq!(paths.len(), count, "a path is declared twice");
1463    }
1464
1465    #[test]
1466    fn the_rendering_names_every_declared_path_and_the_placeholder_rule() {
1467        let writes = runtime_writes(EnforcementTier::KernelSealed);
1468        let rendered = render_runtime_writes(&writes);
1469        assert!(rendered.contains("`<session-id>` is a literal segment"));
1470        for write in &writes {
1471            assert!(
1472                rendered.contains(&write.path),
1473                "{} is missing from the rendering",
1474                write.path
1475            );
1476        }
1477        assert!(rendered.ends_with('\n'));
1478    }
1479}