1use std::collections::HashMap;
17use std::path::PathBuf;
18
19use anyhow::{Context, Result, anyhow};
20use serde::{Deserialize, Serialize};
21
22use crate::sandboxing::{NetworkAllowlistEntry, ResourceLimits, SandboxPolicy, WritableRoot};
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct McpSandboxOverrides {
27 #[serde(default = "default_true")]
30 enforce_conservative_limits: bool,
31 #[serde(default = "default_memory_mb")]
33 max_memory_mb: u64,
34 #[serde(default = "default_max_pids")]
36 max_pids: u64,
37 #[serde(default = "default_cpu_secs")]
39 cpu_time_secs: u64,
40 #[serde(default = "default_timeout_secs")]
42 timeout_secs: u64,
43 #[serde(default)]
47 writable_root: Option<PathBuf>,
48 #[serde(default)]
51 allowed_endpoints: Vec<NetworkAllowlistEntry>,
52}
53
54impl Default for McpSandboxOverrides {
55 fn default() -> Self {
56 Self {
57 enforce_conservative_limits: true,
58 max_memory_mb: 512,
59 max_pids: 64,
60 cpu_time_secs: 60,
61 timeout_secs: 120,
62 writable_root: None,
63 allowed_endpoints: Vec::new(),
64 }
65 }
66}
67
68fn default_true() -> bool {
69 true
70}
71fn default_memory_mb() -> u64 {
72 512
73}
74fn default_max_pids() -> u64 {
75 64
76}
77fn default_cpu_secs() -> u64 {
78 60
79}
80fn default_timeout_secs() -> u64 {
81 120
82}
83
84#[must_use]
98fn derive_mcp_sandbox_policy(parent: &SandboxPolicy, overrides: &McpSandboxOverrides) -> SandboxPolicy {
99 let mut derived = parent.clone();
100
101 if let Some(root) = &overrides.writable_root {
102 derived = match derived {
106 SandboxPolicy::WorkspaceWrite {
107 writable_roots: _,
108 network_access,
109 network_allowlist,
110 sensitive_paths,
111 mut resource_limits,
112 seccomp_profile,
113 exclude_tmpdir_env_var,
114 exclude_slash_tmp,
115 } => {
116 if overrides.enforce_conservative_limits {
117 resource_limits = conservative_resource_limits(overrides);
118 }
119 SandboxPolicy::WorkspaceWrite {
120 writable_roots: vec![WritableRoot::new(root.clone())],
121 network_access,
122 network_allowlist: if overrides.allowed_endpoints.is_empty() {
123 network_allowlist
124 } else {
125 overrides.allowed_endpoints.clone()
126 },
127 sensitive_paths,
128 resource_limits,
129 seccomp_profile,
130 exclude_tmpdir_env_var,
131 exclude_slash_tmp,
132 }
133 }
134 SandboxPolicy::ReadOnly { mut network_allowlist, .. } => {
135 if !overrides.allowed_endpoints.is_empty() {
136 network_allowlist = overrides.allowed_endpoints.clone();
137 }
138 SandboxPolicy::ReadOnly {
139 network_access: !network_allowlist.is_empty(),
140 network_allowlist,
141 }
142 }
143 SandboxPolicy::DangerFullAccess => {
144 let mut resource_limits = ResourceLimits::default();
145 if overrides.enforce_conservative_limits {
146 resource_limits = conservative_resource_limits(overrides);
147 }
148 SandboxPolicy::WorkspaceWrite {
149 writable_roots: vec![WritableRoot::new(root.clone())],
150 network_access: false,
151 network_allowlist: overrides.allowed_endpoints.clone(),
152 sensitive_paths: None,
153 resource_limits,
154 seccomp_profile: Default::default(),
155 exclude_tmpdir_env_var: false,
156 exclude_slash_tmp: false,
157 }
158 }
159 SandboxPolicy::ExternalSandbox { description } => SandboxPolicy::ExternalSandbox { description },
160 };
161 } else if !overrides.allowed_endpoints.is_empty() {
162 derived = match derived {
165 SandboxPolicy::WorkspaceWrite {
166 writable_roots,
167 network_access,
168 sensitive_paths,
169 mut resource_limits,
170 seccomp_profile,
171 exclude_tmpdir_env_var,
172 exclude_slash_tmp,
173 ..
174 } => {
175 if overrides.enforce_conservative_limits {
176 resource_limits = conservative_resource_limits(overrides);
177 }
178 SandboxPolicy::WorkspaceWrite {
179 writable_roots,
180 network_access,
181 network_allowlist: overrides.allowed_endpoints.clone(),
182 sensitive_paths,
183 resource_limits,
184 seccomp_profile,
185 exclude_tmpdir_env_var,
186 exclude_slash_tmp,
187 }
188 }
189 SandboxPolicy::ReadOnly { .. } => SandboxPolicy::ReadOnly {
190 network_access: true,
191 network_allowlist: overrides.allowed_endpoints.clone(),
192 },
193 other => other,
194 };
195 }
196
197 if overrides.enforce_conservative_limits
198 && let SandboxPolicy::WorkspaceWrite { ref mut resource_limits, .. } = derived
199 {
200 *resource_limits = conservative_resource_limits(overrides);
201 }
202
203 derived
204}
205
206fn conservative_resource_limits(overrides: &McpSandboxOverrides) -> ResourceLimits {
207 ResourceLimits {
208 max_memory_mb: overrides.max_memory_mb,
209 max_pids: u32::try_from(overrides.max_pids).unwrap_or(u32::MAX),
210 max_disk_mb: 1024,
211 cpu_time_secs: overrides.cpu_time_secs,
212 timeout_secs: overrides.timeout_secs,
213 }
214}
215
216#[expect(
228 unused_results,
229 reason = "MCP command builder methods configure the owned command and return a fluent mutable reference."
230)]
231pub fn wrap_stdio_command(
232 command: std::process::Command,
233 sandbox_policy: &SandboxPolicy,
234) -> Result<std::process::Command> {
235 if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
236 return Ok(command);
237 }
238 if matches!(sandbox_policy, SandboxPolicy::ExternalSandbox { .. }) {
239 return Err(anyhow!("MCP stdio cannot use an external sandbox policy without an external launcher"));
240 }
241
242 let original_program = command.get_program().to_owned();
243 let original_args: Vec<_> = command.get_args().map(|s| s.to_owned()).collect();
244 let current_dir = command
245 .get_current_dir()
246 .map(PathBuf::from)
247 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
248 let envs: HashMap<_, _> = command
249 .get_envs()
250 .filter_map(|(k, v)| v.map(|val| (k.to_owned(), val.to_owned())))
251 .collect();
252
253 let linux_launcher = crate::sandboxing::LinuxSandboxLauncher::resolve();
254 let spec = crate::sandboxing::CommandSpec::new(original_program)
255 .with_args(original_args.into_iter().map(|arg| arg.to_string_lossy().into_owned()))
256 .with_cwd(current_dir.clone())
257 .with_env(
258 envs.into_iter()
259 .map(|(key, value)| (key.to_string_lossy().into_owned(), value.to_string_lossy().into_owned()))
260 .collect(),
261 );
262 let exec_env = crate::sandboxing::SandboxManager::new()
263 .transform(spec, sandbox_policy, ¤t_dir, linux_launcher.as_ref())
264 .context("transform MCP stdio command with the sandbox policy")?;
265
266 let mut new_command = std::process::Command::new(exec_env.program);
267 new_command.args(exec_env.args).current_dir(exec_env.cwd).env_clear();
268 for (key, value) in exec_env.env {
269 new_command.env(key, value);
270 }
271 Ok(new_command)
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::sandboxing::SeccompProfile;
278
279 fn workspace_parent() -> SandboxPolicy {
280 SandboxPolicy::WorkspaceWrite {
281 writable_roots: vec![WritableRoot::new(PathBuf::from("/workspace"))],
282 network_access: true,
283 network_allowlist: vec![NetworkAllowlistEntry::https("api.example.com")],
284 sensitive_paths: None,
285 resource_limits: ResourceLimits::unlimited(),
286 seccomp_profile: SeccompProfile::permissive(),
287 exclude_tmpdir_env_var: false,
288 exclude_slash_tmp: false,
289 }
290 }
291
292 #[test]
293 fn derive_narrows_writable_root_when_set() {
294 let parent = workspace_parent();
295 let overrides = McpSandboxOverrides {
296 writable_root: Some(PathBuf::from("/tmp/mcp-scratch")),
297 ..McpSandboxOverrides::default()
298 };
299 let derived = derive_mcp_sandbox_policy(&parent, &overrides);
300 match derived {
301 SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
302 assert_eq!(writable_roots.len(), 1);
303 assert_eq!(writable_roots[0].root, PathBuf::from("/tmp/mcp-scratch"));
304 }
305 other => panic!("expected WorkspaceWrite, got {other:?}"),
306 }
307 }
308
309 #[test]
310 fn derive_keeps_parent_roots_when_override_missing() {
311 let parent = workspace_parent();
312 let overrides = McpSandboxOverrides::default();
313 let derived = derive_mcp_sandbox_policy(&parent, &overrides);
314 match derived {
315 SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
316 assert_eq!(writable_roots.len(), 1);
317 assert_eq!(writable_roots[0].root, PathBuf::from("/workspace"));
318 }
319 other => panic!("expected WorkspaceWrite, got {other:?}"),
320 }
321 }
322
323 #[test]
324 fn derive_restricts_network_to_allowed_endpoints() {
325 let parent = workspace_parent();
326 let overrides = McpSandboxOverrides {
327 allowed_endpoints: vec![NetworkAllowlistEntry::https("mcp.internal")],
328 ..McpSandboxOverrides::default()
329 };
330 let derived = derive_mcp_sandbox_policy(&parent, &overrides);
331 match derived {
332 SandboxPolicy::WorkspaceWrite { network_allowlist, .. } => {
333 assert_eq!(network_allowlist.len(), 1);
334 assert_eq!(network_allowlist[0].domain, "mcp.internal");
335 }
336 other => panic!("expected WorkspaceWrite, got {other:?}"),
337 }
338 }
339
340 #[test]
341 fn derive_applies_conservative_resource_caps_by_default() {
342 let parent = workspace_parent();
343 let overrides = McpSandboxOverrides::default();
344 let derived = derive_mcp_sandbox_policy(&parent, &overrides);
345 match derived {
346 SandboxPolicy::WorkspaceWrite { resource_limits, .. } => {
347 assert_eq!(resource_limits.max_memory_mb, 512);
348 assert_eq!(resource_limits.max_pids, 64);
349 assert_eq!(resource_limits.cpu_time_secs, 60);
350 assert_eq!(resource_limits.timeout_secs, 120);
351 }
352 other => panic!("expected WorkspaceWrite, got {other:?}"),
353 }
354 }
355
356 #[test]
357 fn derive_respects_disabled_conservative_caps() {
358 let parent = workspace_parent();
359 let overrides = McpSandboxOverrides {
360 enforce_conservative_limits: false,
361 ..McpSandboxOverrides::default()
362 };
363 let derived = derive_mcp_sandbox_policy(&parent, &overrides);
364 match derived {
365 SandboxPolicy::WorkspaceWrite { resource_limits, .. } => {
366 assert_eq!(resource_limits.max_memory_mb, 0);
368 }
369 other => panic!("expected WorkspaceWrite, got {other:?}"),
370 }
371 }
372
373 #[test]
374 fn derive_downgrades_danger_full_access_with_writable_root() {
375 let overrides = McpSandboxOverrides {
376 writable_root: Some(PathBuf::from("/tmp/mcp-scratch")),
377 ..McpSandboxOverrides::default()
378 };
379 let derived = derive_mcp_sandbox_policy(&SandboxPolicy::DangerFullAccess, &overrides);
380 match derived {
381 SandboxPolicy::WorkspaceWrite { writable_roots, network_access, .. } => {
382 assert_eq!(writable_roots.len(), 1);
383 assert!(!network_access);
384 }
385 other => panic!("expected WorkspaceWrite downgrade, got {other:?}"),
386 }
387 }
388
389 #[test]
390 fn overrides_default_is_conservative() {
391 let defaults = McpSandboxOverrides::default();
392 assert!(defaults.enforce_conservative_limits);
393 assert_eq!(defaults.max_memory_mb, 512);
394 assert!(defaults.allowed_endpoints.is_empty());
395 assert!(defaults.writable_root.is_none());
396 }
397
398 #[test]
399 fn wrapper_for_current_platform_handles_unsupported_targets() {
400 let policy = workspace_parent();
401 let command = std::process::Command::new("mcp-server");
404 let result = wrap_stdio_command(command, &policy);
405 assert!(result.is_err());
406 }
407}