refactor: remove sandbox system and simplify agent architecture

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

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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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:");
}

View File

@@ -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::*;

View File

@@ -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");
}

View File

@@ -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"
);
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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}");
}
}
}

View File

@@ -1,7 +0,0 @@
//! Unit tests for sandbox components
#[cfg(test)]
mod executor;
#[cfg(test)]
mod platform;
#[cfg(test)]
mod profile_builder;

View File

@@ -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
);
}
}

View File

@@ -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
}

View File

@@ -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::*;