refactor: remove sandbox system and simplify agent architecture

Remove the entire sandbox security system including:
- All sandbox-related Rust code and dependencies (gaol crate)
- Sandbox command handlers and platform-specific implementations
- Comprehensive test suite for sandbox functionality
- Agent sandbox settings UI components

Simplify agent configuration by removing sandbox and permission fields:
- Remove sandbox_enabled, enable_file_read, enable_file_write, enable_network from agent configs
- Update all CC agents to use simplified configuration format
- Remove sandbox references from documentation and UI
This commit is contained in:
Vivek R
2025-07-02 19:17:38 +05:30
parent 124fe1544f
commit 2dfdf31b83
47 changed files with 115 additions and 7774 deletions

View File

@@ -353,7 +353,7 @@ fn select_best_installation(installations: Vec<ClaudeInstallation>) -> Option<Cl
(None, Some(_)) => Ordering::Less,
// Neither have version info: prefer the one that is not just
// the bare "claude" lookup from PATH, because that may fail
// at runtime if PATH is sandbox-stripped.
// at runtime if PATH is modified.
(None, None) => {
if a.path == "claude" && b.path != "claude" {
Ordering::Less

View File

@@ -1,4 +1,3 @@
use crate::sandbox::profile::ProfileBuilder;
use anyhow::Result;
use chrono;
use log::{debug, error, info, warn};
@@ -28,7 +27,6 @@ pub struct Agent {
pub system_prompt: String,
pub default_task: Option<String>,
pub model: String,
pub sandbox_enabled: bool,
pub enable_file_read: bool,
pub enable_file_write: bool,
pub enable_network: bool,
@@ -88,10 +86,6 @@ pub struct AgentData {
pub system_prompt: String,
pub default_task: Option<String>,
pub model: String,
pub sandbox_enabled: bool,
pub enable_file_read: bool,
pub enable_file_write: bool,
pub enable_network: bool,
}
/// Database connection state
@@ -235,7 +229,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
system_prompt TEXT NOT NULL,
default_task TEXT,
model TEXT NOT NULL DEFAULT 'sonnet',
sandbox_enabled BOOLEAN NOT NULL DEFAULT 1,
enable_file_read BOOLEAN NOT NULL DEFAULT 1,
enable_file_write BOOLEAN NOT NULL DEFAULT 1,
enable_network BOOLEAN NOT NULL DEFAULT 0,
@@ -251,14 +244,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
"ALTER TABLE agents ADD COLUMN model TEXT DEFAULT 'sonnet'",
[],
);
let _ = conn.execute(
"ALTER TABLE agents ADD COLUMN sandbox_profile_id INTEGER REFERENCES sandbox_profiles(id)",
[],
);
let _ = conn.execute(
"ALTER TABLE agents ADD COLUMN sandbox_enabled BOOLEAN DEFAULT 1",
[],
);
let _ = conn.execute(
"ALTER TABLE agents ADD COLUMN enable_file_read BOOLEAN DEFAULT 1",
[],
@@ -329,75 +314,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
[],
)?;
// Create sandbox profiles table
conn.execute(
"CREATE TABLE IF NOT EXISTS sandbox_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT 0,
is_default BOOLEAN NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
// Create sandbox rules table
conn.execute(
"CREATE TABLE IF NOT EXISTS sandbox_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id INTEGER NOT NULL,
operation_type TEXT NOT NULL,
pattern_type TEXT NOT NULL,
pattern_value TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT 1,
platform_support TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE
)",
[],
)?;
// Create trigger to update sandbox profile timestamp
conn.execute(
"CREATE TRIGGER IF NOT EXISTS update_sandbox_profile_timestamp
AFTER UPDATE ON sandbox_profiles
FOR EACH ROW
BEGIN
UPDATE sandbox_profiles SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END",
[],
)?;
// Create sandbox violations table
conn.execute(
"CREATE TABLE IF NOT EXISTS sandbox_violations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id INTEGER,
agent_id INTEGER,
agent_run_id INTEGER,
operation_type TEXT NOT NULL,
pattern_value TEXT,
process_name TEXT,
pid INTEGER,
denied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE,
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
FOREIGN KEY (agent_run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
)",
[],
)?;
// Create index for efficient querying
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_sandbox_violations_denied_at
ON sandbox_violations(denied_at DESC)",
[],
)?;
// Create default sandbox profiles if they don't exist
crate::sandbox::defaults::create_default_profiles(&conn)?;
// Create settings table for app-wide settings
conn.execute(
@@ -430,7 +346,7 @@ pub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
let mut stmt = conn
.prepare("SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC")
.prepare("SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC")
.map_err(|e| e.to_string())?;
let agents = stmt
@@ -444,12 +360,11 @@ pub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {
model: row
.get::<_, String>(5)
.unwrap_or_else(|_| "sonnet".to_string()),
sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true),
enable_file_read: row.get::<_, bool>(7).unwrap_or(true),
enable_file_write: row.get::<_, bool>(8).unwrap_or(true),
enable_network: row.get::<_, bool>(9).unwrap_or(false),
created_at: row.get(10)?,
updated_at: row.get(11)?,
enable_file_read: row.get::<_, bool>(6).unwrap_or(true),
enable_file_write: row.get::<_, bool>(7).unwrap_or(true),
enable_network: row.get::<_, bool>(8).unwrap_or(false),
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
})
.map_err(|e| e.to_string())?
@@ -468,21 +383,19 @@ pub async fn create_agent(
system_prompt: String,
default_task: Option<String>,
model: Option<String>,
sandbox_enabled: Option<bool>,
enable_file_read: Option<bool>,
enable_file_write: Option<bool>,
enable_network: Option<bool>,
) -> Result<Agent, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
let model = model.unwrap_or_else(|| "sonnet".to_string());
let sandbox_enabled = sandbox_enabled.unwrap_or(true);
let enable_file_read = enable_file_read.unwrap_or(true);
let enable_file_write = enable_file_write.unwrap_or(true);
let enable_network = enable_network.unwrap_or(false);
conn.execute(
"INSERT INTO agents (name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network],
"INSERT INTO agents (name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network],
)
.map_err(|e| e.to_string())?;
@@ -491,7 +404,7 @@ pub async fn create_agent(
// Fetch the created agent
let agent = conn
.query_row(
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
params![id],
|row| {
Ok(Agent {
@@ -501,12 +414,11 @@ pub async fn create_agent(
system_prompt: row.get(3)?,
default_task: row.get(4)?,
model: row.get(5)?,
sandbox_enabled: row.get(6)?,
enable_file_read: row.get(7)?,
enable_file_write: row.get(8)?,
enable_network: row.get(9)?,
created_at: row.get(10)?,
updated_at: row.get(11)?,
enable_file_read: row.get(6)?,
enable_file_write: row.get(7)?,
enable_network: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
},
)
@@ -525,7 +437,6 @@ pub async fn update_agent(
system_prompt: String,
default_task: Option<String>,
model: Option<String>,
sandbox_enabled: Option<bool>,
enable_file_read: Option<bool>,
enable_file_write: Option<bool>,
enable_network: Option<bool>,
@@ -546,11 +457,6 @@ pub async fn update_agent(
];
let mut param_count = 5;
if let Some(se) = sandbox_enabled {
param_count += 1;
query.push_str(&format!(", sandbox_enabled = ?{}", param_count));
params_vec.push(Box::new(se));
}
if let Some(efr) = enable_file_read {
param_count += 1;
query.push_str(&format!(", enable_file_read = ?{}", param_count));
@@ -580,7 +486,7 @@ pub async fn update_agent(
// Fetch the updated agent
let agent = conn
.query_row(
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
params![id],
|row| {
Ok(Agent {
@@ -590,12 +496,11 @@ pub async fn update_agent(
system_prompt: row.get(3)?,
default_task: row.get(4)?,
model: row.get(5)?,
sandbox_enabled: row.get(6)?,
enable_file_read: row.get(7)?,
enable_file_write: row.get(8)?,
enable_network: row.get(9)?,
created_at: row.get(10)?,
updated_at: row.get(11)?,
enable_file_read: row.get(6)?,
enable_file_write: row.get(7)?,
enable_network: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
},
)
@@ -622,7 +527,7 @@ pub async fn get_agent(db: State<'_, AgentDb>, id: i64) -> Result<Agent, String>
let agent = conn
.query_row(
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
params![id],
|row| {
Ok(Agent {
@@ -632,12 +537,11 @@ pub async fn get_agent(db: State<'_, AgentDb>, id: i64) -> Result<Agent, String>
system_prompt: row.get(3)?,
default_task: row.get(4)?,
model: row.get::<_, String>(5).unwrap_or_else(|_| "sonnet".to_string()),
sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true),
enable_file_read: row.get::<_, bool>(7).unwrap_or(true),
enable_file_write: row.get::<_, bool>(8).unwrap_or(true),
enable_network: row.get::<_, bool>(9).unwrap_or(false),
created_at: row.get(10)?,
updated_at: row.get(11)?,
enable_file_read: row.get::<_, bool>(6).unwrap_or(true),
enable_file_write: row.get::<_, bool>(7).unwrap_or(true),
enable_network: row.get::<_, bool>(8).unwrap_or(false),
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
},
)
@@ -788,411 +692,30 @@ pub async fn execute_agent(
conn.last_insert_rowid()
};
// Create sandbox rules based on agent-specific permissions (no database dependency)
let sandbox_profile = if !agent.sandbox_enabled {
info!("🔓 Agent '{}': Sandbox DISABLED", agent.name);
None
} else {
info!(
"🔒 Agent '{}': Sandbox enabled | File Read: {} | File Write: {} | Network: {}",
agent.name, agent.enable_file_read, agent.enable_file_write, agent.enable_network
);
// Create rules dynamically based on agent permissions
let mut rules = Vec::new();
// Add file read rules if enabled
if agent.enable_file_read {
// Project directory access
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(1),
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: Some(r#"["linux", "macos", "windows"]"#.to_string()),
created_at: String::new(),
});
// System libraries (for language runtimes, etc.)
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(2),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/usr/lib".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(3),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/usr/local/lib".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(4),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/System/Library".to_string(),
enabled: true,
platform_support: Some(r#"["macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(5),
profile_id: 0,
operation_type: "file_read_metadata".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/".to_string(),
enabled: true,
platform_support: Some(r#"["macos"]"#.to_string()),
created_at: String::new(),
});
}
// Add network rules if enabled
if agent.enable_network {
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(6),
profile_id: 0,
operation_type: "network_outbound".to_string(),
pattern_type: "all".to_string(),
pattern_value: "".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
}
// Always add essential system paths (needed for executables to run)
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(7),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/usr/bin".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(8),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/opt/homebrew/bin".to_string(),
enabled: true,
platform_support: Some(r#"["macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(9),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/usr/local/bin".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(10),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/bin".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
// System libraries (needed for executables to link)
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(11),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/usr/lib".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(12),
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "/System/Library".to_string(),
enabled: true,
platform_support: Some(r#"["macos"]"#.to_string()),
created_at: String::new(),
});
// Always add system info reading (minimal requirement)
rules.push(crate::sandbox::profile::SandboxRule {
id: Some(13),
profile_id: 0,
operation_type: "system_info_read".to_string(),
pattern_type: "all".to_string(),
pattern_value: "".to_string(),
enabled: true,
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
created_at: String::new(),
});
Some(("Agent-specific".to_string(), rules))
};
// Build the command
let mut cmd = if let Some((_profile_name, rules)) = sandbox_profile {
info!("🧪 DEBUG: Testing Claude command first without sandbox...");
// Quick test to see if Claude is accessible at all
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
error!("❌ Claude binary not found: {}", e);
return Err(e);
}
};
match std::process::Command::new(&claude_path)
.arg("--version")
.output()
{
Ok(output) => {
if output.status.success() {
info!(
"✅ Claude command works: {}",
String::from_utf8_lossy(&output.stdout).trim()
);
} else {
warn!("⚠️ Claude command failed with status: {}", output.status);
warn!(" stdout: {}", String::from_utf8_lossy(&output.stdout));
warn!(" stderr: {}", String::from_utf8_lossy(&output.stderr));
}
}
Err(e) => {
error!("❌ Claude command not found or not executable: {}", e);
error!(" This could be why the agent is failing to start");
}
info!("Running agent '{}'", agent.name);
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
error!("Failed to find claude binary: {}", e);
return Err(e);
}
// Test if Claude can actually start a session (this might reveal auth issues)
info!("🧪 Testing Claude with exact same arguments as agent (without sandbox env vars)...");
let mut test_cmd = std::process::Command::new(&claude_path);
test_cmd
.arg("-p")
.arg(&task)
.arg("--system-prompt")
.arg(&agent.system_prompt)
.arg("--model")
.arg(&execution_model)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--dangerously-skip-permissions")
.current_dir(&project_path);
info!("🧪 Testing command: claude -p \"{}\" --system-prompt \"{}\" --model {} --output-format stream-json --verbose --dangerously-skip-permissions",
task, agent.system_prompt, execution_model);
// Start the test process and give it 5 seconds to produce output
match test_cmd.spawn() {
Ok(mut child) => {
// Wait for 5 seconds to see if it produces output
let start = std::time::Instant::now();
let mut output_received = false;
while start.elapsed() < std::time::Duration::from_secs(5) {
match child.try_wait() {
Ok(Some(status)) => {
info!("🧪 Test process exited with status: {}", status);
output_received = true;
break;
}
Ok(None) => {
// Still running
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(e) => {
warn!("🧪 Error checking test process: {}", e);
break;
}
}
}
if !output_received {
warn!("🧪 Test process is still running after 5 seconds - this suggests Claude might be waiting for input");
// Kill the test process
let _ = child.kill();
let _ = child.wait();
} else {
info!("🧪 Test process completed quickly - command seems to work");
}
}
Err(e) => {
error!("❌ Failed to spawn test Claude process: {}", e);
}
}
info!("🧪 End of Claude test, proceeding with sandbox...");
// Build the gaol profile using agent-specific permissions
let project_path_buf = PathBuf::from(&project_path);
match ProfileBuilder::new(project_path_buf.clone()) {
Ok(builder) => {
// Build agent-specific profile with permission filtering
match builder.build_agent_profile(
rules,
agent.sandbox_enabled,
agent.enable_file_read,
agent.enable_file_write,
agent.enable_network,
) {
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,
"--system-prompt",
&agent.system_prompt,
"--model",
&execution_model,
"--output-format",
"stream-json",
"--verbose",
"--dangerously-skip-permissions",
];
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
error!("Failed to find claude binary: {}", e);
return Err(e);
}
};
executor.prepare_sandboxed_command(&claude_path, &args, &project_path_buf)
}
Err(e) => {
error!("Failed to build agent-specific sandbox profile: {}, falling back to non-sandboxed", e);
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
error!("Failed to find claude binary: {}", e);
return Err(e);
}
};
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("-p")
.arg(&task)
.arg("--system-prompt")
.arg(&agent.system_prompt)
.arg("--model")
.arg(&execution_model)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--dangerously-skip-permissions")
.current_dir(&project_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
}
}
Err(e) => {
error!(
"Failed to create ProfileBuilder: {}, falling back to non-sandboxed",
e
);
// Fall back to non-sandboxed command
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
error!("Failed to find claude binary: {}", e);
return Err(e);
}
};
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("-p")
.arg(&task)
.arg("--system-prompt")
.arg(&agent.system_prompt)
.arg("--model")
.arg(&execution_model)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--dangerously-skip-permissions")
.current_dir(&project_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
}
} else {
// No sandbox or sandbox disabled, use regular command
warn!(
"🚨 Running agent '{}' WITHOUT SANDBOX - full system access!",
agent.name
);
let claude_path = match find_claude_binary(&app) {
Ok(path) => path,
Err(e) => {
error!("Failed to find claude binary: {}", e);
return Err(e);
}
};
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("-p")
.arg(&task)
.arg("--system-prompt")
.arg(&agent.system_prompt)
.arg("--model")
.arg(&execution_model)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--dangerously-skip-permissions")
.current_dir(&project_path)
.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
};
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("-p")
.arg(&task)
.arg("--system-prompt")
.arg(&agent.system_prompt)
.arg("--model")
.arg(&execution_model)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--dangerously-skip-permissions")
.current_dir(&project_path)
.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
.stdout(Stdio::piped())
.stderr(Stdio::piped());
// Spawn the process
info!("🚀 Spawning Claude process...");
@@ -1385,7 +908,7 @@ pub async fn execute_agent(
warn!("⏰ TIMEOUT: No output from Claude process after 30 seconds");
warn!("💡 This usually means:");
warn!(" 1. Claude process is waiting for user input");
warn!(" 2. Sandbox permissions are too restrictive");
warn!(" 3. Claude failed to initialize but didn't report an error");
warn!(" 4. Network connectivity issues");
warn!(" 5. Authentication issues (API key not found/invalid)");
@@ -1807,7 +1330,7 @@ pub async fn export_agent(db: State<'_, AgentDb>, id: i64) -> Result<String, Str
// Fetch the agent
let agent = conn
.query_row(
"SELECT name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network FROM agents WHERE id = ?1",
"SELECT name, icon, system_prompt, default_task, model FROM agents WHERE id = ?1",
params![id],
|row| {
Ok(serde_json::json!({
@@ -1815,11 +1338,7 @@ pub async fn export_agent(db: State<'_, AgentDb>, id: i64) -> Result<String, Str
"icon": row.get::<_, String>(1)?,
"system_prompt": row.get::<_, String>(2)?,
"default_task": row.get::<_, Option<String>>(3)?,
"model": row.get::<_, String>(4)?,
"sandbox_enabled": row.get::<_, bool>(5)?,
"enable_file_read": row.get::<_, bool>(6)?,
"enable_file_write": row.get::<_, bool>(7)?,
"enable_network": row.get::<_, bool>(8)?
"model": row.get::<_, String>(4)?
}))
},
)
@@ -2010,17 +1529,13 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
// Create the agent
conn.execute(
"INSERT INTO agents (name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
"INSERT INTO agents (name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, 1, 1, 0)",
params![
final_name,
agent_data.icon,
agent_data.system_prompt,
agent_data.default_task,
agent_data.model,
agent_data.sandbox_enabled,
agent_data.enable_file_read,
agent_data.enable_file_write,
agent_data.enable_network
agent_data.model
],
)
.map_err(|e| format!("Failed to create agent: {}", e))?;
@@ -2030,7 +1545,7 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
// Fetch the created agent
let agent = conn
.query_row(
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
params![id],
|row| {
Ok(Agent {
@@ -2040,12 +1555,11 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
system_prompt: row.get(3)?,
default_task: row.get(4)?,
model: row.get(5)?,
sandbox_enabled: row.get(6)?,
enable_file_read: row.get(7)?,
enable_file_write: row.get(8)?,
enable_network: row.get(9)?,
created_at: row.get(10)?,
updated_at: row.get(11)?,
enable_file_read: row.get(6)?,
enable_file_write: row.get(7)?,
enable_network: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
},
)

View File

@@ -798,15 +798,8 @@ pub async fn execute_claude_code(
model
);
// Check if sandboxing should be used
let use_sandbox = should_use_sandbox(&app)?;
let mut cmd = if use_sandbox {
create_sandboxed_claude_command(&app, &project_path)?
} else {
let claude_path = find_claude_binary(&app)?;
create_command_with_env(&claude_path)
};
let claude_path = find_claude_binary(&app)?;
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("-p")
.arg(&prompt)
@@ -837,15 +830,8 @@ pub async fn continue_claude_code(
model
);
// Check if sandboxing should be used
let use_sandbox = should_use_sandbox(&app)?;
let mut cmd = if use_sandbox {
create_sandboxed_claude_command(&app, &project_path)?
} else {
let claude_path = find_claude_binary(&app)?;
create_command_with_env(&claude_path)
};
let claude_path = find_claude_binary(&app)?;
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("-c") // Continue flag
.arg("-p")
@@ -879,15 +865,8 @@ pub async fn resume_claude_code(
model
);
// Check if sandboxing should be used
let use_sandbox = should_use_sandbox(&app)?;
let mut cmd = if use_sandbox {
create_sandboxed_claude_command(&app, &project_path)?
} else {
let claude_path = find_claude_binary(&app)?;
create_command_with_env(&claude_path)
};
let claude_path = find_claude_binary(&app)?;
let mut cmd = create_command_with_env(&claude_path);
cmd.arg("--resume")
.arg(&session_id)
@@ -1052,200 +1031,8 @@ pub async fn get_claude_session_output(
}
}
/// Helper function to check if sandboxing should be used based on settings
fn should_use_sandbox(app: &AppHandle) -> Result<bool, String> {
// First check if sandboxing is even available on this platform
if !crate::sandbox::platform::is_sandboxing_available() {
log::info!("Sandboxing not available on this platform");
return Ok(false);
}
// Check if a setting exists to enable/disable sandboxing
let settings = get_claude_settings_sync(app)?;
// Check for a sandboxing setting in the settings
if let Some(sandbox_enabled) = settings
.data
.get("sandboxEnabled")
.and_then(|v| v.as_bool())
{
return Ok(sandbox_enabled);
}
// Default to true (sandboxing enabled) on supported platforms
Ok(true)
}
/// Helper function to create a sandboxed Claude command
fn create_sandboxed_claude_command(app: &AppHandle, project_path: &str) -> Result<Command, String> {
use crate::sandbox::{executor::create_sandboxed_command, profile::ProfileBuilder};
use std::path::PathBuf;
// Get the database connection
let conn = {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
let db_path = app_data_dir.join("agents.db");
rusqlite::Connection::open(&db_path)
.map_err(|e| format!("Failed to open database: {}", e))?
};
// Query for the default active sandbox profile
let profile_id: Option<i64> = conn
.query_row(
"SELECT id FROM sandbox_profiles WHERE is_default = 1 AND is_active = 1",
[],
|row| row.get(0),
)
.ok();
match profile_id {
Some(profile_id) => {
log::info!(
"Using default sandbox profile: {} (id: {})",
profile_id,
profile_id
);
// Get all rules for this profile
let mut stmt = conn
.prepare(
"SELECT operation_type, pattern_type, pattern_value, enabled, platform_support
FROM sandbox_rules WHERE profile_id = ?1 AND enabled = 1",
)
.map_err(|e| e.to_string())?;
let rules = stmt
.query_map(rusqlite::params![profile_id], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, bool>(3)?,
row.get::<_, Option<String>>(4)?,
))
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
log::info!("Building sandbox profile with {} rules", rules.len());
// Build the gaol profile
let project_path_buf = PathBuf::from(project_path);
match ProfileBuilder::new(project_path_buf.clone()) {
Ok(builder) => {
// Convert database rules to SandboxRule structs
let mut sandbox_rules = Vec::new();
for (idx, (op_type, pattern_type, pattern_value, enabled, platform_support)) in
rules.into_iter().enumerate()
{
// Check if this rule applies to the current platform
if let Some(platforms_json) = &platform_support {
if let Ok(platforms) =
serde_json::from_str::<Vec<String>>(platforms_json)
{
let current_platform = if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "freebsd") {
"freebsd"
} else {
"unsupported"
};
if !platforms.contains(&current_platform.to_string()) {
continue;
}
}
}
// Create SandboxRule struct
let rule = crate::sandbox::profile::SandboxRule {
id: Some(idx as i64),
profile_id: 0,
operation_type: op_type,
pattern_type,
pattern_value,
enabled,
platform_support,
created_at: String::new(),
};
sandbox_rules.push(rule);
}
// Try to build the profile
match builder.build_profile(sandbox_rules) {
Ok(profile) => {
log::info!("Successfully built sandbox profile '{}'", profile_id);
// Use the helper function to create sandboxed command
let claude_path = find_claude_binary(app)?;
#[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);
let claude_path = find_claude_binary(app)?;
Ok(create_command_with_env(&claude_path))
}
}
}
Err(e) => {
log::error!(
"Failed to create ProfileBuilder: {}, falling back to non-sandboxed",
e
);
let claude_path = find_claude_binary(app)?;
Ok(create_command_with_env(&claude_path))
}
}
}
None => {
log::info!("No default active sandbox profile found: proceeding without sandbox");
let claude_path = find_claude_binary(app)?;
Ok(create_command_with_env(&claude_path))
}
}
}
/// Synchronous version of get_claude_settings for use in non-async contexts
fn get_claude_settings_sync(_app: &AppHandle) -> Result<ClaudeSettings, String> {
let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;
let settings_path = claude_dir.join("settings.json");
if !settings_path.exists() {
return Ok(ClaudeSettings::default());
}
let content = std::fs::read_to_string(&settings_path)
.map_err(|e| format!("Failed to read settings file: {}", e))?;
let data: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse settings JSON: {}", e))?;
Ok(ClaudeSettings { data })
}
/// Helper function to spawn Claude process and handle streaming
async fn spawn_claude_process(app: AppHandle, mut cmd: Command, prompt: String, model: String, project_path: String) -> Result<(), String> {

View File

@@ -1,6 +1,5 @@
pub mod agents;
pub mod claude;
pub mod mcp;
pub mod sandbox;
pub mod screenshot;
pub mod usage;

View File

@@ -1,947 +0,0 @@
use crate::{
commands::agents::AgentDb,
sandbox::{
platform::PlatformCapabilities,
profile::{SandboxProfile, SandboxRule},
},
};
use rusqlite::params;
use serde::{Deserialize, Serialize};
use tauri::State;
/// Represents a sandbox violation event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxViolation {
pub id: Option<i64>,
pub profile_id: Option<i64>,
pub agent_id: Option<i64>,
pub agent_run_id: Option<i64>,
pub operation_type: String,
pub pattern_value: Option<String>,
pub process_name: Option<String>,
pub pid: Option<i32>,
pub denied_at: String,
}
/// Represents sandbox profile export data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxProfileExport {
pub version: u32,
pub exported_at: String,
pub platform: String,
pub profiles: Vec<SandboxProfileWithRules>,
}
/// Represents a profile with its rules for export
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxProfileWithRules {
pub profile: SandboxProfile,
pub rules: Vec<SandboxRule>,
}
/// Import result for a profile
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResult {
pub profile_name: String,
pub imported: bool,
pub reason: Option<String>,
pub new_name: Option<String>,
}
/// List all sandbox profiles
#[tauri::command]
pub async fn list_sandbox_profiles(db: State<'_, AgentDb>) -> Result<Vec<SandboxProfile>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
let mut stmt = conn
.prepare("SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles ORDER BY name")
.map_err(|e| e.to_string())?;
let profiles = stmt
.query_map([], |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)?,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(profiles)
}
/// Create a new sandbox profile
#[tauri::command]
pub async fn create_sandbox_profile(
db: State<'_, AgentDb>,
name: String,
description: Option<String>,
) -> Result<SandboxProfile, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
conn.execute(
"INSERT INTO sandbox_profiles (name, description) VALUES (?1, ?2)",
params![name, description],
)
.map_err(|e| e.to_string())?;
let id = conn.last_insert_rowid();
// Fetch the created profile
let profile = conn
.query_row(
"SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1",
params![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)?,
})
},
)
.map_err(|e| e.to_string())?;
Ok(profile)
}
/// Update a sandbox profile
#[tauri::command]
pub async fn update_sandbox_profile(
db: State<'_, AgentDb>,
id: i64,
name: String,
description: Option<String>,
is_active: bool,
is_default: bool,
) -> Result<SandboxProfile, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// If setting as default, unset other defaults
if is_default {
conn.execute(
"UPDATE sandbox_profiles SET is_default = 0 WHERE id != ?1",
params![id],
)
.map_err(|e| e.to_string())?;
}
conn.execute(
"UPDATE sandbox_profiles SET name = ?1, description = ?2, is_active = ?3, is_default = ?4 WHERE id = ?5",
params![name, description, is_active, is_default, id],
)
.map_err(|e| e.to_string())?;
// Fetch the updated profile
let profile = conn
.query_row(
"SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1",
params![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)?,
})
},
)
.map_err(|e| e.to_string())?;
Ok(profile)
}
/// Delete a sandbox profile
#[tauri::command]
pub async fn delete_sandbox_profile(db: State<'_, AgentDb>, id: i64) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// Check if it's the default profile
let is_default: bool = conn
.query_row(
"SELECT is_default FROM sandbox_profiles WHERE id = ?1",
params![id],
|row| row.get(0),
)
.map_err(|e| e.to_string())?;
if is_default {
return Err("Cannot delete the default profile".to_string());
}
conn.execute("DELETE FROM sandbox_profiles WHERE id = ?1", params![id])
.map_err(|e| e.to_string())?;
Ok(())
}
/// Get a single sandbox profile by ID
#[tauri::command]
pub async fn get_sandbox_profile(
db: State<'_, AgentDb>,
id: i64,
) -> Result<SandboxProfile, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
let profile = conn
.query_row(
"SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1",
params![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)?,
})
},
)
.map_err(|e| e.to_string())?;
Ok(profile)
}
/// List rules for a sandbox profile
#[tauri::command]
pub async fn list_sandbox_rules(
db: State<'_, AgentDb>,
profile_id: i64,
) -> Result<Vec<SandboxRule>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
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 ORDER BY operation_type, pattern_value")
.map_err(|e| e.to_string())?;
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)?,
})
})
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(rules)
}
/// Create a new sandbox rule
#[tauri::command]
pub async fn create_sandbox_rule(
db: State<'_, AgentDb>,
profile_id: i64,
operation_type: String,
pattern_type: String,
pattern_value: String,
enabled: bool,
platform_support: Option<String>,
) -> Result<SandboxRule, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// Validate rule doesn't conflict
// TODO: Add more validation logic here
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, operation_type, pattern_type, pattern_value, enabled, platform_support],
)
.map_err(|e| e.to_string())?;
let id = conn.last_insert_rowid();
// Fetch the created rule
let rule = conn
.query_row(
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at FROM sandbox_rules WHERE id = ?1",
params![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)?,
})
},
)
.map_err(|e| e.to_string())?;
Ok(rule)
}
/// Update a sandbox rule
#[tauri::command]
pub async fn update_sandbox_rule(
db: State<'_, AgentDb>,
id: i64,
operation_type: String,
pattern_type: String,
pattern_value: String,
enabled: bool,
platform_support: Option<String>,
) -> Result<SandboxRule, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
conn.execute(
"UPDATE sandbox_rules SET operation_type = ?1, pattern_type = ?2, pattern_value = ?3, enabled = ?4, platform_support = ?5 WHERE id = ?6",
params![operation_type, pattern_type, pattern_value, enabled, platform_support, id],
)
.map_err(|e| e.to_string())?;
// Fetch the updated rule
let rule = conn
.query_row(
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at FROM sandbox_rules WHERE id = ?1",
params![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)?,
})
},
)
.map_err(|e| e.to_string())?;
Ok(rule)
}
/// Delete a sandbox rule
#[tauri::command]
pub async fn delete_sandbox_rule(db: State<'_, AgentDb>, id: i64) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
conn.execute("DELETE FROM sandbox_rules WHERE id = ?1", params![id])
.map_err(|e| e.to_string())?;
Ok(())
}
/// Get platform capabilities for sandbox configuration
#[tauri::command]
pub async fn get_platform_capabilities() -> Result<PlatformCapabilities, String> {
Ok(crate::sandbox::platform::get_platform_capabilities())
}
/// Test a sandbox profile by creating a simple test process
#[tauri::command]
pub async fn test_sandbox_profile(
db: State<'_, AgentDb>,
profile_id: i64,
) -> Result<String, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// Load the profile and rules
let profile = crate::sandbox::profile::load_profile(&conn, profile_id)
.map_err(|e| format!("Failed to load profile: {}", e))?;
if !profile.is_active {
return Ok(format!(
"Profile '{}' is currently inactive. Activate it to use with agents.",
profile.name
));
}
let rules = crate::sandbox::profile::load_profile_rules(&conn, profile_id)
.map_err(|e| format!("Failed to load profile rules: {}", e))?;
if rules.is_empty() {
return Ok(format!(
"Profile '{}' has no rules configured. Add rules to define sandbox permissions.",
profile.name
));
}
// Try to build the gaol profile
let test_path = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
let builder = crate::sandbox::profile::ProfileBuilder::new(test_path.clone())
.map_err(|e| format!("Failed to create profile builder: {}", e))?;
let build_result = builder
.build_profile_with_serialization(rules.clone())
.map_err(|e| format!("Failed to build sandbox profile: {}", e))?;
// Check platform support
let platform_caps = crate::sandbox::platform::get_platform_capabilities();
if !platform_caps.sandboxing_supported {
return Ok(format!(
"Profile '{}' validated successfully. {} rules loaded.\n\nNote: Sandboxing is not supported on {} platform. The profile configuration is valid but sandbox enforcement will not be active.",
profile.name,
rules.len(),
platform_caps.os
));
}
// Try to execute a simple command in the sandbox
let executor = crate::sandbox::executor::SandboxExecutor::new_with_serialization(
build_result.profile,
test_path.clone(),
build_result.serialized,
);
// Use a simple echo command for testing
let test_command = if cfg!(windows) { "cmd" } else { "echo" };
let test_args = if cfg!(windows) {
vec!["/C", "echo", "sandbox test successful"]
} else {
vec!["sandbox test successful"]
};
match executor.execute_sandboxed_spawn(test_command, &test_args, &test_path) {
Ok(mut child) => {
// Wait for the process to complete with a timeout
match child.wait() {
Ok(status) => {
if status.success() {
Ok(format!(
"✅ Profile '{}' tested successfully!\n\n\
{} rules loaded and validated\n\
• Sandbox activation: Success\n\
• Test process execution: Success\n\
• Platform: {} (fully supported)",
profile.name,
rules.len(),
platform_caps.os
))
} else {
Ok(format!(
"⚠️ Profile '{}' validated with warnings.\n\n\
{} rules loaded and validated\n\
• Sandbox activation: Success\n\
• Test process exit code: {}\n\
• Platform: {}",
profile.name,
rules.len(),
status.code().unwrap_or(-1),
platform_caps.os
))
}
}
Err(e) => Ok(format!(
"⚠️ Profile '{}' validated with warnings.\n\n\
{} rules loaded and validated\n\
• Sandbox activation: Partial\n\
• Test process: Could not get exit status ({})\n\
• Platform: {}",
profile.name,
rules.len(),
e,
platform_caps.os
)),
}
}
Err(e) => {
// Check if it's a permission error or platform limitation
let error_str = e.to_string();
if error_str.contains("permission") || error_str.contains("denied") {
Ok(format!(
"⚠️ Profile '{}' validated with limitations.\n\n\
{} rules loaded and validated\n\
• Sandbox configuration: Valid\n\
• Sandbox enforcement: Limited by system permissions\n\
• Platform: {}\n\n\
Note: The sandbox profile is correctly configured but may require elevated privileges or system configuration to fully enforce on this platform.",
profile.name,
rules.len(),
platform_caps.os
))
} else {
Ok(format!(
"⚠️ Profile '{}' validated with limitations.\n\n\
{} rules loaded and validated\n\
• Sandbox configuration: Valid\n\
• Test execution: Failed ({})\n\
• Platform: {}\n\n\
The sandbox profile is correctly configured. The test execution failed due to platform-specific limitations, but the profile can still be used.",
profile.name,
rules.len(),
e,
platform_caps.os
))
}
}
}
}
/// List sandbox violations with optional filtering
#[tauri::command]
pub async fn list_sandbox_violations(
db: State<'_, AgentDb>,
profile_id: Option<i64>,
agent_id: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<SandboxViolation>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// Build dynamic query
let mut query = String::from(
"SELECT id, profile_id, agent_id, agent_run_id, operation_type, pattern_value, process_name, pid, denied_at
FROM sandbox_violations WHERE 1=1"
);
let mut param_idx = 1;
if profile_id.is_some() {
query.push_str(&format!(" AND profile_id = ?{}", param_idx));
param_idx += 1;
}
if agent_id.is_some() {
query.push_str(&format!(" AND agent_id = ?{}", param_idx));
param_idx += 1;
}
query.push_str(" ORDER BY denied_at DESC");
if limit.is_some() {
query.push_str(&format!(" LIMIT ?{}", param_idx));
}
// Execute query based on parameters
let violations: Vec<SandboxViolation> = if let Some(pid) = profile_id {
if let Some(aid) = agent_id {
if let Some(lim) = limit {
// All three parameters
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![pid, aid, lim], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
} else {
// profile_id and agent_id only
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![pid, aid], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
}
} else if let Some(lim) = limit {
// profile_id and limit only
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![pid, lim], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
} else {
// profile_id only
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![pid], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
}
} else if let Some(aid) = agent_id {
if let Some(lim) = limit {
// agent_id and limit only
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![aid, lim], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
} else {
// agent_id only
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![aid], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
}
} else if let Some(lim) = limit {
// limit only
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![lim], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
} else {
// No parameters
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
let rows = stmt
.query_map([], |row| {
Ok(SandboxViolation {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
agent_id: row.get(2)?,
agent_run_id: row.get(3)?,
operation_type: row.get(4)?,
pattern_value: row.get(5)?,
process_name: row.get(6)?,
pid: row.get(7)?,
denied_at: row.get(8)?,
})
})
.map_err(|e| e.to_string())?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?
};
Ok(violations)
}
/// Log a sandbox violation
#[tauri::command]
pub async fn log_sandbox_violation(
db: State<'_, AgentDb>,
profile_id: Option<i64>,
agent_id: Option<i64>,
agent_run_id: Option<i64>,
operation_type: String,
pattern_value: Option<String>,
process_name: Option<String>,
pid: Option<i32>,
) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
conn.execute(
"INSERT INTO sandbox_violations (profile_id, agent_id, agent_run_id, operation_type, pattern_value, process_name, pid)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![profile_id, agent_id, agent_run_id, operation_type, pattern_value, process_name, pid],
)
.map_err(|e| e.to_string())?;
Ok(())
}
/// Clear old sandbox violations
#[tauri::command]
pub async fn clear_sandbox_violations(
db: State<'_, AgentDb>,
older_than_days: Option<i64>,
) -> Result<i64, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
let query = if let Some(days) = older_than_days {
format!(
"DELETE FROM sandbox_violations WHERE denied_at < datetime('now', '-{} days')",
days
)
} else {
"DELETE FROM sandbox_violations".to_string()
};
let deleted = conn.execute(&query, []).map_err(|e| e.to_string())?;
Ok(deleted as i64)
}
/// Get sandbox violation statistics
#[tauri::command]
pub async fn get_sandbox_violation_stats(
db: State<'_, AgentDb>,
) -> Result<serde_json::Value, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
// Get total violations
let total: i64 = conn
.query_row("SELECT COUNT(*) FROM sandbox_violations", [], |row| {
row.get(0)
})
.map_err(|e| e.to_string())?;
// Get violations by operation type
let mut stmt = conn
.prepare(
"SELECT operation_type, COUNT(*) as count
FROM sandbox_violations
GROUP BY operation_type
ORDER BY count DESC",
)
.map_err(|e| e.to_string())?;
let by_operation: Vec<(String, i64)> = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
// Get recent violations count (last 24 hours)
let recent: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sandbox_violations WHERE denied_at > datetime('now', '-1 day')",
[],
|row| row.get(0),
)
.map_err(|e| e.to_string())?;
Ok(serde_json::json!({
"total": total,
"recent_24h": recent,
"by_operation": by_operation.into_iter().map(|(op, count)| {
serde_json::json!({
"operation": op,
"count": count
})
}).collect::<Vec<_>>()
}))
}
/// Export a single sandbox profile with its rules
#[tauri::command]
pub async fn export_sandbox_profile(
db: State<'_, AgentDb>,
profile_id: i64,
) -> Result<SandboxProfileExport, String> {
// Get the profile
let profile = {
let conn = db.0.lock().map_err(|e| e.to_string())?;
crate::sandbox::profile::load_profile(&conn, profile_id).map_err(|e| e.to_string())?
};
// Get the rules
let rules = list_sandbox_rules(db.clone(), profile_id).await?;
Ok(SandboxProfileExport {
version: 1,
exported_at: chrono::Utc::now().to_rfc3339(),
platform: std::env::consts::OS.to_string(),
profiles: vec![SandboxProfileWithRules { profile, rules }],
})
}
/// Export all sandbox profiles
#[tauri::command]
pub async fn export_all_sandbox_profiles(
db: State<'_, AgentDb>,
) -> Result<SandboxProfileExport, String> {
let profiles = list_sandbox_profiles(db.clone()).await?;
let mut profile_exports = Vec::new();
for profile in profiles {
if let Some(id) = profile.id {
let rules = list_sandbox_rules(db.clone(), id).await?;
profile_exports.push(SandboxProfileWithRules { profile, rules });
}
}
Ok(SandboxProfileExport {
version: 1,
exported_at: chrono::Utc::now().to_rfc3339(),
platform: std::env::consts::OS.to_string(),
profiles: profile_exports,
})
}
/// Import sandbox profiles from export data
#[tauri::command]
pub async fn import_sandbox_profiles(
db: State<'_, AgentDb>,
export_data: SandboxProfileExport,
) -> Result<Vec<ImportResult>, String> {
let mut results = Vec::new();
// Validate version
if export_data.version != 1 {
return Err(format!(
"Unsupported export version: {}",
export_data.version
));
}
for profile_export in export_data.profiles {
let mut profile = profile_export.profile;
let original_name = profile.name.clone();
// Check for name conflicts
let existing: Result<i64, _> = {
let conn = db.0.lock().map_err(|e| e.to_string())?;
conn.query_row(
"SELECT id FROM sandbox_profiles WHERE name = ?1",
params![&profile.name],
|row| row.get(0),
)
};
let (imported, new_name) = match existing {
Ok(_) => {
// Name conflict - append timestamp
let new_name = format!(
"{} (imported {})",
profile.name,
chrono::Utc::now().format("%Y-%m-%d %H:%M")
);
profile.name = new_name.clone();
(true, Some(new_name))
}
Err(_) => (true, None),
};
if imported {
// Reset profile fields for new insert
profile.id = None;
profile.is_default = false; // Never import as default
// Create the profile
let created_profile =
create_sandbox_profile(db.clone(), profile.name.clone(), profile.description)
.await?;
if let Some(new_id) = created_profile.id {
// Import rules
for rule in profile_export.rules {
if rule.enabled {
// Create the rule with the new profile ID
let _ = create_sandbox_rule(
db.clone(),
new_id,
rule.operation_type,
rule.pattern_type,
rule.pattern_value,
rule.enabled,
rule.platform_support,
)
.await;
}
}
// Update profile status if needed
if profile.is_active {
let _ = update_sandbox_profile(
db.clone(),
new_id,
created_profile.name,
created_profile.description,
profile.is_active,
false, // Never set as default on import
)
.await;
}
}
results.push(ImportResult {
profile_name: original_name,
imported: true,
reason: new_name
.as_ref()
.map(|_| "Name conflict resolved".to_string()),
new_name,
});
}
}
Ok(results)
}

