diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index bdf5eb971..be60af0b7 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -3820,7 +3820,10 @@ mod tests { tool_call_id: None, }, ]; - let approval_cfg = crate::config::AutonomyConfig::default(); + let approval_cfg = crate::config::AutonomyConfig { + level: crate::security::AutonomyLevel::Supervised, + ..crate::config::AutonomyConfig::default() + }; let approval_mgr = ApprovalManager::from_config(&approval_cfg); assert!(!should_execute_tools_in_parallel( diff --git a/src/config/schema.rs b/src/config/schema.rs index 35e6439c5..6b01bb22f 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -1997,9 +1997,14 @@ pub struct BuiltinHooksConfig { /// /// Controls what the agent is allowed to do: shell commands, filesystem access, /// risk approval gates, and per-policy budgets. +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct AutonomyConfig { - /// Autonomy level: `read_only`, `supervised` (default), or `full`. + /// Master toggle for shell security policy enforcement. + /// When false (default), command restrictions, path blocks, and rate limits are disabled. + #[serde(default)] + pub enabled: bool, + /// Autonomy level: `read_only`, `supervised`, or `full` (default). pub level: AutonomyLevel, /// Restrict absolute filesystem paths to workspace-relative references. Default: `true`. /// Resolved paths outside the workspace still require `allowed_roots`. @@ -2070,7 +2075,8 @@ fn is_valid_env_var_name(name: &str) -> bool { impl Default for AutonomyConfig { fn default() -> Self { Self { - level: AutonomyLevel::Supervised, + enabled: false, + level: AutonomyLevel::Full, workspace_only: true, allowed_commands: vec![ "git".into(), @@ -4546,6 +4552,19 @@ impl Config { } } + // Security policy master toggle: ZEROCLAW_SECURITY_POLICY + if let Ok(flag) = std::env::var("ZEROCLAW_SECURITY_POLICY") { + if !flag.trim().is_empty() { + match flag.trim().to_ascii_lowercase().as_str() { + "1" | "true" | "yes" | "on" => self.autonomy.enabled = true, + "0" | "false" | "no" | "off" => self.autonomy.enabled = false, + _ => tracing::warn!( + "Ignoring invalid ZEROCLAW_SECURITY_POLICY (valid: 1|0|true|false|yes|no|on|off)" + ), + } + } + } + // Gateway port: ZEROCLAW_GATEWAY_PORT or PORT if let Ok(port_str) = std::env::var("ZEROCLAW_GATEWAY_PORT").or_else(|_| std::env::var("PORT")) @@ -5000,7 +5019,8 @@ mod tests { #[test] async fn autonomy_config_default() { let a = AutonomyConfig::default(); - assert_eq!(a.level, AutonomyLevel::Supervised); + assert!(!a.enabled); + assert_eq!(a.level, AutonomyLevel::Full); assert!(a.workspace_only); assert!(a.allowed_commands.contains(&"git".to_string())); assert!(a.allowed_commands.contains(&"cargo".to_string())); @@ -5131,6 +5151,7 @@ default_temperature = 0.7 ..ObservabilityConfig::default() }, autonomy: AutonomyConfig { + enabled: false, level: AutonomyLevel::Full, workspace_only: false, allowed_commands: vec!["docker".into()], @@ -5256,7 +5277,7 @@ default_temperature = 0.7 assert!(parsed.default_provider.is_none()); assert_eq!(parsed.observability.backend, "none"); assert_eq!(parsed.observability.runtime_trace_mode, "none"); - assert_eq!(parsed.autonomy.level, AutonomyLevel::Supervised); + assert_eq!(parsed.autonomy.level, AutonomyLevel::Full); assert_eq!(parsed.runtime.kind, "native"); assert!(!parsed.heartbeat.enabled); assert!(parsed.channels_config.cli); diff --git a/src/cron/scheduler.rs b/src/cron/scheduler.rs index 5df493fbd..d80916f48 100644 --- a/src/cron/scheduler.rs +++ b/src/cron/scheduler.rs @@ -569,6 +569,7 @@ mod tests { async fn run_job_command_blocks_forbidden_path_argument() { let tmp = TempDir::new().unwrap(); let mut config = test_config(&tmp).await; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["cat".into()]; let job = test_job("cat /etc/passwd"); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); @@ -584,6 +585,7 @@ mod tests { async fn run_job_command_blocks_forbidden_option_assignment_path_argument() { let tmp = TempDir::new().unwrap(); let mut config = test_config(&tmp).await; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["grep".into()]; let job = test_job("grep --file=/etc/passwd root ./src"); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); @@ -599,6 +601,7 @@ mod tests { async fn run_job_command_blocks_forbidden_short_option_attached_path_argument() { let tmp = TempDir::new().unwrap(); let mut config = test_config(&tmp).await; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["grep".into()]; let job = test_job("grep -f/etc/passwd root ./src"); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); @@ -614,6 +617,7 @@ mod tests { async fn run_job_command_blocks_tilde_user_path_argument() { let tmp = TempDir::new().unwrap(); let mut config = test_config(&tmp).await; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["cat".into()]; let job = test_job("cat ~root/.ssh/id_rsa"); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); @@ -657,6 +661,7 @@ mod tests { async fn run_job_command_blocks_rate_limited() { let tmp = TempDir::new().unwrap(); let mut config = test_config(&tmp).await; + config.autonomy.enabled = true; config.autonomy.max_actions_per_hour = 0; let job = test_job("echo should-not-run"); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); @@ -738,6 +743,7 @@ mod tests { async fn run_agent_job_blocks_rate_limited() { let tmp = TempDir::new().unwrap(); let mut config = test_config(&tmp).await; + config.autonomy.enabled = true; config.autonomy.max_actions_per_hour = 0; let mut job = test_job(""); job.job_type = JobType::Agent; diff --git a/src/security/mod.rs b/src/security/mod.rs index bbf8a7e51..c7a1720a1 100644 --- a/src/security/mod.rs +++ b/src/security/mod.rs @@ -78,7 +78,7 @@ mod tests { #[test] fn reexported_policy_and_pairing_types_are_usable() { let policy = SecurityPolicy::default(); - assert_eq!(policy.autonomy, AutonomyLevel::Supervised); + assert_eq!(policy.autonomy, AutonomyLevel::Full); let guard = PairingGuard::new(false, &[]); assert!(!guard.require_pairing()); diff --git a/src/security/policy.rs b/src/security/policy.rs index 835824074..95fc514ed 100644 --- a/src/security/policy.rs +++ b/src/security/policy.rs @@ -78,8 +78,11 @@ impl Clone for ActionTracker { } /// Security policy enforced on all tool executions +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub struct SecurityPolicy { + /// When false, command restrictions, path blocks, and rate limits are bypassed. + pub enabled: bool, pub autonomy: AutonomyLevel, pub workspace_dir: PathBuf, pub workspace_only: bool, @@ -97,7 +100,8 @@ pub struct SecurityPolicy { impl Default for SecurityPolicy { fn default() -> Self { Self { - autonomy: AutonomyLevel::Supervised, + enabled: false, + autonomy: AutonomyLevel::Full, workspace_dir: PathBuf::from("."), workspace_only: true, allowed_commands: vec![ @@ -673,6 +677,9 @@ impl SecurityPolicy { command: &str, approved: bool, ) -> Result { + if !self.enabled { + return Ok(CommandRiskLevel::Low); + } if !self.is_command_allowed(command) { return Err(format!("Command not allowed by security policy: {command}")); } @@ -824,6 +831,9 @@ impl SecurityPolicy { /// This is best-effort token parsing for shell commands and is intended /// as a safety gate before command execution. pub fn forbidden_path_argument(&self, command: &str) -> Option { + if !self.enabled { + return None; + } let forbidden_candidate = |raw: &str| { let candidate = strip_wrapping_quotes(raw).trim(); if candidate.is_empty() || candidate.contains("://") { @@ -1033,12 +1043,18 @@ impl SecurityPolicy { /// Record an action and check if the rate limit has been exceeded. /// Returns `true` if the action is allowed, `false` if rate-limited. pub fn record_action(&self) -> bool { + if !self.enabled { + return true; + } let count = self.tracker.record(); count <= self.max_actions_per_hour as usize } /// Check if the rate limit would be exceeded without recording. pub fn is_rate_limited(&self) -> bool { + if !self.enabled { + return false; + } self.tracker.count() >= self.max_actions_per_hour as usize } @@ -1048,6 +1064,7 @@ impl SecurityPolicy { workspace_dir: &Path, ) -> Self { Self { + enabled: autonomy_config.enabled, autonomy: autonomy_config.level, workspace_dir: workspace_dir.to_path_buf(), workspace_only: autonomy_config.workspace_only, @@ -1079,12 +1096,18 @@ impl SecurityPolicy { mod tests { use super::*; + /// Returns an **enabled** policy with supervised autonomy for enforcement tests. fn default_policy() -> SecurityPolicy { - SecurityPolicy::default() + SecurityPolicy { + enabled: true, + autonomy: AutonomyLevel::Supervised, + ..SecurityPolicy::default() + } } fn readonly_policy() -> SecurityPolicy { SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() } @@ -1092,6 +1115,7 @@ mod tests { fn full_policy() -> SecurityPolicy { SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Full, ..SecurityPolicy::default() } @@ -1218,6 +1242,7 @@ mod tests { #[test] fn allowlist_supports_wildcard_entry() { let p = SecurityPolicy { + enabled: true, allowed_commands: vec!["*".into()], ..SecurityPolicy::default() }; @@ -1309,6 +1334,7 @@ mod tests { #[test] fn validate_command_requires_approval_for_medium_risk() { let p = SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, require_approval_for_medium_risk: true, allowed_commands: vec!["touch".into()], @@ -1326,6 +1352,7 @@ mod tests { #[test] fn validate_command_blocks_high_risk_by_default() { let p = SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, allowed_commands: vec!["rm".into()], ..SecurityPolicy::default() @@ -1339,6 +1366,7 @@ mod tests { #[test] fn validate_command_full_mode_skips_medium_risk_approval_gate() { let p = SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Full, require_approval_for_medium_risk: true, allowed_commands: vec!["touch".into()], @@ -1424,6 +1452,7 @@ mod tests { #[test] fn from_config_maps_all_fields() { let autonomy_config = crate::config::AutonomyConfig { + enabled: true, level: AutonomyLevel::Full, workspace_only: false, allowed_commands: vec!["docker".into()], @@ -1438,6 +1467,7 @@ mod tests { let workspace = PathBuf::from("/tmp/test-workspace"); let policy = SecurityPolicy::from_config(&autonomy_config, &workspace); + assert!(policy.enabled); assert_eq!(policy.autonomy, AutonomyLevel::Full); assert!(!policy.workspace_only); assert_eq!(policy.allowed_commands, vec!["docker"]); @@ -1482,7 +1512,8 @@ mod tests { #[test] fn default_policy_has_sane_values() { let p = SecurityPolicy::default(); - assert_eq!(p.autonomy, AutonomyLevel::Supervised); + assert!(!p.enabled); + assert_eq!(p.autonomy, AutonomyLevel::Full); assert!(p.workspace_only); assert!(!p.allowed_commands.is_empty()); assert!(!p.forbidden_paths.is_empty()); @@ -1513,6 +1544,7 @@ mod tests { #[test] fn record_action_allows_within_limit() { let p = SecurityPolicy { + enabled: true, max_actions_per_hour: 5, ..SecurityPolicy::default() }; @@ -1524,6 +1556,7 @@ mod tests { #[test] fn record_action_blocks_over_limit() { let p = SecurityPolicy { + enabled: true, max_actions_per_hour: 3, ..SecurityPolicy::default() }; @@ -1536,6 +1569,7 @@ mod tests { #[test] fn is_rate_limited_reflects_count() { let p = SecurityPolicy { + enabled: true, max_actions_per_hour: 2, ..SecurityPolicy::default() }; @@ -1901,6 +1935,7 @@ mod tests { #[test] fn rate_limit_exactly_at_boundary() { let p = SecurityPolicy { + enabled: true, max_actions_per_hour: 1, ..SecurityPolicy::default() }; @@ -1912,6 +1947,7 @@ mod tests { #[test] fn rate_limit_zero_blocks_everything() { let p = SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }; @@ -1921,6 +1957,7 @@ mod tests { #[test] fn rate_limit_high_allows_many() { let p = SecurityPolicy { + enabled: true, max_actions_per_hour: 10000, ..SecurityPolicy::default() }; @@ -2335,4 +2372,36 @@ mod tests { "URL-encoded parent dir traversal must be blocked" ); } + + // ── Disabled policy (enabled=false) ────────────────────── + + #[test] + fn disabled_policy_allows_any_command() { + let p = SecurityPolicy { + enabled: false, + ..SecurityPolicy::default() + }; + let result = p.validate_command_execution("rm -rf /", false); + assert_eq!(result.unwrap(), CommandRiskLevel::Low); + } + + #[test] + fn disabled_policy_allows_forbidden_paths() { + let p = SecurityPolicy { + enabled: false, + ..SecurityPolicy::default() + }; + assert!(p.forbidden_path_argument("cat /etc/passwd").is_none()); + } + + #[test] + fn disabled_policy_skips_rate_limit() { + let p = SecurityPolicy { + enabled: false, + max_actions_per_hour: 0, + ..SecurityPolicy::default() + }; + assert!(!p.is_rate_limited()); + assert!(p.record_action()); + } } diff --git a/src/tools/browser_open.rs b/src/tools/browser_open.rs index 7ac5013f7..7a226c5f2 100644 --- a/src/tools/browser_open.rs +++ b/src/tools/browser_open.rs @@ -503,6 +503,7 @@ mod tests { #[tokio::test] async fn execute_blocks_readonly_mode() { let security = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -518,6 +519,7 @@ mod tests { #[tokio::test] async fn execute_blocks_when_rate_limited() { let security = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/src/tools/composio.rs b/src/tools/composio.rs index d414d1649..b4ede36fd 100644 --- a/src/tools/composio.rs +++ b/src/tools/composio.rs @@ -1419,6 +1419,7 @@ mod tests { #[tokio::test] async fn execute_blocked_in_readonly_mode() { let readonly = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -1441,6 +1442,7 @@ mod tests { #[tokio::test] async fn execute_blocked_when_rate_limited() { let limited = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/src/tools/content_search.rs b/src/tools/content_search.rs index 08a8ad428..11e7c00ac 100644 --- a/src/tools/content_search.rs +++ b/src/tools/content_search.rs @@ -660,6 +660,7 @@ mod tests { fn test_security(workspace: PathBuf) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, ..SecurityPolicy::default() @@ -672,6 +673,7 @@ mod tests { max_actions_per_hour: u32, ) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy, workspace_dir: workspace, max_actions_per_hour, diff --git a/src/tools/cron_add.rs b/src/tools/cron_add.rs index b13979e90..4ee3808b8 100644 --- a/src/tools/cron_add.rs +++ b/src/tools/cron_add.rs @@ -322,6 +322,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["echo".into()]; config.autonomy.level = AutonomyLevel::Supervised; tokio::fs::create_dir_all(&config.workspace_dir) @@ -378,6 +379,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Full; config.autonomy.max_actions_per_hour = 0; std::fs::create_dir_all(&config.workspace_dir).unwrap(); @@ -409,6 +411,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["touch".into()]; config.autonomy.level = AutonomyLevel::Supervised; std::fs::create_dir_all(&config.workspace_dir).unwrap(); diff --git a/src/tools/cron_remove.rs b/src/tools/cron_remove.rs index b4dc110c9..c4c106c3c 100644 --- a/src/tools/cron_remove.rs +++ b/src/tools/cron_remove.rs @@ -185,6 +185,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Full; config.autonomy.max_actions_per_hour = 0; std::fs::create_dir_all(&config.workspace_dir).unwrap(); diff --git a/src/tools/cron_run.rs b/src/tools/cron_run.rs index bb3c9e419..1bb471060 100644 --- a/src/tools/cron_run.rs +++ b/src/tools/cron_run.rs @@ -230,6 +230,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Supervised; config.autonomy.allowed_commands = vec!["touch".into()]; std::fs::create_dir_all(&config.workspace_dir).unwrap(); @@ -259,6 +260,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Full; config.autonomy.max_actions_per_hour = 0; std::fs::create_dir_all(&config.workspace_dir).unwrap(); diff --git a/src/tools/cron_update.rs b/src/tools/cron_update.rs index f41bacb15..c5d211d91 100644 --- a/src/tools/cron_update.rs +++ b/src/tools/cron_update.rs @@ -201,6 +201,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.allowed_commands = vec!["echo".into()]; tokio::fs::create_dir_all(&config.workspace_dir) .await @@ -253,6 +254,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Supervised; config.autonomy.allowed_commands = vec!["echo".into(), "touch".into()]; std::fs::create_dir_all(&config.workspace_dir).unwrap(); @@ -292,6 +294,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Full; config.autonomy.max_actions_per_hour = 0; std::fs::create_dir_all(&config.workspace_dir).unwrap(); diff --git a/src/tools/delegate.rs b/src/tools/delegate.rs index 44a87fcf4..7795bcf36 100644 --- a/src/tools/delegate.rs +++ b/src/tools/delegate.rs @@ -850,6 +850,7 @@ mod tests { #[tokio::test] async fn delegation_blocked_in_readonly_mode() { let readonly = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -869,6 +870,7 @@ mod tests { #[tokio::test] async fn delegation_blocked_when_rate_limited() { let limited = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/src/tools/file_edit.rs b/src/tools/file_edit.rs index 19c5f0cc6..dd51899a9 100644 --- a/src/tools/file_edit.rs +++ b/src/tools/file_edit.rs @@ -229,6 +229,7 @@ mod tests { fn test_security(workspace: std::path::PathBuf) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, ..SecurityPolicy::default() @@ -241,6 +242,7 @@ mod tests { max_actions_per_hour: u32, ) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy, workspace_dir: workspace, max_actions_per_hour, diff --git a/src/tools/file_read.rs b/src/tools/file_read.rs index 3d7c03e0e..406ae8f7b 100644 --- a/src/tools/file_read.rs +++ b/src/tools/file_read.rs @@ -240,6 +240,7 @@ mod tests { fn test_security(workspace: std::path::PathBuf) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, ..SecurityPolicy::default() @@ -252,6 +253,7 @@ mod tests { max_actions_per_hour: u32, ) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy, workspace_dir: workspace, max_actions_per_hour, diff --git a/src/tools/file_write.rs b/src/tools/file_write.rs index 7ce604eb4..1c68bdc40 100644 --- a/src/tools/file_write.rs +++ b/src/tools/file_write.rs @@ -168,6 +168,7 @@ mod tests { fn test_security(workspace: std::path::PathBuf) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, ..SecurityPolicy::default() @@ -180,6 +181,7 @@ mod tests { max_actions_per_hour: u32, ) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy, workspace_dir: workspace, max_actions_per_hour, diff --git a/src/tools/glob_search.rs b/src/tools/glob_search.rs index 179f3ccc1..abb22bdd2 100644 --- a/src/tools/glob_search.rs +++ b/src/tools/glob_search.rs @@ -179,6 +179,7 @@ mod tests { fn test_security(workspace: PathBuf) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, ..SecurityPolicy::default() @@ -191,6 +192,7 @@ mod tests { max_actions_per_hour: u32, ) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy, workspace_dir: workspace, max_actions_per_hour, diff --git a/src/tools/http_request.rs b/src/tools/http_request.rs index 513ba554b..967b54098 100644 --- a/src/tools/http_request.rs +++ b/src/tools/http_request.rs @@ -683,6 +683,7 @@ mod tests { #[tokio::test] async fn execute_blocks_readonly_mode() { let security = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -698,6 +699,7 @@ mod tests { #[tokio::test] async fn execute_blocks_when_rate_limited() { let security = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/src/tools/memory_forget.rs b/src/tools/memory_forget.rs index 67e8ce615..ecde00897 100644 --- a/src/tools/memory_forget.rs +++ b/src/tools/memory_forget.rs @@ -142,6 +142,7 @@ mod tests { .await .unwrap(); let readonly = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -163,6 +164,7 @@ mod tests { .await .unwrap(); let limited = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/src/tools/memory_store.rs b/src/tools/memory_store.rs index 5d7d0439e..af64a6137 100644 --- a/src/tools/memory_store.rs +++ b/src/tools/memory_store.rs @@ -184,6 +184,7 @@ mod tests { async fn store_blocked_in_readonly_mode() { let (_tmp, mem) = test_mem(); let readonly = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -205,6 +206,7 @@ mod tests { async fn store_blocked_when_rate_limited() { let (_tmp, mem) = test_mem(); let limited = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/src/tools/pdf_read.rs b/src/tools/pdf_read.rs index 15cb2092c..671792aa8 100644 --- a/src/tools/pdf_read.rs +++ b/src/tools/pdf_read.rs @@ -236,6 +236,7 @@ mod tests { fn test_security(workspace: std::path::PathBuf) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, ..SecurityPolicy::default() @@ -247,6 +248,7 @@ mod tests { max_actions: u32, ) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: workspace, max_actions_per_hour: max_actions, diff --git a/src/tools/pushover.rs b/src/tools/pushover.rs index 7e64e9a5b..f4dcbcfcd 100644 --- a/src/tools/pushover.rs +++ b/src/tools/pushover.rs @@ -223,6 +223,7 @@ mod tests { fn test_security(level: AutonomyLevel, max_actions_per_hour: u32) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: level, max_actions_per_hour, workspace_dir: std::env::temp_dir(), diff --git a/src/tools/schedule.rs b/src/tools/schedule.rs index 88b824c5b..c118ac501 100644 --- a/src/tools/schedule.rs +++ b/src/tools/schedule.rs @@ -558,6 +558,7 @@ mod tests { workspace_dir: tmp.path().join("workspace"), config_path: tmp.path().join("config.toml"), autonomy: crate::config::AutonomyConfig { + enabled: true, level: AutonomyLevel::Full, max_actions_per_hour: 0, ..Default::default() @@ -600,6 +601,7 @@ mod tests { workspace_dir: tmp.path().join("workspace"), config_path: tmp.path().join("config.toml"), autonomy: crate::config::AutonomyConfig { + enabled: true, level: AutonomyLevel::Full, max_actions_per_hour: 1, ..Default::default() @@ -696,6 +698,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Supervised; config.autonomy.allowed_commands = vec!["echo".into()]; std::fs::create_dir_all(&config.workspace_dir).unwrap(); @@ -730,6 +733,7 @@ mod tests { config_path: tmp.path().join("config.toml"), ..Config::default() }; + config.autonomy.enabled = true; config.autonomy.level = AutonomyLevel::Supervised; config.autonomy.allowed_commands = vec!["touch".into()]; std::fs::create_dir_all(&config.workspace_dir).unwrap(); diff --git a/src/tools/shell.rs b/src/tools/shell.rs index b6244a94d..441072aea 100644 --- a/src/tools/shell.rs +++ b/src/tools/shell.rs @@ -214,6 +214,7 @@ mod tests { fn test_security(autonomy: AutonomyLevel) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy, workspace_dir: std::env::temp_dir(), ..SecurityPolicy::default() @@ -389,6 +390,7 @@ mod tests { fn test_security_with_env_cmd() -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: std::env::temp_dir(), allowed_commands: vec!["env".into(), "echo".into()], @@ -398,6 +400,7 @@ mod tests { fn test_security_with_env_passthrough(vars: &[&str]) -> Arc { Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, workspace_dir: std::env::temp_dir(), allowed_commands: vec!["env".into()], @@ -524,6 +527,7 @@ mod tests { #[tokio::test] async fn shell_requires_approval_for_medium_risk_command() { let security = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, allowed_commands: vec!["touch".into()], workspace_dir: std::env::temp_dir(), @@ -602,6 +606,7 @@ mod tests { #[tokio::test] async fn shell_blocks_rate_limited() { let security = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Supervised, max_actions_per_hour: 0, workspace_dir: std::env::temp_dir(), @@ -644,6 +649,7 @@ mod tests { #[tokio::test] async fn shell_record_action_budget_exhaustion() { let security = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::Full, max_actions_per_hour: 1, workspace_dir: std::env::temp_dir(), diff --git a/src/tools/web_fetch.rs b/src/tools/web_fetch.rs index a93a9d4ba..5393a69f7 100644 --- a/src/tools/web_fetch.rs +++ b/src/tools/web_fetch.rs @@ -711,6 +711,7 @@ mod tests { #[tokio::test] async fn blocks_readonly_mode() { let security = Arc::new(SecurityPolicy { + enabled: true, autonomy: AutonomyLevel::ReadOnly, ..SecurityPolicy::default() }); @@ -726,6 +727,7 @@ mod tests { #[tokio::test] async fn blocks_rate_limited() { let security = Arc::new(SecurityPolicy { + enabled: true, max_actions_per_hour: 0, ..SecurityPolicy::default() }); diff --git a/tests/config_schema.rs b/tests/config_schema.rs index 85c5e8e8a..db6eca12c 100644 --- a/tests/config_schema.rs +++ b/tests/config_schema.rs @@ -175,12 +175,16 @@ fn security_config_toml_roundtrip() { // ───────────────────────────────────────────────────────────────────────────── #[test] -fn autonomy_config_default_is_supervised() { +fn autonomy_config_default_is_full() { let autonomy = AutonomyConfig::default(); + assert!( + !autonomy.enabled, + "default security policy should be disabled" + ); assert_eq!( format!("{:?}", autonomy.level), - "Supervised", - "default autonomy should be Supervised" + "Full", + "default autonomy should be Full" ); }