From 5b5569507d649afcd65e2ec0282c48629bd796e3 Mon Sep 17 00:00:00 2001 From: Mufeed VH Date: Wed, 25 Jun 2025 03:17:33 +0530 Subject: [PATCH] feat(sandbox): implement cross-platform support with Windows fallback - Add conditional compilation for Unix-specific gaol sandbox functionality - Implement Windows-compatible SandboxExecutor with no-op sandboxing - Update ProfileBuilder to handle both Unix and Windows platforms - Add platform-specific dependency declaration for gaol crate - Modify agent and claude commands to use appropriate sandbox implementation - Update test modules with conditional compilation for Unix-only tests - Ensure graceful degradation on Windows with logging warnings This change enables the application to build and run on Windows while maintaining full sandbox security on Unix-like systems. --- src-tauri/Cargo.toml | 3 + src-tauri/src/commands/agents.rs | 8 ++ src-tauri/src/commands/claude.rs | 9 +- src-tauri/src/sandbox/executor.rs | 69 ++++++++++ src-tauri/src/sandbox/profile.rs | 206 +++++++++++++++++++++++------- src-tauri/tests/sandbox/mod.rs | 8 ++ src-tauri/tests/sandbox_tests.rs | 4 +- 7 files changed, 256 insertions(+), 51 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 68f4e84..89deddc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -50,6 +50,9 @@ zstd = "0.13" uuid = { version = "1.6", features = ["v4", "serde"] } walkdir = "2" +[target.'cfg(unix)'.dependencies] +gaol = "0.2" + [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.26" objc = "0.2" diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 43f3cf2..7f61f1d 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -996,12 +996,20 @@ pub async fn execute_agent( Ok(build_result) => { // Create the enhanced sandbox executor + #[cfg(unix)] let executor = crate::sandbox::executor::SandboxExecutor::new_with_serialization( build_result.profile, project_path_buf.clone(), build_result.serialized ); + #[cfg(not(unix))] + let executor = crate::sandbox::executor::SandboxExecutor::new_with_serialization( + (), + project_path_buf.clone(), + build_result.serialized + ); + // Prepare the sandboxed command let args = vec![ "-p", &task, diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index 453f011..5793c10 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -1021,7 +1021,14 @@ fn create_sandboxed_claude_command(app: &AppHandle, project_path: &str) -> Resul // Use the helper function to create sandboxed command let claude_path = find_claude_binary(app)?; - Ok(create_sandboxed_command(&claude_path, &[], &project_path_buf, profile, project_path_buf.clone())) + #[cfg(unix)] + return Ok(create_sandboxed_command(&claude_path, &[], &project_path_buf, profile, project_path_buf.clone())); + + #[cfg(not(unix))] + { + log::warn!("Sandboxing not supported on Windows, using regular command"); + Ok(create_command_with_env(&claude_path)) + } } Err(e) => { log::error!("Failed to build sandbox profile: {}, falling back to non-sandboxed", e); diff --git a/src-tauri/src/sandbox/executor.rs b/src-tauri/src/sandbox/executor.rs index 859ad32..19aec89 100644 --- a/src-tauri/src/sandbox/executor.rs +++ b/src-tauri/src/sandbox/executor.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +#[cfg(unix)] use gaol::sandbox::{ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods}; use log::{info, warn, error, debug}; use std::env; @@ -8,11 +9,13 @@ use tokio::process::Command; /// Sandbox executor for running commands in a sandboxed environment pub struct SandboxExecutor { + #[cfg(unix)] profile: gaol::profile::Profile, project_path: PathBuf, serialized_profile: Option, } +#[cfg(unix)] impl SandboxExecutor { /// Create a new sandbox executor with the given profile pub fn new(profile: gaol::profile::Profile, project_path: PathBuf) -> Self { @@ -267,12 +270,76 @@ impl SandboxExecutor { } } +// Windows implementation - no sandboxing +#[cfg(not(unix))] +impl SandboxExecutor { + /// Create a new sandbox executor (no-op on Windows) + pub fn new(_profile: (), project_path: PathBuf) -> Self { + Self { + project_path, + serialized_profile: None, + } + } + + /// Create a new sandbox executor with serialized profile (no-op on Windows) + pub fn new_with_serialization( + _profile: (), + project_path: PathBuf, + serialized_profile: SerializedProfile + ) -> Self { + Self { + project_path, + serialized_profile: Some(serialized_profile), + } + } + + /// Execute a command in the sandbox (Windows - no sandboxing) + pub fn execute_sandboxed_spawn(&self, command: &str, args: &[&str], cwd: &Path) -> Result { + info!("Executing command without sandbox on Windows: {} {:?}", command, args); + + std::process::Command::new(command) + .args(args) + .current_dir(cwd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("Failed to spawn process") + } + + /// Prepare a sandboxed tokio Command (Windows - no sandboxing) + pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command { + info!("Preparing command without sandbox on Windows: {} {:?}", command, args); + + let mut cmd = Command::new(command); + cmd.args(args) + .current_dir(cwd) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + cmd + } + + /// Extract sandbox rules (no-op on Windows) + fn extract_sandbox_rules(&self) -> Result { + Ok(SerializedProfile { operations: vec![] }) + } + + /// Activate sandbox in child process (no-op on Windows) + pub fn activate_sandbox_in_child() -> Result<()> { + debug!("Sandbox activation skipped on Windows"); + Ok(()) + } +} + /// Check if the current process should activate sandbox pub fn should_activate_sandbox() -> bool { env::var("GAOL_SANDBOX_ACTIVE").unwrap_or_default() == "1" } /// Helper to create a sandboxed tokio Command +#[cfg(unix)] pub fn create_sandboxed_command( command: &str, args: &[&str], @@ -300,6 +367,7 @@ pub enum SerializedOperation { SystemInfoRead, } +#[cfg(unix)] fn deserialize_profile(serialized: SerializedProfile, project_path: &Path) -> Result { let mut operations = Vec::new(); @@ -366,6 +434,7 @@ fn deserialize_profile(serialized: SerializedProfile, project_path: &Path) -> Re }) } +#[cfg(unix)] fn create_minimal_profile(project_path: PathBuf) -> Result { let operations = vec![ gaol::profile::Operation::FileReadAll( diff --git a/src-tauri/src/sandbox/profile.rs b/src-tauri/src/sandbox/profile.rs index 933d460..9a17a5f 100644 --- a/src-tauri/src/sandbox/profile.rs +++ b/src-tauri/src/sandbox/profile.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +#[cfg(unix)] use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile}; use log::{debug, info, warn}; use rusqlite::{params, Connection}; @@ -33,7 +34,10 @@ pub struct SandboxRule { /// Result of building a profile pub struct ProfileBuildResult { + #[cfg(unix)] pub profile: Profile, + #[cfg(not(unix))] + pub profile: (), // Placeholder for Windows pub serialized: SerializedProfile, } @@ -60,7 +64,10 @@ impl ProfileBuilder { // If sandbox is completely disabled, return an empty profile if !sandbox_enabled { return Ok(ProfileBuildResult { + #[cfg(unix)] profile: Profile::new(vec![]).map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?, + #[cfg(not(unix))] + profile: (), serialized: SerializedProfile { operations: vec![] }, }); } @@ -112,72 +119,107 @@ impl ProfileBuilder { } /// Build a gaol Profile from database rules + #[cfg(unix)] pub fn build_profile(&self, rules: Vec) -> Result { let result = self.build_profile_with_serialization(rules)?; Ok(result.profile) } + /// Build a gaol Profile from database rules (Windows stub) + #[cfg(not(unix))] + pub fn build_profile(&self, _rules: Vec) -> Result<()> { + warn!("Sandbox profiles are not supported on Windows"); + Ok(()) + } + /// Build a gaol Profile from database rules and return serialized operations pub fn build_profile_with_serialization(&self, rules: Vec) -> Result { - let mut operations = Vec::new(); - let mut serialized_operations = Vec::new(); - - for rule in rules { - if !rule.enabled { - continue; - } + #[cfg(unix)] + { + let mut operations = Vec::new(); + let mut serialized_operations = Vec::new(); - // Check platform support - if !self.is_rule_supported_on_platform(&rule) { - debug!("Skipping rule {} - not supported on current platform", rule.operation_type); - continue; - } - - match self.build_operation_with_serialization(&rule) { - Ok(Some((op, serialized))) => { - // Check if operation is supported on current platform - if matches!(op.support(), gaol::profile::OperationSupportLevel::CanBeAllowed) { - operations.push(op); - serialized_operations.push(serialized); - } else { - warn!("Operation {:?} not supported at desired level on current platform", rule.operation_type); + for rule in rules { + if !rule.enabled { + continue; + } + + // Check platform support + if !self.is_rule_supported_on_platform(&rule) { + debug!("Skipping rule {} - not supported on current platform", rule.operation_type); + continue; + } + + match self.build_operation_with_serialization(&rule) { + Ok(Some((op, serialized))) => { + // Check if operation is supported on current platform + if matches!(op.support(), gaol::profile::OperationSupportLevel::CanBeAllowed) { + operations.push(op); + serialized_operations.push(serialized); + } else { + warn!("Operation {:?} not supported at desired level on current platform", rule.operation_type); + } + }, + Ok(None) => { + debug!("Skipping unsupported operation type: {}", rule.operation_type); + }, + Err(e) => { + warn!("Failed to build operation for rule {}: {}", rule.id.unwrap_or(0), e); } - }, - Ok(None) => { - debug!("Skipping unsupported operation type: {}", rule.operation_type); - }, - Err(e) => { - warn!("Failed to build operation for rule {}: {}", rule.id.unwrap_or(0), e); } } - } - - // Ensure project path access is included - let has_project_access = serialized_operations.iter().any(|op| { - matches!(op, SerializedOperation::FileReadAll { path, is_subpath: true } if path == &self.project_path) - }); - - if !has_project_access { - operations.push(Operation::FileReadAll(PathPattern::Subpath(self.project_path.clone()))); - serialized_operations.push(SerializedOperation::FileReadAll { - path: self.project_path.clone(), - is_subpath: true, + + // Ensure project path access is included + let has_project_access = serialized_operations.iter().any(|op| { + matches!(op, SerializedOperation::FileReadAll { path, is_subpath: true } if path == &self.project_path) }); + + if !has_project_access { + operations.push(Operation::FileReadAll(PathPattern::Subpath(self.project_path.clone()))); + serialized_operations.push(SerializedOperation::FileReadAll { + path: self.project_path.clone(), + is_subpath: true, + }); + } + + // Create the profile + let profile = Profile::new(operations) + .map_err(|_| anyhow::anyhow!("Failed to create sandbox profile - some operations may not be supported on this platform"))?; + + Ok(ProfileBuildResult { + profile, + serialized: SerializedProfile { + operations: serialized_operations, + }, + }) } - // Create the profile - let profile = Profile::new(operations) - .map_err(|_| anyhow::anyhow!("Failed to create sandbox profile - some operations may not be supported on this platform"))?; - - Ok(ProfileBuildResult { - profile, - serialized: SerializedProfile { - operations: serialized_operations, - }, - }) + #[cfg(not(unix))] + { + // On Windows, we just create a serialized profile without actual sandboxing + let mut serialized_operations = Vec::new(); + + for rule in rules { + if !rule.enabled { + continue; + } + + if let Ok(Some(serialized)) = self.build_serialized_operation(&rule) { + serialized_operations.push(serialized); + } + } + + Ok(ProfileBuildResult { + profile: (), + serialized: SerializedProfile { + operations: serialized_operations, + }, + }) + } } /// Build a gaol Operation from a database rule + #[cfg(unix)] fn build_operation(&self, rule: &SandboxRule) -> Result> { match self.build_operation_with_serialization(rule) { Ok(Some((op, _))) => Ok(Some(op)), @@ -187,6 +229,7 @@ impl ProfileBuilder { } /// Build a gaol Operation and its serialized form from a database rule + #[cfg(unix)] fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result> { match rule.operation_type.as_str() { "file_read_all" => { @@ -218,12 +261,14 @@ impl ProfileBuilder { } /// Build a PathPattern from pattern type and value + #[cfg(unix)] fn build_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result { let (pattern, _, _) = self.build_path_pattern_with_info(pattern_type, pattern_value)?; Ok(pattern) } /// Build a PathPattern and return additional info for serialization + #[cfg(unix)] fn build_path_pattern_with_info(&self, pattern_type: &str, pattern_value: &str) -> Result<(PathPattern, PathBuf, bool)> { // Replace template variables let expanded_value = pattern_value @@ -240,12 +285,14 @@ impl ProfileBuilder { } /// Build an AddressPattern from pattern type and value + #[cfg(unix)] fn build_address_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result { let (pattern, _) = self.build_address_pattern_with_serialization(pattern_type, pattern_value)?; Ok(pattern) } /// Build an AddressPattern and its serialized form + #[cfg(unix)] fn build_address_pattern_with_serialization(&self, pattern_type: &str, pattern_value: &str) -> Result<(AddressPattern, SerializedOperation)> { match pattern_type { "all" => Ok(( @@ -282,6 +329,59 @@ impl ProfileBuilder { // If no platform support specified, assume it's supported true } + + /// Build only the serialized operation (for Windows) + #[cfg(not(unix))] + fn build_serialized_operation(&self, rule: &SandboxRule) -> Result> { + let pattern_value = self.expand_pattern_value(&rule.pattern_value); + + match rule.operation_type.as_str() { + "file_read_all" => { + let (path, is_subpath) = self.parse_path_pattern(&rule.pattern_type, &pattern_value)?; + Ok(Some(SerializedOperation::FileReadAll { path, is_subpath })) + } + "file_read_metadata" => { + let (path, is_subpath) = self.parse_path_pattern(&rule.pattern_type, &pattern_value)?; + Ok(Some(SerializedOperation::FileReadMetadata { path, is_subpath })) + } + "network_outbound" => { + Ok(Some(SerializedOperation::NetworkOutbound { pattern: pattern_value })) + } + "network_tcp" => { + let port = pattern_value.parse::() + .context("Invalid TCP port")?; + Ok(Some(SerializedOperation::NetworkTcp { port })) + } + "network_local_socket" => { + let path = PathBuf::from(pattern_value); + Ok(Some(SerializedOperation::NetworkLocalSocket { path })) + } + "system_info_read" => { + Ok(Some(SerializedOperation::SystemInfoRead)) + } + _ => Ok(None), + } + } + + /// Helper method to expand pattern values (Windows version) + #[cfg(not(unix))] + fn expand_pattern_value(&self, pattern_value: &str) -> String { + pattern_value + .replace("{{PROJECT_PATH}}", &self.project_path.to_string_lossy()) + .replace("{{HOME}}", &self.home_dir.to_string_lossy()) + } + + /// Helper method to parse path patterns (Windows version) + #[cfg(not(unix))] + fn parse_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<(PathBuf, bool)> { + let path = PathBuf::from(pattern_value); + + match pattern_type { + "literal" => Ok((path, false)), + "subpath" => Ok((path, true)), + _ => Err(anyhow::anyhow!("Unknown path pattern type: {}", pattern_type)) + } + } } /// Load a sandbox profile by ID @@ -351,6 +451,7 @@ pub fn load_profile_rules(conn: &Connection, profile_id: i64) -> Result, project_path: PathBuf) -> Result { // Load the profile let profile = if let Some(id) = profile_id { @@ -368,4 +469,11 @@ pub fn get_gaol_profile(conn: &Connection, profile_id: Option, project_path // Build the gaol profile let builder = ProfileBuilder::new(project_path)?; builder.build_profile(rules) +} + +/// Get or create the gaol Profile for execution (Windows stub) +#[cfg(not(unix))] +pub fn get_gaol_profile(_conn: &Connection, _profile_id: Option, _project_path: PathBuf) -> Result<()> { + warn!("Sandbox profiles are not supported on Windows"); + Ok(()) } \ No newline at end of file diff --git a/src-tauri/tests/sandbox/mod.rs b/src-tauri/tests/sandbox/mod.rs index aa7ad1d..bd6c63e 100644 --- a/src-tauri/tests/sandbox/mod.rs +++ b/src-tauri/tests/sandbox/mod.rs @@ -2,8 +2,16 @@ //! //! This test suite validates the sandboxing capabilities across different platforms, //! ensuring that security policies are correctly enforced. + +#[cfg(unix)] #[macro_use] pub mod common; + +#[cfg(unix)] pub mod unit; + +#[cfg(unix)] pub mod integration; + +#[cfg(unix)] pub mod e2e; \ No newline at end of file diff --git a/src-tauri/tests/sandbox_tests.rs b/src-tauri/tests/sandbox_tests.rs index a7838bb..79991e2 100644 --- a/src-tauri/tests/sandbox_tests.rs +++ b/src-tauri/tests/sandbox_tests.rs @@ -2,7 +2,9 @@ //! //! This file integrates all the sandbox test modules and provides //! a central location for running the comprehensive test suite. -#[path = "sandbox/mod.rs"] +#![allow(dead_code)] + +#[cfg(unix)] mod sandbox; // Re-export test modules to make them discoverable