View File

@@ -5,7 +5,6 @@ pub mod checkpoint;
pub mod claude_binary;
pub mod commands;
pub mod process;
pub mod sandbox;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {

View File

@@ -5,7 +5,6 @@ mod checkpoint;
mod claude_binary;
mod commands;
mod process;
mod sandbox;
use checkpoint::state::CheckpointState;
use commands::agents::{
@@ -34,13 +33,6 @@ use commands::mcp::{
mcp_read_project_config, mcp_remove, mcp_reset_project_choices, mcp_save_project_config,
mcp_serve, mcp_test_connection,
};
use commands::sandbox::{
clear_sandbox_violations, create_sandbox_profile, create_sandbox_rule, delete_sandbox_profile,
delete_sandbox_rule, export_all_sandbox_profiles, export_sandbox_profile,
get_platform_capabilities, get_sandbox_profile, get_sandbox_violation_stats,
import_sandbox_profiles, list_sandbox_profiles, list_sandbox_rules, list_sandbox_violations,
log_sandbox_violation, test_sandbox_profile, update_sandbox_profile, update_sandbox_rule,
};
use commands::screenshot::{capture_url_screenshot, cleanup_screenshot_temp_files};
use commands::usage::{
get_session_stats, get_usage_by_date_range, get_usage_details, get_usage_stats,
@@ -53,14 +45,6 @@ fn main() {
// Initialize logger
env_logger::init();
// Check if we need to activate sandbox in this process
if sandbox::executor::should_activate_sandbox() {
// This is a child process that needs sandbox activation
if let Err(e) = sandbox::executor::SandboxExecutor::activate_sandbox_in_child() {
log::error!("Failed to activate sandbox: {}", e);
// Continue without sandbox rather than crashing
}
}
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
@@ -161,24 +145,6 @@ fn main() {
fetch_github_agents,
fetch_github_agent_content,
import_agent_from_github,
list_sandbox_profiles,
get_sandbox_profile,
create_sandbox_profile,
update_sandbox_profile,
delete_sandbox_profile,
list_sandbox_rules,
create_sandbox_rule,
update_sandbox_rule,
delete_sandbox_rule,
test_sandbox_profile,
get_platform_capabilities,
list_sandbox_violations,
log_sandbox_violation,
clear_sandbox_violations,
get_sandbox_violation_stats,
export_sandbox_profile,
export_all_sandbox_profiles,
import_sandbox_profiles,
get_usage_stats,
get_usage_by_date_range,
get_usage_details,

View File

@@ -1,212 +0,0 @@
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(())
}

View File

@@ -1,511 +0,0 @@
use anyhow::{Context, Result};
#[cfg(unix)]
use gaol::sandbox::{
ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods,
};
use log::{debug, error, info, warn};
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 {
#[cfg(unix)]
profile: gaol::profile::Profile,
project_path: PathBuf,
serialized_profile: Option<SerializedProfile>,
}
#[cfg(unix)]
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))
}
}
}
}
// 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
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],
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,
}
#[cfg(unix)]
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
)
})
}
#[cfg(unix)]
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)
})
}

