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        // One `0700` directory per harness run, holding the files the process driver asked for.
299        // Removed when the run ends, so it is present only while a `transport: process` harness
300        // is alive.
301        entry: ".process-driver",
302        kind: RuntimeWriteKind::Directory,
303        scope: RuntimeWriteScope::Session,
304        condition: RuntimeWriteCondition::AgentSession,
305    },
306    DeclaredWrite {
307        entry: "MURMUR.md",
308        kind: RuntimeWriteKind::File,
309        scope: RuntimeWriteScope::Session,
310        condition: RuntimeWriteCondition::AgentSession,
311    },
312    DeclaredWrite {
313        // One `plan-<n>.json` per submitted plan.
314        entry: "plans",
315        kind: RuntimeWriteKind::Directory,
316        scope: RuntimeWriteScope::Session,
317        condition: RuntimeWriteCondition::AgentSession,
318    },
319    DeclaredWrite {
320        // `$HOME` for every shell and native tool subprocess. Reached only through the variable;
321        // nothing outside the runtime names the literal path.
322        entry: ".capsule-home",
323        kind: RuntimeWriteKind::Directory,
324        scope: RuntimeWriteScope::Session,
325        condition: RuntimeWriteCondition::Shell,
326    },
327    DeclaredWrite {
328        // The bind source the composed root mounts at `/tmp`. The capsule sees `/tmp` and never a
329        // workdir path.
330        entry: ".mur-tmp",
331        kind: RuntimeWriteKind::Directory,
332        scope: RuntimeWriteScope::Session,
333        condition: RuntimeWriteCondition::Sealed,
334    },
335    DeclaredWrite {
336        // The bind sources for the composed root's synthetic `/etc/passwd` and `/etc/group`.
337        entry: ".mur-etc",
338        kind: RuntimeWriteKind::Directory,
339        scope: RuntimeWriteScope::Session,
340        condition: RuntimeWriteCondition::Sealed,
341    },
342];
343
344/// Every path the runtime writes inside this launch's workdir, in declaration order.
345///
346/// Pure and filesystem-free: `mur run --explain-scope` returns before a workdir exists, so a
347/// declaration that touched one would make the diagnostic create the thing it describes.
348///
349/// `tier` is the tier the session would actually install — [`crate::sandbox::applied_tier`], the
350/// host's own tier capped by the declared floor — never the declared class on its own. A capsule
351/// declaring `scoped` on a sealed-capable host composes no root; under an unmet `sealed` floor the
352/// launch refuses anyway. Either way the honest answer is the set the mechanism that will really
353/// run writes.
354pub(crate) fn runtime_writes(tier: EnforcementTier) -> Vec<RuntimeWriteReport> {
355    let sealed = tier == EnforcementTier::KernelSealed;
356    DECLARED_WRITES
357        .iter()
358        .filter(|declared| sealed || declared.condition != RuntimeWriteCondition::Sealed)
359        .map(DeclaredWrite::report)
360        .collect()
361}
362
363/// The `Runtime writes` block of `mur run --explain-scope`'s human rendering.
364///
365/// Ends with a newline. Shared with nothing else today, but kept beside the declaration for the
366/// reason [`crate::containment::render_read_only`] is kept beside its own: a second renderer is a
367/// second place for the wording to drift from the data.
368#[must_use]
369pub fn render_runtime_writes(writes: &[RuntimeWriteReport]) -> String {
370    let mut out = String::new();
371    out.push_str(
372        "  paths are relative to the accessible workdir; `<session-id>` is a literal segment, \
373         replaced by the run's own session id\n",
374    );
375    if writes.is_empty() {
376        out.push_str("  runtime writes: <none>\n");
377        return out;
378    }
379    out.push_str("  runtime writes:\n");
380    for write in writes {
381        out.push_str(&format!(
382            "    - {} ({}, {}, {})\n",
383            write.path,
384            write.kind.wire_name(),
385            write.scope.wire_name(),
386            write.condition.wire_name()
387        ));
388    }
389    out
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    /// The source files carrying a production `workdir.join(...)`, each scanned by
397    /// [`every_workdir_write_is_declared_or_exempted`].
398    ///
399    /// `plan.rs` has no production occurrence today and is scanned anyway, so a plan step that
400    /// grows one is covered without anybody remembering to widen this list.
401    const SCANNED: &[(&str, &str)] = &[
402        ("agent.rs", include_str!("agent.rs")),
403        ("agent/inventory.rs", include_str!("agent/inventory.rs")),
404        ("agent/process.rs", include_str!("agent/process.rs")),
405        ("child_launch.rs", include_str!("child_launch.rs")),
406        ("delegation.rs", include_str!("delegation.rs")),
407        ("hooks.rs", include_str!("hooks.rs")),
408        ("murmur_md.rs", include_str!("murmur_md.rs")),
409        ("otel.rs", include_str!("otel.rs")),
410        ("plan.rs", include_str!("plan.rs")),
411        ("protected_paths.rs", include_str!("protected_paths.rs")),
412        ("recipes.rs", include_str!("recipes.rs")),
413        ("resource_plane.rs", include_str!("resource_plane.rs")),
414        ("runtime.rs", include_str!("runtime.rs")),
415        ("sandbox.rs", include_str!("sandbox.rs")),
416        ("sealed.rs", include_str!("sealed.rs")),
417        ("shell.rs", include_str!("shell.rs")),
418        ("tool_annotations.rs", include_str!("tool_annotations.rs")),
419        ("trace.rs", include_str!("trace.rs")),
420        ("trace_blobs.rs", include_str!("trace_blobs.rs")),
421    ];
422
423    /// Constant names a `workdir.join(...)` may name, paired with the constant itself.
424    ///
425    /// The pairing is the point: the scanner reads the identifier out of source text, and this
426    /// table is what turns that text into the value it stands for, so renaming the constant or
427    /// changing what it holds is caught rather than silently accepted.
428    const CONSTANTS: &[(&str, &str)] = &[
429        ("MANIFEST_FILENAME", murmur_artifact::MANIFEST_FILENAME),
430        ("COMPLETION_FILE", crate::delegation::COMPLETION_FILE),
431        ("BLOB_DIR_NAME", crate::trace_blobs::BLOB_DIR_NAME),
432        ("SEALED_TMP_DIR_NAME", crate::sealed::SEALED_TMP_DIR_NAME),
433        (
434            "SEALED_ETC_STAGING_DIR_NAME",
435            crate::sealed::SEALED_ETC_STAGING_DIR_NAME,
436        ),
437        (
438            "SYNTHETIC_HOME_DIR_NAME",
439            crate::shell::SYNTHETIC_HOME_DIR_NAME,
440        ),
441        ("FILES_DIR_NAME", crate::agent::process::FILES_DIR_NAME),
442    ];
443
444    /// Entry names the scanner resolves that are deliberately not declared, each with the reason.
445    const EXEMPT: &[(&str, &str)] = &[
446        (
447            "input.txt",
448            "Read, never written: the fallback name `read_task` tries after `task.md`. Source \
449             text cannot tell a read from a write, so a path the runtime only ever opens for \
450             reading is exempted here rather than declared as something the runtime produces.",
451        ),
452        (
453            "package.json",
454            "Read, never written: the recipe detector opens it to learn which scripts a workdir \
455             already declares. Exempt on `input.txt`'s terms.",
456        ),
457    ];
458
459    /// `workdir.join(...)` calls whose argument is not a literal or a named constant, each with
460    /// the reason no entry name can be resolved from the source text.
461    const DYNAMIC_EXEMPT: &[(&str, &str, &str)] = &[
462        (
463            "protected_paths.rs",
464            "&rule.relative",
465            "A `capabilities.filesystem.read_only` path from the manifest, resolved to decide \
466             whether a call is refused. The runtime reads it and writes nothing.",
467        ),
468        (
469            "protected_paths.rs",
470            "&lexical",
471            "The lexically normalised form of a path a tool call named, resolved for the same \
472             refusal decision.",
473        ),
474        (
475            "recipes.rs",
476            "dir",
477            "A directory name from the recipe detector's own candidate list, opened to see \
478             whether the workdir has it.",
479        ),
480        (
481            "resource_plane.rs",
482            "declared_root",
483            "The `exports.files` / `exports.peer_files` root a manifest declares. The plane \
484             serves what the capsule put there; it creates nothing of its own.",
485        ),
486        (
487            "resource_plane.rs",
488            "&export.root",
489            "The same declared root, resolved when a request is served. Exempt on \
490             `declared_root`'s terms.",
491        ),
492        (
493            "runtime.rs",
494            "&relative",
495            "A resource path a tool call named, resolved to check it is inside the workdir \
496             before the file is read.",
497        ),
498        (
499            "runtime.rs",
500            "&stored_path",
501            "Where a redeemed peer handle's bytes go, composed by `peer_handoff::stored_path_for` \
502             under `peer-in/`. Declared through that directory's own entry, which is the name a \
503             consumer excludes.",
504        ),
505        (
506            "shell.rs",
507            "&output_path",
508            "A demoted command's own log path, `logs/<work-id>.log`, composed by \
509             `detached::output_path_for` and joined here only to measure the file's size. \
510             Declared through the `logs` entry.",
511        ),
512        (
513            "delegation.rs",
514            "format!( \".{COMPLETION_FILE}.{}.tmp\", uuid::Uuid::now_v7().simple() )",
515            "The temporary file `completion.json` is written through and renamed from. It exists \
516             only between two syscalls in the same function, so a consumer diffing a finished \
517             run never sees it.",
518        ),
519    ];
520
521    /// Production text of `source`: everything above its test module.
522    ///
523    /// A test fixture writing `workdir.join("scratch")` says nothing about what the runtime
524    /// produces, so scanning below this marker would fill the tables with names no capsule ever
525    /// sees.
526    fn production_text(source: &str) -> &str {
527        match source.find("#[cfg(test)]\nmod tests {") {
528            Some(index) => &source[..index],
529            None => source,
530        }
531    }
532
533    /// Every `workdir.join(<arg>)` in `source`, as the argument's source text.
534    ///
535    /// The receiver is matched on its `workdir` suffix rather than by name, which is what makes
536    /// `accessible_workdir.join(`, `session_workdir.join(`, `child_workdir.join(` and
537    /// `parent_accessible_workdir\n    .join(` all one case.
538    fn workdir_join_arguments(source: &str) -> Vec<String> {
539        let mut found = Vec::new();
540        let bytes = source.as_bytes();
541        let mut cursor = 0;
542        while let Some(offset) = source[cursor..].find(".join(") {
543            let start = cursor + offset;
544            cursor = start + ".join(".len();
545            if !source[..start].trim_end().ends_with("workdir") {
546                continue;
547            }
548            let mut depth = 1usize;
549            let mut index = cursor;
550            while index < bytes.len() && depth > 0 {
551                match bytes[index] {
552                    b'(' => depth += 1,
553                    b')' => depth -= 1,
554                    _ => {}
555                }
556                index += 1;
557            }
558            assert!(
559                depth == 0,
560                "unbalanced parentheses after a join into a workdir"
561            );
562            // Collapsed to one line: an argument's line breaks and indentation are `rustfmt`'s
563            // business, and a table row that had to reproduce them would fail on a reflow.
564            found.push(
565                source[cursor..index - 1]
566                    .split_whitespace()
567                    .collect::<Vec<_>>()
568                    .join(" "),
569            );
570        }
571        found
572    }
573
574    /// The entry name a `workdir.join(...)` argument stands for, or `None` when the source text
575    /// cannot say — a variable, a field access, a formatted string.
576    fn resolve_entry_name(argument: &str) -> Option<String> {
577        if let Some(rest) = argument.strip_prefix('"') {
578            return rest.split_once('"').map(|(literal, _)| literal.to_string());
579        }
580        let last = argument.rsplit("::").next().unwrap_or(argument);
581        let is_constant_name = !last.is_empty()
582            && last
583                .chars()
584                .all(|character| character.is_ascii_uppercase() || character == '_');
585        if !is_constant_name {
586            return None;
587        }
588        Some(
589            CONSTANTS
590                .iter()
591                .find(|(name, _)| *name == last)
592                .unwrap_or_else(|| {
593                    panic!(
594                        "a workdir.join({argument}) names the constant {last}, which this test's \
595                         CONSTANTS table does not pair with a value. Add \
596                         (\"{last}\", <path to the constant>) so a rename or a value change is \
597                         caught here rather than silently accepted."
598                    )
599                })
600                .1
601                .to_string(),
602        )
603    }
604
605    /// Every entry the runtime writes into a workdir is declared by [`DECLARED_WRITES`] or
606    /// exempted here with a written reason.
607    ///
608    /// The input is the runtime's own source text rather than a list somebody maintains, so a
609    /// developer who adds a `workdir.join("...")` fails this test until they decide which it is.
610    #[test]
611    fn every_workdir_write_is_declared_or_exempted() {
612        let declared: Vec<&str> = DECLARED_WRITES
613            .iter()
614            .map(DeclaredWrite::first_segment)
615            .collect();
616
617        let mut scanned = 0usize;
618        for (file, source) in SCANNED {
619            for argument in workdir_join_arguments(production_text(source)) {
620                scanned += 1;
621                let Some(entry) = resolve_entry_name(&argument) else {
622                    assert!(
623                        DYNAMIC_EXEMPT
624                            .iter()
625                            .any(|(exempt_file, exempt_argument, _)| exempt_file == file
626                                && *exempt_argument == argument),
627                        "{file} writes workdir.join({argument}) into a workdir, and the argument \
628                         is neither a string literal nor a named constant, so this test cannot \
629                         say what it names. Add ({file:?}, {argument:?}, <written reason>) to \
630                         this test's DYNAMIC_EXEMPT table."
631                    );
632                    continue;
633                };
634                assert!(
635                    declared.contains(&entry.as_str())
636                        || EXEMPT.iter().any(|(name, _)| *name == entry),
637                    "{file} writes `{entry}` into a workdir, and no --explain-scope report \
638                     declares it. Either add an entry for `{entry}` to DECLARED_WRITES in \
639                     workdir_writes.rs, so a consumer diffing the workdir can subtract it, or add \
640                     it to this test's EXEMPT table with a written reason — a path the runtime \
641                     only reads is the usual one."
642                );
643            }
644        }
645
646        // A scanner that silently stopped matching would otherwise pass by finding nothing.
647        assert!(
648            scanned >= 50,
649            "the workdir.join scan found only {scanned} occurrences across {} files, far below \
650             what the runtime has — the scanner is broken, not the runtime",
651            SCANNED.len()
652        );
653
654        for (name, _) in EXEMPT {
655            assert!(
656                !declared.contains(name),
657                "`{name}` is listed as exempt and is also declared — remove one"
658            );
659        }
660    }
661
662    /// Every source file with a production `workdir.join(...)` is in `SCANNED`.
663    ///
664    /// Without this, a new module writing into a workdir would be invisible to the scan above and
665    /// the declaration would go quietly stale, which is the exact failure this whole module
666    /// exists to close.
667    #[test]
668    fn every_source_file_that_joins_a_workdir_is_scanned() {
669        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
670        let mut stack = vec![root.clone()];
671        let mut checked = 0usize;
672        while let Some(dir) = stack.pop() {
673            let entries = std::fs::read_dir(&dir)
674                .unwrap_or_else(|error| panic!("reading {}: {error}", dir.display()));
675            for entry in entries {
676                let path = entry.unwrap().path();
677                if path.is_dir() {
678                    stack.push(path);
679                    continue;
680                }
681                if path.extension().is_some_and(|extension| extension == "rs") {
682                    checked += 1;
683                    // This module's own prose and failure messages quote the pattern being
684                    // searched for; it declares the writes rather than performing any.
685                    if path
686                        .file_name()
687                        .is_some_and(|name| name == "workdir_writes.rs")
688                    {
689                        continue;
690                    }
691                    let source = std::fs::read_to_string(&path).unwrap();
692                    if workdir_join_arguments(production_text(&source)).is_empty() {
693                        continue;
694                    }
695                    let relative = path
696                        .strip_prefix(&root)
697                        .unwrap()
698                        .to_string_lossy()
699                        .replace('\\', "/");
700                    assert!(
701                        SCANNED.iter().any(|(file, _)| *file == relative),
702                        "src/{relative} writes into a workdir but is not in \
703                         every_workdir_write_is_declared_or_exempted's SCANNED list, so nothing \
704                         checks its entries against the declaration. Add \
705                         (\"{relative}\", include_str!(\"{relative}\")) to it."
706                    );
707                }
708            }
709        }
710        assert!(
711            checked >= 40,
712            "walked only {checked} source files under src/ — the walk is broken"
713        );
714    }
715
716    /// Sealed is the one condition that changes the reported set, and it adds nothing a consumer
717    /// diffing the accessible workdir's top level would see.
718    #[test]
719    fn the_sealed_tier_adds_exactly_the_two_sealed_session_entries() {
720        let scoped = runtime_writes(EnforcementTier::KernelFull);
721        let sealed = runtime_writes(EnforcementTier::KernelSealed);
722
723        assert!(
724            scoped.iter().all(|write| sealed.contains(write)),
725            "the sealed declaration must be a superset of the scoped one"
726        );
727        let added: Vec<&RuntimeWriteReport> = sealed
728            .iter()
729            .filter(|write| !scoped.contains(write))
730            .collect();
731        assert_eq!(
732            added
733                .iter()
734                .map(|write| write.path.as_str())
735                .collect::<Vec<_>>(),
736            vec![
737                ".murmur/<session-id>/.mur-tmp",
738                ".murmur/<session-id>/.mur-etc"
739            ]
740        );
741        for write in added {
742            assert_eq!(write.condition, RuntimeWriteCondition::Sealed);
743            assert_eq!(write.scope, RuntimeWriteScope::Session);
744            assert_eq!(write.kind, RuntimeWriteKind::Directory);
745        }
746    }
747
748    /// Every non-sealed tier reports the same set, so a host's Landlock or seccomp availability
749    /// cannot quietly change what a consumer subtracts.
750    #[test]
751    fn only_the_sealed_tier_changes_the_declared_set() {
752        let baseline = runtime_writes(EnforcementTier::EnvironmentOnly);
753        assert_eq!(baseline, runtime_writes(EnforcementTier::KernelSeccompOnly));
754        assert_eq!(baseline, runtime_writes(EnforcementTier::KernelFull));
755        assert!(baseline
756            .iter()
757            .all(|write| write.condition != RuntimeWriteCondition::Sealed));
758    }
759
760    /// Serialized rows carry exactly the four documented keys, in declaration order, with the
761    /// kebab-case wire values.
762    ///
763    /// The order is asserted off `to_string` rather than off `to_value`: `serde_json`'s `Value`
764    /// holds a map that sorts its keys, so a round trip through it would prove nothing about the
765    /// bytes `--explain-scope --json` and `trace.jsonl` actually carry.
766    #[test]
767    fn a_serialized_row_carries_exactly_path_kind_scope_and_condition() {
768        for write in runtime_writes(EnforcementTier::KernelSealed) {
769            let line = serde_json::to_string(&write).unwrap();
770            assert_eq!(
771                line,
772                format!(
773                    r#"{{"path":"{}","kind":"{}","scope":"{}","condition":"{}"}}"#,
774                    write.path,
775                    write.kind.wire_name(),
776                    write.scope.wire_name(),
777                    write.condition.wire_name()
778                )
779            );
780        }
781    }
782
783    /// A session-scoped entry is reported under the one prefix a consumer excludes, and an
784    /// accessible one never is.
785    #[test]
786    fn session_scoped_paths_carry_the_murmur_prefix_and_the_literal_session_id() {
787        for write in runtime_writes(EnforcementTier::KernelSealed) {
788            match write.scope {
789                RuntimeWriteScope::Session => {
790                    let prefix = format!(".murmur/{SESSION_ID_PLACEHOLDER}/");
791                    assert!(
792                        write.path.starts_with(&prefix),
793                        "{} is session-scoped but not under {prefix}",
794                        write.path
795                    );
796                }
797                RuntimeWriteScope::Accessible => assert!(
798                    !write
799                        .path
800                        .starts_with(&format!(".murmur/{SESSION_ID_PLACEHOLDER}/")),
801                    "{} is accessible-scoped but sits inside the session directory",
802                    write.path
803                ),
804            }
805            assert!(
806                !write.path.contains("ses_"),
807                "{} carries a real session id where the placeholder belongs",
808                write.path
809            );
810        }
811    }
812
813    /// No two rows name the same path, so a subtraction cannot silently depend on which one it
814    /// matched.
815    #[test]
816    fn no_declared_path_is_reported_twice() {
817        let writes = runtime_writes(EnforcementTier::KernelSealed);
818        let mut paths: Vec<&str> = writes.iter().map(|write| write.path.as_str()).collect();
819        paths.sort_unstable();
820        let count = paths.len();
821        paths.dedup();
822        assert_eq!(paths.len(), count, "a path is declared twice");
823    }
824
825    #[test]
826    fn the_rendering_names_every_declared_path_and_the_placeholder_rule() {
827        let writes = runtime_writes(EnforcementTier::KernelSealed);
828        let rendered = render_runtime_writes(&writes);
829        assert!(rendered.contains("`<session-id>` is a literal segment"));
830        for write in &writes {
831            assert!(
832                rendered.contains(&write.path),
833                "{} is missing from the rendering",
834                write.path
835            );
836        }
837        assert!(rendered.ends_with('\n'));
838    }
839}