Skip to main content

capsule_runtime/
workdir_writes.rs

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