@@ -194,12 +194,13 @@ impl ExecuteCommand {
194
194
let Self { command, .. } = self ;
195
195
let tool_name = if cfg ! ( windows) { "execute_cmd" } else { "execute_bash" } ;
196
196
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) {
199
201
let Settings {
200
- allowed_commands,
201
202
denied_commands,
202
- allow_read_only ,
203
+ ..
203
204
} = match serde_json:: from_value :: < Settings > ( settings. clone ( ) ) {
204
205
Ok ( settings) => settings,
205
206
Err ( e) => {
@@ -211,15 +212,50 @@ impl ExecuteCommand {
211
212
if denied_commands. iter ( ) . any ( |dc| command. contains ( dc) ) {
212
213
return PermissionEvalResult :: Deny ;
213
214
}
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
+ } ;
214
251
215
252
if self . requires_acceptance ( Some ( & allowed_commands) , allow_read_only) {
216
253
PermissionEvalResult :: Ask
217
254
} else {
218
255
PermissionEvalResult :: Allow
219
256
}
220
257
} ,
221
- None if is_in_allowlist => PermissionEvalResult :: Allow ,
222
- _ => {
258
+ None => {
223
259
if self . requires_acceptance ( None , default_allow_read_only ( ) ) {
224
260
PermissionEvalResult :: Ask
225
261
} else {
@@ -384,4 +420,150 @@ mod tests {
384
420
) ;
385
421
}
386
422
}
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
+ }
387
569
}
0 commit comments