Skip to content

Commit ac119a1

Browse files
committed
core: adopt host_executable() rules in zsh-fork
1 parent b148d98 commit ac119a1

File tree

6 files changed

+515
-143
lines changed

6 files changed

+515
-143
lines changed

codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use crate::tools::sandboxing::SandboxablePreference;
1616
use crate::tools::sandboxing::ToolCtx;
1717
use crate::tools::sandboxing::ToolError;
1818
use codex_execpolicy::Decision;
19+
use codex_execpolicy::Evaluation;
20+
use codex_execpolicy::MatchOptions;
1921
use codex_execpolicy::Policy;
2022
use codex_execpolicy::RuleMatch;
2123
use 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]
435443
impl 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+
555636
struct CoreShellCommandExecutor {
556637
command: Vec<String>,
557638
cwd: PathBuf,

0 commit comments

Comments
 (0)