Skip to main content

interlink/
policy_store.rs

1//! Shared peer settings. Readers reload complete snapshots; writers serialize updates.
2
3use std::fs::OpenOptions;
4use std::path::{Path, PathBuf};
5
6use anyhow::Result;
7use fs2::FileExt;
8
9use crate::identity::AgentId;
10use crate::policy::Policy;
11
12pub struct PolicyStore {
13    path: PathBuf,
14}
15
16impl PolicyStore {
17    pub fn open(path: &Path) -> Result<Self> {
18        // Resolve aliases once so sessions using a symlink share the same lock.
19        let path = path.canonicalize()?;
20        Policy::load(&path)?;
21        Ok(Self { path })
22    }
23
24    pub fn read(&self) -> Result<Policy> {
25        Policy::load(&self.path)
26    }
27
28    fn update<T>(&self, change: impl FnOnce(&mut Policy) -> Result<T>) -> Result<T> {
29        // Lock a stable sidecar: atomic replacement changes the policy file's inode.
30        let lock = OpenOptions::new()
31            .create(true)
32            .truncate(false)
33            .write(true)
34            .open(self.path.with_extension("json.lock"))?;
35        lock.lock_exclusive()?;
36        let mut policy = self.read()?;
37        let result = change(&mut policy)?;
38        policy.save(&self.path)?;
39        Ok(result)
40    }
41
42    pub fn add(&self, name: &str, key: &str) -> Result<()> {
43        self.update(|policy| policy.add(name, key))
44    }
45
46    pub fn add_for_pairing(&self, name: &str, key: &str) -> Result<String> {
47        self.update(|policy| {
48            // Another local session may already have paired this identity under
49            // its own petname. Keep that operator-selected name.
50            if let Some(peer) = policy.peer(AgentId::from_b64(key)?) {
51                return Ok(peer.petname.clone());
52            }
53            policy.add(name, key)?;
54            Ok(name.to_string())
55        })
56    }
57
58    pub fn remove(&self, name: &str) -> Result<bool> {
59        self.update(|policy| Ok(policy.remove(name)))
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::identity::AgentKey;
67    use std::thread;
68
69    #[test]
70    fn independent_sessions_merge_updates_and_observe_removals() {
71        let dir = tempfile::tempdir().unwrap();
72        let path = dir.path().join("peers.json");
73        std::fs::write(&path, "{}").unwrap();
74        let first = PolicyStore::open(&path).unwrap();
75        let second = PolicyStore::open(&path).unwrap();
76        let alice = AgentKey::generate().unwrap().id().to_b64();
77        let bob = AgentKey::generate().unwrap().id().to_b64();
78        thread::scope(|scope| {
79            scope.spawn(|| first.add("alice", &alice).unwrap());
80            scope.spawn(|| second.add("bob", &bob).unwrap());
81        });
82        assert_eq!(first.read().unwrap().len(), 2);
83        first.remove("alice").unwrap();
84        assert!(second.read().unwrap().resolve("alice").is_err());
85        second.add("bob", &bob).unwrap();
86        assert_eq!(first.read().unwrap().len(), 1);
87    }
88
89    #[test]
90    fn failed_update_leaves_persisted_policy_unchanged() {
91        let dir = tempfile::tempdir().unwrap();
92        let path = dir.path().join("peers.json");
93        std::fs::write(&path, "{}").unwrap();
94        let store = PolicyStore::open(&path).unwrap();
95        let alice = AgentKey::generate().unwrap().id().to_b64();
96        store.add("alice", &alice).unwrap();
97        let before = std::fs::read(&path).unwrap();
98        assert!(
99            store
100                .add("alice", &AgentKey::generate().unwrap().id().to_b64())
101                .is_err()
102        );
103        assert_eq!(std::fs::read(&path).unwrap(), before);
104        // Failure to acquire the sidecar must also leave the snapshot intact.
105        std::fs::remove_file(path.with_extension("json.lock")).unwrap();
106        std::fs::create_dir(path.with_extension("json.lock")).unwrap();
107        assert!(store.remove("alice").is_err());
108        assert!(store.read().unwrap().resolve("alice").is_ok());
109    }
110}