Skip to main content

mur_common/
paths.rs

1//! Directory names under `<mur_home>` that a fleet / deep-research run writes.
2//!
3//! One list, two readers that must never disagree: `mur-core` creates and
4//! writes these while a run executes, and `mur-agent-runtime` carves exactly
5//! these into the kernel sandbox for an agent-triggered run (`fleet_run`).
6//! They lived as two separate literals and drifted — `runs/` was writable by
7//! the CLI and denied to the sandbox, so every agent-triggered run failed to
8//! register itself and `mur fleet status` reported nothing for a run that was
9//! live (fleet develop-rust, 2026-09-09). The list lives here because the
10//! runtime must not depend on `mur-core`.
11
12use std::path::{Path, PathBuf};
13
14/// Run-status records: `<mur_home>/runs/<run_id>/run.json`.
15pub const RUNS: &str = "runs";
16
17/// Fleet definitions: `<mur_home>/fleets/<name>/fleet.yaml`, plus the
18/// operator's `.stopped` kill-switch and the daemon's `.last_run` stamp.
19///
20/// Configuration, not run state — deliberately absent from
21/// [`RUN_STATE_DIRS`]. A run that could write here could rewrite its own
22/// fleet's members, limits and HITL pre-approvals, or clear its own
23/// kill-switch.
24pub const FLEETS: &str = "fleets";
25
26/// What a fleet's runs write, per fleet: `<mur_home>/fleet-state/<name>/`
27/// (job queue, progress record, event log, parallel-track state).
28///
29/// Its own tree, not `runs/`: that store is keyed by run_id and lists every
30/// directory in it as a run, so a fleet name dropped in there reads as a
31/// phantom run. Not `fleets/`: that is configuration (see [`FLEETS`]).
32pub const FLEET_STATE: &str = "fleet-state";
33
34/// The fleet's single progress record, inside [`fleet_state_dir`]. Kept after
35/// a run as the last-run record; overwritten by the next run. Read by both
36/// the CLI and the runtime's `fleet_run` live-run guard, so named once here.
37pub const FLEET_PROGRESS_FILE: &str = ".run_progress.json";
38
39/// Every directory an in-sandbox fleet run must be able to write.
40///
41/// Add here FIRST, then use it. A directory the runner writes but this list
42/// omits fails only in the sandboxed path — the one nobody runs by hand.
43///
44/// [`FLEET_STATE`] is listed for the CLI side and for tests; the sandbox
45/// carves in only the `fleet_run.fleets` subdirectories of it (see
46/// `mur-agent-runtime` `sandbox/policy.rs`), so an agent cannot queue work
47/// for a fleet the operator never let it run.
48pub const RUN_STATE_DIRS: [&str; 5] =
49    [FLEET_STATE, "commander", "conversations", "artifacts", RUNS];
50
51/// `<mur_home>/fleet-state/<name>/` — every file a run of `name` writes.
52pub fn fleet_state_dir(mur_home: &Path, name: &str) -> PathBuf {
53    mur_home.join(FLEET_STATE).join(name)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn runs_is_listed() {
62        // The drift that broke agent-triggered runs was exactly this: the
63        // writer knew about `runs`, the sandbox list did not.
64        assert!(RUN_STATE_DIRS.contains(&RUNS));
65    }
66
67    #[test]
68    fn fleet_definitions_are_not_run_state() {
69        // The whole point of FLEET_STATE: a run writes there, never into
70        // the tree that holds the fleet's own definition and kill-switch.
71        assert!(!RUN_STATE_DIRS.contains(&FLEETS));
72        assert!(RUN_STATE_DIRS.contains(&FLEET_STATE));
73        assert_ne!(FLEET_STATE, RUNS);
74    }
75
76    #[test]
77    fn fleet_definitions_are_not_an_authoring_grant() {
78        // Same boundary, the other writer: the seeded concierge gets write on
79        // every AUTHORING_DIRS entry, so `fleets/` there hands it the fleet's
80        // members, limits, HITL pre-approvals and `.stopped` kill-switch.
81        assert!(!crate::agent::AUTHORING_DIRS.contains(&FLEETS));
82        assert!(!crate::agent::AUTHORING_DIRS.contains(&FLEET_STATE));
83    }
84}