sandbox_runtime/manager/
mod.rs

1//! Sandbox manager - main orchestration module.
2
3pub mod filesystem;
4pub mod network;
5pub mod state;
6
7use std::sync::Arc;
8
9use parking_lot::RwLock;
10
11use crate::config::SandboxRuntimeConfig;
12use crate::error::SandboxError;
13use crate::utils::{current_platform, check_ripgrep, Platform};
14use crate::violation::SandboxViolationStore;
15
16use self::state::ManagerState;
17
18pub use filesystem::{FsReadRestrictionConfig, FsWriteRestrictionConfig};
19
20/// The sandbox manager - main entry point for sandbox operations.
21pub struct SandboxManager {
22    state: Arc<RwLock<ManagerState>>,
23}
24
25impl Default for SandboxManager {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl SandboxManager {
32    /// Create a new sandbox manager.
33    pub fn new() -> Self {
34        Self {
35            state: Arc::new(RwLock::new(ManagerState::new())),
36        }
37    }
38
39    /// Check if the current platform is supported.
40    pub fn is_supported_platform() -> bool {
41        current_platform().is_some()
42    }
43
44    /// Check if all required dependencies are available.
45    pub fn check_dependencies(&self, config: Option<&SandboxRuntimeConfig>) -> Result<(), SandboxError> {
46        let platform = current_platform()
47            .ok_or_else(|| SandboxError::UnsupportedPlatform("Unsupported platform".to_string()))?;
48
49        // Check platform-specific dependencies
50        crate::sandbox::check_dependencies(platform)?;
51
52        // Check ripgrep (optional on macOS, recommended on Linux)
53        if platform == Platform::Linux {
54            let rg_config = config.and_then(|c| c.ripgrep.as_ref());
55            if !check_ripgrep(rg_config) {
56                tracing::warn!("ripgrep not found - dangerous file detection will be limited");
57            }
58        }
59
60        Ok(())
61    }
62
63    /// Initialize the sandbox manager with the given configuration.
64    pub async fn initialize(&self, config: SandboxRuntimeConfig) -> Result<(), SandboxError> {
65        // Validate configuration
66        config.validate()?;
67
68        // Check dependencies
69        self.check_dependencies(Some(&config))?;
70
71        let platform = current_platform()
72            .ok_or_else(|| SandboxError::UnsupportedPlatform("Unsupported platform".to_string()))?;
73
74        // Initialize proxies
75        let (http_proxy, socks_proxy) =
76            network::initialize_proxies(&config.network).await?;
77
78        let http_port = http_proxy.port();
79        let socks_port = socks_proxy.port();
80
81        // Update state
82        let mut state = self.state.write();
83        state.http_proxy = Some(http_proxy);
84        state.socks_proxy = Some(socks_proxy);
85        state.http_proxy_port = Some(http_port);
86        state.socks_proxy_port = Some(socks_port);
87
88        // Initialize platform-specific infrastructure
89        #[cfg(target_os = "linux")]
90        {
91            use crate::sandbox::linux::{generate_socket_path, SocatBridge};
92
93            // Create Unix socket bridges for proxies
94            let http_socket_path = generate_socket_path("srt-http");
95            let socks_socket_path = generate_socket_path("srt-socks");
96
97            let http_bridge =
98                SocatBridge::unix_to_tcp(http_socket_path.clone(), "localhost", http_port).await?;
99            let socks_bridge =
100                SocatBridge::unix_to_tcp(socks_socket_path.clone(), "localhost", socks_port)
101                    .await?;
102
103            state.http_socket_path = Some(http_socket_path.display().to_string());
104            state.socks_socket_path = Some(socks_socket_path.display().to_string());
105            state.bridges.push(http_bridge);
106            state.bridges.push(socks_bridge);
107        }
108
109        state.config = Some(config);
110        state.initialized = true;
111        state.network_ready = true;
112
113        tracing::info!(
114            "Sandbox manager initialized for {} (HTTP proxy: {}, SOCKS proxy: {})",
115            platform.name(),
116            http_port,
117            socks_port
118        );
119
120        Ok(())
121    }
122
123    /// Check if the manager is initialized.
124    pub fn is_initialized(&self) -> bool {
125        self.state.read().initialized
126    }
127
128    /// Get the current configuration.
129    pub fn get_config(&self) -> Option<SandboxRuntimeConfig> {
130        self.state.read().config.clone()
131    }
132
133    /// Update the configuration.
134    pub fn update_config(&self, config: SandboxRuntimeConfig) -> Result<(), SandboxError> {
135        config.validate()?;
136        self.state.write().config = Some(config);
137        Ok(())
138    }
139
140    /// Get the HTTP proxy port.
141    pub fn get_proxy_port(&self) -> Option<u16> {
142        self.state.read().http_proxy_port
143    }
144
145    /// Get the SOCKS proxy port.
146    pub fn get_socks_proxy_port(&self) -> Option<u16> {
147        self.state.read().socks_proxy_port
148    }
149
150    /// Get the HTTP socket path (Linux only).
151    #[cfg(target_os = "linux")]
152    pub fn get_http_socket_path(&self) -> Option<String> {
153        self.state.read().http_socket_path.clone()
154    }
155
156    /// Get the SOCKS socket path (Linux only).
157    #[cfg(target_os = "linux")]
158    pub fn get_socks_socket_path(&self) -> Option<String> {
159        self.state.read().socks_socket_path.clone()
160    }
161
162    /// Check if network is ready.
163    pub fn is_network_ready(&self) -> bool {
164        self.state.read().network_ready
165    }
166
167    /// Wait for network initialization.
168    pub async fn wait_for_network_initialization(&self) -> bool {
169        // Already ready in this implementation since we initialize synchronously
170        self.is_network_ready()
171    }
172
173    /// Get filesystem read restriction config.
174    pub fn get_fs_read_config(&self) -> FsReadRestrictionConfig {
175        let state = self.state.read();
176        if let Some(ref config) = state.config {
177            filesystem::process_fs_config(&config.filesystem).0
178        } else {
179            FsReadRestrictionConfig::default()
180        }
181    }
182
183    /// Get filesystem write restriction config.
184    pub fn get_fs_write_config(&self) -> FsWriteRestrictionConfig {
185        let state = self.state.read();
186        if let Some(ref config) = state.config {
187            filesystem::process_fs_config(&config.filesystem).1
188        } else {
189            FsWriteRestrictionConfig::default()
190        }
191    }
192
193    /// Get glob pattern warnings for Linux.
194    pub fn get_linux_glob_pattern_warnings(&self) -> Vec<String> {
195        #[cfg(target_os = "linux")]
196        {
197            let state = self.state.read();
198            if let Some(ref config) = state.config {
199                let mut warnings = Vec::new();
200                for path in &config.filesystem.allow_write {
201                    if crate::utils::contains_glob_chars(path) {
202                        warnings.push(format!(
203                            "Glob pattern '{}' is not supported on Linux",
204                            path
205                        ));
206                    }
207                }
208                for path in &config.filesystem.deny_write {
209                    if crate::utils::contains_glob_chars(path) {
210                        warnings.push(format!(
211                            "Glob pattern '{}' is not supported on Linux",
212                            path
213                        ));
214                    }
215                }
216                return warnings;
217            }
218        }
219        Vec::new()
220    }
221
222    /// Get the violation store.
223    pub fn get_violation_store(&self) -> Arc<SandboxViolationStore> {
224        self.state.read().violation_store.clone()
225    }
226
227    /// Wrap a command with sandbox restrictions.
228    pub async fn wrap_with_sandbox(
229        &self,
230        command: &str,
231        shell: Option<&str>,
232        custom_config: Option<SandboxRuntimeConfig>,
233    ) -> Result<String, SandboxError> {
234        // Extract needed values from state while holding the lock
235        let (config, http_port, socks_port) = {
236            let state = self.state.read();
237
238            if !state.initialized {
239                return Err(SandboxError::ExecutionFailed(
240                    "Sandbox manager not initialized".to_string(),
241                ));
242            }
243
244            let config = custom_config
245                .or_else(|| state.config.clone())
246                .ok_or_else(|| SandboxError::ExecutionFailed("No configuration available".to_string()))?;
247
248            (config, state.http_proxy_port, state.socks_proxy_port)
249        };
250
251        let _platform = current_platform()
252            .ok_or_else(|| SandboxError::UnsupportedPlatform("Unsupported platform".to_string()))?;
253
254        // Call platform-specific wrapper
255        #[cfg(target_os = "macos")]
256        {
257            let (wrapped, _log_tag) = crate::sandbox::macos::wrap_command(
258                command,
259                &config,
260                http_port,
261                socks_port,
262                shell,
263                true, // enable log monitor
264            )?;
265            Ok(wrapped)
266        }
267
268        #[cfg(target_os = "linux")]
269        {
270            let (http_socket, socks_socket) = {
271                let state = self.state.read();
272                (state.http_socket_path.clone(), state.socks_socket_path.clone())
273            };
274
275            let cwd = std::env::current_dir()?;
276            let (wrapped, warnings) = crate::sandbox::linux::generate_bwrap_command(
277                command,
278                &config,
279                &cwd,
280                http_socket.as_deref(),
281                socks_socket.as_deref(),
282                http_port.unwrap_or(3128),
283                socks_port.unwrap_or(1080),
284                shell,
285            )?;
286
287            for warning in warnings {
288                tracing::warn!("{}", warning);
289            }
290
291            Ok(wrapped)
292        }
293
294        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
295        {
296            Err(SandboxError::UnsupportedPlatform(
297                "Platform not supported".to_string(),
298            ))
299        }
300    }
301
302    /// Annotate stderr with sandbox failure information.
303    pub fn annotate_stderr_with_sandbox_failures(&self, command: &str, stderr: &str) -> String {
304        let store = self.get_violation_store();
305        let violations = store.get_violations_for_command(command);
306
307        if violations.is_empty() {
308            return stderr.to_string();
309        }
310
311        let mut annotated = stderr.to_string();
312        annotated.push_str("\n\n--- Sandbox Violations ---\n");
313        for violation in violations {
314            annotated.push_str(&format!("  {}\n", violation.line));
315        }
316
317        annotated
318    }
319
320    /// Reset the sandbox manager, cleaning up all resources.
321    pub async fn reset(&self) {
322        // Clean up temp files on macOS
323        #[cfg(target_os = "macos")]
324        {
325            crate::sandbox::macos::cleanup_temp_profiles();
326        }
327
328        let mut state = self.state.write();
329        // We need to release the lock before calling async reset
330        // So we'll just do the cleanup inline
331
332        // Stop proxies
333        if let Some(ref mut proxy) = state.http_proxy {
334            proxy.stop();
335        }
336        if let Some(ref mut proxy) = state.socks_proxy {
337            proxy.stop();
338        }
339
340        // Stop bridges (Linux)
341        #[cfg(target_os = "linux")]
342        {
343            // Note: We can't call async stop here, so we rely on Drop
344            state.bridges.clear();
345            state.http_socket_path = None;
346            state.socks_socket_path = None;
347        }
348
349        // Clear state
350        state.http_proxy = None;
351        state.socks_proxy = None;
352        state.http_proxy_port = None;
353        state.socks_proxy_port = None;
354        state.config = None;
355        state.initialized = false;
356        state.network_ready = false;
357
358        tracing::info!("Sandbox manager reset");
359    }
360}
361
362impl Drop for SandboxManager {
363    fn drop(&mut self) {
364        // Cleanup is handled by reset() or individual component Drop implementations
365    }
366}