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