feat(security): Add Phase 1 security features
* test: add comprehensive recovery tests for agent loop Add recovery test coverage for all edge cases and failure scenarios in the agentic loop, addressing the missing test coverage for recovery use cases. Tool Call Parsing Edge Cases: - Empty tool_result tags - Empty tool_calls arrays - Whitespace-only tool names - Empty string arguments History Management: - Trimming without system prompt - Role ordering consistency after trim - Only system prompt edge case Arguments Parsing: - Invalid JSON string fallback - None arguments handling - Null value handling JSON Extraction: - Empty input handling - Whitespace only input - Multiple JSON objects - JSON arrays Tool Call Value Parsing: - Missing name field - Non-OpenAI format - Empty tool_calls array - Missing tool_calls field fallback - Top-level array format Constants Validation: - MAX_TOOL_ITERATIONS bounds (prevent runaway loops) - MAX_HISTORY_MESSAGES bounds (prevent memory bloat) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(security): Add Phase 1 security features - sandboxing, resource limits, audit logging Phase 1 security enhancements with zero impact on the quick setup wizard: - ✅ Pluggable sandbox trait system (traits.rs) - ✅ Landlock sandbox support (Linux kernel 5.13+) - ✅ Firejail sandbox support (Linux user-space) - ✅ Bubblewrap sandbox support (Linux/macOS user namespaces) - ✅ Docker sandbox support (container isolation) - ✅ No-op fallback (application-layer security only) - ✅ Auto-detection logic (detect.rs) - ✅ Audit logging with HMAC signing support (audit.rs) - ✅ SecurityConfig schema (SandboxConfig, ResourceLimitsConfig, AuditConfig) - ✅ Feature-gated implementation (sandbox-landlock, sandbox-bubblewrap) - ✅ 1,265 tests passing Key design principles: - Silent auto-detection: no new prompts in wizard - Graceful degradation: works on all platforms - Feature flags: zero overhead when disabled - Pluggable architecture: swap sandbox backends via config - Backward compatible: existing configs work unchanged Config usage: ```toml [security.sandbox] enabled = false # Explicitly disable backend = "auto" # auto, landlock, firejail, bubblewrap, docker, none [security.resources] max_memory_mb = 512 max_cpu_time_seconds = 60 [security.audit] enabled = true log_path = "audit.log" sign_events = false ``` Security documentation: - docs/sandboxing.md: Sandbox implementation strategies - docs/resource-limits.md: Resource limit approaches - docs/audit-logging.md: Audit logging specification - docs/security-roadmap.md: 3-phase implementation plan - docs/frictionless-security.md: Zero-impact wizard design - docs/agnostic-security.md: Platform/hardware agnostic approach Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1140a7887d
commit
0383a82a6f
22 changed files with 4129 additions and 13 deletions
151
src/security/detect.rs
Normal file
151
src/security/detect.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
//! Auto-detection of available security features
|
||||
|
||||
use crate::config::{SandboxBackend, SecurityConfig};
|
||||
use crate::security::traits::Sandbox;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Create a sandbox based on auto-detection or explicit config
|
||||
pub fn create_sandbox(config: &SecurityConfig) -> Arc<dyn Sandbox> {
|
||||
let backend = &config.sandbox.backend;
|
||||
|
||||
// If explicitly disabled, return noop
|
||||
if matches!(backend, SandboxBackend::None) || config.sandbox.enabled == Some(false) {
|
||||
return Arc::new(super::traits::NoopSandbox);
|
||||
}
|
||||
|
||||
// If specific backend requested, try that
|
||||
match backend {
|
||||
SandboxBackend::Landlock => {
|
||||
#[cfg(feature = "sandbox-landlock")]
|
||||
{
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(sandbox) = super::landlock::LandlockSandbox::new() {
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!("Landlock requested but not available, falling back to application-layer");
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Firejail => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(sandbox) = super::firejail::FirejailSandbox::new() {
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
tracing::warn!("Firejail requested but not available, falling back to application-layer");
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Bubblewrap => {
|
||||
#[cfg(feature = "sandbox-bubblewrap")]
|
||||
{
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
{
|
||||
if let Ok(sandbox) = super::bubblewrap::BubblewrapSandbox::new() {
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!("Bubblewrap requested but not available, falling back to application-layer");
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Docker => {
|
||||
if let Ok(sandbox) = super::docker::DockerSandbox::new() {
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
tracing::warn!("Docker requested but not available, falling back to application-layer");
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Auto | SandboxBackend::None => {
|
||||
// Auto-detect best available
|
||||
detect_best_sandbox()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-detect the best available sandbox
|
||||
fn detect_best_sandbox() -> Arc<dyn Sandbox> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Try Landlock first (native, no dependencies)
|
||||
#[cfg(feature = "sandbox-landlock")]
|
||||
{
|
||||
if let Ok(sandbox) = super::landlock::LandlockSandbox::probe() {
|
||||
tracing::info!("Landlock sandbox enabled (Linux kernel 5.13+)");
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
|
||||
// Try Firejail second (user-space tool)
|
||||
if let Ok(sandbox) = super::firejail::FirejailSandbox::probe() {
|
||||
tracing::info!("Firejail sandbox enabled");
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Try Bubblewrap on macOS
|
||||
#[cfg(feature = "sandbox-bubblewrap")]
|
||||
{
|
||||
if let Ok(sandbox) = super::bubblewrap::BubblewrapSandbox::probe() {
|
||||
tracing::info!("Bubblewrap sandbox enabled");
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Docker is heavy but works everywhere if docker is installed
|
||||
if let Ok(sandbox) = super::docker::DockerSandbox::probe() {
|
||||
tracing::info!("Docker sandbox enabled");
|
||||
return Arc::new(sandbox);
|
||||
}
|
||||
|
||||
// Fallback: application-layer security only
|
||||
tracing::info!("No sandbox backend available, using application-layer security");
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{SandboxConfig, SecurityConfig};
|
||||
|
||||
#[test]
|
||||
fn detect_best_sandbox_returns_something() {
|
||||
let sandbox = detect_best_sandbox();
|
||||
// Should always return at least NoopSandbox
|
||||
assert!(sandbox.is_available());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_none_returns_noop() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: Some(false),
|
||||
backend: SandboxBackend::None,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
assert_eq!(sandbox.name(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_mode_detects_something() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: None, // Auto-detect
|
||||
backend: SandboxBackend::Auto,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox = create_sandbox(&config);
|
||||
// Should return some sandbox (at least NoopSandbox)
|
||||
assert!(sandbox.is_available());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue