@@ -194,12 +194,47 @@ 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
+
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) {
201
+ let Settings { denied_commands, .. } = match serde_json:: from_value :: < Settings > ( settings. clone ( ) ) {
202
+ Ok ( settings) => settings,
203
+ Err ( e) => {
204
+ error ! ( "Failed to deserialize tool settings for execute_bash: {:?}" , e) ;
205
+ return PermissionEvalResult :: Ask ;
206
+ } ,
207
+ } ;
208
+
209
+ if denied_commands. iter ( ) . any ( |dc| command. contains ( dc) ) {
210
+ return PermissionEvalResult :: Deny ;
211
+ }
212
+ }
213
+ return PermissionEvalResult :: Allow ;
214
+ }
215
+
216
+ // Step 2: Check if command matches denied patterns
217
+ if let Some ( settings) = agent. tools_settings . get ( tool_name) {
218
+ let Settings { denied_commands, .. } = match serde_json:: from_value :: < Settings > ( settings. clone ( ) ) {
219
+ Ok ( settings) => settings,
220
+ Err ( e) => {
221
+ error ! ( "Failed to deserialize tool settings for execute_bash: {:?}" , e) ;
222
+ return PermissionEvalResult :: Ask ;
223
+ } ,
224
+ } ;
225
+
226
+ if denied_commands. iter ( ) . any ( |dc| command. contains ( dc) ) {
227
+ return PermissionEvalResult :: Deny ;
228
+ }
229
+ }
230
+
231
+ // Step 3: Use requires_acceptance logic with allowedCommands patterns
197
232
match agent. tools_settings . get ( tool_name) {
198
- Some ( settings) if is_in_allowlist => {
233
+ Some ( settings) => {
199
234
let Settings {
200
235
allowed_commands,
201
- denied_commands,
202
236
allow_read_only,
237
+ ..
203
238
} = match serde_json:: from_value :: < Settings > ( settings. clone ( ) ) {
204
239
Ok ( settings) => settings,
205
240
Err ( e) => {
@@ -208,18 +243,13 @@ impl ExecuteCommand {
208
243
} ,
209
244
} ;
210
245
211
- if denied_commands. iter ( ) . any ( |dc| command. contains ( dc) ) {
212
- return PermissionEvalResult :: Deny ;
213
- }
214
-
215
246
if self . requires_acceptance ( Some ( & allowed_commands) , allow_read_only) {
216
247
PermissionEvalResult :: Ask
217
248
} else {
218
249
PermissionEvalResult :: Allow
219
250
}
220
251
} ,
221
- None if is_in_allowlist => PermissionEvalResult :: Allow ,
222
- _ => {
252
+ None => {
223
253
if self . requires_acceptance ( None , default_allow_read_only ( ) ) {
224
254
PermissionEvalResult :: Ask
225
255
} else {
@@ -384,4 +414,150 @@ mod tests {
384
414
) ;
385
415
}
386
416
}
417
+
418
+ #[ test]
419
+ fn test_eval_perm_allowed_tools_priority ( ) {
420
+ use crate :: cli:: agent:: { Agent , PermissionEvalResult } ;
421
+
422
+ // Test that when execute_bash is in allowedTools, it always allows (except denied commands)
423
+ let agent_json = serde_json:: json!( {
424
+ "name" : "test_agent" ,
425
+ "allowedTools" : [ "execute_bash" ] ,
426
+ "toolsSettings" : {
427
+ "execute_bash" : {
428
+ "allowedCommands" : [ "ls" ] ,
429
+ "deniedCommands" : [ "rm" ] ,
430
+ "allowReadOnly" : false
431
+ }
432
+ }
433
+ } ) ;
434
+
435
+ let agent: Agent = serde_json:: from_value ( agent_json) . unwrap ( ) ;
436
+
437
+ let dangerous_cmd = ExecuteCommand {
438
+ command : "sudo dd if=/dev/zero of=/dev/sda" . to_string ( ) ,
439
+ summary : None ,
440
+ } ;
441
+
442
+ let denied_cmd = ExecuteCommand {
443
+ command : "rm file.txt" . to_string ( ) ,
444
+ summary : None ,
445
+ } ;
446
+
447
+ // Should allow dangerous command because execute_bash is in allowedTools
448
+ assert_eq ! ( dangerous_cmd. eval_perm( & agent) , PermissionEvalResult :: Allow ) ;
449
+
450
+ // Should deny command matching denied pattern
451
+ assert_eq ! ( denied_cmd. eval_perm( & agent) , PermissionEvalResult :: Deny ) ;
452
+ }
453
+
454
+ #[ test]
455
+ fn test_eval_perm_fine_grained_control ( ) {
456
+ use crate :: cli:: agent:: { Agent , PermissionEvalResult } ;
457
+
458
+ // Test fine-grained control when execute_bash is NOT in allowedTools
459
+ let agent_json = serde_json:: json!( {
460
+ "name" : "test_agent" ,
461
+ "allowedTools" : [ ] ,
462
+ "toolsSettings" : {
463
+ "execute_bash" : {
464
+ "allowedCommands" : [ "git status" , "ls.*" ] ,
465
+ "deniedCommands" : [ "rm" ] ,
466
+ "allowReadOnly" : true
467
+ }
468
+ }
469
+ } ) ;
470
+
471
+ let agent: Agent = serde_json:: from_value ( agent_json) . unwrap ( ) ;
472
+
473
+ let allowed_cmd = ExecuteCommand {
474
+ command : "git status" . to_string ( ) ,
475
+ summary : None ,
476
+ } ;
477
+
478
+ let pattern_match_cmd = ExecuteCommand {
479
+ command : "ls -la" . to_string ( ) ,
480
+ summary : None ,
481
+ } ;
482
+
483
+ let readonly_cmd = ExecuteCommand {
484
+ command : "cat file.txt" . to_string ( ) ,
485
+ summary : None ,
486
+ } ;
487
+
488
+ let denied_cmd = ExecuteCommand {
489
+ command : "rm file.txt" . to_string ( ) ,
490
+ summary : None ,
491
+ } ;
492
+
493
+ let dangerous_cmd = ExecuteCommand {
494
+ command : "sudo dd if=/dev/zero of=/dev/sda" . to_string ( ) ,
495
+ summary : None ,
496
+ } ;
497
+
498
+ // Should allow command matching allowedCommands pattern
499
+ assert_eq ! ( allowed_cmd. eval_perm( & agent) , PermissionEvalResult :: Allow ) ;
500
+
501
+ // Should allow command matching regex pattern
502
+ assert_eq ! ( pattern_match_cmd. eval_perm( & agent) , PermissionEvalResult :: Allow ) ;
503
+
504
+ // Should allow readonly command when allowReadOnly is true
505
+ assert_eq ! ( readonly_cmd. eval_perm( & agent) , PermissionEvalResult :: Allow ) ;
506
+
507
+ // Should deny command matching denied pattern
508
+ assert_eq ! ( denied_cmd. eval_perm( & agent) , PermissionEvalResult :: Deny ) ;
509
+
510
+ // Should ask for dangerous command not matching any allowed pattern
511
+ assert_eq ! ( dangerous_cmd. eval_perm( & agent) , PermissionEvalResult :: Ask ) ;
512
+ }
513
+
514
+ #[ test]
515
+ fn test_eval_perm_default_behavior ( ) {
516
+ use crate :: cli:: agent:: { Agent , PermissionEvalResult } ;
517
+
518
+ // Test default behavior when no settings are configured
519
+ let agent = Agent :: default ( ) ;
520
+
521
+ let readonly_cmd = ExecuteCommand {
522
+ command : "ls -la" . to_string ( ) ,
523
+ summary : None ,
524
+ } ;
525
+
526
+ let dangerous_cmd = ExecuteCommand {
527
+ command : "sudo rm -rf /" . to_string ( ) ,
528
+ summary : None ,
529
+ } ;
530
+
531
+ // Should allow readonly command with default settings
532
+ assert_eq ! ( readonly_cmd. eval_perm( & agent) , PermissionEvalResult :: Allow ) ;
533
+
534
+ // Should ask for dangerous command with default settings
535
+ assert_eq ! ( dangerous_cmd. eval_perm( & agent) , PermissionEvalResult :: Ask ) ;
536
+ }
537
+
538
+ #[ test]
539
+ fn test_eval_perm_invalid_settings ( ) {
540
+ use crate :: cli:: agent:: { Agent , PermissionEvalResult } ;
541
+
542
+ // Test behavior with invalid JSON settings
543
+ let agent_json = serde_json:: json!( {
544
+ "name" : "test_agent" ,
545
+ "allowedTools" : [ ] ,
546
+ "toolsSettings" : {
547
+ "execute_bash" : {
548
+ "invalidField" : "invalidValue"
549
+ }
550
+ }
551
+ } ) ;
552
+
553
+ let agent: Agent = serde_json:: from_value ( agent_json) . unwrap ( ) ;
554
+
555
+ let cmd = ExecuteCommand {
556
+ command : "sudo systemctl restart nginx" . to_string ( ) ,
557
+ summary : None ,
558
+ } ;
559
+
560
+ // Should ask when settings are invalid (falls back to safe behavior)
561
+ assert_eq ! ( cmd. eval_perm( & agent) , PermissionEvalResult :: Ask ) ;
562
+ }
387
563
}
0 commit comments