Skip to main content

mcpmesh_node/
paths.rs

1//! Every filesystem location a node reads or writes, resolved ONCE at construction.
2//! The node itself never consults the environment: the embedder picks a root
3//! ([`NodePaths::under_root`] — layout-identical to a `mcpmesh --profile <root>` dir),
4//! and the daemon shell resolves the standard per-user layout ([`NodePaths::from_env`]).
5use std::path::{Path, PathBuf};
6
7/// The resolved on-disk world of one node. Config-file overrides (`[identity].device_key`,
8/// `[identity].user_key`) still win over the two key paths here, exactly as for the daemon.
9#[derive(Debug, Clone)]
10pub struct NodePaths {
11    pub config_path: PathBuf,
12    pub device_key_path: PathBuf,
13    pub user_key_path: PathBuf,
14    pub roster_path: PathBuf,
15    pub state_db_path: PathBuf,
16    /// Outstanding pairing invites (#87b) — bearer secrets, written 0600. Beside the trust store
17    /// because it is durable per-node state, not config.
18    pub invites_path: PathBuf,
19    pub blobs_dir: PathBuf,
20    pub blob_scopes_path: PathBuf,
21    pub audit_dir: PathBuf,
22}
23
24impl NodePaths {
25    /// The profile-root layout under one directory: `config/` (config.toml + keys + roster),
26    /// `data/` (state.redb, blobs), `state/` (the audit log) — see module doc.
27    pub fn under_root(root: &Path) -> Self {
28        let config = root.join("config");
29        let data = root.join("data");
30        NodePaths {
31            config_path: config.join("config.toml"),
32            device_key_path: config.join("device.key"),
33            user_key_path: config.join("user.key"),
34            roster_path: config.join("roster.json"),
35            state_db_path: data.join("state.redb"),
36            invites_path: data.join("invites.json"),
37            blobs_dir: data.join("blobs"),
38            blob_scopes_path: data.join("blob-scopes.redb"),
39            audit_dir: root.join("state").join("audit"),
40        }
41    }
42
43    /// The standard per-user layout, from the same `mcpmesh_trust::paths` rules the
44    /// porcelain uses (XDG/APPDATA, honoring a profile root). Daemon-shell only —
45    /// an embedded node passes an explicit root instead.
46    pub fn from_env() -> std::io::Result<Self> {
47        use mcpmesh_trust::paths as p;
48        Ok(NodePaths {
49            config_path: p::default_config_path()?,
50            device_key_path: p::default_device_key_path()?,
51            user_key_path: p::default_user_key_path()?,
52            roster_path: p::default_roster_path()?,
53            state_db_path: p::default_state_db_path()?,
54            invites_path: p::default_invites_path()?,
55            blobs_dir: p::default_blobs_dir()?,
56            blob_scopes_path: p::default_blob_scopes_path()?,
57            audit_dir: p::default_audit_dir()?,
58        })
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::path::Path;
66
67    /// The one-root layout MUST equal the `--profile <root>` layout (local-api paths.rs
68    /// profile-root arms): config under `<root>/config`, data under `<root>/data`, state
69    /// under `<root>/state`. An embedded node's root dir is a valid CLI profile dir.
70    #[test]
71    fn under_root_matches_the_profile_layout() {
72        let p = NodePaths::under_root(Path::new("/r"));
73        assert_eq!(p.config_path, Path::new("/r/config/config.toml"));
74        assert_eq!(p.device_key_path, Path::new("/r/config/device.key"));
75        assert_eq!(p.user_key_path, Path::new("/r/config/user.key"));
76        assert_eq!(p.roster_path, Path::new("/r/config/roster.json"));
77        assert_eq!(p.state_db_path, Path::new("/r/data/state.redb"));
78        assert_eq!(p.blobs_dir, Path::new("/r/data/blobs"));
79        assert_eq!(p.blob_scopes_path, Path::new("/r/data/blob-scopes.redb"));
80        assert_eq!(p.audit_dir, Path::new("/r/state/audit"));
81    }
82}