diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 004b7d170..1faae917e 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -77,7 +77,6 @@ pub use crate::remote::RemoteAppServerEndpoint; pub mod legacy_core { pub use codex_core::check_execpolicy_for_warnings; pub use codex_core::format_exec_policy_error_with_source; - pub use codex_core::grant_read_root_non_elevated; pub mod config { pub use codex_core::config::*; @@ -86,10 +85,6 @@ pub mod legacy_core { pub use codex_core::config::edit::*; } } - - pub mod windows_sandbox { - pub use codex_core::windows_sandbox::*; - } } const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8cf1414d7..10d86de54 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -51,8 +51,6 @@ use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; use crate::legacy_core::config::PermissionProfileSnapshot; use crate::legacy_core::config::edit::ConfigEditsBuilder; -#[cfg(target_os = "windows")] -use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::model_catalog::ModelCatalog; use crate::model_migration::ModelMigrationOutcome; use crate::model_migration::migration_copy_for_models; @@ -1063,7 +1061,7 @@ See the Codex keymap documentation for supported actions and examples." #[cfg(target_os = "windows")] { let startup_permission_profile = app.config.permissions.effective_permission_profile(); - let should_check = WindowsSandboxLevel::from_config(&app.config) + let should_check = crate::windows_sandbox::level_from_config(&app.config) != WindowsSandboxLevel::Disabled && managed_filesystem_sandbox_is_restricted(&startup_permission_profile) && !app diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 7d1f6047e..80ed84d01 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -964,7 +964,7 @@ impl App { fn propagate_windows_sandbox_turn_context(&self) { #[cfg(target_os = "windows")] { - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); self.app_event_tx .send(AppEvent::CodexOp(AppCommand::override_turn_context( /*cwd*/ None, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 5ae1b002c..48ad95678 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -949,9 +949,7 @@ impl App { // If the elevated setup already ran on this machine, don't prompt for // elevation again - just flip the config to use the elevated path. - if crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( - codex_home.as_path(), - ) { + if crate::windows_sandbox::sandbox_setup_is_complete(codex_home.as_path()) { tx.send(AppEvent::EnableWindowsSandboxForAgentMode { preset, mode: WindowsSandboxEnableMode::Elevated, @@ -964,7 +962,7 @@ impl App { self.windows_sandbox.setup_started_at = Some(Instant::now()); let session_telemetry = self.session_telemetry.clone(); tokio::task::spawn_blocking(move || { - let result = crate::legacy_core::windows_sandbox::run_elevated_setup( + let result = crate::windows_sandbox::run_elevated_setup( &permission_profile, workspace_roots.as_slice(), command_cwd.as_path(), @@ -988,9 +986,7 @@ impl App { let mut code_tag: Option = None; let mut message_tag: Option = None; if let Some((code, message)) = - crate::legacy_core::windows_sandbox::elevated_setup_failure_details( - &err, - ) + crate::windows_sandbox::elevated_setup_failure_details(&err) { code_tag = Some(code); message_tag = Some(message); @@ -1003,7 +999,7 @@ impl App { tags.push(("message", message)); } session_telemetry.counter( - crate::legacy_core::windows_sandbox::elevated_setup_failure_metric_name( + crate::windows_sandbox::elevated_setup_failure_metric_name( &err, ), /*inc*/ 1, @@ -1074,12 +1070,12 @@ impl App { self.chat_widget.show_windows_sandbox_setup_status(); tokio::task::spawn_blocking(move || { if let Err(err) = - crate::legacy_core::windows_sandbox::run_legacy_setup_preflight( + codex_windows_sandbox::run_windows_sandbox_legacy_preflight( &permission_profile, workspace_roots.as_slice(), + codex_home.as_path(), command_cwd.as_path(), &env_map, - codex_home.as_path(), ) { session_telemetry.counter( @@ -1123,7 +1119,7 @@ impl App { tokio::task::spawn_blocking(move || { let requested_path = PathBuf::from(path); - let event = match crate::legacy_core::grant_read_root_non_elevated( + let event = match crate::windows_sandbox::grant_read_root_non_elevated( &permission_profile, workspace_roots.as_slice(), command_cwd.as_path(), @@ -1219,7 +1215,7 @@ impl App { self.config.permissions.windows_sandbox_mode, ); let windows_sandbox_level = - WindowsSandboxLevel::from_config(&self.config); + crate::windows_sandbox::level_from_config(&self.config); if let Some((sample_paths, extra_count, failed_scan)) = self.chat_widget.world_writable_warning_details() { @@ -1562,7 +1558,7 @@ impl App { return Ok(AppRunControl::Continue); } - let should_check = WindowsSandboxLevel::from_config(&self.config) + let should_check = crate::windows_sandbox::level_from_config(&self.config) != WindowsSandboxLevel::Disabled && permission_profile_is_managed_restricted && !self.chat_widget.world_writable_warning_hidden(); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 832867d75..d8326f310 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -60,8 +60,6 @@ use crate::diff_model::FileChange; use crate::git_action_directives::parse_assistant_markdown; use crate::legacy_core::config::Config; use crate::legacy_core::config::PermissionProfileSnapshot; -#[cfg(any(target_os = "windows", test))] -use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::mention_codec::LinkedMention; use crate::mention_codec::encode_history_mentions; use crate::model_catalog::ModelCatalog; @@ -265,7 +263,7 @@ use crate::app_event::AppEvent; use crate::app_event::ExitMode; use crate::app_event::PermissionProfileSelection; use crate::app_event::RateLimitRefreshOrigin; -#[cfg(any(target_os = "windows", test))] +#[cfg(target_os = "windows")] use crate::app_event::WindowsSandboxEnableMode; use crate::app_event_sender::AppEventSender; use crate::auto_review_denials; diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index efd252c15..10a785ed2 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -251,13 +251,12 @@ impl ChatWidget { .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_hint_binding); #[cfg(target_os = "windows")] - widget.bottom_pane.set_windows_degraded_sandbox_active( - crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&widget.config), - WindowsSandboxLevel::RestrictedToken - ), - ); + widget + .bottom_pane + .set_windows_degraded_sandbox_active(matches!( + crate::windows_sandbox::level_from_config(&widget.config), + WindowsSandboxLevel::RestrictedToken + )); widget.update_collaboration_mode_indicator(); widget diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs index c0d492d7a..1283a9e49 100644 --- a/codex-rs/tui/src/chatwidget/permission_popups.rs +++ b/codex-rs/tui/src/chatwidget/permission_popups.rs @@ -29,7 +29,7 @@ impl ChatWidget { let presets: Vec = builtin_approval_presets(); #[cfg(target_os = "windows")] - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); #[cfg(target_os = "windows")] let windows_degraded_sandbox_enabled = matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); @@ -37,9 +37,7 @@ impl ChatWidget { let windows_degraded_sandbox_enabled = false; let show_elevate_sandbox_hint = - crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && windows_degraded_sandbox_enabled - && presets.iter().any(|preset| preset.id == "auto"); + windows_degraded_sandbox_enabled && presets.iter().any(|preset| preset.id == "auto"); let guardian_disabled_reason = |enabled: bool| { let mut next_features = self.config.features.get().clone(); @@ -326,13 +324,13 @@ impl ChatWidget { if approvals_reviewer == ApprovalsReviewer::User && preset.id == "auto" { #[cfg(target_os = "windows")] { - if WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled { + if crate::windows_sandbox::level_from_config(&self.config) + == WindowsSandboxLevel::Disabled + { let preset = preset.clone(); - if crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( - self.config.codex_home.as_path(), - ) - { + if crate::windows_sandbox::sandbox_setup_is_complete( + self.config.codex_home.as_path(), + ) { return vec![Box::new(move |tx| { tx.send(AppEvent::EnableWindowsSandboxForAgentMode { preset: preset.clone(), diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index c05788aca..bed42e2bb 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -58,13 +58,11 @@ impl ChatWidget { pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option) { self.config.permissions.windows_sandbox_mode = mode; #[cfg(target_os = "windows")] - self.bottom_pane.set_windows_degraded_sandbox_active( - crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&self.config), - WindowsSandboxLevel::RestrictedToken - ), - ); + self.bottom_pane + .set_windows_degraded_sandbox_active(matches!( + crate::windows_sandbox::level_from_config(&self.config), + WindowsSandboxLevel::RestrictedToken + )); } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] @@ -121,13 +119,11 @@ impl ChatWidget { feature, Feature::WindowsSandbox | Feature::WindowsSandboxElevated ) { - self.bottom_pane.set_windows_degraded_sandbox_active( - crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&self.config), - WindowsSandboxLevel::RestrictedToken - ), - ); + self.bottom_pane + .set_windows_degraded_sandbox_active(matches!( + crate::windows_sandbox::level_from_config(&self.config), + WindowsSandboxLevel::RestrictedToken + )); } enabled } diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 18820ce46..5d0efea2f 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -317,12 +317,11 @@ impl ChatWidget { SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_sandbox_level = + crate::windows_sandbox::level_from_config(&self.config); let windows_degraded_sandbox_enabled = matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); - if !windows_degraded_sandbox_enabled - || !crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - { + if !windows_degraded_sandbox_enabled { // This command should not be visible/recognized outside degraded mode, // but guard anyway in case something dispatches it directly. return; @@ -968,7 +967,7 @@ impl ChatWidget { fn builtin_command_flags(&self) -> BuiltinCommandFlags { #[cfg(target_os = "windows")] let allow_elevate_sandbox = { - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken) }; #[cfg(not(target_os = "windows"))] diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index e92180d49..89ea3a9bc 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -15,7 +15,7 @@ impl ChatWidget { #[cfg(any(target_os = "windows", test))] pub(super) fn elevated_windows_sandbox_setup_required(&self) -> bool { - WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Elevated + crate::windows_sandbox::level_from_config(&self.config) == WindowsSandboxLevel::Elevated && self .config .config_layer_stack @@ -23,9 +23,7 @@ impl ChatWidget { .windows_sandbox_mode .source .is_some() - && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( - self.config.codex_home.as_path(), - ) + && !crate::windows_sandbox::sandbox_setup_is_complete(self.config.codex_home.as_path()) } #[cfg(target_os = "windows")] @@ -226,54 +224,6 @@ impl ChatWidget { ) { use ratatui_macros::line; - if !crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { - // Legacy flow (pre-NUX): explain the experimental sandbox and let the user enable it - // directly (no elevation prompts). - let mut header = ColumnRenderable::new(); - header.push(*Box::new( - Paragraph::new(vec![ - line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()], - line!["Learn more: https://developers.openai.com/codex/windows"], - ]) - .wrap(Wrap { trim: false }), - )); - - let preset_clone = preset; - let items = vec![ - SelectionItem { - name: "Enable experimental sandbox".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: preset_clone.clone(), - mode: WindowsSandboxEnableMode::Legacy, - profile_selection: profile_selection.clone(), - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: "Go back".to_string(), - description: None, - actions: vec![Box::new(|tx| { - tx.send(AppEvent::OpenApprovalsPopup); - })], - dismiss_on_select: true, - ..Default::default() - }, - ]; - - self.bottom_pane.show_selection_view(SelectionViewParams { - title: None, - footer_hint: Some(standard_popup_hint_line()), - items, - header: Box::new(header), - ..Default::default() - }); - return; - } - self.session_telemetry.counter( "codex.windows_sandbox.elevated_prompt_shown", /*inc*/ 1, @@ -508,7 +458,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled || self.elevated_windows_sandbox_setup_required(); if show_now diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index c607bb11d..13aecf217 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -11,8 +11,6 @@ use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::config::resolve_profile_v2_config_path; use crate::legacy_core::format_exec_policy_error_with_source; -#[cfg(target_os = "windows")] -use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::session_resume::ResolveCwdOutcome; use crate::session_resume::resolve_cwd_for_resume_or_fork; pub use crate::startup_error::LocalStateDbStartupError; @@ -212,6 +210,8 @@ mod version; #[cfg(not(target_os = "linux"))] mod voice; mod width; +#[cfg(any(target_os = "windows", test))] +mod windows_sandbox; mod workspace_command; #[cfg(target_os = "linux")] #[allow(dead_code)] @@ -1741,7 +1741,7 @@ async fn run_ratatui_app( set_default_client_residency_requirement(config.enforce_residency.value()); let should_show_trust_screen = should_show_trust_screen(&config); #[cfg(target_os = "windows")] - let windows_sandbox_level = WindowsSandboxLevel::from_config(&config); + let windows_sandbox_level = crate::windows_sandbox::level_from_config(&config); #[cfg(target_os = "windows")] let required_elevated_sandbox_needs_setup = windows_sandbox_level == WindowsSandboxLevel::Elevated @@ -1751,9 +1751,7 @@ async fn run_ratatui_app( .windows_sandbox_mode .source .is_some() - && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( - config.codex_home.as_path(), - ); + && !crate::windows_sandbox::sandbox_setup_is_complete(config.codex_home.as_path()); #[cfg(target_os = "windows")] let should_prompt_windows_sandbox_nux_at_startup = (trust_decision_was_made && windows_sandbox_level == WindowsSandboxLevel::Disabled) diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index e4c4b346d..405b37743 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -36,8 +36,6 @@ use crate::config_update::format_config_error; use crate::config_update::write_trusted_project; use crate::key_hint::KeyBindingListExt; use crate::legacy_core::config::Config; -#[cfg(target_os = "windows")] -use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInOption; use crate::onboarding::auth::SignInState; @@ -143,7 +141,7 @@ impl OnboardingScreen { } #[cfg(target_os = "windows")] let show_windows_create_sandbox_hint = - WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled; + crate::windows_sandbox::level_from_config(&config) == WindowsSandboxLevel::Disabled; #[cfg(not(target_os = "windows"))] let show_windows_create_sandbox_hint = false; let highlighted = TrustDirectorySelection::Trust; diff --git a/codex-rs/tui/src/windows_sandbox.rs b/codex-rs/tui/src/windows_sandbox.rs new file mode 100644 index 000000000..ec586e7b1 --- /dev/null +++ b/codex-rs/tui/src/windows_sandbox.rs @@ -0,0 +1,122 @@ +//! TUI-owned Windows sandbox helpers retained while setup still runs in the local client process. +//! +//! TODO: These helpers inspect and modify the TUI host, so they do not support +//! cross-platform remote app servers. Move readiness and setup to the existing +//! `windowsSandbox/*` RPCs while preserving the pending permission profile, +//! use the server platform reported during initialization, and add a remote +//! equivalent for read-root grants. + +use crate::legacy_core::config::Config; +use codex_config::types::WindowsSandboxModeToml; +use codex_features::Feature; +use codex_protocol::config_types::WindowsSandboxLevel; +#[cfg(target_os = "windows")] +use codex_protocol::models::PermissionProfile; +#[cfg(target_os = "windows")] +use codex_utils_absolute_path::AbsolutePathBuf; +#[cfg(target_os = "windows")] +use std::collections::HashMap; +use std::path::Path; +#[cfg(target_os = "windows")] +use std::path::PathBuf; + +pub(crate) fn level_from_config(config: &Config) -> WindowsSandboxLevel { + match config.permissions.windows_sandbox_mode { + Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated, + Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken, + None if config.features.enabled(Feature::WindowsSandboxElevated) => { + WindowsSandboxLevel::Elevated + } + None if config.features.enabled(Feature::WindowsSandbox) => { + WindowsSandboxLevel::RestrictedToken + } + None => WindowsSandboxLevel::Disabled, + } +} + +#[cfg(target_os = "windows")] +pub(crate) use codex_windows_sandbox::sandbox_setup_is_complete; + +#[cfg(not(target_os = "windows"))] +pub(crate) fn sandbox_setup_is_complete(_codex_home: &Path) -> bool { + false +} + +#[cfg(target_os = "windows")] +pub(crate) fn run_elevated_setup( + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, +) -> anyhow::Result<()> { + let permissions = codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + )?; + codex_windows_sandbox::run_elevated_setup( + codex_windows_sandbox::SandboxSetupRequest { + permissions: &permissions, + command_cwd, + env_map, + codex_home, + proxy_enforced: false, + }, + codex_windows_sandbox::SetupRootOverrides::default(), + ) +} + +#[cfg(target_os = "windows")] +pub(crate) fn elevated_setup_failure_details(err: &anyhow::Error) -> Option<(String, String)> { + let failure = codex_windows_sandbox::extract_setup_failure(err)?; + Some(( + failure.code.as_str().to_string(), + codex_windows_sandbox::sanitize_setup_metric_tag_value(&failure.message), + )) +} + +#[cfg(target_os = "windows")] +pub(crate) fn elevated_setup_failure_metric_name(err: &anyhow::Error) -> &'static str { + if codex_windows_sandbox::extract_setup_failure(err).is_some_and(|failure| { + matches!( + failure.code, + codex_windows_sandbox::SetupErrorCode::OrchestratorHelperLaunchCanceled + ) + }) { + "codex.windows_sandbox.elevated_setup_canceled" + } else { + "codex.windows_sandbox.elevated_setup_failure" + } +} + +#[cfg(target_os = "windows")] +pub(crate) fn grant_read_root_non_elevated( + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], + command_cwd: &Path, + env_map: &HashMap, + codex_home: &Path, + read_root: &Path, +) -> anyhow::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)?; + codex_windows_sandbox::run_setup_refresh_with_extra_read_roots( + permission_profile, + workspace_roots, + command_cwd, + env_map, + codex_home, + vec![canonical_root.clone()], + /*proxy_enforced*/ false, + )?; + Ok(canonical_root) +}