@@ -16,6 +16,8 @@ use crate::tools::sandboxing::SandboxablePreference;
1616use crate :: tools:: sandboxing:: ToolCtx ;
1717use crate :: tools:: sandboxing:: ToolError ;
1818use codex_execpolicy:: Decision ;
19+ use codex_execpolicy:: Evaluation ;
20+ use codex_execpolicy:: MatchOptions ;
1921use codex_execpolicy:: Policy ;
2022use codex_execpolicy:: RuleMatch ;
2123use codex_protocol:: config_types:: WindowsSandboxLevel ;
@@ -431,6 +433,12 @@ impl CoreShellActionProvider {
431433 }
432434}
433435
436+ // Shell-wrapper parsing is weaker than direct exec interception because it can
437+ // only see the script text, not the final resolved executable path. Keep it
438+ // disabled by default so path-sensitive rules rely on the later authoritative
439+ // execve interception.
440+ const ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING : bool = false ;
441+
434442#[ async_trait:: async_trait]
435443impl EscalationPolicy for CoreShellActionProvider {
436444 async fn determine_action (
@@ -493,29 +501,18 @@ impl EscalationPolicy for CoreShellActionProvider {
493501 . await ;
494502 }
495503
496- let command = join_program_and_argv ( program, argv) ;
497- let ( commands, used_complex_parsing) =
498- if let Some ( commands) = parse_shell_lc_plain_commands ( & command) {
499- ( commands, false )
500- } else if let Some ( single_command) = parse_shell_lc_single_command_prefix ( & command) {
501- ( vec ! [ single_command] , true )
502- } else {
503- ( vec ! [ command. clone( ) ] , false )
504- } ;
505-
506- let fallback = |cmd : & [ String ] | {
507- crate :: exec_policy:: render_decision_for_unmatched_command (
504+ let evaluation = {
505+ let policy = self . policy . read ( ) . await ;
506+ evaluate_intercepted_exec_policy (
507+ & policy,
508+ program,
509+ argv,
508510 self . approval_policy ,
509511 & self . sandbox_policy ,
510- cmd,
511512 self . sandbox_permissions ,
512- used_complex_parsing ,
513+ ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING ,
513514 )
514515 } ;
515- let evaluation = {
516- let policy = self . policy . read ( ) . await ;
517- policy. check_multiple ( commands. iter ( ) , & fallback)
518- } ;
519516 // When true, means the Evaluation was due to *.rules, not the
520517 // fallback function.
521518 let decision_driven_by_policy =
@@ -528,16 +525,20 @@ impl EscalationPolicy for CoreShellActionProvider {
528525 } else {
529526 DecisionSource :: UnmatchedCommandFallback
530527 } ;
531- let escalation_execution = Self :: shell_request_escalation_execution (
532- self . sandbox_permissions ,
533- & self . sandbox_policy ,
534- self . prompt_permissions . as_ref ( ) ,
535- self . turn
536- . config
537- . permissions
538- . macos_seatbelt_profile_extensions
539- . as_ref ( ) ,
540- ) ;
528+ let escalation_execution = match decision_source {
529+ DecisionSource :: PrefixRule => EscalationExecution :: Unsandboxed ,
530+ DecisionSource :: UnmatchedCommandFallback => Self :: shell_request_escalation_execution (
531+ self . sandbox_permissions ,
532+ & self . sandbox_policy ,
533+ self . prompt_permissions . as_ref ( ) ,
534+ self . turn
535+ . config
536+ . permissions
537+ . macos_seatbelt_profile_extensions
538+ . as_ref ( ) ,
539+ ) ,
540+ DecisionSource :: SkillScript { .. } => unreachable ! ( "handled above" ) ,
541+ } ;
541542 self . process_decision (
542543 evaluation. decision ,
543544 needs_escalation,
@@ -552,6 +553,86 @@ impl EscalationPolicy for CoreShellActionProvider {
552553 }
553554}
554555
556+ fn evaluate_intercepted_exec_policy (
557+ policy : & Policy ,
558+ program : & AbsolutePathBuf ,
559+ argv : & [ String ] ,
560+ approval_policy : AskForApproval ,
561+ sandbox_policy : & SandboxPolicy ,
562+ sandbox_permissions : SandboxPermissions ,
563+ enable_intercepted_exec_policy_shell_wrapper_parsing : bool ,
564+ ) -> Evaluation {
565+ let CandidateCommands {
566+ commands,
567+ used_complex_parsing,
568+ } = if enable_intercepted_exec_policy_shell_wrapper_parsing {
569+ // In this codepath, the first argument in `commands` could be a bare
570+ // name like `find` instead of an absolute path like `/usr/bin/find`.
571+ // It could also be a shell built-in like `echo`.
572+ commands_for_intercepted_exec_policy ( program, argv)
573+ } else {
574+ // In this codepath, `commands` has a single entry where the program
575+ // is always an absolute path.
576+ CandidateCommands {
577+ commands : vec ! [ join_program_and_argv( program, argv) ] ,
578+ used_complex_parsing : false ,
579+ }
580+ } ;
581+
582+ let fallback = |cmd : & [ String ] | {
583+ crate :: exec_policy:: render_decision_for_unmatched_command (
584+ approval_policy,
585+ sandbox_policy,
586+ cmd,
587+ sandbox_permissions,
588+ used_complex_parsing,
589+ )
590+ } ;
591+
592+ policy. check_multiple_with_options (
593+ commands. iter ( ) ,
594+ & fallback,
595+ & MatchOptions {
596+ resolve_host_executables : true ,
597+ } ,
598+ )
599+ }
600+
601+ struct CandidateCommands {
602+ commands : Vec < Vec < String > > ,
603+ used_complex_parsing : bool ,
604+ }
605+
606+ fn commands_for_intercepted_exec_policy (
607+ program : & AbsolutePathBuf ,
608+ argv : & [ String ] ,
609+ ) -> CandidateCommands {
610+ if let [ _, flag, script] = argv {
611+ let shell_command = [
612+ program. to_string_lossy ( ) . to_string ( ) ,
613+ flag. clone ( ) ,
614+ script. clone ( ) ,
615+ ] ;
616+ if let Some ( commands) = parse_shell_lc_plain_commands ( & shell_command) {
617+ return CandidateCommands {
618+ commands,
619+ used_complex_parsing : false ,
620+ } ;
621+ }
622+ if let Some ( single_command) = parse_shell_lc_single_command_prefix ( & shell_command) {
623+ return CandidateCommands {
624+ commands : vec ! [ single_command] ,
625+ used_complex_parsing : true ,
626+ } ;
627+ }
628+ }
629+
630+ CandidateCommands {
631+ commands : vec ! [ join_program_and_argv( program, argv) ] ,
632+ used_complex_parsing : false ,
633+ }
634+ }
635+
555636struct CoreShellCommandExecutor {
556637 command : Vec < String > ,
557638 cwd : PathBuf ,
0 commit comments