auto_accept.rs (10368B)
1 //! Auto-accept rules for tool permission requests. 2 //! 3 //! This module provides a configurable rules-based system for automatically 4 //! accepting certain tool calls without requiring user confirmation. 5 6 use crate::file_update::FileUpdate; 7 use serde_json::Value; 8 9 /// A rule for auto-accepting tool calls 10 #[derive(Debug, Clone)] 11 pub enum AutoAcceptRule { 12 /// Auto-accept Edit tool calls that change at most N lines 13 SmallEdit { max_lines: usize }, 14 /// Auto-accept Bash tool calls matching these command prefixes 15 BashCommand { prefixes: Vec<String> }, 16 /// Auto-accept specific read-only tools unconditionally 17 ReadOnlyTool { tools: Vec<String> }, 18 } 19 20 impl AutoAcceptRule { 21 /// Check if this rule matches the given tool call 22 fn matches(&self, tool_name: &str, tool_input: &Value) -> bool { 23 match self { 24 AutoAcceptRule::SmallEdit { max_lines } => { 25 if let Some(file_update) = FileUpdate::from_tool_call(tool_name, tool_input) { 26 file_update.is_small_edit(*max_lines) 27 } else { 28 false 29 } 30 } 31 AutoAcceptRule::BashCommand { prefixes } => { 32 if tool_name != "Bash" { 33 return false; 34 } 35 let Some(command) = tool_input.get("command").and_then(|v| v.as_str()) else { 36 return false; 37 }; 38 let command_trimmed = command.trim(); 39 prefixes 40 .iter() 41 .any(|prefix| command_trimmed.starts_with(prefix)) 42 } 43 AutoAcceptRule::ReadOnlyTool { tools } => tools.iter().any(|t| t == tool_name), 44 } 45 } 46 } 47 48 /// Collection of auto-accept rules 49 #[derive(Debug, Clone)] 50 pub struct AutoAcceptRules { 51 rules: Vec<AutoAcceptRule>, 52 } 53 54 impl Default for AutoAcceptRules { 55 fn default() -> Self { 56 Self { 57 rules: vec![ 58 AutoAcceptRule::SmallEdit { max_lines: 2 }, 59 AutoAcceptRule::BashCommand { 60 prefixes: vec![ 61 // Cargo commands 62 "cargo build".into(), 63 "cargo check".into(), 64 "cargo test".into(), 65 "cargo fmt".into(), 66 "cargo clippy".into(), 67 "cargo run".into(), 68 "cargo doc".into(), 69 // Read-only bash commands 70 "grep ".into(), 71 "grep\t".into(), 72 "rg ".into(), 73 "rg\t".into(), 74 "find ".into(), 75 "find\t".into(), 76 "ls".into(), 77 "ls ".into(), 78 "ls\t".into(), 79 "cat ".into(), 80 "cat\t".into(), 81 "head ".into(), 82 "head\t".into(), 83 "tail ".into(), 84 "tail\t".into(), 85 "wc ".into(), 86 "wc\t".into(), 87 "file ".into(), 88 "file\t".into(), 89 "stat ".into(), 90 "stat\t".into(), 91 "which ".into(), 92 "which\t".into(), 93 "type ".into(), 94 "type\t".into(), 95 "pwd".into(), 96 "tree".into(), 97 "tree ".into(), 98 "tree\t".into(), 99 "du ".into(), 100 "du\t".into(), 101 "df ".into(), 102 "df\t".into(), 103 // Git read-only commands 104 "git status".into(), 105 "git log".into(), 106 "git diff".into(), 107 "git show".into(), 108 "git branch".into(), 109 "git remote".into(), 110 "git rev-parse".into(), 111 "git ls-files".into(), 112 "git describe".into(), 113 ], 114 }, 115 AutoAcceptRule::ReadOnlyTool { 116 tools: vec!["Glob".into(), "Grep".into(), "Read".into()], 117 }, 118 ], 119 } 120 } 121 } 122 123 impl AutoAcceptRules { 124 /// Check if any rule matches the given tool call 125 pub fn should_auto_accept(&self, tool_name: &str, tool_input: &Value) -> bool { 126 self.rules 127 .iter() 128 .any(|rule| rule.matches(tool_name, tool_input)) 129 } 130 } 131 132 #[cfg(test)] 133 mod tests { 134 use super::*; 135 use serde_json::json; 136 137 fn default_rules() -> AutoAcceptRules { 138 AutoAcceptRules::default() 139 } 140 141 #[test] 142 fn test_small_edit_auto_accept() { 143 let rules = default_rules(); 144 let input = json!({ 145 "file_path": "/path/to/file.rs", 146 "old_string": "let x = 1;", 147 "new_string": "let x = 2;" 148 }); 149 assert!(rules.should_auto_accept("Edit", &input)); 150 } 151 152 #[test] 153 fn test_large_edit_not_auto_accept() { 154 let rules = default_rules(); 155 let input = json!({ 156 "file_path": "/path/to/file.rs", 157 "old_string": "line1\nline2\nline3\nline4", 158 "new_string": "a\nb\nc\nd" 159 }); 160 assert!(!rules.should_auto_accept("Edit", &input)); 161 } 162 163 #[test] 164 fn test_cargo_build_auto_accept() { 165 let rules = default_rules(); 166 let input = json!({ "command": "cargo build" }); 167 assert!(rules.should_auto_accept("Bash", &input)); 168 } 169 170 #[test] 171 fn test_cargo_check_auto_accept() { 172 let rules = default_rules(); 173 let input = json!({ "command": "cargo check" }); 174 assert!(rules.should_auto_accept("Bash", &input)); 175 } 176 177 #[test] 178 fn test_cargo_test_with_args_auto_accept() { 179 let rules = default_rules(); 180 let input = json!({ "command": "cargo test --release" }); 181 assert!(rules.should_auto_accept("Bash", &input)); 182 } 183 184 #[test] 185 fn test_cargo_fmt_auto_accept() { 186 let rules = default_rules(); 187 let input = json!({ "command": "cargo fmt" }); 188 assert!(rules.should_auto_accept("Bash", &input)); 189 } 190 191 #[test] 192 fn test_cargo_clippy_auto_accept() { 193 let rules = default_rules(); 194 let input = json!({ "command": "cargo clippy" }); 195 assert!(rules.should_auto_accept("Bash", &input)); 196 } 197 198 #[test] 199 fn test_rm_not_auto_accept() { 200 let rules = default_rules(); 201 let input = json!({ "command": "rm -rf /tmp/test" }); 202 assert!(!rules.should_auto_accept("Bash", &input)); 203 } 204 205 #[test] 206 fn test_curl_not_auto_accept() { 207 let rules = default_rules(); 208 let input = json!({ "command": "curl https://example.com" }); 209 assert!(!rules.should_auto_accept("Bash", &input)); 210 } 211 212 #[test] 213 fn test_read_auto_accept() { 214 let rules = default_rules(); 215 let input = json!({ "file_path": "/path/to/file.rs" }); 216 assert!(rules.should_auto_accept("Read", &input)); 217 } 218 219 #[test] 220 fn test_glob_auto_accept() { 221 let rules = default_rules(); 222 let input = json!({ "pattern": "**/*.rs" }); 223 assert!(rules.should_auto_accept("Glob", &input)); 224 } 225 226 #[test] 227 fn test_grep_auto_accept() { 228 let rules = default_rules(); 229 let input = json!({ "pattern": "TODO", "path": "/src" }); 230 assert!(rules.should_auto_accept("Grep", &input)); 231 } 232 233 #[test] 234 fn test_write_not_auto_accept() { 235 let rules = default_rules(); 236 let input = json!({ 237 "file_path": "/path/to/file.rs", 238 "content": "new content" 239 }); 240 assert!(!rules.should_auto_accept("Write", &input)); 241 } 242 243 #[test] 244 fn test_unknown_tool_not_auto_accept() { 245 let rules = default_rules(); 246 let input = json!({}); 247 assert!(!rules.should_auto_accept("UnknownTool", &input)); 248 } 249 250 #[test] 251 fn test_bash_with_leading_whitespace() { 252 let rules = default_rules(); 253 let input = json!({ "command": " cargo build" }); 254 assert!(rules.should_auto_accept("Bash", &input)); 255 } 256 257 #[test] 258 fn test_grep_bash_auto_accept() { 259 let rules = default_rules(); 260 let input = json!({ "command": "grep -rn \"pattern\" /path" }); 261 assert!(rules.should_auto_accept("Bash", &input)); 262 } 263 264 #[test] 265 fn test_rg_bash_auto_accept() { 266 let rules = default_rules(); 267 let input = json!({ "command": "rg \"pattern\" /path" }); 268 assert!(rules.should_auto_accept("Bash", &input)); 269 } 270 271 #[test] 272 fn test_find_bash_auto_accept() { 273 let rules = default_rules(); 274 let input = json!({ "command": "find . -name \"*.rs\"" }); 275 assert!(rules.should_auto_accept("Bash", &input)); 276 } 277 278 #[test] 279 fn test_git_status_auto_accept() { 280 let rules = default_rules(); 281 let input = json!({ "command": "git status" }); 282 assert!(rules.should_auto_accept("Bash", &input)); 283 } 284 285 #[test] 286 fn test_git_log_auto_accept() { 287 let rules = default_rules(); 288 let input = json!({ "command": "git log --oneline -10" }); 289 assert!(rules.should_auto_accept("Bash", &input)); 290 } 291 292 #[test] 293 fn test_git_push_not_auto_accept() { 294 let rules = default_rules(); 295 let input = json!({ "command": "git push origin main" }); 296 assert!(!rules.should_auto_accept("Bash", &input)); 297 } 298 299 #[test] 300 fn test_git_commit_not_auto_accept() { 301 let rules = default_rules(); 302 let input = json!({ "command": "git commit -m \"test\"" }); 303 assert!(!rules.should_auto_accept("Bash", &input)); 304 } 305 306 #[test] 307 fn test_ls_auto_accept() { 308 let rules = default_rules(); 309 let input = json!({ "command": "ls -la /tmp" }); 310 assert!(rules.should_auto_accept("Bash", &input)); 311 } 312 313 #[test] 314 fn test_cat_auto_accept() { 315 let rules = default_rules(); 316 let input = json!({ "command": "cat /path/to/file.txt" }); 317 assert!(rules.should_auto_accept("Bash", &input)); 318 } 319 }