notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }