diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 698d058ef0..1e04f6588b 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -3,7 +3,6 @@ mod pid_tracker; #[cfg(target_os = "macos")] mod seatbelt; -use std::path::Path; use std::path::PathBuf; use std::process::Stdio; @@ -16,9 +15,7 @@ use codex_core::exec_env::create_env; use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_protocol::config_types::SandboxMode; -use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::permissions::forbidden_agent_preserved_path_write; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; #[cfg(target_os = "macos")] use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams; @@ -145,7 +142,7 @@ async fn run_command_under_sandbox( // sandbox policy. In the future, we could add a CLI option to set them // separately. let sandbox_policy_cwd = cwd.clone(); - if let Some(reason) = preserved_path_write_forbidden_reason( + if let Some(reason) = codex_shell_command::preserved_path_write_forbidden_reason( &command, cwd.as_path(), &config.permissions.file_system_sandbox_policy, @@ -289,126 +286,6 @@ async fn run_command_under_sandbox( handle_exit_status(status); } -fn preserved_path_write_forbidden_reason( - command: &[String], - cwd: &Path, - file_system_sandbox_policy: &FileSystemSandboxPolicy, -) -> Option { - let commands = codex_shell_command::bash::parse_shell_lc_plain_commands(command) - .or_else(|| codex_shell_command::bash::parse_shell_lc_command_word_prefixes(command)) - .unwrap_or_else(|| vec![command.to_vec()]); - - for simple_command in commands { - if let Some(name) = - simple_command_preserved_path_write(&simple_command, cwd, file_system_sandbox_policy) - { - return Some(preserved_path_write_reason(name)); - } - } - - if let Some(targets) = - codex_shell_command::bash::parse_shell_lc_write_redirection_targets(command) - { - for target in targets { - if let Some(name) = forbidden_agent_preserved_path_write( - Path::new(&target), - cwd, - file_system_sandbox_policy, - ) { - return Some(preserved_path_write_reason(name)); - } - } - } - None -} - -fn preserved_path_write_reason(name: &str) -> String { - format!("command targets preserved workspace metadata path `{name}`") -} - -fn simple_command_preserved_path_write( - command: &[String], - cwd: &Path, - file_system_sandbox_policy: &FileSystemSandboxPolicy, -) -> Option<&'static str> { - let program = command.first().map(|program| { - Path::new(program) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(program) - })?; - - match program { - "git" => git_init_preserved_path_write(command, cwd, file_system_sandbox_policy), - "touch" | "mkdir" | "rm" | "rmdir" | "ln" | "mv" | "cp" | "install" => command - .iter() - .skip(1) - .filter(|arg| !arg.starts_with('-')) - .find_map(|arg| { - forbidden_agent_preserved_path_write( - Path::new(arg), - cwd, - file_system_sandbox_policy, - ) - }), - _ => None, - } -} - -fn git_init_preserved_path_write( - command: &[String], - cwd: &Path, - file_system_sandbox_policy: &FileSystemSandboxPolicy, -) -> Option<&'static str> { - let mut git_cwd = PathBuf::from(cwd); - let mut index = 1; - - while index < command.len() { - match command[index].as_str() { - "-C" => { - let next = command.get(index + 1)?; - git_cwd = resolve_shell_operand(Path::new(next), &git_cwd); - index += 2; - } - "--" => { - index += 1; - break; - } - arg if arg.starts_with('-') => { - index += 1; - } - _ => break, - } - } - - if command.get(index).map(String::as_str) != Some("init") { - return None; - } - - let init_target = command - .iter() - .skip(index + 1) - .find(|arg| !arg.starts_with('-')) - .map_or_else( - || git_cwd.clone(), - |arg| resolve_shell_operand(Path::new(arg), &git_cwd), - ); - - forbidden_agent_preserved_path_write( - init_target.join(".git").as_path(), - &git_cwd, - file_system_sandbox_policy, - ) -} - -fn resolve_shell_operand(path: &Path, cwd: &Path) -> PathBuf { - if path.is_absolute() { - path.to_path_buf() - } else { - cwd.join(path) - } -} - #[cfg(target_os = "windows")] async fn run_command_under_windows_session( config: &Config, @@ -911,111 +788,4 @@ mod tests { Ok(()) } - - fn legacy_workspace_write_policy(cwd: &std::path::Path) -> FileSystemSandboxPolicy { - let policy = codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![], - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd) - } - - #[test] - fn debug_sandbox_preserved_path_guard_blocks_git_init_under_parent_repo() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "git init".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); - } - - #[test] - fn debug_sandbox_preserved_path_guard_allows_normal_git_under_parent_repo() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "git status --short".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!(reason, None); - } - - #[test] - fn debug_sandbox_preserved_path_guard_blocks_preserved_path_redirections() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "printf pwned > .git".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); - } - - #[test] - fn debug_sandbox_preserved_path_guard_blocks_git_init_inside_complex_script() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "set -e\nif git init -q; then\n exit 22\nfi".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); - } } diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 0cdb697886..becce37435 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -1,11 +1,7 @@ use codex_protocol::ThreadId; use codex_protocol::models::ShellCommandToolCallParams; use codex_protocol::models::ShellToolCallParams; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::forbidden_agent_preserved_path_write; use serde_json::Value as JsonValue; -use std::path::Path; -use std::path::PathBuf; use std::sync::Arc; use crate::exec::ExecCapturePolicy; @@ -93,126 +89,6 @@ struct RunExecLikeArgs { shell_runtime_backend: ShellRuntimeBackend, } -fn preserved_path_write_forbidden_reason( - command: &[String], - cwd: &Path, - file_system_sandbox_policy: &FileSystemSandboxPolicy, -) -> Option { - let commands = codex_shell_command::bash::parse_shell_lc_plain_commands(command) - .or_else(|| codex_shell_command::bash::parse_shell_lc_command_word_prefixes(command)) - .unwrap_or_else(|| vec![command.to_vec()]); - - for simple_command in commands { - if let Some(name) = - simple_command_preserved_path_write(&simple_command, cwd, file_system_sandbox_policy) - { - return Some(preserved_path_write_reason(name)); - } - } - - if let Some(targets) = - codex_shell_command::bash::parse_shell_lc_write_redirection_targets(command) - { - for target in targets { - if let Some(name) = forbidden_agent_preserved_path_write( - Path::new(&target), - cwd, - file_system_sandbox_policy, - ) { - return Some(preserved_path_write_reason(name)); - } - } - } - None -} - -fn preserved_path_write_reason(name: &str) -> String { - format!("command targets preserved workspace metadata path `{name}`") -} - -fn simple_command_preserved_path_write( - command: &[String], - cwd: &Path, - file_system_sandbox_policy: &FileSystemSandboxPolicy, -) -> Option<&'static str> { - let program = command.first().map(|program| { - Path::new(program) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(program) - })?; - - match program { - "git" => git_init_preserved_path_write(command, cwd, file_system_sandbox_policy), - "touch" | "mkdir" | "rm" | "rmdir" | "ln" | "mv" | "cp" | "install" => command - .iter() - .skip(1) - .filter(|arg| !arg.starts_with('-')) - .find_map(|arg| { - forbidden_agent_preserved_path_write( - Path::new(arg), - cwd, - file_system_sandbox_policy, - ) - }), - _ => None, - } -} - -fn git_init_preserved_path_write( - command: &[String], - cwd: &Path, - file_system_sandbox_policy: &FileSystemSandboxPolicy, -) -> Option<&'static str> { - let mut git_cwd = PathBuf::from(cwd); - let mut index = 1; - - while index < command.len() { - match command[index].as_str() { - "-C" => { - let next = command.get(index + 1)?; - git_cwd = resolve_shell_operand(Path::new(next), &git_cwd); - index += 2; - } - "--" => { - index += 1; - break; - } - arg if arg.starts_with('-') => { - index += 1; - } - _ => break, - } - } - - if command.get(index).map(String::as_str) != Some("init") { - return None; - } - - let init_target = command - .iter() - .skip(index + 1) - .find(|arg| !arg.starts_with('-')) - .map_or_else( - || git_cwd.clone(), - |arg| resolve_shell_operand(Path::new(arg), &git_cwd), - ); - - forbidden_agent_preserved_path_write( - init_target.join(".git").as_path(), - &git_cwd, - file_system_sandbox_policy, - ) -} - -fn resolve_shell_operand(path: &Path, cwd: &Path) -> PathBuf { - if path.is_absolute() { - path.to_path_buf() - } else { - cwd.join(path) - } -} - impl ShellHandler { fn to_exec_params( params: &ShellToolCallParams, @@ -655,7 +531,7 @@ impl ShellHandler { prefix_rule, }) .await; - let exec_approval_requirement = preserved_path_write_forbidden_reason( + let exec_approval_requirement = codex_shell_command::preserved_path_write_forbidden_reason( &exec_params.command, &exec_params.cwd, &turn.file_system_sandbox_policy, diff --git a/codex-rs/core/src/tools/handlers/shell_tests.rs b/codex-rs/core/src/tools/handlers/shell_tests.rs index 1a46b1dfa9..49e2cf8f75 100644 --- a/codex-rs/core/src/tools/handlers/shell_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_tests.rs @@ -2,9 +2,6 @@ use std::path::PathBuf; use std::sync::Arc; use codex_protocol::models::ShellCommandToolCallParams; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::protocol::ReadOnlyAccess; -use codex_protocol::protocol::SandboxPolicy; use core_test_support::PathBufExt; use core_test_support::test_path_buf; use pretty_assertions::assert_eq; @@ -28,7 +25,6 @@ use codex_shell_command::is_safe_command::is_known_safe_command; use codex_shell_command::powershell::try_find_powershell_executable_blocking; use codex_shell_command::powershell::try_find_pwsh_executable_blocking; use serde_json::json; -use tempfile::TempDir; use tokio::sync::Mutex; use tokio::sync::watch; @@ -79,134 +75,6 @@ fn assert_safe(shell: &Shell, command: &str) { ))); } -fn legacy_workspace_write_policy(cwd: &std::path::Path) -> FileSystemSandboxPolicy { - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![], - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd) -} - -#[test] -fn preserved_path_detector_blocks_git_init_under_parent_repo() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = super::preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "git init".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); -} - -#[test] -fn preserved_path_detector_allows_normal_git_under_parent_repo() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = super::preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "git status --short".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!(reason, None); -} - -#[test] -fn preserved_path_detector_blocks_direct_preserved_path_writes() { - let cwd = TempDir::new().expect("tempdir"); - let policy = legacy_workspace_write_policy(cwd.path()); - - let reason = super::preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "touch .git && mkdir -p .codex".to_string(), - ], - cwd.path(), - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); -} - -#[test] -fn preserved_path_detector_blocks_preserved_path_redirections() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = super::preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "printf pwned > .git".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); -} - -#[test] -fn preserved_path_detector_blocks_git_init_inside_complex_script() { - let repo = TempDir::new().expect("tempdir"); - std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); - let cwd = repo.path().join("sub"); - std::fs::create_dir(&cwd).expect("create cwd"); - let policy = legacy_workspace_write_policy(&cwd); - - let reason = super::preserved_path_write_forbidden_reason( - &[ - "/bin/bash".to_string(), - "-lc".to_string(), - "set -e\nif git init -q; then\n exit 22\nfi".to_string(), - ], - &cwd, - &policy, - ); - - assert_eq!( - reason, - Some("command targets preserved workspace metadata path `.git`".to_string()) - ); -} - #[tokio::test] async fn shell_command_handler_to_exec_params_uses_session_shell_and_turn_context() { let (session, turn_context) = make_session_and_context().await; diff --git a/codex-rs/shell-command/src/lib.rs b/codex-rs/shell-command/src/lib.rs index 1d9e302a4e..4483bbab39 100644 --- a/codex-rs/shell-command/src/lib.rs +++ b/codex-rs/shell-command/src/lib.rs @@ -6,6 +6,8 @@ pub mod bash; pub(crate) mod command_safety; pub mod parse_command; pub mod powershell; +mod preserved_path_write; pub use command_safety::is_dangerous_command; pub use command_safety::is_safe_command; +pub use preserved_path_write::preserved_path_write_forbidden_reason; diff --git a/codex-rs/shell-command/src/preserved_path_write.rs b/codex-rs/shell-command/src/preserved_path_write.rs new file mode 100644 index 0000000000..ab966000dc --- /dev/null +++ b/codex-rs/shell-command/src/preserved_path_write.rs @@ -0,0 +1,290 @@ +use std::path::Path; +use std::path::PathBuf; + +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::forbidden_agent_preserved_path_write; + +pub fn preserved_path_write_forbidden_reason( + command: &[String], + cwd: &Path, + file_system_sandbox_policy: &FileSystemSandboxPolicy, +) -> Option { + let commands = crate::bash::parse_shell_lc_plain_commands(command) + .or_else(|| crate::bash::parse_shell_lc_command_word_prefixes(command)) + .unwrap_or_else(|| vec![command.to_vec()]); + + for simple_command in commands { + if let Some(name) = + simple_command_preserved_path_write(&simple_command, cwd, file_system_sandbox_policy) + { + return Some(preserved_path_write_reason(name)); + } + } + + if let Some(targets) = crate::bash::parse_shell_lc_write_redirection_targets(command) { + for target in targets { + if let Some(name) = forbidden_agent_preserved_path_write( + Path::new(&target), + cwd, + file_system_sandbox_policy, + ) { + return Some(preserved_path_write_reason(name)); + } + } + } + None +} + +fn preserved_path_write_reason(name: &str) -> String { + format!("command targets preserved workspace metadata path `{name}`") +} + +fn simple_command_preserved_path_write( + command: &[String], + cwd: &Path, + file_system_sandbox_policy: &FileSystemSandboxPolicy, +) -> Option<&'static str> { + let program = command.first().map(|program| { + Path::new(program) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(program) + })?; + + match program { + "git" => git_init_preserved_path_write(command, cwd, file_system_sandbox_policy), + "touch" | "mkdir" | "rm" | "rmdir" | "ln" | "mv" | "cp" | "install" => command + .iter() + .skip(1) + .filter(|arg| !arg.starts_with('-')) + .find_map(|arg| { + forbidden_agent_preserved_path_write( + Path::new(arg), + cwd, + file_system_sandbox_policy, + ) + }), + _ => None, + } +} + +fn git_init_preserved_path_write( + command: &[String], + cwd: &Path, + file_system_sandbox_policy: &FileSystemSandboxPolicy, +) -> Option<&'static str> { + let mut git_cwd = PathBuf::from(cwd); + let mut index = 1; + + while index < command.len() { + match command[index].as_str() { + "-C" => { + let next = command.get(index + 1)?; + git_cwd = resolve_shell_operand(Path::new(next), &git_cwd); + index += 2; + } + "--" => { + index += 1; + break; + } + arg if arg.starts_with('-') => { + index += 1; + } + _ => break, + } + } + + if command.get(index).map(String::as_str) != Some("init") { + return None; + } + + let init_target = command + .iter() + .skip(index + 1) + .find(|arg| !arg.starts_with('-')) + .map_or_else( + || git_cwd.clone(), + |arg| resolve_shell_operand(Path::new(arg), &git_cwd), + ); + + forbidden_agent_preserved_path_write( + init_target.join(".git").as_path(), + &git_cwd, + file_system_sandbox_policy, + ) +} + +fn resolve_shell_operand(path: &Path, cwd: &Path) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else { + cwd.join(path) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use std::path::PathBuf; + + use codex_protocol::permissions::FileSystemSandboxPolicy; + use codex_protocol::protocol::ReadOnlyAccess; + use codex_protocol::protocol::SandboxPolicy; + use pretty_assertions::assert_eq; + + use super::preserved_path_write_forbidden_reason; + + struct TestDir { + path: PathBuf, + } + + impl TestDir { + fn new(name: &str) -> Self { + let path = std::env::temp_dir().join(format!( + "codex-preserved-path-write-{name}-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&path); + std::fs::create_dir(&path).expect("create tempdir"); + Self { path } + } + + fn path(&self) -> &Path { + &self.path + } + } + + impl Drop for TestDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.path); + } + } + + fn legacy_workspace_write_policy(cwd: &Path) -> FileSystemSandboxPolicy { + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + read_only_access: ReadOnlyAccess::Restricted { + include_platform_defaults: false, + readable_roots: vec![], + }, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd) + } + + #[test] + fn preserved_path_detector_blocks_git_init_under_parent_repo() { + let repo = TestDir::new("git-init-under-parent-repo"); + std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); + let cwd = repo.path().join("sub"); + std::fs::create_dir(&cwd).expect("create cwd"); + let policy = legacy_workspace_write_policy(&cwd); + + let reason = preserved_path_write_forbidden_reason( + &[ + "/bin/bash".to_string(), + "-lc".to_string(), + "git init".to_string(), + ], + &cwd, + &policy, + ); + + assert_eq!( + reason, + Some("command targets preserved workspace metadata path `.git`".to_string()) + ); + } + + #[test] + fn preserved_path_detector_allows_normal_git_under_parent_repo() { + let repo = TestDir::new("normal-git-under-parent-repo"); + std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); + let cwd = repo.path().join("sub"); + std::fs::create_dir(&cwd).expect("create cwd"); + let policy = legacy_workspace_write_policy(&cwd); + + let reason = preserved_path_write_forbidden_reason( + &[ + "/bin/bash".to_string(), + "-lc".to_string(), + "git status --short".to_string(), + ], + &cwd, + &policy, + ); + + assert_eq!(reason, None); + } + + #[test] + fn preserved_path_detector_blocks_direct_preserved_path_writes() { + let cwd = TestDir::new("direct-preserved-path-writes"); + let policy = legacy_workspace_write_policy(cwd.path()); + + let reason = preserved_path_write_forbidden_reason( + &[ + "/bin/bash".to_string(), + "-lc".to_string(), + "touch .git && mkdir -p .codex".to_string(), + ], + cwd.path(), + &policy, + ); + + assert_eq!( + reason, + Some("command targets preserved workspace metadata path `.git`".to_string()) + ); + } + + #[test] + fn preserved_path_detector_blocks_preserved_path_redirections() { + let repo = TestDir::new("preserved-path-redirections"); + std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); + let cwd = repo.path().join("sub"); + std::fs::create_dir(&cwd).expect("create cwd"); + let policy = legacy_workspace_write_policy(&cwd); + + let reason = preserved_path_write_forbidden_reason( + &[ + "/bin/bash".to_string(), + "-lc".to_string(), + "printf pwned > .git".to_string(), + ], + &cwd, + &policy, + ); + + assert_eq!( + reason, + Some("command targets preserved workspace metadata path `.git`".to_string()) + ); + } + + #[test] + fn preserved_path_detector_blocks_git_init_inside_complex_script() { + let repo = TestDir::new("git-init-inside-complex-script"); + std::fs::create_dir(repo.path().join(".git")).expect("create parent .git"); + let cwd = repo.path().join("sub"); + std::fs::create_dir(&cwd).expect("create cwd"); + let policy = legacy_workspace_write_policy(&cwd); + + let reason = preserved_path_write_forbidden_reason( + &[ + "/bin/bash".to_string(), + "-lc".to_string(), + "set -e\nif git init -q; then\n exit 22\nfi".to_string(), + ], + &cwd, + &policy, + ); + + assert_eq!( + reason, + Some("command targets preserved workspace metadata path `.git`".to_string()) + ); + } +} diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 9e9f5bfd1b..6bf6be4308 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1706,7 +1706,7 @@ mod tests { &workspace_write, )), ); - assert_eq!(sandbox, Some(workspace_write.clone().into())); + assert_eq!(sandbox, Some(workspace_write.into())); assert_eq!(profile, None); let external_sandbox = SandboxPolicy::ExternalSandbox {