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.
This commit is contained in:
Mufeed VH
2025-06-25 03:17:33 +05:30
parent 3dbeaa4746
commit 5b5569507d
7 changed files with 256 additions and 51 deletions

View File

@@ -50,6 +50,9 @@ zstd = "0.13"
uuid = { version = "1.6", features = ["v4", "serde"] } uuid = { version = "1.6", features = ["v4", "serde"] }
walkdir = "2" walkdir = "2"
[target.'cfg(unix)'.dependencies]
gaol = "0.2"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.26" cocoa = "0.26"
objc = "0.2" objc = "0.2"

View File

@@ -996,12 +996,20 @@ pub async fn execute_agent(
Ok(build_result) => { Ok(build_result) => {
// Create the enhanced sandbox executor // Create the enhanced sandbox executor
#[cfg(unix)]
let executor = crate::sandbox::executor::SandboxExecutor::new_with_serialization( let executor = crate::sandbox::executor::SandboxExecutor::new_with_serialization(
build_result.profile, build_result.profile,
project_path_buf.clone(), project_path_buf.clone(),
build_result.serialized 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 // Prepare the sandboxed command
let args = vec![ let args = vec![
"-p", &task, "-p", &task,

View File

@@ -1021,7 +1021,14 @@ fn create_sandboxed_claude_command(app: &AppHandle, project_path: &str) -> Resul
// Use the helper function to create sandboxed command // Use the helper function to create sandboxed command
let claude_path = find_claude_binary(app)?; 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) => { Err(e) => {
log::error!("Failed to build sandbox profile: {}, falling back to non-sandboxed", e); log::error!("Failed to build sandbox profile: {}, falling back to non-sandboxed", e);

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
#[cfg(unix)]
use gaol::sandbox::{ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods}; use gaol::sandbox::{ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods};
use log::{info, warn, error, debug}; use log::{info, warn, error, debug};
use std::env; use std::env;
@@ -8,11 +9,13 @@ use tokio::process::Command;
/// Sandbox executor for running commands in a sandboxed environment /// Sandbox executor for running commands in a sandboxed environment
pub struct SandboxExecutor { pub struct SandboxExecutor {
#[cfg(unix)]
profile: gaol::profile::Profile, profile: gaol::profile::Profile,
project_path: PathBuf, project_path: PathBuf,
serialized_profile: Option<SerializedProfile>, serialized_profile: Option<SerializedProfile>,
} }
#[cfg(unix)]
impl SandboxExecutor { impl SandboxExecutor {
/// Create a new sandbox executor with the given profile /// Create a new sandbox executor with the given profile
pub fn new(profile: gaol::profile::Profile, project_path: PathBuf) -> Self { 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<std::process::Child> {
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<SerializedProfile> {
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 /// Check if the current process should activate sandbox
pub fn should_activate_sandbox() -> bool { pub fn should_activate_sandbox() -> bool {
env::var("GAOL_SANDBOX_ACTIVE").unwrap_or_default() == "1" env::var("GAOL_SANDBOX_ACTIVE").unwrap_or_default() == "1"
} }
/// Helper to create a sandboxed tokio Command /// Helper to create a sandboxed tokio Command
#[cfg(unix)]
pub fn create_sandboxed_command( pub fn create_sandboxed_command(
command: &str, command: &str,
args: &[&str], args: &[&str],
@@ -300,6 +367,7 @@ pub enum SerializedOperation {
SystemInfoRead, SystemInfoRead,
} }
#[cfg(unix)]
fn deserialize_profile(serialized: SerializedProfile, project_path: &Path) -> Result<gaol::profile::Profile> { fn deserialize_profile(serialized: SerializedProfile, project_path: &Path) -> Result<gaol::profile::Profile> {
let mut operations = Vec::new(); 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<gaol::profile::Profile> { fn create_minimal_profile(project_path: PathBuf) -> Result<gaol::profile::Profile> {
let operations = vec![ let operations = vec![
gaol::profile::Operation::FileReadAll( gaol::profile::Operation::FileReadAll(

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
#[cfg(unix)]
use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile}; use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile};
use log::{debug, info, warn}; use log::{debug, info, warn};
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
@@ -33,7 +34,10 @@ pub struct SandboxRule {
/// Result of building a profile /// Result of building a profile
pub struct ProfileBuildResult { pub struct ProfileBuildResult {
#[cfg(unix)]
pub profile: Profile, pub profile: Profile,
#[cfg(not(unix))]
pub profile: (), // Placeholder for Windows
pub serialized: SerializedProfile, pub serialized: SerializedProfile,
} }
@@ -60,7 +64,10 @@ impl ProfileBuilder {
// If sandbox is completely disabled, return an empty profile // If sandbox is completely disabled, return an empty profile
if !sandbox_enabled { if !sandbox_enabled {
return Ok(ProfileBuildResult { return Ok(ProfileBuildResult {
#[cfg(unix)]
profile: Profile::new(vec![]).map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?, profile: Profile::new(vec![]).map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?,
#[cfg(not(unix))]
profile: (),
serialized: SerializedProfile { operations: vec![] }, serialized: SerializedProfile { operations: vec![] },
}); });
} }
@@ -112,13 +119,23 @@ impl ProfileBuilder {
} }
/// Build a gaol Profile from database rules /// Build a gaol Profile from database rules
#[cfg(unix)]
pub fn build_profile(&self, rules: Vec<SandboxRule>) -> Result<Profile> { pub fn build_profile(&self, rules: Vec<SandboxRule>) -> Result<Profile> {
let result = self.build_profile_with_serialization(rules)?; let result = self.build_profile_with_serialization(rules)?;
Ok(result.profile) Ok(result.profile)
} }
/// Build a gaol Profile from database rules (Windows stub)
#[cfg(not(unix))]
pub fn build_profile(&self, _rules: Vec<SandboxRule>) -> Result<()> {
warn!("Sandbox profiles are not supported on Windows");
Ok(())
}
/// Build a gaol Profile from database rules and return serialized operations /// Build a gaol Profile from database rules and return serialized operations
pub fn build_profile_with_serialization(&self, rules: Vec<SandboxRule>) -> Result<ProfileBuildResult> { pub fn build_profile_with_serialization(&self, rules: Vec<SandboxRule>) -> Result<ProfileBuildResult> {
#[cfg(unix)]
{
let mut operations = Vec::new(); let mut operations = Vec::new();
let mut serialized_operations = Vec::new(); let mut serialized_operations = Vec::new();
@@ -177,7 +194,32 @@ impl ProfileBuilder {
}) })
} }
#[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 /// Build a gaol Operation from a database rule
#[cfg(unix)]
fn build_operation(&self, rule: &SandboxRule) -> Result<Option<Operation>> { fn build_operation(&self, rule: &SandboxRule) -> Result<Option<Operation>> {
match self.build_operation_with_serialization(rule) { match self.build_operation_with_serialization(rule) {
Ok(Some((op, _))) => Ok(Some(op)), Ok(Some((op, _))) => Ok(Some(op)),
@@ -187,6 +229,7 @@ impl ProfileBuilder {
} }
/// Build a gaol Operation and its serialized form from a database rule /// Build a gaol Operation and its serialized form from a database rule
#[cfg(unix)]
fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result<Option<(Operation, SerializedOperation)>> { fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result<Option<(Operation, SerializedOperation)>> {
match rule.operation_type.as_str() { match rule.operation_type.as_str() {
"file_read_all" => { "file_read_all" => {
@@ -218,12 +261,14 @@ impl ProfileBuilder {
} }
/// Build a PathPattern from pattern type and value /// Build a PathPattern from pattern type and value
#[cfg(unix)]
fn build_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<PathPattern> { fn build_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<PathPattern> {
let (pattern, _, _) = self.build_path_pattern_with_info(pattern_type, pattern_value)?; let (pattern, _, _) = self.build_path_pattern_with_info(pattern_type, pattern_value)?;
Ok(pattern) Ok(pattern)
} }
/// Build a PathPattern and return additional info for serialization /// 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)> { fn build_path_pattern_with_info(&self, pattern_type: &str, pattern_value: &str) -> Result<(PathPattern, PathBuf, bool)> {
// Replace template variables // Replace template variables
let expanded_value = pattern_value let expanded_value = pattern_value
@@ -240,12 +285,14 @@ impl ProfileBuilder {
} }
/// Build an AddressPattern from pattern type and value /// Build an AddressPattern from pattern type and value
#[cfg(unix)]
fn build_address_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<AddressPattern> { fn build_address_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<AddressPattern> {
let (pattern, _) = self.build_address_pattern_with_serialization(pattern_type, pattern_value)?; let (pattern, _) = self.build_address_pattern_with_serialization(pattern_type, pattern_value)?;
Ok(pattern) Ok(pattern)
} }
/// Build an AddressPattern and its serialized form /// 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)> { fn build_address_pattern_with_serialization(&self, pattern_type: &str, pattern_value: &str) -> Result<(AddressPattern, SerializedOperation)> {
match pattern_type { match pattern_type {
"all" => Ok(( "all" => Ok((
@@ -282,6 +329,59 @@ impl ProfileBuilder {
// If no platform support specified, assume it's supported // If no platform support specified, assume it's supported
true true
} }
/// Build only the serialized operation (for Windows)
#[cfg(not(unix))]
fn build_serialized_operation(&self, rule: &SandboxRule) -> Result<Option<SerializedOperation>> {
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::<u16>()
.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 /// Load a sandbox profile by ID
@@ -351,6 +451,7 @@ pub fn load_profile_rules(conn: &Connection, profile_id: i64) -> Result<Vec<Sand
} }
/// Get or create the gaol Profile for execution /// Get or create the gaol Profile for execution
#[cfg(unix)]
pub fn get_gaol_profile(conn: &Connection, profile_id: Option<i64>, project_path: PathBuf) -> Result<Profile> { pub fn get_gaol_profile(conn: &Connection, profile_id: Option<i64>, project_path: PathBuf) -> Result<Profile> {
// Load the profile // Load the profile
let profile = if let Some(id) = profile_id { let profile = if let Some(id) = profile_id {
@@ -369,3 +470,10 @@ pub fn get_gaol_profile(conn: &Connection, profile_id: Option<i64>, project_path
let builder = ProfileBuilder::new(project_path)?; let builder = ProfileBuilder::new(project_path)?;
builder.build_profile(rules) 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<i64>, _project_path: PathBuf) -> Result<()> {
warn!("Sandbox profiles are not supported on Windows");
Ok(())
}

View File

@@ -2,8 +2,16 @@
//! //!
//! This test suite validates the sandboxing capabilities across different platforms, //! This test suite validates the sandboxing capabilities across different platforms,
//! ensuring that security policies are correctly enforced. //! ensuring that security policies are correctly enforced.
#[cfg(unix)]
#[macro_use] #[macro_use]
pub mod common; pub mod common;
#[cfg(unix)]
pub mod unit; pub mod unit;
#[cfg(unix)]
pub mod integration; pub mod integration;
#[cfg(unix)]
pub mod e2e; pub mod e2e;

View File

@@ -2,7 +2,9 @@
//! //!
//! This file integrates all the sandbox test modules and provides //! This file integrates all the sandbox test modules and provides
//! a central location for running the comprehensive test suite. //! a central location for running the comprehensive test suite.
#[path = "sandbox/mod.rs"] #![allow(dead_code)]
#[cfg(unix)]
mod sandbox; mod sandbox;
// Re-export test modules to make them discoverable // Re-export test modules to make them discoverable