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:
Argenis 2026-02-16 04:14:16 -05:00 committed by GitHub
parent 1140a7887d
commit 0383a82a6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 4129 additions and 13 deletions

View file

@ -68,6 +68,12 @@ pub struct Config {
#[serde(default)]
pub identity: IdentityConfig,
/// Hardware Abstraction Layer (HAL) configuration.
/// Controls how ZeroClaw interfaces with physical hardware
/// (GPIO, serial, debug probes).
#[serde(default)]
pub hardware: crate::hardware::HardwareConfig,
/// Named delegate agents for agent-to-agent handoff.
///
/// ```toml
@ -83,6 +89,10 @@ pub struct Config {
/// ```
#[serde(default)]
pub agents: HashMap<String, DelegateAgentConfig>,
/// Security configuration (sandboxing, resource limits, audit logging)
#[serde(default)]
pub security: SecurityConfig,
}
// ── Identity (AIEOS / OpenClaw format) ──────────────────────────
@ -907,6 +917,174 @@ pub struct LarkConfig {
pub use_feishu: bool,
}
// ── Security Config ─────────────────────────────────────────────────
/// Security configuration for sandboxing, resource limits, and audit logging
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
/// Sandbox configuration
#[serde(default)]
pub sandbox: SandboxConfig,
/// Resource limits
#[serde(default)]
pub resources: ResourceLimitsConfig,
/// Audit logging configuration
#[serde(default)]
pub audit: AuditConfig,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
sandbox: SandboxConfig::default(),
resources: ResourceLimitsConfig::default(),
audit: AuditConfig::default(),
}
}
}
/// Sandbox configuration for OS-level isolation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig {
/// Enable sandboxing (None = auto-detect, Some = explicit)
#[serde(default)]
pub enabled: Option<bool>,
/// Sandbox backend to use
#[serde(default)]
pub backend: SandboxBackend,
/// Custom Firejail arguments (when backend = firejail)
#[serde(default)]
pub firejail_args: Vec<String>,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
enabled: None, // Auto-detect
backend: SandboxBackend::Auto,
firejail_args: Vec::new(),
}
}
}
/// Sandbox backend selection
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SandboxBackend {
/// Auto-detect best available (default)
Auto,
/// Landlock (Linux kernel LSM, native)
Landlock,
/// Firejail (user-space sandbox)
Firejail,
/// Bubblewrap (user namespaces)
Bubblewrap,
/// Docker container isolation
Docker,
/// No sandboxing (application-layer only)
None,
}
impl Default for SandboxBackend {
fn default() -> Self {
Self::Auto
}
}
/// Resource limits for command execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimitsConfig {
/// Maximum memory in MB per command
#[serde(default = "default_max_memory_mb")]
pub max_memory_mb: u32,
/// Maximum CPU time in seconds per command
#[serde(default = "default_max_cpu_time_seconds")]
pub max_cpu_time_seconds: u64,
/// Maximum number of subprocesses
#[serde(default = "default_max_subprocesses")]
pub max_subprocesses: u32,
/// Enable memory monitoring
#[serde(default = "default_memory_monitoring_enabled")]
pub memory_monitoring: bool,
}
fn default_max_memory_mb() -> u32 {
512
}
fn default_max_cpu_time_seconds() -> u64 {
60
}
fn default_max_subprocesses() -> u32 {
10
}
fn default_memory_monitoring_enabled() -> bool {
true
}
impl Default for ResourceLimitsConfig {
fn default() -> Self {
Self {
max_memory_mb: default_max_memory_mb(),
max_cpu_time_seconds: default_max_cpu_time_seconds(),
max_subprocesses: default_max_subprocesses(),
memory_monitoring: default_memory_monitoring_enabled(),
}
}
}
/// Audit logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditConfig {
/// Enable audit logging
#[serde(default = "default_audit_enabled")]
pub enabled: bool,
/// Path to audit log file (relative to zeroclaw dir)
#[serde(default = "default_audit_log_path")]
pub log_path: String,
/// Maximum log size in MB before rotation
#[serde(default = "default_audit_max_size_mb")]
pub max_size_mb: u32,
/// Sign events with HMAC for tamper evidence
#[serde(default)]
pub sign_events: bool,
}
fn default_audit_enabled() -> bool {
true
}
fn default_audit_log_path() -> String {
"audit.log".to_string()
}
fn default_audit_max_size_mb() -> u32 {
100
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
enabled: default_audit_enabled(),
log_path: default_audit_log_path(),
max_size_mb: default_audit_max_size_mb(),
sign_events: false,
}
}
}
// ── Config impl ──────────────────────────────────────────────────
impl Default for Config {
@ -937,7 +1115,9 @@ impl Default for Config {
browser: BrowserConfig::default(),
http_request: HttpRequestConfig::default(),
identity: IdentityConfig::default(),
hardware: crate::hardware::HardwareConfig::default(),
agents: HashMap::new(),
security: SecurityConfig::default(),
}
}
}
@ -1289,7 +1469,9 @@ mod tests {
browser: BrowserConfig::default(),
http_request: HttpRequestConfig::default(),
identity: IdentityConfig::default(),
hardware: crate::hardware::HardwareConfig::default(),
agents: HashMap::new(),
security: SecurityConfig::default(),
};
let toml_str = toml::to_string_pretty(&config).unwrap();
@ -1362,7 +1544,9 @@ default_temperature = 0.7
browser: BrowserConfig::default(),
http_request: HttpRequestConfig::default(),
identity: IdentityConfig::default(),
hardware: crate::hardware::HardwareConfig::default(),
agents: HashMap::new(),
security: SecurityConfig::default(),
};
config.save().unwrap();
@ -1428,6 +1612,7 @@ default_temperature = 0.7
bot_token: "discord-token".into(),
guild_id: Some("12345".into()),
allowed_users: vec![],
listen_to_bots: false,
};
let json = serde_json::to_string(&dc).unwrap();
let parsed: DiscordConfig = serde_json::from_str(&json).unwrap();
@ -1441,6 +1626,7 @@ default_temperature = 0.7
bot_token: "tok".into(),
guild_id: None,
allowed_users: vec![],
listen_to_bots: false,
};
let json = serde_json::to_string(&dc).unwrap();
let parsed: DiscordConfig = serde_json::from_str(&json).unwrap();