View File

@@ -1,21 +0,0 @@
#[allow(unused)]
pub mod defaults;
#[allow(unused)]
pub mod executor;
#[allow(unused)]
pub mod platform;
#[allow(unused)]
pub mod profile;
// These are used in agents.rs and claude.rs via direct module paths
#[allow(unused)]
pub use profile::{ProfileBuilder, SandboxProfile, SandboxRule};
// These are used in main.rs and sandbox.rs
#[allow(unused)]
pub use executor::{should_activate_sandbox, SandboxExecutor};
// These are used in sandbox.rs
#[allow(unused)]
pub use platform::{get_platform_capabilities, PlatformCapabilities};
// Used for initial setup
#[allow(unused)]
pub use defaults::create_default_profiles;

View File

@@ -1,179 +0,0 @@
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")
}

View File

@@ -1,556 +0,0 @@
use crate::sandbox::executor::{SerializedOperation, SerializedProfile};
use anyhow::{Context, Result};
#[cfg(unix)]
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;
/// 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 {
#[cfg(unix)]
pub profile: Profile,
#[cfg(not(unix))]
pub profile: (), // Placeholder for Windows
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 {
#[cfg(unix)]
profile: Profile::new(vec![])
.map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?,
#[cfg(not(unix))]
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
#[cfg(unix)]
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 (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
pub fn build_profile_with_serialization(
&self,
rules: Vec<SandboxRule>,
) -> Result<ProfileBuildResult> {
#[cfg(unix)]
{
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,
},
})
}
#[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<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
#[cfg(unix)]
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
#[cfg(unix)]
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
#[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
.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
#[cfg(unix)]
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
#[cfg(unix)]
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(&current_os.to_string());
}
}
// 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<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
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
#[cfg(unix)]
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)
}
/// 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(())
}