@@ -4,7 +4,9 @@ use codex_network_proxy::has_proxy_url_env_vars;
44use codex_network_proxy:: proxy_url_env_value;
55use codex_protocol:: permissions:: FileSystemSandboxPolicy ;
66use codex_protocol:: permissions:: NetworkSandboxPolicy ;
7+ use codex_protocol:: permissions:: PRESERVED_PATH_NAMES ;
78use codex_protocol:: protocol:: SandboxPolicy ;
9+ use codex_protocol:: protocol:: WritableRoot ;
810use codex_utils_absolute_path:: AbsolutePathBuf ;
911use std:: collections:: BTreeMap ;
1012use std:: collections:: BTreeSet ;
@@ -328,6 +330,7 @@ fn root_absolute_path() -> AbsolutePathBuf {
328330struct SeatbeltAccessRoot {
329331 root : AbsolutePathBuf ,
330332 excluded_subpaths : Vec < AbsolutePathBuf > ,
333+ preserved_path_names : Vec < String > ,
331334}
332335
333336fn build_seatbelt_access_policy (
@@ -342,9 +345,9 @@ fn build_seatbelt_access_policy(
342345 let root =
343346 normalize_path_for_sandbox ( access_root. root . as_path ( ) ) . unwrap_or ( access_root. root ) ;
344347 let root_param = format ! ( "{param_prefix}_{index}" ) ;
345- params. push ( ( root_param. clone ( ) , root. into_path_buf ( ) ) ) ;
348+ params. push ( ( root_param. clone ( ) , root. clone ( ) . into_path_buf ( ) ) ) ;
346349
347- if access_root. excluded_subpaths . is_empty ( ) {
350+ if access_root. excluded_subpaths . is_empty ( ) && access_root . preserved_path_names . is_empty ( ) {
348351 policy_components. push ( format ! ( "(subpath (param \" {root_param}\" ))" ) ) ;
349352 continue ;
350353 }
@@ -367,6 +370,11 @@ fn build_seatbelt_access_policy(
367370 "(require-not (subpath (param \" {excluded_param}\" )))"
368371 ) ) ;
369372 }
373+ for preserved_name in access_root. preserved_path_names {
374+ let regex =
375+ seatbelt_preserved_path_name_regex ( & root, & preserved_name) . replace ( '"' , "\\ \" " ) ;
376+ require_parts. push ( format ! ( r#"(require-not (regex #"{regex}"))"# ) ) ;
377+ }
370378 policy_components. push ( format ! ( "(require-all {} )" , require_parts. join( " " ) ) ) ;
371379 }
372380
@@ -380,6 +388,38 @@ fn build_seatbelt_access_policy(
380388 }
381389}
382390
391+ fn seatbelt_preserved_path_name_regex ( root : & AbsolutePathBuf , name : & str ) -> String {
392+ let mut root = root. to_string_lossy ( ) . to_string ( ) ;
393+ while root. len ( ) > 1 && root. ends_with ( '/' ) {
394+ root. pop ( ) ;
395+ }
396+ let root = regex_lite:: escape ( & root) ;
397+ let name = regex_lite:: escape ( name) ;
398+ if root == "/" {
399+ format ! ( r#"^/(.*/)?{name}(/.*)?$"# )
400+ } else {
401+ format ! ( r#"^{root}/(.*/)?{name}(/.*)?$"# )
402+ }
403+ }
404+
405+ fn preserved_path_names_for_writable_root (
406+ file_system_sandbox_policy : & FileSystemSandboxPolicy ,
407+ writable_root : & WritableRoot ,
408+ cwd : & Path ,
409+ ) -> Vec < String > {
410+ let mut names = writable_root. preserved_path_names . clone ( ) ;
411+ for name in PRESERVED_PATH_NAMES {
412+ if names. iter ( ) . any ( |existing| existing == name) {
413+ continue ;
414+ }
415+ let path = writable_root. root . join ( * name) ;
416+ if !file_system_sandbox_policy. can_write_path_with_cwd ( path. as_path ( ) , cwd) {
417+ names. push ( ( * name) . to_string ( ) ) ;
418+ }
419+ }
420+ names
421+ }
422+
383423fn build_seatbelt_unreadable_glob_policy (
384424 file_system_sandbox_policy : & FileSystemSandboxPolicy ,
385425 cwd : & Path ,
@@ -586,6 +626,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
586626 vec ! [ SeatbeltAccessRoot {
587627 root: root_absolute_path( ) ,
588628 excluded_subpaths: unreadable_roots. clone( ) ,
629+ preserved_path_names: Vec :: new( ) ,
589630 } ] ,
590631 )
591632 }
@@ -597,6 +638,11 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
597638 . get_writable_roots_with_cwd ( sandbox_policy_cwd)
598639 . into_iter ( )
599640 . map ( |root| SeatbeltAccessRoot {
641+ preserved_path_names : preserved_path_names_for_writable_root (
642+ file_system_sandbox_policy,
643+ & root,
644+ sandbox_policy_cwd,
645+ ) ,
600646 root : root. root ,
601647 excluded_subpaths : root. read_only_subpaths ,
602648 } )
@@ -618,6 +664,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
618664 vec ! [ SeatbeltAccessRoot {
619665 root: root_absolute_path( ) ,
620666 excluded_subpaths: unreadable_roots,
667+ preserved_path_names: Vec :: new( ) ,
621668 } ] ,
622669 ) ;
623670 (
@@ -638,6 +685,7 @@ pub fn create_seatbelt_command_args(args: CreateSeatbeltCommandArgsParams<'_>) -
638685 . filter ( |path| path. as_path ( ) . starts_with ( root. as_path ( ) ) )
639686 . cloned ( )
640687 . collect ( ) ,
688+ preserved_path_names : Vec :: new ( ) ,
641689 root,
642690 } )
643691 . collect ( ) ,
0 commit comments