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:
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -680,7 +680,6 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"gaol",
|
||||
"glob",
|
||||
"headless_chrome",
|
||||
"libc",
|
||||
@@ -1640,16 +1639,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gaol"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061957ca7a966a39a79ebca393a9a6c7babda10bf9dd6f11d00041558d929c22"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk"
|
||||
version = "0.18.2"
|
||||
|
@@ -49,8 +49,6 @@ zstd = "0.13"
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
walkdir = "2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
gaol = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26"
|
||||
|
@@ -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
|
||||
|
@@ -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)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
@@ -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(¤t_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> {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
pub mod agents;
|
||||
pub mod claude;
|
||||
pub mod mcp;
|
||||
pub mod sandbox;
|
||||
pub mod screenshot;
|
||||
pub mod usage;
|
||||
|
@@ -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)
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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,
|
||||
|
@@ -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(())
|
||||
}
|
@@ -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)
|
||||
})
|
||||
}
|
@@ -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;
|
@@ -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")
|
||||
}
|
@@ -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(¤t_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(())
|
||||
}
|
@@ -1,143 +0,0 @@
|
||||
# Sandbox Test Suite Summary
|
||||
|
||||
## Overview
|
||||
|
||||
A comprehensive test suite has been created for the sandbox functionality in Claudia. The test suite validates that the sandboxing operations using the `gaol` crate work correctly across different platforms (Linux, macOS, FreeBSD).
|
||||
|
||||
## Test Structure Created
|
||||
|
||||
### 1. **Test Organization** (`tests/sandbox_tests.rs`)
|
||||
- Main entry point for all sandbox tests
|
||||
- Integrates all test modules
|
||||
|
||||
### 2. **Common Test Utilities** (`tests/sandbox/common/`)
|
||||
- **fixtures.rs**: Test data, database setup, file system creation, and standard profiles
|
||||
- **helpers.rs**: Helper functions, platform detection, test command execution, and code generation
|
||||
|
||||
### 3. **Unit Tests** (`tests/sandbox/unit/`)
|
||||
- **profile_builder.rs**: Tests for ProfileBuilder including rule parsing, platform filtering, and template expansion
|
||||
- **platform.rs**: Tests for platform capability detection and operation support levels
|
||||
- **executor.rs**: Tests for SandboxExecutor creation and command preparation
|
||||
|
||||
### 4. **Integration Tests** (`tests/sandbox/integration/`)
|
||||
- **file_operations.rs**: Tests file access control (allowed/forbidden reads, writes, metadata)
|
||||
- **network_operations.rs**: Tests network access control (TCP, local sockets, port filtering)
|
||||
- **system_info.rs**: Tests system information access (platform-specific)
|
||||
- **process_isolation.rs**: Tests process spawning restrictions (fork, exec, threads)
|
||||
- **violations.rs**: Tests violation detection and patterns
|
||||
|
||||
### 5. **End-to-End Tests** (`tests/sandbox/e2e/`)
|
||||
- **agent_sandbox.rs**: Tests agent execution with sandbox profiles
|
||||
- **claude_sandbox.rs**: Tests Claude command execution with sandboxing
|
||||
|
||||
## Key Features
|
||||
|
||||
### Platform Support
|
||||
- **Cross-platform testing**: Tests adapt to platform capabilities
|
||||
- **Skip unsupported**: Tests gracefully skip on unsupported platforms
|
||||
- **Platform-specific tests**: Special tests for platform-specific features
|
||||
|
||||
### Test Helpers
|
||||
- **Test binary creation**: Dynamically compiles test programs
|
||||
- **Mock file systems**: Creates temporary test environments
|
||||
- **Database fixtures**: Sets up test databases with profiles
|
||||
- **Assertion helpers**: Specialized assertions for sandbox behavior
|
||||
|
||||
### Safety Features
|
||||
- **Serial execution**: Tests run serially to avoid conflicts
|
||||
- **Timeout handling**: Commands have timeout protection
|
||||
- **Resource cleanup**: Temporary files and resources are cleaned up
|
||||
|
||||
## Running the Tests
|
||||
|
||||
```bash
|
||||
# Run all sandbox tests
|
||||
cargo test --test sandbox_tests
|
||||
|
||||
# Run specific categories
|
||||
cargo test --test sandbox_tests unit::
|
||||
cargo test --test sandbox_tests integration::
|
||||
cargo test --test sandbox_tests e2e:: -- --ignored
|
||||
|
||||
# Run with output
|
||||
cargo test --test sandbox_tests -- --nocapture
|
||||
|
||||
# Run serially (required for some tests)
|
||||
cargo test --test sandbox_tests -- --test-threads=1
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
1. **Profile Management**
|
||||
- Profile creation and validation
|
||||
- Rule parsing and conflicts
|
||||
- Template variable expansion
|
||||
- Platform compatibility
|
||||
|
||||
2. **File Operations**
|
||||
- Allowed file reads
|
||||
- Forbidden file access
|
||||
- File write prevention
|
||||
- Metadata operations
|
||||
|
||||
3. **Network Operations**
|
||||
- Network access control
|
||||
- Port-specific rules (macOS)
|
||||
- Local socket connections
|
||||
|
||||
4. **Process Isolation**
|
||||
- Process spawn prevention
|
||||
- Fork/exec blocking
|
||||
- Thread creation (allowed)
|
||||
|
||||
5. **System Information**
|
||||
- Platform-specific access control
|
||||
- macOS sysctl operations
|
||||
|
||||
6. **Violation Tracking**
|
||||
- Violation detection
|
||||
- Pattern matching
|
||||
- Multiple violations
|
||||
|
||||
## Platform-Specific Behavior
|
||||
|
||||
| Feature | Linux | macOS | FreeBSD |
|
||||
|---------|-------|-------|---------|
|
||||
| File Read Control | ✅ | ✅ | ❌ |
|
||||
| Metadata Read | 🟡¹ | ✅ | ❌ |
|
||||
| Network All | ✅ | ✅ | ❌ |
|
||||
| Network TCP Port | ❌ | ✅ | ❌ |
|
||||
| Network Local Socket | ❌ | ✅ | ❌ |
|
||||
| System Info Read | ❌ | ✅ | ✅² |
|
||||
|
||||
¹ Cannot be precisely controlled on Linux
|
||||
² Always allowed on FreeBSD
|
||||
|
||||
## Dependencies Added
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
serial_test = "3"
|
||||
test-case = "3"
|
||||
once_cell = "1"
|
||||
proptest = "1"
|
||||
pretty_assertions = "1"
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **CI Integration**: Configure CI to run sandbox tests on multiple platforms
|
||||
2. **Performance Tests**: Add benchmarks for sandbox overhead
|
||||
3. **Stress Tests**: Test with many simultaneous sandboxed processes
|
||||
4. **Mock Claude**: Create mock Claude command for E2E tests without dependencies
|
||||
5. **Coverage Report**: Generate test coverage reports
|
||||
|
||||
## Notes
|
||||
|
||||
- Some E2E tests are marked `#[ignore]` as they require Claude to be installed
|
||||
- Integration tests use `serial_test` to prevent conflicts
|
||||
- Test binaries are compiled on-demand for realistic testing
|
||||
- The test suite gracefully handles platform limitations
|
@@ -21,7 +21,7 @@ test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
### Implementation Details:
|
||||
|
||||
#### Real Claude Execution (`tests/sandbox/common/claude_real.rs`):
|
||||
#### Real Claude Execution:
|
||||
- `execute_claude_task()` - Executes Claude with specified task and captures output
|
||||
- Supports timeout handling (gtimeout on macOS, timeout on Linux)
|
||||
- Returns structured output with stdout, stderr, exit code, and duration
|
||||
@@ -33,18 +33,18 @@ test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
- 20-second timeout to allow Claude sufficient time to respond
|
||||
|
||||
#### Key Test Updates:
|
||||
1. **Agent Tests** (`agent_sandbox.rs`):
|
||||
- `test_agent_with_minimal_profile` - Tests with minimal sandbox permissions
|
||||
- `test_agent_with_standard_profile` - Tests with standard permissions
|
||||
- `test_agent_without_sandbox` - Control test without sandbox
|
||||
1. **Agent Tests**:
|
||||
- Test agent execution with various permission configurations
|
||||
- Test agent execution in different project contexts
|
||||
- Control tests for baseline behavior
|
||||
|
||||
2. **Claude Sandbox Tests** (`claude_sandbox.rs`):
|
||||
- `test_claude_with_default_sandbox` - Tests default sandbox profile
|
||||
- `test_claude_sandbox_disabled` - Tests with inactive sandbox
|
||||
2. **Claude Tests**:
|
||||
- Test Claude execution with default settings
|
||||
- Test Claude execution with custom configurations
|
||||
|
||||
### Benefits of Real Claude Testing:
|
||||
- **Authenticity**: Tests validate actual Claude behavior, not mocked responses
|
||||
- **Integration**: Ensures the sandbox system works with real Claude execution
|
||||
- **Integration**: Ensures the system works with real Claude execution
|
||||
- **End-to-End**: Complete validation from command invocation to output parsing
|
||||
- **No External Dependencies**: Uses `--dangerously-skip-permissions` flag
|
||||
|
||||
@@ -53,6 +53,6 @@ test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
- No ignored tests
|
||||
- No TODOs in test code
|
||||
- Clean compilation with no warnings
|
||||
- Platform-aware sandbox expectations (Linux vs macOS)
|
||||
- Platform-aware expectations for different operating systems
|
||||
|
||||
The test suite now provides comprehensive end-to-end validation with actual Claude execution.
|
||||
The test suite now provides comprehensive end-to-end validation with actual Claude execution.
|
||||
|
@@ -13,7 +13,7 @@
|
||||
- Created `create_test_binary_with_deps` function
|
||||
|
||||
3. **Fixed Database Schema Issue** ✅
|
||||
- Added missing tables (agents, agent_runs, sandbox_violations) to test database
|
||||
- Added missing tables (agents, agent_runs) to test database
|
||||
- Fixed foreign key constraint issues
|
||||
|
||||
4. **Fixed Mutex Poisoning** ✅
|
||||
@@ -35,8 +35,8 @@
|
||||
7. **Removed All TODOs** ✅
|
||||
- No TODOs remain in test code
|
||||
|
||||
8. **Handled Platform-Specific Sandbox Limitations** ✅
|
||||
- Tests properly handle macOS sandbox limitations
|
||||
8. **Handled Platform-Specific Limitations** ✅
|
||||
- Tests properly handle platform-specific differences
|
||||
- Platform-aware assertions prevent false failures
|
||||
|
||||
## Test Results:
|
||||
@@ -52,4 +52,4 @@ test result: ok. 61 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
- Comprehensive mock system for external dependencies
|
||||
- Platform-aware testing for cross-platform compatibility
|
||||
|
||||
The test suite is now production-ready with full coverage and no issues.
|
||||
The test suite is now production-ready with full coverage and no issues.
|
||||
|
@@ -1,155 +0,0 @@
|
||||
# Sandbox Test Suite
|
||||
|
||||
This directory contains a comprehensive test suite for the sandbox functionality in Claudia. The tests are designed to verify that the sandboxing operations work correctly across different platforms (Linux, macOS, FreeBSD).
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
sandbox/
|
||||
├── common/ # Shared test utilities
|
||||
│ ├── fixtures.rs # Test data and environment setup
|
||||
│ └── helpers.rs # Helper functions and assertions
|
||||
├── unit/ # Unit tests for individual components
|
||||
│ ├── profile_builder.rs # ProfileBuilder tests
|
||||
│ ├── platform.rs # Platform capability tests
|
||||
│ └── executor.rs # SandboxExecutor tests
|
||||
├── integration/ # Integration tests for sandbox operations
|
||||
│ ├── file_operations.rs # File access control tests
|
||||
│ ├── network_operations.rs # Network access control tests
|
||||
│ ├── system_info.rs # System info access tests
|
||||
│ ├── process_isolation.rs # Process spawning tests
|
||||
│ └── violations.rs # Violation detection tests
|
||||
└── e2e/ # End-to-end tests
|
||||
├── agent_sandbox.rs # Agent execution with sandbox
|
||||
└── claude_sandbox.rs # Claude command with sandbox
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run all sandbox tests:
|
||||
```bash
|
||||
cargo test --test sandbox_tests
|
||||
```
|
||||
|
||||
### Run specific test categories:
|
||||
```bash
|
||||
# Unit tests only
|
||||
cargo test --test sandbox_tests unit::
|
||||
|
||||
# Integration tests only
|
||||
cargo test --test sandbox_tests integration::
|
||||
|
||||
# End-to-end tests only (requires Claude to be installed)
|
||||
cargo test --test sandbox_tests e2e:: -- --ignored
|
||||
```
|
||||
|
||||
### Run tests with output:
|
||||
```bash
|
||||
cargo test --test sandbox_tests -- --nocapture
|
||||
```
|
||||
|
||||
### Run tests serially (required for some integration tests):
|
||||
```bash
|
||||
cargo test --test sandbox_tests -- --test-threads=1
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **ProfileBuilder Tests** (`unit/profile_builder.rs`)
|
||||
- Profile creation and validation
|
||||
- Rule parsing and platform filtering
|
||||
- Template variable expansion
|
||||
- Invalid operation handling
|
||||
|
||||
2. **Platform Tests** (`unit/platform.rs`)
|
||||
- Platform capability detection
|
||||
- Operation support levels
|
||||
- Cross-platform compatibility
|
||||
|
||||
3. **Executor Tests** (`unit/executor.rs`)
|
||||
- Sandbox executor creation
|
||||
- Command preparation
|
||||
- Environment variable handling
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **File Operations** (`integration/file_operations.rs`)
|
||||
- ✅ Allowed file reads succeed
|
||||
- ❌ Forbidden file reads fail
|
||||
- ❌ File writes always fail
|
||||
- 📊 Metadata operations respect permissions
|
||||
- 🔄 Template variable expansion works
|
||||
|
||||
2. **Network Operations** (`integration/network_operations.rs`)
|
||||
- ✅ Allowed network connections succeed
|
||||
- ❌ Forbidden network connections fail
|
||||
- 🎯 Port-specific rules (macOS only)
|
||||
- 🔌 Local socket connections
|
||||
|
||||
3. **System Information** (`integration/system_info.rs`)
|
||||
- 🍎 macOS: Can be allowed/forbidden
|
||||
- 🐧 Linux: Never allowed
|
||||
- 👹 FreeBSD: Always allowed
|
||||
|
||||
4. **Process Isolation** (`integration/process_isolation.rs`)
|
||||
- ❌ Process spawning forbidden
|
||||
- ❌ Fork/exec operations blocked
|
||||
- ✅ Thread creation allowed
|
||||
|
||||
5. **Violations** (`integration/violations.rs`)
|
||||
- 🚨 Violation detection
|
||||
- 📝 Violation patterns
|
||||
- 🔢 Multiple violations handling
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
1. **Agent Sandbox** (`e2e/agent_sandbox.rs`)
|
||||
- Agent execution with profiles
|
||||
- Profile switching
|
||||
- Violation logging
|
||||
|
||||
2. **Claude Sandbox** (`e2e/claude_sandbox.rs`)
|
||||
- Claude command sandboxing
|
||||
- Settings integration
|
||||
- Session management
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Feature | Linux | macOS | FreeBSD |
|
||||
|---------|-------|-------|---------|
|
||||
| File Read Control | ✅ | ✅ | ❌ |
|
||||
| Metadata Read | 🟡¹ | ✅ | ❌ |
|
||||
| Network All | ✅ | ✅ | ❌ |
|
||||
| Network TCP Port | ❌ | ✅ | ❌ |
|
||||
| Network Local Socket | ❌ | ✅ | ❌ |
|
||||
| System Info Read | ❌ | ✅ | ✅² |
|
||||
|
||||
¹ Cannot be precisely controlled on Linux (allowed if file read is allowed)
|
||||
² Always allowed on FreeBSD (cannot be restricted)
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Serial Execution**: Many integration tests are marked with `#[serial]` and must run one at a time to avoid conflicts.
|
||||
|
||||
2. **Platform Dependencies**: Some tests will be skipped on unsupported platforms. The test suite handles this gracefully.
|
||||
|
||||
3. **Privilege Requirements**: Sandbox tests generally don't require elevated privileges, but some operations may fail in restricted environments (e.g., CI).
|
||||
|
||||
4. **Claude Dependency**: E2E tests that actually execute Claude are marked with `#[ignore]` by default. Run with `--ignored` flag when Claude is installed.
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
1. **Enable Logging**: Set `RUST_LOG=debug` to see detailed sandbox operations
|
||||
2. **Check Platform**: Verify the test is supported on your platform
|
||||
3. **Check Permissions**: Ensure test binaries can be created and executed
|
||||
4. **Inspect Output**: Use `--nocapture` to see all test output
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
1. Choose the appropriate category (unit/integration/e2e)
|
||||
2. Use the test helpers from `common/`
|
||||
3. Mark with `#[serial]` if the test modifies global state
|
||||
4. Use `skip_if_unsupported!()` macro for platform-specific tests
|
||||
5. Document any special requirements or limitations
|
@@ -1,187 +0,0 @@
|
||||
//! Helper functions for executing real Claude commands in tests
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Execute Claude with a specific task and capture output
|
||||
pub fn execute_claude_task(
|
||||
project_path: &Path,
|
||||
task: &str,
|
||||
system_prompt: Option<&str>,
|
||||
model: Option<&str>,
|
||||
sandbox_profile_id: Option<i64>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<ClaudeOutput> {
|
||||
let mut cmd = Command::new("claude");
|
||||
|
||||
// Add task
|
||||
cmd.arg("-p").arg(task);
|
||||
|
||||
// Add system prompt if provided
|
||||
if let Some(prompt) = system_prompt {
|
||||
cmd.arg("--system-prompt").arg(prompt);
|
||||
}
|
||||
|
||||
// Add model if provided
|
||||
if let Some(m) = model {
|
||||
cmd.arg("--model").arg(m);
|
||||
}
|
||||
|
||||
// Always add these flags for testing
|
||||
cmd.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Add sandbox profile ID if provided
|
||||
if let Some(profile_id) = sandbox_profile_id {
|
||||
cmd.env("CLAUDIA_SANDBOX_PROFILE_ID", profile_id.to_string());
|
||||
}
|
||||
|
||||
// Execute with timeout (use gtimeout on macOS, timeout on Linux)
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let timeout_cmd = if cfg!(target_os = "macos") {
|
||||
// On macOS, try gtimeout (from GNU coreutils) first, fallback to direct execution
|
||||
if std::process::Command::new("which")
|
||||
.arg("gtimeout")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"gtimeout"
|
||||
} else {
|
||||
// If gtimeout not available, just run without timeout
|
||||
""
|
||||
}
|
||||
} else {
|
||||
"timeout"
|
||||
};
|
||||
|
||||
let output = if timeout_cmd.is_empty() {
|
||||
// Run without timeout wrapper
|
||||
cmd.output().context("Failed to execute Claude command")?
|
||||
} else {
|
||||
// Run with timeout wrapper
|
||||
let mut timeout_cmd = Command::new(timeout_cmd);
|
||||
timeout_cmd
|
||||
.arg(timeout_secs.to_string())
|
||||
.arg("claude")
|
||||
.args(cmd.get_args())
|
||||
.current_dir(project_path)
|
||||
.envs(cmd.get_envs().filter_map(|(k, v)| v.map(|v| (k, v))))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.context("Failed to execute Claude command with timeout")?
|
||||
};
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
Ok(ClaudeOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
exit_code: output.status.code().unwrap_or(-1),
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of Claude execution
|
||||
#[derive(Debug)]
|
||||
pub struct ClaudeOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: i32,
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
impl ClaudeOutput {
|
||||
/// Check if the output contains evidence of a specific operation
|
||||
pub fn contains_operation(&self, operation: &str) -> bool {
|
||||
self.stdout.contains(operation) || self.stderr.contains(operation)
|
||||
}
|
||||
|
||||
/// Check if operation was blocked (look for permission denied, sandbox violation, etc)
|
||||
pub fn operation_was_blocked(&self, operation: &str) -> bool {
|
||||
let blocked_patterns = [
|
||||
"permission denied",
|
||||
"not permitted",
|
||||
"blocked by sandbox",
|
||||
"operation not allowed",
|
||||
"access denied",
|
||||
"sandbox violation",
|
||||
];
|
||||
|
||||
let output = format!("{}\n{}", self.stdout, self.stderr).to_lowercase();
|
||||
let op_lower = operation.to_lowercase();
|
||||
|
||||
// Check if operation was mentioned along with a block pattern
|
||||
blocked_patterns
|
||||
.iter()
|
||||
.any(|pattern| output.contains(&op_lower) && output.contains(pattern))
|
||||
}
|
||||
|
||||
/// Check if file read was successful
|
||||
pub fn file_read_succeeded(&self, filename: &str) -> bool {
|
||||
// Look for patterns indicating successful file read
|
||||
let patterns = [
|
||||
&format!("Read {}", filename),
|
||||
&format!("Reading {}", filename),
|
||||
&format!("Contents of {}", filename),
|
||||
"test content", // Our test files contain this
|
||||
];
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
|
||||
/// Check if network connection was attempted
|
||||
pub fn network_attempted(&self, host: &str) -> bool {
|
||||
let patterns = [
|
||||
&format!("Connecting to {}", host),
|
||||
&format!("Connected to {}", host),
|
||||
&format!("connect to {}", host),
|
||||
host,
|
||||
];
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
/// Common test tasks for Claude
|
||||
pub mod tasks {
|
||||
/// Task to read a file
|
||||
pub fn read_file(filename: &str) -> String {
|
||||
format!("Read the file {} and show me its contents", filename)
|
||||
}
|
||||
|
||||
/// Task to attempt network connection
|
||||
pub fn connect_network(host: &str) -> String {
|
||||
format!("Try to connect to {} and tell me if it works", host)
|
||||
}
|
||||
|
||||
/// Task to do multiple operations
|
||||
pub fn multi_operation() -> String {
|
||||
"Read the file ./test.txt in the current directory and show its contents".to_string()
|
||||
}
|
||||
|
||||
/// Task to test file write
|
||||
pub fn write_file(filename: &str, content: &str) -> String {
|
||||
format!(
|
||||
"Create a file called {} with the content '{}'",
|
||||
filename, content
|
||||
)
|
||||
}
|
||||
|
||||
/// Task to test process spawning
|
||||
pub fn spawn_process(command: &str) -> String {
|
||||
format!("Run the command '{}' and show me the output", command)
|
||||
}
|
||||
}
|
@@ -1,332 +0,0 @@
|
||||
//! Test fixtures and data for sandbox testing
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::PathBuf;
|
||||
// Removed std::sync::Mutex - using parking_lot::Mutex instead
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
/// Global test database for sandbox testing
|
||||
/// Using parking_lot::Mutex which doesn't poison on panic
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub static TEST_DB: Lazy<Mutex<TestDatabase>> =
|
||||
Lazy::new(|| Mutex::new(TestDatabase::new().expect("Failed to create test database")));
|
||||
|
||||
/// Test database manager
|
||||
pub struct TestDatabase {
|
||||
pub conn: Connection,
|
||||
pub temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestDatabase {
|
||||
/// Create a new test database with schema
|
||||
pub fn new() -> Result<Self> {
|
||||
let temp_dir = tempdir()?;
|
||||
let db_path = temp_dir.path().join("test_sandbox.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
// Initialize schema
|
||||
Self::init_schema(&conn)?;
|
||||
|
||||
Ok(Self { conn, temp_dir })
|
||||
}
|
||||
|
||||
/// Initialize database schema
|
||||
fn init_schema(conn: &Connection) -> Result<()> {
|
||||
// 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 agents table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
system_prompt TEXT NOT NULL,
|
||||
default_task TEXT,
|
||||
model TEXT NOT NULL DEFAULT 'sonnet',
|
||||
sandbox_profile_id INTEGER REFERENCES sandbox_profiles(id),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create agent_runs table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agent_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id INTEGER NOT NULL,
|
||||
agent_name TEXT NOT NULL,
|
||||
agent_icon TEXT NOT NULL,
|
||||
task TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
project_path TEXT NOT NULL,
|
||||
output TEXT NOT NULL DEFAULT '',
|
||||
duration_ms INTEGER,
|
||||
total_tokens INTEGER,
|
||||
cost_usd REAL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TEXT,
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 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 trigger to update the updated_at timestamp for agents
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_agent_timestamp
|
||||
AFTER UPDATE ON agents
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE agents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 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",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a test profile with rules
|
||||
pub fn create_test_profile(&self, name: &str, rules: Vec<TestRule>) -> Result<i64> {
|
||||
// Insert profile
|
||||
self.conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![name, format!("Test profile: {name}"), true, false],
|
||||
)?;
|
||||
|
||||
let profile_id = self.conn.last_insert_rowid();
|
||||
|
||||
// Insert rules
|
||||
for rule in rules {
|
||||
self.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,
|
||||
rule.operation_type,
|
||||
rule.pattern_type,
|
||||
rule.pattern_value,
|
||||
rule.enabled,
|
||||
rule.platform_support
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(profile_id)
|
||||
}
|
||||
|
||||
/// Reset database to clean state
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
// Delete in the correct order to respect foreign key constraints
|
||||
self.conn.execute("DELETE FROM sandbox_violations", [])?;
|
||||
self.conn.execute("DELETE FROM agent_runs", [])?;
|
||||
self.conn.execute("DELETE FROM agents", [])?;
|
||||
self.conn.execute("DELETE FROM sandbox_rules", [])?;
|
||||
self.conn.execute("DELETE FROM sandbox_profiles", [])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Test rule structure
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestRule {
|
||||
pub operation_type: String,
|
||||
pub pattern_type: String,
|
||||
pub pattern_value: String,
|
||||
pub enabled: bool,
|
||||
pub platform_support: Option<String>,
|
||||
}
|
||||
|
||||
impl TestRule {
|
||||
/// Create a file read rule
|
||||
pub fn file_read(path: &str, subpath: bool) -> Self {
|
||||
Self {
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: if subpath { "subpath" } else { "literal" }.to_string(),
|
||||
pattern_value: path.to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network rule
|
||||
pub fn network_all() -> Self {
|
||||
Self {
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: String::new(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network TCP rule
|
||||
pub fn network_tcp(port: u16) -> Self {
|
||||
Self {
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "tcp".to_string(),
|
||||
pattern_value: port.to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a system info read rule
|
||||
pub fn system_info_read() -> Self {
|
||||
Self {
|
||||
operation_type: "system_info_read".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: String::new(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file system structure
|
||||
pub struct TestFileSystem {
|
||||
pub root: TempDir,
|
||||
pub project_path: PathBuf,
|
||||
pub allowed_path: PathBuf,
|
||||
pub forbidden_path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestFileSystem {
|
||||
/// Create a new test file system with predefined structure
|
||||
pub fn new() -> Result<Self> {
|
||||
let root = tempdir()?;
|
||||
let root_path = root.path();
|
||||
|
||||
// Create project directory
|
||||
let project_path = root_path.join("test_project");
|
||||
std::fs::create_dir_all(&project_path)?;
|
||||
|
||||
// Create allowed directory
|
||||
let allowed_path = root_path.join("allowed");
|
||||
std::fs::create_dir_all(&allowed_path)?;
|
||||
std::fs::write(allowed_path.join("test.txt"), "allowed content")?;
|
||||
|
||||
// Create forbidden directory
|
||||
let forbidden_path = root_path.join("forbidden");
|
||||
std::fs::create_dir_all(&forbidden_path)?;
|
||||
std::fs::write(forbidden_path.join("secret.txt"), "forbidden content")?;
|
||||
|
||||
// Create project files
|
||||
std::fs::write(project_path.join("main.rs"), "fn main() {}")?;
|
||||
std::fs::write(
|
||||
project_path.join("Cargo.toml"),
|
||||
"[package]\nname = \"test\"",
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
project_path,
|
||||
allowed_path,
|
||||
forbidden_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard test profiles
|
||||
pub mod profiles {
|
||||
use super::*;
|
||||
|
||||
/// Minimal profile - only project access
|
||||
pub fn minimal(project_path: &str) -> Vec<TestRule> {
|
||||
vec![TestRule::file_read(project_path, true)]
|
||||
}
|
||||
|
||||
/// Standard profile - project + system libraries
|
||||
pub fn standard(project_path: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
TestRule::file_read("/usr/lib", true),
|
||||
TestRule::file_read("/usr/local/lib", true),
|
||||
TestRule::network_all(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Development profile - more permissive
|
||||
pub fn development(project_path: &str, home_dir: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
TestRule::file_read("/usr", true),
|
||||
TestRule::file_read("/opt", true),
|
||||
TestRule::file_read(home_dir, true),
|
||||
TestRule::network_all(),
|
||||
TestRule::system_info_read(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Network-only profile
|
||||
pub fn network_only() -> Vec<TestRule> {
|
||||
vec![TestRule::network_all()]
|
||||
}
|
||||
|
||||
/// File-only profile
|
||||
pub fn file_only(paths: Vec<&str>) -> Vec<TestRule> {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|path| TestRule::file_read(path, true))
|
||||
.collect()
|
||||
}
|
||||
}
|
@@ -1,483 +0,0 @@
|
||||
//! Helper functions for sandbox testing
|
||||
use anyhow::{Context, Result};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Check if sandboxing is supported on the current platform
|
||||
pub fn is_sandboxing_supported() -> bool {
|
||||
matches!(env::consts::OS, "linux" | "macos" | "freebsd")
|
||||
}
|
||||
|
||||
/// Skip test if sandboxing is not supported
|
||||
#[macro_export]
|
||||
macro_rules! skip_if_unsupported {
|
||||
() => {
|
||||
if !$crate::sandbox::common::is_sandboxing_supported() {
|
||||
eprintln!(
|
||||
"Skipping test: sandboxing not supported on {}",
|
||||
std::env::consts::OS
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Platform-specific test configuration
|
||||
pub struct PlatformConfig {
|
||||
pub supports_file_read: bool,
|
||||
pub supports_metadata_read: bool,
|
||||
pub supports_network_all: bool,
|
||||
pub supports_network_tcp: bool,
|
||||
pub supports_network_local: bool,
|
||||
pub supports_system_info: bool,
|
||||
}
|
||||
|
||||
impl PlatformConfig {
|
||||
/// Get configuration for current platform
|
||||
pub fn current() -> Self {
|
||||
match env::consts::OS {
|
||||
"linux" => Self {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: false, // Cannot be precisely controlled
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: false, // Cannot filter by port
|
||||
supports_network_local: false, // Cannot filter by path
|
||||
supports_system_info: false,
|
||||
},
|
||||
"macos" => Self {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: true,
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: true,
|
||||
supports_network_local: true,
|
||||
supports_system_info: true,
|
||||
},
|
||||
"freebsd" => Self {
|
||||
supports_file_read: false,
|
||||
supports_metadata_read: false,
|
||||
supports_network_all: false,
|
||||
supports_network_tcp: false,
|
||||
supports_network_local: false,
|
||||
supports_system_info: true, // Always allowed
|
||||
},
|
||||
_ => Self {
|
||||
supports_file_read: false,
|
||||
supports_metadata_read: false,
|
||||
supports_network_all: false,
|
||||
supports_network_tcp: false,
|
||||
supports_network_local: false,
|
||||
supports_system_info: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test command builder
|
||||
pub struct TestCommand {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env_vars: Vec<(String, String)>,
|
||||
working_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl TestCommand {
|
||||
/// Create a new test command
|
||||
pub fn new(command: &str) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
env_vars: Vec::new(),
|
||||
working_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an argument
|
||||
pub fn arg(mut self, arg: &str) -> Self {
|
||||
self.args.push(arg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple arguments
|
||||
pub fn args(mut self, args: &[&str]) -> Self {
|
||||
self.args.extend(args.iter().map(|s| s.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an environment variable
|
||||
pub fn env(mut self, key: &str, value: &str) -> Self {
|
||||
self.env_vars.push((key.to_string(), value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set working directory
|
||||
pub fn current_dir(mut self, dir: &Path) -> Self {
|
||||
self.working_dir = Some(dir.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the command with timeout
|
||||
pub fn execute_with_timeout(&self, timeout: Duration) -> Result<Output> {
|
||||
let mut cmd = Command::new(&self.command);
|
||||
|
||||
cmd.args(&self.args);
|
||||
|
||||
for (key, value) in &self.env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
if let Some(dir) = &self.working_dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
|
||||
// On Unix, we can use a timeout mechanism
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
let mut child = cmd.spawn().context("Failed to spawn command")?;
|
||||
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let output = child.wait_with_output()?;
|
||||
return Ok(Output {
|
||||
status,
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() > timeout {
|
||||
child.kill()?;
|
||||
return Err(anyhow::anyhow!("Command timed out"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Fallback for non-Unix platforms
|
||||
cmd.output().context("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute and expect success
|
||||
pub fn execute_expect_success(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command failed with status {:?}. Stderr: {stderr}",
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
/// Execute and expect failure
|
||||
pub fn execute_expect_failure(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command unexpectedly succeeded. Stdout: {stdout}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stderr).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple test binary that attempts an operation
|
||||
pub fn create_test_binary(name: &str, code: &str, test_dir: &Path) -> Result<PathBuf> {
|
||||
create_test_binary_with_deps(name, code, test_dir, &[])
|
||||
}
|
||||
|
||||
/// Create a test binary with optional dependencies
|
||||
pub fn create_test_binary_with_deps(
|
||||
name: &str,
|
||||
code: &str,
|
||||
test_dir: &Path,
|
||||
dependencies: &[(&str, &str)],
|
||||
) -> Result<PathBuf> {
|
||||
let src_dir = test_dir.join("src");
|
||||
std::fs::create_dir_all(&src_dir)?;
|
||||
|
||||
// Build dependencies section
|
||||
let deps_section = if dependencies.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut deps = String::from("\n[dependencies]\n");
|
||||
for (dep_name, dep_version) in dependencies {
|
||||
deps.push_str(&format!("{dep_name} = \"{dep_version}\"\n"));
|
||||
}
|
||||
deps
|
||||
};
|
||||
|
||||
// Create Cargo.toml
|
||||
let cargo_toml = format!(
|
||||
r#"[package]
|
||||
name = "{name}"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "{name}"
|
||||
path = "src/main.rs"
|
||||
{deps_section}"#
|
||||
);
|
||||
std::fs::write(test_dir.join("Cargo.toml"), cargo_toml)?;
|
||||
|
||||
// Create main.rs
|
||||
std::fs::write(src_dir.join("main.rs"), code)?;
|
||||
|
||||
// Build the binary
|
||||
let output = Command::new("cargo")
|
||||
.arg("build")
|
||||
.arg("--release")
|
||||
.current_dir(test_dir)
|
||||
.output()
|
||||
.context("Failed to build test binary")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Failed to build test binary: {stderr}"));
|
||||
}
|
||||
|
||||
let binary_path = test_dir.join("target/release").join(name);
|
||||
Ok(binary_path)
|
||||
}
|
||||
|
||||
/// Test code snippets for various operations
|
||||
pub mod test_code {
|
||||
/// Code that reads a file
|
||||
pub fn file_read(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::read_to_string("{path}") {{
|
||||
Ok(content) => {{
|
||||
println!("SUCCESS: Read {{}} bytes", content.len());
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that reads file metadata
|
||||
pub fn file_metadata(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::metadata("{path}") {{
|
||||
Ok(metadata) => {{
|
||||
println!("SUCCESS: File size: {{}} bytes", metadata.len());
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that makes a network connection
|
||||
pub fn network_connect(addr: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
use std::net::TcpStream;
|
||||
|
||||
fn main() {{
|
||||
match TcpStream::connect("{addr}") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Connected to {addr}");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that reads system information
|
||||
pub fn system_info() -> &'static str {
|
||||
r#"
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_void;
|
||||
|
||||
extern "C" {
|
||||
fn sysctlbyname(
|
||||
name: *const std::os::raw::c_char,
|
||||
oldp: *mut c_void,
|
||||
oldlenp: *mut usize,
|
||||
newp: *const c_void,
|
||||
newlen: usize,
|
||||
) -> std::os::raw::c_int;
|
||||
}
|
||||
|
||||
let name = CString::new("hw.ncpu").unwrap();
|
||||
let mut ncpu: i32 = 0;
|
||||
let mut len = std::mem::size_of::<i32>();
|
||||
|
||||
unsafe {
|
||||
let result = sysctlbyname(
|
||||
name.as_ptr(),
|
||||
&mut ncpu as *mut _ as *mut c_void,
|
||||
&mut len,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
println!("SUCCESS: CPU count: {}", ncpu);
|
||||
} else {
|
||||
eprintln!("FAILURE: sysctlbyname failed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
println!("SUCCESS: System info test not applicable on this platform");
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that tries to spawn a process
|
||||
pub fn spawn_process() -> &'static str {
|
||||
r#"
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
match Command::new("echo").arg("test").output() {
|
||||
Ok(_) => {
|
||||
println!("SUCCESS: Spawned process");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("FAILURE: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that uses fork (requires libc)
|
||||
pub fn fork_process() -> &'static str {
|
||||
r#"
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
unsafe {
|
||||
let pid = libc::fork();
|
||||
if pid < 0 {
|
||||
eprintln!("FAILURE: fork failed");
|
||||
std::process::exit(1);
|
||||
} else if pid == 0 {
|
||||
// Child process
|
||||
println!("SUCCESS: Child process created");
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
// Parent process
|
||||
let mut status = 0;
|
||||
libc::waitpid(pid, &mut status, 0);
|
||||
println!("SUCCESS: Fork completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {
|
||||
eprintln!("FAILURE: fork not supported on this platform");
|
||||
std::process::exit(1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that uses exec (requires libc)
|
||||
pub fn exec_process() -> &'static str {
|
||||
r#"
|
||||
use std::ffi::CString;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
unsafe {
|
||||
let program = CString::new("/bin/echo").unwrap();
|
||||
let arg = CString::new("test").unwrap();
|
||||
let args = vec![program.as_ptr(), arg.as_ptr(), std::ptr::null()];
|
||||
|
||||
let result = libc::execv(program.as_ptr(), args.as_ptr());
|
||||
|
||||
// If we reach here, exec failed
|
||||
eprintln!("FAILURE: exec failed with result {}", result);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {
|
||||
eprintln!("FAILURE: exec not supported on this platform");
|
||||
std::process::exit(1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that tries to write a file
|
||||
pub fn file_write(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::write("{path}", "test content") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Wrote file");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that a command output contains expected text
|
||||
pub fn assert_output_contains(output: &str, expected: &str) {
|
||||
assert!(
|
||||
output.contains(expected),
|
||||
"Expected output to contain '{expected}', but got: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Assert that a command output indicates success
|
||||
pub fn assert_sandbox_success(output: &str) {
|
||||
assert_output_contains(output, "SUCCESS:");
|
||||
}
|
||||
|
||||
/// Assert that a command output indicates failure
|
||||
pub fn assert_sandbox_failure(output: &str) {
|
||||
assert_output_contains(output, "FAILURE:");
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
//! Common test utilities and helpers for sandbox testing
|
||||
pub mod claude_real;
|
||||
pub mod fixtures;
|
||||
pub mod helpers;
|
||||
|
||||
pub use claude_real::*;
|
||||
pub use fixtures::*;
|
||||
pub use helpers::*;
|
@@ -1,294 +0,0 @@
|
||||
//! End-to-end tests for agent execution with sandbox profiles
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use serial_test::serial;
|
||||
|
||||
/// Test agent execution with minimal sandbox profile
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_with_minimal_profile() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create minimal sandbox profile
|
||||
let rules = profiles::minimal(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("minimal_agent_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Create test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Test Agent",
|
||||
"🤖",
|
||||
"You are a test agent. Only perform the requested task.",
|
||||
"sonnet",
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Execute real Claude command with minimal profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are a test agent. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("Duration: {:?}", result.duration);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification - just check Claude ran
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124, // 0 = success, 124 = timeout
|
||||
"Claude should execute (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent execution with standard sandbox profile
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_with_standard_profile() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create standard sandbox profile
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("standard_agent_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Create test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Standard Agent",
|
||||
"🔧",
|
||||
"You are a test agent with standard permissions.",
|
||||
"sonnet",
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Execute real Claude command with standard profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are a test agent with standard permissions."),
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Standard Profile) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with standard profile (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent execution without sandbox (control test)
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_without_sandbox() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create agent without sandbox profile
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model) VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![
|
||||
"Unsandboxed Agent",
|
||||
"⚠️",
|
||||
"You are a test agent without sandbox restrictions.",
|
||||
"sonnet"
|
||||
],
|
||||
)
|
||||
.expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Execute real Claude command without sandbox profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are a test agent without sandbox restrictions."),
|
||||
Some("sonnet"),
|
||||
None, // No sandbox profile
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (No Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent run violation logging
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_run_violation_logging() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create a test profile first
|
||||
let profile_id = test_db
|
||||
.create_test_profile("violation_test", vec![])
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Create a test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Violation Test Agent",
|
||||
"⚠️",
|
||||
"Test agent for violation logging.",
|
||||
"sonnet",
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Create a test agent run
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agent_runs (agent_id, agent_name, agent_icon, task, model, project_path) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
rusqlite::params![
|
||||
agent_id,
|
||||
"Violation Test Agent",
|
||||
"⚠️",
|
||||
"Test task",
|
||||
"sonnet",
|
||||
"/test/path"
|
||||
],
|
||||
).expect("Failed to create agent run");
|
||||
|
||||
let agent_run_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Insert test violations
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO sandbox_violations (profile_id, agent_id, agent_run_id, operation_type, pattern_value)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![profile_id, agent_id, agent_run_id, "file_read_all", "/etc/passwd"],
|
||||
).expect("Failed to insert violation");
|
||||
|
||||
// Query violations
|
||||
let count: i64 = test_db
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sandbox_violations WHERE agent_id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("Failed to query violations");
|
||||
|
||||
assert_eq!(count, 1, "Should have recorded one violation");
|
||||
}
|
||||
|
||||
/// Test profile switching between agent runs
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_profile_switching() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create two different profiles
|
||||
let minimal_rules = profiles::minimal(&test_fs.project_path.to_string_lossy());
|
||||
let minimal_id = test_db
|
||||
.create_test_profile("minimal_switch", minimal_rules)
|
||||
.expect("Failed to create minimal profile");
|
||||
|
||||
let standard_rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let standard_id = test_db
|
||||
.create_test_profile("standard_switch", standard_rules)
|
||||
.expect("Failed to create standard profile");
|
||||
|
||||
// Create agent initially with minimal profile
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Switchable Agent",
|
||||
"🔄",
|
||||
"Test agent for profile switching.",
|
||||
"sonnet",
|
||||
minimal_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Update agent to use standard profile
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE agents SET sandbox_profile_id = ?1 WHERE id = ?2",
|
||||
rusqlite::params![standard_id, agent_id],
|
||||
)
|
||||
.expect("Failed to update agent profile");
|
||||
|
||||
// Verify profile was updated
|
||||
let current_profile: i64 = test_db
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT sandbox_profile_id FROM agents WHERE id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("Failed to query agent profile");
|
||||
|
||||
assert_eq!(current_profile, standard_id, "Profile should be updated");
|
||||
}
|
@@ -1,220 +0,0 @@
|
||||
//! End-to-end tests for Claude command execution with sandbox profiles
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use serial_test::serial;
|
||||
|
||||
/// Test Claude Code execution with default sandbox profile
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_with_default_sandbox() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create default sandbox profile
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("claude_default", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Set as default and active
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 1 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
)
|
||||
.expect("Failed to set default profile");
|
||||
|
||||
// Execute real Claude command with default sandbox profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are Claude. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Default Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with default sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test Claude Code with sandboxing disabled
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_sandbox_disabled() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create profile but mark as inactive
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("claude_inactive", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Set as default but inactive
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 0 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
)
|
||||
.expect("Failed to set inactive profile");
|
||||
|
||||
// Execute real Claude command without active sandbox
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are Claude. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
None, // No sandbox since profile is inactive
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Inactive Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without active sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test Claude Code session operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_session_operations() {
|
||||
// This test doesn't require actual Claude execution
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create mock session structure
|
||||
let claude_dir = test_fs.root.path().join(".claude");
|
||||
let projects_dir = claude_dir.join("projects");
|
||||
let project_id = test_fs.project_path.to_string_lossy().replace('/', "-");
|
||||
let session_dir = projects_dir.join(&project_id);
|
||||
|
||||
std::fs::create_dir_all(&session_dir).expect("Failed to create session dir");
|
||||
|
||||
// Create mock session file
|
||||
let session_id = "test-session-123";
|
||||
let session_file = session_dir.join(format!("{}.jsonl", session_id));
|
||||
|
||||
let session_data = serde_json::json!({
|
||||
"type": "session_start",
|
||||
"cwd": test_fs.project_path.to_string_lossy(),
|
||||
"timestamp": "2024-01-01T00:00:00Z"
|
||||
});
|
||||
|
||||
std::fs::write(&session_file, format!("{}\n", session_data))
|
||||
.expect("Failed to write session file");
|
||||
|
||||
// Verify session file exists
|
||||
assert!(session_file.exists(), "Session file should exist");
|
||||
}
|
||||
|
||||
/// Test Claude settings with sandbox configuration
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_settings_sandbox_config() {
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create mock settings
|
||||
let claude_dir = test_fs.root.path().join(".claude");
|
||||
std::fs::create_dir_all(&claude_dir).expect("Failed to create claude dir");
|
||||
|
||||
let settings_file = claude_dir.join("settings.json");
|
||||
let settings = serde_json::json!({
|
||||
"sandboxEnabled": true,
|
||||
"defaultSandboxProfile": "standard",
|
||||
"theme": "dark",
|
||||
"model": "sonnet"
|
||||
});
|
||||
|
||||
std::fs::write(
|
||||
&settings_file,
|
||||
serde_json::to_string_pretty(&settings).unwrap(),
|
||||
)
|
||||
.expect("Failed to write settings");
|
||||
|
||||
// Read and verify settings
|
||||
let content = std::fs::read_to_string(&settings_file).expect("Failed to read settings");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&content).expect("Failed to parse settings");
|
||||
|
||||
assert_eq!(parsed["sandboxEnabled"], true, "Sandbox should be enabled");
|
||||
assert_eq!(
|
||||
parsed["defaultSandboxProfile"], "standard",
|
||||
"Default profile should be standard"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test profile-based file access restrictions
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_profile_file_access_simulation() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let _test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create a custom profile with specific file access
|
||||
let custom_rules = vec![
|
||||
TestRule::file_read("{{PROJECT_PATH}}", true),
|
||||
TestRule::file_read("/usr/local/bin", true),
|
||||
TestRule::file_read("/etc/hosts", false), // Literal file
|
||||
];
|
||||
|
||||
let profile_id = test_db
|
||||
.create_test_profile("file_access_test", custom_rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Load the profile rules
|
||||
let loaded_rules: Vec<(String, String, String)> = test_db.conn
|
||||
.prepare("SELECT operation_type, pattern_type, pattern_value FROM sandbox_rules WHERE profile_id = ?1")
|
||||
.expect("Failed to prepare query")
|
||||
.query_map(rusqlite::params![profile_id], |row| {
|
||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
||||
})
|
||||
.expect("Failed to query rules")
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("Failed to collect rules");
|
||||
|
||||
// Verify rules were created correctly
|
||||
assert_eq!(loaded_rules.len(), 3, "Should have 3 rules");
|
||||
assert!(
|
||||
loaded_rules.iter().any(|(op, _, _)| op == "file_read_all"),
|
||||
"Should have file_read_all operation"
|
||||
);
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
//! End-to-end tests for sandbox integration with agents and Claude
|
||||
#[cfg(test)]
|
||||
mod agent_sandbox;
|
||||
#[cfg(test)]
|
||||
mod claude_sandbox;
|
@@ -1,301 +0,0 @@
|
||||
//! Integration tests for file operations in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use claudia_lib::sandbox::profile::ProfileBuilder;
|
||||
use gaol::profile::{Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test allowed file read operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allowed_file_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing project path access
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads from allowed path
|
||||
let test_code = test_code::file_read(&test_fs.project_path.join("main.rs").to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_file_read", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Allowed file read should succeed");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test forbidden file read operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_forbidden_file_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing only project path (not forbidden path)
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads from forbidden path
|
||||
let forbidden_file = test_fs.forbidden_path.join("secret.txt");
|
||||
let test_code = test_code::file_read(&forbidden_file.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_forbidden_read", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// On some platforms (like macOS), gaol might not block all file reads
|
||||
// so we check if the operation failed OR if it's a platform limitation
|
||||
if status.success() {
|
||||
eprintln!(
|
||||
"WARNING: File read was not blocked - this might be a platform limitation"
|
||||
);
|
||||
// Check if we're on a platform where this is expected
|
||||
let platform_config = PlatformConfig::current();
|
||||
if !platform_config.supports_file_read {
|
||||
panic!("File read should have been blocked on this platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file write operations (should always be forbidden)
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_file_write_always_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with file read permissions (write should still be blocked)
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to write a file
|
||||
let write_path = test_fs.project_path.join("test_write.txt");
|
||||
let test_code = test_code::file_write(&write_path.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_file_write", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// File writes might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: File write was not blocked - checking platform capabilities");
|
||||
// On macOS, file writes might not be fully blocked by gaol
|
||||
if std::env::consts::OS != "macos" {
|
||||
panic!("File write should have been blocked on this platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file metadata operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_file_metadata_operations() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_metadata_read && !platform.supports_file_read {
|
||||
eprintln!("Skipping test: metadata read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with metadata read permission
|
||||
let operations = if platform.supports_metadata_read {
|
||||
vec![Operation::FileReadMetadata(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))]
|
||||
} else {
|
||||
// On Linux, metadata is allowed if file read is allowed
|
||||
vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))]
|
||||
};
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads file metadata
|
||||
let test_file = test_fs.project_path.join("main.rs");
|
||||
let test_code = test_code::file_metadata(&test_file.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_metadata", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
if platform.supports_metadata_read || platform.supports_file_read {
|
||||
assert!(
|
||||
status.success(),
|
||||
"Metadata read should succeed when allowed"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test template variable expansion in file paths
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_template_variable_expansion() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test database and profile
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create a profile with template variables
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let rules = vec![TestRule::file_read("{{PROJECT_PATH}}", true)];
|
||||
|
||||
let profile_id = test_db
|
||||
.create_test_profile("template_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Load and build the profile
|
||||
let db_rules = claudia_lib::sandbox::profile::load_profile_rules(&test_db.conn, profile_id)
|
||||
.expect("Failed to load profile rules");
|
||||
|
||||
let builder = ProfileBuilder::new(test_fs.project_path.clone())
|
||||
.expect("Failed to create profile builder");
|
||||
|
||||
let profile = match builder.build_profile(db_rules) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to build profile with templates");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads from project path
|
||||
let test_code = test_code::file_read(&test_fs.project_path.join("main.rs").to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_template", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Template-based file access should work");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
//! Integration tests for sandbox functionality
|
||||
#[cfg(test)]
|
||||
mod file_operations;
|
||||
#[cfg(test)]
|
||||
mod network_operations;
|
||||
#[cfg(test)]
|
||||
mod process_isolation;
|
||||
#[cfg(test)]
|
||||
mod system_info;
|
||||
#[cfg(test)]
|
||||
mod violations;
|
@@ -1,312 +0,0 @@
|
||||
//! Integration tests for network operations in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{AddressPattern, Operation, Profile};
|
||||
use serial_test::serial;
|
||||
use std::net::TcpListener;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Get an available port for testing
|
||||
fn get_available_port() -> u16 {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to 0");
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.expect("Failed to get local addr")
|
||||
.port();
|
||||
drop(listener); // Release the port
|
||||
port
|
||||
}
|
||||
|
||||
/// Test allowed network operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allowed_network_all() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_network_all {
|
||||
eprintln!("Skipping test: network all not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing all network access
|
||||
let operations = vec![Operation::NetworkOutbound(AddressPattern::All)];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that connects to localhost
|
||||
let port = get_available_port();
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_network", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Start a listener on the port
|
||||
let listener =
|
||||
TcpListener::bind(format!("127.0.0.1:{}", port)).expect("Failed to bind listener");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
// Accept connection in a thread
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Network connection should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test forbidden network operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_forbidden_network() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile without network permissions
|
||||
let operations = vec![Operation::FileReadAll(gaol::profile::PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to connect
|
||||
let test_code = test_code::network_connect("google.com:80");
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_no_network", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Network restrictions might not work on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Network connection was not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Network should be blocked on Linux when not allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test TCP port-specific network rules (macOS only)
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_network_tcp_port_specific() {
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_network_tcp {
|
||||
eprintln!("Skipping test: TCP port filtering not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Get two ports - one allowed, one forbidden
|
||||
let allowed_port = get_available_port();
|
||||
let forbidden_port = get_available_port();
|
||||
|
||||
// Create profile allowing only specific port
|
||||
let operations = vec![Operation::NetworkOutbound(AddressPattern::Tcp(
|
||||
allowed_port,
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Test 1: Allowed port
|
||||
{
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", allowed_port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_allowed_port", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", allowed_port))
|
||||
.expect("Failed to bind listener");
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Connection to allowed port should succeed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Forbidden port
|
||||
{
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", forbidden_port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_forbidden_port", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
!status.success(),
|
||||
"Connection to forbidden port should fail"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test local socket connections (Unix domain sockets)
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
fn test_local_socket_connections() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let socket_path = test_fs.project_path.join("test.sock");
|
||||
|
||||
// Create appropriate profile based on platform
|
||||
let operations = if platform.supports_network_local {
|
||||
vec![Operation::NetworkOutbound(AddressPattern::LocalSocket(
|
||||
socket_path.clone(),
|
||||
))]
|
||||
} else if platform.supports_network_all {
|
||||
// Fallback to allowing all network
|
||||
vec![Operation::NetworkOutbound(AddressPattern::All)]
|
||||
} else {
|
||||
eprintln!("Skipping test: no network support on this platform");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that connects to local socket
|
||||
let test_code = format!(
|
||||
r#"
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
fn main() {{
|
||||
match UnixStream::connect("{}") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Connected to local socket");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
socket_path.to_string_lossy()
|
||||
);
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_local_socket", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Create Unix socket listener
|
||||
use std::os::unix::net::UnixListener;
|
||||
let listener = UnixListener::bind(&socket_path).expect("Failed to bind Unix socket");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Local socket connection should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up socket file
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
@@ -1,247 +0,0 @@
|
||||
//! Integration tests for process isolation in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{AddressPattern, Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test that process spawning is always forbidden
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_process_spawn_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with various permissions (process spawn should still be blocked)
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to spawn a process
|
||||
let test_code = test_code::spawn_process();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_spawn", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Process spawning might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Process spawning was not blocked");
|
||||
// macOS sandbox might have limitations
|
||||
if std::env::consts::OS != "linux" {
|
||||
eprintln!(
|
||||
"Process spawning might not be fully blocked on {}",
|
||||
std::env::consts::OS
|
||||
);
|
||||
} else {
|
||||
panic!("Process spawning should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that fork is blocked
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
fn test_fork_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to fork
|
||||
let test_code = test_code::fork_process();
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary_with_deps(
|
||||
"test_fork",
|
||||
test_code,
|
||||
binary_dir.path(),
|
||||
&[("libc", "0.2")],
|
||||
)
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Fork might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Fork was not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Fork should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that exec is blocked
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
fn test_exec_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to exec
|
||||
let test_code = test_code::exec_process();
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary_with_deps(
|
||||
"test_exec",
|
||||
test_code,
|
||||
binary_dir.path(),
|
||||
&[("libc", "0.2")],
|
||||
)
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Exec might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Exec was not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Exec should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test thread creation is allowed
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_thread_creation_allowed() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that creates threads
|
||||
let test_code = r#"
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let handle = thread::spawn(|| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
42
|
||||
});
|
||||
|
||||
match handle.join() {
|
||||
Ok(value) => {
|
||||
println!("SUCCESS: Thread returned {}", value);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("FAILURE: Thread panicked");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_thread", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Thread creation should be allowed");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
//! Integration tests for system information operations in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Operation, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test system info read operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_system_info_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_system_info {
|
||||
eprintln!("Skipping test: system info read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing system info read
|
||||
let operations = vec![Operation::SystemInfoRead];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads system info
|
||||
let test_code = test_code::system_info();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_sysinfo", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"System info read should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test forbidden system info access
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_forbidden_system_info() {
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile without system info permission
|
||||
let operations = vec![Operation::FileReadAll(gaol::profile::PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads system info
|
||||
let test_code = test_code::system_info();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_no_sysinfo", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// System info might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: System info read was not blocked - checking platform");
|
||||
// On FreeBSD, system info is always allowed
|
||||
if std::env::consts::OS == "freebsd" {
|
||||
eprintln!("System info is always allowed on FreeBSD");
|
||||
} else if std::env::consts::OS == "macos" {
|
||||
// macOS might allow some system info reads
|
||||
eprintln!("System info read allowed on macOS (platform limitation)");
|
||||
} else {
|
||||
panic!("System info read should have been blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test platform-specific system info behavior
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_platform_specific_system_info() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
|
||||
match std::env::consts::OS {
|
||||
"linux" => {
|
||||
// On Linux, system info is never allowed
|
||||
assert!(
|
||||
!platform.supports_system_info,
|
||||
"Linux should not support system info read"
|
||||
);
|
||||
}
|
||||
"macos" => {
|
||||
// On macOS, system info can be allowed
|
||||
assert!(
|
||||
platform.supports_system_info,
|
||||
"macOS should support system info read"
|
||||
);
|
||||
}
|
||||
"freebsd" => {
|
||||
// On FreeBSD, system info is always allowed (can't be restricted)
|
||||
assert!(
|
||||
platform.supports_system_info,
|
||||
"FreeBSD always allows system info read"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown platform behavior for system info");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,297 +0,0 @@
|
||||
//! Integration tests for sandbox violation detection and logging
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Mock violation collector for testing
|
||||
#[derive(Clone)]
|
||||
struct ViolationCollector {
|
||||
violations: Arc<Mutex<Vec<ViolationEvent>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
struct ViolationEvent {
|
||||
operation_type: String,
|
||||
pattern_value: Option<String>,
|
||||
process_name: String,
|
||||
}
|
||||
|
||||
impl ViolationCollector {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
violations: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&self, operation_type: &str, pattern_value: Option<&str>, process_name: &str) {
|
||||
let event = ViolationEvent {
|
||||
operation_type: operation_type.to_string(),
|
||||
pattern_value: pattern_value.map(|s| s.to_string()),
|
||||
process_name: process_name.to_string(),
|
||||
};
|
||||
|
||||
if let Ok(mut violations) = self.violations.lock() {
|
||||
violations.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_violations(&self) -> Vec<ViolationEvent> {
|
||||
self.violations.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that violations are detected for forbidden operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_violation_detection() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let collector = ViolationCollector::new();
|
||||
|
||||
// Create profile allowing only project path
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Test various forbidden operations
|
||||
let test_cases = vec![
|
||||
(
|
||||
"file_read",
|
||||
test_code::file_read(&test_fs.forbidden_path.join("secret.txt").to_string_lossy()),
|
||||
"file_read_forbidden",
|
||||
),
|
||||
(
|
||||
"file_write",
|
||||
test_code::file_write(&test_fs.project_path.join("new.txt").to_string_lossy()),
|
||||
"file_write_forbidden",
|
||||
),
|
||||
(
|
||||
"process_spawn",
|
||||
test_code::spawn_process().to_string(),
|
||||
"process_spawn_forbidden",
|
||||
),
|
||||
];
|
||||
|
||||
for (op_type, test_code, binary_name) in test_cases {
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary(binary_name, &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
if !status.success() {
|
||||
// Record violation
|
||||
collector.record(op_type, None, binary_name);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Sandbox setup failure, not a violation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify violations were detected
|
||||
let violations = collector.get_violations();
|
||||
// On some platforms (like macOS), sandbox might not block all operations
|
||||
if violations.is_empty() {
|
||||
eprintln!("WARNING: No violations detected - this might be a platform limitation");
|
||||
// On Linux, we expect at least some violations
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Should have detected some violations on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test violation patterns and details
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_violation_patterns() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with specific allowed paths
|
||||
let allowed_dir = test_fs.root.path().join("allowed_specific");
|
||||
std::fs::create_dir_all(&allowed_dir).expect("Failed to create allowed dir");
|
||||
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
Operation::FileReadAll(PathPattern::Literal(allowed_dir.join("file.txt"))),
|
||||
];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Test accessing different forbidden paths
|
||||
let forbidden_db_path = test_fs
|
||||
.forbidden_path
|
||||
.join("data.db")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let forbidden_paths = vec![
|
||||
("/etc/passwd", "system_file"),
|
||||
("/tmp/test.txt", "temp_file"),
|
||||
(forbidden_db_path.as_str(), "forbidden_db"),
|
||||
];
|
||||
|
||||
for (path, test_name) in forbidden_paths {
|
||||
let test_code = test_code::file_read(path);
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary(test_name, &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Some platforms might not block all file access
|
||||
if status.success() {
|
||||
eprintln!(
|
||||
"WARNING: Access to {} was allowed (possible platform limitation)",
|
||||
path
|
||||
);
|
||||
if std::env::consts::OS == "linux" && path.starts_with("/etc") {
|
||||
panic!("Access to {} should be denied on Linux", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Sandbox setup failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test multiple violations in sequence
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_multiple_violations_sequence() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that attempts multiple forbidden operations
|
||||
let test_code = r#"
|
||||
use std::fs;
|
||||
use std::net::TcpStream;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {{
|
||||
let mut failures = 0;
|
||||
|
||||
// Try file write
|
||||
if fs::write("/tmp/test.txt", "data").is_err() {{
|
||||
eprintln!("File write failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
// Try network connection
|
||||
if TcpStream::connect("google.com:80").is_err() {{
|
||||
eprintln!("Network connection failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
// Try process spawn
|
||||
if Command::new("ls").output().is_err() {{
|
||||
eprintln!("Process spawn failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
// Try forbidden file read
|
||||
if fs::read_to_string("/etc/passwd").is_err() {{
|
||||
eprintln!("Forbidden file read failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
if failures > 0 {{
|
||||
eprintln!("FAILURE: {{failures}} operations were blocked");
|
||||
std::process::exit(1);
|
||||
}} else {{
|
||||
println!("SUCCESS: No operations were blocked (unexpected)");
|
||||
}}
|
||||
}}
|
||||
"#;
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_multi_violations", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Multiple operations might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Forbidden operations were not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Operations should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
//! Comprehensive test suite for sandbox functionality
|
||||
//!
|
||||
//! This test suite validates the sandboxing capabilities across different platforms,
|
||||
//! ensuring that security policies are correctly enforced.
|
||||
|
||||
#[cfg(unix)]
|
||||
#[macro_use]
|
||||
pub mod common;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod unit;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod integration;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod e2e;
|
@@ -1,146 +0,0 @@
|
||||
//! Unit tests for SandboxExecutor
|
||||
use claudia_lib::sandbox::executor::{should_activate_sandbox, SandboxExecutor};
|
||||
use gaol::profile::{AddressPattern, Operation, PathPattern, Profile};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Create a simple test profile
|
||||
fn create_test_profile(project_path: PathBuf) -> Profile {
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(project_path)),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
Profile::new(operations).expect("Failed to create test profile")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_creation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
|
||||
let _executor = SandboxExecutor::new(profile, project_path);
|
||||
// Executor should be created successfully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_activate_sandbox_env_var() {
|
||||
// Test when env var is not set
|
||||
env::remove_var("GAOL_SANDBOX_ACTIVE");
|
||||
assert!(
|
||||
!should_activate_sandbox(),
|
||||
"Should not activate when env var is not set"
|
||||
);
|
||||
|
||||
// Test when env var is set to "1"
|
||||
env::set_var("GAOL_SANDBOX_ACTIVE", "1");
|
||||
assert!(
|
||||
should_activate_sandbox(),
|
||||
"Should activate when env var is '1'"
|
||||
);
|
||||
|
||||
// Test when env var is set to other value
|
||||
env::set_var("GAOL_SANDBOX_ACTIVE", "0");
|
||||
assert!(
|
||||
!should_activate_sandbox(),
|
||||
"Should not activate when env var is not '1'"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
env::remove_var("GAOL_SANDBOX_ACTIVE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_sandboxed_command() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["hello"], &project_path);
|
||||
|
||||
// The command should have sandbox environment variables set
|
||||
// Note: We can't easily test Command internals, but we can verify it doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_with_empty_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = Profile::new(vec![]).expect("Failed to create empty profile");
|
||||
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["test"], &project_path);
|
||||
|
||||
// Should handle empty profile gracefully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_with_complex_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(project_path.clone())),
|
||||
Operation::FileReadAll(PathPattern::Subpath(PathBuf::from("/usr/lib"))),
|
||||
Operation::FileReadAll(PathPattern::Literal(PathBuf::from("/etc/hosts"))),
|
||||
Operation::FileReadMetadata(PathPattern::Subpath(PathBuf::from("/"))),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
Operation::NetworkOutbound(AddressPattern::Tcp(443)),
|
||||
Operation::SystemInfoRead,
|
||||
];
|
||||
|
||||
// Only create profile with supported operations
|
||||
let filtered_ops: Vec<_> = operations
|
||||
.into_iter()
|
||||
.filter(|op| {
|
||||
use gaol::profile::{OperationSupport, OperationSupportLevel};
|
||||
matches!(op.support(), OperationSupportLevel::CanBeAllowed)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !filtered_ops.is_empty() {
|
||||
let profile = Profile::new(filtered_ops).expect("Failed to create complex profile");
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["test"], &project_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_environment_setup() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
// Test with various arguments
|
||||
let _cmd1 = executor.prepare_sandboxed_command("ls", &[], &project_path);
|
||||
let _cmd2 = executor.prepare_sandboxed_command("cat", &["file.txt"], &project_path);
|
||||
let _cmd3 = executor.prepare_sandboxed_command("grep", &["-r", "pattern", "."], &project_path);
|
||||
|
||||
// Commands should be prepared without panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_spawn_sandboxed_process() {
|
||||
use crate::sandbox::common::is_sandboxing_supported;
|
||||
|
||||
if !is_sandboxing_supported() {
|
||||
return;
|
||||
}
|
||||
|
||||
let project_path = env::current_dir().unwrap_or_else(|_| PathBuf::from("/tmp"));
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
// Try to spawn a simple command
|
||||
let result = executor.execute_sandboxed_spawn("echo", &["sandbox test"], &project_path);
|
||||
|
||||
// On supported platforms, this should either succeed or fail gracefully
|
||||
match result {
|
||||
Ok(mut child) => {
|
||||
// If spawned successfully, wait for it to complete
|
||||
let _ = child.wait();
|
||||
}
|
||||
Err(e) => {
|
||||
// Sandboxing might fail due to permissions or platform limitations
|
||||
println!("Sandbox spawn failed (expected in some environments): {e}");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
//! Unit tests for sandbox components
|
||||
#[cfg(test)]
|
||||
mod executor;
|
||||
#[cfg(test)]
|
||||
mod platform;
|
||||
#[cfg(test)]
|
||||
mod profile_builder;
|
@@ -1,181 +0,0 @@
|
||||
//! Unit tests for platform capabilities
|
||||
use claudia_lib::sandbox::platform::{get_platform_capabilities, is_sandboxing_available};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_sandboxing_availability() {
|
||||
let is_available = is_sandboxing_available();
|
||||
let expected = matches!(env::consts::OS, "linux" | "macos" | "freebsd");
|
||||
|
||||
assert_eq!(
|
||||
is_available, expected,
|
||||
"Sandboxing availability should match platform support"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_capabilities_structure() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
// Verify basic structure
|
||||
assert_eq!(caps.os, env::consts::OS, "OS should match current platform");
|
||||
assert!(
|
||||
!caps.operations.is_empty() || !caps.sandboxing_supported,
|
||||
"Should have operations if sandboxing is supported"
|
||||
);
|
||||
assert!(
|
||||
!caps.notes.is_empty(),
|
||||
"Should have platform-specific notes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_linux_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert_eq!(caps.os, "linux");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
// Verify Linux-specific capabilities
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "can_be_allowed");
|
||||
|
||||
let metadata_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_metadata")
|
||||
.expect("file_read_metadata should be present");
|
||||
assert_eq!(metadata_read.support_level, "cannot_be_precisely");
|
||||
|
||||
let network_all = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_all")
|
||||
.expect("network_outbound_all should be present");
|
||||
assert_eq!(network_all.support_level, "can_be_allowed");
|
||||
|
||||
let network_tcp = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_tcp")
|
||||
.expect("network_outbound_tcp should be present");
|
||||
assert_eq!(network_tcp.support_level, "cannot_be_precisely");
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "never");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_macos_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert_eq!(caps.os, "macos");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
// Verify macOS-specific capabilities
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "can_be_allowed");
|
||||
|
||||
let metadata_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_metadata")
|
||||
.expect("file_read_metadata should be present");
|
||||
assert_eq!(metadata_read.support_level, "can_be_allowed");
|
||||
|
||||
let network_tcp = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_tcp")
|
||||
.expect("network_outbound_tcp should be present");
|
||||
assert_eq!(network_tcp.support_level, "can_be_allowed");
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "can_be_allowed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "freebsd")]
|
||||
fn test_freebsd_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert_eq!(caps.os, "freebsd");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
// Verify FreeBSD-specific capabilities
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "never");
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "always");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
|
||||
fn test_unsupported_platform_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert!(!caps.sandboxing_supported);
|
||||
assert_eq!(caps.operations.len(), 0);
|
||||
assert!(caps.notes.iter().any(|note| note.contains("not supported")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_operations_have_descriptions() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
for op in &caps.operations {
|
||||
assert!(
|
||||
!op.description.is_empty(),
|
||||
"Operation {} should have a description",
|
||||
op.operation
|
||||
);
|
||||
assert!(
|
||||
!op.support_level.is_empty(),
|
||||
"Operation {} should have a support level",
|
||||
op.operation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_support_level_values() {
|
||||
let caps = get_platform_capabilities();
|
||||
let valid_levels = ["never", "can_be_allowed", "cannot_be_precisely", "always"];
|
||||
|
||||
for op in &caps.operations {
|
||||
assert!(
|
||||
valid_levels.contains(&op.support_level.as_str()),
|
||||
"Operation {} has invalid support level: {}",
|
||||
op.operation,
|
||||
op.support_level
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,337 +0,0 @@
|
||||
//! Unit tests for ProfileBuilder
|
||||
use claudia_lib::sandbox::profile::{ProfileBuilder, SandboxRule};
|
||||
use std::path::PathBuf;
|
||||
use test_case::test_case;
|
||||
|
||||
/// Helper to create a sandbox rule
|
||||
fn make_rule(
|
||||
operation_type: &str,
|
||||
pattern_type: &str,
|
||||
pattern_value: &str,
|
||||
platforms: Option<&[&str]>,
|
||||
) -> SandboxRule {
|
||||
SandboxRule {
|
||||
id: None,
|
||||
profile_id: 0,
|
||||
operation_type: operation_type.to_string(),
|
||||
pattern_type: pattern_type.to_string(),
|
||||
pattern_value: pattern_value.to_string(),
|
||||
enabled: true,
|
||||
platform_support: platforms.map(|p| {
|
||||
serde_json::to_string(&p.iter().map(|s| s.to_string()).collect::<Vec<_>>()).unwrap()
|
||||
}),
|
||||
created_at: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profile_builder_creation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone());
|
||||
|
||||
assert!(
|
||||
builder.is_ok(),
|
||||
"ProfileBuilder should be created successfully"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_rules_creates_empty_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let profile = builder.build_profile(vec![]);
|
||||
assert!(
|
||||
profile.is_ok(),
|
||||
"Empty rules should create valid empty profile"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_read_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"literal",
|
||||
"/usr/lib/test.so",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
// Profile creation might fail on unsupported platforms, but parsing should work
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"File read rules should be parsed on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("network_outbound", "tcp", "8080", Some(&["macos"])),
|
||||
make_rule(
|
||||
"network_outbound",
|
||||
"local_socket",
|
||||
"/tmp/socket",
|
||||
Some(&["macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"Network rules should be parsed on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_info_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule("system_info_read", "all", "", Some(&["macos"]))];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
if std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"System info rule should be parsed on macOS"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_variable_replacement() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}/src",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{HOME}}/.config",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// We can't easily verify the exact paths without inspecting the Profile internals,
|
||||
// but this test ensures template replacement doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_rules_are_ignored() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let mut rule = make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
);
|
||||
rule.enabled = false;
|
||||
|
||||
let profile = builder.build_profile(vec![rule]);
|
||||
assert!(profile.is_ok(), "Disabled rules should be ignored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_filtering() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let current_os = std::env::consts::OS;
|
||||
let other_os = if current_os == "linux" {
|
||||
"macos"
|
||||
} else {
|
||||
"linux"
|
||||
};
|
||||
|
||||
let rules = vec![
|
||||
// Rule for current platform
|
||||
make_rule("file_read_all", "subpath", "/test1", Some(&[current_os])),
|
||||
// Rule for other platform
|
||||
make_rule("file_read_all", "subpath", "/test2", Some(&[other_os])),
|
||||
// Rule for both platforms
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/test3",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
// Rule with no platform specification (should be included)
|
||||
make_rule("file_read_all", "subpath", "/test4", None),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Rules for other platforms should be filtered out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_operation_type() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"invalid_operation",
|
||||
"subpath",
|
||||
"/test",
|
||||
Some(&["linux", "macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
assert!(_profile.is_ok(), "Invalid operations should be skipped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_pattern_type() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"file_read_all",
|
||||
"invalid_pattern",
|
||||
"/test",
|
||||
Some(&["linux", "macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Should either skip the rule or fail gracefully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_tcp_port() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"network_outbound",
|
||||
"tcp",
|
||||
"not_a_number",
|
||||
Some(&["macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Should handle invalid port gracefully
|
||||
}
|
||||
|
||||
#[test_case("file_read_all", "subpath", "/test" ; "file read operation")]
|
||||
#[test_case("file_read_metadata", "literal", "/test/file" ; "metadata read operation")]
|
||||
#[test_case("network_outbound", "all", "" ; "network all operation")]
|
||||
#[test_case("system_info_read", "all", "" ; "system info operation")]
|
||||
fn test_operation_support_level(operation_type: &str, pattern_type: &str, pattern_value: &str) {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rule = make_rule(operation_type, pattern_type, pattern_value, None);
|
||||
let rules = vec![rule];
|
||||
|
||||
match builder.build_profile(rules) {
|
||||
Ok(_) => {
|
||||
// Profile created successfully - operation is supported
|
||||
println!("Operation {operation_type} is supported on this platform");
|
||||
}
|
||||
Err(e) => {
|
||||
// Profile creation failed - likely due to unsupported operation
|
||||
println!("Operation {operation_type} is not supported: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_profile_with_multiple_rules() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
// File operations
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"literal",
|
||||
"/etc/hosts",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule("file_read_metadata", "subpath", "/", Some(&["macos"])),
|
||||
// Network operations
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("network_outbound", "tcp", "443", Some(&["macos"])),
|
||||
make_rule("network_outbound", "tcp", "80", Some(&["macos"])),
|
||||
// System info
|
||||
make_rule("system_info_read", "all", "", Some(&["macos"])),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"Complex profile should be created on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rule_order_preservation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
// Create rules with specific order
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/first",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/second",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Order should be preserved in the resulting profile
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
//! Main entry point for sandbox tests
|
||||
//!
|
||||
//! This file integrates all the sandbox test modules and provides
|
||||
//! a central location for running the comprehensive test suite.
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[cfg(unix)]
|
||||
mod sandbox;
|
||||
|
||||
// Re-export test modules to make them discoverable
|
||||
pub use sandbox::*;
|
Reference in New Issue
Block a user