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}