Skip to main content

mur_common/
git_push.rs

1//! On-disk layout shared by the git push broker (daemon side) and its agent-side
2//! tools (runtime side). Lives here because both `mur-agent-runtime` and
3//! `mur-daemon` read it and neither may depend on the other.
4//!
5//! Two trust zones, kept as separate paths on purpose:
6//!
7//! - `<mur_home>/git-push/` is **broker-owned**. The daemon (or `mur` setup)
8//!   writes it; no agent sandbox may write it. `LaunchChain` puts the whole
9//!   directory in its write-protected set, so on Linux any write grant that
10//!   overlaps it is dropped whole, on macOS SBPL denies it, and the file tools
11//!   refuse it. The registry is the reason: an agent that could write it could
12//!   remap a `repo_id` to any path and override the human's enrollment.
13//! - `<agent_home>/inbox/git-push*` is **agent-owned**: the agent drops signed
14//!   requests and cancel markers there; the daemon verifies before acting.
15//!
16//! The agent may READ exactly two broker paths — [`registry_path`] and its own
17//! [`status_dir`] — never the whole directory, which also holds broker-private
18//! state (pending table, per-request repos).
19
20use std::path::{Path, PathBuf};
21
22/// Broker-owned directory under `<mur_home>`.
23pub const GIT_PUSH_DIR: &str = "git-push";
24/// `repo_id` → sandbox repo path + enrolled remote ids. Daemon writes, agent reads.
25pub const REGISTRY_FILE: &str = "registry.yaml";
26/// Human-written enrollment records, the single source of truth. Lives in
27/// [`PRIVATE_DIR`]; `registry.yaml` is derived from it.
28pub const ENROLLMENTS_FILE: &str = "enrollments.yaml";
29/// Per-agent status files the daemon publishes: `status/<agent>/<request_id>.yaml`.
30pub const STATUS_DIR: &str = "status";
31/// Broker-private state (pending table, per-request repos). No agent grant may reach it.
32pub const PRIVATE_DIR: &str = "private";
33/// Agent-side request drop, relative to the agent home.
34pub const INBOX_DIR: &str = "inbox/git-push";
35/// Agent-side cancel markers, relative to the agent home.
36pub const CANCEL_DIR: &str = "inbox/git-push-cancel";
37
38/// `<mur_home>/git-push`.
39pub fn broker_dir(mur_home: &Path) -> PathBuf {
40    mur_home.join(GIT_PUSH_DIR)
41}
42
43/// `<mur_home>/git-push/registry.yaml`.
44pub fn registry_path(mur_home: &Path) -> PathBuf {
45    broker_dir(mur_home).join(REGISTRY_FILE)
46}
47
48/// `<mur_home>/git-push/status/<agent>`.
49pub fn status_dir(mur_home: &Path, agent: &str) -> PathBuf {
50    broker_dir(mur_home).join(STATUS_DIR).join(agent)
51}
52
53/// `<mur_home>/git-push/private`.
54pub fn private_dir(mur_home: &Path) -> PathBuf {
55    broker_dir(mur_home).join(PRIVATE_DIR)
56}
57
58/// `<mur_home>/git-push/private/enrollments.yaml`.
59pub fn enrollments_path(mur_home: &Path) -> PathBuf {
60    private_dir(mur_home).join(ENROLLMENTS_FILE)
61}
62
63/// `<agent_home>/inbox/git-push`.
64pub fn inbox_dir(agent_home: &Path) -> PathBuf {
65    agent_home.join(INBOX_DIR)
66}
67
68/// `<agent_home>/inbox/git-push-cancel`.
69pub fn cancel_dir(agent_home: &Path) -> PathBuf {
70    agent_home.join(CANCEL_DIR)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn agent_side_paths_stay_outside_the_broker_dir() {
79        let mur = Path::new("/m");
80        let agent = mur.join("agents").join("bob");
81        let broker = broker_dir(mur);
82        assert!(!inbox_dir(&agent).starts_with(&broker));
83        assert!(!cancel_dir(&agent).starts_with(&broker));
84        assert!(registry_path(mur).starts_with(&broker));
85        assert!(status_dir(mur, "bob").starts_with(&broker));
86        assert!(private_dir(mur).starts_with(&broker));
87        assert!(
88            !status_dir(mur, "bob").starts_with(private_dir(mur)),
89            "the agent-readable status dir must not sit inside broker-private state"
90        );
91    }
92}