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:
28
README.md
28
README.md
@@ -37,7 +37,7 @@ Think of Claudia as your command center for Claude Code - bridging the gap betwe
|
||||
- [✨ Features](#-features)
|
||||
- [🗂️ Project & Session Management](#️-project--session-management)
|
||||
- [🤖 CC Agents](#-cc-agents)
|
||||
- [🛡️ Advanced Sandboxing](#️-advanced-sandboxing)
|
||||
|
||||
- [📊 Usage Analytics Dashboard](#-usage-analytics-dashboard)
|
||||
- [🔌 MCP Server Management](#-mcp-server-management)
|
||||
- [⏰ Timeline & Checkpoints](#-timeline--checkpoints)
|
||||
@@ -67,14 +67,10 @@ Think of Claudia as your command center for Claude Code - bridging the gap betwe
|
||||
### 🤖 **CC Agents**
|
||||
- **Custom AI Agents**: Create specialized agents with custom system prompts and behaviors
|
||||
- **Agent Library**: Build a collection of purpose-built agents for different tasks
|
||||
- **Secure Execution**: Run agents in sandboxed environments with fine-grained permissions
|
||||
- **Background Execution**: Run agents in separate processes for non-blocking operations
|
||||
- **Execution History**: Track all agent runs with detailed logs and performance metrics
|
||||
|
||||
### 🛡️ **Advanced Sandboxing**
|
||||
- **OS-Level Security**: Platform-specific sandboxing (seccomp on Linux, Seatbelt on macOS)
|
||||
- **Permission Profiles**: Create reusable security profiles with granular access controls
|
||||
- **Violation Tracking**: Monitor and log all security violations in real-time
|
||||
- **Import/Export**: Share sandbox profiles across teams and systems
|
||||
|
||||
|
||||
### 📊 **Usage Analytics Dashboard**
|
||||
- **Cost Tracking**: Monitor your Claude API usage and costs in real-time
|
||||
@@ -127,7 +123,7 @@ CC Agents → Create Agent → Configure → Execute
|
||||
|
||||
1. **Design Your Agent**: Set name, icon, and system prompt
|
||||
2. **Configure Model**: Choose between available Claude models
|
||||
3. **Set Sandbox Profile**: Apply security restrictions
|
||||
3. **Set Permissions**: Configure file read/write and network access
|
||||
4. **Execute Tasks**: Run your agent on any project
|
||||
|
||||
### Tracking Usage
|
||||
@@ -351,8 +347,8 @@ claudia/
|
||||
├── src-tauri/ # Rust backend
|
||||
│ ├── src/
|
||||
│ │ ├── commands/ # Tauri command handlers
|
||||
│ │ ├── sandbox/ # Security sandboxing
|
||||
│ │ └── checkpoint/ # Timeline management
|
||||
│ │ ├── checkpoint/ # Timeline management
|
||||
│ │ └── process/ # Process management
|
||||
│ └── tests/ # Rust test suite
|
||||
└── public/ # Public assets
|
||||
```
|
||||
@@ -378,13 +374,13 @@ cd src-tauri && cargo fmt
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
Claudia implements multiple layers of security:
|
||||
Claudia prioritizes your privacy and security:
|
||||
|
||||
1. **Process Isolation**: Agents run in separate sandboxed processes
|
||||
2. **Filesystem Access Control**: Whitelist-based file access
|
||||
3. **Network Restrictions**: Control external connections
|
||||
4. **Audit Logging**: All security violations are logged
|
||||
5. **No Data Collection**: Everything stays local on your machine
|
||||
1. **Process Isolation**: Agents run in separate processes
|
||||
2. **Permission Control**: Configure file and network access per agent
|
||||
3. **Local Storage**: All data stays on your machine
|
||||
4. **No Telemetry**: No data collection or tracking
|
||||
5. **Open Source**: Full transparency through open source code
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
|
@@ -16,11 +16,11 @@
|
||||
|
||||
## 📦 Available Agents
|
||||
|
||||
| Agent | Model | Permissions | Description | Default Task |
|
||||
|-------|-------|-------------|-------------|--------------|
|
||||
| **🎯 Git Commit Bot**<br/>🤖 `bot` | <img src="https://img.shields.io/badge/Sonnet-blue?style=flat-square" alt="Sonnet"> | ✅ File Read<br/>✅ File Write<br/>✅ Network<br/>❌ Sandbox | **Automate your Git workflow with intelligent commit messages**<br/><br/>Analyzes Git repository changes, generates detailed commit messages following Conventional Commits specification, and pushes changes to remote repository. | "Push all changes." |
|
||||
| **🛡️ Security Scanner**<br/>🛡️ `shield` | <img src="https://img.shields.io/badge/Opus-purple?style=flat-square" alt="Opus"> | ✅ File Read<br/>✅ File Write<br/>❌ Network<br/>❌ Sandbox | **Advanced AI-powered Static Application Security Testing (SAST)**<br/><br/>Performs comprehensive security audits by spawning specialized sub-agents for: codebase intelligence gathering, threat modeling (STRIDE), vulnerability scanning (OWASP Top 10, CWE), exploit validation, remediation design, and professional report generation. | "Review the codebase for security issues." |
|
||||
| **🧪 Unit Tests Bot**<br/>💻 `code` | <img src="https://img.shields.io/badge/Opus-purple?style=flat-square" alt="Opus"> | ✅ File Read<br/>✅ File Write<br/>❌ Network<br/>❌ Sandbox | **Automated comprehensive unit test generation for any codebase**<br/><br/>Analyzes codebase and generates comprehensive unit tests by: analyzing code structure, creating test plans, writing tests matching your style, verifying execution, optimizing coverage (>80% overall, 100% critical paths), and generating documentation. | "Generate unit tests for this codebase." |
|
||||
| Agent | Model | Description | Default Task |
|
||||
|-------|-------|-------------|--------------|
|
||||
| **🎯 Git Commit Bot**<br/>🤖 `bot` | <img src="https://img.shields.io/badge/Sonnet-blue?style=flat-square" alt="Sonnet"> | **Automate your Git workflow with intelligent commit messages**<br/><br/>Analyzes Git repository changes, generates detailed commit messages following Conventional Commits specification, and pushes changes to remote repository. | "Push all changes." |
|
||||
| **🛡️ Security Scanner**<br/>🛡️ `shield` | <img src="https://img.shields.io/badge/Opus-purple?style=flat-square" alt="Opus"> | **Advanced AI-powered Static Application Security Testing (SAST)**<br/><br/>Performs comprehensive security audits by spawning specialized sub-agents for: codebase intelligence gathering, threat modeling (STRIDE), vulnerability scanning (OWASP Top 10, CWE), exploit validation, remediation design, and professional report generation. | "Review the codebase for security issues." |
|
||||
| **🧪 Unit Tests Bot**<br/>💻 `code` | <img src="https://img.shields.io/badge/Opus-purple?style=flat-square" alt="Opus"> | **Automated comprehensive unit test generation for any codebase**<br/><br/>Analyzes codebase and generates comprehensive unit tests by: analyzing code structure, creating test plans, writing tests matching your style, verifying execution, optimizing coverage (>80% overall, 100% critical paths), and generating documentation. | "Generate unit tests for this codebase." |
|
||||
|
||||
### Available Icons
|
||||
|
||||
@@ -76,11 +76,7 @@ All agents are stored in `.claudia.json` format with the following structure:
|
||||
"icon": "bot",
|
||||
"model": "opus|sonnet|haiku",
|
||||
"system_prompt": "Your agent's instructions...",
|
||||
"default_task": "Default task description",
|
||||
"sandbox_enabled": false,
|
||||
"enable_file_read": true,
|
||||
"enable_file_write": true,
|
||||
"enable_network": false
|
||||
"default_task": "Default task description"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -109,9 +105,8 @@ The agent import/export system is built on a robust architecture:
|
||||
|
||||
1. **Version Control**: Each agent export includes version metadata
|
||||
2. **Duplicate Prevention**: Automatic naming conflict resolution
|
||||
3. **Permission System**: Granular control over file, network, and sandbox access
|
||||
4. **Model Selection**: Choose between Opus, Sonnet, and Haiku models
|
||||
5. **GitHub Integration**: Direct import from the official repository
|
||||
3. **Model Selection**: Choose between Opus, Sonnet, and Haiku models
|
||||
4. **GitHub Integration**: Direct import from the official repository
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -133,7 +128,6 @@ Export your agent to a `.claudia.json` file with a descriptive name.
|
||||
|
||||
- **Single Purpose**: Each agent should excel at one specific task
|
||||
- **Clear Documentation**: Write comprehensive system prompts
|
||||
- **Safe Defaults**: Be conservative with permissions
|
||||
- **Model Choice**: Use Haiku for simple tasks, Sonnet for general purpose, Opus for complex reasoning
|
||||
- **Naming**: Use descriptive names that clearly indicate the agent's function
|
||||
|
||||
@@ -145,4 +139,4 @@ These agents are provided under the same license as the Claudia project. See the
|
||||
|
||||
<div align="center">
|
||||
<strong>Built with ❤️ by the Claudia community</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,15 +1,11 @@
|
||||
{
|
||||
"agent": {
|
||||
"default_task": "Push all changes.",
|
||||
"enable_file_read": true,
|
||||
"enable_file_write": true,
|
||||
"enable_network": true,
|
||||
"icon": "bot",
|
||||
"model": "sonnet",
|
||||
"name": "Git Commit Bot",
|
||||
"sandbox_enabled": false,
|
||||
"system_prompt": "<task>\nYou are a Git Commit Push bot. Your task is to analyze changes in a git repository, write a detailed commit message following the Conventional Commits specification, and push the changes to git.\n</task>\n\n# Instructions\n\n<instructions>\nAnalyze the changes shown in the git diff and status outputs. Pay attention to:\n1. Which files were modified, added, or deleted\n2. The nature of the changes (e.g., bug fixes, new features, refactoring)\n3. The scope of the changes (which part of the project was affected)\n\nBased on your analysis, write a commit message following the Conventional Commits specification:\n1. Use one of the following types: feat, fix, docs, style, refactor, perf, test, or chore\n2. Include a scope in parentheses if applicable\n3. Write a concise description in the present tense\n4. If necessary, add a longer description after a blank line\n5. Include any breaking changes or issues closed\n\nThen finally push the changes to git.\n</instructions>\n\n# Notes\n\n<notes>\n- Replace [branch_name] with the appropriate branch name based on the information in the git log. If you cannot determine the branch name, use \"main\" as the default.\n- Remember to think carefully about the changes and their impact on the project when crafting your commit message. Your goal is to provide a clear and informative record of the changes made to the repository.\n</notes>"
|
||||
},
|
||||
"exported_at": "2025-06-23T14:29:58.156063+00:00",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -680,7 +680,6 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"gaol",
|
||||
"glob",
|
||||
"headless_chrome",
|
||||
"libc",
|
||||
@@ -1640,16 +1639,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gaol"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061957ca7a966a39a79ebca393a9a6c7babda10bf9dd6f11d00041558d929c22"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk"
|
||||
version = "0.18.2"
|
||||
|
@@ -49,8 +49,6 @@ zstd = "0.13"
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
walkdir = "2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
gaol = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26"
|
||||
|
@@ -353,7 +353,7 @@ fn select_best_installation(installations: Vec<ClaudeInstallation>) -> Option<Cl
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
// Neither have version info: prefer the one that is not just
|
||||
// the bare "claude" lookup from PATH, because that may fail
|
||||
// at runtime if PATH is sandbox-stripped.
|
||||
// at runtime if PATH is modified.
|
||||
(None, None) => {
|
||||
if a.path == "claude" && b.path != "claude" {
|
||||
Ordering::Less
|
||||
|
@@ -1,4 +1,3 @@
|
||||
use crate::sandbox::profile::ProfileBuilder;
|
||||
use anyhow::Result;
|
||||
use chrono;
|
||||
use log::{debug, error, info, warn};
|
||||
@@ -28,7 +27,6 @@ pub struct Agent {
|
||||
pub system_prompt: String,
|
||||
pub default_task: Option<String>,
|
||||
pub model: String,
|
||||
pub sandbox_enabled: bool,
|
||||
pub enable_file_read: bool,
|
||||
pub enable_file_write: bool,
|
||||
pub enable_network: bool,
|
||||
@@ -88,10 +86,6 @@ pub struct AgentData {
|
||||
pub system_prompt: String,
|
||||
pub default_task: Option<String>,
|
||||
pub model: String,
|
||||
pub sandbox_enabled: bool,
|
||||
pub enable_file_read: bool,
|
||||
pub enable_file_write: bool,
|
||||
pub enable_network: bool,
|
||||
}
|
||||
|
||||
/// Database connection state
|
||||
@@ -235,7 +229,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
|
||||
system_prompt TEXT NOT NULL,
|
||||
default_task TEXT,
|
||||
model TEXT NOT NULL DEFAULT 'sonnet',
|
||||
sandbox_enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
enable_file_read BOOLEAN NOT NULL DEFAULT 1,
|
||||
enable_file_write BOOLEAN NOT NULL DEFAULT 1,
|
||||
enable_network BOOLEAN NOT NULL DEFAULT 0,
|
||||
@@ -251,14 +244,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
|
||||
"ALTER TABLE agents ADD COLUMN model TEXT DEFAULT 'sonnet'",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE agents ADD COLUMN sandbox_profile_id INTEGER REFERENCES sandbox_profiles(id)",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE agents ADD COLUMN sandbox_enabled BOOLEAN DEFAULT 1",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE agents ADD COLUMN enable_file_read BOOLEAN DEFAULT 1",
|
||||
[],
|
||||
@@ -329,75 +314,6 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox profiles table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 0,
|
||||
is_default BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox rules table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER NOT NULL,
|
||||
operation_type TEXT NOT NULL,
|
||||
pattern_type TEXT NOT NULL,
|
||||
pattern_value TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
platform_support TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create trigger to update sandbox profile timestamp
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_sandbox_profile_timestamp
|
||||
AFTER UPDATE ON sandbox_profiles
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE sandbox_profiles SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox violations table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_violations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER,
|
||||
agent_id INTEGER,
|
||||
agent_run_id INTEGER,
|
||||
operation_type TEXT NOT NULL,
|
||||
pattern_value TEXT,
|
||||
process_name TEXT,
|
||||
pid INTEGER,
|
||||
denied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (agent_run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create index for efficient querying
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_sandbox_violations_denied_at
|
||||
ON sandbox_violations(denied_at DESC)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create default sandbox profiles if they don't exist
|
||||
crate::sandbox::defaults::create_default_profiles(&conn)?;
|
||||
|
||||
// Create settings table for app-wide settings
|
||||
conn.execute(
|
||||
@@ -430,7 +346,7 @@ pub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC")
|
||||
.prepare("SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents ORDER BY created_at DESC")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let agents = stmt
|
||||
@@ -444,12 +360,11 @@ pub async fn list_agents(db: State<'_, AgentDb>) -> Result<Vec<Agent>, String> {
|
||||
model: row
|
||||
.get::<_, String>(5)
|
||||
.unwrap_or_else(|_| "sonnet".to_string()),
|
||||
sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true),
|
||||
enable_file_read: row.get::<_, bool>(7).unwrap_or(true),
|
||||
enable_file_write: row.get::<_, bool>(8).unwrap_or(true),
|
||||
enable_network: row.get::<_, bool>(9).unwrap_or(false),
|
||||
created_at: row.get(10)?,
|
||||
updated_at: row.get(11)?,
|
||||
enable_file_read: row.get::<_, bool>(6).unwrap_or(true),
|
||||
enable_file_write: row.get::<_, bool>(7).unwrap_or(true),
|
||||
enable_network: row.get::<_, bool>(8).unwrap_or(false),
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
@@ -468,21 +383,19 @@ pub async fn create_agent(
|
||||
system_prompt: String,
|
||||
default_task: Option<String>,
|
||||
model: Option<String>,
|
||||
sandbox_enabled: Option<bool>,
|
||||
enable_file_read: Option<bool>,
|
||||
enable_file_write: Option<bool>,
|
||||
enable_network: Option<bool>,
|
||||
) -> Result<Agent, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
let model = model.unwrap_or_else(|| "sonnet".to_string());
|
||||
let sandbox_enabled = sandbox_enabled.unwrap_or(true);
|
||||
let enable_file_read = enable_file_read.unwrap_or(true);
|
||||
let enable_file_write = enable_file_write.unwrap_or(true);
|
||||
let enable_network = enable_network.unwrap_or(false);
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network],
|
||||
"INSERT INTO agents (name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -491,7 +404,7 @@ pub async fn create_agent(
|
||||
// Fetch the created agent
|
||||
let agent = conn
|
||||
.query_row(
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Agent {
|
||||
@@ -501,12 +414,11 @@ pub async fn create_agent(
|
||||
system_prompt: row.get(3)?,
|
||||
default_task: row.get(4)?,
|
||||
model: row.get(5)?,
|
||||
sandbox_enabled: row.get(6)?,
|
||||
enable_file_read: row.get(7)?,
|
||||
enable_file_write: row.get(8)?,
|
||||
enable_network: row.get(9)?,
|
||||
created_at: row.get(10)?,
|
||||
updated_at: row.get(11)?,
|
||||
enable_file_read: row.get(6)?,
|
||||
enable_file_write: row.get(7)?,
|
||||
enable_network: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -525,7 +437,6 @@ pub async fn update_agent(
|
||||
system_prompt: String,
|
||||
default_task: Option<String>,
|
||||
model: Option<String>,
|
||||
sandbox_enabled: Option<bool>,
|
||||
enable_file_read: Option<bool>,
|
||||
enable_file_write: Option<bool>,
|
||||
enable_network: Option<bool>,
|
||||
@@ -546,11 +457,6 @@ pub async fn update_agent(
|
||||
];
|
||||
let mut param_count = 5;
|
||||
|
||||
if let Some(se) = sandbox_enabled {
|
||||
param_count += 1;
|
||||
query.push_str(&format!(", sandbox_enabled = ?{}", param_count));
|
||||
params_vec.push(Box::new(se));
|
||||
}
|
||||
if let Some(efr) = enable_file_read {
|
||||
param_count += 1;
|
||||
query.push_str(&format!(", enable_file_read = ?{}", param_count));
|
||||
@@ -580,7 +486,7 @@ pub async fn update_agent(
|
||||
// Fetch the updated agent
|
||||
let agent = conn
|
||||
.query_row(
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Agent {
|
||||
@@ -590,12 +496,11 @@ pub async fn update_agent(
|
||||
system_prompt: row.get(3)?,
|
||||
default_task: row.get(4)?,
|
||||
model: row.get(5)?,
|
||||
sandbox_enabled: row.get(6)?,
|
||||
enable_file_read: row.get(7)?,
|
||||
enable_file_write: row.get(8)?,
|
||||
enable_network: row.get(9)?,
|
||||
created_at: row.get(10)?,
|
||||
updated_at: row.get(11)?,
|
||||
enable_file_read: row.get(6)?,
|
||||
enable_file_write: row.get(7)?,
|
||||
enable_network: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -622,7 +527,7 @@ pub async fn get_agent(db: State<'_, AgentDb>, id: i64) -> Result<Agent, String>
|
||||
|
||||
let agent = conn
|
||||
.query_row(
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Agent {
|
||||
@@ -632,12 +537,11 @@ pub async fn get_agent(db: State<'_, AgentDb>, id: i64) -> Result<Agent, String>
|
||||
system_prompt: row.get(3)?,
|
||||
default_task: row.get(4)?,
|
||||
model: row.get::<_, String>(5).unwrap_or_else(|_| "sonnet".to_string()),
|
||||
sandbox_enabled: row.get::<_, bool>(6).unwrap_or(true),
|
||||
enable_file_read: row.get::<_, bool>(7).unwrap_or(true),
|
||||
enable_file_write: row.get::<_, bool>(8).unwrap_or(true),
|
||||
enable_network: row.get::<_, bool>(9).unwrap_or(false),
|
||||
created_at: row.get(10)?,
|
||||
updated_at: row.get(11)?,
|
||||
enable_file_read: row.get::<_, bool>(6).unwrap_or(true),
|
||||
enable_file_write: row.get::<_, bool>(7).unwrap_or(true),
|
||||
enable_network: row.get::<_, bool>(8).unwrap_or(false),
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -788,411 +692,30 @@ pub async fn execute_agent(
|
||||
conn.last_insert_rowid()
|
||||
};
|
||||
|
||||
// Create sandbox rules based on agent-specific permissions (no database dependency)
|
||||
let sandbox_profile = if !agent.sandbox_enabled {
|
||||
info!("🔓 Agent '{}': Sandbox DISABLED", agent.name);
|
||||
None
|
||||
} else {
|
||||
info!(
|
||||
"🔒 Agent '{}': Sandbox enabled | File Read: {} | File Write: {} | Network: {}",
|
||||
agent.name, agent.enable_file_read, agent.enable_file_write, agent.enable_network
|
||||
);
|
||||
|
||||
// Create rules dynamically based on agent permissions
|
||||
let mut rules = Vec::new();
|
||||
|
||||
// Add file read rules if enabled
|
||||
if agent.enable_file_read {
|
||||
// Project directory access
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(1),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "{{PROJECT_PATH}}".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos", "windows"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
// System libraries (for language runtimes, etc.)
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(2),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/usr/lib".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(3),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/usr/local/lib".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(4),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/System/Library".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(5),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_metadata".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Add network rules if enabled
|
||||
if agent.enable_network {
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(6),
|
||||
profile_id: 0,
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: "".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Always add essential system paths (needed for executables to run)
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(7),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/usr/bin".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(8),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/opt/homebrew/bin".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(9),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/usr/local/bin".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(10),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/bin".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
// System libraries (needed for executables to link)
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(11),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/usr/lib".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(12),
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "/System/Library".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
// Always add system info reading (minimal requirement)
|
||||
rules.push(crate::sandbox::profile::SandboxRule {
|
||||
id: Some(13),
|
||||
profile_id: 0,
|
||||
operation_type: "system_info_read".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: "".to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
created_at: String::new(),
|
||||
});
|
||||
|
||||
Some(("Agent-specific".to_string(), rules))
|
||||
};
|
||||
|
||||
// Build the command
|
||||
let mut cmd = if let Some((_profile_name, rules)) = sandbox_profile {
|
||||
info!("🧪 DEBUG: Testing Claude command first without sandbox...");
|
||||
// Quick test to see if Claude is accessible at all
|
||||
let claude_path = match find_claude_binary(&app) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("❌ Claude binary not found: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
match std::process::Command::new(&claude_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
info!(
|
||||
"✅ Claude command works: {}",
|
||||
String::from_utf8_lossy(&output.stdout).trim()
|
||||
);
|
||||
} else {
|
||||
warn!("⚠️ Claude command failed with status: {}", output.status);
|
||||
warn!(" stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
warn!(" stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Claude command not found or not executable: {}", e);
|
||||
error!(" This could be why the agent is failing to start");
|
||||
}
|
||||
info!("Running agent '{}'", agent.name);
|
||||
let claude_path = match find_claude_binary(&app) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to find claude binary: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Test if Claude can actually start a session (this might reveal auth issues)
|
||||
info!("🧪 Testing Claude with exact same arguments as agent (without sandbox env vars)...");
|
||||
let mut test_cmd = std::process::Command::new(&claude_path);
|
||||
test_cmd
|
||||
.arg("-p")
|
||||
.arg(&task)
|
||||
.arg("--system-prompt")
|
||||
.arg(&agent.system_prompt)
|
||||
.arg("--model")
|
||||
.arg(&execution_model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path);
|
||||
|
||||
info!("🧪 Testing command: claude -p \"{}\" --system-prompt \"{}\" --model {} --output-format stream-json --verbose --dangerously-skip-permissions",
|
||||
task, agent.system_prompt, execution_model);
|
||||
|
||||
// Start the test process and give it 5 seconds to produce output
|
||||
match test_cmd.spawn() {
|
||||
Ok(mut child) => {
|
||||
// Wait for 5 seconds to see if it produces output
|
||||
let start = std::time::Instant::now();
|
||||
let mut output_received = false;
|
||||
|
||||
while start.elapsed() < std::time::Duration::from_secs(5) {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
info!("🧪 Test process exited with status: {}", status);
|
||||
output_received = true;
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
// Still running
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("🧪 Error checking test process: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !output_received {
|
||||
warn!("🧪 Test process is still running after 5 seconds - this suggests Claude might be waiting for input");
|
||||
// Kill the test process
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
} else {
|
||||
info!("🧪 Test process completed quickly - command seems to work");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Failed to spawn test Claude process: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
info!("🧪 End of Claude test, proceeding with sandbox...");
|
||||
|
||||
// Build the gaol profile using agent-specific permissions
|
||||
let project_path_buf = PathBuf::from(&project_path);
|
||||
|
||||
match ProfileBuilder::new(project_path_buf.clone()) {
|
||||
Ok(builder) => {
|
||||
// Build agent-specific profile with permission filtering
|
||||
match builder.build_agent_profile(
|
||||
rules,
|
||||
agent.sandbox_enabled,
|
||||
agent.enable_file_read,
|
||||
agent.enable_file_write,
|
||||
agent.enable_network,
|
||||
) {
|
||||
Ok(build_result) => {
|
||||
// Create the enhanced sandbox executor
|
||||
#[cfg(unix)]
|
||||
let executor =
|
||||
crate::sandbox::executor::SandboxExecutor::new_with_serialization(
|
||||
build_result.profile,
|
||||
project_path_buf.clone(),
|
||||
build_result.serialized,
|
||||
);
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let executor =
|
||||
crate::sandbox::executor::SandboxExecutor::new_with_serialization(
|
||||
(),
|
||||
project_path_buf.clone(),
|
||||
build_result.serialized,
|
||||
);
|
||||
|
||||
// Prepare the sandboxed command
|
||||
let args = vec![
|
||||
"-p",
|
||||
&task,
|
||||
"--system-prompt",
|
||||
&agent.system_prompt,
|
||||
"--model",
|
||||
&execution_model,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
"--dangerously-skip-permissions",
|
||||
];
|
||||
|
||||
let claude_path = match find_claude_binary(&app) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to find claude binary: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
executor.prepare_sandboxed_command(&claude_path, &args, &project_path_buf)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to build agent-specific sandbox profile: {}, falling back to non-sandboxed", e);
|
||||
let claude_path = match find_claude_binary(&app) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to find claude binary: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
cmd.arg("-p")
|
||||
.arg(&task)
|
||||
.arg("--system-prompt")
|
||||
.arg(&agent.system_prompt)
|
||||
.arg("--model")
|
||||
.arg(&execution_model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to create ProfileBuilder: {}, falling back to non-sandboxed",
|
||||
e
|
||||
);
|
||||
|
||||
// Fall back to non-sandboxed command
|
||||
let claude_path = match find_claude_binary(&app) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to find claude binary: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
cmd.arg("-p")
|
||||
.arg(&task)
|
||||
.arg("--system-prompt")
|
||||
.arg(&agent.system_prompt)
|
||||
.arg("--model")
|
||||
.arg(&execution_model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
cmd
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No sandbox or sandbox disabled, use regular command
|
||||
warn!(
|
||||
"🚨 Running agent '{}' WITHOUT SANDBOX - full system access!",
|
||||
agent.name
|
||||
);
|
||||
let claude_path = match find_claude_binary(&app) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to find claude binary: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
cmd.arg("-p")
|
||||
.arg(&task)
|
||||
.arg("--system-prompt")
|
||||
.arg(&agent.system_prompt)
|
||||
.arg("--model")
|
||||
.arg(&execution_model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
cmd
|
||||
};
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
cmd.arg("-p")
|
||||
.arg(&task)
|
||||
.arg("--system-prompt")
|
||||
.arg(&agent.system_prompt)
|
||||
.arg("--model")
|
||||
.arg(&execution_model)
|
||||
.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(&project_path)
|
||||
.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Spawn the process
|
||||
info!("🚀 Spawning Claude process...");
|
||||
@@ -1385,7 +908,7 @@ pub async fn execute_agent(
|
||||
warn!("⏰ TIMEOUT: No output from Claude process after 30 seconds");
|
||||
warn!("💡 This usually means:");
|
||||
warn!(" 1. Claude process is waiting for user input");
|
||||
warn!(" 2. Sandbox permissions are too restrictive");
|
||||
|
||||
warn!(" 3. Claude failed to initialize but didn't report an error");
|
||||
warn!(" 4. Network connectivity issues");
|
||||
warn!(" 5. Authentication issues (API key not found/invalid)");
|
||||
@@ -1807,7 +1330,7 @@ pub async fn export_agent(db: State<'_, AgentDb>, id: i64) -> Result<String, Str
|
||||
// Fetch the agent
|
||||
let agent = conn
|
||||
.query_row(
|
||||
"SELECT name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network FROM agents WHERE id = ?1",
|
||||
"SELECT name, icon, system_prompt, default_task, model FROM agents WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(serde_json::json!({
|
||||
@@ -1815,11 +1338,7 @@ pub async fn export_agent(db: State<'_, AgentDb>, id: i64) -> Result<String, Str
|
||||
"icon": row.get::<_, String>(1)?,
|
||||
"system_prompt": row.get::<_, String>(2)?,
|
||||
"default_task": row.get::<_, Option<String>>(3)?,
|
||||
"model": row.get::<_, String>(4)?,
|
||||
"sandbox_enabled": row.get::<_, bool>(5)?,
|
||||
"enable_file_read": row.get::<_, bool>(6)?,
|
||||
"enable_file_write": row.get::<_, bool>(7)?,
|
||||
"enable_network": row.get::<_, bool>(8)?
|
||||
"model": row.get::<_, String>(4)?
|
||||
}))
|
||||
},
|
||||
)
|
||||
@@ -2010,17 +1529,13 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
|
||||
|
||||
// Create the agent
|
||||
conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
"INSERT INTO agents (name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, 1, 1, 0)",
|
||||
params![
|
||||
final_name,
|
||||
agent_data.icon,
|
||||
agent_data.system_prompt,
|
||||
agent_data.default_task,
|
||||
agent_data.model,
|
||||
agent_data.sandbox_enabled,
|
||||
agent_data.enable_file_read,
|
||||
agent_data.enable_file_write,
|
||||
agent_data.enable_network
|
||||
agent_data.model
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to create agent: {}", e))?;
|
||||
@@ -2030,7 +1545,7 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
|
||||
// Fetch the created agent
|
||||
let agent = conn
|
||||
.query_row(
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
"SELECT id, name, icon, system_prompt, default_task, model, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Agent {
|
||||
@@ -2040,12 +1555,11 @@ pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<A
|
||||
system_prompt: row.get(3)?,
|
||||
default_task: row.get(4)?,
|
||||
model: row.get(5)?,
|
||||
sandbox_enabled: row.get(6)?,
|
||||
enable_file_read: row.get(7)?,
|
||||
enable_file_write: row.get(8)?,
|
||||
enable_network: row.get(9)?,
|
||||
created_at: row.get(10)?,
|
||||
updated_at: row.get(11)?,
|
||||
enable_file_read: row.get(6)?,
|
||||
enable_file_write: row.get(7)?,
|
||||
enable_network: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
@@ -798,15 +798,8 @@ pub async fn execute_claude_code(
|
||||
model
|
||||
);
|
||||
|
||||
// Check if sandboxing should be used
|
||||
let use_sandbox = should_use_sandbox(&app)?;
|
||||
|
||||
let mut cmd = if use_sandbox {
|
||||
create_sandboxed_claude_command(&app, &project_path)?
|
||||
} else {
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
create_command_with_env(&claude_path)
|
||||
};
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
|
||||
cmd.arg("-p")
|
||||
.arg(&prompt)
|
||||
@@ -837,15 +830,8 @@ pub async fn continue_claude_code(
|
||||
model
|
||||
);
|
||||
|
||||
// Check if sandboxing should be used
|
||||
let use_sandbox = should_use_sandbox(&app)?;
|
||||
|
||||
let mut cmd = if use_sandbox {
|
||||
create_sandboxed_claude_command(&app, &project_path)?
|
||||
} else {
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
create_command_with_env(&claude_path)
|
||||
};
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
|
||||
cmd.arg("-c") // Continue flag
|
||||
.arg("-p")
|
||||
@@ -879,15 +865,8 @@ pub async fn resume_claude_code(
|
||||
model
|
||||
);
|
||||
|
||||
// Check if sandboxing should be used
|
||||
let use_sandbox = should_use_sandbox(&app)?;
|
||||
|
||||
let mut cmd = if use_sandbox {
|
||||
create_sandboxed_claude_command(&app, &project_path)?
|
||||
} else {
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
create_command_with_env(&claude_path)
|
||||
};
|
||||
let claude_path = find_claude_binary(&app)?;
|
||||
let mut cmd = create_command_with_env(&claude_path);
|
||||
|
||||
cmd.arg("--resume")
|
||||
.arg(&session_id)
|
||||
@@ -1052,200 +1031,8 @@ pub async fn get_claude_session_output(
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to check if sandboxing should be used based on settings
|
||||
fn should_use_sandbox(app: &AppHandle) -> Result<bool, String> {
|
||||
// First check if sandboxing is even available on this platform
|
||||
if !crate::sandbox::platform::is_sandboxing_available() {
|
||||
log::info!("Sandboxing not available on this platform");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check if a setting exists to enable/disable sandboxing
|
||||
let settings = get_claude_settings_sync(app)?;
|
||||
|
||||
// Check for a sandboxing setting in the settings
|
||||
if let Some(sandbox_enabled) = settings
|
||||
.data
|
||||
.get("sandboxEnabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
{
|
||||
return Ok(sandbox_enabled);
|
||||
}
|
||||
|
||||
// Default to true (sandboxing enabled) on supported platforms
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Helper function to create a sandboxed Claude command
|
||||
fn create_sandboxed_claude_command(app: &AppHandle, project_path: &str) -> Result<Command, String> {
|
||||
use crate::sandbox::{executor::create_sandboxed_command, profile::ProfileBuilder};
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Get the database connection
|
||||
let conn = {
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
|
||||
let db_path = app_data_dir.join("agents.db");
|
||||
rusqlite::Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?
|
||||
};
|
||||
|
||||
// Query for the default active sandbox profile
|
||||
let profile_id: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT id FROM sandbox_profiles WHERE is_default = 1 AND is_active = 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.ok();
|
||||
|
||||
match profile_id {
|
||||
Some(profile_id) => {
|
||||
log::info!(
|
||||
"Using default sandbox profile: {} (id: {})",
|
||||
profile_id,
|
||||
profile_id
|
||||
);
|
||||
|
||||
// Get all rules for this profile
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT operation_type, pattern_type, pattern_value, enabled, platform_support
|
||||
FROM sandbox_rules WHERE profile_id = ?1 AND enabled = 1",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let rules = stmt
|
||||
.query_map(rusqlite::params![profile_id], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, bool>(3)?,
|
||||
row.get::<_, Option<String>>(4)?,
|
||||
))
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
log::info!("Building sandbox profile with {} rules", rules.len());
|
||||
|
||||
// Build the gaol profile
|
||||
let project_path_buf = PathBuf::from(project_path);
|
||||
|
||||
match ProfileBuilder::new(project_path_buf.clone()) {
|
||||
Ok(builder) => {
|
||||
// Convert database rules to SandboxRule structs
|
||||
let mut sandbox_rules = Vec::new();
|
||||
|
||||
for (idx, (op_type, pattern_type, pattern_value, enabled, platform_support)) in
|
||||
rules.into_iter().enumerate()
|
||||
{
|
||||
// Check if this rule applies to the current platform
|
||||
if let Some(platforms_json) = &platform_support {
|
||||
if let Ok(platforms) =
|
||||
serde_json::from_str::<Vec<String>>(platforms_json)
|
||||
{
|
||||
let current_platform = if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"freebsd"
|
||||
} else {
|
||||
"unsupported"
|
||||
};
|
||||
|
||||
if !platforms.contains(¤t_platform.to_string()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create SandboxRule struct
|
||||
let rule = crate::sandbox::profile::SandboxRule {
|
||||
id: Some(idx as i64),
|
||||
profile_id: 0,
|
||||
operation_type: op_type,
|
||||
pattern_type,
|
||||
pattern_value,
|
||||
enabled,
|
||||
platform_support,
|
||||
created_at: String::new(),
|
||||
};
|
||||
|
||||
sandbox_rules.push(rule);
|
||||
}
|
||||
|
||||
// Try to build the profile
|
||||
match builder.build_profile(sandbox_rules) {
|
||||
Ok(profile) => {
|
||||
log::info!("Successfully built sandbox profile '{}'", profile_id);
|
||||
|
||||
// Use the helper function to create sandboxed command
|
||||
let claude_path = find_claude_binary(app)?;
|
||||
#[cfg(unix)]
|
||||
return Ok(create_sandboxed_command(
|
||||
&claude_path,
|
||||
&[],
|
||||
&project_path_buf,
|
||||
profile,
|
||||
project_path_buf.clone(),
|
||||
));
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
log::warn!(
|
||||
"Sandboxing not supported on Windows, using regular command"
|
||||
);
|
||||
Ok(create_command_with_env(&claude_path))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to build sandbox profile: {}, falling back to non-sandboxed", e);
|
||||
let claude_path = find_claude_binary(app)?;
|
||||
Ok(create_command_with_env(&claude_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to create ProfileBuilder: {}, falling back to non-sandboxed",
|
||||
e
|
||||
);
|
||||
let claude_path = find_claude_binary(app)?;
|
||||
Ok(create_command_with_env(&claude_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::info!("No default active sandbox profile found: proceeding without sandbox");
|
||||
let claude_path = find_claude_binary(app)?;
|
||||
Ok(create_command_with_env(&claude_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronous version of get_claude_settings for use in non-async contexts
|
||||
fn get_claude_settings_sync(_app: &AppHandle) -> Result<ClaudeSettings, String> {
|
||||
let claude_dir = get_claude_dir().map_err(|e| e.to_string())?;
|
||||
let settings_path = claude_dir.join("settings.json");
|
||||
|
||||
if !settings_path.exists() {
|
||||
return Ok(ClaudeSettings::default());
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&settings_path)
|
||||
.map_err(|e| format!("Failed to read settings file: {}", e))?;
|
||||
|
||||
let data: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse settings JSON: {}", e))?;
|
||||
|
||||
Ok(ClaudeSettings { data })
|
||||
}
|
||||
|
||||
/// Helper function to spawn Claude process and handle streaming
|
||||
async fn spawn_claude_process(app: AppHandle, mut cmd: Command, prompt: String, model: String, project_path: String) -> Result<(), String> {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
pub mod agents;
|
||||
pub mod claude;
|
||||
pub mod mcp;
|
||||
pub mod sandbox;
|
||||
pub mod screenshot;
|
||||
pub mod usage;
|
||||
|
@@ -1,947 +0,0 @@
|
||||
use crate::{
|
||||
commands::agents::AgentDb,
|
||||
sandbox::{
|
||||
platform::PlatformCapabilities,
|
||||
profile::{SandboxProfile, SandboxRule},
|
||||
},
|
||||
};
|
||||
use rusqlite::params;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
/// Represents a sandbox violation event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxViolation {
|
||||
pub id: Option<i64>,
|
||||
pub profile_id: Option<i64>,
|
||||
pub agent_id: Option<i64>,
|
||||
pub agent_run_id: Option<i64>,
|
||||
pub operation_type: String,
|
||||
pub pattern_value: Option<String>,
|
||||
pub process_name: Option<String>,
|
||||
pub pid: Option<i32>,
|
||||
pub denied_at: String,
|
||||
}
|
||||
|
||||
/// Represents sandbox profile export data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxProfileExport {
|
||||
pub version: u32,
|
||||
pub exported_at: String,
|
||||
pub platform: String,
|
||||
pub profiles: Vec<SandboxProfileWithRules>,
|
||||
}
|
||||
|
||||
/// Represents a profile with its rules for export
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxProfileWithRules {
|
||||
pub profile: SandboxProfile,
|
||||
pub rules: Vec<SandboxRule>,
|
||||
}
|
||||
|
||||
/// Import result for a profile
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImportResult {
|
||||
pub profile_name: String,
|
||||
pub imported: bool,
|
||||
pub reason: Option<String>,
|
||||
pub new_name: Option<String>,
|
||||
}
|
||||
|
||||
/// List all sandbox profiles
|
||||
#[tauri::command]
|
||||
pub async fn list_sandbox_profiles(db: State<'_, AgentDb>) -> Result<Vec<SandboxProfile>, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles ORDER BY name")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let profiles = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
/// Create a new sandbox profile
|
||||
#[tauri::command]
|
||||
pub async fn create_sandbox_profile(
|
||||
db: State<'_, AgentDb>,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Result<SandboxProfile, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description) VALUES (?1, ?2)",
|
||||
params![name, description],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let id = conn.last_insert_rowid();
|
||||
|
||||
// Fetch the created profile
|
||||
let profile = conn
|
||||
.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// Update a sandbox profile
|
||||
#[tauri::command]
|
||||
pub async fn update_sandbox_profile(
|
||||
db: State<'_, AgentDb>,
|
||||
id: i64,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
is_active: bool,
|
||||
is_default: bool,
|
||||
) -> Result<SandboxProfile, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// If setting as default, unset other defaults
|
||||
if is_default {
|
||||
conn.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 0 WHERE id != ?1",
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"UPDATE sandbox_profiles SET name = ?1, description = ?2, is_active = ?3, is_default = ?4 WHERE id = ?5",
|
||||
params![name, description, is_active, is_default, id],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Fetch the updated profile
|
||||
let profile = conn
|
||||
.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// Delete a sandbox profile
|
||||
#[tauri::command]
|
||||
pub async fn delete_sandbox_profile(db: State<'_, AgentDb>, id: i64) -> Result<(), String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Check if it's the default profile
|
||||
let is_default: bool = conn
|
||||
.query_row(
|
||||
"SELECT is_default FROM sandbox_profiles WHERE id = ?1",
|
||||
params![id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if is_default {
|
||||
return Err("Cannot delete the default profile".to_string());
|
||||
}
|
||||
|
||||
conn.execute("DELETE FROM sandbox_profiles WHERE id = ?1", params![id])
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a single sandbox profile by ID
|
||||
#[tauri::command]
|
||||
pub async fn get_sandbox_profile(
|
||||
db: State<'_, AgentDb>,
|
||||
id: i64,
|
||||
) -> Result<SandboxProfile, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let profile = conn
|
||||
.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// List rules for a sandbox profile
|
||||
#[tauri::command]
|
||||
pub async fn list_sandbox_rules(
|
||||
db: State<'_, AgentDb>,
|
||||
profile_id: i64,
|
||||
) -> Result<Vec<SandboxRule>, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at FROM sandbox_rules WHERE profile_id = ?1 ORDER BY operation_type, pattern_value")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let rules = stmt
|
||||
.query_map(params![profile_id], |row| {
|
||||
Ok(SandboxRule {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
operation_type: row.get(2)?,
|
||||
pattern_type: row.get(3)?,
|
||||
pattern_value: row.get(4)?,
|
||||
enabled: row.get(5)?,
|
||||
platform_support: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
/// Create a new sandbox rule
|
||||
#[tauri::command]
|
||||
pub async fn create_sandbox_rule(
|
||||
db: State<'_, AgentDb>,
|
||||
profile_id: i64,
|
||||
operation_type: String,
|
||||
pattern_type: String,
|
||||
pattern_value: String,
|
||||
enabled: bool,
|
||||
platform_support: Option<String>,
|
||||
) -> Result<SandboxRule, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Validate rule doesn't conflict
|
||||
// TODO: Add more validation logic here
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let id = conn.last_insert_rowid();
|
||||
|
||||
// Fetch the created rule
|
||||
let rule = conn
|
||||
.query_row(
|
||||
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at FROM sandbox_rules WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(SandboxRule {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
operation_type: row.get(2)?,
|
||||
pattern_type: row.get(3)?,
|
||||
pattern_value: row.get(4)?,
|
||||
enabled: row.get(5)?,
|
||||
platform_support: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
/// Update a sandbox rule
|
||||
#[tauri::command]
|
||||
pub async fn update_sandbox_rule(
|
||||
db: State<'_, AgentDb>,
|
||||
id: i64,
|
||||
operation_type: String,
|
||||
pattern_type: String,
|
||||
pattern_value: String,
|
||||
enabled: bool,
|
||||
platform_support: Option<String>,
|
||||
) -> Result<SandboxRule, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute(
|
||||
"UPDATE sandbox_rules SET operation_type = ?1, pattern_type = ?2, pattern_value = ?3, enabled = ?4, platform_support = ?5 WHERE id = ?6",
|
||||
params![operation_type, pattern_type, pattern_value, enabled, platform_support, id],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Fetch the updated rule
|
||||
let rule = conn
|
||||
.query_row(
|
||||
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at FROM sandbox_rules WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(SandboxRule {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
operation_type: row.get(2)?,
|
||||
pattern_type: row.get(3)?,
|
||||
pattern_value: row.get(4)?,
|
||||
enabled: row.get(5)?,
|
||||
platform_support: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
/// Delete a sandbox rule
|
||||
#[tauri::command]
|
||||
pub async fn delete_sandbox_rule(db: State<'_, AgentDb>, id: i64) -> Result<(), String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute("DELETE FROM sandbox_rules WHERE id = ?1", params![id])
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get platform capabilities for sandbox configuration
|
||||
#[tauri::command]
|
||||
pub async fn get_platform_capabilities() -> Result<PlatformCapabilities, String> {
|
||||
Ok(crate::sandbox::platform::get_platform_capabilities())
|
||||
}
|
||||
|
||||
/// Test a sandbox profile by creating a simple test process
|
||||
#[tauri::command]
|
||||
pub async fn test_sandbox_profile(
|
||||
db: State<'_, AgentDb>,
|
||||
profile_id: i64,
|
||||
) -> Result<String, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Load the profile and rules
|
||||
let profile = crate::sandbox::profile::load_profile(&conn, profile_id)
|
||||
.map_err(|e| format!("Failed to load profile: {}", e))?;
|
||||
|
||||
if !profile.is_active {
|
||||
return Ok(format!(
|
||||
"Profile '{}' is currently inactive. Activate it to use with agents.",
|
||||
profile.name
|
||||
));
|
||||
}
|
||||
|
||||
let rules = crate::sandbox::profile::load_profile_rules(&conn, profile_id)
|
||||
.map_err(|e| format!("Failed to load profile rules: {}", e))?;
|
||||
|
||||
if rules.is_empty() {
|
||||
return Ok(format!(
|
||||
"Profile '{}' has no rules configured. Add rules to define sandbox permissions.",
|
||||
profile.name
|
||||
));
|
||||
}
|
||||
|
||||
// Try to build the gaol profile
|
||||
let test_path = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
|
||||
|
||||
let builder = crate::sandbox::profile::ProfileBuilder::new(test_path.clone())
|
||||
.map_err(|e| format!("Failed to create profile builder: {}", e))?;
|
||||
|
||||
let build_result = builder
|
||||
.build_profile_with_serialization(rules.clone())
|
||||
.map_err(|e| format!("Failed to build sandbox profile: {}", e))?;
|
||||
|
||||
// Check platform support
|
||||
let platform_caps = crate::sandbox::platform::get_platform_capabilities();
|
||||
if !platform_caps.sandboxing_supported {
|
||||
return Ok(format!(
|
||||
"Profile '{}' validated successfully. {} rules loaded.\n\nNote: Sandboxing is not supported on {} platform. The profile configuration is valid but sandbox enforcement will not be active.",
|
||||
profile.name,
|
||||
rules.len(),
|
||||
platform_caps.os
|
||||
));
|
||||
}
|
||||
|
||||
// Try to execute a simple command in the sandbox
|
||||
let executor = crate::sandbox::executor::SandboxExecutor::new_with_serialization(
|
||||
build_result.profile,
|
||||
test_path.clone(),
|
||||
build_result.serialized,
|
||||
);
|
||||
|
||||
// Use a simple echo command for testing
|
||||
let test_command = if cfg!(windows) { "cmd" } else { "echo" };
|
||||
|
||||
let test_args = if cfg!(windows) {
|
||||
vec!["/C", "echo", "sandbox test successful"]
|
||||
} else {
|
||||
vec!["sandbox test successful"]
|
||||
};
|
||||
|
||||
match executor.execute_sandboxed_spawn(test_command, &test_args, &test_path) {
|
||||
Ok(mut child) => {
|
||||
// Wait for the process to complete with a timeout
|
||||
match child.wait() {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
Ok(format!(
|
||||
"✅ Profile '{}' tested successfully!\n\n\
|
||||
• {} rules loaded and validated\n\
|
||||
• Sandbox activation: Success\n\
|
||||
• Test process execution: Success\n\
|
||||
• Platform: {} (fully supported)",
|
||||
profile.name,
|
||||
rules.len(),
|
||||
platform_caps.os
|
||||
))
|
||||
} else {
|
||||
Ok(format!(
|
||||
"⚠️ Profile '{}' validated with warnings.\n\n\
|
||||
• {} rules loaded and validated\n\
|
||||
• Sandbox activation: Success\n\
|
||||
• Test process exit code: {}\n\
|
||||
• Platform: {}",
|
||||
profile.name,
|
||||
rules.len(),
|
||||
status.code().unwrap_or(-1),
|
||||
platform_caps.os
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => Ok(format!(
|
||||
"⚠️ Profile '{}' validated with warnings.\n\n\
|
||||
• {} rules loaded and validated\n\
|
||||
• Sandbox activation: Partial\n\
|
||||
• Test process: Could not get exit status ({})\n\
|
||||
• Platform: {}",
|
||||
profile.name,
|
||||
rules.len(),
|
||||
e,
|
||||
platform_caps.os
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if it's a permission error or platform limitation
|
||||
let error_str = e.to_string();
|
||||
if error_str.contains("permission") || error_str.contains("denied") {
|
||||
Ok(format!(
|
||||
"⚠️ Profile '{}' validated with limitations.\n\n\
|
||||
• {} rules loaded and validated\n\
|
||||
• Sandbox configuration: Valid\n\
|
||||
• Sandbox enforcement: Limited by system permissions\n\
|
||||
• Platform: {}\n\n\
|
||||
Note: The sandbox profile is correctly configured but may require elevated privileges or system configuration to fully enforce on this platform.",
|
||||
profile.name,
|
||||
rules.len(),
|
||||
platform_caps.os
|
||||
))
|
||||
} else {
|
||||
Ok(format!(
|
||||
"⚠️ Profile '{}' validated with limitations.\n\n\
|
||||
• {} rules loaded and validated\n\
|
||||
• Sandbox configuration: Valid\n\
|
||||
• Test execution: Failed ({})\n\
|
||||
• Platform: {}\n\n\
|
||||
The sandbox profile is correctly configured. The test execution failed due to platform-specific limitations, but the profile can still be used.",
|
||||
profile.name,
|
||||
rules.len(),
|
||||
e,
|
||||
platform_caps.os
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List sandbox violations with optional filtering
|
||||
#[tauri::command]
|
||||
pub async fn list_sandbox_violations(
|
||||
db: State<'_, AgentDb>,
|
||||
profile_id: Option<i64>,
|
||||
agent_id: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
) -> Result<Vec<SandboxViolation>, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Build dynamic query
|
||||
let mut query = String::from(
|
||||
"SELECT id, profile_id, agent_id, agent_run_id, operation_type, pattern_value, process_name, pid, denied_at
|
||||
FROM sandbox_violations WHERE 1=1"
|
||||
);
|
||||
|
||||
let mut param_idx = 1;
|
||||
|
||||
if profile_id.is_some() {
|
||||
query.push_str(&format!(" AND profile_id = ?{}", param_idx));
|
||||
param_idx += 1;
|
||||
}
|
||||
|
||||
if agent_id.is_some() {
|
||||
query.push_str(&format!(" AND agent_id = ?{}", param_idx));
|
||||
param_idx += 1;
|
||||
}
|
||||
|
||||
query.push_str(" ORDER BY denied_at DESC");
|
||||
|
||||
if limit.is_some() {
|
||||
query.push_str(&format!(" LIMIT ?{}", param_idx));
|
||||
}
|
||||
|
||||
// Execute query based on parameters
|
||||
let violations: Vec<SandboxViolation> = if let Some(pid) = profile_id {
|
||||
if let Some(aid) = agent_id {
|
||||
if let Some(lim) = limit {
|
||||
// All three parameters
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![pid, aid, lim], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
// profile_id and agent_id only
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![pid, aid], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
} else if let Some(lim) = limit {
|
||||
// profile_id and limit only
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![pid, lim], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
// profile_id only
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![pid], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
} else if let Some(aid) = agent_id {
|
||||
if let Some(lim) = limit {
|
||||
// agent_id and limit only
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![aid, lim], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
// agent_id only
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![aid], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
} else if let Some(lim) = limit {
|
||||
// limit only
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![lim], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
// No parameters
|
||||
let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(SandboxViolation {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
agent_id: row.get(2)?,
|
||||
agent_run_id: row.get(3)?,
|
||||
operation_type: row.get(4)?,
|
||||
pattern_value: row.get(5)?,
|
||||
process_name: row.get(6)?,
|
||||
pid: row.get(7)?,
|
||||
denied_at: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
Ok(violations)
|
||||
}
|
||||
|
||||
/// Log a sandbox violation
|
||||
#[tauri::command]
|
||||
pub async fn log_sandbox_violation(
|
||||
db: State<'_, AgentDb>,
|
||||
profile_id: Option<i64>,
|
||||
agent_id: Option<i64>,
|
||||
agent_run_id: Option<i64>,
|
||||
operation_type: String,
|
||||
pattern_value: Option<String>,
|
||||
process_name: Option<String>,
|
||||
pid: Option<i32>,
|
||||
) -> Result<(), String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_violations (profile_id, agent_id, agent_run_id, operation_type, pattern_value, process_name, pid)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![profile_id, agent_id, agent_run_id, operation_type, pattern_value, process_name, pid],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear old sandbox violations
|
||||
#[tauri::command]
|
||||
pub async fn clear_sandbox_violations(
|
||||
db: State<'_, AgentDb>,
|
||||
older_than_days: Option<i64>,
|
||||
) -> Result<i64, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let query = if let Some(days) = older_than_days {
|
||||
format!(
|
||||
"DELETE FROM sandbox_violations WHERE denied_at < datetime('now', '-{} days')",
|
||||
days
|
||||
)
|
||||
} else {
|
||||
"DELETE FROM sandbox_violations".to_string()
|
||||
};
|
||||
|
||||
let deleted = conn.execute(&query, []).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(deleted as i64)
|
||||
}
|
||||
|
||||
/// Get sandbox violation statistics
|
||||
#[tauri::command]
|
||||
pub async fn get_sandbox_violation_stats(
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Get total violations
|
||||
let total: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM sandbox_violations", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Get violations by operation type
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT operation_type, COUNT(*) as count
|
||||
FROM sandbox_violations
|
||||
GROUP BY operation_type
|
||||
ORDER BY count DESC",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let by_operation: Vec<(String, i64)> = stmt
|
||||
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
|
||||
.map_err(|e| e.to_string())?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Get recent violations count (last 24 hours)
|
||||
let recent: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sandbox_violations WHERE denied_at > datetime('now', '-1 day')",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"total": total,
|
||||
"recent_24h": recent,
|
||||
"by_operation": by_operation.into_iter().map(|(op, count)| {
|
||||
serde_json::json!({
|
||||
"operation": op,
|
||||
"count": count
|
||||
})
|
||||
}).collect::<Vec<_>>()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Export a single sandbox profile with its rules
|
||||
#[tauri::command]
|
||||
pub async fn export_sandbox_profile(
|
||||
db: State<'_, AgentDb>,
|
||||
profile_id: i64,
|
||||
) -> Result<SandboxProfileExport, String> {
|
||||
// Get the profile
|
||||
let profile = {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
crate::sandbox::profile::load_profile(&conn, profile_id).map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
// Get the rules
|
||||
let rules = list_sandbox_rules(db.clone(), profile_id).await?;
|
||||
|
||||
Ok(SandboxProfileExport {
|
||||
version: 1,
|
||||
exported_at: chrono::Utc::now().to_rfc3339(),
|
||||
platform: std::env::consts::OS.to_string(),
|
||||
profiles: vec![SandboxProfileWithRules { profile, rules }],
|
||||
})
|
||||
}
|
||||
|
||||
/// Export all sandbox profiles
|
||||
#[tauri::command]
|
||||
pub async fn export_all_sandbox_profiles(
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<SandboxProfileExport, String> {
|
||||
let profiles = list_sandbox_profiles(db.clone()).await?;
|
||||
let mut profile_exports = Vec::new();
|
||||
|
||||
for profile in profiles {
|
||||
if let Some(id) = profile.id {
|
||||
let rules = list_sandbox_rules(db.clone(), id).await?;
|
||||
profile_exports.push(SandboxProfileWithRules { profile, rules });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SandboxProfileExport {
|
||||
version: 1,
|
||||
exported_at: chrono::Utc::now().to_rfc3339(),
|
||||
platform: std::env::consts::OS.to_string(),
|
||||
profiles: profile_exports,
|
||||
})
|
||||
}
|
||||
|
||||
/// Import sandbox profiles from export data
|
||||
#[tauri::command]
|
||||
pub async fn import_sandbox_profiles(
|
||||
db: State<'_, AgentDb>,
|
||||
export_data: SandboxProfileExport,
|
||||
) -> Result<Vec<ImportResult>, String> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Validate version
|
||||
if export_data.version != 1 {
|
||||
return Err(format!(
|
||||
"Unsupported export version: {}",
|
||||
export_data.version
|
||||
));
|
||||
}
|
||||
|
||||
for profile_export in export_data.profiles {
|
||||
let mut profile = profile_export.profile;
|
||||
let original_name = profile.name.clone();
|
||||
|
||||
// Check for name conflicts
|
||||
let existing: Result<i64, _> = {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
conn.query_row(
|
||||
"SELECT id FROM sandbox_profiles WHERE name = ?1",
|
||||
params![&profile.name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
};
|
||||
|
||||
let (imported, new_name) = match existing {
|
||||
Ok(_) => {
|
||||
// Name conflict - append timestamp
|
||||
let new_name = format!(
|
||||
"{} (imported {})",
|
||||
profile.name,
|
||||
chrono::Utc::now().format("%Y-%m-%d %H:%M")
|
||||
);
|
||||
profile.name = new_name.clone();
|
||||
(true, Some(new_name))
|
||||
}
|
||||
Err(_) => (true, None),
|
||||
};
|
||||
|
||||
if imported {
|
||||
// Reset profile fields for new insert
|
||||
profile.id = None;
|
||||
profile.is_default = false; // Never import as default
|
||||
|
||||
// Create the profile
|
||||
let created_profile =
|
||||
create_sandbox_profile(db.clone(), profile.name.clone(), profile.description)
|
||||
.await?;
|
||||
|
||||
if let Some(new_id) = created_profile.id {
|
||||
// Import rules
|
||||
for rule in profile_export.rules {
|
||||
if rule.enabled {
|
||||
// Create the rule with the new profile ID
|
||||
let _ = create_sandbox_rule(
|
||||
db.clone(),
|
||||
new_id,
|
||||
rule.operation_type,
|
||||
rule.pattern_type,
|
||||
rule.pattern_value,
|
||||
rule.enabled,
|
||||
rule.platform_support,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile status if needed
|
||||
if profile.is_active {
|
||||
let _ = update_sandbox_profile(
|
||||
db.clone(),
|
||||
new_id,
|
||||
created_profile.name,
|
||||
created_profile.description,
|
||||
profile.is_active,
|
||||
false, // Never set as default on import
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
results.push(ImportResult {
|
||||
profile_name: original_name,
|
||||
imported: true,
|
||||
reason: new_name
|
||||
.as_ref()
|
||||
.map(|_| "Name conflict resolved".to_string()),
|
||||
new_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
@@ -5,7 +5,6 @@ pub mod checkpoint;
|
||||
pub mod claude_binary;
|
||||
pub mod commands;
|
||||
pub mod process;
|
||||
pub mod sandbox;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
|
@@ -5,7 +5,6 @@ mod checkpoint;
|
||||
mod claude_binary;
|
||||
mod commands;
|
||||
mod process;
|
||||
mod sandbox;
|
||||
|
||||
use checkpoint::state::CheckpointState;
|
||||
use commands::agents::{
|
||||
@@ -34,13 +33,6 @@ use commands::mcp::{
|
||||
mcp_read_project_config, mcp_remove, mcp_reset_project_choices, mcp_save_project_config,
|
||||
mcp_serve, mcp_test_connection,
|
||||
};
|
||||
use commands::sandbox::{
|
||||
clear_sandbox_violations, create_sandbox_profile, create_sandbox_rule, delete_sandbox_profile,
|
||||
delete_sandbox_rule, export_all_sandbox_profiles, export_sandbox_profile,
|
||||
get_platform_capabilities, get_sandbox_profile, get_sandbox_violation_stats,
|
||||
import_sandbox_profiles, list_sandbox_profiles, list_sandbox_rules, list_sandbox_violations,
|
||||
log_sandbox_violation, test_sandbox_profile, update_sandbox_profile, update_sandbox_rule,
|
||||
};
|
||||
use commands::screenshot::{capture_url_screenshot, cleanup_screenshot_temp_files};
|
||||
use commands::usage::{
|
||||
get_session_stats, get_usage_by_date_range, get_usage_details, get_usage_stats,
|
||||
@@ -53,14 +45,6 @@ fn main() {
|
||||
// Initialize logger
|
||||
env_logger::init();
|
||||
|
||||
// Check if we need to activate sandbox in this process
|
||||
if sandbox::executor::should_activate_sandbox() {
|
||||
// This is a child process that needs sandbox activation
|
||||
if let Err(e) = sandbox::executor::SandboxExecutor::activate_sandbox_in_child() {
|
||||
log::error!("Failed to activate sandbox: {}", e);
|
||||
// Continue without sandbox rather than crashing
|
||||
}
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
@@ -161,24 +145,6 @@ fn main() {
|
||||
fetch_github_agents,
|
||||
fetch_github_agent_content,
|
||||
import_agent_from_github,
|
||||
list_sandbox_profiles,
|
||||
get_sandbox_profile,
|
||||
create_sandbox_profile,
|
||||
update_sandbox_profile,
|
||||
delete_sandbox_profile,
|
||||
list_sandbox_rules,
|
||||
create_sandbox_rule,
|
||||
update_sandbox_rule,
|
||||
delete_sandbox_rule,
|
||||
test_sandbox_profile,
|
||||
get_platform_capabilities,
|
||||
list_sandbox_violations,
|
||||
log_sandbox_violation,
|
||||
clear_sandbox_violations,
|
||||
get_sandbox_violation_stats,
|
||||
export_sandbox_profile,
|
||||
export_all_sandbox_profiles,
|
||||
import_sandbox_profiles,
|
||||
get_usage_stats,
|
||||
get_usage_by_date_range,
|
||||
get_usage_details,
|
||||
|
@@ -1,212 +0,0 @@
|
||||
use crate::sandbox::profile::{SandboxProfile, SandboxRule};
|
||||
use rusqlite::{params, Connection, Result};
|
||||
|
||||
/// Create default sandbox profiles for initial setup
|
||||
pub fn create_default_profiles(conn: &Connection) -> Result<()> {
|
||||
// Check if we already have profiles
|
||||
let count: i64 = conn.query_row("SELECT COUNT(*) FROM sandbox_profiles", [], |row| {
|
||||
row.get(0)
|
||||
})?;
|
||||
|
||||
if count > 0 {
|
||||
// Already have profiles, don't create defaults
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create Standard Profile
|
||||
create_standard_profile(conn)?;
|
||||
|
||||
// Create Minimal Profile
|
||||
create_minimal_profile(conn)?;
|
||||
|
||||
// Create Development Profile
|
||||
create_development_profile(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_standard_profile(conn: &Connection) -> Result<()> {
|
||||
// Insert profile
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
"Standard",
|
||||
"Standard sandbox profile with balanced permissions for most use cases",
|
||||
true,
|
||||
true // Set as default
|
||||
],
|
||||
)?;
|
||||
|
||||
let profile_id = conn.last_insert_rowid();
|
||||
|
||||
// Add rules
|
||||
let rules = vec![
|
||||
// File access
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/local/lib",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/System/Library",
|
||||
true,
|
||||
Some(r#"["macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_metadata",
|
||||
"subpath",
|
||||
"/",
|
||||
true,
|
||||
Some(r#"["macos"]"#),
|
||||
),
|
||||
// Network access
|
||||
(
|
||||
"network_outbound",
|
||||
"all",
|
||||
"",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
];
|
||||
|
||||
for (op_type, pattern_type, pattern_value, enabled, platforms) in rules {
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![profile_id, op_type, pattern_type, pattern_value, enabled, platforms],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_minimal_profile(conn: &Connection) -> Result<()> {
|
||||
// Insert profile
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
"Minimal",
|
||||
"Minimal sandbox profile with only project directory access",
|
||||
true,
|
||||
false
|
||||
],
|
||||
)?;
|
||||
|
||||
let profile_id = conn.last_insert_rowid();
|
||||
|
||||
// Add minimal rules - only project access
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
profile_id,
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
true,
|
||||
Some(r#"["linux", "macos", "windows"]"#)
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_development_profile(conn: &Connection) -> Result<()> {
|
||||
// Insert profile
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
"Development",
|
||||
"Development profile with broader permissions for development tasks",
|
||||
true,
|
||||
false
|
||||
],
|
||||
)?;
|
||||
|
||||
let profile_id = conn.last_insert_rowid();
|
||||
|
||||
// Add development rules
|
||||
let rules = vec![
|
||||
// Broad file access
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{HOME}}",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/opt",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/Applications",
|
||||
true,
|
||||
Some(r#"["macos"]"#),
|
||||
),
|
||||
(
|
||||
"file_read_metadata",
|
||||
"subpath",
|
||||
"/",
|
||||
true,
|
||||
Some(r#"["macos"]"#),
|
||||
),
|
||||
// Network access
|
||||
(
|
||||
"network_outbound",
|
||||
"all",
|
||||
"",
|
||||
true,
|
||||
Some(r#"["linux", "macos"]"#),
|
||||
),
|
||||
// System info (macOS only)
|
||||
("system_info_read", "all", "", true, Some(r#"["macos"]"#)),
|
||||
];
|
||||
|
||||
for (op_type, pattern_type, pattern_value, enabled, platforms) in rules {
|
||||
conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![profile_id, op_type, pattern_type, pattern_value, enabled, platforms],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,511 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
#[cfg(unix)]
|
||||
use gaol::sandbox::{
|
||||
ChildSandbox, ChildSandboxMethods, Command as GaolCommand, Sandbox, SandboxMethods,
|
||||
};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Sandbox executor for running commands in a sandboxed environment
|
||||
pub struct SandboxExecutor {
|
||||
#[cfg(unix)]
|
||||
profile: gaol::profile::Profile,
|
||||
project_path: PathBuf,
|
||||
serialized_profile: Option<SerializedProfile>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl SandboxExecutor {
|
||||
/// Create a new sandbox executor with the given profile
|
||||
pub fn new(profile: gaol::profile::Profile, project_path: PathBuf) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
project_path,
|
||||
serialized_profile: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new sandbox executor with serialized profile for child process communication
|
||||
pub fn new_with_serialization(
|
||||
profile: gaol::profile::Profile,
|
||||
project_path: PathBuf,
|
||||
serialized_profile: SerializedProfile,
|
||||
) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
project_path,
|
||||
serialized_profile: Some(serialized_profile),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command in the sandbox (for the parent process)
|
||||
/// This is used when we need to spawn a child process with sandbox
|
||||
pub fn execute_sandboxed_spawn(
|
||||
&self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
cwd: &Path,
|
||||
) -> Result<std::process::Child> {
|
||||
info!("Executing sandboxed command: {} {:?}", command, args);
|
||||
|
||||
// On macOS, we need to check if the command is allowed by the system
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// For testing purposes, we'll skip actual sandboxing for simple commands like echo
|
||||
if command == "echo" || command == "/bin/echo" {
|
||||
debug!(
|
||||
"Using direct execution for simple test command: {}",
|
||||
command
|
||||
);
|
||||
return std::process::Command::new(command)
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn test command");
|
||||
}
|
||||
}
|
||||
|
||||
// Create the sandbox
|
||||
let sandbox = Sandbox::new(self.profile.clone());
|
||||
|
||||
// Create the command
|
||||
let mut gaol_command = GaolCommand::new(command);
|
||||
for arg in args {
|
||||
gaol_command.arg(arg);
|
||||
}
|
||||
|
||||
// Set environment variables
|
||||
gaol_command.env("GAOL_CHILD_PROCESS", "1");
|
||||
gaol_command.env("GAOL_SANDBOX_ACTIVE", "1");
|
||||
gaol_command.env(
|
||||
"GAOL_PROJECT_PATH",
|
||||
self.project_path.to_string_lossy().as_ref(),
|
||||
);
|
||||
|
||||
// Inherit specific parent environment variables that are safe
|
||||
for (key, value) in env::vars() {
|
||||
// Only pass through safe environment variables
|
||||
if key.starts_with("PATH")
|
||||
|| key.starts_with("HOME")
|
||||
|| key.starts_with("USER")
|
||||
|| key == "SHELL"
|
||||
|| key == "LANG"
|
||||
|| key == "LC_ALL"
|
||||
|| key.starts_with("LC_")
|
||||
{
|
||||
gaol_command.env(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to start the sandboxed process using gaol
|
||||
match sandbox.start(&mut gaol_command) {
|
||||
Ok(process) => {
|
||||
debug!("Successfully started sandboxed process using gaol");
|
||||
// Unfortunately, gaol doesn't expose the underlying Child process
|
||||
// So we need to use a different approach for now
|
||||
|
||||
// This is a limitation of the gaol library - we can't get the Child back
|
||||
// For now, we'll have to use the fallback approach
|
||||
warn!(
|
||||
"Gaol started the process but we can't get the Child handle - using fallback"
|
||||
);
|
||||
|
||||
// Drop the process to avoid zombie
|
||||
drop(process);
|
||||
|
||||
// Fall through to fallback
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to start sandboxed process with gaol: {}", e);
|
||||
debug!("Gaol error details: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use regular process spawn with sandbox activation in child
|
||||
info!("Using child-side sandbox activation as fallback");
|
||||
|
||||
// Serialize the sandbox rules for the child process
|
||||
let rules_json = if let Some(ref serialized) = self.serialized_profile {
|
||||
serde_json::to_string(serialized)?
|
||||
} else {
|
||||
let serialized_rules = self.extract_sandbox_rules()?;
|
||||
serde_json::to_string(&serialized_rules)?
|
||||
};
|
||||
|
||||
let mut std_command = std::process::Command::new(command);
|
||||
std_command
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.env("GAOL_SANDBOX_ACTIVE", "1")
|
||||
.env(
|
||||
"GAOL_PROJECT_PATH",
|
||||
self.project_path.to_string_lossy().as_ref(),
|
||||
)
|
||||
.env("GAOL_SANDBOX_RULES", rules_json)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
std_command
|
||||
.spawn()
|
||||
.context("Failed to spawn process with sandbox environment")
|
||||
}
|
||||
|
||||
/// Prepare a tokio Command for sandboxed execution
|
||||
/// The sandbox will be activated in the child process
|
||||
pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command {
|
||||
info!("Preparing sandboxed command: {} {:?}", command, args);
|
||||
|
||||
let mut cmd = Command::new(command);
|
||||
cmd.args(args).current_dir(cwd);
|
||||
|
||||
// Inherit essential environment variables from parent process
|
||||
// This is crucial for commands like Claude that need to find Node.js
|
||||
for (key, value) in env::vars() {
|
||||
// Pass through PATH and other essential environment variables
|
||||
if key == "PATH"
|
||||
|| key == "HOME"
|
||||
|| key == "USER"
|
||||
|| key == "SHELL"
|
||||
|| key == "LANG"
|
||||
|| key == "LC_ALL"
|
||||
|| key.starts_with("LC_")
|
||||
|| key == "NODE_PATH"
|
||||
|| key == "NVM_DIR"
|
||||
|| key == "NVM_BIN"
|
||||
{
|
||||
debug!("Inheriting env var: {}={}", key, value);
|
||||
cmd.env(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the sandbox rules for the child process
|
||||
let rules_json = if let Some(ref serialized) = self.serialized_profile {
|
||||
let json = serde_json::to_string(serialized).ok();
|
||||
info!(
|
||||
"🔧 Using serialized sandbox profile with {} operations",
|
||||
serialized.operations.len()
|
||||
);
|
||||
for (i, op) in serialized.operations.iter().enumerate() {
|
||||
match op {
|
||||
SerializedOperation::FileReadAll { path, is_subpath } => {
|
||||
info!(
|
||||
" Rule {}: FileReadAll {} (subpath: {})",
|
||||
i,
|
||||
path.display(),
|
||||
is_subpath
|
||||
);
|
||||
}
|
||||
SerializedOperation::NetworkOutbound { pattern } => {
|
||||
info!(" Rule {}: NetworkOutbound {}", i, pattern);
|
||||
}
|
||||
SerializedOperation::SystemInfoRead => {
|
||||
info!(" Rule {}: SystemInfoRead", i);
|
||||
}
|
||||
_ => {
|
||||
info!(" Rule {}: {:?}", i, op);
|
||||
}
|
||||
}
|
||||
}
|
||||
json
|
||||
} else {
|
||||
info!("🔧 No serialized profile, extracting from gaol profile");
|
||||
self.extract_sandbox_rules()
|
||||
.ok()
|
||||
.and_then(|r| serde_json::to_string(&r).ok())
|
||||
};
|
||||
|
||||
if let Some(json) = rules_json {
|
||||
// TEMPORARILY DISABLED: Claude Code might not understand these env vars and could hang
|
||||
// cmd.env("GAOL_SANDBOX_ACTIVE", "1");
|
||||
// cmd.env("GAOL_PROJECT_PATH", self.project_path.to_string_lossy().as_ref());
|
||||
// cmd.env("GAOL_SANDBOX_RULES", &json);
|
||||
warn!("🚨 TEMPORARILY DISABLED sandbox environment variables for debugging");
|
||||
info!("🔧 Would have set sandbox environment variables for child process");
|
||||
info!(" GAOL_SANDBOX_ACTIVE=1 (disabled)");
|
||||
info!(
|
||||
" GAOL_PROJECT_PATH={} (disabled)",
|
||||
self.project_path.display()
|
||||
);
|
||||
info!(" GAOL_SANDBOX_RULES={} chars (disabled)", json.len());
|
||||
} else {
|
||||
warn!("🚨 Failed to serialize sandbox rules - running without sandbox!");
|
||||
}
|
||||
|
||||
cmd.stdin(Stdio::null()) // Don't pipe stdin - we have no input to send
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Extract sandbox rules from the profile
|
||||
/// This is a workaround since gaol doesn't expose the operations
|
||||
fn extract_sandbox_rules(&self) -> Result<SerializedProfile> {
|
||||
// We need to track the rules when building the profile
|
||||
// For now, return a default set based on what we know
|
||||
// This should be improved by tracking rules during profile creation
|
||||
let operations = vec![
|
||||
SerializedOperation::FileReadAll {
|
||||
path: self.project_path.clone(),
|
||||
is_subpath: true,
|
||||
},
|
||||
SerializedOperation::NetworkOutbound {
|
||||
pattern: "all".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
Ok(SerializedProfile { operations })
|
||||
}
|
||||
|
||||
/// Activate sandbox in the current process (for child processes)
|
||||
/// This should be called early in the child process
|
||||
pub fn activate_sandbox_in_child() -> Result<()> {
|
||||
// Check if sandbox should be activated
|
||||
if !should_activate_sandbox() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Activating sandbox in child process");
|
||||
|
||||
// Get project path
|
||||
let project_path = env::var("GAOL_PROJECT_PATH").context("GAOL_PROJECT_PATH not set")?;
|
||||
let project_path = PathBuf::from(project_path);
|
||||
|
||||
// Try to deserialize the sandbox rules from environment
|
||||
let profile = if let Ok(rules_json) = env::var("GAOL_SANDBOX_RULES") {
|
||||
match serde_json::from_str::<SerializedProfile>(&rules_json) {
|
||||
Ok(serialized) => {
|
||||
debug!(
|
||||
"Deserializing {} sandbox rules",
|
||||
serialized.operations.len()
|
||||
);
|
||||
deserialize_profile(serialized, &project_path)?
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to deserialize sandbox rules: {}", e);
|
||||
// Fallback to minimal profile
|
||||
create_minimal_profile(project_path)?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("No sandbox rules found in environment, using minimal profile");
|
||||
// Fallback to minimal profile
|
||||
create_minimal_profile(project_path)?
|
||||
};
|
||||
|
||||
// Create and activate the child sandbox
|
||||
let sandbox = ChildSandbox::new(profile);
|
||||
|
||||
match sandbox.activate() {
|
||||
Ok(_) => {
|
||||
info!("Sandbox activated successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to activate sandbox: {:?}", e);
|
||||
Err(anyhow::anyhow!("Failed to activate sandbox: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Windows implementation - no sandboxing
|
||||
#[cfg(not(unix))]
|
||||
impl SandboxExecutor {
|
||||
/// Create a new sandbox executor (no-op on Windows)
|
||||
pub fn new(_profile: (), project_path: PathBuf) -> Self {
|
||||
Self {
|
||||
project_path,
|
||||
serialized_profile: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new sandbox executor with serialized profile (no-op on Windows)
|
||||
pub fn new_with_serialization(
|
||||
_profile: (),
|
||||
project_path: PathBuf,
|
||||
serialized_profile: SerializedProfile,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_path,
|
||||
serialized_profile: Some(serialized_profile),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command in the sandbox (Windows - no sandboxing)
|
||||
pub fn execute_sandboxed_spawn(
|
||||
&self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
cwd: &Path,
|
||||
) -> Result<std::process::Child> {
|
||||
info!(
|
||||
"Executing command without sandbox on Windows: {} {:?}",
|
||||
command, args
|
||||
);
|
||||
|
||||
std::process::Command::new(command)
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn process")
|
||||
}
|
||||
|
||||
/// Prepare a sandboxed tokio Command (Windows - no sandboxing)
|
||||
pub fn prepare_sandboxed_command(&self, command: &str, args: &[&str], cwd: &Path) -> Command {
|
||||
info!(
|
||||
"Preparing command without sandbox on Windows: {} {:?}",
|
||||
command, args
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(command);
|
||||
cmd.args(args)
|
||||
.current_dir(cwd)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Extract sandbox rules (no-op on Windows)
|
||||
fn extract_sandbox_rules(&self) -> Result<SerializedProfile> {
|
||||
Ok(SerializedProfile { operations: vec![] })
|
||||
}
|
||||
|
||||
/// Activate sandbox in child process (no-op on Windows)
|
||||
pub fn activate_sandbox_in_child() -> Result<()> {
|
||||
debug!("Sandbox activation skipped on Windows");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the current process should activate sandbox
|
||||
pub fn should_activate_sandbox() -> bool {
|
||||
env::var("GAOL_SANDBOX_ACTIVE").unwrap_or_default() == "1"
|
||||
}
|
||||
|
||||
/// Helper to create a sandboxed tokio Command
|
||||
#[cfg(unix)]
|
||||
pub fn create_sandboxed_command(
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
cwd: &Path,
|
||||
profile: gaol::profile::Profile,
|
||||
project_path: PathBuf,
|
||||
) -> Command {
|
||||
let executor = SandboxExecutor::new(profile, project_path);
|
||||
executor.prepare_sandboxed_command(command, args, cwd)
|
||||
}
|
||||
|
||||
// Serialization helpers for passing profile between processes
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct SerializedProfile {
|
||||
pub operations: Vec<SerializedOperation>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub enum SerializedOperation {
|
||||
FileReadAll { path: PathBuf, is_subpath: bool },
|
||||
FileReadMetadata { path: PathBuf, is_subpath: bool },
|
||||
NetworkOutbound { pattern: String },
|
||||
NetworkTcp { port: u16 },
|
||||
NetworkLocalSocket { path: PathBuf },
|
||||
SystemInfoRead,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn deserialize_profile(
|
||||
serialized: SerializedProfile,
|
||||
project_path: &Path,
|
||||
) -> Result<gaol::profile::Profile> {
|
||||
let mut operations = Vec::new();
|
||||
|
||||
for op in serialized.operations {
|
||||
match op {
|
||||
SerializedOperation::FileReadAll { path, is_subpath } => {
|
||||
let pattern = if is_subpath {
|
||||
gaol::profile::PathPattern::Subpath(path)
|
||||
} else {
|
||||
gaol::profile::PathPattern::Literal(path)
|
||||
};
|
||||
operations.push(gaol::profile::Operation::FileReadAll(pattern));
|
||||
}
|
||||
SerializedOperation::FileReadMetadata { path, is_subpath } => {
|
||||
let pattern = if is_subpath {
|
||||
gaol::profile::PathPattern::Subpath(path)
|
||||
} else {
|
||||
gaol::profile::PathPattern::Literal(path)
|
||||
};
|
||||
operations.push(gaol::profile::Operation::FileReadMetadata(pattern));
|
||||
}
|
||||
SerializedOperation::NetworkOutbound { pattern } => {
|
||||
let addr_pattern = match pattern.as_str() {
|
||||
"all" => gaol::profile::AddressPattern::All,
|
||||
_ => {
|
||||
warn!("Unknown network pattern '{}', defaulting to All", pattern);
|
||||
gaol::profile::AddressPattern::All
|
||||
}
|
||||
};
|
||||
operations.push(gaol::profile::Operation::NetworkOutbound(addr_pattern));
|
||||
}
|
||||
SerializedOperation::NetworkTcp { port } => {
|
||||
operations.push(gaol::profile::Operation::NetworkOutbound(
|
||||
gaol::profile::AddressPattern::Tcp(port),
|
||||
));
|
||||
}
|
||||
SerializedOperation::NetworkLocalSocket { path } => {
|
||||
operations.push(gaol::profile::Operation::NetworkOutbound(
|
||||
gaol::profile::AddressPattern::LocalSocket(path),
|
||||
));
|
||||
}
|
||||
SerializedOperation::SystemInfoRead => {
|
||||
operations.push(gaol::profile::Operation::SystemInfoRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure project path access
|
||||
let has_project_access = operations.iter().any(|op| {
|
||||
matches!(op, gaol::profile::Operation::FileReadAll(gaol::profile::PathPattern::Subpath(p)) if p == project_path)
|
||||
});
|
||||
|
||||
if !has_project_access {
|
||||
operations.push(gaol::profile::Operation::FileReadAll(
|
||||
gaol::profile::PathPattern::Subpath(project_path.to_path_buf()),
|
||||
));
|
||||
}
|
||||
|
||||
let op_count = operations.len();
|
||||
gaol::profile::Profile::new(operations).map_err(|e| {
|
||||
error!("Failed to create profile: {:?}", e);
|
||||
anyhow::anyhow!(
|
||||
"Failed to create profile from {} operations: {:?}",
|
||||
op_count,
|
||||
e
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn create_minimal_profile(project_path: PathBuf) -> Result<gaol::profile::Profile> {
|
||||
let operations = vec![
|
||||
gaol::profile::Operation::FileReadAll(gaol::profile::PathPattern::Subpath(project_path)),
|
||||
gaol::profile::Operation::NetworkOutbound(gaol::profile::AddressPattern::All),
|
||||
];
|
||||
|
||||
gaol::profile::Profile::new(operations).map_err(|e| {
|
||||
error!("Failed to create minimal profile: {:?}", e);
|
||||
anyhow::anyhow!("Failed to create minimal sandbox profile: {:?}", e)
|
||||
})
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
#[allow(unused)]
|
||||
pub mod defaults;
|
||||
#[allow(unused)]
|
||||
pub mod executor;
|
||||
#[allow(unused)]
|
||||
pub mod platform;
|
||||
#[allow(unused)]
|
||||
pub mod profile;
|
||||
|
||||
// These are used in agents.rs and claude.rs via direct module paths
|
||||
#[allow(unused)]
|
||||
pub use profile::{ProfileBuilder, SandboxProfile, SandboxRule};
|
||||
// These are used in main.rs and sandbox.rs
|
||||
#[allow(unused)]
|
||||
pub use executor::{should_activate_sandbox, SandboxExecutor};
|
||||
// These are used in sandbox.rs
|
||||
#[allow(unused)]
|
||||
pub use platform::{get_platform_capabilities, PlatformCapabilities};
|
||||
// Used for initial setup
|
||||
#[allow(unused)]
|
||||
pub use defaults::create_default_profiles;
|
@@ -1,179 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
/// Represents the sandbox capabilities of the current platform
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlatformCapabilities {
|
||||
/// The current operating system
|
||||
pub os: String,
|
||||
/// Whether sandboxing is supported on this platform
|
||||
pub sandboxing_supported: bool,
|
||||
/// Supported operations and their support levels
|
||||
pub operations: Vec<OperationSupport>,
|
||||
/// Platform-specific notes or warnings
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Represents support for a specific operation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OperationSupport {
|
||||
/// The operation type
|
||||
pub operation: String,
|
||||
/// Support level: "never", "can_be_allowed", "cannot_be_precisely", "always"
|
||||
pub support_level: String,
|
||||
/// Human-readable description
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Get the platform capabilities for sandboxing
|
||||
pub fn get_platform_capabilities() -> PlatformCapabilities {
|
||||
let os = env::consts::OS;
|
||||
|
||||
match os {
|
||||
"linux" => get_linux_capabilities(),
|
||||
"macos" => get_macos_capabilities(),
|
||||
"freebsd" => get_freebsd_capabilities(),
|
||||
_ => get_unsupported_capabilities(os),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_linux_capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: "linux".to_string(),
|
||||
sandboxing_supported: true,
|
||||
operations: vec![
|
||||
OperationSupport {
|
||||
operation: "file_read_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow file reading via bind mounts in chroot jail".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_metadata".to_string(),
|
||||
support_level: "cannot_be_precisely".to_string(),
|
||||
description: "Cannot be precisely controlled, allowed if file read is allowed".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow all network access by not creating network namespace".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_tcp".to_string(),
|
||||
support_level: "cannot_be_precisely".to_string(),
|
||||
description: "Cannot filter by specific ports with seccomp".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_local".to_string(),
|
||||
support_level: "cannot_be_precisely".to_string(),
|
||||
description: "Cannot filter by specific socket paths with seccomp".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "system_info_read".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported on Linux".to_string(),
|
||||
},
|
||||
],
|
||||
notes: vec![
|
||||
"Linux sandboxing uses namespaces (user, PID, IPC, mount, UTS, network) and seccomp-bpf".to_string(),
|
||||
"File access is controlled via bind mounts in a chroot jail".to_string(),
|
||||
"Network filtering is all-or-nothing (cannot filter by port/address)".to_string(),
|
||||
"Process creation and privilege escalation are always blocked".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_macos_capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: "macos".to_string(),
|
||||
sandboxing_supported: true,
|
||||
operations: vec![
|
||||
OperationSupport {
|
||||
operation: "file_read_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow file reading with Seatbelt profiles".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_metadata".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow metadata reading with Seatbelt profiles".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_all".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow all network access".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_tcp".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow specific TCP ports".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_local".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow specific local socket paths".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "system_info_read".to_string(),
|
||||
support_level: "can_be_allowed".to_string(),
|
||||
description: "Can allow sysctl reads".to_string(),
|
||||
},
|
||||
],
|
||||
notes: vec![
|
||||
"macOS sandboxing uses Seatbelt (sandbox_init API)".to_string(),
|
||||
"More fine-grained control compared to Linux".to_string(),
|
||||
"Can filter network access by port and socket path".to_string(),
|
||||
"Supports platform-specific operations like Mach port lookups".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_freebsd_capabilities() -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: "freebsd".to_string(),
|
||||
sandboxing_supported: true,
|
||||
operations: vec![
|
||||
OperationSupport {
|
||||
operation: "system_info_read".to_string(),
|
||||
support_level: "always".to_string(),
|
||||
description: "Always allowed with Capsicum".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_all".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported with current Capsicum implementation".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "file_read_metadata".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported with current Capsicum implementation".to_string(),
|
||||
},
|
||||
OperationSupport {
|
||||
operation: "network_outbound_all".to_string(),
|
||||
support_level: "never".to_string(),
|
||||
description: "Not supported with current Capsicum implementation".to_string(),
|
||||
},
|
||||
],
|
||||
notes: vec![
|
||||
"FreeBSD support is very limited in gaol".to_string(),
|
||||
"Uses Capsicum for capability-based security".to_string(),
|
||||
"Most operations are not supported".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_unsupported_capabilities(os: &str) -> PlatformCapabilities {
|
||||
PlatformCapabilities {
|
||||
os: os.to_string(),
|
||||
sandboxing_supported: false,
|
||||
operations: vec![],
|
||||
notes: vec![
|
||||
format!("Sandboxing is not supported on {} platform", os),
|
||||
"Claude Code will run without sandbox restrictions".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if sandboxing is available on the current platform
|
||||
pub fn is_sandboxing_available() -> bool {
|
||||
matches!(env::consts::OS, "linux" | "macos" | "freebsd")
|
||||
}
|
@@ -1,556 +0,0 @@
|
||||
use crate::sandbox::executor::{SerializedOperation, SerializedProfile};
|
||||
use anyhow::{Context, Result};
|
||||
#[cfg(unix)]
|
||||
use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile};
|
||||
use log::{debug, info, warn};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Represents a sandbox profile from the database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxProfile {
|
||||
pub id: Option<i64>,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_default: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// Represents a sandbox rule from the database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxRule {
|
||||
pub id: Option<i64>,
|
||||
pub profile_id: i64,
|
||||
pub operation_type: String,
|
||||
pub pattern_type: String,
|
||||
pub pattern_value: String,
|
||||
pub enabled: bool,
|
||||
pub platform_support: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Result of building a profile
|
||||
pub struct ProfileBuildResult {
|
||||
#[cfg(unix)]
|
||||
pub profile: Profile,
|
||||
#[cfg(not(unix))]
|
||||
pub profile: (), // Placeholder for Windows
|
||||
pub serialized: SerializedProfile,
|
||||
}
|
||||
|
||||
/// Builder for creating gaol profiles from database configuration
|
||||
pub struct ProfileBuilder {
|
||||
project_path: PathBuf,
|
||||
home_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ProfileBuilder {
|
||||
/// Create a new profile builder
|
||||
pub fn new(project_path: PathBuf) -> Result<Self> {
|
||||
let home_dir = dirs::home_dir().context("Could not determine home directory")?;
|
||||
|
||||
Ok(Self {
|
||||
project_path,
|
||||
home_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules filtered by agent permissions
|
||||
pub fn build_agent_profile(
|
||||
&self,
|
||||
rules: Vec<SandboxRule>,
|
||||
sandbox_enabled: bool,
|
||||
enable_file_read: bool,
|
||||
enable_file_write: bool,
|
||||
enable_network: bool,
|
||||
) -> Result<ProfileBuildResult> {
|
||||
// If sandbox is completely disabled, return an empty profile
|
||||
if !sandbox_enabled {
|
||||
return Ok(ProfileBuildResult {
|
||||
#[cfg(unix)]
|
||||
profile: Profile::new(vec![])
|
||||
.map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?,
|
||||
#[cfg(not(unix))]
|
||||
profile: (),
|
||||
serialized: SerializedProfile { operations: vec![] },
|
||||
});
|
||||
}
|
||||
|
||||
let mut filtered_rules = Vec::new();
|
||||
|
||||
for rule in rules {
|
||||
if !rule.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter rules based on agent permissions
|
||||
let include_rule = match rule.operation_type.as_str() {
|
||||
"file_read_all" | "file_read_metadata" => enable_file_read,
|
||||
"network_outbound" => enable_network,
|
||||
"system_info_read" => true, // Always allow system info reading
|
||||
_ => true, // Include unknown rule types by default
|
||||
};
|
||||
|
||||
if include_rule {
|
||||
filtered_rules.push(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Always ensure project path access if file reading is enabled
|
||||
if enable_file_read {
|
||||
let has_project_access = filtered_rules.iter().any(|rule| {
|
||||
rule.operation_type == "file_read_all"
|
||||
&& rule.pattern_type == "subpath"
|
||||
&& rule.pattern_value.contains("{{PROJECT_PATH}}")
|
||||
});
|
||||
|
||||
if !has_project_access {
|
||||
// Add a default project access rule
|
||||
filtered_rules.push(SandboxRule {
|
||||
id: None,
|
||||
profile_id: 0,
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: "subpath".to_string(),
|
||||
pattern_value: "{{PROJECT_PATH}}".to_string(),
|
||||
enabled: true,
|
||||
platform_support: None,
|
||||
created_at: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.build_profile_with_serialization(filtered_rules)
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules
|
||||
#[cfg(unix)]
|
||||
pub fn build_profile(&self, rules: Vec<SandboxRule>) -> Result<Profile> {
|
||||
let result = self.build_profile_with_serialization(rules)?;
|
||||
Ok(result.profile)
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules (Windows stub)
|
||||
#[cfg(not(unix))]
|
||||
pub fn build_profile(&self, _rules: Vec<SandboxRule>) -> Result<()> {
|
||||
warn!("Sandbox profiles are not supported on Windows");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a gaol Profile from database rules and return serialized operations
|
||||
pub fn build_profile_with_serialization(
|
||||
&self,
|
||||
rules: Vec<SandboxRule>,
|
||||
) -> Result<ProfileBuildResult> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut operations = Vec::new();
|
||||
let mut serialized_operations = Vec::new();
|
||||
|
||||
for rule in rules {
|
||||
if !rule.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check platform support
|
||||
if !self.is_rule_supported_on_platform(&rule) {
|
||||
debug!(
|
||||
"Skipping rule {} - not supported on current platform",
|
||||
rule.operation_type
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.build_operation_with_serialization(&rule) {
|
||||
Ok(Some((op, serialized))) => {
|
||||
// Check if operation is supported on current platform
|
||||
if matches!(
|
||||
op.support(),
|
||||
gaol::profile::OperationSupportLevel::CanBeAllowed
|
||||
) {
|
||||
operations.push(op);
|
||||
serialized_operations.push(serialized);
|
||||
} else {
|
||||
warn!(
|
||||
"Operation {:?} not supported at desired level on current platform",
|
||||
rule.operation_type
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
debug!(
|
||||
"Skipping unsupported operation type: {}",
|
||||
rule.operation_type
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to build operation for rule {}: {}",
|
||||
rule.id.unwrap_or(0),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure project path access is included
|
||||
let has_project_access = serialized_operations.iter().any(|op| {
|
||||
matches!(op, SerializedOperation::FileReadAll { path, is_subpath: true } if path == &self.project_path)
|
||||
});
|
||||
|
||||
if !has_project_access {
|
||||
operations.push(Operation::FileReadAll(PathPattern::Subpath(
|
||||
self.project_path.clone(),
|
||||
)));
|
||||
serialized_operations.push(SerializedOperation::FileReadAll {
|
||||
path: self.project_path.clone(),
|
||||
is_subpath: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Create the profile
|
||||
let profile = Profile::new(operations)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to create sandbox profile - some operations may not be supported on this platform"))?;
|
||||
|
||||
Ok(ProfileBuildResult {
|
||||
profile,
|
||||
serialized: SerializedProfile {
|
||||
operations: serialized_operations,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On Windows, we just create a serialized profile without actual sandboxing
|
||||
let mut serialized_operations = Vec::new();
|
||||
|
||||
for rule in rules {
|
||||
if !rule.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(Some(serialized)) = self.build_serialized_operation(&rule) {
|
||||
serialized_operations.push(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ProfileBuildResult {
|
||||
profile: (),
|
||||
serialized: SerializedProfile {
|
||||
operations: serialized_operations,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a gaol Operation from a database rule
|
||||
#[cfg(unix)]
|
||||
fn build_operation(&self, rule: &SandboxRule) -> Result<Option<Operation>> {
|
||||
match self.build_operation_with_serialization(rule) {
|
||||
Ok(Some((op, _))) => Ok(Some(op)),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a gaol Operation and its serialized form from a database rule
|
||||
#[cfg(unix)]
|
||||
fn build_operation_with_serialization(
|
||||
&self,
|
||||
rule: &SandboxRule,
|
||||
) -> Result<Option<(Operation, SerializedOperation)>> {
|
||||
match rule.operation_type.as_str() {
|
||||
"file_read_all" => {
|
||||
let (pattern, path, is_subpath) =
|
||||
self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?;
|
||||
Ok(Some((
|
||||
Operation::FileReadAll(pattern),
|
||||
SerializedOperation::FileReadAll { path, is_subpath },
|
||||
)))
|
||||
}
|
||||
"file_read_metadata" => {
|
||||
let (pattern, path, is_subpath) =
|
||||
self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?;
|
||||
Ok(Some((
|
||||
Operation::FileReadMetadata(pattern),
|
||||
SerializedOperation::FileReadMetadata { path, is_subpath },
|
||||
)))
|
||||
}
|
||||
"network_outbound" => {
|
||||
let (pattern, serialized) = self.build_address_pattern_with_serialization(
|
||||
&rule.pattern_type,
|
||||
&rule.pattern_value,
|
||||
)?;
|
||||
Ok(Some((Operation::NetworkOutbound(pattern), serialized)))
|
||||
}
|
||||
"system_info_read" => Ok(Some((
|
||||
Operation::SystemInfoRead,
|
||||
SerializedOperation::SystemInfoRead,
|
||||
))),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a PathPattern from pattern type and value
|
||||
#[cfg(unix)]
|
||||
fn build_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result<PathPattern> {
|
||||
let (pattern, _, _) = self.build_path_pattern_with_info(pattern_type, pattern_value)?;
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
/// Build a PathPattern and return additional info for serialization
|
||||
#[cfg(unix)]
|
||||
fn build_path_pattern_with_info(
|
||||
&self,
|
||||
pattern_type: &str,
|
||||
pattern_value: &str,
|
||||
) -> Result<(PathPattern, PathBuf, bool)> {
|
||||
// Replace template variables
|
||||
let expanded_value = pattern_value
|
||||
.replace("{{PROJECT_PATH}}", &self.project_path.to_string_lossy())
|
||||
.replace("{{HOME}}", &self.home_dir.to_string_lossy());
|
||||
|
||||
let path = PathBuf::from(expanded_value);
|
||||
|
||||
match pattern_type {
|
||||
"literal" => Ok((PathPattern::Literal(path.clone()), path, false)),
|
||||
"subpath" => Ok((PathPattern::Subpath(path.clone()), path, true)),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Unknown path pattern type: {}",
|
||||
pattern_type
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an AddressPattern from pattern type and value
|
||||
#[cfg(unix)]
|
||||
fn build_address_pattern(
|
||||
&self,
|
||||
pattern_type: &str,
|
||||
pattern_value: &str,
|
||||
) -> Result<AddressPattern> {
|
||||
let (pattern, _) =
|
||||
self.build_address_pattern_with_serialization(pattern_type, pattern_value)?;
|
||||
Ok(pattern)
|
||||
}
|
||||
|
||||
/// Build an AddressPattern and its serialized form
|
||||
#[cfg(unix)]
|
||||
fn build_address_pattern_with_serialization(
|
||||
&self,
|
||||
pattern_type: &str,
|
||||
pattern_value: &str,
|
||||
) -> Result<(AddressPattern, SerializedOperation)> {
|
||||
match pattern_type {
|
||||
"all" => Ok((
|
||||
AddressPattern::All,
|
||||
SerializedOperation::NetworkOutbound {
|
||||
pattern: "all".to_string(),
|
||||
},
|
||||
)),
|
||||
"tcp" => {
|
||||
let port = pattern_value
|
||||
.parse::<u16>()
|
||||
.context("Invalid TCP port number")?;
|
||||
Ok((
|
||||
AddressPattern::Tcp(port),
|
||||
SerializedOperation::NetworkTcp { port },
|
||||
))
|
||||
}
|
||||
"local_socket" => {
|
||||
let path = PathBuf::from(pattern_value);
|
||||
Ok((
|
||||
AddressPattern::LocalSocket(path.clone()),
|
||||
SerializedOperation::NetworkLocalSocket { path },
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Unknown address pattern type: {}",
|
||||
pattern_type
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a rule is supported on the current platform
|
||||
fn is_rule_supported_on_platform(&self, rule: &SandboxRule) -> bool {
|
||||
if let Some(platforms_json) = &rule.platform_support {
|
||||
if let Ok(platforms) = serde_json::from_str::<Vec<String>>(platforms_json) {
|
||||
let current_os = std::env::consts::OS;
|
||||
return platforms.contains(¤t_os.to_string());
|
||||
}
|
||||
}
|
||||
// If no platform support specified, assume it's supported
|
||||
true
|
||||
}
|
||||
|
||||
/// Build only the serialized operation (for Windows)
|
||||
#[cfg(not(unix))]
|
||||
fn build_serialized_operation(
|
||||
&self,
|
||||
rule: &SandboxRule,
|
||||
) -> Result<Option<SerializedOperation>> {
|
||||
let pattern_value = self.expand_pattern_value(&rule.pattern_value);
|
||||
|
||||
match rule.operation_type.as_str() {
|
||||
"file_read_all" => {
|
||||
let (path, is_subpath) =
|
||||
self.parse_path_pattern(&rule.pattern_type, &pattern_value)?;
|
||||
Ok(Some(SerializedOperation::FileReadAll { path, is_subpath }))
|
||||
}
|
||||
"file_read_metadata" => {
|
||||
let (path, is_subpath) =
|
||||
self.parse_path_pattern(&rule.pattern_type, &pattern_value)?;
|
||||
Ok(Some(SerializedOperation::FileReadMetadata {
|
||||
path,
|
||||
is_subpath,
|
||||
}))
|
||||
}
|
||||
"network_outbound" => Ok(Some(SerializedOperation::NetworkOutbound {
|
||||
pattern: pattern_value,
|
||||
})),
|
||||
"network_tcp" => {
|
||||
let port = pattern_value.parse::<u16>().context("Invalid TCP port")?;
|
||||
Ok(Some(SerializedOperation::NetworkTcp { port }))
|
||||
}
|
||||
"network_local_socket" => {
|
||||
let path = PathBuf::from(pattern_value);
|
||||
Ok(Some(SerializedOperation::NetworkLocalSocket { path }))
|
||||
}
|
||||
"system_info_read" => Ok(Some(SerializedOperation::SystemInfoRead)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to expand pattern values (Windows version)
|
||||
#[cfg(not(unix))]
|
||||
fn expand_pattern_value(&self, pattern_value: &str) -> String {
|
||||
pattern_value
|
||||
.replace("{{PROJECT_PATH}}", &self.project_path.to_string_lossy())
|
||||
.replace("{{HOME}}", &self.home_dir.to_string_lossy())
|
||||
}
|
||||
|
||||
/// Helper method to parse path patterns (Windows version)
|
||||
#[cfg(not(unix))]
|
||||
fn parse_path_pattern(
|
||||
&self,
|
||||
pattern_type: &str,
|
||||
pattern_value: &str,
|
||||
) -> Result<(PathBuf, bool)> {
|
||||
let path = PathBuf::from(pattern_value);
|
||||
|
||||
match pattern_type {
|
||||
"literal" => Ok((path, false)),
|
||||
"subpath" => Ok((path, true)),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Unknown path pattern type: {}",
|
||||
pattern_type
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a sandbox profile by ID
|
||||
pub fn load_profile(conn: &Connection, profile_id: i64) -> Result<SandboxProfile> {
|
||||
conn.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at
|
||||
FROM sandbox_profiles WHERE id = ?1",
|
||||
params![profile_id],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.context("Failed to load sandbox profile")
|
||||
}
|
||||
|
||||
/// Load the default sandbox profile
|
||||
pub fn load_default_profile(conn: &Connection) -> Result<SandboxProfile> {
|
||||
conn.query_row(
|
||||
"SELECT id, name, description, is_active, is_default, created_at, updated_at
|
||||
FROM sandbox_profiles WHERE is_default = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(SandboxProfile {
|
||||
id: Some(row.get(0)?),
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
is_active: row.get(3)?,
|
||||
is_default: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
updated_at: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.context("Failed to load default sandbox profile")
|
||||
}
|
||||
|
||||
/// Load rules for a sandbox profile
|
||||
pub fn load_profile_rules(conn: &Connection, profile_id: i64) -> Result<Vec<SandboxRule>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at
|
||||
FROM sandbox_rules WHERE profile_id = ?1 AND enabled = 1"
|
||||
)?;
|
||||
|
||||
let rules = stmt
|
||||
.query_map(params![profile_id], |row| {
|
||||
Ok(SandboxRule {
|
||||
id: Some(row.get(0)?),
|
||||
profile_id: row.get(1)?,
|
||||
operation_type: row.get(2)?,
|
||||
pattern_type: row.get(3)?,
|
||||
pattern_value: row.get(4)?,
|
||||
enabled: row.get(5)?,
|
||||
platform_support: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
/// Get or create the gaol Profile for execution
|
||||
#[cfg(unix)]
|
||||
pub fn get_gaol_profile(
|
||||
conn: &Connection,
|
||||
profile_id: Option<i64>,
|
||||
project_path: PathBuf,
|
||||
) -> Result<Profile> {
|
||||
// Load the profile
|
||||
let profile = if let Some(id) = profile_id {
|
||||
load_profile(conn, id)?
|
||||
} else {
|
||||
load_default_profile(conn)?
|
||||
};
|
||||
|
||||
info!("Using sandbox profile: {}", profile.name);
|
||||
|
||||
// Load the rules
|
||||
let rules = load_profile_rules(conn, profile.id.unwrap())?;
|
||||
info!("Loaded {} sandbox rules", rules.len());
|
||||
|
||||
// Build the gaol profile
|
||||
let builder = ProfileBuilder::new(project_path)?;
|
||||
builder.build_profile(rules)
|
||||
}
|
||||
|
||||
/// Get or create the gaol Profile for execution (Windows stub)
|
||||
#[cfg(not(unix))]
|
||||
pub fn get_gaol_profile(
|
||||
_conn: &Connection,
|
||||
_profile_id: Option<i64>,
|
||||
_project_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
warn!("Sandbox profiles are not supported on Windows");
|
||||
Ok(())
|
||||
}
|
@@ -1,143 +0,0 @@
|
||||
# Sandbox Test Suite Summary
|
||||
|
||||
## Overview
|
||||
|
||||
A comprehensive test suite has been created for the sandbox functionality in Claudia. The test suite validates that the sandboxing operations using the `gaol` crate work correctly across different platforms (Linux, macOS, FreeBSD).
|
||||
|
||||
## Test Structure Created
|
||||
|
||||
### 1. **Test Organization** (`tests/sandbox_tests.rs`)
|
||||
- Main entry point for all sandbox tests
|
||||
- Integrates all test modules
|
||||
|
||||
### 2. **Common Test Utilities** (`tests/sandbox/common/`)
|
||||
- **fixtures.rs**: Test data, database setup, file system creation, and standard profiles
|
||||
- **helpers.rs**: Helper functions, platform detection, test command execution, and code generation
|
||||
|
||||
### 3. **Unit Tests** (`tests/sandbox/unit/`)
|
||||
- **profile_builder.rs**: Tests for ProfileBuilder including rule parsing, platform filtering, and template expansion
|
||||
- **platform.rs**: Tests for platform capability detection and operation support levels
|
||||
- **executor.rs**: Tests for SandboxExecutor creation and command preparation
|
||||
|
||||
### 4. **Integration Tests** (`tests/sandbox/integration/`)
|
||||
- **file_operations.rs**: Tests file access control (allowed/forbidden reads, writes, metadata)
|
||||
- **network_operations.rs**: Tests network access control (TCP, local sockets, port filtering)
|
||||
- **system_info.rs**: Tests system information access (platform-specific)
|
||||
- **process_isolation.rs**: Tests process spawning restrictions (fork, exec, threads)
|
||||
- **violations.rs**: Tests violation detection and patterns
|
||||
|
||||
### 5. **End-to-End Tests** (`tests/sandbox/e2e/`)
|
||||
- **agent_sandbox.rs**: Tests agent execution with sandbox profiles
|
||||
- **claude_sandbox.rs**: Tests Claude command execution with sandboxing
|
||||
|
||||
## Key Features
|
||||
|
||||
### Platform Support
|
||||
- **Cross-platform testing**: Tests adapt to platform capabilities
|
||||
- **Skip unsupported**: Tests gracefully skip on unsupported platforms
|
||||
- **Platform-specific tests**: Special tests for platform-specific features
|
||||
|
||||
### Test Helpers
|
||||
- **Test binary creation**: Dynamically compiles test programs
|
||||
- **Mock file systems**: Creates temporary test environments
|
||||
- **Database fixtures**: Sets up test databases with profiles
|
||||
- **Assertion helpers**: Specialized assertions for sandbox behavior
|
||||
|
||||
### Safety Features
|
||||
- **Serial execution**: Tests run serially to avoid conflicts
|
||||
- **Timeout handling**: Commands have timeout protection
|
||||
- **Resource cleanup**: Temporary files and resources are cleaned up
|
||||
|
||||
## Running the Tests
|
||||
|
||||
```bash
|
||||
# Run all sandbox tests
|
||||
cargo test --test sandbox_tests
|
||||
|
||||
# Run specific categories
|
||||
cargo test --test sandbox_tests unit::
|
||||
cargo test --test sandbox_tests integration::
|
||||
cargo test --test sandbox_tests e2e:: -- --ignored
|
||||
|
||||
# Run with output
|
||||
cargo test --test sandbox_tests -- --nocapture
|
||||
|
||||
# Run serially (required for some tests)
|
||||
cargo test --test sandbox_tests -- --test-threads=1
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite covers:
|
||||
|
||||
1. **Profile Management**
|
||||
- Profile creation and validation
|
||||
- Rule parsing and conflicts
|
||||
- Template variable expansion
|
||||
- Platform compatibility
|
||||
|
||||
2. **File Operations**
|
||||
- Allowed file reads
|
||||
- Forbidden file access
|
||||
- File write prevention
|
||||
- Metadata operations
|
||||
|
||||
3. **Network Operations**
|
||||
- Network access control
|
||||
- Port-specific rules (macOS)
|
||||
- Local socket connections
|
||||
|
||||
4. **Process Isolation**
|
||||
- Process spawn prevention
|
||||
- Fork/exec blocking
|
||||
- Thread creation (allowed)
|
||||
|
||||
5. **System Information**
|
||||
- Platform-specific access control
|
||||
- macOS sysctl operations
|
||||
|
||||
6. **Violation Tracking**
|
||||
- Violation detection
|
||||
- Pattern matching
|
||||
- Multiple violations
|
||||
|
||||
## Platform-Specific Behavior
|
||||
|
||||
| Feature | Linux | macOS | FreeBSD |
|
||||
|---------|-------|-------|---------|
|
||||
| File Read Control | ✅ | ✅ | ❌ |
|
||||
| Metadata Read | 🟡¹ | ✅ | ❌ |
|
||||
| Network All | ✅ | ✅ | ❌ |
|
||||
| Network TCP Port | ❌ | ✅ | ❌ |
|
||||
| Network Local Socket | ❌ | ✅ | ❌ |
|
||||
| System Info Read | ❌ | ✅ | ✅² |
|
||||
|
||||
¹ Cannot be precisely controlled on Linux
|
||||
² Always allowed on FreeBSD
|
||||
|
||||
## Dependencies Added
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
serial_test = "3"
|
||||
test-case = "3"
|
||||
once_cell = "1"
|
||||
proptest = "1"
|
||||
pretty_assertions = "1"
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **CI Integration**: Configure CI to run sandbox tests on multiple platforms
|
||||
2. **Performance Tests**: Add benchmarks for sandbox overhead
|
||||
3. **Stress Tests**: Test with many simultaneous sandboxed processes
|
||||
4. **Mock Claude**: Create mock Claude command for E2E tests without dependencies
|
||||
5. **Coverage Report**: Generate test coverage reports
|
||||
|
||||
## Notes
|
||||
|
||||
- Some E2E tests are marked `#[ignore]` as they require Claude to be installed
|
||||
- Integration tests use `serial_test` to prevent conflicts
|
||||
- Test binaries are compiled on-demand for realistic testing
|
||||
- The test suite gracefully handles platform limitations
|
@@ -21,7 +21,7 @@ test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
|
||||
### Implementation Details:
|
||||
|
||||
#### Real Claude Execution (`tests/sandbox/common/claude_real.rs`):
|
||||
#### Real Claude Execution:
|
||||
- `execute_claude_task()` - Executes Claude with specified task and captures output
|
||||
- Supports timeout handling (gtimeout on macOS, timeout on Linux)
|
||||
- Returns structured output with stdout, stderr, exit code, and duration
|
||||
@@ -33,18 +33,18 @@ test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
- 20-second timeout to allow Claude sufficient time to respond
|
||||
|
||||
#### Key Test Updates:
|
||||
1. **Agent Tests** (`agent_sandbox.rs`):
|
||||
- `test_agent_with_minimal_profile` - Tests with minimal sandbox permissions
|
||||
- `test_agent_with_standard_profile` - Tests with standard permissions
|
||||
- `test_agent_without_sandbox` - Control test without sandbox
|
||||
1. **Agent Tests**:
|
||||
- Test agent execution with various permission configurations
|
||||
- Test agent execution in different project contexts
|
||||
- Control tests for baseline behavior
|
||||
|
||||
2. **Claude Sandbox Tests** (`claude_sandbox.rs`):
|
||||
- `test_claude_with_default_sandbox` - Tests default sandbox profile
|
||||
- `test_claude_sandbox_disabled` - Tests with inactive sandbox
|
||||
2. **Claude Tests**:
|
||||
- Test Claude execution with default settings
|
||||
- Test Claude execution with custom configurations
|
||||
|
||||
### Benefits of Real Claude Testing:
|
||||
- **Authenticity**: Tests validate actual Claude behavior, not mocked responses
|
||||
- **Integration**: Ensures the sandbox system works with real Claude execution
|
||||
- **Integration**: Ensures the system works with real Claude execution
|
||||
- **End-to-End**: Complete validation from command invocation to output parsing
|
||||
- **No External Dependencies**: Uses `--dangerously-skip-permissions` flag
|
||||
|
||||
@@ -53,6 +53,6 @@ test result: ok. 58 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
- No ignored tests
|
||||
- No TODOs in test code
|
||||
- Clean compilation with no warnings
|
||||
- Platform-aware sandbox expectations (Linux vs macOS)
|
||||
- Platform-aware expectations for different operating systems
|
||||
|
||||
The test suite now provides comprehensive end-to-end validation with actual Claude execution.
|
||||
The test suite now provides comprehensive end-to-end validation with actual Claude execution.
|
||||
|
@@ -13,7 +13,7 @@
|
||||
- Created `create_test_binary_with_deps` function
|
||||
|
||||
3. **Fixed Database Schema Issue** ✅
|
||||
- Added missing tables (agents, agent_runs, sandbox_violations) to test database
|
||||
- Added missing tables (agents, agent_runs) to test database
|
||||
- Fixed foreign key constraint issues
|
||||
|
||||
4. **Fixed Mutex Poisoning** ✅
|
||||
@@ -35,8 +35,8 @@
|
||||
7. **Removed All TODOs** ✅
|
||||
- No TODOs remain in test code
|
||||
|
||||
8. **Handled Platform-Specific Sandbox Limitations** ✅
|
||||
- Tests properly handle macOS sandbox limitations
|
||||
8. **Handled Platform-Specific Limitations** ✅
|
||||
- Tests properly handle platform-specific differences
|
||||
- Platform-aware assertions prevent false failures
|
||||
|
||||
## Test Results:
|
||||
@@ -52,4 +52,4 @@ test result: ok. 61 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
- Comprehensive mock system for external dependencies
|
||||
- Platform-aware testing for cross-platform compatibility
|
||||
|
||||
The test suite is now production-ready with full coverage and no issues.
|
||||
The test suite is now production-ready with full coverage and no issues.
|
||||
|
@@ -1,155 +0,0 @@
|
||||
# Sandbox Test Suite
|
||||
|
||||
This directory contains a comprehensive test suite for the sandbox functionality in Claudia. The tests are designed to verify that the sandboxing operations work correctly across different platforms (Linux, macOS, FreeBSD).
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
sandbox/
|
||||
├── common/ # Shared test utilities
|
||||
│ ├── fixtures.rs # Test data and environment setup
|
||||
│ └── helpers.rs # Helper functions and assertions
|
||||
├── unit/ # Unit tests for individual components
|
||||
│ ├── profile_builder.rs # ProfileBuilder tests
|
||||
│ ├── platform.rs # Platform capability tests
|
||||
│ └── executor.rs # SandboxExecutor tests
|
||||
├── integration/ # Integration tests for sandbox operations
|
||||
│ ├── file_operations.rs # File access control tests
|
||||
│ ├── network_operations.rs # Network access control tests
|
||||
│ ├── system_info.rs # System info access tests
|
||||
│ ├── process_isolation.rs # Process spawning tests
|
||||
│ └── violations.rs # Violation detection tests
|
||||
└── e2e/ # End-to-end tests
|
||||
├── agent_sandbox.rs # Agent execution with sandbox
|
||||
└── claude_sandbox.rs # Claude command with sandbox
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run all sandbox tests:
|
||||
```bash
|
||||
cargo test --test sandbox_tests
|
||||
```
|
||||
|
||||
### Run specific test categories:
|
||||
```bash
|
||||
# Unit tests only
|
||||
cargo test --test sandbox_tests unit::
|
||||
|
||||
# Integration tests only
|
||||
cargo test --test sandbox_tests integration::
|
||||
|
||||
# End-to-end tests only (requires Claude to be installed)
|
||||
cargo test --test sandbox_tests e2e:: -- --ignored
|
||||
```
|
||||
|
||||
### Run tests with output:
|
||||
```bash
|
||||
cargo test --test sandbox_tests -- --nocapture
|
||||
```
|
||||
|
||||
### Run tests serially (required for some integration tests):
|
||||
```bash
|
||||
cargo test --test sandbox_tests -- --test-threads=1
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **ProfileBuilder Tests** (`unit/profile_builder.rs`)
|
||||
- Profile creation and validation
|
||||
- Rule parsing and platform filtering
|
||||
- Template variable expansion
|
||||
- Invalid operation handling
|
||||
|
||||
2. **Platform Tests** (`unit/platform.rs`)
|
||||
- Platform capability detection
|
||||
- Operation support levels
|
||||
- Cross-platform compatibility
|
||||
|
||||
3. **Executor Tests** (`unit/executor.rs`)
|
||||
- Sandbox executor creation
|
||||
- Command preparation
|
||||
- Environment variable handling
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **File Operations** (`integration/file_operations.rs`)
|
||||
- ✅ Allowed file reads succeed
|
||||
- ❌ Forbidden file reads fail
|
||||
- ❌ File writes always fail
|
||||
- 📊 Metadata operations respect permissions
|
||||
- 🔄 Template variable expansion works
|
||||
|
||||
2. **Network Operations** (`integration/network_operations.rs`)
|
||||
- ✅ Allowed network connections succeed
|
||||
- ❌ Forbidden network connections fail
|
||||
- 🎯 Port-specific rules (macOS only)
|
||||
- 🔌 Local socket connections
|
||||
|
||||
3. **System Information** (`integration/system_info.rs`)
|
||||
- 🍎 macOS: Can be allowed/forbidden
|
||||
- 🐧 Linux: Never allowed
|
||||
- 👹 FreeBSD: Always allowed
|
||||
|
||||
4. **Process Isolation** (`integration/process_isolation.rs`)
|
||||
- ❌ Process spawning forbidden
|
||||
- ❌ Fork/exec operations blocked
|
||||
- ✅ Thread creation allowed
|
||||
|
||||
5. **Violations** (`integration/violations.rs`)
|
||||
- 🚨 Violation detection
|
||||
- 📝 Violation patterns
|
||||
- 🔢 Multiple violations handling
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
1. **Agent Sandbox** (`e2e/agent_sandbox.rs`)
|
||||
- Agent execution with profiles
|
||||
- Profile switching
|
||||
- Violation logging
|
||||
|
||||
2. **Claude Sandbox** (`e2e/claude_sandbox.rs`)
|
||||
- Claude command sandboxing
|
||||
- Settings integration
|
||||
- Session management
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Feature | Linux | macOS | FreeBSD |
|
||||
|---------|-------|-------|---------|
|
||||
| File Read Control | ✅ | ✅ | ❌ |
|
||||
| Metadata Read | 🟡¹ | ✅ | ❌ |
|
||||
| Network All | ✅ | ✅ | ❌ |
|
||||
| Network TCP Port | ❌ | ✅ | ❌ |
|
||||
| Network Local Socket | ❌ | ✅ | ❌ |
|
||||
| System Info Read | ❌ | ✅ | ✅² |
|
||||
|
||||
¹ Cannot be precisely controlled on Linux (allowed if file read is allowed)
|
||||
² Always allowed on FreeBSD (cannot be restricted)
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Serial Execution**: Many integration tests are marked with `#[serial]` and must run one at a time to avoid conflicts.
|
||||
|
||||
2. **Platform Dependencies**: Some tests will be skipped on unsupported platforms. The test suite handles this gracefully.
|
||||
|
||||
3. **Privilege Requirements**: Sandbox tests generally don't require elevated privileges, but some operations may fail in restricted environments (e.g., CI).
|
||||
|
||||
4. **Claude Dependency**: E2E tests that actually execute Claude are marked with `#[ignore]` by default. Run with `--ignored` flag when Claude is installed.
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
1. **Enable Logging**: Set `RUST_LOG=debug` to see detailed sandbox operations
|
||||
2. **Check Platform**: Verify the test is supported on your platform
|
||||
3. **Check Permissions**: Ensure test binaries can be created and executed
|
||||
4. **Inspect Output**: Use `--nocapture` to see all test output
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
1. Choose the appropriate category (unit/integration/e2e)
|
||||
2. Use the test helpers from `common/`
|
||||
3. Mark with `#[serial]` if the test modifies global state
|
||||
4. Use `skip_if_unsupported!()` macro for platform-specific tests
|
||||
5. Document any special requirements or limitations
|
@@ -1,187 +0,0 @@
|
||||
//! Helper functions for executing real Claude commands in tests
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Execute Claude with a specific task and capture output
|
||||
pub fn execute_claude_task(
|
||||
project_path: &Path,
|
||||
task: &str,
|
||||
system_prompt: Option<&str>,
|
||||
model: Option<&str>,
|
||||
sandbox_profile_id: Option<i64>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<ClaudeOutput> {
|
||||
let mut cmd = Command::new("claude");
|
||||
|
||||
// Add task
|
||||
cmd.arg("-p").arg(task);
|
||||
|
||||
// Add system prompt if provided
|
||||
if let Some(prompt) = system_prompt {
|
||||
cmd.arg("--system-prompt").arg(prompt);
|
||||
}
|
||||
|
||||
// Add model if provided
|
||||
if let Some(m) = model {
|
||||
cmd.arg("--model").arg(m);
|
||||
}
|
||||
|
||||
// Always add these flags for testing
|
||||
cmd.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Add sandbox profile ID if provided
|
||||
if let Some(profile_id) = sandbox_profile_id {
|
||||
cmd.env("CLAUDIA_SANDBOX_PROFILE_ID", profile_id.to_string());
|
||||
}
|
||||
|
||||
// Execute with timeout (use gtimeout on macOS, timeout on Linux)
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let timeout_cmd = if cfg!(target_os = "macos") {
|
||||
// On macOS, try gtimeout (from GNU coreutils) first, fallback to direct execution
|
||||
if std::process::Command::new("which")
|
||||
.arg("gtimeout")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"gtimeout"
|
||||
} else {
|
||||
// If gtimeout not available, just run without timeout
|
||||
""
|
||||
}
|
||||
} else {
|
||||
"timeout"
|
||||
};
|
||||
|
||||
let output = if timeout_cmd.is_empty() {
|
||||
// Run without timeout wrapper
|
||||
cmd.output().context("Failed to execute Claude command")?
|
||||
} else {
|
||||
// Run with timeout wrapper
|
||||
let mut timeout_cmd = Command::new(timeout_cmd);
|
||||
timeout_cmd
|
||||
.arg(timeout_secs.to_string())
|
||||
.arg("claude")
|
||||
.args(cmd.get_args())
|
||||
.current_dir(project_path)
|
||||
.envs(cmd.get_envs().filter_map(|(k, v)| v.map(|v| (k, v))))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.context("Failed to execute Claude command with timeout")?
|
||||
};
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
Ok(ClaudeOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
exit_code: output.status.code().unwrap_or(-1),
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of Claude execution
|
||||
#[derive(Debug)]
|
||||
pub struct ClaudeOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: i32,
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
impl ClaudeOutput {
|
||||
/// Check if the output contains evidence of a specific operation
|
||||
pub fn contains_operation(&self, operation: &str) -> bool {
|
||||
self.stdout.contains(operation) || self.stderr.contains(operation)
|
||||
}
|
||||
|
||||
/// Check if operation was blocked (look for permission denied, sandbox violation, etc)
|
||||
pub fn operation_was_blocked(&self, operation: &str) -> bool {
|
||||
let blocked_patterns = [
|
||||
"permission denied",
|
||||
"not permitted",
|
||||
"blocked by sandbox",
|
||||
"operation not allowed",
|
||||
"access denied",
|
||||
"sandbox violation",
|
||||
];
|
||||
|
||||
let output = format!("{}\n{}", self.stdout, self.stderr).to_lowercase();
|
||||
let op_lower = operation.to_lowercase();
|
||||
|
||||
// Check if operation was mentioned along with a block pattern
|
||||
blocked_patterns
|
||||
.iter()
|
||||
.any(|pattern| output.contains(&op_lower) && output.contains(pattern))
|
||||
}
|
||||
|
||||
/// Check if file read was successful
|
||||
pub fn file_read_succeeded(&self, filename: &str) -> bool {
|
||||
// Look for patterns indicating successful file read
|
||||
let patterns = [
|
||||
&format!("Read {}", filename),
|
||||
&format!("Reading {}", filename),
|
||||
&format!("Contents of {}", filename),
|
||||
"test content", // Our test files contain this
|
||||
];
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
|
||||
/// Check if network connection was attempted
|
||||
pub fn network_attempted(&self, host: &str) -> bool {
|
||||
let patterns = [
|
||||
&format!("Connecting to {}", host),
|
||||
&format!("Connected to {}", host),
|
||||
&format!("connect to {}", host),
|
||||
host,
|
||||
];
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
/// Common test tasks for Claude
|
||||
pub mod tasks {
|
||||
/// Task to read a file
|
||||
pub fn read_file(filename: &str) -> String {
|
||||
format!("Read the file {} and show me its contents", filename)
|
||||
}
|
||||
|
||||
/// Task to attempt network connection
|
||||
pub fn connect_network(host: &str) -> String {
|
||||
format!("Try to connect to {} and tell me if it works", host)
|
||||
}
|
||||
|
||||
/// Task to do multiple operations
|
||||
pub fn multi_operation() -> String {
|
||||
"Read the file ./test.txt in the current directory and show its contents".to_string()
|
||||
}
|
||||
|
||||
/// Task to test file write
|
||||
pub fn write_file(filename: &str, content: &str) -> String {
|
||||
format!(
|
||||
"Create a file called {} with the content '{}'",
|
||||
filename, content
|
||||
)
|
||||
}
|
||||
|
||||
/// Task to test process spawning
|
||||
pub fn spawn_process(command: &str) -> String {
|
||||
format!("Run the command '{}' and show me the output", command)
|
||||
}
|
||||
}
|
@@ -1,332 +0,0 @@
|
||||
//! Test fixtures and data for sandbox testing
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::PathBuf;
|
||||
// Removed std::sync::Mutex - using parking_lot::Mutex instead
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
/// Global test database for sandbox testing
|
||||
/// Using parking_lot::Mutex which doesn't poison on panic
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub static TEST_DB: Lazy<Mutex<TestDatabase>> =
|
||||
Lazy::new(|| Mutex::new(TestDatabase::new().expect("Failed to create test database")));
|
||||
|
||||
/// Test database manager
|
||||
pub struct TestDatabase {
|
||||
pub conn: Connection,
|
||||
pub temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestDatabase {
|
||||
/// Create a new test database with schema
|
||||
pub fn new() -> Result<Self> {
|
||||
let temp_dir = tempdir()?;
|
||||
let db_path = temp_dir.path().join("test_sandbox.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
// Initialize schema
|
||||
Self::init_schema(&conn)?;
|
||||
|
||||
Ok(Self { conn, temp_dir })
|
||||
}
|
||||
|
||||
/// Initialize database schema
|
||||
fn init_schema(conn: &Connection) -> Result<()> {
|
||||
// Create sandbox profiles table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 0,
|
||||
is_default BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox rules table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER NOT NULL,
|
||||
operation_type TEXT NOT NULL,
|
||||
pattern_type TEXT NOT NULL,
|
||||
pattern_value TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
platform_support TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create agents table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
system_prompt TEXT NOT NULL,
|
||||
default_task TEXT,
|
||||
model TEXT NOT NULL DEFAULT 'sonnet',
|
||||
sandbox_profile_id INTEGER REFERENCES sandbox_profiles(id),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create agent_runs table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agent_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id INTEGER NOT NULL,
|
||||
agent_name TEXT NOT NULL,
|
||||
agent_icon TEXT NOT NULL,
|
||||
task TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
project_path TEXT NOT NULL,
|
||||
output TEXT NOT NULL DEFAULT '',
|
||||
duration_ms INTEGER,
|
||||
total_tokens INTEGER,
|
||||
cost_usd REAL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TEXT,
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox violations table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_violations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER,
|
||||
agent_id INTEGER,
|
||||
agent_run_id INTEGER,
|
||||
operation_type TEXT NOT NULL,
|
||||
pattern_value TEXT,
|
||||
process_name TEXT,
|
||||
pid INTEGER,
|
||||
denied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (agent_run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create trigger to update the updated_at timestamp for agents
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_agent_timestamp
|
||||
AFTER UPDATE ON agents
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE agents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create trigger to update sandbox profile timestamp
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_sandbox_profile_timestamp
|
||||
AFTER UPDATE ON sandbox_profiles
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE sandbox_profiles SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a test profile with rules
|
||||
pub fn create_test_profile(&self, name: &str, rules: Vec<TestRule>) -> Result<i64> {
|
||||
// Insert profile
|
||||
self.conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![name, format!("Test profile: {name}"), true, false],
|
||||
)?;
|
||||
|
||||
let profile_id = self.conn.last_insert_rowid();
|
||||
|
||||
// Insert rules
|
||||
for rule in rules {
|
||||
self.conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
profile_id,
|
||||
rule.operation_type,
|
||||
rule.pattern_type,
|
||||
rule.pattern_value,
|
||||
rule.enabled,
|
||||
rule.platform_support
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(profile_id)
|
||||
}
|
||||
|
||||
/// Reset database to clean state
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
// Delete in the correct order to respect foreign key constraints
|
||||
self.conn.execute("DELETE FROM sandbox_violations", [])?;
|
||||
self.conn.execute("DELETE FROM agent_runs", [])?;
|
||||
self.conn.execute("DELETE FROM agents", [])?;
|
||||
self.conn.execute("DELETE FROM sandbox_rules", [])?;
|
||||
self.conn.execute("DELETE FROM sandbox_profiles", [])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Test rule structure
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestRule {
|
||||
pub operation_type: String,
|
||||
pub pattern_type: String,
|
||||
pub pattern_value: String,
|
||||
pub enabled: bool,
|
||||
pub platform_support: Option<String>,
|
||||
}
|
||||
|
||||
impl TestRule {
|
||||
/// Create a file read rule
|
||||
pub fn file_read(path: &str, subpath: bool) -> Self {
|
||||
Self {
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: if subpath { "subpath" } else { "literal" }.to_string(),
|
||||
pattern_value: path.to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network rule
|
||||
pub fn network_all() -> Self {
|
||||
Self {
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: String::new(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network TCP rule
|
||||
pub fn network_tcp(port: u16) -> Self {
|
||||
Self {
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "tcp".to_string(),
|
||||
pattern_value: port.to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a system info read rule
|
||||
pub fn system_info_read() -> Self {
|
||||
Self {
|
||||
operation_type: "system_info_read".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: String::new(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file system structure
|
||||
pub struct TestFileSystem {
|
||||
pub root: TempDir,
|
||||
pub project_path: PathBuf,
|
||||
pub allowed_path: PathBuf,
|
||||
pub forbidden_path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestFileSystem {
|
||||
/// Create a new test file system with predefined structure
|
||||
pub fn new() -> Result<Self> {
|
||||
let root = tempdir()?;
|
||||
let root_path = root.path();
|
||||
|
||||
// Create project directory
|
||||
let project_path = root_path.join("test_project");
|
||||
std::fs::create_dir_all(&project_path)?;
|
||||
|
||||
// Create allowed directory
|
||||
let allowed_path = root_path.join("allowed");
|
||||
std::fs::create_dir_all(&allowed_path)?;
|
||||
std::fs::write(allowed_path.join("test.txt"), "allowed content")?;
|
||||
|
||||
// Create forbidden directory
|
||||
let forbidden_path = root_path.join("forbidden");
|
||||
std::fs::create_dir_all(&forbidden_path)?;
|
||||
std::fs::write(forbidden_path.join("secret.txt"), "forbidden content")?;
|
||||
|
||||
// Create project files
|
||||
std::fs::write(project_path.join("main.rs"), "fn main() {}")?;
|
||||
std::fs::write(
|
||||
project_path.join("Cargo.toml"),
|
||||
"[package]\nname = \"test\"",
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
project_path,
|
||||
allowed_path,
|
||||
forbidden_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard test profiles
|
||||
pub mod profiles {
|
||||
use super::*;
|
||||
|
||||
/// Minimal profile - only project access
|
||||
pub fn minimal(project_path: &str) -> Vec<TestRule> {
|
||||
vec![TestRule::file_read(project_path, true)]
|
||||
}
|
||||
|
||||
/// Standard profile - project + system libraries
|
||||
pub fn standard(project_path: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
TestRule::file_read("/usr/lib", true),
|
||||
TestRule::file_read("/usr/local/lib", true),
|
||||
TestRule::network_all(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Development profile - more permissive
|
||||
pub fn development(project_path: &str, home_dir: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
TestRule::file_read("/usr", true),
|
||||
TestRule::file_read("/opt", true),
|
||||
TestRule::file_read(home_dir, true),
|
||||
TestRule::network_all(),
|
||||
TestRule::system_info_read(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Network-only profile
|
||||
pub fn network_only() -> Vec<TestRule> {
|
||||
vec![TestRule::network_all()]
|
||||
}
|
||||
|
||||
/// File-only profile
|
||||
pub fn file_only(paths: Vec<&str>) -> Vec<TestRule> {
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|path| TestRule::file_read(path, true))
|
||||
.collect()
|
||||
}
|
||||
}
|
@@ -1,483 +0,0 @@
|
||||
//! Helper functions for sandbox testing
|
||||
use anyhow::{Context, Result};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Check if sandboxing is supported on the current platform
|
||||
pub fn is_sandboxing_supported() -> bool {
|
||||
matches!(env::consts::OS, "linux" | "macos" | "freebsd")
|
||||
}
|
||||
|
||||
/// Skip test if sandboxing is not supported
|
||||
#[macro_export]
|
||||
macro_rules! skip_if_unsupported {
|
||||
() => {
|
||||
if !$crate::sandbox::common::is_sandboxing_supported() {
|
||||
eprintln!(
|
||||
"Skipping test: sandboxing not supported on {}",
|
||||
std::env::consts::OS
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Platform-specific test configuration
|
||||
pub struct PlatformConfig {
|
||||
pub supports_file_read: bool,
|
||||
pub supports_metadata_read: bool,
|
||||
pub supports_network_all: bool,
|
||||
pub supports_network_tcp: bool,
|
||||
pub supports_network_local: bool,
|
||||
pub supports_system_info: bool,
|
||||
}
|
||||
|
||||
impl PlatformConfig {
|
||||
/// Get configuration for current platform
|
||||
pub fn current() -> Self {
|
||||
match env::consts::OS {
|
||||
"linux" => Self {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: false, // Cannot be precisely controlled
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: false, // Cannot filter by port
|
||||
supports_network_local: false, // Cannot filter by path
|
||||
supports_system_info: false,
|
||||
},
|
||||
"macos" => Self {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: true,
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: true,
|
||||
supports_network_local: true,
|
||||
supports_system_info: true,
|
||||
},
|
||||
"freebsd" => Self {
|
||||
supports_file_read: false,
|
||||
supports_metadata_read: false,
|
||||
supports_network_all: false,
|
||||
supports_network_tcp: false,
|
||||
supports_network_local: false,
|
||||
supports_system_info: true, // Always allowed
|
||||
},
|
||||
_ => Self {
|
||||
supports_file_read: false,
|
||||
supports_metadata_read: false,
|
||||
supports_network_all: false,
|
||||
supports_network_tcp: false,
|
||||
supports_network_local: false,
|
||||
supports_system_info: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test command builder
|
||||
pub struct TestCommand {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env_vars: Vec<(String, String)>,
|
||||
working_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl TestCommand {
|
||||
/// Create a new test command
|
||||
pub fn new(command: &str) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
env_vars: Vec::new(),
|
||||
working_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an argument
|
||||
pub fn arg(mut self, arg: &str) -> Self {
|
||||
self.args.push(arg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple arguments
|
||||
pub fn args(mut self, args: &[&str]) -> Self {
|
||||
self.args.extend(args.iter().map(|s| s.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an environment variable
|
||||
pub fn env(mut self, key: &str, value: &str) -> Self {
|
||||
self.env_vars.push((key.to_string(), value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set working directory
|
||||
pub fn current_dir(mut self, dir: &Path) -> Self {
|
||||
self.working_dir = Some(dir.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the command with timeout
|
||||
pub fn execute_with_timeout(&self, timeout: Duration) -> Result<Output> {
|
||||
let mut cmd = Command::new(&self.command);
|
||||
|
||||
cmd.args(&self.args);
|
||||
|
||||
for (key, value) in &self.env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
if let Some(dir) = &self.working_dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
|
||||
// On Unix, we can use a timeout mechanism
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
let mut child = cmd.spawn().context("Failed to spawn command")?;
|
||||
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let output = child.wait_with_output()?;
|
||||
return Ok(Output {
|
||||
status,
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() > timeout {
|
||||
child.kill()?;
|
||||
return Err(anyhow::anyhow!("Command timed out"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Fallback for non-Unix platforms
|
||||
cmd.output().context("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute and expect success
|
||||
pub fn execute_expect_success(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command failed with status {:?}. Stderr: {stderr}",
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
/// Execute and expect failure
|
||||
pub fn execute_expect_failure(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command unexpectedly succeeded. Stdout: {stdout}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stderr).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple test binary that attempts an operation
|
||||
pub fn create_test_binary(name: &str, code: &str, test_dir: &Path) -> Result<PathBuf> {
|
||||
create_test_binary_with_deps(name, code, test_dir, &[])
|
||||
}
|
||||
|
||||
/// Create a test binary with optional dependencies
|
||||
pub fn create_test_binary_with_deps(
|
||||
name: &str,
|
||||
code: &str,
|
||||
test_dir: &Path,
|
||||
dependencies: &[(&str, &str)],
|
||||
) -> Result<PathBuf> {
|
||||
let src_dir = test_dir.join("src");
|
||||
std::fs::create_dir_all(&src_dir)?;
|
||||
|
||||
// Build dependencies section
|
||||
let deps_section = if dependencies.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut deps = String::from("\n[dependencies]\n");
|
||||
for (dep_name, dep_version) in dependencies {
|
||||
deps.push_str(&format!("{dep_name} = \"{dep_version}\"\n"));
|
||||
}
|
||||
deps
|
||||
};
|
||||
|
||||
// Create Cargo.toml
|
||||
let cargo_toml = format!(
|
||||
r#"[package]
|
||||
name = "{name}"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "{name}"
|
||||
path = "src/main.rs"
|
||||
{deps_section}"#
|
||||
);
|
||||
std::fs::write(test_dir.join("Cargo.toml"), cargo_toml)?;
|
||||
|
||||
// Create main.rs
|
||||
std::fs::write(src_dir.join("main.rs"), code)?;
|
||||
|
||||
// Build the binary
|
||||
let output = Command::new("cargo")
|
||||
.arg("build")
|
||||
.arg("--release")
|
||||
.current_dir(test_dir)
|
||||
.output()
|
||||
.context("Failed to build test binary")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Failed to build test binary: {stderr}"));
|
||||
}
|
||||
|
||||
let binary_path = test_dir.join("target/release").join(name);
|
||||
Ok(binary_path)
|
||||
}
|
||||
|
||||
/// Test code snippets for various operations
|
||||
pub mod test_code {
|
||||
/// Code that reads a file
|
||||
pub fn file_read(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::read_to_string("{path}") {{
|
||||
Ok(content) => {{
|
||||
println!("SUCCESS: Read {{}} bytes", content.len());
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that reads file metadata
|
||||
pub fn file_metadata(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::metadata("{path}") {{
|
||||
Ok(metadata) => {{
|
||||
println!("SUCCESS: File size: {{}} bytes", metadata.len());
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that makes a network connection
|
||||
pub fn network_connect(addr: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
use std::net::TcpStream;
|
||||
|
||||
fn main() {{
|
||||
match TcpStream::connect("{addr}") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Connected to {addr}");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that reads system information
|
||||
pub fn system_info() -> &'static str {
|
||||
r#"
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_void;
|
||||
|
||||
extern "C" {
|
||||
fn sysctlbyname(
|
||||
name: *const std::os::raw::c_char,
|
||||
oldp: *mut c_void,
|
||||
oldlenp: *mut usize,
|
||||
newp: *const c_void,
|
||||
newlen: usize,
|
||||
) -> std::os::raw::c_int;
|
||||
}
|
||||
|
||||
let name = CString::new("hw.ncpu").unwrap();
|
||||
let mut ncpu: i32 = 0;
|
||||
let mut len = std::mem::size_of::<i32>();
|
||||
|
||||
unsafe {
|
||||
let result = sysctlbyname(
|
||||
name.as_ptr(),
|
||||
&mut ncpu as *mut _ as *mut c_void,
|
||||
&mut len,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
println!("SUCCESS: CPU count: {}", ncpu);
|
||||
} else {
|
||||
eprintln!("FAILURE: sysctlbyname failed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
println!("SUCCESS: System info test not applicable on this platform");
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that tries to spawn a process
|
||||
pub fn spawn_process() -> &'static str {
|
||||
r#"
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
match Command::new("echo").arg("test").output() {
|
||||
Ok(_) => {
|
||||
println!("SUCCESS: Spawned process");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("FAILURE: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that uses fork (requires libc)
|
||||
pub fn fork_process() -> &'static str {
|
||||
r#"
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
unsafe {
|
||||
let pid = libc::fork();
|
||||
if pid < 0 {
|
||||
eprintln!("FAILURE: fork failed");
|
||||
std::process::exit(1);
|
||||
} else if pid == 0 {
|
||||
// Child process
|
||||
println!("SUCCESS: Child process created");
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
// Parent process
|
||||
let mut status = 0;
|
||||
libc::waitpid(pid, &mut status, 0);
|
||||
println!("SUCCESS: Fork completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {
|
||||
eprintln!("FAILURE: fork not supported on this platform");
|
||||
std::process::exit(1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that uses exec (requires libc)
|
||||
pub fn exec_process() -> &'static str {
|
||||
r#"
|
||||
use std::ffi::CString;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
unsafe {
|
||||
let program = CString::new("/bin/echo").unwrap();
|
||||
let arg = CString::new("test").unwrap();
|
||||
let args = vec![program.as_ptr(), arg.as_ptr(), std::ptr::null()];
|
||||
|
||||
let result = libc::execv(program.as_ptr(), args.as_ptr());
|
||||
|
||||
// If we reach here, exec failed
|
||||
eprintln!("FAILURE: exec failed with result {}", result);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {
|
||||
eprintln!("FAILURE: exec not supported on this platform");
|
||||
std::process::exit(1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that tries to write a file
|
||||
pub fn file_write(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::write("{path}", "test content") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Wrote file");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that a command output contains expected text
|
||||
pub fn assert_output_contains(output: &str, expected: &str) {
|
||||
assert!(
|
||||
output.contains(expected),
|
||||
"Expected output to contain '{expected}', but got: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Assert that a command output indicates success
|
||||
pub fn assert_sandbox_success(output: &str) {
|
||||
assert_output_contains(output, "SUCCESS:");
|
||||
}
|
||||
|
||||
/// Assert that a command output indicates failure
|
||||
pub fn assert_sandbox_failure(output: &str) {
|
||||
assert_output_contains(output, "FAILURE:");
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
//! Common test utilities and helpers for sandbox testing
|
||||
pub mod claude_real;
|
||||
pub mod fixtures;
|
||||
pub mod helpers;
|
||||
|
||||
pub use claude_real::*;
|
||||
pub use fixtures::*;
|
||||
pub use helpers::*;
|
@@ -1,294 +0,0 @@
|
||||
//! End-to-end tests for agent execution with sandbox profiles
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use serial_test::serial;
|
||||
|
||||
/// Test agent execution with minimal sandbox profile
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_with_minimal_profile() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create minimal sandbox profile
|
||||
let rules = profiles::minimal(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("minimal_agent_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Create test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Test Agent",
|
||||
"🤖",
|
||||
"You are a test agent. Only perform the requested task.",
|
||||
"sonnet",
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Execute real Claude command with minimal profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are a test agent. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("Duration: {:?}", result.duration);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification - just check Claude ran
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124, // 0 = success, 124 = timeout
|
||||
"Claude should execute (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent execution with standard sandbox profile
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_with_standard_profile() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create standard sandbox profile
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("standard_agent_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Create test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Standard Agent",
|
||||
"🔧",
|
||||
"You are a test agent with standard permissions.",
|
||||
"sonnet",
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Execute real Claude command with standard profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are a test agent with standard permissions."),
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Standard Profile) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with standard profile (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent execution without sandbox (control test)
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_without_sandbox() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create agent without sandbox profile
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model) VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![
|
||||
"Unsandboxed Agent",
|
||||
"⚠️",
|
||||
"You are a test agent without sandbox restrictions.",
|
||||
"sonnet"
|
||||
],
|
||||
)
|
||||
.expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Execute real Claude command without sandbox profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are a test agent without sandbox restrictions."),
|
||||
Some("sonnet"),
|
||||
None, // No sandbox profile
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (No Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent run violation logging
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_agent_run_violation_logging() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create a test profile first
|
||||
let profile_id = test_db
|
||||
.create_test_profile("violation_test", vec![])
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Create a test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Violation Test Agent",
|
||||
"⚠️",
|
||||
"Test agent for violation logging.",
|
||||
"sonnet",
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Create a test agent run
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agent_runs (agent_id, agent_name, agent_icon, task, model, project_path) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
rusqlite::params![
|
||||
agent_id,
|
||||
"Violation Test Agent",
|
||||
"⚠️",
|
||||
"Test task",
|
||||
"sonnet",
|
||||
"/test/path"
|
||||
],
|
||||
).expect("Failed to create agent run");
|
||||
|
||||
let agent_run_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Insert test violations
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO sandbox_violations (profile_id, agent_id, agent_run_id, operation_type, pattern_value)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![profile_id, agent_id, agent_run_id, "file_read_all", "/etc/passwd"],
|
||||
).expect("Failed to insert violation");
|
||||
|
||||
// Query violations
|
||||
let count: i64 = test_db
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sandbox_violations WHERE agent_id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("Failed to query violations");
|
||||
|
||||
assert_eq!(count, 1, "Should have recorded one violation");
|
||||
}
|
||||
|
||||
/// Test profile switching between agent runs
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_profile_switching() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create two different profiles
|
||||
let minimal_rules = profiles::minimal(&test_fs.project_path.to_string_lossy());
|
||||
let minimal_id = test_db
|
||||
.create_test_profile("minimal_switch", minimal_rules)
|
||||
.expect("Failed to create minimal profile");
|
||||
|
||||
let standard_rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let standard_id = test_db
|
||||
.create_test_profile("standard_switch", standard_rules)
|
||||
.expect("Failed to create standard profile");
|
||||
|
||||
// Create agent initially with minimal profile
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![
|
||||
"Switchable Agent",
|
||||
"🔄",
|
||||
"Test agent for profile switching.",
|
||||
"sonnet",
|
||||
minimal_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
let agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
// Update agent to use standard profile
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE agents SET sandbox_profile_id = ?1 WHERE id = ?2",
|
||||
rusqlite::params![standard_id, agent_id],
|
||||
)
|
||||
.expect("Failed to update agent profile");
|
||||
|
||||
// Verify profile was updated
|
||||
let current_profile: i64 = test_db
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT sandbox_profile_id FROM agents WHERE id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("Failed to query agent profile");
|
||||
|
||||
assert_eq!(current_profile, standard_id, "Profile should be updated");
|
||||
}
|
@@ -1,220 +0,0 @@
|
||||
//! End-to-end tests for Claude command execution with sandbox profiles
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use serial_test::serial;
|
||||
|
||||
/// Test Claude Code execution with default sandbox profile
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_with_default_sandbox() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create default sandbox profile
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("claude_default", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Set as default and active
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 1 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
)
|
||||
.expect("Failed to set default profile");
|
||||
|
||||
// Execute real Claude command with default sandbox profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are Claude. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Default Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with default sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test Claude Code with sandboxing disabled
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_sandbox_disabled() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create profile but mark as inactive
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db
|
||||
.create_test_profile("claude_inactive", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Set as default but inactive
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 0 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
)
|
||||
.expect("Failed to set inactive profile");
|
||||
|
||||
// Execute real Claude command without active sandbox
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
&tasks::multi_operation(),
|
||||
Some("You are Claude. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
None, // No sandbox since profile is inactive
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Inactive Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
// Basic verification
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without active sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test Claude Code session operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_session_operations() {
|
||||
// This test doesn't require actual Claude execution
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create mock session structure
|
||||
let claude_dir = test_fs.root.path().join(".claude");
|
||||
let projects_dir = claude_dir.join("projects");
|
||||
let project_id = test_fs.project_path.to_string_lossy().replace('/', "-");
|
||||
let session_dir = projects_dir.join(&project_id);
|
||||
|
||||
std::fs::create_dir_all(&session_dir).expect("Failed to create session dir");
|
||||
|
||||
// Create mock session file
|
||||
let session_id = "test-session-123";
|
||||
let session_file = session_dir.join(format!("{}.jsonl", session_id));
|
||||
|
||||
let session_data = serde_json::json!({
|
||||
"type": "session_start",
|
||||
"cwd": test_fs.project_path.to_string_lossy(),
|
||||
"timestamp": "2024-01-01T00:00:00Z"
|
||||
});
|
||||
|
||||
std::fs::write(&session_file, format!("{}\n", session_data))
|
||||
.expect("Failed to write session file");
|
||||
|
||||
// Verify session file exists
|
||||
assert!(session_file.exists(), "Session file should exist");
|
||||
}
|
||||
|
||||
/// Test Claude settings with sandbox configuration
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_claude_settings_sandbox_config() {
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create mock settings
|
||||
let claude_dir = test_fs.root.path().join(".claude");
|
||||
std::fs::create_dir_all(&claude_dir).expect("Failed to create claude dir");
|
||||
|
||||
let settings_file = claude_dir.join("settings.json");
|
||||
let settings = serde_json::json!({
|
||||
"sandboxEnabled": true,
|
||||
"defaultSandboxProfile": "standard",
|
||||
"theme": "dark",
|
||||
"model": "sonnet"
|
||||
});
|
||||
|
||||
std::fs::write(
|
||||
&settings_file,
|
||||
serde_json::to_string_pretty(&settings).unwrap(),
|
||||
)
|
||||
.expect("Failed to write settings");
|
||||
|
||||
// Read and verify settings
|
||||
let content = std::fs::read_to_string(&settings_file).expect("Failed to read settings");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&content).expect("Failed to parse settings");
|
||||
|
||||
assert_eq!(parsed["sandboxEnabled"], true, "Sandbox should be enabled");
|
||||
assert_eq!(
|
||||
parsed["defaultSandboxProfile"], "standard",
|
||||
"Default profile should be standard"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test profile-based file access restrictions
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_profile_file_access_simulation() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test environment
|
||||
let _test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create a custom profile with specific file access
|
||||
let custom_rules = vec![
|
||||
TestRule::file_read("{{PROJECT_PATH}}", true),
|
||||
TestRule::file_read("/usr/local/bin", true),
|
||||
TestRule::file_read("/etc/hosts", false), // Literal file
|
||||
];
|
||||
|
||||
let profile_id = test_db
|
||||
.create_test_profile("file_access_test", custom_rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Load the profile rules
|
||||
let loaded_rules: Vec<(String, String, String)> = test_db.conn
|
||||
.prepare("SELECT operation_type, pattern_type, pattern_value FROM sandbox_rules WHERE profile_id = ?1")
|
||||
.expect("Failed to prepare query")
|
||||
.query_map(rusqlite::params![profile_id], |row| {
|
||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
||||
})
|
||||
.expect("Failed to query rules")
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("Failed to collect rules");
|
||||
|
||||
// Verify rules were created correctly
|
||||
assert_eq!(loaded_rules.len(), 3, "Should have 3 rules");
|
||||
assert!(
|
||||
loaded_rules.iter().any(|(op, _, _)| op == "file_read_all"),
|
||||
"Should have file_read_all operation"
|
||||
);
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
//! End-to-end tests for sandbox integration with agents and Claude
|
||||
#[cfg(test)]
|
||||
mod agent_sandbox;
|
||||
#[cfg(test)]
|
||||
mod claude_sandbox;
|
@@ -1,301 +0,0 @@
|
||||
//! Integration tests for file operations in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use claudia_lib::sandbox::profile::ProfileBuilder;
|
||||
use gaol::profile::{Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test allowed file read operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allowed_file_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing project path access
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads from allowed path
|
||||
let test_code = test_code::file_read(&test_fs.project_path.join("main.rs").to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_file_read", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Allowed file read should succeed");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test forbidden file read operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_forbidden_file_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing only project path (not forbidden path)
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads from forbidden path
|
||||
let forbidden_file = test_fs.forbidden_path.join("secret.txt");
|
||||
let test_code = test_code::file_read(&forbidden_file.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_forbidden_read", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// On some platforms (like macOS), gaol might not block all file reads
|
||||
// so we check if the operation failed OR if it's a platform limitation
|
||||
if status.success() {
|
||||
eprintln!(
|
||||
"WARNING: File read was not blocked - this might be a platform limitation"
|
||||
);
|
||||
// Check if we're on a platform where this is expected
|
||||
let platform_config = PlatformConfig::current();
|
||||
if !platform_config.supports_file_read {
|
||||
panic!("File read should have been blocked on this platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file write operations (should always be forbidden)
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_file_write_always_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with file read permissions (write should still be blocked)
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to write a file
|
||||
let write_path = test_fs.project_path.join("test_write.txt");
|
||||
let test_code = test_code::file_write(&write_path.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_file_write", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// File writes might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: File write was not blocked - checking platform capabilities");
|
||||
// On macOS, file writes might not be fully blocked by gaol
|
||||
if std::env::consts::OS != "macos" {
|
||||
panic!("File write should have been blocked on this platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file metadata operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_file_metadata_operations() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_metadata_read && !platform.supports_file_read {
|
||||
eprintln!("Skipping test: metadata read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with metadata read permission
|
||||
let operations = if platform.supports_metadata_read {
|
||||
vec![Operation::FileReadMetadata(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))]
|
||||
} else {
|
||||
// On Linux, metadata is allowed if file read is allowed
|
||||
vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))]
|
||||
};
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads file metadata
|
||||
let test_file = test_fs.project_path.join("main.rs");
|
||||
let test_code = test_code::file_metadata(&test_file.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_metadata", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
if platform.supports_metadata_read || platform.supports_file_read {
|
||||
assert!(
|
||||
status.success(),
|
||||
"Metadata read should succeed when allowed"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test template variable expansion in file paths
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_template_variable_expansion() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test database and profile
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
// Create a profile with template variables
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let rules = vec![TestRule::file_read("{{PROJECT_PATH}}", true)];
|
||||
|
||||
let profile_id = test_db
|
||||
.create_test_profile("template_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
// Load and build the profile
|
||||
let db_rules = claudia_lib::sandbox::profile::load_profile_rules(&test_db.conn, profile_id)
|
||||
.expect("Failed to load profile rules");
|
||||
|
||||
let builder = ProfileBuilder::new(test_fs.project_path.clone())
|
||||
.expect("Failed to create profile builder");
|
||||
|
||||
let profile = match builder.build_profile(db_rules) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to build profile with templates");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads from project path
|
||||
let test_code = test_code::file_read(&test_fs.project_path.join("main.rs").to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_template", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Template-based file access should work");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
//! Integration tests for sandbox functionality
|
||||
#[cfg(test)]
|
||||
mod file_operations;
|
||||
#[cfg(test)]
|
||||
mod network_operations;
|
||||
#[cfg(test)]
|
||||
mod process_isolation;
|
||||
#[cfg(test)]
|
||||
mod system_info;
|
||||
#[cfg(test)]
|
||||
mod violations;
|
@@ -1,312 +0,0 @@
|
||||
//! Integration tests for network operations in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{AddressPattern, Operation, Profile};
|
||||
use serial_test::serial;
|
||||
use std::net::TcpListener;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Get an available port for testing
|
||||
fn get_available_port() -> u16 {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to 0");
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.expect("Failed to get local addr")
|
||||
.port();
|
||||
drop(listener); // Release the port
|
||||
port
|
||||
}
|
||||
|
||||
/// Test allowed network operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allowed_network_all() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_network_all {
|
||||
eprintln!("Skipping test: network all not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing all network access
|
||||
let operations = vec![Operation::NetworkOutbound(AddressPattern::All)];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that connects to localhost
|
||||
let port = get_available_port();
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_network", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Start a listener on the port
|
||||
let listener =
|
||||
TcpListener::bind(format!("127.0.0.1:{}", port)).expect("Failed to bind listener");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
// Accept connection in a thread
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Network connection should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test forbidden network operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_forbidden_network() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile without network permissions
|
||||
let operations = vec![Operation::FileReadAll(gaol::profile::PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to connect
|
||||
let test_code = test_code::network_connect("google.com:80");
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_no_network", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Network restrictions might not work on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Network connection was not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Network should be blocked on Linux when not allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test TCP port-specific network rules (macOS only)
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_network_tcp_port_specific() {
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_network_tcp {
|
||||
eprintln!("Skipping test: TCP port filtering not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Get two ports - one allowed, one forbidden
|
||||
let allowed_port = get_available_port();
|
||||
let forbidden_port = get_available_port();
|
||||
|
||||
// Create profile allowing only specific port
|
||||
let operations = vec![Operation::NetworkOutbound(AddressPattern::Tcp(
|
||||
allowed_port,
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Test 1: Allowed port
|
||||
{
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", allowed_port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_allowed_port", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", allowed_port))
|
||||
.expect("Failed to bind listener");
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Connection to allowed port should succeed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Forbidden port
|
||||
{
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", forbidden_port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_forbidden_port", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
!status.success(),
|
||||
"Connection to forbidden port should fail"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test local socket connections (Unix domain sockets)
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
fn test_local_socket_connections() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let socket_path = test_fs.project_path.join("test.sock");
|
||||
|
||||
// Create appropriate profile based on platform
|
||||
let operations = if platform.supports_network_local {
|
||||
vec![Operation::NetworkOutbound(AddressPattern::LocalSocket(
|
||||
socket_path.clone(),
|
||||
))]
|
||||
} else if platform.supports_network_all {
|
||||
// Fallback to allowing all network
|
||||
vec![Operation::NetworkOutbound(AddressPattern::All)]
|
||||
} else {
|
||||
eprintln!("Skipping test: no network support on this platform");
|
||||
return;
|
||||
};
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that connects to local socket
|
||||
let test_code = format!(
|
||||
r#"
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
fn main() {{
|
||||
match UnixStream::connect("{}") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Connected to local socket");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
socket_path.to_string_lossy()
|
||||
);
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_local_socket", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Create Unix socket listener
|
||||
use std::os::unix::net::UnixListener;
|
||||
let listener = UnixListener::bind(&socket_path).expect("Failed to bind Unix socket");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Local socket connection should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up socket file
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
@@ -1,247 +0,0 @@
|
||||
//! Integration tests for process isolation in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{AddressPattern, Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test that process spawning is always forbidden
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_process_spawn_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with various permissions (process spawn should still be blocked)
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to spawn a process
|
||||
let test_code = test_code::spawn_process();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_spawn", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Process spawning might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Process spawning was not blocked");
|
||||
// macOS sandbox might have limitations
|
||||
if std::env::consts::OS != "linux" {
|
||||
eprintln!(
|
||||
"Process spawning might not be fully blocked on {}",
|
||||
std::env::consts::OS
|
||||
);
|
||||
} else {
|
||||
panic!("Process spawning should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that fork is blocked
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
fn test_fork_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to fork
|
||||
let test_code = test_code::fork_process();
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary_with_deps(
|
||||
"test_fork",
|
||||
test_code,
|
||||
binary_dir.path(),
|
||||
&[("libc", "0.2")],
|
||||
)
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Fork might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Fork was not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Fork should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that exec is blocked
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
fn test_exec_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that tries to exec
|
||||
let test_code = test_code::exec_process();
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary_with_deps(
|
||||
"test_exec",
|
||||
test_code,
|
||||
binary_dir.path(),
|
||||
&[("libc", "0.2")],
|
||||
)
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Exec might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Exec was not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Exec should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test thread creation is allowed
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_thread_creation_allowed() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that creates threads
|
||||
let test_code = r#"
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let handle = thread::spawn(|| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
42
|
||||
});
|
||||
|
||||
match handle.join() {
|
||||
Ok(value) => {
|
||||
println!("SUCCESS: Thread returned {}", value);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("FAILURE: Thread panicked");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_thread", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Thread creation should be allowed");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
//! Integration tests for system information operations in sandbox
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Operation, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Test system info read operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_system_info_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_system_info {
|
||||
eprintln!("Skipping test: system info read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile allowing system info read
|
||||
let operations = vec![Operation::SystemInfoRead];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads system info
|
||||
let test_code = test_code::system_info();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_sysinfo", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(
|
||||
status.success(),
|
||||
"System info read should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test forbidden system info access
|
||||
#[test]
|
||||
#[serial]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_forbidden_system_info() {
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile without system info permission
|
||||
let operations = vec![Operation::FileReadAll(gaol::profile::PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that reads system info
|
||||
let test_code = test_code::system_info();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_no_sysinfo", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// System info might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: System info read was not blocked - checking platform");
|
||||
// On FreeBSD, system info is always allowed
|
||||
if std::env::consts::OS == "freebsd" {
|
||||
eprintln!("System info is always allowed on FreeBSD");
|
||||
} else if std::env::consts::OS == "macos" {
|
||||
// macOS might allow some system info reads
|
||||
eprintln!("System info read allowed on macOS (platform limitation)");
|
||||
} else {
|
||||
panic!("System info read should have been blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test platform-specific system info behavior
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_platform_specific_system_info() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
|
||||
match std::env::consts::OS {
|
||||
"linux" => {
|
||||
// On Linux, system info is never allowed
|
||||
assert!(
|
||||
!platform.supports_system_info,
|
||||
"Linux should not support system info read"
|
||||
);
|
||||
}
|
||||
"macos" => {
|
||||
// On macOS, system info can be allowed
|
||||
assert!(
|
||||
platform.supports_system_info,
|
||||
"macOS should support system info read"
|
||||
);
|
||||
}
|
||||
"freebsd" => {
|
||||
// On FreeBSD, system info is always allowed (can't be restricted)
|
||||
assert!(
|
||||
platform.supports_system_info,
|
||||
"FreeBSD always allows system info read"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown platform behavior for system info");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,297 +0,0 @@
|
||||
//! Integration tests for sandbox violation detection and logging
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Mock violation collector for testing
|
||||
#[derive(Clone)]
|
||||
struct ViolationCollector {
|
||||
violations: Arc<Mutex<Vec<ViolationEvent>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
struct ViolationEvent {
|
||||
operation_type: String,
|
||||
pattern_value: Option<String>,
|
||||
process_name: String,
|
||||
}
|
||||
|
||||
impl ViolationCollector {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
violations: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&self, operation_type: &str, pattern_value: Option<&str>, process_name: &str) {
|
||||
let event = ViolationEvent {
|
||||
operation_type: operation_type.to_string(),
|
||||
pattern_value: pattern_value.map(|s| s.to_string()),
|
||||
process_name: process_name.to_string(),
|
||||
};
|
||||
|
||||
if let Ok(mut violations) = self.violations.lock() {
|
||||
violations.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_violations(&self) -> Vec<ViolationEvent> {
|
||||
self.violations.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that violations are detected for forbidden operations
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_violation_detection() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let collector = ViolationCollector::new();
|
||||
|
||||
// Create profile allowing only project path
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Test various forbidden operations
|
||||
let test_cases = vec![
|
||||
(
|
||||
"file_read",
|
||||
test_code::file_read(&test_fs.forbidden_path.join("secret.txt").to_string_lossy()),
|
||||
"file_read_forbidden",
|
||||
),
|
||||
(
|
||||
"file_write",
|
||||
test_code::file_write(&test_fs.project_path.join("new.txt").to_string_lossy()),
|
||||
"file_write_forbidden",
|
||||
),
|
||||
(
|
||||
"process_spawn",
|
||||
test_code::spawn_process().to_string(),
|
||||
"process_spawn_forbidden",
|
||||
),
|
||||
];
|
||||
|
||||
for (op_type, test_code, binary_name) in test_cases {
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary(binary_name, &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
if !status.success() {
|
||||
// Record violation
|
||||
collector.record(op_type, None, binary_name);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Sandbox setup failure, not a violation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify violations were detected
|
||||
let violations = collector.get_violations();
|
||||
// On some platforms (like macOS), sandbox might not block all operations
|
||||
if violations.is_empty() {
|
||||
eprintln!("WARNING: No violations detected - this might be a platform limitation");
|
||||
// On Linux, we expect at least some violations
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Should have detected some violations on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test violation patterns and details
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_violation_patterns() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create profile with specific allowed paths
|
||||
let allowed_dir = test_fs.root.path().join("allowed_specific");
|
||||
std::fs::create_dir_all(&allowed_dir).expect("Failed to create allowed dir");
|
||||
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
Operation::FileReadAll(PathPattern::Literal(allowed_dir.join("file.txt"))),
|
||||
];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Test accessing different forbidden paths
|
||||
let forbidden_db_path = test_fs
|
||||
.forbidden_path
|
||||
.join("data.db")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let forbidden_paths = vec![
|
||||
("/etc/passwd", "system_file"),
|
||||
("/tmp/test.txt", "temp_file"),
|
||||
(forbidden_db_path.as_str(), "forbidden_db"),
|
||||
];
|
||||
|
||||
for (path, test_name) in forbidden_paths {
|
||||
let test_code = test_code::file_read(path);
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary(test_name, &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Some platforms might not block all file access
|
||||
if status.success() {
|
||||
eprintln!(
|
||||
"WARNING: Access to {} was allowed (possible platform limitation)",
|
||||
path
|
||||
);
|
||||
if std::env::consts::OS == "linux" && path.starts_with("/etc") {
|
||||
panic!("Access to {} should be denied on Linux", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Sandbox setup failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test multiple violations in sequence
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_multiple_violations_sequence() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
eprintln!("Failed to create profile - operation not supported");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create test binary that attempts multiple forbidden operations
|
||||
let test_code = r#"
|
||||
use std::fs;
|
||||
use std::net::TcpStream;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {{
|
||||
let mut failures = 0;
|
||||
|
||||
// Try file write
|
||||
if fs::write("/tmp/test.txt", "data").is_err() {{
|
||||
eprintln!("File write failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
// Try network connection
|
||||
if TcpStream::connect("google.com:80").is_err() {{
|
||||
eprintln!("Network connection failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
// Try process spawn
|
||||
if Command::new("ls").output().is_err() {{
|
||||
eprintln!("Process spawn failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
// Try forbidden file read
|
||||
if fs::read_to_string("/etc/passwd").is_err() {{
|
||||
eprintln!("Forbidden file read failed (expected)");
|
||||
failures += 1;
|
||||
}}
|
||||
|
||||
if failures > 0 {{
|
||||
eprintln!("FAILURE: {{failures}} operations were blocked");
|
||||
std::process::exit(1);
|
||||
}} else {{
|
||||
println!("SUCCESS: No operations were blocked (unexpected)");
|
||||
}}
|
||||
}}
|
||||
"#;
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_multi_violations", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
&[],
|
||||
&test_fs.project_path,
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Multiple operations might not be blocked on all platforms
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Forbidden operations were not blocked (platform limitation)");
|
||||
if std::env::consts::OS == "linux" {
|
||||
panic!("Operations should be blocked on Linux");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
//! Comprehensive test suite for sandbox functionality
|
||||
//!
|
||||
//! This test suite validates the sandboxing capabilities across different platforms,
|
||||
//! ensuring that security policies are correctly enforced.
|
||||
|
||||
#[cfg(unix)]
|
||||
#[macro_use]
|
||||
pub mod common;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod unit;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod integration;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod e2e;
|
@@ -1,146 +0,0 @@
|
||||
//! Unit tests for SandboxExecutor
|
||||
use claudia_lib::sandbox::executor::{should_activate_sandbox, SandboxExecutor};
|
||||
use gaol::profile::{AddressPattern, Operation, PathPattern, Profile};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Create a simple test profile
|
||||
fn create_test_profile(project_path: PathBuf) -> Profile {
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(project_path)),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
Profile::new(operations).expect("Failed to create test profile")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_creation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
|
||||
let _executor = SandboxExecutor::new(profile, project_path);
|
||||
// Executor should be created successfully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_activate_sandbox_env_var() {
|
||||
// Test when env var is not set
|
||||
env::remove_var("GAOL_SANDBOX_ACTIVE");
|
||||
assert!(
|
||||
!should_activate_sandbox(),
|
||||
"Should not activate when env var is not set"
|
||||
);
|
||||
|
||||
// Test when env var is set to "1"
|
||||
env::set_var("GAOL_SANDBOX_ACTIVE", "1");
|
||||
assert!(
|
||||
should_activate_sandbox(),
|
||||
"Should activate when env var is '1'"
|
||||
);
|
||||
|
||||
// Test when env var is set to other value
|
||||
env::set_var("GAOL_SANDBOX_ACTIVE", "0");
|
||||
assert!(
|
||||
!should_activate_sandbox(),
|
||||
"Should not activate when env var is not '1'"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
env::remove_var("GAOL_SANDBOX_ACTIVE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_sandboxed_command() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["hello"], &project_path);
|
||||
|
||||
// The command should have sandbox environment variables set
|
||||
// Note: We can't easily test Command internals, but we can verify it doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_with_empty_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = Profile::new(vec![]).expect("Failed to create empty profile");
|
||||
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["test"], &project_path);
|
||||
|
||||
// Should handle empty profile gracefully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_with_complex_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(project_path.clone())),
|
||||
Operation::FileReadAll(PathPattern::Subpath(PathBuf::from("/usr/lib"))),
|
||||
Operation::FileReadAll(PathPattern::Literal(PathBuf::from("/etc/hosts"))),
|
||||
Operation::FileReadMetadata(PathPattern::Subpath(PathBuf::from("/"))),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
Operation::NetworkOutbound(AddressPattern::Tcp(443)),
|
||||
Operation::SystemInfoRead,
|
||||
];
|
||||
|
||||
// Only create profile with supported operations
|
||||
let filtered_ops: Vec<_> = operations
|
||||
.into_iter()
|
||||
.filter(|op| {
|
||||
use gaol::profile::{OperationSupport, OperationSupportLevel};
|
||||
matches!(op.support(), OperationSupportLevel::CanBeAllowed)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !filtered_ops.is_empty() {
|
||||
let profile = Profile::new(filtered_ops).expect("Failed to create complex profile");
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["test"], &project_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_environment_setup() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
// Test with various arguments
|
||||
let _cmd1 = executor.prepare_sandboxed_command("ls", &[], &project_path);
|
||||
let _cmd2 = executor.prepare_sandboxed_command("cat", &["file.txt"], &project_path);
|
||||
let _cmd3 = executor.prepare_sandboxed_command("grep", &["-r", "pattern", "."], &project_path);
|
||||
|
||||
// Commands should be prepared without panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_spawn_sandboxed_process() {
|
||||
use crate::sandbox::common::is_sandboxing_supported;
|
||||
|
||||
if !is_sandboxing_supported() {
|
||||
return;
|
||||
}
|
||||
|
||||
let project_path = env::current_dir().unwrap_or_else(|_| PathBuf::from("/tmp"));
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
// Try to spawn a simple command
|
||||
let result = executor.execute_sandboxed_spawn("echo", &["sandbox test"], &project_path);
|
||||
|
||||
// On supported platforms, this should either succeed or fail gracefully
|
||||
match result {
|
||||
Ok(mut child) => {
|
||||
// If spawned successfully, wait for it to complete
|
||||
let _ = child.wait();
|
||||
}
|
||||
Err(e) => {
|
||||
// Sandboxing might fail due to permissions or platform limitations
|
||||
println!("Sandbox spawn failed (expected in some environments): {e}");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
//! Unit tests for sandbox components
|
||||
#[cfg(test)]
|
||||
mod executor;
|
||||
#[cfg(test)]
|
||||
mod platform;
|
||||
#[cfg(test)]
|
||||
mod profile_builder;
|
@@ -1,181 +0,0 @@
|
||||
//! Unit tests for platform capabilities
|
||||
use claudia_lib::sandbox::platform::{get_platform_capabilities, is_sandboxing_available};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_sandboxing_availability() {
|
||||
let is_available = is_sandboxing_available();
|
||||
let expected = matches!(env::consts::OS, "linux" | "macos" | "freebsd");
|
||||
|
||||
assert_eq!(
|
||||
is_available, expected,
|
||||
"Sandboxing availability should match platform support"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_capabilities_structure() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
// Verify basic structure
|
||||
assert_eq!(caps.os, env::consts::OS, "OS should match current platform");
|
||||
assert!(
|
||||
!caps.operations.is_empty() || !caps.sandboxing_supported,
|
||||
"Should have operations if sandboxing is supported"
|
||||
);
|
||||
assert!(
|
||||
!caps.notes.is_empty(),
|
||||
"Should have platform-specific notes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_linux_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert_eq!(caps.os, "linux");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
// Verify Linux-specific capabilities
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "can_be_allowed");
|
||||
|
||||
let metadata_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_metadata")
|
||||
.expect("file_read_metadata should be present");
|
||||
assert_eq!(metadata_read.support_level, "cannot_be_precisely");
|
||||
|
||||
let network_all = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_all")
|
||||
.expect("network_outbound_all should be present");
|
||||
assert_eq!(network_all.support_level, "can_be_allowed");
|
||||
|
||||
let network_tcp = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_tcp")
|
||||
.expect("network_outbound_tcp should be present");
|
||||
assert_eq!(network_tcp.support_level, "cannot_be_precisely");
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "never");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_macos_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert_eq!(caps.os, "macos");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
// Verify macOS-specific capabilities
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "can_be_allowed");
|
||||
|
||||
let metadata_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_metadata")
|
||||
.expect("file_read_metadata should be present");
|
||||
assert_eq!(metadata_read.support_level, "can_be_allowed");
|
||||
|
||||
let network_tcp = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_tcp")
|
||||
.expect("network_outbound_tcp should be present");
|
||||
assert_eq!(network_tcp.support_level, "can_be_allowed");
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "can_be_allowed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "freebsd")]
|
||||
fn test_freebsd_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert_eq!(caps.os, "freebsd");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
// Verify FreeBSD-specific capabilities
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "never");
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "always");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
|
||||
fn test_unsupported_platform_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
assert!(!caps.sandboxing_supported);
|
||||
assert_eq!(caps.operations.len(), 0);
|
||||
assert!(caps.notes.iter().any(|note| note.contains("not supported")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_operations_have_descriptions() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
for op in &caps.operations {
|
||||
assert!(
|
||||
!op.description.is_empty(),
|
||||
"Operation {} should have a description",
|
||||
op.operation
|
||||
);
|
||||
assert!(
|
||||
!op.support_level.is_empty(),
|
||||
"Operation {} should have a support level",
|
||||
op.operation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_support_level_values() {
|
||||
let caps = get_platform_capabilities();
|
||||
let valid_levels = ["never", "can_be_allowed", "cannot_be_precisely", "always"];
|
||||
|
||||
for op in &caps.operations {
|
||||
assert!(
|
||||
valid_levels.contains(&op.support_level.as_str()),
|
||||
"Operation {} has invalid support level: {}",
|
||||
op.operation,
|
||||
op.support_level
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,337 +0,0 @@
|
||||
//! Unit tests for ProfileBuilder
|
||||
use claudia_lib::sandbox::profile::{ProfileBuilder, SandboxRule};
|
||||
use std::path::PathBuf;
|
||||
use test_case::test_case;
|
||||
|
||||
/// Helper to create a sandbox rule
|
||||
fn make_rule(
|
||||
operation_type: &str,
|
||||
pattern_type: &str,
|
||||
pattern_value: &str,
|
||||
platforms: Option<&[&str]>,
|
||||
) -> SandboxRule {
|
||||
SandboxRule {
|
||||
id: None,
|
||||
profile_id: 0,
|
||||
operation_type: operation_type.to_string(),
|
||||
pattern_type: pattern_type.to_string(),
|
||||
pattern_value: pattern_value.to_string(),
|
||||
enabled: true,
|
||||
platform_support: platforms.map(|p| {
|
||||
serde_json::to_string(&p.iter().map(|s| s.to_string()).collect::<Vec<_>>()).unwrap()
|
||||
}),
|
||||
created_at: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profile_builder_creation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone());
|
||||
|
||||
assert!(
|
||||
builder.is_ok(),
|
||||
"ProfileBuilder should be created successfully"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_rules_creates_empty_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let profile = builder.build_profile(vec![]);
|
||||
assert!(
|
||||
profile.is_ok(),
|
||||
"Empty rules should create valid empty profile"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_read_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"literal",
|
||||
"/usr/lib/test.so",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
// Profile creation might fail on unsupported platforms, but parsing should work
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"File read rules should be parsed on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("network_outbound", "tcp", "8080", Some(&["macos"])),
|
||||
make_rule(
|
||||
"network_outbound",
|
||||
"local_socket",
|
||||
"/tmp/socket",
|
||||
Some(&["macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"Network rules should be parsed on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_info_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule("system_info_read", "all", "", Some(&["macos"]))];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
if std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"System info rule should be parsed on macOS"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_variable_replacement() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}/src",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{HOME}}/.config",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// We can't easily verify the exact paths without inspecting the Profile internals,
|
||||
// but this test ensures template replacement doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_rules_are_ignored() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let mut rule = make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
);
|
||||
rule.enabled = false;
|
||||
|
||||
let profile = builder.build_profile(vec![rule]);
|
||||
assert!(profile.is_ok(), "Disabled rules should be ignored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_filtering() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let current_os = std::env::consts::OS;
|
||||
let other_os = if current_os == "linux" {
|
||||
"macos"
|
||||
} else {
|
||||
"linux"
|
||||
};
|
||||
|
||||
let rules = vec![
|
||||
// Rule for current platform
|
||||
make_rule("file_read_all", "subpath", "/test1", Some(&[current_os])),
|
||||
// Rule for other platform
|
||||
make_rule("file_read_all", "subpath", "/test2", Some(&[other_os])),
|
||||
// Rule for both platforms
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/test3",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
// Rule with no platform specification (should be included)
|
||||
make_rule("file_read_all", "subpath", "/test4", None),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Rules for other platforms should be filtered out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_operation_type() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"invalid_operation",
|
||||
"subpath",
|
||||
"/test",
|
||||
Some(&["linux", "macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
assert!(_profile.is_ok(), "Invalid operations should be skipped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_pattern_type() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"file_read_all",
|
||||
"invalid_pattern",
|
||||
"/test",
|
||||
Some(&["linux", "macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Should either skip the rule or fail gracefully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_tcp_port() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"network_outbound",
|
||||
"tcp",
|
||||
"not_a_number",
|
||||
Some(&["macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Should handle invalid port gracefully
|
||||
}
|
||||
|
||||
#[test_case("file_read_all", "subpath", "/test" ; "file read operation")]
|
||||
#[test_case("file_read_metadata", "literal", "/test/file" ; "metadata read operation")]
|
||||
#[test_case("network_outbound", "all", "" ; "network all operation")]
|
||||
#[test_case("system_info_read", "all", "" ; "system info operation")]
|
||||
fn test_operation_support_level(operation_type: &str, pattern_type: &str, pattern_value: &str) {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rule = make_rule(operation_type, pattern_type, pattern_value, None);
|
||||
let rules = vec![rule];
|
||||
|
||||
match builder.build_profile(rules) {
|
||||
Ok(_) => {
|
||||
// Profile created successfully - operation is supported
|
||||
println!("Operation {operation_type} is supported on this platform");
|
||||
}
|
||||
Err(e) => {
|
||||
// Profile creation failed - likely due to unsupported operation
|
||||
println!("Operation {operation_type} is not supported: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_profile_with_multiple_rules() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
// File operations
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"literal",
|
||||
"/etc/hosts",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule("file_read_metadata", "subpath", "/", Some(&["macos"])),
|
||||
// Network operations
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("network_outbound", "tcp", "443", Some(&["macos"])),
|
||||
make_rule("network_outbound", "tcp", "80", Some(&["macos"])),
|
||||
// System info
|
||||
make_rule("system_info_read", "all", "", Some(&["macos"])),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"Complex profile should be created on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rule_order_preservation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
// Create rules with specific order
|
||||
let rules = vec![
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/first",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/second",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Order should be preserved in the resulting profile
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
//! Main entry point for sandbox tests
|
||||
//!
|
||||
//! This file integrates all the sandbox test modules and provides
|
||||
//! a central location for running the comprehensive test suite.
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[cfg(unix)]
|
||||
mod sandbox;
|
||||
|
||||
// Re-export test modules to make them discoverable
|
||||
pub use sandbox::*;
|
@@ -1,122 +0,0 @@
|
||||
import React from "react";
|
||||
import { Shield, FileText, Upload, Network, AlertTriangle } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { type Agent } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AgentSandboxSettingsProps {
|
||||
agent: Agent;
|
||||
onUpdate: (updates: Partial<Agent>) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for managing per-agent sandbox permissions
|
||||
* Provides simple toggles for sandbox enable/disable and file/network permissions
|
||||
*/
|
||||
export const AgentSandboxSettings: React.FC<AgentSandboxSettingsProps> = ({
|
||||
agent,
|
||||
onUpdate,
|
||||
className
|
||||
}) => {
|
||||
const handleToggle = (field: keyof Agent, value: boolean) => {
|
||||
onUpdate({ [field]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cn("p-4 space-y-4", className)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-amber-500" />
|
||||
<h4 className="font-semibold">Sandbox Permissions</h4>
|
||||
{!agent.sandbox_enabled && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Master sandbox toggle */}
|
||||
<div className="flex items-center justify-between p-3 rounded-lg border bg-muted/30">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm font-medium">Enable Sandbox</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Run this agent in a secure sandbox environment
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={agent.sandbox_enabled}
|
||||
onCheckedChange={(checked) => handleToggle('sandbox_enabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Permission toggles - only visible when sandbox is enabled */}
|
||||
{agent.sandbox_enabled && (
|
||||
<div className="space-y-3 pl-4 border-l-2 border-amber-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-blue-500" />
|
||||
<div>
|
||||
<Label className="text-sm font-medium">File Read Access</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Allow reading files and directories
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={agent.enable_file_read}
|
||||
onCheckedChange={(checked) => handleToggle('enable_file_read', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Upload className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<Label className="text-sm font-medium">File Write Access</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Allow creating and modifying files
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={agent.enable_file_write}
|
||||
onCheckedChange={(checked) => handleToggle('enable_file_write', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Network className="h-4 w-4 text-purple-500" />
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Network Access</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Allow outbound network connections
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={agent.enable_network}
|
||||
onCheckedChange={(checked) => handleToggle('enable_network', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning when sandbox is disabled */}
|
||||
{!agent.sandbox_enabled && (
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-50 border border-amber-200 text-amber-800 dark:bg-amber-950/50 dark:border-amber-800 dark:text-amber-200">
|
||||
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs">
|
||||
<p className="font-medium">Sandbox Disabled</p>
|
||||
<p>This agent will run with full system access. Use with caution.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
@@ -9,7 +9,6 @@ import { api, type Agent } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import { type AgentIconName } from "./CCAgents";
|
||||
import { AgentSandboxSettings } from "./AgentSandboxSettings";
|
||||
import { IconPicker, ICON_MAP } from "./IconPicker";
|
||||
|
||||
interface CreateAgentProps {
|
||||
@@ -48,10 +47,6 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
||||
const [systemPrompt, setSystemPrompt] = useState(agent?.system_prompt || "");
|
||||
const [defaultTask, setDefaultTask] = useState(agent?.default_task || "");
|
||||
const [model, setModel] = useState(agent?.model || "sonnet");
|
||||
const [sandboxEnabled, setSandboxEnabled] = useState(agent?.sandbox_enabled ?? true);
|
||||
const [enableFileRead, setEnableFileRead] = useState(agent?.enable_file_read ?? true);
|
||||
const [enableFileWrite, setEnableFileWrite] = useState(agent?.enable_file_write ?? true);
|
||||
const [enableNetwork, setEnableNetwork] = useState(agent?.enable_network ?? false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||
@@ -81,11 +76,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
||||
selectedIcon,
|
||||
systemPrompt,
|
||||
defaultTask || undefined,
|
||||
model,
|
||||
sandboxEnabled,
|
||||
enableFileRead,
|
||||
enableFileWrite,
|
||||
enableNetwork
|
||||
model
|
||||
);
|
||||
} else {
|
||||
await api.createAgent(
|
||||
@@ -93,11 +84,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
||||
selectedIcon,
|
||||
systemPrompt,
|
||||
defaultTask || undefined,
|
||||
model,
|
||||
sandboxEnabled,
|
||||
enableFileRead,
|
||||
enableFileWrite,
|
||||
enableNetwork
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,11 +106,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
||||
selectedIcon !== (agent?.icon || "bot") ||
|
||||
systemPrompt !== (agent?.system_prompt || "") ||
|
||||
defaultTask !== (agent?.default_task || "") ||
|
||||
model !== (agent?.model || "sonnet") ||
|
||||
sandboxEnabled !== (agent?.sandbox_enabled ?? true) ||
|
||||
enableFileRead !== (agent?.enable_file_read ?? true) ||
|
||||
enableFileWrite !== (agent?.enable_file_write ?? true) ||
|
||||
enableNetwork !== (agent?.enable_network ?? false)) &&
|
||||
model !== (agent?.model || "sonnet")) &&
|
||||
!confirm("You have unsaved changes. Are you sure you want to leave?")) {
|
||||
return;
|
||||
}
|
||||
@@ -309,29 +292,7 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sandbox Settings */}
|
||||
<AgentSandboxSettings
|
||||
agent={{
|
||||
id: agent?.id,
|
||||
name,
|
||||
icon: selectedIcon,
|
||||
system_prompt: systemPrompt,
|
||||
default_task: defaultTask || undefined,
|
||||
model,
|
||||
sandbox_enabled: sandboxEnabled,
|
||||
enable_file_read: enableFileRead,
|
||||
enable_file_write: enableFileWrite,
|
||||
enable_network: enableNetwork,
|
||||
created_at: agent?.created_at || "",
|
||||
updated_at: agent?.updated_at || ""
|
||||
}}
|
||||
onUpdate={(updates) => {
|
||||
if ('sandbox_enabled' in updates) setSandboxEnabled(updates.sandbox_enabled!);
|
||||
if ('enable_file_read' in updates) setEnableFileRead(updates.enable_file_read!);
|
||||
if ('enable_file_write' in updates) setEnableFileWrite(updates.enable_file_write!);
|
||||
if ('enable_network' in updates) setEnableNetwork(updates.enable_network!);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{/* System Prompt Editor */}
|
||||
<div className="space-y-2">
|
||||
@@ -377,4 +338,4 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@@ -314,9 +314,6 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline">{selectedAgent.data.agent.model}</Badge>
|
||||
{selectedAgent.data.agent.sandbox_enabled && (
|
||||
<Badge variant="secondary">Sandbox</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,21 +338,7 @@ export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Permissions */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Permissions</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={selectedAgent.data.agent.enable_file_read ? "default" : "secondary"}>
|
||||
File Read: {selectedAgent.data.agent.enable_file_read ? "Yes" : "No"}
|
||||
</Badge>
|
||||
<Badge variant={selectedAgent.data.agent.enable_file_write ? "default" : "secondary"}>
|
||||
File Write: {selectedAgent.data.agent.enable_file_write ? "Yes" : "No"}
|
||||
</Badge>
|
||||
<Badge variant={selectedAgent.data.agent.enable_network ? "default" : "secondary"}>
|
||||
Network: {selectedAgent.data.agent.enable_network ? "Yes" : "No"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
|
@@ -580,4 +580,4 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
448
src/lib/api.ts
448
src/lib/api.ts
@@ -106,83 +106,6 @@ export interface ClaudeInstallation {
|
||||
source: string;
|
||||
}
|
||||
|
||||
// Sandbox API types
|
||||
export interface SandboxProfile {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
is_active: boolean;
|
||||
is_default: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SandboxRule {
|
||||
id?: number;
|
||||
profile_id: number;
|
||||
operation_type: string;
|
||||
pattern_type: string;
|
||||
pattern_value: string;
|
||||
enabled: boolean;
|
||||
platform_support?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PlatformCapabilities {
|
||||
os: string;
|
||||
sandboxing_supported: boolean;
|
||||
operations: OperationSupport[];
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
export interface OperationSupport {
|
||||
operation: string;
|
||||
support_level: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Sandbox violation types
|
||||
export interface SandboxViolation {
|
||||
id?: number;
|
||||
profile_id?: number;
|
||||
agent_id?: number;
|
||||
agent_run_id?: number;
|
||||
operation_type: string;
|
||||
pattern_value?: string;
|
||||
process_name?: string;
|
||||
pid?: number;
|
||||
denied_at: string;
|
||||
}
|
||||
|
||||
export interface SandboxViolationStats {
|
||||
total: number;
|
||||
recent_24h: number;
|
||||
by_operation: Array<{
|
||||
operation: string;
|
||||
count: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Import/Export types
|
||||
export interface SandboxProfileExport {
|
||||
version: number;
|
||||
exported_at: string;
|
||||
platform: string;
|
||||
profiles: SandboxProfileWithRules[];
|
||||
}
|
||||
|
||||
export interface SandboxProfileWithRules {
|
||||
profile: SandboxProfile;
|
||||
rules: SandboxRule[];
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
profile_name: string;
|
||||
imported: boolean;
|
||||
reason?: string;
|
||||
new_name?: string;
|
||||
}
|
||||
|
||||
// Agent API types
|
||||
export interface Agent {
|
||||
id?: number;
|
||||
@@ -191,10 +114,6 @@ export interface Agent {
|
||||
system_prompt: string;
|
||||
default_task?: string;
|
||||
model: string;
|
||||
sandbox_enabled: boolean;
|
||||
enable_file_read: boolean;
|
||||
enable_file_write: boolean;
|
||||
enable_network: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -208,10 +127,6 @@ export interface AgentExport {
|
||||
system_prompt: string;
|
||||
default_task?: string;
|
||||
model: string;
|
||||
sandbox_enabled: boolean;
|
||||
enable_file_read: boolean;
|
||||
enable_file_write: boolean;
|
||||
enable_network: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -718,10 +633,6 @@ export const api = {
|
||||
* @param system_prompt - The system prompt for the agent
|
||||
* @param default_task - Optional default task
|
||||
* @param model - Optional model (defaults to 'sonnet')
|
||||
* @param sandbox_enabled - Optional sandbox enable flag
|
||||
* @param enable_file_read - Optional file read permission
|
||||
* @param enable_file_write - Optional file write permission
|
||||
* @param enable_network - Optional network permission
|
||||
* @returns Promise resolving to the created agent
|
||||
*/
|
||||
async createAgent(
|
||||
@@ -729,11 +640,7 @@ export const api = {
|
||||
icon: string,
|
||||
system_prompt: string,
|
||||
default_task?: string,
|
||||
model?: string,
|
||||
sandbox_enabled?: boolean,
|
||||
enable_file_read?: boolean,
|
||||
enable_file_write?: boolean,
|
||||
enable_network?: boolean
|
||||
model?: string
|
||||
): Promise<Agent> {
|
||||
try {
|
||||
return await invoke<Agent>('create_agent', {
|
||||
@@ -741,11 +648,7 @@ export const api = {
|
||||
icon,
|
||||
systemPrompt: system_prompt,
|
||||
defaultTask: default_task,
|
||||
model,
|
||||
sandboxEnabled: sandbox_enabled,
|
||||
enableFileRead: enable_file_read,
|
||||
enableFileWrite: enable_file_write,
|
||||
enableNetwork: enable_network
|
||||
model
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to create agent:", error);
|
||||
@@ -761,10 +664,6 @@ export const api = {
|
||||
* @param system_prompt - The updated system prompt
|
||||
* @param default_task - Optional default task
|
||||
* @param model - Optional model
|
||||
* @param sandbox_enabled - Optional sandbox enable flag
|
||||
* @param enable_file_read - Optional file read permission
|
||||
* @param enable_file_write - Optional file write permission
|
||||
* @param enable_network - Optional network permission
|
||||
* @returns Promise resolving to the updated agent
|
||||
*/
|
||||
async updateAgent(
|
||||
@@ -773,11 +672,7 @@ export const api = {
|
||||
icon: string,
|
||||
system_prompt: string,
|
||||
default_task?: string,
|
||||
model?: string,
|
||||
sandbox_enabled?: boolean,
|
||||
enable_file_read?: boolean,
|
||||
enable_file_write?: boolean,
|
||||
enable_network?: boolean
|
||||
model?: string
|
||||
): Promise<Agent> {
|
||||
try {
|
||||
return await invoke<Agent>('update_agent', {
|
||||
@@ -786,11 +681,7 @@ export const api = {
|
||||
icon,
|
||||
systemPrompt: system_prompt,
|
||||
defaultTask: default_task,
|
||||
model,
|
||||
sandboxEnabled: sandbox_enabled,
|
||||
enableFileRead: enable_file_read,
|
||||
enableFileWrite: enable_file_write,
|
||||
enableNetwork: enable_network
|
||||
model
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update agent:", error);
|
||||
@@ -1092,337 +983,6 @@ export const api = {
|
||||
return invoke("search_files", { basePath, query });
|
||||
},
|
||||
|
||||
// Sandbox API methods
|
||||
|
||||
/**
|
||||
* Lists all sandbox profiles
|
||||
* @returns Promise resolving to an array of sandbox profiles
|
||||
*/
|
||||
async listSandboxProfiles(): Promise<SandboxProfile[]> {
|
||||
try {
|
||||
return await invoke<SandboxProfile[]>('list_sandbox_profiles');
|
||||
} catch (error) {
|
||||
console.error("Failed to list sandbox profiles:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new sandbox profile
|
||||
* @param name - The profile name
|
||||
* @param description - Optional description
|
||||
* @returns Promise resolving to the created profile
|
||||
*/
|
||||
async createSandboxProfile(name: string, description?: string): Promise<SandboxProfile> {
|
||||
try {
|
||||
return await invoke<SandboxProfile>('create_sandbox_profile', { name, description });
|
||||
} catch (error) {
|
||||
console.error("Failed to create sandbox profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a sandbox profile
|
||||
* @param id - The profile ID
|
||||
* @param name - The updated name
|
||||
* @param description - Optional description
|
||||
* @param is_active - Whether the profile is active
|
||||
* @param is_default - Whether the profile is the default
|
||||
* @returns Promise resolving to the updated profile
|
||||
*/
|
||||
async updateSandboxProfile(
|
||||
id: number,
|
||||
name: string,
|
||||
description: string | undefined,
|
||||
is_active: boolean,
|
||||
is_default: boolean
|
||||
): Promise<SandboxProfile> {
|
||||
try {
|
||||
return await invoke<SandboxProfile>('update_sandbox_profile', {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
is_active,
|
||||
is_default
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update sandbox profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a sandbox profile
|
||||
* @param id - The profile ID to delete
|
||||
* @returns Promise resolving when the profile is deleted
|
||||
*/
|
||||
async deleteSandboxProfile(id: number): Promise<void> {
|
||||
try {
|
||||
return await invoke('delete_sandbox_profile', { id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete sandbox profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a single sandbox profile by ID
|
||||
* @param id - The profile ID
|
||||
* @returns Promise resolving to the profile
|
||||
*/
|
||||
async getSandboxProfile(id: number): Promise<SandboxProfile> {
|
||||
try {
|
||||
return await invoke<SandboxProfile>('get_sandbox_profile', { id });
|
||||
} catch (error) {
|
||||
console.error("Failed to get sandbox profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Lists rules for a sandbox profile
|
||||
* @param profileId - The profile ID
|
||||
* @returns Promise resolving to an array of rules
|
||||
*/
|
||||
async listSandboxRules(profileId: number): Promise<SandboxRule[]> {
|
||||
try {
|
||||
return await invoke<SandboxRule[]>('list_sandbox_rules', { profile_id: profileId });
|
||||
} catch (error) {
|
||||
console.error("Failed to list sandbox rules:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new sandbox rule
|
||||
* @param profileId - The profile ID
|
||||
* @param operation_type - The operation type
|
||||
* @param pattern_type - The pattern type
|
||||
* @param pattern_value - The pattern value
|
||||
* @param enabled - Whether the rule is enabled
|
||||
* @param platform_support - Optional platform support JSON
|
||||
* @returns Promise resolving to the created rule
|
||||
*/
|
||||
async createSandboxRule(
|
||||
profileId: number,
|
||||
operation_type: string,
|
||||
pattern_type: string,
|
||||
pattern_value: string,
|
||||
enabled: boolean,
|
||||
platform_support?: string
|
||||
): Promise<SandboxRule> {
|
||||
try {
|
||||
return await invoke<SandboxRule>('create_sandbox_rule', {
|
||||
profile_id: profileId,
|
||||
operation_type,
|
||||
pattern_type,
|
||||
pattern_value,
|
||||
enabled,
|
||||
platform_support
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to create sandbox rule:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a sandbox rule
|
||||
* @param id - The rule ID
|
||||
* @param operation_type - The operation type
|
||||
* @param pattern_type - The pattern type
|
||||
* @param pattern_value - The pattern value
|
||||
* @param enabled - Whether the rule is enabled
|
||||
* @param platform_support - Optional platform support JSON
|
||||
* @returns Promise resolving to the updated rule
|
||||
*/
|
||||
async updateSandboxRule(
|
||||
id: number,
|
||||
operation_type: string,
|
||||
pattern_type: string,
|
||||
pattern_value: string,
|
||||
enabled: boolean,
|
||||
platform_support?: string
|
||||
): Promise<SandboxRule> {
|
||||
try {
|
||||
return await invoke<SandboxRule>('update_sandbox_rule', {
|
||||
id,
|
||||
operation_type,
|
||||
pattern_type,
|
||||
pattern_value,
|
||||
enabled,
|
||||
platform_support
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update sandbox rule:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a sandbox rule
|
||||
* @param id - The rule ID to delete
|
||||
* @returns Promise resolving when the rule is deleted
|
||||
*/
|
||||
async deleteSandboxRule(id: number): Promise<void> {
|
||||
try {
|
||||
return await invoke('delete_sandbox_rule', { id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete sandbox rule:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets platform capabilities for sandbox configuration
|
||||
* @returns Promise resolving to platform capabilities
|
||||
*/
|
||||
async getPlatformCapabilities(): Promise<PlatformCapabilities> {
|
||||
try {
|
||||
return await invoke<PlatformCapabilities>('get_platform_capabilities');
|
||||
} catch (error) {
|
||||
console.error("Failed to get platform capabilities:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests a sandbox profile
|
||||
* @param profileId - The profile ID to test
|
||||
* @returns Promise resolving to test result message
|
||||
*/
|
||||
async testSandboxProfile(profileId: number): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>('test_sandbox_profile', { profile_id: profileId });
|
||||
} catch (error) {
|
||||
console.error("Failed to test sandbox profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Sandbox violation methods
|
||||
|
||||
/**
|
||||
* Lists sandbox violations with optional filtering
|
||||
* @param profileId - Optional profile ID to filter by
|
||||
* @param agentId - Optional agent ID to filter by
|
||||
* @param limit - Optional limit on number of results
|
||||
* @returns Promise resolving to array of violations
|
||||
*/
|
||||
async listSandboxViolations(profileId?: number, agentId?: number, limit?: number): Promise<SandboxViolation[]> {
|
||||
try {
|
||||
return await invoke<SandboxViolation[]>('list_sandbox_violations', {
|
||||
profile_id: profileId,
|
||||
agent_id: agentId,
|
||||
limit
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to list sandbox violations:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Logs a sandbox violation
|
||||
* @param violation - The violation details
|
||||
* @returns Promise resolving when logged
|
||||
*/
|
||||
async logSandboxViolation(violation: {
|
||||
profileId?: number;
|
||||
agentId?: number;
|
||||
agentRunId?: number;
|
||||
operationType: string;
|
||||
patternValue?: string;
|
||||
processName?: string;
|
||||
pid?: number;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
return await invoke('log_sandbox_violation', {
|
||||
profile_id: violation.profileId,
|
||||
agent_id: violation.agentId,
|
||||
agent_run_id: violation.agentRunId,
|
||||
operation_type: violation.operationType,
|
||||
pattern_value: violation.patternValue,
|
||||
process_name: violation.processName,
|
||||
pid: violation.pid
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to log sandbox violation:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears old sandbox violations
|
||||
* @param olderThanDays - Optional days to keep (clears all if not specified)
|
||||
* @returns Promise resolving to number of deleted violations
|
||||
*/
|
||||
async clearSandboxViolations(olderThanDays?: number): Promise<number> {
|
||||
try {
|
||||
return await invoke<number>('clear_sandbox_violations', { older_than_days: olderThanDays });
|
||||
} catch (error) {
|
||||
console.error("Failed to clear sandbox violations:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets sandbox violation statistics
|
||||
* @returns Promise resolving to violation stats
|
||||
*/
|
||||
async getSandboxViolationStats(): Promise<SandboxViolationStats> {
|
||||
try {
|
||||
return await invoke<SandboxViolationStats>('get_sandbox_violation_stats');
|
||||
} catch (error) {
|
||||
console.error("Failed to get sandbox violation stats:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Import/Export methods
|
||||
|
||||
/**
|
||||
* Exports a single sandbox profile with its rules
|
||||
* @param profileId - The profile ID to export
|
||||
* @returns Promise resolving to export data
|
||||
*/
|
||||
async exportSandboxProfile(profileId: number): Promise<SandboxProfileExport> {
|
||||
try {
|
||||
return await invoke<SandboxProfileExport>('export_sandbox_profile', { profile_id: profileId });
|
||||
} catch (error) {
|
||||
console.error("Failed to export sandbox profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Exports all sandbox profiles
|
||||
* @returns Promise resolving to export data
|
||||
*/
|
||||
async exportAllSandboxProfiles(): Promise<SandboxProfileExport> {
|
||||
try {
|
||||
return await invoke<SandboxProfileExport>('export_all_sandbox_profiles');
|
||||
} catch (error) {
|
||||
console.error("Failed to export all sandbox profiles:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Imports sandbox profiles from export data
|
||||
* @param exportData - The export data to import
|
||||
* @returns Promise resolving to import results
|
||||
*/
|
||||
async importSandboxProfiles(exportData: SandboxProfileExport): Promise<ImportResult[]> {
|
||||
try {
|
||||
return await invoke<ImportResult[]>('import_sandbox_profiles', { export_data: exportData });
|
||||
} catch (error) {
|
||||
console.error("Failed to import sandbox profiles:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets overall usage statistics
|
||||
* @returns Promise resolving to usage statistics
|
||||
|
Reference in New Issue
Block a user