Skip to main content

capsule_runtime/
records_dir.rs

1//! The session's records directory: every record the runtime writes about a session, outside the
2//! workdir the capsule's commands can write, and kept after the session ends.
3//!
4//! ```text
5//! ~/.murmur/records/                              0700
6//! └── <workdir root without its leading />/       0700, each component
7//!     └── <session-id>/                           0700, created at staging, kept after the session
8//!         ├── trace.jsonl
9//!         ├── blobs/<sha256>
10//!         ├── logs/bootstrap.log | otel.log | hook-<name>.log
11//!         ├── completion.json                     a delegated child's outcome
12//!         └── hooks/                              `.` of every hook declared
13//!                                                 `capabilities.filesystem.records: true`
14//! ```
15//!
16//! The group a session's directory sits in is its **workdir root**: the directory passed to
17//! `mur run --workdir`, otherwise the directory holding the accessible workdir
18//! (`<manifest-dir>/workdir`). Neither is beneath the accessible workdir, so no capsule command
19//! can rename or replace it, and every reader given the same directory computes the same group.
20//!
21//! The Landlock rule and sealed bind that keep it read-only to commands are the runtime
22//! directory's ([`crate::runtime_dir`]); [`crate::containment::records_dir_report`] says per tier
23//! what holds. Unlike the runtime directory it is not removed when the session ends;
24//! `trace.retain` ([`crate::retention::prune_sessions`]) prunes it.
25
26use std::path::{Component, Path, PathBuf};
27
28use crate::murmur_home::HomeUnusable;
29use crate::protected_paths::canonicalize_existing_prefix;
30use crate::workdir_fs::WorkdirRoot;
31
32/// `~/.murmur/records`, relative to the murmur home.
33pub const RECORDS_DIR_NAME: &str = "records";
34
35/// The variable naming the records directory in every shell command's, plan shell step's,
36/// native tool's and `transport: process` harness's environment. Runtime-owned like
37/// [`crate::runtime_dir::RUNTIME_DIR_ENV`]: no manifest, override or host value can set it.
38pub const RECORDS_DIR_ENV: &str = "MURMUR_RECORDS_DIR";
39
40/// The directory beneath `<session-id>/` a hook declared `capabilities.filesystem.records: true`
41/// is preopened at as `"."`, read-write.
42pub const HOOKS_SUBDIR: &str = "hooks";
43
44/// The mode every directory beneath `~/.murmur/records` is created at.
45const PRIVATE_DIR_MODE: u32 = crate::murmur_home::MURMUR_HOME_DIR_MODE;
46
47/// `~/.murmur/records`, decided without creating anything.
48pub fn records_dir_root() -> Result<PathBuf, HomeUnusable> {
49    Ok(crate::murmur_home::usable_murmur_home()?.join(RECORDS_DIR_NAME))
50}
51
52/// The directory every session of `workdir_root` keeps its records in:
53/// `~/.murmur/records/<workdir_root, absolute and canonical, without its leading />`. Creates
54/// nothing.
55///
56/// `workdir_root` is made absolute against the current directory and canonicalized through its
57/// nearest existing ancestor, the rest appended, so the runtime at staging and a reader given the
58/// same directory compute the same group whether or not it exists yet.
59pub fn records_group(workdir_root: &Path) -> Result<PathBuf, HomeUnusable> {
60    Ok(records_group_under(
61        &crate::murmur_home::usable_murmur_home()?,
62        workdir_root,
63    ))
64}
65
66/// [`records_group`] beneath `murmur_home` instead of the process's `~/.murmur`, for a caller
67/// that knows the home a session ran under without it being this process's `HOME`.
68pub fn records_group_under(murmur_home: &Path, workdir_root: &Path) -> PathBuf {
69    murmur_home
70        .join(RECORDS_DIR_NAME)
71        .join(mirrored(workdir_root))
72}
73
74/// `workdir_root` as a relative path beneath `~/.murmur/records`: absolute, canonical through its
75/// nearest existing ancestor, and with no `.` or `..` left in it, so the mirror never names a
76/// directory outside the records root.
77fn mirrored(workdir_root: &Path) -> PathBuf {
78    let absolute = std::path::absolute(workdir_root).unwrap_or_else(|_| workdir_root.to_path_buf());
79    let canonical = canonicalize_existing_prefix(&absolute);
80    let mut mirror = PathBuf::new();
81    for component in canonical.components() {
82        match component {
83            Component::Normal(name) => mirror.push(name),
84            Component::ParentDir => {
85                mirror.pop();
86            }
87            Component::RootDir | Component::CurDir | Component::Prefix(_) => {}
88        }
89    }
90    mirror
91}
92
93/// One session's records directory, created at staging and kept after the session.
94#[derive(Debug, Clone)]
95pub struct RecordsDir {
96    /// `<group>/<session-id>`, absolute.
97    path: PathBuf,
98    /// [`Self::path`], held, sharing the accessible root's refusal latch.
99    root: WorkdirRoot,
100}
101
102impl RecordsDir {
103    /// Creates `<group>/<session_id>/` and every directory above it up to `~/.murmur/records` at
104    /// `0700`, with no link followed beneath `~/.murmur/records`, and holds it. Every refusal
105    /// beneath it lands on `latch_of`'s latch and names its path absolutely.
106    ///
107    /// `group` is [`records_group`]'s answer for the session's workdir root, the one the launch
108    /// was judged against.
109    pub fn create(group: &Path, session_id: &str, latch_of: &WorkdirRoot) -> Result<Self, String> {
110        let located = |path: &Path, reason: String| format!("{}: {reason}", path.display());
111        let home = crate::murmur_home::ensure_murmur_home()?;
112        let records = home.join(RECORDS_DIR_NAME);
113        let rel = group
114            .strip_prefix(&records)
115            .map_err(|_| located(group, format!("is not beneath {}", records.display())))?
116            .join(session_id);
117        crate::murmur_home::hold_private_dir(&records)
118            .map_err(|reason| located(&records, reason))?;
119        let records_tree = WorkdirRoot::open(&records).map_err(|error| error.to_string())?;
120        records_tree
121            .create_dir_all(&rel, PRIVATE_DIR_MODE)
122            .map_err(|error| error.to_string())?;
123        let rel_text = rel.to_string_lossy();
124        let walked = records_tree
125            .open_beneath(&rel_text)
126            .map_err(|error| error.to_string())?
127            .ok_or_else(|| located(&records.join(&rel), "vanished after its creation".into()))?;
128        let path = records.join(&rel);
129        let root = WorkdirRoot::open_sharing_latch_absolute(&path, latch_of)
130            .map_err(|error| error.to_string())?;
131        // The held root was opened by path; it must be the directory the no-follow walk created.
132        if root.identity().map_err(|error| error.to_string())?
133            != walked.identity().map_err(|error| error.to_string())?
134        {
135            return Err(located(
136                &path,
137                "changed between its creation and its open".to_string(),
138            ));
139        }
140        Ok(Self { path, root })
141    }
142
143    /// The records directory's absolute path, the value of [`RECORDS_DIR_ENV`].
144    #[must_use]
145    pub fn path(&self) -> &Path {
146        &self.path
147    }
148
149    /// The held directory every runtime record of the session is written through.
150    #[must_use]
151    pub fn root(&self) -> &WorkdirRoot {
152        &self.root
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn records_dir_mirror_drops_the_root_and_every_dot_segment() {
162        let temp = tempfile::tempdir().unwrap();
163        let real = temp.path().canonicalize().unwrap();
164        let mirror = mirrored(&real.join("proj/./workdir/../workdir"));
165        assert_eq!(
166            mirror,
167            real.strip_prefix("/").unwrap().join("proj").join("workdir")
168        );
169        assert!(mirror.is_relative());
170        assert_eq!(mirrored(Path::new("/../../etc")), PathBuf::from("etc"));
171    }
172
173    #[test]
174    fn records_dir_mirror_resolves_a_link_above_the_workdir_root() {
175        let temp = tempfile::tempdir().unwrap();
176        let real = temp.path().join("real");
177        std::fs::create_dir_all(&real).unwrap();
178        let link = temp.path().join("link");
179        std::os::unix::fs::symlink(&real, &link).unwrap();
180        assert_eq!(
181            mirrored(&link.join("workdir")),
182            mirrored(&real.join("workdir"))
183        );
184    }
185
186    /// The group is the workdir root mirrored under `~/.murmur/records`; creating a session's
187    /// directory makes every level `0700`, and a link planted in the mirrored path is refused.
188    #[test]
189    fn records_dir_is_created_owner_only_beneath_the_murmur_home() {
190        let home = tempfile::tempdir().unwrap();
191        crate::murmur_home::run_with_home(
192            "records_dir::tests::records_dir_create_inner",
193            home.path(),
194        );
195    }
196
197    #[test]
198    #[ignore = "run by records_dir_is_created_owner_only_beneath_the_murmur_home"]
199    fn records_dir_create_inner() {
200        if !crate::murmur_home::in_scratch_home() {
201            return;
202        }
203        let home = PathBuf::from(std::env::var_os("HOME").unwrap());
204        let project = home.join("proj");
205        std::fs::create_dir_all(&project).unwrap();
206        let group = records_group(&project.join("workdir")).unwrap();
207        assert_eq!(
208            group,
209            home.join(".murmur/records")
210                .join(mirrored(&project.join("workdir")))
211        );
212        let accessible = WorkdirRoot::open(&project).unwrap();
213        let created = RecordsDir::create(&group, "ses_0001", &accessible).unwrap();
214        assert_eq!(created.path(), group.join("ses_0001"));
215        let mut level = created.path().to_path_buf();
216        while level != home.join(".murmur") {
217            assert_eq!(
218                crate::murmur_home::mode_of(&level),
219                0o700,
220                "{}",
221                level.display()
222            );
223            level.pop();
224        }
225        created
226            .root()
227            .write_file("trace.jsonl", b"{}\n", crate::workdir_fs::DEFAULT_FILE_MODE)
228            .unwrap();
229
230        let elsewhere = home.join("elsewhere");
231        std::fs::create_dir_all(&elsewhere).unwrap();
232        let planted_group = records_group(&home.join("planted/workdir")).unwrap();
233        std::fs::create_dir_all(planted_group.parent().unwrap()).unwrap();
234        std::os::unix::fs::symlink(&elsewhere, &planted_group).unwrap();
235        assert!(RecordsDir::create(&planted_group, "ses_0002", &accessible).is_err());
236        assert!(!elsewhere.join("ses_0002").exists());
237    }
238}