Skip to main content

mur_core/mcp/
trust.rs

1//! Three-tier trust system for MCP servers.
2//!
3//! Trust levels:
4//! - **Trusted**: Full access, no restrictions. Used for first-party servers.
5//! - **Verified**: Checked against a known-good hash. Community servers.
6//! - **Untrusted**: Sandboxed with strict limits. Unknown servers.
7
8use anyhow::{Context, Result};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14/// Trust level for an MCP server.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum TrustLevel {
18    /// Full access — first-party or explicitly trusted by user.
19    Trusted,
20    /// Verified via checksum — community servers with known-good hashes.
21    Verified,
22    /// Untrusted — sandboxed with strict resource limits.
23    Untrusted,
24}
25
26impl std::fmt::Display for TrustLevel {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            TrustLevel::Trusted => write!(f, "trusted"),
30            TrustLevel::Verified => write!(f, "verified"),
31            TrustLevel::Untrusted => write!(f, "untrusted"),
32        }
33    }
34}
35
36impl std::str::FromStr for TrustLevel {
37    type Err = anyhow::Error;
38
39    fn from_str(s: &str) -> Result<Self> {
40        match s.to_lowercase().as_str() {
41            "trusted" => Ok(TrustLevel::Trusted),
42            "verified" => Ok(TrustLevel::Verified),
43            "untrusted" => Ok(TrustLevel::Untrusted),
44            _ => anyhow::bail!("Unknown trust level: '{}' (expected: trusted, verified, untrusted)", s),
45        }
46    }
47}
48
49/// Validated MCP capability enum.
50///
51/// Uses an allow-list pattern: only recognized capabilities are permitted.
52/// Unrecognized capability strings are rejected at parse time.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum McpCapability {
55    ReadFile,
56    ListTools,
57    Search,
58    WriteFile,
59    ExecuteSafe,
60    NetworkHttp,
61}
62
63impl std::fmt::Display for McpCapability {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            McpCapability::ReadFile => write!(f, "read_file"),
67            McpCapability::ListTools => write!(f, "list_tools"),
68            McpCapability::Search => write!(f, "search"),
69            McpCapability::WriteFile => write!(f, "write_file"),
70            McpCapability::ExecuteSafe => write!(f, "execute_safe"),
71            McpCapability::NetworkHttp => write!(f, "network_http"),
72        }
73    }
74}
75
76impl std::str::FromStr for McpCapability {
77    type Err = anyhow::Error;
78
79    fn from_str(s: &str) -> Result<Self> {
80        match s {
81            "read_file" => Ok(McpCapability::ReadFile),
82            "list_tools" => Ok(McpCapability::ListTools),
83            "search" => Ok(McpCapability::Search),
84            "write_file" => Ok(McpCapability::WriteFile),
85            "execute_safe" => Ok(McpCapability::ExecuteSafe),
86            "network_http" => Ok(McpCapability::NetworkHttp),
87            _ => anyhow::bail!(
88                "Unknown MCP capability: '{}' (known: read_file, list_tools, search, write_file, execute_safe, network_http)",
89                s
90            ),
91        }
92    }
93}
94
95/// Trust entry for a specific MCP server.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct TrustEntry {
98    /// Name of the MCP server.
99    pub server_name: String,
100    /// Trust level assigned.
101    pub level: TrustLevel,
102    /// Optional SHA-256 checksum for verified servers.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub checksum: Option<String>,
105    /// Who set this trust level.
106    #[serde(default)]
107    pub set_by: String,
108    /// When this trust entry was last modified.
109    #[serde(default)]
110    pub updated_at: String,
111}
112
113/// Manages trust levels for MCP servers.
114///
115/// Renamed from `TrustStore` to distinguish from `SkillTrustStore` in `crate::trust`.
116/// Entries are cached in memory and invalidated on `set_trust`/`revoke_trust`.
117pub struct McpTrustStore {
118    store_dir: PathBuf,
119    /// In-memory cache of trust entries. `None` means cache is cold/invalidated.
120    cache: Mutex<Option<HashMap<String, TrustEntry>>>,
121}
122
123impl McpTrustStore {
124    /// Create a new trust store.
125    pub fn new(store_dir: &Path) -> Self {
126        Self {
127            store_dir: store_dir.to_path_buf(),
128            cache: Mutex::new(None),
129        }
130    }
131
132    /// Get the trust level for a server. Defaults to Untrusted if not set.
133    pub fn get_trust(&self, server_name: &str) -> Result<TrustLevel> {
134        let entries = self.load_entries()?;
135        Ok(entries
136            .get(server_name)
137            .map(|e| e.level)
138            .unwrap_or(TrustLevel::Untrusted))
139    }
140
141    /// Set the trust level for a server. Invalidates the in-memory cache.
142    pub fn set_trust(
143        &self,
144        server_name: &str,
145        level: TrustLevel,
146        checksum: Option<String>,
147    ) -> Result<()> {
148        let mut entries = self.load_entries()?;
149
150        entries.insert(
151            server_name.to_string(),
152            TrustEntry {
153                server_name: server_name.to_string(),
154                level,
155                checksum,
156                set_by: whoami(),
157                updated_at: chrono::Utc::now().to_rfc3339(),
158            },
159        );
160
161        self.save_entries(&entries)?;
162        self.invalidate_cache();
163        Ok(())
164    }
165
166    /// Remove a trust entry (reverts to Untrusted). Invalidates the in-memory cache.
167    pub fn revoke_trust(&self, server_name: &str) -> Result<()> {
168        let mut entries = self.load_entries()?;
169        entries.remove(server_name);
170        self.save_entries(&entries)?;
171        self.invalidate_cache();
172        Ok(())
173    }
174
175    /// List all trust entries.
176    pub fn list(&self) -> Result<Vec<TrustEntry>> {
177        let entries = self.load_entries()?;
178        Ok(entries.into_values().collect())
179    }
180
181    /// Verify a server's checksum matches its stored entry.
182    /// Returns Ok(true) if verified, Ok(false) if no checksum stored, Err if mismatch.
183    pub fn verify_checksum(&self, server_name: &str, command_path: &std::path::Path) -> Result<bool> {
184        let entries = self.load_entries()?;
185        let entry = match entries.get(server_name) {
186            Some(e) => e,
187            None => return Ok(false),
188        };
189        let stored = match &entry.checksum {
190            Some(c) => c,
191            None => return Ok(false),
192        };
193        // Compute SHA-256 of the server binary
194        let bytes = std::fs::read(command_path)
195            .with_context(|| format!("Reading server binary: {:?}", command_path))?;
196        use sha2::{Sha256, Digest};
197        let hash = format!("{:x}", Sha256::digest(&bytes));
198        if hash != *stored {
199            anyhow::bail!(
200                "Checksum mismatch for '{}': expected {}, got {}. Server binary may have been tampered with.",
201                server_name, stored, hash
202            );
203        }
204        Ok(true)
205    }
206
207    /// Compute SHA-256 checksum of a file (for use when setting trust).
208    pub fn compute_checksum(path: &std::path::Path) -> Result<String> {
209        let bytes = std::fs::read(path)?;
210        use sha2::{Sha256, Digest};
211        Ok(format!("{:x}", Sha256::digest(&bytes)))
212    }
213
214    /// Check whether a server's trust level permits a given validated capability.
215    pub fn check_permission(&self, server_name: &str, capability: McpCapability) -> Result<bool> {
216        let level = self.get_trust(server_name)?;
217        Ok(match level {
218            TrustLevel::Trusted => true,
219            TrustLevel::Verified => matches!(
220                capability,
221                McpCapability::ReadFile
222                    | McpCapability::ListTools
223                    | McpCapability::Search
224                    | McpCapability::WriteFile
225                    | McpCapability::ExecuteSafe
226                    | McpCapability::NetworkHttp
227            ),
228            TrustLevel::Untrusted => matches!(
229                capability,
230                McpCapability::ReadFile | McpCapability::ListTools | McpCapability::Search
231            ),
232        })
233    }
234
235    /// Check permission using a string capability name.
236    ///
237    /// Parses the string into an `McpCapability` first. Unknown capability
238    /// strings are denied (returns `Ok(false)`) rather than causing an error,
239    /// following the allow-list pattern.
240    pub fn check_permission_str(&self, server_name: &str, capability: &str) -> Result<bool> {
241        match capability.parse::<McpCapability>() {
242            Ok(cap) => self.check_permission(server_name, cap),
243            Err(_) => Ok(false), // Unknown capabilities are denied
244        }
245    }
246
247    fn store_path(&self) -> PathBuf {
248        self.store_dir.join("trust.json")
249    }
250
251    /// Load entries, using the in-memory cache if available.
252    fn load_entries(&self) -> Result<HashMap<String, TrustEntry>> {
253        let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
254        if let Some(ref entries) = *cache {
255            return Ok(entries.clone());
256        }
257        let entries = self.load_entries_from_disk()?;
258        *cache = Some(entries.clone());
259        Ok(entries)
260    }
261
262    /// Load entries directly from disk (bypasses cache).
263    fn load_entries_from_disk(&self) -> Result<HashMap<String, TrustEntry>> {
264        let path = self.store_path();
265        if !path.exists() {
266            return Ok(HashMap::new());
267        }
268        let content =
269            std::fs::read_to_string(&path).context("Reading trust store")?;
270        let entries: HashMap<String, TrustEntry> =
271            serde_json::from_str(&content).context("Parsing trust store")?;
272        Ok(entries)
273    }
274
275    /// Invalidate the in-memory cache, forcing the next read to hit disk.
276    fn invalidate_cache(&self) {
277        let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
278        *cache = None;
279    }
280
281    fn save_entries(&self, entries: &HashMap<String, TrustEntry>) -> Result<()> {
282        std::fs::create_dir_all(&self.store_dir)?;
283        let json = serde_json::to_string_pretty(entries)?;
284        let tmp_path = self.store_path().with_extension("json.tmp");
285        std::fs::write(&tmp_path, &json)?;
286        std::fs::rename(&tmp_path, self.store_path())?;
287        #[cfg(unix)]
288        {
289            use std::os::unix::fs::PermissionsExt;
290            let _ = std::fs::set_permissions(
291                self.store_path(),
292                std::fs::Permissions::from_mode(0o600),
293            );
294        }
295        Ok(())
296    }
297}
298
299fn whoami() -> String {
300    std::env::var("USER").unwrap_or_else(|_| "unknown".into())
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_trust_level_display() {
309        assert_eq!(TrustLevel::Trusted.to_string(), "trusted");
310        assert_eq!(TrustLevel::Verified.to_string(), "verified");
311        assert_eq!(TrustLevel::Untrusted.to_string(), "untrusted");
312    }
313
314    #[test]
315    fn test_trust_level_parse() {
316        assert_eq!("trusted".parse::<TrustLevel>().unwrap(), TrustLevel::Trusted);
317        assert_eq!("Verified".parse::<TrustLevel>().unwrap(), TrustLevel::Verified);
318        assert_eq!("UNTRUSTED".parse::<TrustLevel>().unwrap(), TrustLevel::Untrusted);
319        assert!("invalid".parse::<TrustLevel>().is_err());
320    }
321
322    #[test]
323    fn test_mcp_capability_parse() {
324        assert_eq!("read_file".parse::<McpCapability>().unwrap(), McpCapability::ReadFile);
325        assert_eq!("list_tools".parse::<McpCapability>().unwrap(), McpCapability::ListTools);
326        assert_eq!("search".parse::<McpCapability>().unwrap(), McpCapability::Search);
327        assert_eq!("write_file".parse::<McpCapability>().unwrap(), McpCapability::WriteFile);
328        assert_eq!("execute_safe".parse::<McpCapability>().unwrap(), McpCapability::ExecuteSafe);
329        assert_eq!("network_http".parse::<McpCapability>().unwrap(), McpCapability::NetworkHttp);
330        assert!("unknown_cap".parse::<McpCapability>().is_err());
331    }
332
333    #[test]
334    fn test_mcp_capability_display() {
335        assert_eq!(McpCapability::ReadFile.to_string(), "read_file");
336        assert_eq!(McpCapability::ListTools.to_string(), "list_tools");
337        assert_eq!(McpCapability::Search.to_string(), "search");
338        assert_eq!(McpCapability::WriteFile.to_string(), "write_file");
339        assert_eq!(McpCapability::ExecuteSafe.to_string(), "execute_safe");
340        assert_eq!(McpCapability::NetworkHttp.to_string(), "network_http");
341    }
342
343    #[test]
344    fn test_default_untrusted() {
345        let dir = tempfile::TempDir::new().unwrap();
346        let store = McpTrustStore::new(dir.path());
347        assert_eq!(store.get_trust("unknown-server").unwrap(), TrustLevel::Untrusted);
348    }
349
350    #[test]
351    fn test_set_and_get_trust() {
352        let dir = tempfile::TempDir::new().unwrap();
353        let store = McpTrustStore::new(dir.path());
354
355        store.set_trust("my-server", TrustLevel::Trusted, None).unwrap();
356        assert_eq!(store.get_trust("my-server").unwrap(), TrustLevel::Trusted);
357
358        store.set_trust("my-server", TrustLevel::Verified, Some("abc123".into())).unwrap();
359        assert_eq!(store.get_trust("my-server").unwrap(), TrustLevel::Verified);
360    }
361
362    #[test]
363    fn test_revoke_trust() {
364        let dir = tempfile::TempDir::new().unwrap();
365        let store = McpTrustStore::new(dir.path());
366
367        store.set_trust("temp", TrustLevel::Trusted, None).unwrap();
368        assert_eq!(store.get_trust("temp").unwrap(), TrustLevel::Trusted);
369
370        store.revoke_trust("temp").unwrap();
371        assert_eq!(store.get_trust("temp").unwrap(), TrustLevel::Untrusted);
372    }
373
374    #[test]
375    fn test_list_entries() {
376        let dir = tempfile::TempDir::new().unwrap();
377        let store = McpTrustStore::new(dir.path());
378
379        store.set_trust("a", TrustLevel::Trusted, None).unwrap();
380        store.set_trust("b", TrustLevel::Verified, None).unwrap();
381
382        let entries = store.list().unwrap();
383        assert_eq!(entries.len(), 2);
384    }
385
386    #[test]
387    fn test_check_permission_trusted() {
388        let dir = tempfile::TempDir::new().unwrap();
389        let store = McpTrustStore::new(dir.path());
390        store.set_trust("full", TrustLevel::Trusted, None).unwrap();
391
392        // Trusted servers get access to all known capabilities
393        assert!(store.check_permission("full", McpCapability::ReadFile).unwrap());
394        assert!(store.check_permission("full", McpCapability::ExecuteSafe).unwrap());
395        assert!(store.check_permission("full", McpCapability::NetworkHttp).unwrap());
396
397        // Trusted servers also pass unknown strings via check_permission_str (still denied for unknowns)
398        assert!(store.check_permission_str("full", "read_file").unwrap());
399        // Unknown capabilities are denied even for trusted (via check_permission_str)
400        // but trusted servers get all *known* capabilities via check_permission
401    }
402
403    #[test]
404    fn test_check_permission_str_unknown_denied() {
405        let dir = tempfile::TempDir::new().unwrap();
406        let store = McpTrustStore::new(dir.path());
407        store.set_trust("full", TrustLevel::Trusted, None).unwrap();
408
409        // Unknown capability strings are denied via check_permission_str
410        assert!(!store.check_permission_str("full", "execute_arbitrary").unwrap());
411        assert!(!store.check_permission_str("full", "system_config").unwrap());
412    }
413
414    #[test]
415    fn test_check_permission_verified() {
416        let dir = tempfile::TempDir::new().unwrap();
417        let store = McpTrustStore::new(dir.path());
418        store.set_trust("community", TrustLevel::Verified, None).unwrap();
419
420        assert!(store.check_permission("community", McpCapability::ReadFile).unwrap());
421        assert!(store.check_permission("community", McpCapability::WriteFile).unwrap());
422        assert!(store.check_permission("community", McpCapability::ExecuteSafe).unwrap());
423
424        // Unknown capabilities denied
425        assert!(!store.check_permission_str("community", "execute_arbitrary").unwrap());
426        assert!(!store.check_permission_str("community", "system_config").unwrap());
427    }
428
429    #[test]
430    fn test_check_permission_untrusted() {
431        let dir = tempfile::TempDir::new().unwrap();
432        let store = McpTrustStore::new(dir.path());
433
434        assert!(store.check_permission("unknown", McpCapability::ReadFile).unwrap());
435        assert!(store.check_permission("unknown", McpCapability::Search).unwrap());
436        assert!(!store.check_permission("unknown", McpCapability::WriteFile).unwrap());
437        assert!(!store.check_permission("unknown", McpCapability::ExecuteSafe).unwrap());
438
439        // Unknown capabilities denied
440        assert!(!store.check_permission_str("unknown", "execute_arbitrary").unwrap());
441        assert!(!store.check_permission_str("unknown", "network_unrestricted").unwrap());
442    }
443
444    #[test]
445    fn test_cache_invalidation_on_set() {
446        let dir = tempfile::TempDir::new().unwrap();
447        let store = McpTrustStore::new(dir.path());
448
449        // Prime cache
450        assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Untrusted);
451
452        // Set trust (should invalidate cache)
453        store.set_trust("srv", TrustLevel::Trusted, None).unwrap();
454
455        // Should see updated value
456        assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Trusted);
457    }
458
459    #[test]
460    fn test_cache_invalidation_on_revoke() {
461        let dir = tempfile::TempDir::new().unwrap();
462        let store = McpTrustStore::new(dir.path());
463
464        store.set_trust("srv", TrustLevel::Trusted, None).unwrap();
465        assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Trusted);
466
467        // Revoke (should invalidate cache)
468        store.revoke_trust("srv").unwrap();
469        assert_eq!(store.get_trust("srv").unwrap(), TrustLevel::Untrusted);
470    }
471
472    #[test]
473    fn test_trust_entry_serialization() {
474        let entry = TrustEntry {
475            server_name: "test".into(),
476            level: TrustLevel::Verified,
477            checksum: Some("sha256:abc".into()),
478            set_by: "user".into(),
479            updated_at: "2025-01-01T00:00:00Z".into(),
480        };
481        let json = serde_json::to_string(&entry).unwrap();
482        let parsed: TrustEntry = serde_json::from_str(&json).unwrap();
483        assert_eq!(parsed.level, TrustLevel::Verified);
484        assert_eq!(parsed.checksum.as_deref(), Some("sha256:abc"));
485    }
486}