@@ -11,7 +11,9 @@ use crate::paths;
1111/// ignored outside workspace-write, whereas `--add-dir` is fatal at startup
1212/// when the effective sandbox is read-only (e.g. workspace-write on Windows
1313/// without the Windows sandbox). The override replaces the whole list, so it
14- /// carries the user's roots from the CLI or `config.toml`.
14+ /// carries the user's roots from the CLI or `$CODEX_HOME/config.toml`; roots
15+ /// set only in project or system config are dropped. `[permissions]` profiles
16+ /// ignore these legacy roots, so there the hcom dir is not made writable.
1517pub fn ensure_hcom_writable ( tokens : & [ String ] , codex_home : Option < & Path > ) -> Vec < String > {
1618 const ROOTS_KEY : & str = "sandbox_workspace_write.writable_roots" ;
1719 let end = tokens
@@ -40,7 +42,12 @@ pub fn ensure_hcom_writable(tokens: &[String], codex_home: Option<&Path>) -> Vec
4042 . or_else ( || token. strip_prefix ( "-c=" ) )
4143 . or_else ( || token. strip_prefix ( "-c" ) )
4244 } ;
43- if let Some ( ( key, value) ) = raw. and_then ( |raw| raw. split_once ( '=' ) )
45+ let override_kv = raw. and_then ( |raw| raw. split_once ( '=' ) ) ;
46+ // A whole-table override would be replaced by the dotted one below.
47+ if override_kv. is_some_and ( |( key, _) | key. trim ( ) == "sandbox_workspace_write" ) {
48+ return tokens. to_vec ( ) ;
49+ }
50+ if let Some ( ( key, value) ) = override_kv
4451 && key. trim ( ) == ROOTS_KEY
4552 {
4653 match parse_roots ( value) {
@@ -86,10 +93,17 @@ fn string_array(value: &toml::Value) -> Option<Vec<String>> {
8693fn config_writable_roots ( codex_home : & Path ) -> Option < Vec < String > > {
8794 let text = std:: fs:: read_to_string ( codex_home. join ( "config.toml" ) ) . ok ( ) ?;
8895 let table: toml:: Table = toml:: from_str ( & text) . ok ( ) ?;
89- string_array (
96+ let roots = string_array (
9097 table
9198 . get ( "sandbox_workspace_write" ) ?
9299 . get ( "writable_roots" ) ?,
100+ ) ?;
101+ // Codex resolves these against the config file; a CLI override would not.
102+ Some (
103+ roots
104+ . into_iter ( )
105+ . map ( |root| codex_home. join ( root) . to_string_lossy ( ) . into_owned ( ) )
106+ . collect ( ) ,
93107 )
94108}
95109
@@ -390,17 +404,43 @@ mod tests {
390404 fn test_ensure_hcom_writable_keeps_config_roots ( ) {
391405 init_config ( ) ;
392406 let codex_home = tempfile:: tempdir ( ) . unwrap ( ) ;
407+ let root = codex_home
408+ . path ( )
409+ . join ( "work" )
410+ . to_string_lossy ( )
411+ . into_owned ( ) ;
412+ let roots = toml:: Value :: Array ( vec ! [ toml:: Value :: String ( root. clone( ) ) ] ) ;
393413 std:: fs:: write (
394414 codex_home. path ( ) . join ( "config.toml" ) ,
395- "[sandbox_workspace_write]\n writable_roots = [ \" C: \\ \\ work \" ] \n " ,
415+ format ! ( "[sandbox_workspace_write]\n writable_roots = {roots} \n " ) ,
396416 )
397417 . unwrap ( ) ;
398418 let result = ensure_hcom_writable ( & [ ] , Some ( codex_home. path ( ) ) ) ;
399419 let hcom_dir = paths:: hcom_dir ( ) . to_string_lossy ( ) . to_string ( ) ;
400- assert_eq ! (
401- injected_roots( & result) ,
402- vec![ r"C:\work" . to_string( ) , hcom_dir]
403- ) ;
420+ assert_eq ! ( injected_roots( & result) , vec![ root, hcom_dir] ) ;
421+ }
422+
423+ #[ test]
424+ #[ serial]
425+ fn test_ensure_hcom_writable_resolves_relative_config_roots ( ) {
426+ init_config ( ) ;
427+ let codex_home = tempfile:: tempdir ( ) . unwrap ( ) ;
428+ std:: fs:: write (
429+ codex_home. path ( ) . join ( "config.toml" ) ,
430+ "[sandbox_workspace_write]\n writable_roots = [\" rel\" ]\n " ,
431+ )
432+ . unwrap ( ) ;
433+ let result = ensure_hcom_writable ( & [ ] , Some ( codex_home. path ( ) ) ) ;
434+ let expected = codex_home. path ( ) . join ( "rel" ) . to_string_lossy ( ) . into_owned ( ) ;
435+ assert_eq ! ( injected_roots( & result) [ 0 ] , expected) ;
436+ }
437+
438+ #[ test]
439+ #[ serial]
440+ fn test_ensure_hcom_writable_leaves_table_override ( ) {
441+ init_config ( ) ;
442+ let tokens = s ( & [ "-c" , r#"sandbox_workspace_write={writable_roots=["/x"]}"# ] ) ;
443+ assert_eq ! ( ensure_hcom_writable( & tokens, None ) , tokens) ;
404444 }
405445
406446 #[ test]
@@ -644,7 +684,9 @@ mod tests {
644684 assert ! (
645685 !result. contains( & "sandbox_workspace_write.network_access=true" . to_string( ) )
646686 ) ;
647- assert ! ( has_hcom_writable_dir( & result) ) ;
687+ // A whole-table override owns writable_roots too.
688+ let table = args. iter ( ) . any ( |arg| arg. contains ( "={" ) ) ;
689+ assert_eq ! ( has_hcom_writable_dir( & result) , !table) ;
648690 }
649691 }
650692 }
0 commit comments