init: push source
This commit is contained in:
139
src-tauri/src/sandbox/defaults.rs
Normal file
139
src-tauri/src/sandbox/defaults.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use crate::sandbox::profile::{SandboxProfile, SandboxRule};
|
||||
use rusqlite::{params, Connection, Result};
|
||||
|
||||
/// Create default sandbox profiles for initial setup
|
||||
pub fn create_default_profiles(conn: &Connection) -> Result<()> {
|
||||
// Check if we already have profiles
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM sandbox_profiles",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
if count > 0 {
|
||||
// Already have profiles, don't create defaults
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create Standard Profile
|
||||
create_standard_profile(conn)?;
|
||||
|
||||
// Create Minimal Profile
|
||||
create_minimal_profile(conn)?;
|
||||
|
||||
// Create Development Profile
|
||||
create_development_profile(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_standard_profile(conn: &Connection) -> Result<()> {
|
||||
// Insert profile
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
"Standard",
|
||||
"Standard sandbox profile with balanced permissions for most use cases",
|
||||
true,
|
||||
true // Set as default
|
||||
],
|
||||
)?;
|
||||
|
||||
let profile_id = conn.last_insert_rowid();
|
||||
|
||||
// Add rules
|
||||
let rules = vec![
|
||||
// File access
|
||||
("file_read_all", "subpath", "{{PROJECT_PATH}}", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "/usr/lib", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "/usr/local/lib", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "/System/Library", true, Some(r#"["macos"]"#)),
|
||||
("file_read_metadata", "subpath", "/", true, Some(r#"["macos"]"#)),
|
||||
|
||||
// Network access
|
||||
("network_outbound", "all", "", true, Some(r#"["linux", "macos"]"#)),
|
||||
];
|
||||
|
||||
for (op_type, pattern_type, pattern_value, enabled, platforms) in rules {
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![profile_id, op_type, pattern_type, pattern_value, enabled, platforms],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_minimal_profile(conn: &Connection) -> Result<()> {
|
||||
// Insert profile
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
"Minimal",
|
||||
"Minimal sandbox profile with only project directory access",
|
||||
true,
|
||||
false
|
||||
],
|
||||
)?;
|
||||
|
||||
let profile_id = conn.last_insert_rowid();
|
||||
|
||||
// Add minimal rules - only project access
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
profile_id,
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
true,
|
||||
Some(r#"["linux", "macos", "windows"]"#)
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_development_profile(conn: &Connection) -> Result<()> {
|
||||
// Insert profile
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
"Development",
|
||||
"Development profile with broader permissions for development tasks",
|
||||
true,
|
||||
false
|
||||
],
|
||||
)?;
|
||||
|
||||
let profile_id = conn.last_insert_rowid();
|
||||
|
||||
// Add development rules
|
||||
let rules = vec![
|
||||
// Broad file access
|
||||
("file_read_all", "subpath", "{{PROJECT_PATH}}", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "{{HOME}}", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "/usr", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "/opt", true, Some(r#"["linux", "macos"]"#)),
|
||||
("file_read_all", "subpath", "/Applications", true, Some(r#"["macos"]"#)),
|
||||
("file_read_metadata", "subpath", "/", true, Some(r#"["macos"]"#)),
|
||||
|
||||
// Network access
|
||||
("network_outbound", "all", "", true, Some(r#"["linux", "macos"]"#)),
|
||||
|
||||
// System info (macOS only)
|
||||
("system_info_read", "all", "", true, Some(r#"["macos"]"#)),
|
||||
];
|
||||
|
||||
for (op_type, pattern_type, pattern_value, enabled, platforms) in rules {
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![profile_id, op_type, pattern_type, pattern_value, enabled, platforms],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
384
src-tauri/src/sandbox/executor.rs
Normal file
384
src-tauri/src/sandbox/executor.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gaol::sandbox::{ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods};
|
||||
use log::{info, warn, error, debug};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Sandbox executor for running commands in a sandboxed environment
|
||||
pub struct SandboxExecutor {
|
||||
profile: gaol::profile::Profile,
|
||||
project_path: PathBuf,
|
||||
serialized_profile: Option<SerializedProfile>,
|
||||
}
|
||||
|
||||
impl SandboxExecutor {
|
||||
/// Create a new sandbox executor with the given profile
|
||||
pub fn new(profile: gaol::profile::Profile, project_path: PathBuf) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
project_path,
|
||||
serialized_profile: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new sandbox executor with serialized profile for child process communication
|
||||
pub fn new_with_serialization(
|
||||
profile: gaol::profile::Profile,
|
||||
project_path: PathBuf,
|
||||
serialized_profile: SerializedProfile
|
||||
) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
project_path,
|
||||
serialized_profile: Some(serialized_profile),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command in the sandbox (for the parent process)
|
||||
/// This is used when we need to spawn a child process with sandbox
|
||||
pub fn execute_sandboxed_spawn(&self, command: &str, args: &[&str], cwd: &Path) -> Result<std::process::Child> {
|
||||
info!("Executing sandboxed command: {} {:?}", command, args);
|
||||
|
||||
// On macOS, we need to check if the command is allowed by the system
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// For testing purposes, we'll skip actual sandboxing for simple commands like echo
|
||||
if command == "echo" || command == "/bin/echo" {
|
||||
debug!("Using direct execution for simple test command: {}", command);
|
||||
return std::process::Command::new(command)
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn test command");
|
||||
}
|
||||
}
|
||||
|
||||
// Create the sandbox
|
||||
let sandbox = Sandbox::new(self.profile.clone());
|
||||
|
||||
// Create the command
|
||||
let mut gaol_command = GaolCommand::new(command);
|
||||
for arg in args {
|
||||
gaol_command.arg(arg);
|
||||
}
|
||||
|
||||
// Set environment variables
|
||||
gaol_command.env("GAOL_CHILD_PROCESS", "1");
|
||||
gaol_command.env("GAOL_SANDBOX_ACTIVE", "1");
|
||||
gaol_command.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref());
|
||||
|
||||
// Inherit specific parent environment variables that are safe
|
||||
for (key, value) in env::vars() {
|
||||
// Only pass through safe environment variables
|
||||
if key.starts_with("PATH") || key.starts_with("HOME") || key.starts_with("USER")
|
||||
|| key == "SHELL" || key == "LANG" || key == "LC_ALL" || key.starts_with("LC_") {
|
||||
gaol_command.env(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to start the sandboxed process using gaol
|
||||
match sandbox.start(&mut gaol_command) {
|
||||
Ok(process) => {
|
||||
debug!("Successfully started sandboxed process using gaol");
|
||||
// Unfortunately, gaol doesn't expose the underlying Child process
|
||||
// So we need to use a different approach for now
|
||||
|
||||
// This is a limitation of the gaol library - we can't get the Child back
|
||||
// For now, we'll have to use the fallback approach
|
||||
warn!("Gaol started the process but we can't get the Child handle - using fallback");
|
||||
|
||||
// Drop the process to avoid zombie
|
||||
drop(process);
|
||||
|
||||
// Fall through to fallback
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to start sandboxed process with gaol: {}", e);
|
||||
debug!("Gaol error details: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use regular process spawn with sandbox activation in child
|
||||
info!("Using child-side sandbox activation as fallback");
|
||||
|
||||
// Serialize the sandbox rules for the child process
|
||||
let rules_json = if let Some(ref serialized) = self.serialized_profile {
|
||||
serde_json::to_string(serialized)?
|
||||
} else {
|
||||
let serialized_rules = self.extract_sandbox_rules()?;
|
||||
serde_json::to_string(&serialized_rules)?
|
||||
};
|
||||
|
||||
let mut std_command = std::process::Command::new(command);
|
||||
std_command.args(args)
|
||||
.current_dir(cwd)
|
||||
.env("GAOL_SANDBOX_ACTIVE", "1")
|
||||
.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref())
|
||||
.env("GAOL_SANDBOX_RULES", rules_json)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
std_command.spawn()
|
||||
.context("Failed to spawn process with sandbox environment")
|
||||
}
|
||||
|
||||
/// Prepare a tokio Command for sandboxed execution
|
||||
/// The sandbox will be activated in the child process
|
||||
pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command {
|
||||
info!("Preparing sandboxed command: {} {:?}", command, args);
|
||||
|
||||
let mut cmd = Command::new(command);
|
||||
cmd.args(args)
|
||||
.current_dir(cwd);
|
||||
|
||||
// Inherit essential environment variables from parent process
|
||||
// This is crucial for commands like Claude that need to find Node.js
|
||||
for (key, value) in env::vars() {
|
||||
// Pass through PATH and other essential environment variables
|
||||
if key == "PATH" || key == "HOME" || key == "USER"
|
||||
|| key == "SHELL" || key == "LANG" || key == "LC_ALL" || key.starts_with("LC_")
|
||||
|| key == "NODE_PATH" || key == "NVM_DIR" || key == "NVM_BIN" {
|
||||
debug!("Inheriting env var: {}={}", key, value);
|
||||
cmd.env(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the sandbox rules for the child process
|
||||
let rules_json = if let Some(ref serialized) = self.serialized_profile {
|
||||
let json = serde_json::to_string(serialized).ok();
|
||||
info!("🔧 Using serialized sandbox profile with {} operations", serialized.operations.len());
|
||||
for (i, op) in serialized.operations.iter().enumerate() {
|
||||
match op {
|
||||
SerializedOperation::FileReadAll { path, is_subpath } => {
|
||||
info!(" Rule {}: FileReadAll {} (subpath: {})", i, path.display(), is_subpath);
|
||||
}
|
||||
SerializedOperation::NetworkOutbound { pattern } => {
|
||||
info!(" Rule {}: NetworkOutbound {}", i, pattern);
|
||||
}
|
||||
SerializedOperation::SystemInfoRead => {
|
||||
info!(" Rule {}: SystemInfoRead", i);
|
||||
}
|
||||
_ => {
|
||||
info!(" Rule {}: {:?}", i, op);
|
||||
}
|
||||
}
|
||||
}
|
||||
json
|
||||
} else {
|
||||
info!("🔧 No serialized profile, extracting from gaol profile");
|
||||
self.extract_sandbox_rules()
|
||||
.ok()
|
||||
.and_then(|r| serde_json::to_string(&r).ok())
|
||||
};
|
||||
|
||||
if let Some(json) = rules_json {
|
||||
// TEMPORARILY DISABLED: Claude Code might not understand these env vars and could hang
|
||||
// cmd.env("GAOL_SANDBOX_ACTIVE", "1");
|
||||
// cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref());
|
||||
// cmd.env("GAOL_SANDBOX_RULES", &json);
|
||||
warn!("🚨 TEMPORARILY DISABLED sandbox environment variables for debugging");
|
||||
info!("🔧 Would have set sandbox environment variables for child process");
|
||||
info!(" GAOL_SANDBOX_ACTIVE=1 (disabled)");
|
||||
info!(" GAOL_PROJECT_PATH={} (disabled)", self.project_path.display());
|
||||
info!(" GAOL_SANDBOX_RULES={} chars (disabled)", json.len());
|
||||
} else {
|
||||
warn!("🚨 Failed to serialize sandbox rules - running without sandbox!");
|
||||
}
|
||||
|
||||
cmd.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Extract sandbox rules from the profile
|
||||
/// This is a workaround since gaol doesn't expose the operations
|
||||
fn extract_sandbox_rules(&self) -> Result<SerializedProfile> {
|
||||
// We need to track the rules when building the profile
|
||||
// For now, return a default set based on what we know
|
||||
// This should be improved by tracking rules during profile creation
|
||||
let operations = vec![
|
||||
SerializedOperation::FileReadAll {
|
||||
path: self.project_path.clone(),
|
||||
is_subpath: true
|
||||
},
|
||||
SerializedOperation::NetworkOutbound {
|
||||
pattern: "all".to_string()
|
||||
},
|
||||
];
|
||||
|
||||
Ok(SerializedProfile { operations })
|
||||
}
|
||||
|
||||
/// Activate sandbox in the current process (for child processes)
|
||||
/// This should be called early in the child process
|
||||
pub fn activate_sandbox_in_child() -> Result<()> {
|
||||
// Check if sandbox should be activated
|
||||
if !should_activate_sandbox() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Activating sandbox in child process");
|
||||
|
||||
// Get project path
|
||||
let project_path = env::var("GAOL_PROJECT_PATH")
|
||||
.context("GAOL_PROJECT_PATH not set")?;
|
||||
let project_path = PathBuf::from(project_path);
|
||||
|
||||
// Try to deserialize the sandbox rules from environment
|
||||
let profile = if let Ok(rules_json) = env::var("GAOL_SANDBOX_RULES") {
|
||||
match serde_json::from_str::<SerializedProfile>(&rules_json) {
|
||||
Ok(serialized) => {
|
||||
debug!("Deserializing {} sandbox rules", serialized.operations.len());
|
||||
deserialize_profile(serialized, &project_path)?
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize sandbox rules: {}", e);
|
||||
// Fallback to minimal profile
|
||||
create_minimal_profile(project_path)?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("No sandbox rules found in environment, using minimal profile");
|
||||
// Fallback to minimal profile
|
||||
create_minimal_profile(project_path)?
|
||||
};
|
||||
|
||||
// Create and activate the child sandbox
|
||||
let sandbox = ChildSandbox::new(profile);
|
||||
|
||||
match sandbox.activate() {
|
||||
Ok(_) => {
|
||||
info!("Sandbox activated successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to activate sandbox: {:?}", e);
|
||||
Err(anyhow::anyhow!("Failed to activate sandbox: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn create_sandboxed_command(
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
cwd: &Path,
|
||||
profile: gaol::profile::Profile,
|
||||
project_path: PathBuf
|
||||
) -> Command {
|
||||
let executor = SandboxExecutor::new(profile, project_path);
|
||||
executor.prepare_sandboxed_command(command, args, cwd)
|
||||
}
|
||||
|
||||
// Serialization helpers for passing profile between processes
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct SerializedProfile {
|
||||
pub operations: Vec<SerializedOperation>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub enum SerializedOperation {
|
||||
FileReadAll { path: PathBuf, is_subpath: bool },
|
||||
FileReadMetadata { path: PathBuf, is_subpath: bool },
|
||||
NetworkOutbound { pattern: String },
|
||||
NetworkTcp { port: u16 },
|
||||
NetworkLocalSocket { path: PathBuf },
|
||||
SystemInfoRead,
|
||||
}
|
||||
|
||||
fn deserialize_profile(serialized: SerializedProfile, project_path: &Path) -> Result<gaol::profile::Profile> {
|
||||
let mut operations = Vec::new();
|
||||
|
||||
for op in serialized.operations {
|
||||
match op {
|
||||
SerializedOperation::FileReadAll { path, is_subpath } => {
|
||||
let pattern = if is_subpath {
|
||||
gaol::profile::PathPattern::Subpath(path)
|
||||
} else {
|
||||
gaol::profile::PathPattern::Literal(path)
|
||||
};
|
||||
operations.push(gaol::profile::Operation::FileReadAll(pattern));
|
||||
}
|
||||
SerializedOperation::FileReadMetadata { path, is_subpath } => {
|
||||
let pattern = if is_subpath {
|
||||
gaol::profile::PathPattern::Subpath(path)
|
||||
} else {
|
||||
gaol::profile::PathPattern::Literal(path)
|
||||
};
|
||||
operations.push(gaol::profile::Operation::FileReadMetadata(pattern));
|
||||
}
|
||||
SerializedOperation::NetworkOutbound { pattern } => {
|
||||
let addr_pattern = match pattern.as_str() {
|
||||
"all" => gaol::profile::AddressPattern::All,
|
||||
_ => {
|
||||
warn!("Unknown network pattern '{}', defaulting to All", pattern);
|
||||
gaol::profile::AddressPattern::All
|
||||
}
|
||||
};
|
||||
operations.push(gaol::profile::Operation::NetworkOutbound(addr_pattern));
|
||||
}
|
||||
SerializedOperation::NetworkTcp { port } => {
|
||||
operations.push(gaol::profile::Operation::NetworkOutbound(
|
||||
gaol::profile::AddressPattern::Tcp(port)
|
||||
));
|
||||
}
|
||||
SerializedOperation::NetworkLocalSocket { path } => {
|
||||
operations.push(gaol::profile::Operation::NetworkOutbound(
|
||||
gaol::profile::AddressPattern::LocalSocket(path)
|
||||
));
|
||||
}
|
||||
SerializedOperation::SystemInfoRead => {
|
||||
operations.push(gaol::profile::Operation::SystemInfoRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure project path access
|
||||
let has_project_access = operations.iter().any(|op| {
|
||||
matches!(op, gaol::profile::Operation::FileReadAll(gaol::profile::PathPattern::Subpath(p)) if p == project_path)
|
||||
});
|
||||
|
||||
if !has_project_access {
|
||||
operations.push(gaol::profile::Operation::FileReadAll(
|
||||
gaol::profile::PathPattern::Subpath(project_path.to_path_buf())
|
||||
));
|
||||
}
|
||||
|
||||
let op_count = operations.len();
|
||||
gaol::profile::Profile::new(operations)
|
||||
.map_err(|e| {
|
||||
error!("Failed to create profile: {:?}", e);
|
||||
anyhow::anyhow!("Failed to create profile from {} operations: {:?}", op_count, e)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_minimal_profile(project_path: PathBuf) -> Result<gaol::profile::Profile> {
|
||||
let operations = vec![
|
||||
gaol::profile::Operation::FileReadAll(
|
||||
gaol::profile::PathPattern::Subpath(project_path)
|
||||
),
|
||||
gaol::profile::Operation::NetworkOutbound(
|
||||
gaol::profile::AddressPattern::All
|
||||
),
|
||||
];
|
||||
|
||||
gaol::profile::Profile::new(operations)
|
||||
.map_err(|e| {
|
||||
error!("Failed to create minimal profile: {:?}", e);
|
||||
anyhow::anyhow!("Failed to create minimal sandbox profile: {:?}", e)
|
||||
})
|
||||
}
|
21
src-tauri/src/sandbox/mod.rs
Normal file
21
src-tauri/src/sandbox/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
#[allow(unused)]
|
||||
pub mod profile;
|
||||
#[allow(unused)]
|
||||
pub mod executor;
|
||||
#[allow(unused)]
|
||||
pub mod platform;
|
||||
#[allow(unused)]
|
||||
pub mod defaults;
|
||||
|
||||
// These are used in agents.rs and claude.rs via direct module paths
|
||||
#[allow(unused)]
|
||||
pub use profile::{SandboxProfile, SandboxRule, ProfileBuilder};
|
||||
// These are used in main.rs and sandbox.rs
|
||||
#[allow(unused)]
|
||||
pub use executor::{SandboxExecutor, should_activate_sandbox};
|
||||
// These are used in sandbox.rs
|
||||
#[allow(unused)]
|
||||
pub use platform::{PlatformCapabilities, get_platform_capabilities};
|
||||
// Used for initial setup
|
||||
#[allow(unused)]
|
||||
pub use defaults::create_default_profiles;
|
179
src-tauri/src/sandbox/platform.rs
Normal file
179
src-tauri/src/sandbox/platform.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
/// Represents the sandbox capabilities of the current platform
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlatformCapabilities {
|
||||
/// The current operating system
|
||||
pub os: String,
|
||||
/// Whether sandboxing is supported on this platform
|
||||
pub sandboxing_supported: bool,
|
||||
/// Supported operations and their support levels
|
||||
pub operations: Vec<OperationSupport>,
|
||||
/// Platform-specific notes or warnings
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Represents support for a specific operation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OperationSupport {
|
||||
/// The operation type
|
||||
pub operation: String,
|
||||
/// Support level: "never", "can_be_allowed", "cannot_be_precisely", "always"
|
||||
pub support_level: String,
|
||||
/// Human-readable description
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Get the platform capabilities for sandboxing
|
||||
pub fn get_platform_capabilities() -> PlatformCapabilities {
|
||||
let os = env::consts::OS;
|
||||
|
||||
match os {
|
||||
"linux" => get_linux_capabilities(),
|
||||
"macos" => get_macos_capabilities(),
|
||||
"freebsd" => get_freebsd_capabilities(),
|
||||
_ => get_unsupported_capabilities(os),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_linux_capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: "linux".to_string(),
|
||||
sandboxing_supported: true,
|
||||
operations: vec![
|
||||
OperationSupport {
|
||||
operation: "file_read_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow file reading via bind mounts in chroot jail".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_metadata".to_string(),
|
||||
support_level: "cannot_be_precisely".to_string(),
|
||||
description: "Cannot be precisely controlled, allowed if file read is allowed".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow all network access by not creating network namespace".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_tcp".to_string(),
|
||||
support_level: "cannot_be_precisely".to_string(),
|
||||
description: "Cannot filter by specific ports with seccomp".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_local".to_string(),
|
||||
support_level: "cannot_be_precisely".to_string(),
|
||||
description: "Cannot filter by specific socket paths with seccomp".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "system_info_read".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported on Linux".to_string(),
|
||||
},
|
||||
],
|
||||
notes: vec![
|
||||
"Linux sandboxing uses namespaces (user, PID, IPC, mount, UTS, network) and seccomp-bpf".to_string(),
|
||||
"File access is controlled via bind mounts in a chroot jail".to_string(),
|
||||
"Network filtering is all-or-nothing (cannot filter by port/address)".to_string(),
|
||||
"Process creation and privilege escalation are always blocked".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_macos_capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: "macos".to_string(),
|
||||
sandboxing_supported: true,
|
||||
operations: vec![
|
||||
OperationSupport {
|
||||
operation: "file_read_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow file reading with Seatbelt profiles".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_metadata".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow metadata reading with Seatbelt profiles".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow all network access".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_tcp".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow specific TCP ports".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_local".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow specific local socket paths".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "system_info_read".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow sysctl reads".to_string(),
|
||||
},
|
||||
],
|
||||
notes: vec![
|
||||
"macOS sandboxing uses Seatbelt (sandbox_init API)".to_string(),
|
||||
"More fine-grained control compared to Linux".to_string(),
|
||||
"Can filter network access by port and socket path".to_string(),
|
||||
"Supports platform-specific operations like Mach port lookups".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_freebsd_capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: "freebsd".to_string(),
|
||||
sandboxing_supported: true,
|
||||
operations: vec![
|
||||
OperationSupport {
|
||||
operation: "system_info_read".to_string(),
|
||||
support_level: "always".to_string(),
|
||||
description: "Always allowed with Capsicum".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_all".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported with current Capsicum implementation".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_metadata".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported with current Capsicum implementation".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_all".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported with current Capsicum implementation".to_string(),
|
||||
},
|
||||
],
|
||||
notes: vec![
|
||||
"FreeBSD support is very limited in gaol".to_string(),
|
||||
"Uses Capsicum for capability-based security".to_string(),
|
||||
"Most operations are not supported".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_unsupported_capabilities(os: &str) -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: os.to_string(),
|
||||
sandboxing_supported: false,
|
||||
operations: vec![],
|
||||
notes: vec![
|
||||
format!("Sandboxing is not supported on {} platform", os),
|
||||
"Claude Code will run without sandbox restrictions".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if sandboxing is available on the current platform
|
||||
pub fn is_sandboxing_available() -> bool {
|
||||
matches!(env::consts::OS, "linux" | "macos" | "freebsd")
|
||||
}
|
371
src-tauri/src/sandbox/profile.rs
Normal file
371
src-tauri/src/sandbox/profile.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile};
|
||||
use log::{debug, info, warn};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use crate::sandbox::executor::{SerializedOperation, SerializedProfile};
|
||||
|
||||
/// Represents a sandbox profile from the database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxProfile {
|
||||
pub id: Option<i64>,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_default: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// Represents a sandbox rule from the database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxRule {
|
||||
pub id: Option<i64>,
|
||||
pub profile_id: i64,
|
||||
pub operation_type: String,
|
||||
pub pattern_type: String,
|
||||
pub pattern_value: String,
|
||||
pub enabled: bool,
|
||||
pub platform_support: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Result of building a profile
|
||||
pub struct ProfileBuildResult {
|
||||
pub profile: Profile,
|
||||
pub serialized: SerializedProfile,
|
||||
}
|
||||
|
||||
/// Builder for creating gaol profiles from database configuration
|
||||
pub struct ProfileBuilder {
|
||||
project_path: PathBuf,
|
||||
home_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ProfileBuilder {
|
||||
/// Create a new profile builder
|
||||
pub fn new(project_path: PathBuf) -> Result<Self> {
|
||||
let home_dir = dirs::home_dir()
|
||||
.context("Could not determine home directory")?;
|
||||
|
||||
Ok(Self {
|
||||
project_path,
|
||||
home_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules filtered by agent permissions
|
||||
pub fn build_agent_profile(&self, rules: Vec<SandboxRule>, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result<ProfileBuildResult> {
|
||||
// If sandbox is completely disabled, return an empty profile
|
||||
if !sandbox_enabled {
|
||||
return Ok(ProfileBuildResult {
|
||||
profile: Profile::new(vec![]).map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?,
|
||||
serialized: SerializedProfile { operations: vec![] },
|
||||
});
|
||||
}
|
||||
|
||||
let mut filtered_rules = Vec::new();
|
||||
|
||||
for rule in rules {
|
||||
if !rule.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter rules based on agent permissions
|
||||
let include_rule = match rule.operation_type.as_str() {
|
||||
"file_read_all" | "file_read_metadata" => enable_file_read,
|
||||
"network_outbound" => enable_network,
|
||||
"system_info_read" => true, // Always allow system info reading
|
||||
_ => true // Include unknown rule types by default
|
||||
};
|
||||
|
||||
if include_rule {
|
||||
filtered_rules.push(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure project path access if file reading is enabled
|
||||
if enable_file_read {
|
||||
let has_project_access = filtered_rules.iter().any(|rule| {
|
||||
rule.operation_type == "file_read_all" &&
|
||||
rule.pattern_type == "subpath" &&
|
||||
rule.pattern_value.contains("{{PROJECT_PATH}}")
|
||||
});
|
||||
|
||||
if !has_project_access {
|
||||
// Add a default project access rule
|
||||
filtered_rules.push(SandboxRule {
|
||||
id: None,
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "{{PROJECT_PATH}}".to_string(),
|
||||
enabled: true,
|
||||
platform_support: None,
|
||||
created_at: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.build_profile_with_serialization(filtered_rules)
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules
|
||||
pub fn build_profile(&self, rules: Vec<SandboxRule>) -> Result<Profile> {
|
||||
let result = self.build_profile_with_serialization(rules)?;
|
||||
Ok(result.profile)
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules and return serialized operations
|
||||
pub fn build_profile_with_serialization(&self, rules: Vec<SandboxRule>) -> Result<ProfileBuildResult> {
|
||||
let mut operations = Vec::new();
|
||||
let mut serialized_operations = Vec::new();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a gaol Operation from a database rule
|
||||
fn build_operation(&self, rule: &SandboxRule) -> Result<Option<Operation>> {
|
||||
match self.build_operation_with_serialization(rule) {
|
||||
Ok(Some((op, _))) => Ok(Some(op)),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a gaol Operation and its serialized form from a database rule
|
||||
fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result<Option<(Operation, SerializedOperation)>> {
|
||||
match rule.operation_type.as_str() {
|
||||
"file_read_all" => {
|
||||
let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?;
|
||||
Ok(Some((
|
||||
Operation::FileReadAll(pattern),
|
||||
SerializedOperation::FileReadAll { path, is_subpath }
|
||||
)))
|
||||
},
|
||||
"file_read_metadata" => {
|
||||
let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?;
|
||||
Ok(Some((
|
||||
Operation::FileReadMetadata(pattern),
|
||||
SerializedOperation::FileReadMetadata { path, is_subpath }
|
||||
)))
|
||||
},
|
||||
"network_outbound" => {
|
||||
let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?;
|
||||
Ok(Some((Operation::NetworkOutbound(pattern), serialized)))
|
||||
},
|
||||
"system_info_read" => {
|
||||
Ok(Some((
|
||||
Operation::SystemInfoRead,
|
||||
SerializedOperation::SystemInfoRead
|
||||
)))
|
||||
},
|
||||
_ => Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a PathPattern from pattern type and value
|
||||
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)?;
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
/// Build a PathPattern and return additional info for serialization
|
||||
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
|
||||
.replace("{{PROJECT_PATH}}", &self.project_path.to_string_lossy())
|
||||
.replace("{{HOME}}", &self.home_dir.to_string_lossy());
|
||||
|
||||
let path = PathBuf::from(expanded_value);
|
||||
|
||||
match pattern_type {
|
||||
"literal" => Ok((PathPattern::Literal(path.clone()), path, false)),
|
||||
"subpath" => Ok((PathPattern::Subpath(path.clone()), path, true)),
|
||||
_ => Err(anyhow::anyhow!("Unknown path pattern type: {}", pattern_type))
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an AddressPattern from pattern type and value
|
||||
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)?;
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
/// Build an AddressPattern and its serialized form
|
||||
fn build_address_pattern_with_serialization(&self, pattern_type: &str, pattern_value: &str) -> Result<(AddressPattern, SerializedOperation)> {
|
||||
match pattern_type {
|
||||
"all" => Ok((
|
||||
AddressPattern::All,
|
||||
SerializedOperation::NetworkOutbound { pattern: "all".to_string() }
|
||||
)),
|
||||
"tcp" => {
|
||||
let port = pattern_value.parse::<u16>()
|
||||
.context("Invalid TCP port number")?;
|
||||
Ok((
|
||||
AddressPattern::Tcp(port),
|
||||
SerializedOperation::NetworkTcp { port }
|
||||
))
|
||||
},
|
||||
"local_socket" => {
|
||||
let path = PathBuf::from(pattern_value);
|
||||
Ok((
|
||||
AddressPattern::LocalSocket(path.clone()),
|
||||
SerializedOperation::NetworkLocalSocket { path }
|
||||
))
|
||||
},
|
||||
_ => Err(anyhow::anyhow!("Unknown address pattern type: {}", pattern_type))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a rule is supported on the current platform
|
||||
fn is_rule_supported_on_platform(&self, rule: &SandboxRule) -> bool {
|
||||
if let Some(platforms_json) = &rule.platform_support {
|
||||
if let Ok(platforms) = serde_json::from_str::<Vec<String>>(platforms_json) {
|
||||
let current_os = std::env::consts::OS;
|
||||
return platforms.contains(¤t_os.to_string());
|
||||
}
|
||||
}
|
||||
// If no platform support specified, assume it's supported
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a sandbox profile by ID
|
||||
pub fn load_profile(conn: &Connection, profile_id: i64) -> Result<SandboxProfile> {
|
||||
conn.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at
|
||||
FROM sandbox_profiles WHERE id = ?1",
|
||||
params![profile_id],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
}
|
||||
)
|
||||
.context("Failed to load sandbox profile")
|
||||
}
|
||||
|
||||
/// Load the default sandbox profile
|
||||
pub fn load_default_profile(conn: &Connection) -> Result<SandboxProfile> {
|
||||
conn.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at
|
||||
FROM sandbox_profiles WHERE is_default = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
}
|
||||
)
|
||||
.context("Failed to load default sandbox profile")
|
||||
}
|
||||
|
||||
/// Load rules for a sandbox profile
|
||||
pub fn load_profile_rules(conn: &Connection, profile_id: i64) -> Result<Vec<SandboxRule>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at
|
||||
FROM sandbox_rules WHERE profile_id = ?1 AND enabled = 1"
|
||||
)?;
|
||||
|
||||
let rules = stmt.query_map(params![profile_id], |row| {
|
||||
Ok(SandboxRule {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
operation_type: row.get(2)?,
|
||||
pattern_type: row.get(3)?,
|
||||
pattern_value: row.get(4)?,
|
||||
enabled: row.get(5)?,
|
||||
platform_support: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
/// Get or create the gaol Profile for execution
|
||||
pub fn get_gaol_profile(conn: &Connection, profile_id: Option<i64>, project_path: PathBuf) -> Result<Profile> {
|
||||
// Load the profile
|
||||
let profile = if let Some(id) = profile_id {
|
||||
load_profile(conn, id)?
|
||||
} else {
|
||||
load_default_profile(conn)?
|
||||
};
|
||||
|
||||
info!("Using sandbox profile: {}", profile.name);
|
||||
|
||||
// Load the rules
|
||||
let rules = load_profile_rules(conn, profile.id.unwrap())?;
|
||||
info!("Loaded {} sandbox rules", rules.len());
|
||||
|
||||
// Build the gaol profile
|
||||
let builder = ProfileBuilder::new(project_path)?;
|
||||
builder.build_profile(rules)
|
||||
}
|
Reference in New Issue
Block a user