@@ -4,17 +4,25 @@ mod pid_tracker;
44mod seatbelt;
55
66use std:: path:: PathBuf ;
7+ use std:: process:: Stdio ;
78
89use codex_core:: config:: Config ;
10+ use codex_core:: config:: ConfigBuilder ;
911use codex_core:: config:: ConfigOverrides ;
1012use codex_core:: config:: NetworkProxyAuditMetadata ;
1113use codex_core:: exec_env:: create_env;
12- use codex_core:: landlock:: spawn_command_under_linux_sandbox ;
14+ use codex_core:: landlock:: create_linux_sandbox_command_args_for_policies ;
1315#[ cfg( target_os = "macos" ) ]
14- use codex_core:: seatbelt:: spawn_command_under_seatbelt;
15- use codex_core:: spawn:: StdioPolicy ;
16+ use codex_core:: seatbelt:: create_seatbelt_command_args_for_policies_with_extensions;
17+ #[ cfg( target_os = "macos" ) ]
18+ use codex_core:: spawn:: CODEX_SANDBOX_ENV_VAR ;
19+ use codex_core:: spawn:: CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR ;
1620use codex_protocol:: config_types:: SandboxMode ;
21+ use codex_protocol:: permissions:: NetworkSandboxPolicy ;
1722use codex_utils_cli:: CliConfigOverrides ;
23+ use tokio:: process:: Child ;
24+ use tokio:: process:: Command as TokioCommand ;
25+ use toml:: Value as TomlValue ;
1826
1927use crate :: LandlockCommand ;
2028use crate :: SeatbeltCommand ;
@@ -109,16 +117,12 @@ async fn run_command_under_sandbox(
109117 sandbox_type : SandboxType ,
110118 log_denials : bool ,
111119) -> anyhow:: Result < ( ) > {
112- let sandbox_mode = create_sandbox_mode ( full_auto) ;
113- let config = Config :: load_with_cli_overrides_and_harness_overrides (
120+ let config = load_debug_sandbox_config (
114121 config_overrides
115122 . parse_overrides ( )
116123 . map_err ( anyhow:: Error :: msg) ?,
117- ConfigOverrides {
118- sandbox_mode : Some ( sandbox_mode) ,
119- codex_linux_sandbox_exe,
120- ..Default :: default ( )
121- } ,
124+ codex_linux_sandbox_exe,
125+ full_auto,
122126 )
123127 . await ?;
124128
@@ -130,7 +134,6 @@ async fn run_command_under_sandbox(
130134 // separately.
131135 let sandbox_policy_cwd = cwd. clone ( ) ;
132136
133- let stdio_policy = StdioPolicy :: Inherit ;
134137 let env = create_env ( & config. permissions . shell_environment_policy , None ) ;
135138
136139 // Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
@@ -238,14 +241,29 @@ async fn run_command_under_sandbox(
238241 let mut child = match sandbox_type {
239242 #[ cfg( target_os = "macos" ) ]
240243 SandboxType :: Seatbelt => {
241- spawn_command_under_seatbelt (
244+ let args = create_seatbelt_command_args_for_policies_with_extensions (
242245 command,
243- cwd ,
244- config. permissions . sandbox_policy . get ( ) ,
246+ & config . permissions . file_system_sandbox_policy ,
247+ config. permissions . network_sandbox_policy ,
245248 sandbox_policy_cwd. as_path ( ) ,
246- stdio_policy ,
249+ false ,
247250 network. as_ref ( ) ,
251+ None ,
252+ ) ;
253+ let network_policy = config. permissions . network_sandbox_policy ;
254+ spawn_debug_sandbox_child (
255+ PathBuf :: from ( "/usr/bin/sandbox-exec" ) ,
256+ args,
257+ None ,
258+ cwd,
259+ network_policy,
248260 env,
261+ |env_map| {
262+ env_map. insert ( CODEX_SANDBOX_ENV_VAR . to_string ( ) , "seatbelt" . to_string ( ) ) ;
263+ if let Some ( network) = network. as_ref ( ) {
264+ network. apply_to_env ( env_map) ;
265+ }
266+ } ,
249267 )
250268 . await ?
251269 }
@@ -256,16 +274,28 @@ async fn run_command_under_sandbox(
256274 . codex_linux_sandbox_exe
257275 . expect ( "codex-linux-sandbox executable not found" ) ;
258276 let use_bwrap_sandbox = config. features . enabled ( Feature :: UseLinuxSandboxBwrap ) ;
259- spawn_command_under_linux_sandbox (
260- codex_linux_sandbox_exe,
277+ let args = create_linux_sandbox_command_args_for_policies (
261278 command,
262- cwd,
263279 config. permissions . sandbox_policy . get ( ) ,
280+ & config. permissions . file_system_sandbox_policy ,
281+ config. permissions . network_sandbox_policy ,
264282 sandbox_policy_cwd. as_path ( ) ,
265283 use_bwrap_sandbox,
266- stdio_policy,
267- network. as_ref ( ) ,
284+ false ,
285+ ) ;
286+ let network_policy = config. permissions . network_sandbox_policy ;
287+ spawn_debug_sandbox_child (
288+ codex_linux_sandbox_exe,
289+ args,
290+ Some ( "codex-linux-sandbox" ) ,
291+ cwd,
292+ network_policy,
268293 env,
294+ |env_map| {
295+ if let Some ( network) = network. as_ref ( ) {
296+ network. apply_to_env ( env_map) ;
297+ }
298+ } ,
269299 )
270300 . await ?
271301 }
@@ -304,3 +334,202 @@ pub fn create_sandbox_mode(full_auto: bool) -> SandboxMode {
304334 SandboxMode :: ReadOnly
305335 }
306336}
337+
338+ async fn spawn_debug_sandbox_child (
339+ program : PathBuf ,
340+ args : Vec < String > ,
341+ arg0 : Option < & str > ,
342+ cwd : PathBuf ,
343+ network_sandbox_policy : NetworkSandboxPolicy ,
344+ mut env : std:: collections:: HashMap < String , String > ,
345+ apply_env : impl FnOnce ( & mut std:: collections:: HashMap < String , String > ) ,
346+ ) -> std:: io:: Result < Child > {
347+ let mut cmd = TokioCommand :: new ( & program) ;
348+ #[ cfg( unix) ]
349+ cmd. arg0 ( arg0. map_or_else ( || program. to_string_lossy ( ) . to_string ( ) , String :: from) ) ;
350+ #[ cfg( not( unix) ) ]
351+ let _ = arg0;
352+ cmd. args ( args) ;
353+ cmd. current_dir ( cwd) ;
354+ apply_env ( & mut env) ;
355+ cmd. env_clear ( ) ;
356+ cmd. envs ( env) ;
357+
358+ if !network_sandbox_policy. is_enabled ( ) {
359+ cmd. env ( CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR , "1" ) ;
360+ }
361+
362+ cmd. stdin ( Stdio :: inherit ( ) )
363+ . stdout ( Stdio :: inherit ( ) )
364+ . stderr ( Stdio :: inherit ( ) )
365+ . kill_on_drop ( true )
366+ . spawn ( )
367+ }
368+
369+ async fn load_debug_sandbox_config (
370+ cli_overrides : Vec < ( String , TomlValue ) > ,
371+ codex_linux_sandbox_exe : Option < PathBuf > ,
372+ full_auto : bool ,
373+ ) -> anyhow:: Result < Config > {
374+ load_debug_sandbox_config_with_codex_home (
375+ cli_overrides,
376+ codex_linux_sandbox_exe,
377+ full_auto,
378+ None ,
379+ )
380+ . await
381+ }
382+
383+ async fn load_debug_sandbox_config_with_codex_home (
384+ cli_overrides : Vec < ( String , TomlValue ) > ,
385+ codex_linux_sandbox_exe : Option < PathBuf > ,
386+ full_auto : bool ,
387+ codex_home : Option < PathBuf > ,
388+ ) -> anyhow:: Result < Config > {
389+ let config = build_debug_sandbox_config (
390+ cli_overrides. clone ( ) ,
391+ ConfigOverrides {
392+ codex_linux_sandbox_exe : codex_linux_sandbox_exe. clone ( ) ,
393+ ..Default :: default ( )
394+ } ,
395+ codex_home. clone ( ) ,
396+ )
397+ . await ?;
398+
399+ if config_uses_permission_profiles ( & config) {
400+ if full_auto {
401+ anyhow:: bail!(
402+ "`codex sandbox --full-auto` is only supported for legacy `sandbox_mode` configs; choose a writable `[permissions]` profile instead"
403+ ) ;
404+ }
405+ return Ok ( config) ;
406+ }
407+
408+ build_debug_sandbox_config (
409+ cli_overrides,
410+ ConfigOverrides {
411+ sandbox_mode : Some ( create_sandbox_mode ( full_auto) ) ,
412+ codex_linux_sandbox_exe,
413+ ..Default :: default ( )
414+ } ,
415+ codex_home,
416+ )
417+ . await
418+ . map_err ( Into :: into)
419+ }
420+
421+ async fn build_debug_sandbox_config (
422+ cli_overrides : Vec < ( String , TomlValue ) > ,
423+ harness_overrides : ConfigOverrides ,
424+ codex_home : Option < PathBuf > ,
425+ ) -> std:: io:: Result < Config > {
426+ let mut builder = ConfigBuilder :: default ( )
427+ . cli_overrides ( cli_overrides)
428+ . harness_overrides ( harness_overrides) ;
429+ if let Some ( codex_home) = codex_home {
430+ builder = builder
431+ . codex_home ( codex_home. clone ( ) )
432+ . fallback_cwd ( Some ( codex_home) ) ;
433+ }
434+ builder. build ( ) . await
435+ }
436+
437+ fn config_uses_permission_profiles ( config : & Config ) -> bool {
438+ config
439+ . config_layer_stack
440+ . effective_config ( )
441+ . get ( "default_permissions" )
442+ . is_some ( )
443+ }
444+
445+ #[ cfg( test) ]
446+ mod tests {
447+ use super :: * ;
448+ use tempfile:: TempDir ;
449+
450+ fn escape_toml_path ( path : & std:: path:: Path ) -> String {
451+ path. display ( ) . to_string ( ) . replace ( '\\' , "\\ \\ " )
452+ }
453+
454+ fn write_permissions_profile_config (
455+ codex_home : & TempDir ,
456+ ) -> std:: io:: Result < ( PathBuf , PathBuf ) > {
457+ let docs = codex_home. path ( ) . join ( "docs" ) ;
458+ let private = docs. join ( "private" ) ;
459+ std:: fs:: create_dir_all ( & private) ?;
460+ let config = format ! (
461+ "default_permissions = \" limited-read-test\" \n \
462+ [permissions.limited-read-test.filesystem]\n \
463+ \" :minimal\" = \" read\" \n \
464+ \" {}\" = \" read\" \n \
465+ \" {}\" = \" none\" \n \
466+ \n \
467+ [permissions.limited-read-test.network]\n \
468+ enabled = true\n ",
469+ escape_toml_path( & docs) ,
470+ escape_toml_path( & private) ,
471+ ) ;
472+ std:: fs:: write ( codex_home. path ( ) . join ( "config.toml" ) , config) ?;
473+ Ok ( ( docs, private) )
474+ }
475+
476+ #[ tokio:: test]
477+ async fn debug_sandbox_honors_active_permission_profiles ( ) -> anyhow:: Result < ( ) > {
478+ let codex_home = TempDir :: new ( ) ?;
479+ let ( docs, private) = write_permissions_profile_config ( & codex_home) ?;
480+
481+ let config = load_debug_sandbox_config_with_codex_home (
482+ Vec :: new ( ) ,
483+ None ,
484+ false ,
485+ Some ( codex_home. path ( ) . to_path_buf ( ) ) ,
486+ )
487+ . await ?;
488+
489+ assert ! ( config_uses_permission_profiles( & config) ) ;
490+ let readable_roots = config
491+ . permissions
492+ . file_system_sandbox_policy
493+ . get_readable_roots_with_cwd ( config. cwd . as_path ( ) ) ;
494+ assert ! (
495+ readable_roots
496+ . iter( )
497+ . any( |path| path. as_path( ) == docs. as_path( ) ) ,
498+ "expected {docs:?} in readable roots, got {readable_roots:?}"
499+ ) ;
500+ let unreadable_roots = config
501+ . permissions
502+ . file_system_sandbox_policy
503+ . get_unreadable_roots_with_cwd ( config. cwd . as_path ( ) ) ;
504+ assert ! (
505+ unreadable_roots
506+ . iter( )
507+ . any( |path| path. as_path( ) == private. as_path( ) ) ,
508+ "expected {private:?} in unreadable roots, got {unreadable_roots:?}"
509+ ) ;
510+
511+ Ok ( ( ) )
512+ }
513+
514+ #[ tokio:: test]
515+ async fn debug_sandbox_rejects_full_auto_for_permission_profiles ( ) -> anyhow:: Result < ( ) > {
516+ let codex_home = TempDir :: new ( ) ?;
517+ let _ = write_permissions_profile_config ( & codex_home) ?;
518+
519+ let err = load_debug_sandbox_config_with_codex_home (
520+ Vec :: new ( ) ,
521+ None ,
522+ true ,
523+ Some ( codex_home. path ( ) . to_path_buf ( ) ) ,
524+ )
525+ . await
526+ . expect_err ( "full-auto should be rejected for active permission profiles" ) ;
527+
528+ assert ! (
529+ err. to_string( ) . contains( "--full-auto" ) ,
530+ "unexpected error: {err}"
531+ ) ;
532+
533+ Ok ( ( ) )
534+ }
535+ }
0 commit comments