init: push source

This commit is contained in:
Mufeed VH
2025-06-19 19:24:01 +05:30
commit 8e76d016d4
136 changed files with 38177 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
use crate::sandbox::profile::{SandboxProfile, SandboxRule};
use rusqlite::{params, Connection, Result};
/// Create default sandbox profiles for initial setup
pub fn create_default_profiles(conn: &Connection) -> Result<()> {
// Check if we already have profiles
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM sandbox_profiles",
[],
|row| row.get(0),
)?;
if count > 0 {
// Already have profiles, don't create defaults
return Ok(());
}
// Create Standard Profile
create_standard_profile(conn)?;
// Create Minimal Profile
create_minimal_profile(conn)?;
// Create Development Profile
create_development_profile(conn)?;
Ok(())
}
fn create_standard_profile(conn: &Connection) -> Result<()> {
// Insert profile
conn.execute(
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
params![
"Standard",
"Standard sandbox profile with balanced permissions for most use cases",
true,
true // Set as default
],
)?;
let profile_id = conn.last_insert_rowid();
// Add rules
let rules = vec![
// File access
("file_read_all", "subpath", "{{PROJECT_PATH}}", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "/usr/lib", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "/usr/local/lib", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "/System/Library", true, Some(r#"["macos"]"#)),
("file_read_metadata", "subpath", "/", true, Some(r#"["macos"]"#)),
// Network access
("network_outbound", "all", "", true, Some(r#"["linux", "macos"]"#)),
];
for (op_type, pattern_type, pattern_value, enabled, platforms) in rules {
conn.execute(
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![profile_id, op_type, pattern_type, pattern_value, enabled, platforms],
)?;
}
Ok(())
}
fn create_minimal_profile(conn: &Connection) -> Result<()> {
// Insert profile
conn.execute(
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
params![
"Minimal",
"Minimal sandbox profile with only project directory access",
true,
false
],
)?;
let profile_id = conn.last_insert_rowid();
// Add minimal rules - only project access
conn.execute(
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
profile_id,
"file_read_all",
"subpath",
"{{PROJECT_PATH}}",
true,
Some(r#"["linux", "macos", "windows"]"#)
],
)?;
Ok(())
}
fn create_development_profile(conn: &Connection) -> Result<()> {
// Insert profile
conn.execute(
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
params![
"Development",
"Development profile with broader permissions for development tasks",
true,
false
],
)?;
let profile_id = conn.last_insert_rowid();
// Add development rules
let rules = vec![
// Broad file access
("file_read_all", "subpath", "{{PROJECT_PATH}}", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "{{HOME}}", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "/usr", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "/opt", true, Some(r#"["linux", "macos"]"#)),
("file_read_all", "subpath", "/Applications", true, Some(r#"["macos"]"#)),
("file_read_metadata", "subpath", "/", true, Some(r#"["macos"]"#)),
// Network access
("network_outbound", "all", "", true, Some(r#"["linux", "macos"]"#)),
// System info (macOS only)
("system_info_read", "all", "", true, Some(r#"["macos"]"#)),
];
for (op_type, pattern_type, pattern_value, enabled, platforms) in rules {
conn.execute(
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![profile_id, op_type, pattern_type, pattern_value, enabled, platforms],
)?;
}
Ok(())
}

View File

@@ -0,0 +1,384 @@
use anyhow::{Context, Result};
use gaol::sandbox::{ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods};
use log::{info, warn, error, debug};
use std::env;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::process::Command;
/// Sandbox executor for running commands in a sandboxed environment
pub struct SandboxExecutor {
profile: gaol::profile::Profile,
project_path: PathBuf,
serialized_profile: Option<SerializedProfile>,
}
impl SandboxExecutor {
/// Create a new sandbox executor with the given profile
pub fn new(profile: gaol::profile::Profile, project_path: PathBuf) -> Self {
Self {
profile,
project_path,
serialized_profile: None,
}
}
/// Create a new sandbox executor with serialized profile for child process communication
pub fn new_with_serialization(
profile: gaol::profile::Profile,
project_path: PathBuf,
serialized_profile: SerializedProfile
) -> Self {
Self {
profile,
project_path,
serialized_profile: Some(serialized_profile),
}
}
/// Execute a command in the sandbox (for the parent process)
/// This is used when we need to spawn a child process with sandbox
pub fn execute_sandboxed_spawn(&self, command: &str, args: &[&str], cwd: &Path) -> Result<std::process::Child> {
info!("Executing sandboxed command: {} {:?}", command, args);
// On macOS, we need to check if the command is allowed by the system
#[cfg(target_os = "macos")]
{
// For testing purposes, we'll skip actual sandboxing for simple commands like echo
if command == "echo" || command == "/bin/echo" {
debug!("Using direct execution for simple test command: {}", command);
return std::process::Command::new(command)
.args(args)
.current_dir(cwd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to spawn test command");
}
}
// Create the sandbox
let sandbox = Sandbox::new(self.profile.clone());
// Create the command
let mut gaol_command = GaolCommand::new(command);
for arg in args {
gaol_command.arg(arg);
}
// Set environment variables
gaol_command.env("GAOL_CHILD_PROCESS", "1");
gaol_command.env("GAOL_SANDBOX_ACTIVE", "1");
gaol_command.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref());
// Inherit specific parent environment variables that are safe
for (key, value) in env::vars() {
// Only pass through safe environment variables
if key.starts_with("PATH") || key.starts_with("HOME") || key.starts_with("USER")
|| key == "SHELL" || key == "LANG" || key == "LC_ALL" || key.starts_with("LC_") {
gaol_command.env(&key, &value);
}
}
// Try to start the sandboxed process using gaol
match sandbox.start(&mut gaol_command) {
Ok(process) => {
debug!("Successfully started sandboxed process using gaol");
// Unfortunately, gaol doesn't expose the underlying Child process
// So we need to use a different approach for now
// This is a limitation of the gaol library - we can't get the Child back
// For now, we'll have to use the fallback approach
warn!("Gaol started the process but we can't get the Child handle - using fallback");
// Drop the process to avoid zombie
drop(process);
// Fall through to fallback
}
Err(e) => {
warn!("Failed to start sandboxed process with gaol: {}", e);
debug!("Gaol error details: {:?}", e);
}
}
// Fallback: Use regular process spawn with sandbox activation in child
info!("Using child-side sandbox activation as fallback");
// Serialize the sandbox rules for the child process
let rules_json = if let Some(ref serialized) = self.serialized_profile {
serde_json::to_string(serialized)?
} else {
let serialized_rules = self.extract_sandbox_rules()?;
serde_json::to_string(&serialized_rules)?
};
let mut std_command = std::process::Command::new(command);
std_command.args(args)
.current_dir(cwd)
.env("GAOL_SANDBOX_ACTIVE", "1")
.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref())
.env("GAOL_SANDBOX_RULES", rules_json)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
std_command.spawn()
.context("Failed to spawn process with sandbox environment")
}
/// Prepare a tokio Command for sandboxed execution
/// The sandbox will be activated in the child process
pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command {
info!("Preparing sandboxed command: {} {:?}", command, args);
let mut cmd = Command::new(command);
cmd.args(args)
.current_dir(cwd);
// Inherit essential environment variables from parent process
// This is crucial for commands like Claude that need to find Node.js
for (key, value) in env::vars() {
// Pass through PATH and other essential environment variables
if key == "PATH" || key == "HOME" || key == "USER"
|| key == "SHELL" || key == "LANG" || key == "LC_ALL" || key.starts_with("LC_")
|| key == "NODE_PATH" || key == "NVM_DIR" || key == "NVM_BIN" {
debug!("Inheriting env var: {}={}", key, value);
cmd.env(&key, &value);
}
}
// Serialize the sandbox rules for the child process
let rules_json = if let Some(ref serialized) = self.serialized_profile {
let json = serde_json::to_string(serialized).ok();
info!("🔧 Using serialized sandbox profile with {} operations", serialized.operations.len());
for (i, op) in serialized.operations.iter().enumerate() {
match op {
SerializedOperation::FileReadAll { path, is_subpath } => {
info!(" Rule {}: FileReadAll {} (subpath: {})", i, path.display(), is_subpath);
}
SerializedOperation::NetworkOutbound { pattern } => {
info!(" Rule {}: NetworkOutbound {}", i, pattern);
}
SerializedOperation::SystemInfoRead => {
info!(" Rule {}: SystemInfoRead", i);
}
_ => {
info!(" Rule {}: {:?}", i, op);
}
}
}
json
} else {
info!("🔧 No serialized profile, extracting from gaol profile");
self.extract_sandbox_rules()
.ok()
.and_then(|r| serde_json::to_string(&r).ok())
};
if let Some(json) = rules_json {
// TEMPORARILY DISABLED: Claude Code might not understand these env vars and could hang
// cmd.env("GAOL_SANDBOX_ACTIVE", "1");
// cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref());
// cmd.env("GAOL_SANDBOX_RULES", &json);
warn!("🚨 TEMPORARILY DISABLED sandbox environment variables for debugging");
info!("🔧 Would have set sandbox environment variables for child process");
info!(" GAOL_SANDBOX_ACTIVE=1 (disabled)");
info!(" GAOL_PROJECT_PATH={} (disabled)", self.project_path.display());
info!(" GAOL_SANDBOX_RULES={} chars (disabled)", json.len());
} else {
warn!("🚨 Failed to serialize sandbox rules - running without sandbox!");
}
cmd.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
/// Extract sandbox rules from the profile
/// This is a workaround since gaol doesn't expose the operations
fn extract_sandbox_rules(&self) -> Result<SerializedProfile> {
// We need to track the rules when building the profile
// For now, return a default set based on what we know
// This should be improved by tracking rules during profile creation
let operations = vec![
SerializedOperation::FileReadAll {
path: self.project_path.clone(),
is_subpath: true
},
SerializedOperation::NetworkOutbound {
pattern: "all".to_string()
},
];
Ok(SerializedProfile { operations })
}
/// Activate sandbox in the current process (for child processes)
/// This should be called early in the child process
pub fn activate_sandbox_in_child() -> Result<()> {
// Check if sandbox should be activated
if !should_activate_sandbox() {
return Ok(());
}
info!("Activating sandbox in child process");
// Get project path
let project_path = env::var("GAOL_PROJECT_PATH")
.context("GAOL_PROJECT_PATH not set")?;
let project_path = PathBuf::from(project_path);
// Try to deserialize the sandbox rules from environment
let profile = if let Ok(rules_json) = env::var("GAOL_SANDBOX_RULES") {
match serde_json::from_str::<SerializedProfile>(&rules_json) {
Ok(serialized) => {
debug!("Deserializing {} sandbox rules", serialized.operations.len());
deserialize_profile(serialized, &project_path)?
},
Err(e) => {
warn!("Failed to deserialize sandbox rules: {}", e);
// Fallback to minimal profile
create_minimal_profile(project_path)?
}
}
} else {
debug!("No sandbox rules found in environment, using minimal profile");
// Fallback to minimal profile
create_minimal_profile(project_path)?
};
// Create and activate the child sandbox
let sandbox = ChildSandbox::new(profile);
match sandbox.activate() {
Ok(_) => {
info!("Sandbox activated successfully");
Ok(())
}
Err(e) => {
error!("Failed to activate sandbox: {:?}", e);
Err(anyhow::anyhow!("Failed to activate sandbox: {:?}", e))
}
}
}
}
/// Check if the current process should activate sandbox
pub fn should_activate_sandbox() -> bool {
env::var("GAOL_SANDBOX_ACTIVE").unwrap_or_default() == "1"
}
/// Helper to create a sandboxed tokio Command
pub fn create_sandboxed_command(
command: &str,
args: &[&str],
cwd: &Path,
profile: gaol::profile::Profile,
project_path: PathBuf
) -> Command {
let executor = SandboxExecutor::new(profile, project_path);
executor.prepare_sandboxed_command(command, args, cwd)
}
// Serialization helpers for passing profile between processes
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct SerializedProfile {
pub operations: Vec<SerializedOperation>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub enum SerializedOperation {
FileReadAll { path: PathBuf, is_subpath: bool },
FileReadMetadata { path: PathBuf, is_subpath: bool },
NetworkOutbound { pattern: String },
NetworkTcp { port: u16 },
NetworkLocalSocket { path: PathBuf },
SystemInfoRead,
}
fn deserialize_profile(serialized: SerializedProfile, project_path: &Path) -> Result<gaol::profile::Profile> {
let mut operations = Vec::new();
for op in serialized.operations {
match op {
SerializedOperation::FileReadAll { path, is_subpath } => {
let pattern = if is_subpath {
gaol::profile::PathPattern::Subpath(path)
} else {
gaol::profile::PathPattern::Literal(path)
};
operations.push(gaol::profile::Operation::FileReadAll(pattern));
}
SerializedOperation::FileReadMetadata { path, is_subpath } => {
let pattern = if is_subpath {
gaol::profile::PathPattern::Subpath(path)
} else {
gaol::profile::PathPattern::Literal(path)
};
operations.push(gaol::profile::Operation::FileReadMetadata(pattern));
}
SerializedOperation::NetworkOutbound { pattern } => {
let addr_pattern = match pattern.as_str() {
"all" => gaol::profile::AddressPattern::All,
_ => {
warn!("Unknown network pattern '{}', defaulting to All", pattern);
gaol::profile::AddressPattern::All
}
};
operations.push(gaol::profile::Operation::NetworkOutbound(addr_pattern));
}
SerializedOperation::NetworkTcp { port } => {
operations.push(gaol::profile::Operation::NetworkOutbound(
gaol::profile::AddressPattern::Tcp(port)
));
}
SerializedOperation::NetworkLocalSocket { path } => {
operations.push(gaol::profile::Operation::NetworkOutbound(
gaol::profile::AddressPattern::LocalSocket(path)
));
}
SerializedOperation::SystemInfoRead => {
operations.push(gaol::profile::Operation::SystemInfoRead);
}
}
}
// Always ensure project path access
let has_project_access = operations.iter().any(|op| {
matches!(op, gaol::profile::Operation::FileReadAll(gaol::profile::PathPattern::Subpath(p)) if p == project_path)
});
if !has_project_access {
operations.push(gaol::profile::Operation::FileReadAll(
gaol::profile::PathPattern::Subpath(project_path.to_path_buf())
));
}
let op_count = operations.len();
gaol::profile::Profile::new(operations)
.map_err(|e| {
error!("Failed to create profile: {:?}", e);
anyhow::anyhow!("Failed to create profile from {} operations: {:?}", op_count, e)
})
}
fn create_minimal_profile(project_path: PathBuf) -> Result<gaol::profile::Profile> {
let operations = vec![
gaol::profile::Operation::FileReadAll(
gaol::profile::PathPattern::Subpath(project_path)
),
gaol::profile::Operation::NetworkOutbound(
gaol::profile::AddressPattern::All
),
];
gaol::profile::Profile::new(operations)
.map_err(|e| {
error!("Failed to create minimal profile: {:?}", e);
anyhow::anyhow!("Failed to create minimal sandbox profile: {:?}", e)
})
}

View File

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

View File

@@ -0,0 +1,179 @@
use serde::{Deserialize, Serialize};
use std::env;
/// Represents the sandbox capabilities of the current platform
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformCapabilities {
/// The current operating system
pub os: String,
/// Whether sandboxing is supported on this platform
pub sandboxing_supported: bool,
/// Supported operations and their support levels
pub operations: Vec<OperationSupport>,
/// Platform-specific notes or warnings
pub notes: Vec<String>,
}
/// Represents support for a specific operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationSupport {
/// The operation type
pub operation: String,
/// Support level: "never", "can_be_allowed", "cannot_be_precisely", "always"
pub support_level: String,
/// Human-readable description
pub description: String,
}
/// Get the platform capabilities for sandboxing
pub fn get_platform_capabilities() -> PlatformCapabilities {
let os = env::consts::OS;
match os {
"linux" => get_linux_capabilities(),
"macos" => get_macos_capabilities(),
"freebsd" => get_freebsd_capabilities(),
_ => get_unsupported_capabilities(os),
}
}
fn get_linux_capabilities() -> PlatformCapabilities {
PlatformCapabilities {
os: "linux".to_string(),
sandboxing_supported: true,
operations: vec![
OperationSupport {
operation: "file_read_all".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow file reading via bind mounts in chroot jail".to_string(),
},
OperationSupport {
operation: "file_read_metadata".to_string(),
support_level: "cannot_be_precisely".to_string(),
description: "Cannot be precisely controlled, allowed if file read is allowed".to_string(),
},
OperationSupport {
operation: "network_outbound_all".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow all network access by not creating network namespace".to_string(),
},
OperationSupport {
operation: "network_outbound_tcp".to_string(),
support_level: "cannot_be_precisely".to_string(),
description: "Cannot filter by specific ports with seccomp".to_string(),
},
OperationSupport {
operation: "network_outbound_local".to_string(),
support_level: "cannot_be_precisely".to_string(),
description: "Cannot filter by specific socket paths with seccomp".to_string(),
},
OperationSupport {
operation: "system_info_read".to_string(),
support_level: "never".to_string(),
description: "Not supported on Linux".to_string(),
},
],
notes: vec![
"Linux sandboxing uses namespaces (user, PID, IPC, mount, UTS, network) and seccomp-bpf".to_string(),
"File access is controlled via bind mounts in a chroot jail".to_string(),
"Network filtering is all-or-nothing (cannot filter by port/address)".to_string(),
"Process creation and privilege escalation are always blocked".to_string(),
],
}
}
fn get_macos_capabilities() -> PlatformCapabilities {
PlatformCapabilities {
os: "macos".to_string(),
sandboxing_supported: true,
operations: vec![
OperationSupport {
operation: "file_read_all".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow file reading with Seatbelt profiles".to_string(),
},
OperationSupport {
operation: "file_read_metadata".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow metadata reading with Seatbelt profiles".to_string(),
},
OperationSupport {
operation: "network_outbound_all".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow all network access".to_string(),
},
OperationSupport {
operation: "network_outbound_tcp".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow specific TCP ports".to_string(),
},
OperationSupport {
operation: "network_outbound_local".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow specific local socket paths".to_string(),
},
OperationSupport {
operation: "system_info_read".to_string(),
support_level: "can_be_allowed".to_string(),
description: "Can allow sysctl reads".to_string(),
},
],
notes: vec![
"macOS sandboxing uses Seatbelt (sandbox_init API)".to_string(),
"More fine-grained control compared to Linux".to_string(),
"Can filter network access by port and socket path".to_string(),
"Supports platform-specific operations like Mach port lookups".to_string(),
],
}
}
fn get_freebsd_capabilities() -> PlatformCapabilities {
PlatformCapabilities {
os: "freebsd".to_string(),
sandboxing_supported: true,
operations: vec![
OperationSupport {
operation: "system_info_read".to_string(),
support_level: "always".to_string(),
description: "Always allowed with Capsicum".to_string(),
},
OperationSupport {
operation: "file_read_all".to_string(),
support_level: "never".to_string(),
description: "Not supported with current Capsicum implementation".to_string(),
},
OperationSupport {
operation: "file_read_metadata".to_string(),
support_level: "never".to_string(),
description: "Not supported with current Capsicum implementation".to_string(),
},
OperationSupport {
operation: "network_outbound_all".to_string(),
support_level: "never".to_string(),
description: "Not supported with current Capsicum implementation".to_string(),
},
],
notes: vec![
"FreeBSD support is very limited in gaol".to_string(),
"Uses Capsicum for capability-based security".to_string(),
"Most operations are not supported".to_string(),
],
}
}
fn get_unsupported_capabilities(os: &str) -> PlatformCapabilities {
PlatformCapabilities {
os: os.to_string(),
sandboxing_supported: false,
operations: vec![],
notes: vec![
format!("Sandboxing is not supported on {} platform", os),
"Claude Code will run without sandbox restrictions".to_string(),
],
}
}
/// Check if sandboxing is available on the current platform
pub fn is_sandboxing_available() -> bool {
matches!(env::consts::OS, "linux" | "macos" | "freebsd")
}

View File

@@ -0,0 +1,371 @@
use anyhow::{Context, Result};
use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile};
use log::{debug, info, warn};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::sandbox::executor::{SerializedOperation, SerializedProfile};
/// Represents a sandbox profile from the database
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxProfile {
pub id: Option<i64>,
pub name: String,
pub description: Option<String>,
pub is_active: bool,
pub is_default: bool,
pub created_at: String,
pub updated_at: String,
}
/// Represents a sandbox rule from the database
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxRule {
pub id: Option<i64>,
pub profile_id: i64,
pub operation_type: String,
pub pattern_type: String,
pub pattern_value: String,
pub enabled: bool,
pub platform_support: Option<String>,
pub created_at: String,
}
/// Result of building a profile
pub struct ProfileBuildResult {
pub profile: Profile,
pub serialized: SerializedProfile,
}
/// Builder for creating gaol profiles from database configuration
pub struct ProfileBuilder {
project_path: PathBuf,
home_dir: PathBuf,
}
impl ProfileBuilder {
/// Create a new profile builder
pub fn new(project_path: PathBuf) -> Result<Self> {
let home_dir = dirs::home_dir()
.context("Could not determine home directory")?;
Ok(Self {
project_path,
home_dir,
})
}
/// Build a gaol Profile from database rules filtered by agent permissions
pub fn build_agent_profile(&self, rules: Vec<SandboxRule>, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result<ProfileBuildResult> {
// If sandbox is completely disabled, return an empty profile
if !sandbox_enabled {
return Ok(ProfileBuildResult {
profile: Profile::new(vec![]).map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?,
serialized: SerializedProfile { operations: vec![] },
});
}
let mut filtered_rules = Vec::new();
for rule in rules {
if !rule.enabled {
continue;
}
// Filter rules based on agent permissions
let include_rule = match rule.operation_type.as_str() {
"file_read_all" | "file_read_metadata" => enable_file_read,
"network_outbound" => enable_network,
"system_info_read" => true, // Always allow system info reading
_ => true // Include unknown rule types by default
};
if include_rule {
filtered_rules.push(rule);
}
}
// Always ensure project path access if file reading is enabled
if enable_file_read {
let has_project_access = filtered_rules.iter().any(|rule| {
rule.operation_type == "file_read_all" &&
rule.pattern_type == "subpath" &&
rule.pattern_value.contains("{{PROJECT_PATH}}")
});
if !has_project_access {
// Add a default project access rule
filtered_rules.push(SandboxRule {
id: None,
profile_id: 0,
operation_type: "file_read_all".to_string(),
pattern_type: "subpath".to_string(),
pattern_value: "{{PROJECT_PATH}}".to_string(),
enabled: true,
platform_support: None,
created_at: String::new(),
});
}
}
self.build_profile_with_serialization(filtered_rules)
}
/// Build a gaol Profile from database rules
pub fn build_profile(&self, rules: Vec<SandboxRule>) -> Result<Profile> {
let result = self.build_profile_with_serialization(rules)?;
Ok(result.profile)
}
/// Build a gaol Profile from database rules and return serialized operations
pub fn build_profile_with_serialization(&self, rules: Vec<SandboxRule>) -> Result<ProfileBuildResult> {
let mut operations = Vec::new();
let mut serialized_operations = Vec::new();
for rule in rules {
if !rule.enabled {
continue;
}
// Check platform support
if !self.is_rule_supported_on_platform(&rule) {
debug!("Skipping rule {} - not supported on current platform", rule.operation_type);
continue;
}
match self.build_operation_with_serialization(&rule) {
Ok(Some((op, serialized))) => {
// Check if operation is supported on current platform
if matches!(op.support(), gaol::profile::OperationSupportLevel::CanBeAllowed) {
operations.push(op);
serialized_operations.push(serialized);
} else {
warn!("Operation {:?} not supported at desired level on current platform", rule.operation_type);
}
},
Ok(None) => {
debug!("Skipping unsupported operation type: {}", rule.operation_type);
},
Err(e) => {
warn!("Failed to build operation for rule {}: {}", rule.id.unwrap_or(0), e);
}
}
}
// Ensure project path access is included
let has_project_access = serialized_operations.iter().any(|op| {
matches!(op, SerializedOperation::FileReadAll { path, is_subpath: true } if path == &self.project_path)
});
if !has_project_access {
operations.push(Operation::FileReadAll(PathPattern::Subpath(self.project_path.clone())));
serialized_operations.push(SerializedOperation::FileReadAll {
path: self.project_path.clone(),
is_subpath: true,
});
}
// Create the profile
let profile = Profile::new(operations)
.map_err(|_| anyhow::anyhow!("Failed to create sandbox profile - some operations may not be supported on this platform"))?;
Ok(ProfileBuildResult {
profile,
serialized: SerializedProfile {
operations: serialized_operations,
},
})
}
/// Build a gaol Operation from a database rule
fn build_operation(&self, rule: &SandboxRule) -> Result<Option<Operation>> {
match self.build_operation_with_serialization(rule) {
Ok(Some((op, _))) => Ok(Some(op)),
Ok(None) => Ok(None),
Err(e) => Err(e),
}
}
/// Build a gaol Operation and its serialized form from a database rule
fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result<Option<(Operation, SerializedOperation)>> {
match rule.operation_type.as_str() {
"file_read_all" => {
let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?;
Ok(Some((
Operation::FileReadAll(pattern),
SerializedOperation::FileReadAll { path, is_subpath }
)))
},
"file_read_metadata" => {
let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?;
Ok(Some((
Operation::FileReadMetadata(pattern),
SerializedOperation::FileReadMetadata { path, is_subpath }
)))
},
"network_outbound" => {
let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?;
Ok(Some((Operation::NetworkOutbound(pattern), serialized)))
},
"system_info_read" => {
Ok(Some((
Operation::SystemInfoRead,
SerializedOperation::SystemInfoRead
)))
},
_ => Ok(None)
}
}
/// Build a PathPattern from pattern type and value
fn build_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<PathPattern> {
let (pattern, _, _) = self.build_path_pattern_with_info(pattern_type, pattern_value)?;
Ok(pattern)
}
/// Build a PathPattern and return additional info for serialization
fn build_path_pattern_with_info(&self, pattern_type: &str, pattern_value: &str) -> Result<(PathPattern, PathBuf, bool)> {
// Replace template variables
let expanded_value = pattern_value
.replace("{{PROJECT_PATH}}", &self.project_path.to_string_lossy())
.replace("{{HOME}}", &self.home_dir.to_string_lossy());
let path = PathBuf::from(expanded_value);
match pattern_type {
"literal" => Ok((PathPattern::Literal(path.clone()), path, false)),
"subpath" => Ok((PathPattern::Subpath(path.clone()), path, true)),
_ => Err(anyhow::anyhow!("Unknown path pattern type: {}", pattern_type))
}
}
/// Build an AddressPattern from pattern type and value
fn build_address_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<AddressPattern> {
let (pattern, _) = self.build_address_pattern_with_serialization(pattern_type, pattern_value)?;
Ok(pattern)
}
/// Build an AddressPattern and its serialized form
fn build_address_pattern_with_serialization(&self, pattern_type: &str, pattern_value: &str) -> Result<(AddressPattern, SerializedOperation)> {
match pattern_type {
"all" => Ok((
AddressPattern::All,
SerializedOperation::NetworkOutbound { pattern: "all".to_string() }
)),
"tcp" => {
let port = pattern_value.parse::<u16>()
.context("Invalid TCP port number")?;
Ok((
AddressPattern::Tcp(port),
SerializedOperation::NetworkTcp { port }
))
},
"local_socket" => {
let path = PathBuf::from(pattern_value);
Ok((
AddressPattern::LocalSocket(path.clone()),
SerializedOperation::NetworkLocalSocket { path }
))
},
_ => Err(anyhow::anyhow!("Unknown address pattern type: {}", pattern_type))
}
}
/// Check if a rule is supported on the current platform
fn is_rule_supported_on_platform(&self, rule: &SandboxRule) -> bool {
if let Some(platforms_json) = &rule.platform_support {
if let Ok(platforms) = serde_json::from_str::<Vec<String>>(platforms_json) {
let current_os = std::env::consts::OS;
return platforms.contains(&current_os.to_string());
}
}
// If no platform support specified, assume it's supported
true
}
}
/// Load a sandbox profile by ID
pub fn load_profile(conn: &Connection, profile_id: i64) -> Result<SandboxProfile> {
conn.query_row(
"SELECT id, name, description, is_active, is_default, created_at, updated_at
FROM sandbox_profiles WHERE id = ?1",
params![profile_id],
|row| {
Ok(SandboxProfile {
id: Some(row.get(0)?),
name: row.get(1)?,
description: row.get(2)?,
is_active: row.get(3)?,
is_default: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
})
}
)
.context("Failed to load sandbox profile")
}
/// Load the default sandbox profile
pub fn load_default_profile(conn: &Connection) -> Result<SandboxProfile> {
conn.query_row(
"SELECT id, name, description, is_active, is_default, created_at, updated_at
FROM sandbox_profiles WHERE is_default = 1",
[],
|row| {
Ok(SandboxProfile {
id: Some(row.get(0)?),
name: row.get(1)?,
description: row.get(2)?,
is_active: row.get(3)?,
is_default: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
})
}
)
.context("Failed to load default sandbox profile")
}
/// Load rules for a sandbox profile
pub fn load_profile_rules(conn: &Connection, profile_id: i64) -> Result<Vec<SandboxRule>> {
let mut stmt = conn.prepare(
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at
FROM sandbox_rules WHERE profile_id = ?1 AND enabled = 1"
)?;
let rules = stmt.query_map(params![profile_id], |row| {
Ok(SandboxRule {
id: Some(row.get(0)?),
profile_id: row.get(1)?,
operation_type: row.get(2)?,
pattern_type: row.get(3)?,
pattern_value: row.get(4)?,
enabled: row.get(5)?,
platform_support: row.get(6)?,
created_at: row.get(7)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rules)
}
/// Get or create the gaol Profile for execution
pub fn get_gaol_profile(conn: &Connection, profile_id: Option<i64>, project_path: PathBuf) -> Result<Profile> {
// Load the profile
let profile = if let Some(id) = profile_id {
load_profile(conn, id)?
} else {
load_default_profile(conn)?
};
info!("Using sandbox profile: {}", profile.name);
// Load the rules
let rules = load_profile_rules(conn, profile.id.unwrap())?;
info!("Loaded {} sandbox rules", rules.len());
// Build the gaol profile
let builder = ProfileBuilder::new(project_path)?;
builder.build_profile(rules)
}