@@ -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,25 @@ fn join_program_and_argv_replaces_original_argv_zero() {
126147 ) ;
127148}
128149
150+ #[ test]
151+ fn commands_for_intercepted_exec_policy_defaults_to_wrapper_command ( ) {
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![ vec![
160+ program. to_string_lossy( ) . to_string( ) ,
161+ "-lc" . to_string( ) ,
162+ "git status && pwd" . to_string( ) ,
163+ ] ] ,
164+ false
165+ )
166+ ) ;
167+ }
168+
129169#[ test]
130170fn map_exec_result_preserves_stdout_and_stderr ( ) {
131171 let out = map_exec_result (
@@ -203,6 +243,84 @@ fn shell_request_escalation_execution_is_explicit() {
203243 ) ;
204244}
205245
246+ #[ test]
247+ fn intercepted_exec_policy_uses_host_executable_mappings ( ) {
248+ let git_path = host_absolute_path ( & [ "usr" , "bin" , "git" ] ) ;
249+ let git_path_literal = starlark_string ( & git_path) ;
250+ let policy_src = format ! (
251+ r#"
252+ prefix_rule(pattern = ["git", "status"], decision = "prompt")
253+ host_executable(name = "git", paths = ["{git_path_literal}"])
254+ "#
255+ ) ;
256+ let mut parser = PolicyParser :: new ( ) ;
257+ parser. parse ( "test.rules" , & policy_src) . unwrap ( ) ;
258+ let policy = parser. build ( ) ;
259+ let program = AbsolutePathBuf :: try_from ( git_path) . unwrap ( ) ;
260+
261+ let evaluation = evaluate_intercepted_exec_policy (
262+ & policy,
263+ & program,
264+ & [ "git" . to_string ( ) , "status" . to_string ( ) ] ,
265+ AskForApproval :: OnRequest ,
266+ & SandboxPolicy :: new_read_only_policy ( ) ,
267+ SandboxPermissions :: UseDefault ,
268+ ) ;
269+
270+ assert_eq ! (
271+ evaluation,
272+ Evaluation {
273+ decision: Decision :: Prompt ,
274+ matched_rules: vec![ RuleMatch :: PrefixRuleMatch {
275+ matched_prefix: vec![ "git" . to_string( ) , "status" . to_string( ) ] ,
276+ decision: Decision :: Prompt ,
277+ resolved_program: Some ( program) ,
278+ justification: None ,
279+ } ] ,
280+ }
281+ ) ;
282+ assert ! ( CoreShellActionProvider :: decision_driven_by_policy(
283+ & evaluation. matched_rules,
284+ evaluation. decision
285+ ) ) ;
286+ }
287+
288+ #[ test]
289+ fn intercepted_exec_policy_rejects_disallowed_host_executable_mapping ( ) {
290+ let allowed_git = host_absolute_path ( & [ "usr" , "bin" , "git" ] ) ;
291+ let other_git = host_absolute_path ( & [ "opt" , "homebrew" , "bin" , "git" ] ) ;
292+ let allowed_git_literal = starlark_string ( & allowed_git) ;
293+ let policy_src = format ! (
294+ r#"
295+ prefix_rule(pattern = ["git", "status"], decision = "prompt")
296+ host_executable(name = "git", paths = ["{allowed_git_literal}"])
297+ "#
298+ ) ;
299+ let mut parser = PolicyParser :: new ( ) ;
300+ parser. parse ( "test.rules" , & policy_src) . unwrap ( ) ;
301+ let policy = parser. build ( ) ;
302+ let program = AbsolutePathBuf :: try_from ( other_git. clone ( ) ) . unwrap ( ) ;
303+
304+ let evaluation = evaluate_intercepted_exec_policy (
305+ & policy,
306+ & program,
307+ & [ "git" . to_string ( ) , "status" . to_string ( ) ] ,
308+ AskForApproval :: OnRequest ,
309+ & SandboxPolicy :: new_read_only_policy ( ) ,
310+ SandboxPermissions :: UseDefault ,
311+ ) ;
312+
313+ assert ! ( matches!(
314+ evaluation. matched_rules. as_slice( ) ,
315+ [ RuleMatch :: HeuristicsRuleMatch { command, .. } ]
316+ if command == & vec![ other_git, "status" . to_string( ) ]
317+ ) ) ;
318+ assert ! ( !CoreShellActionProvider :: decision_driven_by_policy(
319+ & evaluation. matched_rules,
320+ evaluation. decision
321+ ) ) ;
322+ }
323+
206324#[ cfg( target_os = "macos" ) ]
207325#[ tokio:: test]
208326async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions ( ) {
0 commit comments