From 5c3ca73914cfef7a9317d76cf1cb00ded3cc338a Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Thu, 12 Feb 2026 12:48:36 -0800 Subject: [PATCH] add a slash command to grant sandbox read access to inaccessible directories (#11512) There is an edge case where a directory is not readable by the sandbox. In practice, we've seen very little of it, but it can happen so this slash command unlocks users when it does. Future idea is to make this a tool that the agent knows about so it can be more integrated. --- codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/windows_sandbox.rs | 32 ++++++ .../core/src/windows_sandbox_read_grants.rs | 97 +++++++++++++++++++ codex-rs/tui/src/app.rs | 57 +++++++++++ codex-rs/tui/src/app_event.rs | 13 +++ codex-rs/tui/src/chatwidget.rs | 17 ++++ codex-rs/tui/src/slash_command.rs | 14 ++- codex-rs/windows-sandbox-rs/src/lib.rs | 2 + .../src/setup_orchestrator.rs | 45 ++++++++- 9 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 codex-rs/core/src/windows_sandbox_read_grants.rs diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 6f075e7dc..764fc4fbb 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -79,6 +79,7 @@ pub mod review_format; pub mod review_prompts; mod thread_manager; pub mod web_search; +pub mod windows_sandbox_read_grants; pub use codex_protocol::protocol::InitialHistory; pub use thread_manager::NewThread; pub use thread_manager::ThreadManager; diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 180cce6c2..02f740ebd 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -10,6 +10,7 @@ use codex_protocol::config_types::WindowsSandboxLevel; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; +use std::path::PathBuf; /// Kill switch for the elevated sandbox NUX on Windows. /// @@ -200,6 +201,25 @@ pub fn run_legacy_setup_preflight( ) } +#[cfg(target_os = "windows")] +pub fn run_setup_refresh_with_extra_read_roots( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + extra_read_roots: Vec, +) -> anyhow::Result<()> { + codex_windows_sandbox::run_setup_refresh_with_extra_read_roots( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + extra_read_roots, + ) +} + #[cfg(not(target_os = "windows"))] pub fn run_legacy_setup_preflight( _policy: &SandboxPolicy, @@ -211,6 +231,18 @@ pub fn run_legacy_setup_preflight( anyhow::bail!("legacy Windows sandbox setup is only supported on Windows") } +#[cfg(not(target_os = "windows"))] +pub fn run_setup_refresh_with_extra_read_roots( + _policy: &SandboxPolicy, + _policy_cwd: &Path, + _command_cwd: &Path, + _env_map: &HashMap, + _codex_home: &Path, + _extra_read_roots: Vec, +) -> anyhow::Result<()> { + anyhow::bail!("Windows sandbox read-root refresh is only supported on Windows") +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/core/src/windows_sandbox_read_grants.rs b/codex-rs/core/src/windows_sandbox_read_grants.rs new file mode 100644 index 000000000..8fa843c8b --- /dev/null +++ b/codex-rs/core/src/windows_sandbox_read_grants.rs @@ -0,0 +1,97 @@ +use crate::protocol::SandboxPolicy; +use crate::windows_sandbox::run_setup_refresh_with_extra_read_roots; +use anyhow::Result; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +pub fn grant_read_root_non_elevated( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + read_root: &Path, +) -> Result { + if !read_root.is_absolute() { + anyhow::bail!("path must be absolute: {}", read_root.display()); + } + if !read_root.exists() { + anyhow::bail!("path does not exist: {}", read_root.display()); + } + if !read_root.is_dir() { + anyhow::bail!("path must be a directory: {}", read_root.display()); + } + + let canonical_root = dunce::canonicalize(read_root)?; + run_setup_refresh_with_extra_read_roots( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + vec![canonical_root.clone()], + )?; + Ok(canonical_root) +} + +#[cfg(test)] +mod tests { + use super::grant_read_root_non_elevated; + use crate::protocol::SandboxPolicy; + use std::collections::HashMap; + use std::path::Path; + use tempfile::TempDir; + + fn policy() -> SandboxPolicy { + SandboxPolicy::new_workspace_write_policy() + } + + #[test] + fn rejects_relative_path() { + let tmp = TempDir::new().expect("tempdir"); + let err = grant_read_root_non_elevated( + &policy(), + tmp.path(), + tmp.path(), + &HashMap::new(), + tmp.path(), + Path::new("relative"), + ) + .expect_err("relative path should fail"); + assert!(err.to_string().contains("path must be absolute")); + } + + #[test] + fn rejects_missing_path() { + let tmp = TempDir::new().expect("tempdir"); + let missing = tmp.path().join("does-not-exist"); + let err = grant_read_root_non_elevated( + &policy(), + tmp.path(), + tmp.path(), + &HashMap::new(), + tmp.path(), + missing.as_path(), + ) + .expect_err("missing path should fail"); + assert!(err.to_string().contains("path does not exist")); + } + + #[test] + fn rejects_file_path() { + let tmp = TempDir::new().expect("tempdir"); + let file_path = tmp.path().join("file.txt"); + std::fs::write(&file_path, "hello").expect("write file"); + let err = grant_read_root_non_elevated( + &policy(), + tmp.path(), + tmp.path(), + &HashMap::new(), + tmp.path(), + file_path.as_path(), + ) + .expect_err("file path should fail"); + assert!(err.to_string().contains("path must be a directory")); + } +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c700a3a84..67d5e30a8 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1826,6 +1826,63 @@ impl App { let _ = preset; } } + AppEvent::BeginWindowsSandboxGrantReadRoot { path } => { + #[cfg(target_os = "windows")] + { + self.chat_widget + .add_to_history(history_cell::new_info_event( + format!("Granting sandbox read access to {path} ..."), + None, + )); + + let policy = self.config.sandbox_policy.get().clone(); + let policy_cwd = self.config.cwd.clone(); + let command_cwd = self.config.cwd.clone(); + let env_map: std::collections::HashMap = + std::env::vars().collect(); + let codex_home = self.config.codex_home.clone(); + let tx = self.app_event_tx.clone(); + + tokio::task::spawn_blocking(move || { + let requested_path = PathBuf::from(path); + let event = match codex_core::windows_sandbox_read_grants::grant_read_root_non_elevated( + &policy, + policy_cwd.as_path(), + command_cwd.as_path(), + &env_map, + codex_home.as_path(), + requested_path.as_path(), + ) { + Ok(canonical_path) => AppEvent::WindowsSandboxGrantReadRootCompleted { + path: canonical_path, + error: None, + }, + Err(err) => AppEvent::WindowsSandboxGrantReadRootCompleted { + path: requested_path, + error: Some(err.to_string()), + }, + }; + tx.send(event); + }); + } + #[cfg(not(target_os = "windows"))] + { + let _ = path; + } + } + AppEvent::WindowsSandboxGrantReadRootCompleted { path, error } => match error { + Some(err) => { + self.chat_widget + .add_to_history(history_cell::new_error_event(format!("Error: {err}"))); + } + None => { + self.chat_widget + .add_to_history(history_cell::new_info_event( + format!("Sandbox read access granted for {}", path.display()), + None, + )); + } + }, AppEvent::EnableWindowsSandboxForAgentMode { preset, mode } => { #[cfg(target_os = "windows")] { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index d2168e961..67db6d7c8 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -218,6 +218,19 @@ pub(crate) enum AppEvent { preset: ApprovalPreset, }, + /// Begin a non-elevated grant of read access for an additional directory. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + BeginWindowsSandboxGrantReadRoot { + path: String, + }, + + /// Result of attempting to grant read access for an additional directory. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + WindowsSandboxGrantReadRootCompleted { + path: PathBuf, + error: Option, + }, + /// Enable the Windows sandbox feature and switch to Agent mode. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] EnableWindowsSandboxForAgentMode { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0ae056f83..46dcc1999 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3342,6 +3342,11 @@ impl ChatWidget { // Not supported; on non-Windows this command should never be reachable. }; } + SlashCommand::SandboxReadRoot => { + self.add_error_message( + "Usage: /sandbox-add-read-dir ".to_string(), + ); + } SlashCommand::Experimental => { self.open_experimental_popup(); } @@ -3544,6 +3549,18 @@ impl ChatWidget { }); self.bottom_pane.drain_pending_submission_state(); } + SlashCommand::SandboxReadRoot if !trimmed.is_empty() => { + let Some((prepared_args, _prepared_elements)) = + self.bottom_pane.prepare_inline_args_submission(false) + else { + return; + }; + self.app_event_tx + .send(AppEvent::BeginWindowsSandboxGrantReadRoot { + path: prepared_args, + }); + self.bottom_pane.drain_pending_submission_state(); + } _ => self.dispatch_command(cmd), } } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index a1699c37b..16e767f80 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -17,6 +17,8 @@ pub enum SlashCommand { Permissions, #[strum(serialize = "setup-default-sandbox")] ElevateSandbox, + #[strum(serialize = "sandbox-add-read-dir")] + SandboxReadRoot, Experimental, Skills, Review, @@ -84,7 +86,10 @@ impl SlashCommand { SlashCommand::Agent => "switch the active agent thread", SlashCommand::Approvals => "choose what Codex is allowed to do", SlashCommand::Permissions => "choose what Codex is allowed to do", - SlashCommand::ElevateSandbox => "set up default agent sandbox", + SlashCommand::ElevateSandbox => "set up elevated agent sandbox", + SlashCommand::SandboxReadRoot => { + "let sandbox read a directory: /sandbox-add-read-dir " + } SlashCommand::Experimental => "toggle experimental features", SlashCommand::Mcp => "list configured MCP tools", SlashCommand::Apps => "manage apps", @@ -104,7 +109,10 @@ impl SlashCommand { pub fn supports_inline_args(self) -> bool { matches!( self, - SlashCommand::Review | SlashCommand::Rename | SlashCommand::Plan + SlashCommand::Review + | SlashCommand::Rename + | SlashCommand::Plan + | SlashCommand::SandboxReadRoot ) } @@ -122,6 +130,7 @@ impl SlashCommand { | SlashCommand::Approvals | SlashCommand::Permissions | SlashCommand::ElevateSandbox + | SlashCommand::SandboxReadRoot | SlashCommand::Experimental | SlashCommand::Review | SlashCommand::Plan @@ -151,6 +160,7 @@ impl SlashCommand { fn is_visible(self) -> bool { match self { + SlashCommand::SandboxReadRoot => cfg!(target_os = "windows"), SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions), _ => true, } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 3e15c7a27..61077342d 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -83,6 +83,8 @@ pub use setup::run_elevated_setup; #[cfg(target_os = "windows")] pub use setup::run_setup_refresh; #[cfg(target_os = "windows")] +pub use setup::run_setup_refresh_with_extra_read_roots; +#[cfg(target_os = "windows")] pub use setup::sandbox_dir; #[cfg(target_os = "windows")] pub use setup::sandbox_secrets_dir; diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 0e43d1ba4..e7f9bee69 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -61,6 +61,47 @@ pub fn run_setup_refresh( command_cwd: &Path, env_map: &HashMap, codex_home: &Path, +) -> Result<()> { + run_setup_refresh_inner( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + None, + None, + ) +} + +pub fn run_setup_refresh_with_extra_read_roots( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + extra_read_roots: Vec, +) -> Result<()> { + let mut read_roots = gather_read_roots(command_cwd, policy); + read_roots.extend(extra_read_roots); + run_setup_refresh_inner( + policy, + policy_cwd, + command_cwd, + env_map, + codex_home, + Some(read_roots), + Some(Vec::new()), + ) +} + +fn run_setup_refresh_inner( + policy: &SandboxPolicy, + policy_cwd: &Path, + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + read_roots_override: Option>, + write_roots_override: Option>, ) -> Result<()> { // Skip in danger-full-access. if matches!( @@ -75,8 +116,8 @@ pub fn run_setup_refresh( command_cwd, env_map, codex_home, - None, - None, + read_roots_override, + write_roots_override, ); let payload = ElevationPayload { version: SETUP_VERSION,