use super::traits::{Tool, ToolResult}; use crate::config::Config; use crate::cron; use crate::security::SecurityPolicy; use async_trait::async_trait; use serde_json::json; use std::sync::Arc; pub struct CronRemoveTool { config: Arc, security: Arc, } impl CronRemoveTool { pub fn new(config: Arc, security: Arc) -> Self { Self { config, security } } fn enforce_mutation_allowed(&self, action: &str) -> Option { if !self.security.can_act() { return Some(ToolResult { success: false, output: String::new(), error: Some(format!( "Security policy: read-only mode, cannot perform '{action}'" )), }); } if self.security.is_rate_limited() { return Some(ToolResult { success: false, output: String::new(), error: Some("Rate limit exceeded: too many actions in the last hour".to_string()), }); } if !self.security.record_action() { return Some(ToolResult { success: false, output: String::new(), error: Some("Rate limit exceeded: action budget exhausted".to_string()), }); } None } } #[async_trait] impl Tool for CronRemoveTool { fn name(&self) -> &str { "cron_remove" } fn description(&self) -> &str { "Remove a cron job by id" } fn parameters_schema(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "job_id": { "type": "string" } }, "required": ["job_id"] }) } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { if !self.config.cron.enabled { return Ok(ToolResult { success: false, output: String::new(), error: Some("cron is disabled by config (cron.enabled=false)".to_string()), }); } let job_id = match args.get("job_id").and_then(serde_json::Value::as_str) { Some(v) if !v.trim().is_empty() => v, _ => { return Ok(ToolResult { success: false, output: String::new(), error: Some("Missing 'job_id' parameter".to_string()), }); } }; if let Some(blocked) = self.enforce_mutation_allowed("cron_remove") { return Ok(blocked); } match cron::remove_job(&self.config, job_id) { Ok(()) => Ok(ToolResult { success: true, output: format!("Removed cron job {job_id}"), error: None, }), Err(e) => Ok(ToolResult { success: false, output: String::new(), error: Some(e.to_string()), }), } } } #[cfg(test)] mod tests { use super::*; use crate::config::Config; use crate::security::AutonomyLevel; use tempfile::TempDir; async fn test_config(tmp: &TempDir) -> Arc { let config = Config { workspace_dir: tmp.path().join("workspace"), config_path: tmp.path().join("config.toml"), ..Config::default() }; tokio::fs::create_dir_all(&config.workspace_dir) .await .unwrap(); Arc::new(config) } fn test_security(cfg: &Config) -> Arc { Arc::new(SecurityPolicy::from_config( &cfg.autonomy, &cfg.workspace_dir, )) } #[tokio::test] async fn removes_existing_job() { let tmp = TempDir::new().unwrap(); let cfg = test_config(&tmp).await; let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let tool = CronRemoveTool::new(cfg.clone(), test_security(&cfg)); let result = tool.execute(json!({"job_id": job.id})).await.unwrap(); assert!(result.success); assert!(cron::list_jobs(&cfg).unwrap().is_empty()); } #[tokio::test] async fn errors_when_job_id_missing() { let tmp = TempDir::new().unwrap(); let cfg = test_config(&tmp).await; let tool = CronRemoveTool::new(cfg.clone(), test_security(&cfg)); let result = tool.execute(json!({})).await.unwrap(); assert!(!result.success); assert!(result .error .unwrap_or_default() .contains("Missing 'job_id'")); } #[tokio::test] async fn blocks_remove_in_read_only_mode() { let tmp = TempDir::new().unwrap(); let mut config = Config { workspace_dir: tmp.path().join("workspace"), config_path: tmp.path().join("config.toml"), ..Config::default() }; config.autonomy.level = AutonomyLevel::ReadOnly; std::fs::create_dir_all(&config.workspace_dir).unwrap(); let cfg = Arc::new(config); let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let tool = CronRemoveTool::new(cfg.clone(), test_security(&cfg)); let result = tool.execute(json!({"job_id": job.id})).await.unwrap(); assert!(!result.success); assert!(result.error.unwrap_or_default().contains("read-only")); } #[tokio::test] async fn blocks_remove_when_rate_limited() { let tmp = TempDir::new().unwrap(); let mut config = Config { workspace_dir: tmp.path().join("workspace"), config_path: tmp.path().join("config.toml"), ..Config::default() }; config.autonomy.level = AutonomyLevel::Full; config.autonomy.max_actions_per_hour = 0; std::fs::create_dir_all(&config.workspace_dir).unwrap(); let cfg = Arc::new(config); let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let tool = CronRemoveTool::new(cfg.clone(), test_security(&cfg)); let result = tool.execute(json!({"job_id": job.id})).await.unwrap(); assert!(!result.success); assert!(result .error .unwrap_or_default() .contains("Rate limit exceeded")); assert_eq!(cron::list_jobs(&cfg).unwrap().len(), 1); } }