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:: config:: find_codex_home;
65+ use codex_core:: is_dangerous_command:: command_might_be_dangerous;
66+ use codex_execpolicy:: Decision ;
67+ use codex_execpolicy:: Policy ;
68+ use codex_execpolicy:: RuleMatch ;
69+ use rmcp:: ErrorData as McpError ;
70+ use tokio:: sync:: RwLock ;
6271use tracing_subscriber:: EnvFilter ;
6372use tracing_subscriber:: { self } ;
6473
@@ -87,6 +96,11 @@ struct McpServerCli {
8796 /// Path to Bash that has been patched to support execve() wrapping.
8897 #[ arg( long = "bash" ) ]
8998 bash_path : Option < PathBuf > ,
99+
100+ /// Preserve program paths when applying execpolicy (e.g., keep /usr/bin/echo instead of echo).
101+ /// Note: this does change the actual program being run.
102+ #[ arg( long) ]
103+ preserve_program_paths : bool ,
90104}
91105
92106#[ tokio:: main]
@@ -113,13 +127,19 @@ pub async fn main_mcp_server() -> anyhow::Result<()> {
113127 Some ( path) => path,
114128 None => mcp:: get_bash_path ( ) ?,
115129 } ;
130+ let policy = Arc :: new ( RwLock :: new ( load_exec_policy ( ) . await ?) ) ;
116131
117132 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- } ) ?;
133+ let service = mcp:: serve (
134+ bash_path,
135+ execve_wrapper,
136+ policy,
137+ cli. preserve_program_paths ,
138+ )
139+ . await
140+ . inspect_err ( |e| {
141+ tracing:: error!( "serving error: {:?}" , e) ;
142+ } ) ?;
123143
124144 service. waiting ( ) . await ?;
125145 Ok ( ( ) )
@@ -146,26 +166,116 @@ pub async fn main_execve_wrapper() -> anyhow::Result<()> {
146166 std:: process:: exit ( exit_code) ;
147167}
148168
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 ,
169+ /// Decide how to handle an exec() call for a specific command.
170+ ///
171+ /// `file` is the absolute, canonical path to the executable to run, i.e. the first arg to exec.
172+ /// `argv` is the argv, including the program name (`argv[0]`).
173+ pub ( crate ) fn evaluate_exec_policy (
174+ policy : & Policy ,
175+ file : & Path ,
176+ argv : & [ String ] ,
177+ preserve_program_paths : bool ,
178+ ) -> Result < ExecPolicyOutcome , McpError > {
179+ let program_name = format_program_name ( file, preserve_program_paths) . ok_or_else ( || {
180+ McpError :: internal_error (
181+ format ! ( "failed to format program name for `{}`" , file. display( ) ) ,
182+ None ,
183+ )
184+ } ) ?;
185+ let command: Vec < String > = std:: iter:: once ( program_name)
186+ // Use the normalized program name instead of argv[0].
187+ . chain ( argv. iter ( ) . skip ( 1 ) . cloned ( ) )
188+ . collect ( ) ;
189+ let evaluation = policy. check ( & command, & |cmd| {
190+ if command_might_be_dangerous ( cmd) {
191+ Decision :: Prompt
192+ } else {
193+ Decision :: Allow
165194 }
195+ } ) ;
196+
197+ // decisions driven by policy should run outside sandbox
198+ let decision_driven_by_policy = evaluation. matched_rules . iter ( ) . any ( |rule_match| {
199+ !matches ! ( rule_match, RuleMatch :: HeuristicsRuleMatch { .. } )
200+ && rule_match. decision ( ) == evaluation. decision
201+ } ) ;
202+
203+ Ok ( match evaluation. decision {
204+ Decision :: Forbidden => ExecPolicyOutcome :: Forbidden ,
205+ Decision :: Prompt => ExecPolicyOutcome :: Prompt {
206+ run_with_escalated_permissions : decision_driven_by_policy,
207+ } ,
208+ Decision :: Allow => ExecPolicyOutcome :: Allow {
209+ run_with_escalated_permissions : decision_driven_by_policy,
210+ } ,
211+ } )
212+ }
213+
214+ fn format_program_name ( path : & Path , preserve_program_paths : bool ) -> Option < String > {
215+ if preserve_program_paths {
216+ path. to_str ( ) . map ( str:: to_string)
166217 } else {
167- ExecPolicyOutcome :: Allow {
168- run_with_escalated_permissions : false ,
169- }
218+ path. file_name ( ) ?. to_str ( ) . map ( str:: to_string)
219+ }
220+ }
221+
222+ async fn load_exec_policy ( ) -> anyhow:: Result < Policy > {
223+ let codex_home = find_codex_home ( ) . context ( "failed to resolve codex_home for execpolicy" ) ?;
224+ codex_core:: load_exec_policy ( & codex_home)
225+ . await
226+ . map_err ( anyhow:: Error :: from)
227+ }
228+
229+ #[ cfg( test) ]
230+ mod tests {
231+ use super :: * ;
232+ use codex_execpolicy:: Decision ;
233+ use codex_execpolicy:: Policy ;
234+ use pretty_assertions:: assert_eq;
235+ use std:: path:: Path ;
236+
237+ #[ test]
238+ fn evaluate_exec_policy_uses_heuristics_for_dangerous_commands ( ) {
239+ let policy = Policy :: empty ( ) ;
240+ let file = Path :: new ( "/bin/rm" ) ;
241+ let argv = vec ! [ "rm" . to_string( ) , "-rf" . to_string( ) , "/" . to_string( ) ] ;
242+
243+ let outcome = evaluate_exec_policy ( & policy, file, & argv, false ) . expect ( "policy evaluation" ) ;
244+
245+ assert_eq ! (
246+ outcome,
247+ ExecPolicyOutcome :: Prompt {
248+ run_with_escalated_permissions: false
249+ }
250+ ) ;
251+ }
252+
253+ #[ test]
254+ fn evaluate_exec_policy_respects_preserve_program_paths ( ) {
255+ let mut policy = Policy :: empty ( ) ;
256+ policy
257+ . add_prefix_rule (
258+ & [
259+ "/usr/local/bin/custom-cmd" . to_string ( ) ,
260+ "--flag" . to_string ( ) ,
261+ ] ,
262+ Decision :: Allow ,
263+ )
264+ . expect ( "policy rule should be added" ) ;
265+ let file = Path :: new ( "/usr/local/bin/custom-cmd" ) ;
266+ let argv = vec ! [
267+ "/usr/local/bin/custom-cmd" . to_string( ) ,
268+ "--flag" . to_string( ) ,
269+ "value" . to_string( ) ,
270+ ] ;
271+
272+ let outcome = evaluate_exec_policy ( & policy, file, & argv, true ) . expect ( "policy evaluation" ) ;
273+
274+ assert_eq ! (
275+ outcome,
276+ ExecPolicyOutcome :: Allow {
277+ run_with_escalated_permissions: true
278+ }
279+ ) ;
170280 }
171281}
0 commit comments