5757//!
5858use std:: path:: Path ;
5959use std:: path:: PathBuf ;
60+ use std:: sync:: Arc ;
6061
62+ use anyhow:: Context as _;
6163use clap:: Parser ;
64+ use codex_core:: ExecPolicyError ;
65+ use codex_core:: bash:: parse_shell_lc_plain_commands;
66+ use codex_core:: config:: find_codex_home;
67+ use codex_core:: exec_policy_for;
68+ use codex_core:: features:: Features ;
69+ use codex_core:: is_dangerous_command:: command_might_be_dangerous;
70+ use codex_execpolicy:: Decision ;
71+ use codex_execpolicy:: Policy ;
72+ use codex_execpolicy:: RuleMatch ;
73+ use tracing:: debug;
6274use tracing_subscriber:: EnvFilter ;
6375use tracing_subscriber:: { self } ;
6476
@@ -76,6 +88,7 @@ mod stopwatch;
7688/// Default value of --execve option relative to the current executable.
7789/// Note this must match the name of the binary as specified in Cargo.toml.
7890const CODEX_EXECVE_WRAPPER_EXE_NAME : & str = "codex-execve-wrapper" ;
91+ const POLICY_DIR_NAME : & str = "policy" ;
7992
8093#[ derive( Parser ) ]
8194#[ clap( version) ]
@@ -87,6 +100,10 @@ struct McpServerCli {
87100 /// Path to Bash that has been patched to support execve() wrapping.
88101 #[ arg( long = "bash" ) ]
89102 bash_path : Option < PathBuf > ,
103+
104+ /// Strip program paths before applying execpolicy (e.g., /usr/bin/echo -> echo).
105+ #[ arg( long, default_value_t = true ) ]
106+ strip_program_paths : bool ,
90107}
91108
92109#[ tokio:: main]
@@ -113,13 +130,19 @@ pub async fn main_mcp_server() -> anyhow::Result<()> {
113130 Some ( path) => path,
114131 None => mcp:: get_bash_path ( ) ?,
115132 } ;
133+ let policy = load_exec_policy ( ) . await ?;
116134
117135 tracing:: info!( "Starting MCP server" ) ;
118- let service = mcp:: serve ( bash_path, execve_wrapper, dummy_exec_policy)
119- . await
120- . inspect_err ( |e| {
121- tracing:: error!( "serving error: {:?}" , e) ;
122- } ) ?;
136+ let service = mcp:: serve (
137+ bash_path,
138+ execve_wrapper,
139+ policy. clone ( ) ,
140+ cli. strip_program_paths ,
141+ )
142+ . await
143+ . inspect_err ( |e| {
144+ tracing:: error!( "serving error: {:?}" , e) ;
145+ } ) ?;
123146
124147 service. waiting ( ) . await ?;
125148 Ok ( ( ) )
@@ -146,26 +169,64 @@ pub async fn main_execve_wrapper() -> anyhow::Result<()> {
146169 std:: process:: exit ( exit_code) ;
147170}
148171
149- // TODO: replace with execpolicy
150-
151- fn dummy_exec_policy ( file : & Path , argv : & [ String ] , _workdir : & Path ) -> ExecPolicyOutcome {
152- if file. ends_with ( "rm" ) {
153- ExecPolicyOutcome :: Forbidden
154- } else if file. ends_with ( "git" ) {
155- ExecPolicyOutcome :: Prompt {
156- run_with_escalated_permissions : false ,
157- }
158- } else if file == Path :: new ( "/opt/homebrew/bin/gh" )
159- && let [ _, arg1, arg2, ..] = argv
160- && arg1 == "issue"
161- && arg2 == "list"
162- {
163- ExecPolicyOutcome :: Allow {
164- run_with_escalated_permissions : true ,
172+ /// Decide how to handle an exec() call for a specific command.
173+ ///
174+ /// `file` is the absolute, canonical path to the executable to run, i.e. the first arg to exec.
175+ /// `argv` is the argv, including the program name (`argv[0]`).
176+ pub ( crate ) fn evaluate_exec_policy (
177+ policy : & Policy ,
178+ file : & Path ,
179+ argv : & [ String ] ,
180+ strip_program_paths : bool ,
181+ ) -> ExecPolicyOutcome {
182+ let command: Vec < String > = std:: iter:: once ( format_program_name ( file, strip_program_paths) )
183+ // Use the normalized program name instead of argv[0].
184+ . chain ( argv. iter ( ) . skip ( 1 ) . cloned ( ) )
185+ . collect ( ) ;
186+ let commands = parse_shell_lc_plain_commands ( & command) . unwrap_or_else ( || vec ! [ command] ) ;
187+ let evaluation = policy. check_multiple ( commands. iter ( ) , & |cmd| {
188+ if command_might_be_dangerous ( cmd) {
189+ Decision :: Prompt
190+ } else {
191+ Decision :: Allow
165192 }
193+ } ) ;
194+
195+ // decisions driven by policy should run outside sandbox
196+ let decision_driven_by_policy = evaluation. matched_rules . iter ( ) . any ( |rule_match| {
197+ !matches ! ( rule_match, RuleMatch :: HeuristicsRuleMatch { .. } )
198+ && rule_match. decision ( ) == evaluation. decision
199+ } ) ;
200+
201+ match evaluation. decision {
202+ Decision :: Forbidden => ExecPolicyOutcome :: Forbidden ,
203+ Decision :: Prompt => ExecPolicyOutcome :: Prompt {
204+ run_with_escalated_permissions : decision_driven_by_policy,
205+ } ,
206+ Decision :: Allow => ExecPolicyOutcome :: Allow {
207+ run_with_escalated_permissions : decision_driven_by_policy,
208+ } ,
209+ }
210+ }
211+
212+ fn format_program_name ( path : & Path , strip_program_paths : bool ) -> String {
213+ if strip_program_paths {
214+ path. file_name ( )
215+ . and_then ( |name| name. to_str ( ) )
216+ . map ( str:: to_string)
217+ . unwrap_or_else ( || path. display ( ) . to_string ( ) )
166218 } else {
167- ExecPolicyOutcome :: Allow {
168- run_with_escalated_permissions : false ,
169- }
219+ path. display ( ) . to_string ( )
170220 }
171221}
222+
223+ async fn load_exec_policy ( ) -> anyhow:: Result < Arc < Policy > > {
224+ let codex_home = find_codex_home ( ) . context ( "failed to resolve codex_home for execpolicy" ) ?;
225+ let policy_dir = codex_home. join ( POLICY_DIR_NAME ) ;
226+ let policy = exec_policy_for ( & Features :: with_defaults ( ) , & codex_home)
227+ . await
228+ . map_err ( |err : ExecPolicyError | anyhow:: anyhow!( err) ) ?;
229+ let policy = policy. read ( ) . await . clone ( ) ;
230+ debug ! ( "loaded execpolicy from {}" , policy_dir. display( ) ) ;
231+ Ok ( Arc :: new ( policy) )
232+ }
0 commit comments