Skip to content

Commit 82077d3

Browse files
author
Olivier Mansour
committed
fix: enhance execute tool permission logic for fine-grained command control
- Implement 3-step permission evaluation logic: 1. If execute_bash in allowedTools → always allow (except denied commands) 2. Check denied command patterns → deny if matched 3. Use requires_acceptance with allowedCommands for fine-grained control - Fix design flaw where allowedCommands patterns only worked when execute_bash was in allowedTools array - Enable users to specify command patterns without granting blanket tool access - Add comprehensive test suite with 4 new test cases: - test_eval_perm_allowed_tools_priority: validates trusted tool behavior - test_eval_perm_fine_grained_control: validates pattern-based permissions - test_eval_perm_default_behavior: validates default fallback behavior - test_eval_perm_invalid_settings: validates error handling - Maintain backward compatibility - all existing tests pass This resolves the issue where /tools command displayed allowed command patterns but the underlying permission logic didn't properly evaluate them for fine-grained control scenarios.
1 parent dd6de52 commit 82077d3

File tree

1 file changed

+188
-6
lines changed
  • crates/chat-cli/src/cli/chat/tools/execute

1 file changed

+188
-6
lines changed

crates/chat-cli/src/cli/chat/tools/execute/mod.rs

Lines changed: 188 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,13 @@ impl ExecuteCommand {
194194
let Self { command, .. } = self;
195195
let tool_name = if cfg!(windows) { "execute_cmd" } else { "execute_bash" };
196196
let is_in_allowlist = agent.allowed_tools.contains("execute_bash");
197-
match agent.tools_settings.get(tool_name) {
198-
Some(settings) if is_in_allowlist => {
197+
198+
// Step 1: If execute_bash is in allowedTools, always allow (except denied commands)
199+
if is_in_allowlist {
200+
if let Some(settings) = agent.tools_settings.get(tool_name) {
199201
let Settings {
200-
allowed_commands,
201202
denied_commands,
202-
allow_read_only,
203+
..
203204
} = match serde_json::from_value::<Settings>(settings.clone()) {
204205
Ok(settings) => settings,
205206
Err(e) => {
@@ -211,15 +212,50 @@ impl ExecuteCommand {
211212
if denied_commands.iter().any(|dc| command.contains(dc)) {
212213
return PermissionEvalResult::Deny;
213214
}
215+
}
216+
return PermissionEvalResult::Allow;
217+
}
218+
219+
// Step 2: Check if command matches denied patterns
220+
if let Some(settings) = agent.tools_settings.get(tool_name) {
221+
let Settings {
222+
denied_commands,
223+
..
224+
} = match serde_json::from_value::<Settings>(settings.clone()) {
225+
Ok(settings) => settings,
226+
Err(e) => {
227+
error!("Failed to deserialize tool settings for execute_bash: {:?}", e);
228+
return PermissionEvalResult::Ask;
229+
},
230+
};
231+
232+
if denied_commands.iter().any(|dc| command.contains(dc)) {
233+
return PermissionEvalResult::Deny;
234+
}
235+
}
236+
237+
// Step 3: Use requires_acceptance logic with allowedCommands patterns
238+
match agent.tools_settings.get(tool_name) {
239+
Some(settings) => {
240+
let Settings {
241+
allowed_commands,
242+
allow_read_only,
243+
..
244+
} = match serde_json::from_value::<Settings>(settings.clone()) {
245+
Ok(settings) => settings,
246+
Err(e) => {
247+
error!("Failed to deserialize tool settings for execute_bash: {:?}", e);
248+
return PermissionEvalResult::Ask;
249+
},
250+
};
214251

215252
if self.requires_acceptance(Some(&allowed_commands), allow_read_only) {
216253
PermissionEvalResult::Ask
217254
} else {
218255
PermissionEvalResult::Allow
219256
}
220257
},
221-
None if is_in_allowlist => PermissionEvalResult::Allow,
222-
_ => {
258+
None => {
223259
if self.requires_acceptance(None, default_allow_read_only()) {
224260
PermissionEvalResult::Ask
225261
} else {
@@ -384,4 +420,150 @@ mod tests {
384420
);
385421
}
386422
}
423+
424+
#[test]
425+
fn test_eval_perm_allowed_tools_priority() {
426+
use crate::cli::agent::{Agent, PermissionEvalResult};
427+
428+
// Test that when execute_bash is in allowedTools, it always allows (except denied commands)
429+
let agent_json = serde_json::json!({
430+
"name": "test_agent",
431+
"allowedTools": ["execute_bash"],
432+
"toolsSettings": {
433+
"execute_bash": {
434+
"allowedCommands": ["ls"],
435+
"deniedCommands": ["rm"],
436+
"allowReadOnly": false
437+
}
438+
}
439+
});
440+
441+
let agent: Agent = serde_json::from_value(agent_json).unwrap();
442+
443+
let dangerous_cmd = ExecuteCommand {
444+
command: "sudo dd if=/dev/zero of=/dev/sda".to_string(),
445+
summary: None,
446+
};
447+
448+
let denied_cmd = ExecuteCommand {
449+
command: "rm file.txt".to_string(),
450+
summary: None,
451+
};
452+
453+
// Should allow dangerous command because execute_bash is in allowedTools
454+
assert_eq!(dangerous_cmd.eval_perm(&agent), PermissionEvalResult::Allow);
455+
456+
// Should deny command matching denied pattern
457+
assert_eq!(denied_cmd.eval_perm(&agent), PermissionEvalResult::Deny);
458+
}
459+
460+
#[test]
461+
fn test_eval_perm_fine_grained_control() {
462+
use crate::cli::agent::{Agent, PermissionEvalResult};
463+
464+
// Test fine-grained control when execute_bash is NOT in allowedTools
465+
let agent_json = serde_json::json!({
466+
"name": "test_agent",
467+
"allowedTools": [],
468+
"toolsSettings": {
469+
"execute_bash": {
470+
"allowedCommands": ["git status", "ls.*"],
471+
"deniedCommands": ["rm"],
472+
"allowReadOnly": true
473+
}
474+
}
475+
});
476+
477+
let agent: Agent = serde_json::from_value(agent_json).unwrap();
478+
479+
let allowed_cmd = ExecuteCommand {
480+
command: "git status".to_string(),
481+
summary: None,
482+
};
483+
484+
let pattern_match_cmd = ExecuteCommand {
485+
command: "ls -la".to_string(),
486+
summary: None,
487+
};
488+
489+
let readonly_cmd = ExecuteCommand {
490+
command: "cat file.txt".to_string(),
491+
summary: None,
492+
};
493+
494+
let denied_cmd = ExecuteCommand {
495+
command: "rm file.txt".to_string(),
496+
summary: None,
497+
};
498+
499+
let dangerous_cmd = ExecuteCommand {
500+
command: "sudo dd if=/dev/zero of=/dev/sda".to_string(),
501+
summary: None,
502+
};
503+
504+
// Should allow command matching allowedCommands pattern
505+
assert_eq!(allowed_cmd.eval_perm(&agent), PermissionEvalResult::Allow);
506+
507+
// Should allow command matching regex pattern
508+
assert_eq!(pattern_match_cmd.eval_perm(&agent), PermissionEvalResult::Allow);
509+
510+
// Should allow readonly command when allowReadOnly is true
511+
assert_eq!(readonly_cmd.eval_perm(&agent), PermissionEvalResult::Allow);
512+
513+
// Should deny command matching denied pattern
514+
assert_eq!(denied_cmd.eval_perm(&agent), PermissionEvalResult::Deny);
515+
516+
// Should ask for dangerous command not matching any allowed pattern
517+
assert_eq!(dangerous_cmd.eval_perm(&agent), PermissionEvalResult::Ask);
518+
}
519+
520+
#[test]
521+
fn test_eval_perm_default_behavior() {
522+
use crate::cli::agent::{Agent, PermissionEvalResult};
523+
524+
// Test default behavior when no settings are configured
525+
let agent = Agent::default();
526+
527+
let readonly_cmd = ExecuteCommand {
528+
command: "ls -la".to_string(),
529+
summary: None,
530+
};
531+
532+
let dangerous_cmd = ExecuteCommand {
533+
command: "sudo rm -rf /".to_string(),
534+
summary: None,
535+
};
536+
537+
// Should allow readonly command with default settings
538+
assert_eq!(readonly_cmd.eval_perm(&agent), PermissionEvalResult::Allow);
539+
540+
// Should ask for dangerous command with default settings
541+
assert_eq!(dangerous_cmd.eval_perm(&agent), PermissionEvalResult::Ask);
542+
}
543+
544+
#[test]
545+
fn test_eval_perm_invalid_settings() {
546+
use crate::cli::agent::{Agent, PermissionEvalResult};
547+
548+
// Test behavior with invalid JSON settings
549+
let agent_json = serde_json::json!({
550+
"name": "test_agent",
551+
"allowedTools": [],
552+
"toolsSettings": {
553+
"execute_bash": {
554+
"invalidField": "invalidValue"
555+
}
556+
}
557+
});
558+
559+
let agent: Agent = serde_json::from_value(agent_json).unwrap();
560+
561+
let cmd = ExecuteCommand {
562+
command: "sudo systemctl restart nginx".to_string(),
563+
summary: None,
564+
};
565+
566+
// Should ask when settings are invalid (falls back to safe behavior)
567+
assert_eq!(cmd.eval_perm(&agent), PermissionEvalResult::Ask);
568+
}
387569
}

0 commit comments

Comments
 (0)