ai_agent/utils/permissions/
shell_rule_matching.rs1#![allow(dead_code)]
3
4use crate::types::permissions::PermissionUpdate;
12
13const ESCAPED_STAR_PLACEHOLDER: &str = "\x00ESCAPED_STAR\x00";
15const ESCAPED_BACKSLASH_PLACEHOLDER: &str = "\x00ESCAPED_BACKSLASH\x00";
16
17#[derive(Debug, Clone)]
19pub enum ShellPermissionRule {
20 Exact { command: String },
21 Prefix { prefix: String },
22 Wildcard { pattern: String },
23}
24
25pub fn permission_rule_extract_prefix(permission_rule: &str) -> Option<String> {
27 if permission_rule.ends_with(":*") {
28 Some(permission_rule[..permission_rule.len() - 2].to_string())
29 } else {
30 None
31 }
32}
33
34pub fn has_wildcards(pattern: &str) -> bool {
36 if pattern.ends_with(":*") {
37 return false;
38 }
39
40 let chars: Vec<char> = pattern.chars().collect();
41 for (i, &c) in chars.iter().enumerate() {
42 if c == '*' {
43 let mut backslash_count = 0;
44 let mut j = i as i32 - 1;
45 while j >= 0 && chars[j as usize] == '\\' {
46 backslash_count += 1;
47 j -= 1;
48 }
49 if backslash_count % 2 == 0 {
50 return true;
51 }
52 }
53 }
54 false
55}
56
57pub fn match_wildcard_pattern(pattern: &str, command: &str, case_insensitive: bool) -> bool {
59 let trimmed = pattern.trim();
60
61 let mut processed = String::new();
63 let chars: Vec<char> = trimmed.chars().collect();
64 let mut i = 0;
65
66 while i < chars.len() {
67 if chars[i] == '\\' && i + 1 < chars.len() {
68 match chars[i + 1] {
69 '*' => {
70 processed.push_str(ESCAPED_STAR_PLACEHOLDER);
71 i += 2;
72 continue;
73 }
74 '\\' => {
75 processed.push_str(ESCAPED_BACKSLASH_PLACEHOLDER);
76 i += 2;
77 continue;
78 }
79 _ => {}
80 }
81 }
82 processed.push(chars[i]);
83 i += 1;
84 }
85
86 let escaped = processed
88 .replace('.', "\\.")
89 .replace('+', "\\+")
90 .replace('?', "\\?")
91 .replace('^', "\\^")
92 .replace('$', "\\$")
93 .replace('{', "\\{")
94 .replace('}', "\\}")
95 .replace('(', "\\(")
96 .replace(')', "\\)")
97 .replace('[', "\\[")
98 .replace(']', "\\]")
99 .replace('\\', "\\\\")
100 .replace('\'', "\\'")
101 .replace('"', "\\\"");
102
103 let with_wildcards = escaped.replace('*', ".*");
105
106 let regex_pattern = with_wildcards
108 .replace(ESCAPED_STAR_PLACEHOLDER, "\\*")
109 .replace(ESCAPED_BACKSLASH_PLACEHOLDER, "\\\\");
110
111 let unescaped_star_count = processed.matches('*').count();
113 let mut final_pattern = regex_pattern;
114 if final_pattern.ends_with(" .*") && unescaped_star_count == 1 {
115 let without_trailing = &final_pattern[..final_pattern.len() - 3];
116 final_pattern = format!("{}( .*)?", without_trailing);
117 }
118
119 let flags = if case_insensitive { "(?i)" } else { "" };
120 let regex_str = format!("{}^{}$", flags, final_pattern);
121
122 match regex::Regex::new(®ex_str) {
123 Ok(re) => re.is_match(command),
124 Err(_) => false,
125 }
126}
127
128pub fn parse_permission_rule(permission_rule: &str) -> ShellPermissionRule {
130 if let Some(prefix) = permission_rule_extract_prefix(permission_rule) {
132 return ShellPermissionRule::Prefix { prefix };
133 }
134
135 if has_wildcards(permission_rule) {
137 return ShellPermissionRule::Wildcard {
138 pattern: permission_rule.to_string(),
139 };
140 }
141
142 ShellPermissionRule::Exact {
144 command: permission_rule.to_string(),
145 }
146}
147
148pub fn suggestion_for_exact_command(tool_name: &str, command: &str) -> Vec<PermissionUpdate> {
150 vec![PermissionUpdate::AddRules {
151 rules: vec![crate::types::permissions::PermissionRuleValue {
152 tool_name: tool_name.to_string(),
153 rule_content: Some(command.to_string()),
154 }],
155 behavior: crate::types::permissions::PermissionBehavior::Allow,
156 destination: crate::types::permissions::PermissionUpdateDestination::LocalSettings,
157 }]
158}
159
160pub fn suggestion_for_prefix(tool_name: &str, prefix: &str) -> Vec<PermissionUpdate> {
162 vec![PermissionUpdate::AddRules {
163 rules: vec![crate::types::permissions::PermissionRuleValue {
164 tool_name: tool_name.to_string(),
165 rule_content: Some(format!("{}:*", prefix)),
166 }],
167 behavior: crate::types::permissions::PermissionBehavior::Allow,
168 destination: crate::types::permissions::PermissionUpdateDestination::LocalSettings,
169 }]
170}