Skip to main content

mcpls_core/config/
trust.rs

1//! Trust the user places in the analyzed workspace.
2//!
3//! Language servers run workspace code (build scripts, procedural macros,
4//! tsconfig plugins). [`WorkspaceTrust::Untrusted`] makes mcpls start only the
5//! servers the user named, so an unvetted checkout cannot reach the rest.
6
7use std::path::PathBuf;
8
9use super::ServerId;
10
11/// The current user's home directory as the account database records it,
12/// canonicalized; `None` when it cannot be determined.
13///
14/// Deliberately not `$HOME`: a checkout's tooling can set that, which would
15/// let it name itself "the home directory" and so exempt itself from the
16/// untrusted-mode workspace boundary.
17pub fn login_home_dir() -> Option<PathBuf> {
18    #[cfg(unix)]
19    let home = nix::unistd::User::from_uid(nix::unistd::Uid::current())
20        .ok()
21        .flatten()
22        .map(|user| user.dir);
23    #[cfg(not(unix))]
24    let home = dirs::home_dir();
25    home.and_then(|home| dunce::canonicalize(home).ok())
26}
27
28/// Whether the analyzed workspace is trusted to be handed to language servers.
29///
30/// Distinct from [`ProjectConfigTrust`](super::ProjectConfigTrust), which
31/// governs only a project-local `./mcpls.toml` and defaults to untrusted.
32/// This value defaults to [`Trusted`](Self::Trusted), the behavior of every
33/// earlier release. It is never read from a config file: consent to start a
34/// server comes from the command line of the user who launches mcpls, so a
35/// planted `mcpls.toml` cannot grant it.
36///
37/// Untrusted mode is not a sandbox. A server the user allows still runs the
38/// workspace code it would run in a trusted workspace.
39///
40/// # Examples
41///
42/// ```
43/// use mcpls_core::config::{ServerId, WorkspaceTrust};
44///
45/// let rust = ServerId::from_static("rust");
46/// assert!(WorkspaceTrust::default().allows(&rust));
47///
48/// let untrusted = WorkspaceTrust::untrusted([rust.clone()]);
49/// assert!(untrusted.allows(&rust));
50/// assert!(!untrusted.allows(&ServerId::from_static("python")));
51/// ```
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub enum WorkspaceTrust {
54    /// Every applicable server is started.
55    #[default]
56    Trusted,
57    /// Only the servers in the allowlist are started.
58    Untrusted(ServerAllowlist),
59}
60
61impl WorkspaceTrust {
62    /// Untrusted mode that starts exactly the servers in `allowed`.
63    #[must_use]
64    pub fn untrusted(allowed: impl IntoIterator<Item = ServerId>) -> Self {
65        Self::Untrusted(ServerAllowlist::new(allowed))
66    }
67
68    /// Whether the server `id` may be started.
69    #[must_use]
70    pub fn allows(&self, id: &ServerId) -> bool {
71        match self {
72            Self::Trusted => true,
73            Self::Untrusted(allowlist) => allowlist.contains(id),
74        }
75    }
76}
77
78/// The servers a user explicitly allowed in an untrusted workspace.
79///
80/// Duplicates are dropped, keeping the first occurrence, so the order the user
81/// gave is preserved for error messages.
82///
83/// # Examples
84///
85/// ```
86/// use mcpls_core::config::{ServerAllowlist, ServerId};
87///
88/// let list = ServerAllowlist::new(["rust", "rust", "go"].map(ServerId::from_static));
89/// assert_eq!(list.as_slice().len(), 2);
90/// assert!(list.contains(&ServerId::from_static("go")));
91/// assert!(!ServerAllowlist::default().contains(&ServerId::from_static("go")));
92/// ```
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct ServerAllowlist(Vec<ServerId>);
95
96impl ServerAllowlist {
97    /// Collects `allowed`, dropping repeated ids.
98    #[must_use]
99    pub fn new(allowed: impl IntoIterator<Item = ServerId>) -> Self {
100        let mut ids: Vec<ServerId> = Vec::new();
101        for id in allowed {
102            if !ids.contains(&id) {
103                ids.push(id);
104            }
105        }
106        Self(ids)
107    }
108
109    /// Whether `id` was allowed.
110    #[must_use]
111    pub fn contains(&self, id: &ServerId) -> bool {
112        self.0.contains(id)
113    }
114
115    /// The allowed ids, in the order they were first given.
116    #[must_use]
117    pub fn as_slice(&self) -> &[ServerId] {
118        &self.0
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_default_is_trusted_and_allows_everything() {
128        assert_eq!(WorkspaceTrust::default(), WorkspaceTrust::Trusted);
129        assert!(WorkspaceTrust::Trusted.allows(&ServerId::from_static("anything")));
130    }
131
132    #[test]
133    fn test_untrusted_allows_only_listed_ids() {
134        let trust = WorkspaceTrust::untrusted([ServerId::from_static("rust")]);
135        assert!(trust.allows(&ServerId::from_static("rust")));
136        assert!(!trust.allows(&ServerId::from_static("python")));
137        assert!(!WorkspaceTrust::untrusted([]).allows(&ServerId::from_static("rust")));
138    }
139
140    #[test]
141    fn test_allowlist_deduplicates_keeping_first_occurrence() {
142        let list = ServerAllowlist::new(["b", "a", "b", "a"].map(ServerId::from_static));
143        assert_eq!(list.as_slice(), ["b", "a"].map(ServerId::from_static));
144    }
145}