@@ -2,6 +2,8 @@ use super::CoreShellActionProvider;
22#[ cfg( target_os = "macos" ) ]
33use super :: CoreShellCommandExecutor ;
44use super :: ParsedShellCommand ;
5+ use super :: commands_for_intercepted_exec_policy;
6+ use super :: evaluate_intercepted_exec_policy;
57use super :: extract_shell_script;
68use super :: join_program_and_argv;
79use super :: map_exec_result;
@@ -12,14 +14,16 @@ use crate::config::Permissions;
1214#[ cfg( target_os = "macos" ) ]
1315use crate :: config:: types:: ShellEnvironmentPolicy ;
1416use crate :: exec:: SandboxType ;
15- #[ cfg( target_os = "macos" ) ]
1617use crate :: protocol:: AskForApproval ;
1718use crate :: protocol:: ReadOnlyAccess ;
1819use crate :: protocol:: SandboxPolicy ;
19- #[ cfg( target_os = "macos" ) ]
2020use crate :: sandboxing:: SandboxPermissions ;
2121#[ cfg( target_os = "macos" ) ]
2222use 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" ) ]
2428use codex_protocol:: config_types:: WindowsSandboxLevel ;
2529use codex_protocol:: models:: FileSystemPermissions ;
@@ -36,8 +40,25 @@ use codex_utils_absolute_path::AbsolutePathBuf;
3640use pretty_assertions:: assert_eq;
3741#[ cfg( target_os = "macos" ) ]
3842use std:: collections:: HashMap ;
43+ use std:: path:: PathBuf ;
3944use 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]
4263fn 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]
130169fn 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]
208325async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions ( ) {
0 commit comments