refactor: the missions root has one definition, not five

`mission_workspace::missions_root()` is now the only place that answers "where
does mission state live". It had fragmented into five: this function, private
`env::var("CLAWMATES_MISSIONS_ROOT")` copies in security_scan, benchmark_runner
and mission_outputs, and a hardcoded `MISSIONS_HOST_ROOT` const in
mission_runtime that read no env at all.

They agree on the deployed value, so nothing has broken. The risk is entirely
in what comes next: anything that sweeps or reclaims this tree has to be
sweeping the same tree the writers use, and five definitions cannot promise
that — a reaper written against one would silently leave the others' directories
behind forever, which is how the orphans got there in the first place.

A source-walk test fails any module outside `mission_workspace` that reads the
env var itself.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-09 06:18:01 -07:00
co-authored by Claude Opus 5
parent e5f097c291
commit 91fbd2dc88
5 changed files with 68 additions and 15 deletions
+50 -1
View File
@@ -26,7 +26,17 @@ use std::path::PathBuf;
use tokio::process::Command;
use uuid::Uuid;
pub(crate) fn missions_root() -> PathBuf {
/// The ONE definition of where mission state lives on the docker host.
///
/// There used to be five: this function, three private copies of the same
/// `env::var(...).unwrap_or(...)` in `security_scan`, `benchmark_runner` and
/// `mission_outputs`, and a hardcoded `MISSIONS_HOST_ROOT` const in
/// `mission_runtime` that read no env at all. They agree on today's
/// deployment, which is why nothing had broken — but anything that sweeps or
/// reclaims this tree has to be sure it is sweeping the same tree the writers
/// use, and five definitions cannot promise that. A GC written against one of
/// them would silently miss the others.
pub fn missions_root() -> PathBuf {
std::env::var("CLAWMATES_MISSIONS_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/var/lib/clawmates-missions"))
@@ -1074,4 +1084,43 @@ mod tests {
mark_phase_started(repo);
assert!(checkout_in_use(repo));
}
/// Nobody re-derives the missions root.
///
/// It had fragmented into five definitions — this function, three private
/// `env::var("CLAWMATES_MISSIONS_ROOT")` copies, and a hardcoded const
/// that read no env at all. They agreed on the deployed value, so nothing
/// ever broke; the risk is entirely in what comes next. Anything that
/// sweeps, reclaims or reaps this tree has to be sweeping the same tree the
/// writers use, and five definitions cannot promise that.
#[test]
fn the_missions_root_has_exactly_one_definition() {
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
for entry in std::fs::read_dir(dir).expect("readable source dir") {
let path = entry.expect("readable entry").path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
walk(&root, &mut files);
for path in files {
if path.ends_with("mission_workspace.rs") {
continue;
}
let src = std::fs::read_to_string(&path).expect("readable source");
assert!(
!src.contains("var(\"CLAWMATES_MISSIONS_ROOT\")"),
"{} reads CLAWMATES_MISSIONS_ROOT itself — call \
`mission_workspace::missions_root()` so a reaper and a writer \
cannot disagree about which tree they are looking at",
path.display()
);
}
}
}