Skip to content

Commit acf6838

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

File tree

2 files changed

+177
-20
lines changed

2 files changed

+177
-20
lines changed

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

Lines changed: 58 additions & 18 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;
@@ -493,29 +495,17 @@ impl EscalationPolicy for CoreShellActionProvider {
493495
.await;
494496
}
495497

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(
498+
let evaluation = {
499+
let policy = self.policy.read().await;
500+
evaluate_intercepted_exec_policy(
501+
&policy,
502+
program,
503+
argv,
508504
self.approval_policy,
509505
&self.sandbox_policy,
510-
cmd,
511506
self.sandbox_permissions,
512-
used_complex_parsing,
513507
)
514508
};
515-
let evaluation = {
516-
let policy = self.policy.read().await;
517-
policy.check_multiple(commands.iter(), &fallback)
518-
};
519509
// When true, means the Evaluation was due to *.rules, not the
520510
// fallback function.
521511
let decision_driven_by_policy =
@@ -552,6 +542,56 @@ impl EscalationPolicy for CoreShellActionProvider {
552542
}
553543
}
554544

545+
fn evaluate_intercepted_exec_policy(
546+
policy: &Policy,
547+
program: &AbsolutePathBuf,
548+
argv: &[String],
549+
approval_policy: AskForApproval,
550+
sandbox_policy: &SandboxPolicy,
551+
sandbox_permissions: SandboxPermissions,
552+
) -> Evaluation {
553+
let (commands, used_complex_parsing) = commands_for_intercepted_exec_policy(program, argv);
554+
555+
let fallback = |cmd: &[String]| {
556+
crate::exec_policy::render_decision_for_unmatched_command(
557+
approval_policy,
558+
sandbox_policy,
559+
cmd,
560+
sandbox_permissions,
561+
used_complex_parsing,
562+
)
563+
};
564+
565+
policy.check_multiple_with_options(
566+
commands.iter(),
567+
&fallback,
568+
&MatchOptions {
569+
resolve_host_executables: true,
570+
},
571+
)
572+
}
573+
574+
fn commands_for_intercepted_exec_policy(
575+
program: &AbsolutePathBuf,
576+
argv: &[String],
577+
) -> (Vec<Vec<String>>, bool) {
578+
if let [_, flag, script] = argv {
579+
let shell_command = [
580+
program.to_string_lossy().to_string(),
581+
flag.clone(),
582+
script.clone(),
583+
];
584+
if let Some(commands) = parse_shell_lc_plain_commands(&shell_command) {
585+
return (commands, false);
586+
}
587+
if let Some(single_command) = parse_shell_lc_single_command_prefix(&shell_command) {
588+
return (vec![single_command], true);
589+
}
590+
}
591+
592+
(vec![join_program_and_argv(program, argv)], false)
593+
}
594+
555595
struct CoreShellCommandExecutor {
556596
command: Vec<String>,
557597
cwd: PathBuf,

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

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use super::CoreShellActionProvider;
22
#[cfg(target_os = "macos")]
33
use super::CoreShellCommandExecutor;
44
use super::ParsedShellCommand;
5+
use super::commands_for_intercepted_exec_policy;
6+
use super::evaluate_intercepted_exec_policy;
57
use super::extract_shell_script;
68
use super::join_program_and_argv;
79
use super::map_exec_result;
@@ -12,14 +14,16 @@ use crate::config::Permissions;
1214
#[cfg(target_os = "macos")]
1315
use crate::config::types::ShellEnvironmentPolicy;
1416
use crate::exec::SandboxType;
15-
#[cfg(target_os = "macos")]
1617
use crate::protocol::AskForApproval;
1718
use crate::protocol::ReadOnlyAccess;
1819
use crate::protocol::SandboxPolicy;
19-
#[cfg(target_os = "macos")]
2020
use crate::sandboxing::SandboxPermissions;
2121
#[cfg(target_os = "macos")]
2222
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
23+
use codex_execpolicy::Decision;
24+
use codex_execpolicy::Evaluation;
25+
use codex_execpolicy::PolicyParser;
26+
use codex_execpolicy::RuleMatch;
2327
#[cfg(target_os = "macos")]
2428
use codex_protocol::config_types::WindowsSandboxLevel;
2529
use codex_protocol::models::FileSystemPermissions;
@@ -36,8 +40,25 @@ use codex_utils_absolute_path::AbsolutePathBuf;
3640
use pretty_assertions::assert_eq;
3741
#[cfg(target_os = "macos")]
3842
use std::collections::HashMap;
43+
use std::path::PathBuf;
3944
use std::time::Duration;
4045

46+
fn host_absolute_path(segments: &[&str]) -> String {
47+
let mut path = if cfg!(windows) {
48+
PathBuf::from(r"C:\")
49+
} else {
50+
PathBuf::from("/")
51+
};
52+
for segment in segments {
53+
path.push(segment);
54+
}
55+
path.to_string_lossy().into_owned()
56+
}
57+
58+
fn starlark_string(value: &str) -> String {
59+
value.replace('\\', "\\\\").replace('"', "\\\"")
60+
}
61+
4162
#[test]
4263
fn extract_shell_script_preserves_login_flag() {
4364
assert_eq!(
@@ -126,6 +147,24 @@ fn join_program_and_argv_replaces_original_argv_zero() {
126147
);
127148
}
128149

150+
#[test]
151+
fn commands_for_intercepted_exec_policy_uses_program_path_for_shell_wrapper_parsing() {
152+
let program = AbsolutePathBuf::try_from(host_absolute_path(&["bin", "bash"])).unwrap();
153+
assert_eq!(
154+
commands_for_intercepted_exec_policy(
155+
&program,
156+
&["not-bash".into(), "-lc".into(), "git status && pwd".into()],
157+
),
158+
(
159+
vec![
160+
vec!["git".to_string(), "status".to_string()],
161+
vec!["pwd".to_string()],
162+
],
163+
false,
164+
)
165+
);
166+
}
167+
129168
#[test]
130169
fn map_exec_result_preserves_stdout_and_stderr() {
131170
let out = map_exec_result(
@@ -203,6 +242,84 @@ fn shell_request_escalation_execution_is_explicit() {
203242
);
204243
}
205244

245+
#[test]
246+
fn intercepted_exec_policy_uses_host_executable_mappings() {
247+
let git_path = host_absolute_path(&["usr", "bin", "git"]);
248+
let git_path_literal = starlark_string(&git_path);
249+
let policy_src = format!(
250+
r#"
251+
prefix_rule(pattern = ["git", "status"], decision = "prompt")
252+
host_executable(name = "git", paths = ["{git_path_literal}"])
253+
"#
254+
);
255+
let mut parser = PolicyParser::new();
256+
parser.parse("test.rules", &policy_src).unwrap();
257+
let policy = parser.build();
258+
let program = AbsolutePathBuf::try_from(git_path).unwrap();
259+
260+
let evaluation = evaluate_intercepted_exec_policy(
261+
&policy,
262+
&program,
263+
&["git".to_string(), "status".to_string()],
264+
AskForApproval::OnRequest,
265+
&SandboxPolicy::new_read_only_policy(),
266+
SandboxPermissions::UseDefault,
267+
);
268+
269+
assert_eq!(
270+
evaluation,
271+
Evaluation {
272+
decision: Decision::Prompt,
273+
matched_rules: vec![RuleMatch::PrefixRuleMatch {
274+
matched_prefix: vec!["git".to_string(), "status".to_string()],
275+
decision: Decision::Prompt,
276+
resolved_program: Some(program),
277+
justification: None,
278+
}],
279+
}
280+
);
281+
assert!(CoreShellActionProvider::decision_driven_by_policy(
282+
&evaluation.matched_rules,
283+
evaluation.decision
284+
));
285+
}
286+
287+
#[test]
288+
fn intercepted_exec_policy_rejects_disallowed_host_executable_mapping() {
289+
let allowed_git = host_absolute_path(&["usr", "bin", "git"]);
290+
let other_git = host_absolute_path(&["opt", "homebrew", "bin", "git"]);
291+
let allowed_git_literal = starlark_string(&allowed_git);
292+
let policy_src = format!(
293+
r#"
294+
prefix_rule(pattern = ["git", "status"], decision = "prompt")
295+
host_executable(name = "git", paths = ["{allowed_git_literal}"])
296+
"#
297+
);
298+
let mut parser = PolicyParser::new();
299+
parser.parse("test.rules", &policy_src).unwrap();
300+
let policy = parser.build();
301+
let program = AbsolutePathBuf::try_from(other_git.clone()).unwrap();
302+
303+
let evaluation = evaluate_intercepted_exec_policy(
304+
&policy,
305+
&program,
306+
&["git".to_string(), "status".to_string()],
307+
AskForApproval::OnRequest,
308+
&SandboxPolicy::new_read_only_policy(),
309+
SandboxPermissions::UseDefault,
310+
);
311+
312+
assert!(matches!(
313+
evaluation.matched_rules.as_slice(),
314+
[RuleMatch::HeuristicsRuleMatch { command, .. }]
315+
if command == &vec![other_git, "status".to_string()]
316+
));
317+
assert!(!CoreShellActionProvider::decision_driven_by_policy(
318+
&evaluation.matched_rules,
319+
evaluation.decision
320+
));
321+
}
322+
206323
#[cfg(target_os = "macos")]
207324
#[tokio::test]
208325
async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions() {

0 commit comments

Comments
 (0)