From 9e9a4059f00773e09a00da32a253a30f11d89ff9 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Thu, 7 May 2026 17:11:05 -0700
Subject: [PATCH 01/15] fix: refresh profile network proxy on permission
 switches

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/config/mod.rs               |  91 +++++++++----
 codex-rs/core/src/guardian/review.rs          |   3 +-
 codex-rs/core/src/session/mod.rs              |  39 +++++-
 codex-rs/core/src/session/session.rs          |  33 ++++-
 codex-rs/core/src/session/tests.rs            | 124 +++++++++++++++++-
 codex-rs/core/src/session/turn_context.rs     |   1 +
 codex-rs/core/src/state/service.rs            |   6 +-
 codex-rs/core/src/tasks/mod.rs                |   3 +-
 codex-rs/tui/src/app/config_persistence.rs    |   2 +
 codex-rs/tui/src/app/event_dispatch.rs        |   2 +
 codex-rs/tui/src/app/tests.rs                 |   6 +
 codex-rs/tui/src/app_command.rs               |   4 +
 codex-rs/tui/src/chatwidget/service_tiers.rs  |   1 +
 .../tui/src/chatwidget/tests/permissions.rs   |   1 +
 14 files changed, 278 insertions(+), 38 deletions(-)

diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs
index 434969e2c5f..6960bcdd5a4 100644
--- a/codex-rs/core/src/config/mod.rs
+++ b/codex-rs/core/src/config/mod.rs
@@ -495,6 +495,39 @@ fn profile_allows_configured_network_proxy(permission_profile: &PermissionProfil
     }
 }
 
+fn build_network_proxy_spec(
+    configured_network_proxy_config: NetworkProxyConfig,
+    network_requirements: Option<Sourced<codex_config::NetworkConstraints>>,
+    permission_profile: &PermissionProfile,
+) -> std::io::Result<Option<NetworkProxySpec>> {
+    let (network_requirements, network_requirements_source) = match network_requirements {
+        Some(Sourced { value, source }) => (Some(value), Some(source)),
+        None => (None, None),
+    };
+    let has_network_requirements = network_requirements.is_some();
+    let network = NetworkProxySpec::from_config_and_constraints(
+        configured_network_proxy_config,
+        network_requirements,
+        permission_profile,
+    )
+    .map_err(|err| {
+        if let Some(source) = network_requirements_source.as_ref() {
+            std::io::Error::new(
+                err.kind(),
+                format!("failed to build managed network proxy from {source}: {err}"),
+            )
+        } else {
+            err
+        }
+    })?;
+
+    Ok(if has_network_requirements {
+        Some(network)
+    } else {
+        network.enabled().then_some(network)
+    })
+}
+
 /// Configured thread persistence backend.
 #[derive(Debug, Clone, PartialEq, Eq, Default)]
 pub enum ThreadStoreConfig {
@@ -3320,32 +3353,12 @@ impl Config {
         let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref())
             .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
 
-        let (network_requirements, network_requirements_source) = match network_requirements {
-            Some(Sourced { value, source }) => (Some(value), Some(source)),
-            None => (None, None),
-        };
-        let has_network_requirements = network_requirements.is_some();
         let network_permission_profile = constrained_permission_profile.get().clone();
-        let network = NetworkProxySpec::from_config_and_constraints(
+        let network = build_network_proxy_spec(
             configured_network_proxy_config,
             network_requirements,
             &network_permission_profile,
-        )
-        .map_err(|err| {
-            if let Some(source) = network_requirements_source.as_ref() {
-                std::io::Error::new(
-                    err.kind(),
-                    format!("failed to build managed network proxy from {source}: {err}"),
-                )
-            } else {
-                err
-            }
-        })?;
-        let network = if has_network_requirements {
-            Some(network)
-        } else {
-            network.enabled().then_some(network)
-        };
+        )?;
         let helper_readable_roots = get_readable_roots_required_for_codex_runtime(
             &codex_home,
             zsh_path.as_ref(),
@@ -3700,6 +3713,40 @@ impl Config {
             .is_some()
     }
 
+    pub(crate) fn network_proxy_spec_for_active_permission_profile(
+        &self,
+        active_permission_profile: &ActivePermissionProfile,
+        permission_profile: &PermissionProfile,
+    ) -> std::io::Result<Option<NetworkProxySpec>> {
+        let configured_network_proxy_config =
+            if profile_allows_configured_network_proxy(permission_profile) {
+                let cfg: ConfigToml = self
+                    .config_layer_stack
+                    .effective_config()
+                    .try_into()
+                    .map_err(|err| {
+                        std::io::Error::new(
+                            ErrorKind::InvalidInput,
+                            format!(
+                                "failed to read effective config for selected permission profile: {err}"
+                            ),
+                        )
+                    })?;
+                network_proxy_config_for_profile_selection(
+                    cfg.permissions.as_ref(),
+                    active_permission_profile.id.as_str(),
+                )?
+            } else {
+                NetworkProxyConfig::default()
+            };
+
+        build_network_proxy_spec(
+            configured_network_proxy_config,
+            self.config_layer_stack.requirements().network.clone(),
+            permission_profile,
+        )
+    }
+
     pub fn bundled_skills_enabled(&self) -> bool {
         crate::manager::bundled_skills_enabled_from_stack(&self.config_layer_stack)
     }
diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs
index 2908492ddd5..7df7a96928a 100644
--- a/codex-rs/core/src/guardian/review.rs
+++ b/codex-rs/core/src/guardian/review.rs
@@ -635,7 +635,8 @@ pub(super) async fn run_guardian_review_session(
     schema: serde_json::Value,
     external_cancel: Option<CancellationToken>,
 ) -> (GuardianReviewOutcome, GuardianReviewAnalyticsResult) {
-    let live_network_config = match session.services.network_proxy.as_ref() {
+    let network_proxy = session.services.network_proxy.load_full();
+    let live_network_config = match network_proxy.as_ref() {
         Some(network_proxy) => match network_proxy.proxy().current_cfg().await {
             Ok(config) => Some(config),
             Err(err) => {
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index b710e095ef8..55fd5444c6d 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -952,9 +952,6 @@ impl Session {
     }
 
     async fn refresh_managed_network_proxy_for_current_permission_profile(&self) {
-        let Some(started_proxy) = self.services.network_proxy.as_ref() else {
-            return;
-        };
         let Ok(_refresh_guard) = self.managed_network_proxy_refresh_lock.acquire().await else {
             error!("managed network proxy refresh semaphore closed");
             return;
@@ -968,7 +965,9 @@ impl Session {
             .permissions
             .network
             .as_ref()
+            .cloned()
         else {
+            self.services.network_proxy.store(None);
             return;
         };
 
@@ -991,8 +990,36 @@ impl Session {
                 spec
             }
         };
-        if let Err(err) = spec.apply_to_started_proxy(started_proxy).await {
-            warn!("failed to refresh managed network proxy for sandbox change: {err}");
+        if let Some(started_proxy) = self.services.network_proxy.load_full() {
+            if let Err(err) = spec.apply_to_started_proxy(started_proxy.as_ref()).await {
+                warn!("failed to refresh managed network proxy for sandbox change: {err}");
+            }
+            return;
+        }
+
+        match Self::start_managed_network_proxy(
+            &spec,
+            current_exec_policy.as_ref(),
+            &session_configuration.permission_profile(),
+            /*network_policy_decider*/ None,
+            self.services
+                .managed_network_requirements_configured
+                .then(|| {
+                    build_blocked_request_observer(Arc::clone(&self.services.network_approval))
+                }),
+            self.services.managed_network_requirements_configured,
+            self.services.network_proxy_audit_metadata.clone(),
+        )
+        .await
+        {
+            Ok((started_proxy, _session_network_proxy)) => {
+                self.services
+                    .network_proxy
+                    .store(Some(Arc::new(started_proxy)));
+            }
+            Err(err) => {
+                warn!("failed to start managed network proxy for sandbox change: {err}");
+            }
         }
     }
 
@@ -1886,7 +1913,7 @@ impl Session {
         let execpolicy_amendment =
             execpolicy_network_rule_amendment(amendment, network_approval_context, &host);
 
-        if let Some(started_network_proxy) = self.services.network_proxy.as_ref() {
+        if let Some(started_network_proxy) = self.services.network_proxy.load_full() {
             let proxy = started_network_proxy.proxy();
             match amendment.action {
                 NetworkPolicyRuleAction::Allow => proxy
diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs
index 88958b39ef4..8d6d1b49be2 100644
--- a/codex-rs/core/src/session/session.rs
+++ b/codex-rs/core/src/session/session.rs
@@ -1,5 +1,6 @@
 use super::input_queue::InputQueue;
 use super::*;
+use crate::config::ConstraintError;
 use crate::goals::GoalRuntimeState;
 use crate::skills::SkillError;
 use crate::state::ActiveTurn;
@@ -291,6 +292,32 @@ impl SessionConfiguration {
                 updates.profile_workspace_roots.clone().unwrap_or_default(),
                 Some(&current_file_system_sandbox_policy),
             )?;
+            if let Some(active_permission_profile) =
+                next_configuration.active_permission_profile()
+            {
+                let mut config = (*next_configuration.original_config_do_not_use).clone();
+                let permission_profile = next_configuration.permission_profile();
+                config.permissions.network = config
+                    .network_proxy_spec_for_active_permission_profile(
+                        &active_permission_profile,
+                        &permission_profile,
+                    )
+                    .map_err(|err| ConstraintError::InvalidValue {
+                        field_name: "default_permissions",
+                        candidate: active_permission_profile.id.clone(),
+                        allowed: format!(
+                            "configured permission profile with valid network policy ({err})"
+                        ),
+                        requirement_source: codex_config::RequirementSource::Unknown,
+                    })?;
+                config
+                    .permissions
+                    .set_permission_profile_with_active_profile(
+                        permission_profile,
+                        Some(active_permission_profile),
+                    )?;
+                next_configuration.original_config_do_not_use = Arc::new(config);
+            }
         } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
             let file_system_sandbox_policy =
                 FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
@@ -885,7 +912,7 @@ impl Session {
                         network_policy_decider.as_ref().map(Arc::clone),
                         blocked_request_observer.as_ref().map(Arc::clone),
                         managed_network_requirements_configured,
-                        network_proxy_audit_metadata,
+                        network_proxy_audit_metadata.clone(),
                     )
                     .instrument(info_span!(
                         "session_init.network_proxy",
@@ -977,7 +1004,9 @@ impl Session {
                 session_extension_data,
                 thread_extension_data,
                 agent_control,
-                network_proxy,
+                network_proxy: arc_swap::ArcSwapOption::from(network_proxy.map(Arc::new)),
+                network_proxy_audit_metadata,
+                managed_network_requirements_configured,
                 network_approval: Arc::clone(&network_approval),
                 state_db: state_db_ctx.clone(),
                 live_thread: live_thread_init.as_ref().cloned(),
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index a08243701c6..1bf502a9958 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -1,6 +1,7 @@
 use super::turn_context::TurnEnvironment;
 use super::*;
 use crate::config::ConfigBuilder;
+use crate::config::ConfigOverrides;
 use crate::config::test_config;
 use crate::context::ContextualUserFragment;
 use crate::context::TurnAborted;
@@ -80,6 +81,11 @@ use codex_app_server_protocol::AppInfo;
 use codex_app_server_protocol::McpElicitationSchema;
 use codex_config::config_toml::ConfigToml;
 use codex_config::config_toml::ProjectConfig;
+use codex_config::permissions_toml::FilesystemPermissionToml;
+use codex_config::permissions_toml::FilesystemPermissionsToml;
+use codex_config::permissions_toml::NetworkToml;
+use codex_config::permissions_toml::PermissionProfileToml;
+use codex_config::permissions_toml::PermissionsToml;
 use codex_execpolicy::Decision;
 use codex_execpolicy::NetworkRuleProtocol;
 use codex_execpolicy::Policy;
@@ -818,7 +824,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R
 
 #[tokio::test]
 async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow::Result<()> {
-    let (mut session, _turn_context) = make_session_and_context().await;
+    let (session, _turn_context) = make_session_and_context().await;
     let initial_permission_profile = PermissionProfile::workspace_write();
 
     let mut network_config = NetworkProxyConfig::default();
@@ -873,7 +879,10 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
             .set_permission_profile_for_tests(initial_permission_profile)
             .expect("test setup should allow permission profile");
     }
-    session.services.network_proxy = Some(started_proxy);
+    session
+        .services
+        .network_proxy
+        .store(Some(Arc::new(started_proxy)));
 
     session
         .new_turn_with_sub_id(
@@ -888,7 +897,7 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
     let started_proxy = session
         .services
         .network_proxy
-        .as_ref()
+        .load_full()
         .expect("managed network proxy should be present");
     assert_eq!(
         started_proxy
@@ -3805,6 +3814,107 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
     assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
 }
 
+#[tokio::test]
+async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Result<()> {
+    let codex_home = tempfile::tempdir().expect("create codex home");
+    let cwd = tempfile::tempdir().expect("create cwd");
+    let permissions = PermissionsToml {
+        entries: std::collections::BTreeMap::from([
+            (
+                "locked-down".to_string(),
+                PermissionProfileToml {
+                    filesystem: Some(FilesystemPermissionsToml {
+                        glob_scan_max_depth: None,
+                        entries: std::collections::BTreeMap::from([(
+                            ":minimal".to_string(),
+                            FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
+                        )]),
+                    }),
+                    network: None,
+                },
+            ),
+            (
+                "web-enabled".to_string(),
+                PermissionProfileToml {
+                    filesystem: Some(FilesystemPermissionsToml {
+                        glob_scan_max_depth: None,
+                        entries: std::collections::BTreeMap::from([(
+                            ":minimal".to_string(),
+                            FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
+                        )]),
+                    }),
+                    network: Some(NetworkToml {
+                        enabled: Some(true),
+                        proxy_url: Some("http://127.0.0.1:43128".to_string()),
+                        enable_socks5: Some(false),
+                        ..Default::default()
+                    }),
+                },
+            ),
+        ]),
+    };
+    let base_config = ConfigToml {
+        default_permissions: Some("locked-down".to_string()),
+        permissions: Some(permissions),
+        ..Default::default()
+    };
+    std::fs::write(
+        codex_home.path().join(codex_config::CONFIG_TOML_FILE),
+        toml::to_string(&base_config).expect("serialize config"),
+    )?;
+    let locked_config = Arc::new(
+        ConfigBuilder::default()
+            .codex_home(codex_home.path().to_path_buf())
+            .harness_overrides(ConfigOverrides {
+                cwd: Some(cwd.path().to_path_buf()),
+                ..Default::default()
+            })
+            .build()
+            .await?,
+    );
+    assert_ne!(
+        locked_config
+            .permissions
+            .network
+            .as_ref()
+            .map(crate::config::NetworkProxySpec::proxy_host_and_port)
+            .as_deref(),
+        Some("127.0.0.1:43128")
+    );
+    let selected_config = ConfigBuilder::default()
+        .codex_home(codex_home.path().to_path_buf())
+        .harness_overrides(ConfigOverrides {
+            cwd: Some(cwd.path().to_path_buf()),
+            default_permissions: Some("web-enabled".to_string()),
+            ..Default::default()
+        })
+        .build()
+        .await?;
+
+    let mut session_configuration = make_session_configuration_for_tests().await;
+    session_configuration.permission_profile_state =
+        locked_config.permissions.permission_profile_state().clone();
+    session_configuration.original_config_do_not_use = Arc::clone(&locked_config);
+
+    let updated = session_configuration
+        .apply(&SessionSettingsUpdate {
+            permission_profile: Some(selected_config.permissions.permission_profile()),
+            active_permission_profile: selected_config.permissions.active_permission_profile(),
+            ..Default::default()
+        })
+        .expect("active profile update should apply");
+
+    let network = updated
+        .original_config_do_not_use
+        .permissions
+        .network
+        .as_ref()
+        .expect("selected profile proxy should become the session proxy config");
+    assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
+    assert!(!network.socks_enabled());
+    Ok(())
+}
+
 #[cfg_attr(windows, ignore)]
 #[tokio::test]
 async fn new_default_turn_uses_config_aware_skills_for_role_overrides() {
@@ -4359,7 +4469,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
         ),
         thread_extension_data: codex_extension_api::ExtensionData::new(thread_id.to_string()),
         agent_control,
-        network_proxy: None,
+        network_proxy: arc_swap::ArcSwapOption::from(None),
+        network_proxy_audit_metadata: crate::config::NetworkProxyAuditMetadata::default(),
+        managed_network_requirements_configured: false,
         network_approval: Arc::clone(&network_approval),
         state_db: None,
         live_thread: None,
@@ -6186,7 +6298,9 @@ where
         ),
         thread_extension_data: codex_extension_api::ExtensionData::new(thread_id.to_string()),
         agent_control,
-        network_proxy: None,
+        network_proxy: arc_swap::ArcSwapOption::from(None),
+        network_proxy_audit_metadata: crate::config::NetworkProxyAuditMetadata::default(),
+        managed_network_requirements_configured: false,
         network_approval: Arc::clone(&network_approval),
         state_db: state_db.clone(),
         live_thread: None,
diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs
index 3338d0a5581..818a8b7bbe3 100644
--- a/codex-rs/core/src/session/turn_context.rs
+++ b/codex-rs/core/src/session/turn_context.rs
@@ -704,6 +704,7 @@ impl Session {
             &self.services.models_manager,
             self.services
                 .network_proxy
+                .load_full()
                 .as_ref()
                 .and_then(|started_proxy| {
                     Self::managed_network_proxy_active_for_permission_profile(
diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs
index afba409a068..59def9c7aba 100644
--- a/codex-rs/core/src/state/service.rs
+++ b/codex-rs/core/src/state/service.rs
@@ -5,6 +5,7 @@ use crate::SkillsManager;
 use crate::agent::AgentControl;
 use crate::attestation::AttestationProvider;
 use crate::client::ModelClient;
+use crate::config::NetworkProxyAuditMetadata;
 use crate::config::StartedNetworkProxy;
 use crate::exec_policy::ExecPolicyManager;
 use crate::guardian::GuardianRejection;
@@ -15,6 +16,7 @@ use crate::tools::network_approval::NetworkApprovalService;
 use crate::tools::sandboxing::ApprovalStore;
 use crate::unified_exec::UnifiedExecProcessManager;
 use arc_swap::ArcSwap;
+use arc_swap::ArcSwapOption;
 use codex_analytics::AnalyticsEventsClient;
 use codex_core_plugins::PluginsManager;
 use codex_exec_server::EnvironmentManager;
@@ -65,7 +67,9 @@ pub(crate) struct SessionServices {
     pub(crate) session_extension_data: ExtensionData,
     pub(crate) thread_extension_data: ExtensionData,
     pub(crate) agent_control: AgentControl,
-    pub(crate) network_proxy: Option<StartedNetworkProxy>,
+    pub(crate) network_proxy: ArcSwapOption<StartedNetworkProxy>,
+    pub(crate) network_proxy_audit_metadata: NetworkProxyAuditMetadata,
+    pub(crate) managed_network_requirements_configured: bool,
     pub(crate) network_approval: Arc<NetworkApprovalService>,
     pub(crate) state_db: Option<StateDbHandle>,
     pub(crate) live_thread: Option<LiveThread>,
diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs
index 80ddfee8974..9cb6b5f0f20 100644
--- a/codex-rs/core/src/tasks/mod.rs
+++ b/codex-rs/core/src/tasks/mod.rs
@@ -655,7 +655,8 @@ impl Session {
                     "false"
                 },
             );
-            let network_proxy_active = match self.services.network_proxy.as_ref() {
+            let network_proxy = self.services.network_proxy.load_full();
+            let network_proxy_active = match network_proxy.as_ref() {
                 Some(started_network_proxy) => {
                     match started_network_proxy.proxy().current_cfg().await {
                         Ok(config) => config.network.enabled,
diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs
index 1f594d64cef..3955b5ab09d 100644
--- a/codex-rs/tui/src/app/config_persistence.rs
+++ b/codex-rs/tui/src/app/config_persistence.rs
@@ -360,6 +360,7 @@ impl App {
                 /*cwd*/ None,
                 approval_policy_override,
                 approvals_reviewer_override,
+                permission_profile_override,
                 active_permission_profile_override,
                 /*windows_sandbox_level*/ None,
                 /*model*/ None,
@@ -387,6 +388,7 @@ impl App {
                         /*cwd*/ None,
                         /*approval_policy*/ None,
                         /*approvals_reviewer*/ None,
+                        /*permission_profile*/ None,
                         /*active_permission_profile*/ None,
                         #[cfg(target_os = "windows")]
                         Some(windows_sandbox_level),
diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs
index ce0eb0ff936..51615f6a956 100644
--- a/codex-rs/tui/src/app/event_dispatch.rs
+++ b/codex-rs/tui/src/app/event_dispatch.rs
@@ -1125,6 +1125,7 @@ impl App {
                                         /*cwd*/ None,
                                         /*approval_policy*/ None,
                                         /*approvals_reviewer*/ None,
+                                        /*permission_profile*/ None,
                                         /*active_permission_profile*/ None,
                                         #[cfg(target_os = "windows")]
                                         Some(windows_sandbox_level),
@@ -1150,6 +1151,7 @@ impl App {
                                         /*cwd*/ None,
                                         Some(AskForApproval::from(preset.approval)),
                                         Some(self.config.approvals_reviewer),
+                                        Some(preset.permission_profile.clone()),
                                         Some(preset.active_permission_profile.clone()),
                                         #[cfg(target_os = "windows")]
                                         Some(windows_sandbox_level),
diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs
index 0a2f382a8a4..d99b831bd47 100644
--- a/codex-rs/tui/src/app/tests.rs
+++ b/codex-rs/tui/src/app/tests.rs
@@ -1695,6 +1695,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
             cwd: None,
             approval_policy: Some(auto_review.approval_policy),
             approvals_reviewer: Some(auto_review.approvals_reviewer),
+            permission_profile: Some(auto_review.permission_profile.clone()),
             active_permission_profile: Some(auto_review.active_permission_profile.clone()),
             windows_sandbox_level: None,
             model: None,
@@ -1787,6 +1788,7 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor
             cwd: None,
             approval_policy: None,
             approvals_reviewer: Some(ApprovalsReviewer::User),
+            permission_profile: None,
             active_permission_profile: None,
             windows_sandbox_level: None,
             model: None,
@@ -1865,6 +1867,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review
             cwd: None,
             approval_policy: Some(auto_review.approval_policy),
             approvals_reviewer: Some(auto_review.approvals_reviewer),
+            permission_profile: Some(auto_review.permission_profile.clone()),
             active_permission_profile: Some(auto_review.active_permission_profile.clone()),
             windows_sandbox_level: None,
             model: None,
@@ -1922,6 +1925,7 @@ async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_wit
             cwd: None,
             approval_policy: None,
             approvals_reviewer: Some(ApprovalsReviewer::User),
+            permission_profile: None,
             active_permission_profile: None,
             windows_sandbox_level: None,
             model: None,
@@ -1981,6 +1985,7 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev
             cwd: None,
             approval_policy: Some(auto_review.approval_policy),
             approvals_reviewer: Some(auto_review.approvals_reviewer),
+            permission_profile: Some(auto_review.permission_profile.clone()),
             active_permission_profile: Some(auto_review.active_permission_profile.clone()),
             windows_sandbox_level: None,
             model: None,
@@ -2068,6 +2073,7 @@ guardian_approval = true
             cwd: None,
             approval_policy: None,
             approvals_reviewer: Some(ApprovalsReviewer::User),
+            permission_profile: None,
             active_permission_profile: None,
             windows_sandbox_level: None,
             model: None,
diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs
index 0b6484d0155..2ecf17f5a34 100644
--- a/codex-rs/tui/src/app_command.rs
+++ b/codex-rs/tui/src/app_command.rs
@@ -17,6 +17,7 @@ use codex_protocol::config_types::Personality;
 use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
 use codex_protocol::config_types::WindowsSandboxLevel;
 use codex_protocol::models::ActivePermissionProfile;
+use codex_protocol::models::PermissionProfile;
 use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
 use codex_protocol::request_permissions::RequestPermissionsResponse;
 use serde::Serialize;
@@ -54,6 +55,7 @@ pub(crate) enum AppCommand {
         cwd: Option<PathBuf>,
         approval_policy: Option<AskForApproval>,
         approvals_reviewer: Option<ApprovalsReviewer>,
+        permission_profile: Option<PermissionProfile>,
         active_permission_profile: Option<ActivePermissionProfile>,
         windows_sandbox_level: Option<WindowsSandboxLevel>,
         model: Option<String>,
@@ -172,6 +174,7 @@ impl AppCommand {
         cwd: Option<PathBuf>,
         approval_policy: Option<AskForApproval>,
         approvals_reviewer: Option<ApprovalsReviewer>,
+        permission_profile: Option<PermissionProfile>,
         active_permission_profile: Option<ActivePermissionProfile>,
         windows_sandbox_level: Option<WindowsSandboxLevel>,
         model: Option<String>,
@@ -185,6 +188,7 @@ impl AppCommand {
             cwd,
             approval_policy,
             approvals_reviewer,
+            permission_profile,
             active_permission_profile,
             windows_sandbox_level,
             model,
diff --git a/codex-rs/tui/src/chatwidget/service_tiers.rs b/codex-rs/tui/src/chatwidget/service_tiers.rs
index a60af0a69eb..fd11048cb24 100644
--- a/codex-rs/tui/src/chatwidget/service_tiers.rs
+++ b/codex-rs/tui/src/chatwidget/service_tiers.rs
@@ -107,6 +107,7 @@ impl ChatWidget {
                 /*cwd*/ None,
                 /*approval_policy*/ None,
                 /*approvals_reviewer*/ None,
+                /*permission_profile*/ None,
                 /*active_permission_profile*/ None,
                 /*windows_sandbox_level*/ None,
                 /*model*/ None,
diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs
index 71483bd39a7..ae58271dff3 100644
--- a/codex-rs/tui/src/chatwidget/tests/permissions.rs
+++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs
@@ -743,6 +743,7 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context
             cwd: None,
             approval_policy: Some(AskForApproval::OnRequest),
             approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
+            permission_profile: Some(PermissionProfile::workspace_write()),
             active_permission_profile: Some(ActivePermissionProfile::new(
                 BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
             )),

From b79b55650fd4107ad1d8f563b7b35396dfa52c4f Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Thu, 7 May 2026 17:21:02 -0700
Subject: [PATCH 02/15] style: stabilize network proxy config formatting

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/config/mod.rs | 43 +++++++++++++++++----------------
 1 file changed, 22 insertions(+), 21 deletions(-)

diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs
index 6960bcdd5a4..fade3ea392d 100644
--- a/codex-rs/core/src/config/mod.rs
+++ b/codex-rs/core/src/config/mod.rs
@@ -3718,27 +3718,28 @@ impl Config {
         active_permission_profile: &ActivePermissionProfile,
         permission_profile: &PermissionProfile,
     ) -> std::io::Result<Option<NetworkProxySpec>> {
-        let configured_network_proxy_config =
-            if profile_allows_configured_network_proxy(permission_profile) {
-                let cfg: ConfigToml = self
-                    .config_layer_stack
-                    .effective_config()
-                    .try_into()
-                    .map_err(|err| {
-                        std::io::Error::new(
-                            ErrorKind::InvalidInput,
-                            format!(
-                                "failed to read effective config for selected permission profile: {err}"
-                            ),
-                        )
-                    })?;
-                network_proxy_config_for_profile_selection(
-                    cfg.permissions.as_ref(),
-                    active_permission_profile.id.as_str(),
-                )?
-            } else {
-                NetworkProxyConfig::default()
-            };
+        let profile_allows_network_proxy =
+            profile_allows_configured_network_proxy(permission_profile);
+        let configured_network_proxy_config = if profile_allows_network_proxy {
+            let cfg: ConfigToml = self
+                .config_layer_stack
+                .effective_config()
+                .try_into()
+                .map_err(|err| {
+                    std::io::Error::new(
+                        ErrorKind::InvalidInput,
+                        format!(
+                            "failed to read effective config for selected permission profile: {err}"
+                        ),
+                    )
+                })?;
+            network_proxy_config_for_profile_selection(
+                cfg.permissions.as_ref(),
+                active_permission_profile.id.as_str(),
+            )?
+        } else {
+            NetworkProxyConfig::default()
+        };
 
         build_network_proxy_spec(
             configured_network_proxy_config,

From 7d923c586a1eeef34bf0c072adffdfe24f57ed9e Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Fri, 15 May 2026 16:54:33 -0700
Subject: [PATCH 03/15] fix: align runtime profile refresh with current session
 APIs

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/session/session.rs | 2 +-
 codex-rs/core/src/session/tests.rs   | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs
index 8d6d1b49be2..ebe1027dba5 100644
--- a/codex-rs/core/src/session/session.rs
+++ b/codex-rs/core/src/session/session.rs
@@ -312,7 +312,7 @@ impl SessionConfiguration {
                     })?;
                 config
                     .permissions
-                    .set_permission_profile_with_active_profile(
+                    .set_permission_profile_from_session_snapshot(
                         permission_profile,
                         Some(active_permission_profile),
                     )?;
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index 1bf502a9958..851d894d783 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -3823,6 +3823,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
             (
                 "locked-down".to_string(),
                 PermissionProfileToml {
+                    workspace_roots: None,
                     filesystem: Some(FilesystemPermissionsToml {
                         glob_scan_max_depth: None,
                         entries: std::collections::BTreeMap::from([(
@@ -3836,6 +3837,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
             (
                 "web-enabled".to_string(),
                 PermissionProfileToml {
+                    workspace_roots: None,
                     filesystem: Some(FilesystemPermissionsToml {
                         glob_scan_max_depth: None,
                         entries: std::collections::BTreeMap::from([(
@@ -3898,7 +3900,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
 
     let updated = session_configuration
         .apply(&SessionSettingsUpdate {
-            permission_profile: Some(selected_config.permissions.permission_profile()),
+            permission_profile: Some(selected_config.permissions.permission_profile().clone()),
             active_permission_profile: selected_config.permissions.active_permission_profile(),
             ..Default::default()
         })

From 0f94819a746d96e56f56ed76cb96107d13a54891 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Fri, 15 May 2026 17:01:28 -0700
Subject: [PATCH 04/15] style(core): satisfy session formatting

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/session/session.rs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs
index ebe1027dba5..6882303c56d 100644
--- a/codex-rs/core/src/session/session.rs
+++ b/codex-rs/core/src/session/session.rs
@@ -292,8 +292,7 @@ impl SessionConfiguration {
                 updates.profile_workspace_roots.clone().unwrap_or_default(),
                 Some(&current_file_system_sandbox_policy),
             )?;
-            if let Some(active_permission_profile) =
-                next_configuration.active_permission_profile()
+            if let Some(active_permission_profile) = next_configuration.active_permission_profile()
             {
                 let mut config = (*next_configuration.original_config_do_not_use).clone();
                 let permission_profile = next_configuration.permission_profile();

From e84f80c735be78101096da0994adea1bde7d4450 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Mon, 18 May 2026 10:46:33 -0700
Subject: [PATCH 05/15] fix(core): adopt permission snapshot refresh API

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/session/session.rs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs
index 6882303c56d..7ae5ff241d4 100644
--- a/codex-rs/core/src/session/session.rs
+++ b/codex-rs/core/src/session/session.rs
@@ -312,8 +312,10 @@ impl SessionConfiguration {
                 config
                     .permissions
                     .set_permission_profile_from_session_snapshot(
-                        permission_profile,
-                        Some(active_permission_profile),
+                        PermissionProfileSnapshot::active(
+                            permission_profile,
+                            active_permission_profile,
+                        ),
                     )?;
                 next_configuration.original_config_do_not_use = Arc::new(config);
             }

From a91223148490a26186faa5eb41d66dd1cdef1916 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Mon, 18 May 2026 18:17:53 -0700
Subject: [PATCH 06/15] fix(tui): restore permission profile override plumbing

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/tui/src/app/tests.rs                    | 6 +++---
 codex-rs/tui/src/chatwidget/permission_popups.rs | 8 ++++++++
 codex-rs/tui/src/chatwidget/rate_limits.rs       | 1 +
 codex-rs/tui/src/chatwidget/settings_popups.rs   | 1 +
 4 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs
index d99b831bd47..a501c251907 100644
--- a/codex-rs/tui/src/app/tests.rs
+++ b/codex-rs/tui/src/app/tests.rs
@@ -1695,7 +1695,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
             cwd: None,
             approval_policy: Some(auto_review.approval_policy),
             approvals_reviewer: Some(auto_review.approvals_reviewer),
-            permission_profile: Some(auto_review.permission_profile.clone()),
+            permission_profile: Some(auto_review.permission_profile()),
             active_permission_profile: Some(auto_review.active_permission_profile.clone()),
             windows_sandbox_level: None,
             model: None,
@@ -1867,7 +1867,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review
             cwd: None,
             approval_policy: Some(auto_review.approval_policy),
             approvals_reviewer: Some(auto_review.approvals_reviewer),
-            permission_profile: Some(auto_review.permission_profile.clone()),
+            permission_profile: Some(auto_review.permission_profile()),
             active_permission_profile: Some(auto_review.active_permission_profile.clone()),
             windows_sandbox_level: None,
             model: None,
@@ -1985,7 +1985,7 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev
             cwd: None,
             approval_policy: Some(auto_review.approval_policy),
             approvals_reviewer: Some(auto_review.approvals_reviewer),
-            permission_profile: Some(auto_review.permission_profile.clone()),
+            permission_profile: Some(auto_review.permission_profile()),
             active_permission_profile: Some(auto_review.active_permission_profile.clone()),
             windows_sandbox_level: None,
             model: None,
diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs
index a82f3a6cdbe..dc5d6c1870d 100644
--- a/codex-rs/tui/src/chatwidget/permission_popups.rs
+++ b/codex-rs/tui/src/chatwidget/permission_popups.rs
@@ -124,6 +124,7 @@ impl ChatWidget {
                     } else {
                         Self::approval_preset_actions(
                             preset_approval,
+                            preset.permission_profile.clone(),
                             preset.active_permission_profile.clone(),
                             base_name.clone(),
                             ApprovalsReviewer::User,
@@ -134,6 +135,7 @@ impl ChatWidget {
                 {
                     Self::approval_preset_actions(
                         preset_approval,
+                        preset.permission_profile.clone(),
                         preset.active_permission_profile.clone(),
                         base_name.clone(),
                         ApprovalsReviewer::User,
@@ -142,6 +144,7 @@ impl ChatWidget {
             } else {
                 Self::approval_preset_actions(
                     preset_approval,
+                    preset.permission_profile.clone(),
                     preset.active_permission_profile.clone(),
                     base_name.clone(),
                     ApprovalsReviewer::User,
@@ -180,6 +183,7 @@ impl ChatWidget {
                             ),
                         actions: Self::approval_preset_actions(
                             preset_approval,
+                            preset.permission_profile.clone(),
                             preset.active_permission_profile.clone(),
                             "Auto-review".to_string(),
                             ApprovalsReviewer::AutoReview,
@@ -308,6 +312,7 @@ impl ChatWidget {
 
     pub(super) fn approval_preset_actions(
         approval: AskForApproval,
+        permission_profile: PermissionProfile,
         active_permission_profile: ActivePermissionProfile,
         label: String,
         approvals_reviewer: ApprovalsReviewer,
@@ -317,6 +322,7 @@ impl ChatWidget {
                 /*cwd*/ None,
                 Some(approval),
                 Some(approvals_reviewer),
+                Some(permission_profile.clone()),
                 Some(active_permission_profile.clone()),
                 /*windows_sandbox_level*/ None,
                 /*model*/ None,
@@ -402,6 +408,7 @@ impl ChatWidget {
 
         let mut accept_actions = Self::approval_preset_actions(
             approval,
+            preset.permission_profile.clone(),
             preset.active_permission_profile.clone(),
             selected_name.clone(),
             ApprovalsReviewer::User,
@@ -412,6 +419,7 @@ impl ChatWidget {
 
         let mut accept_and_remember_actions = Self::approval_preset_actions(
             approval,
+            preset.permission_profile,
             preset.active_permission_profile,
             selected_name,
             ApprovalsReviewer::User,
diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs
index 544611f4f0f..df45a11c7d3 100644
--- a/codex-rs/tui/src/chatwidget/rate_limits.rs
+++ b/codex-rs/tui/src/chatwidget/rate_limits.rs
@@ -308,6 +308,7 @@ impl ChatWidget {
                 /*cwd*/ None,
                 /*approval_policy*/ None,
                 /*approvals_reviewer*/ None,
+                /*permission_profile*/ None,
                 /*active_permission_profile*/ None,
                 /*windows_sandbox_level*/ None,
                 Some(switch_model_for_events.clone()),
diff --git a/codex-rs/tui/src/chatwidget/settings_popups.rs b/codex-rs/tui/src/chatwidget/settings_popups.rs
index 2dcfefb73ac..357223a6ca4 100644
--- a/codex-rs/tui/src/chatwidget/settings_popups.rs
+++ b/codex-rs/tui/src/chatwidget/settings_popups.rs
@@ -53,6 +53,7 @@ impl ChatWidget {
                         /*cwd*/ None,
                         /*approval_policy*/ None,
                         /*approvals_reviewer*/ None,
+                        /*permission_profile*/ None,
                         /*active_permission_profile*/ None,
                         /*windows_sandbox_level*/ None,
                         /*model*/ None,

From fcf5260e738cd22731d3358e4add60330c0b5b31 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Mon, 18 May 2026 18:49:43 -0700
Subject: [PATCH 07/15] fix(ci): repair profile permission refresh

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/config/mod.rs               | 16 ++++++++++++++--
 .../src/chatwidget/windows_sandbox_prompts.rs | 19 ++++++++++++-------
 2 files changed, 26 insertions(+), 9 deletions(-)

diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs
index fade3ea392d..ff09f26c284 100644
--- a/codex-rs/core/src/config/mod.rs
+++ b/codex-rs/core/src/config/mod.rs
@@ -3733,10 +3733,22 @@ impl Config {
                         ),
                     )
                 })?;
-            network_proxy_config_for_profile_selection(
+            let mut configured_network_proxy_config = network_proxy_config_for_profile_selection(
                 cfg.permissions.as_ref(),
                 active_permission_profile.id.as_str(),
-            )?
+            )?;
+            if self.features.enabled(Feature::NetworkProxy)
+                && permission_profile.network_sandbox_policy().is_enabled()
+            {
+                if let Some(network_proxy) = network_proxy_toml_config(cfg.features.as_ref()) {
+                    apply_network_proxy_feature_config(
+                        &mut configured_network_proxy_config,
+                        network_proxy,
+                    );
+                }
+                configured_network_proxy_config.network.enabled = true;
+            }
+            configured_network_proxy_config
         } else {
             NetworkProxyConfig::default()
         };
diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs
index d8ff59da484..3b71d467bf2 100644
--- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs
+++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs
@@ -42,12 +42,13 @@ impl ChatWidget {
         extra_count: usize,
         failed_scan: bool,
     ) {
-        let (approval, active_permission_profile) = match &preset {
+        let (approval, permission_profile, active_permission_profile) = match &preset {
             Some(p) => (
                 Some(AskForApproval::from(p.approval)),
+                Some(p.permission_profile.clone()),
                 Some(p.active_permission_profile.clone()),
             ),
-            None => (None, None),
+            None => (None, None, None),
         };
         let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
         let describe_profile = |profile: &PermissionProfile| {
@@ -110,11 +111,14 @@ impl ChatWidget {
                 tx.send(AppEvent::SkipNextWorldWritableScan);
             }));
         }
-        if let (Some(approval), Some(active_permission_profile)) =
-            (approval, active_permission_profile.clone())
-        {
+        if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) = (
+            approval,
+            permission_profile.clone(),
+            active_permission_profile.clone(),
+        ) {
             accept_actions.extend(Self::approval_preset_actions(
                 approval,
+                permission_profile,
                 active_permission_profile,
                 mode_label.to_string(),
                 ApprovalsReviewer::User,
@@ -126,11 +130,12 @@ impl ChatWidget {
             tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));
             tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
         }));
-        if let (Some(approval), Some(active_permission_profile)) =
-            (approval, active_permission_profile)
+        if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) =
+            (approval, permission_profile, active_permission_profile)
         {
             accept_and_remember_actions.extend(Self::approval_preset_actions(
                 approval,
+                permission_profile,
                 active_permission_profile,
                 mode_label.to_string(),
                 ApprovalsReviewer::User,

From 2ccc164bd922c80bdbc97c3c40cca0350f4de8f4 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Mon, 18 May 2026 19:03:11 -0700
Subject: [PATCH 08/15] test(core): pin network proxy in profile refresh
 coverage

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/session/tests.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index 851d894d783..de6fbf0f254 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -3856,6 +3856,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
         ]),
     };
     let base_config = ConfigToml {
+        features: Some(toml::from_str("network_proxy = true").expect("valid features")),
         default_permissions: Some("locked-down".to_string()),
         permissions: Some(permissions),
         ..Default::default()

From 9badcc117a82e1706c1292665d9ce15832aee77b Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Tue, 19 May 2026 22:09:37 -0700
Subject: [PATCH 09/15] fix(core): populate profile descriptions in runtime
 tests

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/session/tests.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index de6fbf0f254..fd16594dc8a 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -3823,6 +3823,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
             (
                 "locked-down".to_string(),
                 PermissionProfileToml {
+                    description: None,
                     workspace_roots: None,
                     filesystem: Some(FilesystemPermissionsToml {
                         glob_scan_max_depth: None,
@@ -3837,6 +3838,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
             (
                 "web-enabled".to_string(),
                 PermissionProfileToml {
+                    description: None,
                     workspace_roots: None,
                     filesystem: Some(FilesystemPermissionsToml {
                         glob_scan_max_depth: None,

From b526f8b8b5a4f757fc6c6661d3f40a5fdb479fba Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Wed, 20 May 2026 14:27:16 -0700
Subject: [PATCH 10/15] fix(tui): align profile runtime with current settings

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/core/src/session/tests.rs      | 2 ++
 codex-rs/tui/src/app/thread_settings.rs | 1 +
 codex-rs/tui/src/chatwidget/settings.rs | 1 +
 3 files changed, 4 insertions(+)

diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index fd16594dc8a..e669becc4c9 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -3824,6 +3824,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
                 "locked-down".to_string(),
                 PermissionProfileToml {
                     description: None,
+                    extends: None,
                     workspace_roots: None,
                     filesystem: Some(FilesystemPermissionsToml {
                         glob_scan_max_depth: None,
@@ -3839,6 +3840,7 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
                 "web-enabled".to_string(),
                 PermissionProfileToml {
                     description: None,
+                    extends: None,
                     workspace_roots: None,
                     filesystem: Some(FilesystemPermissionsToml {
                         glob_scan_max_depth: None,
diff --git a/codex-rs/tui/src/app/thread_settings.rs b/codex-rs/tui/src/app/thread_settings.rs
index bf928278799..27c435af3fd 100644
--- a/codex-rs/tui/src/app/thread_settings.rs
+++ b/codex-rs/tui/src/app/thread_settings.rs
@@ -101,6 +101,7 @@ impl App {
             cwd,
             approval_policy,
             approvals_reviewer,
+            permission_profile: _,
             active_permission_profile,
             windows_sandbox_level: _,
             model,
diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs
index 0434f7e3866..0df36f6bf89 100644
--- a/codex-rs/tui/src/chatwidget/settings.rs
+++ b/codex-rs/tui/src/chatwidget/settings.rs
@@ -734,6 +734,7 @@ impl ChatWidget {
                 /*cwd*/ None,
                 /*approval_policy*/ None,
                 /*approvals_reviewer*/ None,
+                /*permission_profile*/ None,
                 /*active_permission_profile*/ None,
                 /*windows_sandbox_level*/ None,
                 /*model*/ None,

From b7084b84b513198f925f6acef3d1e47a6c18d721 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Wed, 20 May 2026 14:37:21 -0700
Subject: [PATCH 11/15] fix(tui): align profile runtime test override args

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/tui/src/app/tests.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs
index a501c251907..92e824aec72 100644
--- a/codex-rs/tui/src/app/tests.rs
+++ b/codex-rs/tui/src/app/tests.rs
@@ -5282,6 +5282,7 @@ async fn override_turn_context_sends_thread_settings_update() {
             /*cwd*/ None,
             Some(AskForApproval::OnRequest),
             Some(ApprovalsReviewer::AutoReview),
+            /*permission_profile*/ None,
             Some(ActivePermissionProfile::new(
                 codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
             )),

From 0ae28633186cf7a176bd067ed13178eb27ca142d Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Wed, 20 May 2026 08:47:26 -0700
Subject: [PATCH 12/15] feat(tui): plumb permission profile selections

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/tui/src/app.rs                       |  20 +-
 codex-rs/tui/src/app/config_persistence.rs    | 168 ++++++++++++-
 codex-rs/tui/src/app/event_dispatch.rs        |  99 ++++++--
 codex-rs/tui/src/app/platform_actions.rs      |   1 +
 codex-rs/tui/src/app/tests.rs                 |  86 ++++++-
 codex-rs/tui/src/app/thread_routing.rs        |   4 +-
 codex-rs/tui/src/app_event.rs                 |  19 ++
 .../tui/src/chatwidget/permission_popups.rs   | 227 ++++++++++--------
 codex-rs/tui/src/chatwidget/settings.rs       |  22 ++
 .../src/chatwidget/windows_sandbox_prompts.rs |  47 +++-
 10 files changed, 557 insertions(+), 136 deletions(-)

diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index cbdbee0402e..8d1797e7247 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -10,6 +10,7 @@ use crate::app_event::AppEvent;
 use crate::app_event::ExitMode;
 use crate::app_event::FeedbackCategory;
 use crate::app_event::HistoryLookupResponse;
+use crate::app_event::PermissionProfileSelection;
 use crate::app_event::RateLimitRefreshOrigin;
 use crate::app_event::RealtimeAudioDeviceKind;
 #[cfg(target_os = "windows")]
@@ -484,7 +485,7 @@ pub(crate) struct App {
     harness_overrides: ConfigOverrides,
     loader_overrides: LoaderOverrides,
     runtime_approval_policy_override: Option<AskForApproval>,
-    runtime_permission_profile_override: Option<PermissionProfile>,
+    runtime_permission_profile_override: Option<RuntimePermissionProfileOverride>,
 
     pub(crate) file_search: FileSearchManager,
 
@@ -554,6 +555,23 @@ pub(crate) struct App {
     pending_hook_enabled_writes: HashMap<String, Option<bool>>,
 }
 
+#[derive(Debug, Clone, PartialEq)]
+struct RuntimePermissionProfileOverride {
+    permission_profile: PermissionProfile,
+    active_permission_profile: Option<ActivePermissionProfile>,
+    network: Option<crate::legacy_core::config::NetworkProxySpec>,
+}
+
+impl RuntimePermissionProfileOverride {
+    fn from_config(config: &Config) -> Self {
+        Self {
+            permission_profile: config.permissions.permission_profile().clone(),
+            active_permission_profile: config.permissions.active_permission_profile(),
+            network: config.permissions.network.clone(),
+        }
+    }
+}
+
 fn active_turn_not_steerable_turn_error(error: &TypedRequestError) -> Option<AppServerTurnError> {
     let TypedRequestError::Server { source, .. } = error else {
         return None;
diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs
index 3955b5ab09d..cdcce127b13 100644
--- a/codex-rs/tui/src/app/config_persistence.rs
+++ b/codex-rs/tui/src/app/config_persistence.rs
@@ -21,6 +21,143 @@ impl App {
             .wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}"))
     }
 
+    pub(super) async fn rebuild_config_for_permission_profile(
+        &self,
+        profile_id: &str,
+    ) -> Result<Config> {
+        let mut overrides = self.harness_overrides.clone();
+        overrides.cwd = Some(self.chat_widget.config_ref().cwd.to_path_buf());
+        overrides.sandbox_mode = None;
+        overrides.permission_profile = None;
+        overrides.default_permissions = Some(profile_id.to_string());
+        ConfigBuilder::default()
+            .codex_home(self.config.codex_home.to_path_buf())
+            .cli_overrides(self.cli_kv_overrides.clone())
+            .harness_overrides(overrides)
+            .build()
+            .await
+            .wrap_err_with(|| {
+                format!("Failed to rebuild config for permission profile {profile_id}")
+            })
+    }
+
+    pub(super) async fn apply_permission_profile_selection(
+        &mut self,
+        selection: PermissionProfileSelection,
+    ) -> bool {
+        let PermissionProfileSelection {
+            profile_id,
+            approval_policy,
+            approvals_reviewer,
+            display_label,
+        } = selection;
+        let selected_config = match self
+            .rebuild_config_for_permission_profile(profile_id.as_str())
+            .await
+        {
+            Ok(config) => config,
+            Err(err) => {
+                tracing::warn!(
+                    error = %err,
+                    profile_id,
+                    "failed to resolve selected permission profile"
+                );
+                self.chat_widget.add_error_message(format!(
+                    "Failed to set permission profile `{profile_id}`: {err}"
+                ));
+                return false;
+            }
+        };
+        let permission_profile = selected_config.permissions.permission_profile();
+        let active_permission_profile = selected_config.permissions.active_permission_profile();
+        let network = selected_config.permissions.network.clone();
+
+        let mut config = self.config.clone();
+        if let Some(policy) = approval_policy
+            && !self.try_set_approval_policy_on_config(
+                &mut config,
+                policy,
+                "Failed to set approval policy",
+                "failed to set selected permission profile approval policy on app config",
+            )
+        {
+            return false;
+        }
+        if let Err(err) = config
+            .permissions
+            .set_permission_profile_from_session_snapshot(
+                PermissionProfileSnapshot::from_session_snapshot(
+                    permission_profile.clone(),
+                    active_permission_profile.clone(),
+                ),
+            )
+        {
+            tracing::warn!(
+                error = %err,
+                profile_id,
+                "failed to set selected permission profile on app config"
+            );
+            self.chat_widget.add_error_message(format!(
+                "Failed to set permission profile `{profile_id}`: {err}"
+            ));
+            return false;
+        }
+        if let Some(reviewer) = approvals_reviewer {
+            config.approvals_reviewer = reviewer;
+        }
+        config.permissions.network = network.clone();
+        self.config = config;
+
+        if let Some(policy) = approval_policy {
+            self.runtime_approval_policy_override = Some(policy);
+            self.chat_widget.set_approval_policy(policy);
+        }
+        if let Err(err) = self.chat_widget.set_permission_profile_with_active_profile(
+            permission_profile.clone(),
+            active_permission_profile.clone(),
+        ) {
+            tracing::warn!(
+                error = %err,
+                profile_id,
+                "failed to set selected permission profile on chat config"
+            );
+            self.chat_widget.add_error_message(format!(
+                "Failed to set permission profile `{profile_id}`: {err}"
+            ));
+            return false;
+        }
+        if let Some(reviewer) = approvals_reviewer {
+            self.chat_widget.set_approvals_reviewer(reviewer);
+        }
+        self.chat_widget.set_permission_network(network);
+        self.runtime_permission_profile_override =
+            Some(RuntimePermissionProfileOverride::from_config(&self.config));
+        self.sync_active_thread_permission_settings_to_cached_session()
+            .await;
+        self.app_event_tx
+            .send(AppEvent::CodexOp(AppCommand::override_turn_context(
+                /*cwd*/ None,
+                approval_policy,
+                approvals_reviewer,
+                Some(permission_profile.clone()),
+                active_permission_profile,
+                /*windows_sandbox_level*/ None,
+                /*model*/ None,
+                /*effort*/ None,
+                /*summary*/ None,
+                /*service_tier*/ None,
+                /*collaboration_mode*/ None,
+                /*personality*/ None,
+            )));
+        self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
+            history_cell::new_info_event(
+                format!("Permissions updated to {display_label}"),
+                /*hint*/ None,
+            ),
+        )));
+        true
+    }
+
     pub(super) async fn refresh_in_memory_config_from_disk(&mut self) -> Result<()> {
         let mut config = self
             .rebuild_config_for_cwd(self.chat_widget.config_ref().cwd.to_path_buf())
@@ -73,13 +210,25 @@ impl App {
                 "Failed to carry forward approval policy override: {err}"
             ));
         }
-        if let Some(profile) = self.runtime_permission_profile_override.as_ref()
-            && let Err(err) = config.permissions.set_permission_profile(profile.clone())
-        {
-            tracing::warn!(%err, "failed to carry forward permission profile override");
-            self.chat_widget.add_error_message(format!(
-                "Failed to carry forward permission profile override: {err}"
-            ));
+        if let Some(profile_override) = self.runtime_permission_profile_override.as_ref() {
+            match config
+                .permissions
+                .set_permission_profile_from_session_snapshot(
+                    PermissionProfileSnapshot::from_session_snapshot(
+                        profile_override.permission_profile.clone(),
+                        profile_override.active_permission_profile.clone(),
+                    ),
+                ) {
+                Ok(()) => {
+                    config.permissions.network = profile_override.network.clone();
+                }
+                Err(err) => {
+                    tracing::warn!(%err, "failed to carry forward permission profile override");
+                    self.chat_widget.add_error_message(format!(
+                        "Failed to carry forward permission profile override: {err}"
+                    ));
+                }
+            }
         }
     }
 
@@ -341,8 +490,9 @@ impl App {
             self.chat_widget
                 .add_error_message(format!("Failed to enable Auto-review: {err}"));
         }
-        if let Some(permission_profile) = permission_profile_override_value {
-            self.runtime_permission_profile_override = Some(permission_profile);
+        if permission_profile_override.is_some() {
+            self.runtime_permission_profile_override =
+                Some(RuntimePermissionProfileOverride::from_config(&self.config));
         }
 
         if approval_policy_override.is_some()
diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs
index 51615f6a956..61f1fe5fcdb 100644
--- a/codex-rs/tui/src/app/event_dispatch.rs
+++ b/codex-rs/tui/src/app/event_dispatch.rs
@@ -798,18 +798,24 @@ impl App {
             AppEvent::OpenFullAccessConfirmation {
                 preset,
                 return_to_permissions,
+                profile_selection,
             } => {
-                self.chat_widget
-                    .open_full_access_confirmation(preset, return_to_permissions);
+                self.chat_widget.open_full_access_confirmation(
+                    preset,
+                    return_to_permissions,
+                    profile_selection,
+                );
             }
             AppEvent::OpenWorldWritableWarningConfirmation {
                 preset,
+                profile_selection,
                 sample_paths,
                 extra_count,
                 failed_scan,
             } => {
                 self.chat_widget.open_world_writable_warning_confirmation(
                     preset,
+                    profile_selection,
                     sample_paths,
                     extra_count,
                     failed_scan,
@@ -846,10 +852,17 @@ impl App {
                     self.launch_external_editor(tui).await;
                 }
             }
-            AppEvent::OpenWindowsSandboxEnablePrompt { preset } => {
-                self.chat_widget.open_windows_sandbox_enable_prompt(preset);
+            AppEvent::OpenWindowsSandboxEnablePrompt {
+                preset,
+                profile_selection,
+            } => {
+                self.chat_widget
+                    .open_windows_sandbox_enable_prompt(preset, profile_selection);
             }
-            AppEvent::OpenWindowsSandboxFallbackPrompt { preset } => {
+            AppEvent::OpenWindowsSandboxFallbackPrompt {
+                preset,
+                profile_selection,
+            } => {
                 self.session_telemetry.counter(
                     "codex.windows_sandbox.fallback_prompt_shown",
                     /*inc*/ 1,
@@ -864,9 +877,12 @@ impl App {
                     );
                 }
                 self.chat_widget
-                    .open_windows_sandbox_fallback_prompt(preset);
+                    .open_windows_sandbox_fallback_prompt(preset, profile_selection);
             }
-            AppEvent::BeginWindowsSandboxElevatedSetup { preset } => {
+            AppEvent::BeginWindowsSandboxElevatedSetup {
+                preset,
+                profile_selection,
+            } => {
                 #[cfg(target_os = "windows")]
                 {
                     let permission_profile = preset.permission_profile.clone();
@@ -885,6 +901,7 @@ impl App {
                         tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
                             preset,
                             mode: WindowsSandboxEnableMode::Elevated,
+                            profile_selection,
                         });
                         return Ok(AppRunControl::Continue);
                     }
@@ -901,7 +918,10 @@ impl App {
                             );
                         })
                     else {
-                        tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset });
+                        tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {
+                            preset,
+                            profile_selection,
+                        });
                         return Ok(AppRunControl::Continue);
                     };
                     tokio::task::spawn_blocking(move || {
@@ -922,6 +942,7 @@ impl App {
                                 AppEvent::EnableWindowsSandboxForAgentMode {
                                     preset: preset.clone(),
                                     mode: WindowsSandboxEnableMode::Elevated,
+                                    profile_selection: profile_selection.clone(),
                                 }
                             }
                             Err(err) => {
@@ -953,7 +974,10 @@ impl App {
                                     error = %err,
                                     "failed to run elevated Windows sandbox setup"
                                 );
-                                AppEvent::OpenWindowsSandboxFallbackPrompt { preset }
+                                AppEvent::OpenWindowsSandboxFallbackPrompt {
+                                    preset,
+                                    profile_selection,
+                                }
                             }
                         };
                         tx.send(event);
@@ -961,10 +985,13 @@ impl App {
                 }
                 #[cfg(not(target_os = "windows"))]
                 {
-                    let _ = preset;
+                    let _ = (preset, profile_selection);
                 }
             }
-            AppEvent::BeginWindowsSandboxLegacySetup { preset } => {
+            AppEvent::BeginWindowsSandboxLegacySetup {
+                preset,
+                profile_selection,
+            } => {
                 #[cfg(target_os = "windows")]
                 {
                     let permission_profile = preset.permission_profile.clone();
@@ -986,7 +1013,10 @@ impl App {
                             );
                         })
                     else {
-                        tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset });
+                        tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {
+                            preset,
+                            profile_selection,
+                        });
                         return Ok(AppRunControl::Continue);
                     };
                     tokio::task::spawn_blocking(move || {
@@ -1012,12 +1042,13 @@ impl App {
                         tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
                             preset,
                             mode: WindowsSandboxEnableMode::Legacy,
+                            profile_selection,
                         });
                     });
                 }
                 #[cfg(not(target_os = "windows"))]
                 {
-                    let _ = preset;
+                    let _ = (preset, profile_selection);
                 }
             }
             AppEvent::BeginWindowsSandboxGrantReadRoot { path } => {
@@ -1080,7 +1111,11 @@ impl App {
                         ));
                 }
             },
-            AppEvent::EnableWindowsSandboxForAgentMode { preset, mode } => {
+            AppEvent::EnableWindowsSandboxForAgentMode {
+                preset,
+                mode,
+                profile_selection,
+            } => {
                 #[cfg(target_os = "windows")]
                 {
                     self.chat_widget.clear_windows_sandbox_setup_status();
@@ -1140,11 +1175,40 @@ impl App {
                                 self.app_event_tx.send(
                                     AppEvent::OpenWorldWritableWarningConfirmation {
                                         preset: Some(preset.clone()),
+                                        profile_selection: profile_selection.clone(),
                                         sample_paths,
                                         extra_count,
                                         failed_scan,
                                     },
                                 );
+                            } else if let Some(selection) = profile_selection {
+                                self.app_event_tx.send(AppEvent::CodexOp(
+                                    AppCommand::override_turn_context(
+                                        /*cwd*/ None,
+                                        /*approval_policy*/ None,
+                                        /*approvals_reviewer*/ None,
+                                        /*permission_profile*/ None,
+                                        /*active_permission_profile*/ None,
+                                        #[cfg(target_os = "windows")]
+                                        Some(windows_sandbox_level),
+                                        /*model*/ None,
+                                        /*effort*/ None,
+                                        /*summary*/ None,
+                                        /*service_tier*/ None,
+                                        /*collaboration_mode*/ None,
+                                        /*personality*/ None,
+                                    ),
+                                ));
+                                self.apply_permission_profile_selection(selection).await;
+                                let _ = mode;
+                                self.chat_widget.add_plain_history_lines(vec![
+                                    Line::from(vec!["• ".dim(), "Sandbox ready".into()]),
+                                    Line::from(vec![
+                                        "  ".into(),
+                                        "Codex can now safely edit files and execute commands in your computer"
+                                            .dark_gray(),
+                                    ]),
+                                ]);
                             } else {
                                 self.app_event_tx.send(AppEvent::CodexOp(
                                     AppCommand::override_turn_context(
@@ -1194,7 +1258,7 @@ impl App {
                 }
                 #[cfg(not(target_os = "windows"))]
                 {
-                    let _ = (preset, mode);
+                    let _ = (preset, mode, profile_selection);
                 }
             }
             AppEvent::PersistModelSelection { model, effort } => {
@@ -1461,7 +1525,7 @@ impl App {
                     return Ok(AppRunControl::Continue);
                 }
                 self.runtime_permission_profile_override =
-                    Some(self.config.permissions.permission_profile().clone());
+                    Some(RuntimePermissionProfileOverride::from_config(&self.config));
                 self.sync_active_thread_permission_settings_to_cached_session()
                     .await;
 
@@ -1497,6 +1561,9 @@ impl App {
                     }
                 }
             }
+            AppEvent::SelectPermissionProfile(selection) => {
+                self.apply_permission_profile_selection(selection).await;
+            }
             AppEvent::UpdateApprovalsReviewer(policy) => {
                 self.config.approvals_reviewer = policy;
                 self.chat_widget.set_approvals_reviewer(policy);
diff --git a/codex-rs/tui/src/app/platform_actions.rs b/codex-rs/tui/src/app/platform_actions.rs
index 11289bc2aae..c87776f48b1 100644
--- a/codex-rs/tui/src/app/platform_actions.rs
+++ b/codex-rs/tui/src/app/platform_actions.rs
@@ -47,6 +47,7 @@ impl App {
 fn send_world_writable_scan_failed(tx: &AppEventSender) {
     tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
         preset: None,
+        profile_selection: None,
         sample_paths: Vec::new(),
         extra_count: 0usize,
         failed_scan: true,
diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs
index 92e824aec72..6a99d599827 100644
--- a/codex-rs/tui/src/app/tests.rs
+++ b/codex-rs/tui/src/app/tests.rs
@@ -80,6 +80,7 @@ use codex_protocol::config_types::CollaborationMode;
 use codex_protocol::config_types::CollaborationModeMask;
 use codex_protocol::config_types::ModeKind;
 use codex_protocol::config_types::Personality;
+use codex_protocol::config_types::SandboxMode;
 use codex_protocol::config_types::ServiceTier;
 use codex_protocol::config_types::Settings;
 use codex_protocol::models::ActivePermissionProfile;
@@ -1627,6 +1628,89 @@ async fn reset_memories_clears_local_memory_directories() -> Result<()> {
     .await
 }
 
+#[tokio::test]
+async fn apply_permission_profile_selection_ignores_legacy_harness_overrides() -> Result<()> {
+    let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
+    let codex_home = tempdir()?;
+    std::fs::write(
+        codex_home.path().join("config.toml"),
+        r#"
+default_permissions = "locked-down"
+
+[permissions.locked-down.filesystem]
+":minimal" = "read"
+"#,
+    )?;
+    app.config.codex_home = codex_home.path().to_path_buf().abs();
+    app.harness_overrides.sandbox_mode = Some(SandboxMode::WorkspaceWrite);
+    app.harness_overrides.permission_profile = Some(PermissionProfile::workspace_write());
+
+    assert!(
+        app.apply_permission_profile_selection(PermissionProfileSelection {
+            profile_id: "locked-down".to_string(),
+            approval_policy: None,
+            approvals_reviewer: None,
+            display_label: "locked-down".to_string(),
+        })
+        .await
+    );
+
+    assert_eq!(
+        app.config
+            .permissions
+            .active_permission_profile()
+            .as_ref()
+            .map(|profile| profile.id.as_str()),
+        Some("locked-down")
+    );
+    assert_eq!(
+        app.chat_widget
+            .config_ref()
+            .permissions
+            .active_permission_profile()
+            .as_ref()
+            .map(|profile| profile.id.as_str()),
+        Some("locked-down")
+    );
+    assert_eq!(
+        app.runtime_permission_profile_override,
+        Some(RuntimePermissionProfileOverride::from_config(&app.config))
+    );
+    let op = match app_event_rx.try_recv() {
+        Ok(AppEvent::CodexOp(op)) => op,
+        other => panic!("expected CodexOp event, got {other:?}"),
+    };
+    assert_eq!(
+        op,
+        Op::OverrideTurnContext {
+            cwd: None,
+            approval_policy: None,
+            approvals_reviewer: None,
+            permission_profile: Some(app.config.permissions.permission_profile().clone()),
+            active_permission_profile: app.config.permissions.active_permission_profile(),
+            windows_sandbox_level: None,
+            model: None,
+            effort: None,
+            summary: None,
+            service_tier: None,
+            collaboration_mode: None,
+            personality: None,
+        }
+    );
+    let cell = match app_event_rx.try_recv() {
+        Ok(AppEvent::InsertHistoryCell(cell)) => cell,
+        other => panic!("expected InsertHistoryCell event, got {other:?}"),
+    };
+    let rendered = cell
+        .display_lines(/*width*/ 120)
+        .into_iter()
+        .map(|line| line.to_string())
+        .collect::<Vec<_>>()
+        .join("\n");
+    assert!(rendered.contains("Permissions updated to locked-down"));
+    Ok(())
+}
+
 #[tokio::test]
 async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<()> {
     let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
@@ -1687,7 +1771,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
     assert_eq!(app.runtime_approval_policy_override, None);
     assert_eq!(
         app.runtime_permission_profile_override,
-        Some(auto_review.permission_profile())
+        Some(RuntimePermissionProfileOverride::from_config(&app.config))
     );
     assert_eq!(
         op_rx.try_recv(),
diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs
index 2c2baf512dd..8f15aed79a3 100644
--- a/codex-rs/tui/src/app/thread_routing.rs
+++ b/codex-rs/tui/src/app/thread_routing.rs
@@ -591,7 +591,9 @@ impl App {
                     let permissions_override = Self::turn_permissions_override_from_config(
                         config,
                         active_permission_profile.as_ref(),
-                        self.runtime_permission_profile_override.as_ref(),
+                        self.runtime_permission_profile_override
+                            .as_ref()
+                            .map(|profile| &profile.permission_profile),
                     );
                     app_server
                         .turn_start(
diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
index a85ee5a70c9..17b749971a2 100644
--- a/codex-rs/tui/src/app_event.rs
+++ b/codex-rs/tui/src/app_event.rs
@@ -686,6 +686,7 @@ pub(crate) enum AppEvent {
     OpenFullAccessConfirmation {
         preset: ApprovalPreset,
         return_to_permissions: bool,
+        profile_selection: Option<PermissionProfileSelection>,
     },
 
     /// Open the Windows world-writable directories warning.
@@ -695,6 +696,7 @@ pub(crate) enum AppEvent {
     #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
     OpenWorldWritableWarningConfirmation {
         preset: Option<ApprovalPreset>,
+        profile_selection: Option<PermissionProfileSelection>,
         /// Up to 3 sample world-writable directories to display in the warning.
         sample_paths: Vec<String>,
         /// If there are more than `sample_paths`, this carries the remaining count.
@@ -707,24 +709,28 @@ pub(crate) enum AppEvent {
     #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
     OpenWindowsSandboxEnablePrompt {
         preset: ApprovalPreset,
+        profile_selection: Option<PermissionProfileSelection>,
     },
 
     /// Open the Windows sandbox fallback prompt after declining or failing elevation.
     #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
     OpenWindowsSandboxFallbackPrompt {
         preset: ApprovalPreset,
+        profile_selection: Option<PermissionProfileSelection>,
     },
 
     /// Begin the elevated Windows sandbox setup flow.
     #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
     BeginWindowsSandboxElevatedSetup {
         preset: ApprovalPreset,
+        profile_selection: Option<PermissionProfileSelection>,
     },
 
     /// Begin the non-elevated Windows sandbox setup flow.
     #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
     BeginWindowsSandboxLegacySetup {
         preset: ApprovalPreset,
+        profile_selection: Option<PermissionProfileSelection>,
     },
 
     /// Begin a non-elevated grant of read access for an additional directory.
@@ -745,6 +751,7 @@ pub(crate) enum AppEvent {
     EnableWindowsSandboxForAgentMode {
         preset: ApprovalPreset,
         mode: WindowsSandboxEnableMode,
+        profile_selection: Option<PermissionProfileSelection>,
     },
 
     /// Update the Windows sandbox feature mode without changing approval presets.
@@ -756,6 +763,9 @@ pub(crate) enum AppEvent {
     /// Update the current built-in active permission profile in the running app and widget.
     UpdateActivePermissionProfile(ActivePermissionProfile),
 
+    /// Select a named permission profile, optionally applying built-in mode settings too.
+    SelectPermissionProfile(PermissionProfileSelection),
+
     /// Update the current approvals reviewer in the running app and widget.
     UpdateApprovalsReviewer(ApprovalsReviewer),
 
@@ -995,6 +1005,15 @@ pub(crate) enum AppEvent {
     },
 }
 
+/// Named profile selection to apply after any required UI guardrails complete.
+#[derive(Debug, Clone)]
+pub(crate) struct PermissionProfileSelection {
+    pub profile_id: String,
+    pub approval_policy: Option<AskForApproval>,
+    pub approvals_reviewer: Option<ApprovalsReviewer>,
+    pub display_label: String,
+}
+
 #[derive(Debug)]
 pub(crate) struct RealtimeWebrtcOffer {
     pub(crate) offer_sdp: String,
diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs
index dc5d6c1870d..d908cf67750 100644
--- a/codex-rs/tui/src/chatwidget/permission_popups.rs
+++ b/codex-rs/tui/src/chatwidget/permission_popups.rs
@@ -55,7 +55,6 @@ impl ChatWidget {
             } else {
                 preset.label.to_string()
             };
-            let preset_approval = AskForApproval::from(preset.approval);
             let base_description =
                 Some(preset.description.replace(" (Identical to Agent mode)", ""));
             let approval_disabled_reason = match self
@@ -70,86 +69,13 @@ impl ChatWidget {
             let default_disabled_reason = approval_disabled_reason
                 .clone()
                 .or_else(|| guardian_disabled_reason(false));
-            let requires_confirmation = preset.id == "full-access"
-                && !self
-                    .config
-                    .notices
-                    .hide_full_access_warning
-                    .unwrap_or(false);
-            let default_actions: Vec<SelectionAction> = if requires_confirmation {
-                let preset_clone = preset.clone();
-                vec![Box::new(move |tx| {
-                    tx.send(AppEvent::OpenFullAccessConfirmation {
-                        preset: preset_clone.clone(),
-                        return_to_permissions: !include_read_only,
-                    });
-                })]
-            } else if preset.id == "auto" {
-                #[cfg(target_os = "windows")]
-                {
-                    if WindowsSandboxLevel::from_config(&self.config)
-                        == WindowsSandboxLevel::Disabled
-                    {
-                        let preset_clone = 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(),
-                            )
-                        {
-                            vec![Box::new(move |tx| {
-                                tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
-                                    preset: preset_clone.clone(),
-                                    mode: WindowsSandboxEnableMode::Elevated,
-                                });
-                            })]
-                        } else {
-                            vec![Box::new(move |tx| {
-                                tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
-                                    preset: preset_clone.clone(),
-                                });
-                            })]
-                        }
-                    } else if let Some((sample_paths, extra_count, failed_scan)) =
-                        self.world_writable_warning_details()
-                    {
-                        let preset_clone = preset.clone();
-                        vec![Box::new(move |tx| {
-                            tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
-                                preset: Some(preset_clone.clone()),
-                                sample_paths: sample_paths.clone(),
-                                extra_count,
-                                failed_scan,
-                            });
-                        })]
-                    } else {
-                        Self::approval_preset_actions(
-                            preset_approval,
-                            preset.permission_profile.clone(),
-                            preset.active_permission_profile.clone(),
-                            base_name.clone(),
-                            ApprovalsReviewer::User,
-                        )
-                    }
-                }
-                #[cfg(not(target_os = "windows"))]
-                {
-                    Self::approval_preset_actions(
-                        preset_approval,
-                        preset.permission_profile.clone(),
-                        preset.active_permission_profile.clone(),
-                        base_name.clone(),
-                        ApprovalsReviewer::User,
-                    )
-                }
-            } else {
-                Self::approval_preset_actions(
-                    preset_approval,
-                    preset.permission_profile.clone(),
-                    preset.active_permission_profile.clone(),
-                    base_name.clone(),
-                    ApprovalsReviewer::User,
-                )
-            };
+            let default_actions = self.permission_mode_actions(
+                &preset,
+                base_name.clone(),
+                ApprovalsReviewer::User,
+                /*profile_selection*/ None,
+                /*return_to_permissions*/ !include_read_only,
+            );
             if preset.id == "auto" {
                 items.push(SelectionItem {
                     name: base_name.clone(),
@@ -170,10 +96,7 @@ impl ChatWidget {
                 if guardian_approval_enabled {
                     items.push(SelectionItem {
                         name: "Auto-review".to_string(),
-                        description: Some(
-                            "Same workspace-write permissions as Default, but eligible `on-request` approvals are routed through the auto-reviewer subagent."
-                                .to_string(),
-                        ),
+                        description: Some(AUTO_REVIEW_DESCRIPTION.to_string()),
                         is_current: current_review_policy == ApprovalsReviewer::AutoReview
                             && Self::preset_matches_current(
                                 current_approval,
@@ -181,12 +104,12 @@ impl ChatWidget {
                                 self.config.cwd.as_path(),
                                 &preset,
                             ),
-                        actions: Self::approval_preset_actions(
-                            preset_approval,
-                            preset.permission_profile.clone(),
-                            preset.active_permission_profile.clone(),
+                        actions: self.permission_mode_actions(
+                            &preset,
                             "Auto-review".to_string(),
                             ApprovalsReviewer::AutoReview,
+                            /*profile_selection*/ None,
+                            /*return_to_permissions*/ !include_read_only,
                         ),
                         dismiss_on_select: true,
                         disabled_reason: approval_disabled_reason
@@ -346,6 +269,97 @@ impl ChatWidget {
         })]
     }
 
+    pub(super) fn permission_profile_selection_actions(
+        selection: PermissionProfileSelection,
+    ) -> Vec<SelectionAction> {
+        vec![Box::new(move |tx| {
+            tx.send(AppEvent::SelectPermissionProfile(selection.clone()));
+        })]
+    }
+
+    pub(super) fn permission_mode_actions(
+        &self,
+        preset: &ApprovalPreset,
+        label: String,
+        approvals_reviewer: ApprovalsReviewer,
+        profile_selection: Option<PermissionProfileSelection>,
+        return_to_permissions: bool,
+    ) -> Vec<SelectionAction> {
+        let apply_actions = || {
+            profile_selection.clone().map_or_else(
+                || {
+                    Self::approval_preset_actions(
+                        AskForApproval::from(preset.approval),
+                        preset.permission_profile.clone(),
+                        preset.active_permission_profile.clone(),
+                        label.clone(),
+                        approvals_reviewer,
+                    )
+                },
+                Self::permission_profile_selection_actions,
+            )
+        };
+        let requires_confirmation = approvals_reviewer == ApprovalsReviewer::User
+            && preset.id == "full-access"
+            && !self
+                .config
+                .notices
+                .hide_full_access_warning
+                .unwrap_or(false);
+        if requires_confirmation {
+            let preset = preset.clone();
+            return vec![Box::new(move |tx| {
+                tx.send(AppEvent::OpenFullAccessConfirmation {
+                    preset: preset.clone(),
+                    return_to_permissions,
+                    profile_selection: profile_selection.clone(),
+                });
+            })];
+        }
+        if approvals_reviewer == ApprovalsReviewer::User && preset.id == "auto" {
+            #[cfg(target_os = "windows")]
+            {
+                if WindowsSandboxLevel::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(),
+                        )
+                    {
+                        return vec![Box::new(move |tx| {
+                            tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
+                                preset: preset.clone(),
+                                mode: WindowsSandboxEnableMode::Elevated,
+                                profile_selection: profile_selection.clone(),
+                            });
+                        })];
+                    }
+                    return vec![Box::new(move |tx| {
+                        tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
+                            preset: preset.clone(),
+                            profile_selection: profile_selection.clone(),
+                        });
+                    })];
+                }
+                if let Some((sample_paths, extra_count, failed_scan)) =
+                    self.world_writable_warning_details()
+                {
+                    let preset = preset.clone();
+                    return vec![Box::new(move |tx| {
+                        tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
+                            preset: Some(preset.clone()),
+                            profile_selection: profile_selection.clone(),
+                            sample_paths: sample_paths.clone(),
+                            extra_count,
+                            failed_scan,
+                        });
+                    })];
+                }
+            }
+        }
+        apply_actions()
+    }
+
     pub(super) fn preset_matches_current(
         current_approval: AskForApproval,
         current_permission_profile: &PermissionProfile,
@@ -389,6 +403,7 @@ impl ChatWidget {
         &mut self,
         preset: ApprovalPreset,
         return_to_permissions: bool,
+        profile_selection: Option<PermissionProfileSelection>,
     ) {
         let selected_name = preset.label.to_string();
         let approval = AskForApproval::from(preset.approval);
@@ -406,23 +421,33 @@ impl ChatWidget {
         ));
         let header = ColumnRenderable::with(header_children);
 
-        let mut accept_actions = Self::approval_preset_actions(
-            approval,
-            preset.permission_profile.clone(),
-            preset.active_permission_profile.clone(),
-            selected_name.clone(),
-            ApprovalsReviewer::User,
+        let mut accept_actions = profile_selection.clone().map_or_else(
+            || {
+                Self::approval_preset_actions(
+                    approval,
+                    preset.permission_profile.clone(),
+                    preset.active_permission_profile.clone(),
+                    selected_name.clone(),
+                    ApprovalsReviewer::User,
+                )
+            },
+            Self::permission_profile_selection_actions,
         );
         accept_actions.push(Box::new(|tx| {
             tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
         }));
 
-        let mut accept_and_remember_actions = Self::approval_preset_actions(
-            approval,
-            preset.permission_profile,
-            preset.active_permission_profile,
-            selected_name,
-            ApprovalsReviewer::User,
+        let mut accept_and_remember_actions = profile_selection.map_or_else(
+            || {
+                Self::approval_preset_actions(
+                    approval,
+                    preset.permission_profile,
+                    preset.active_permission_profile,
+                    selected_name,
+                    ApprovalsReviewer::User,
+                )
+            },
+            Self::permission_profile_selection_actions,
         );
         accept_and_remember_actions.push(Box::new(|tx| {
             tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs
index 0df36f6bf89..cf1d902017c 100644
--- a/codex-rs/tui/src/chatwidget/settings.rs
+++ b/codex-rs/tui/src/chatwidget/settings.rs
@@ -30,6 +30,28 @@ impl ChatWidget {
         Ok(())
     }
 
+    pub(crate) fn set_permission_profile_with_active_profile(
+        &mut self,
+        profile: PermissionProfile,
+        active_permission_profile: Option<ActivePermissionProfile>,
+    ) -> ConstraintResult<()> {
+        self.config
+            .permissions
+            .set_permission_profile_from_session_snapshot(
+                PermissionProfileSnapshot::from_session_snapshot(
+                    profile,
+                    active_permission_profile,
+                ),
+            )
+    }
+
+    pub(crate) fn set_permission_network(
+        &mut self,
+        network: Option<crate::legacy_core::config::NetworkProxySpec>,
+    ) {
+        self.config.permissions.network = network;
+    }
+
     #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
     pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option<WindowsSandboxModeToml>) {
         self.config.permissions.windows_sandbox_mode = mode;
diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs
index 3b71d467bf2..dd90af0557b 100644
--- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs
+++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs
@@ -38,6 +38,7 @@ impl ChatWidget {
     pub(crate) fn open_world_writable_warning_confirmation(
         &mut self,
         preset: Option<ApprovalPreset>,
+        profile_selection: Option<PermissionProfileSelection>,
         sample_paths: Vec<String>,
         extra_count: usize,
         failed_scan: bool,
@@ -111,7 +112,9 @@ impl ChatWidget {
                 tx.send(AppEvent::SkipNextWorldWritableScan);
             }));
         }
-        if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) = (
+        if let Some(selection) = profile_selection.clone() {
+            accept_actions.extend(Self::permission_profile_selection_actions(selection));
+        } else if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) = (
             approval,
             permission_profile.clone(),
             active_permission_profile.clone(),
@@ -130,7 +133,10 @@ impl ChatWidget {
             tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));
             tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
         }));
-        if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) =
+        if let Some(selection) = profile_selection {
+            accept_and_remember_actions
+                .extend(Self::permission_profile_selection_actions(selection));
+        } else if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) =
             (approval, permission_profile, active_permission_profile)
         {
             accept_and_remember_actions.extend(Self::approval_preset_actions(
@@ -171,6 +177,7 @@ impl ChatWidget {
     pub(crate) fn open_world_writable_warning_confirmation(
         &mut self,
         _preset: Option<ApprovalPreset>,
+        _profile_selection: Option<PermissionProfileSelection>,
         _sample_paths: Vec<String>,
         _extra_count: usize,
         _failed_scan: bool,
@@ -178,7 +185,11 @@ impl ChatWidget {
     }
 
     #[cfg(target_os = "windows")]
-    pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) {
+    pub(crate) fn open_windows_sandbox_enable_prompt(
+        &mut self,
+        preset: ApprovalPreset,
+        profile_selection: Option<PermissionProfileSelection>,
+    ) {
         use ratatui_macros::line;
 
         if !crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED {
@@ -202,6 +213,7 @@ impl ChatWidget {
                         tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
                             preset: preset_clone.clone(),
                             mode: WindowsSandboxEnableMode::Legacy,
+                            profile_selection: profile_selection.clone(),
                         });
                     })],
                     dismiss_on_select: true,
@@ -245,6 +257,7 @@ impl ChatWidget {
         let accept_otel = self.session_telemetry.clone();
         let legacy_otel = self.session_telemetry.clone();
         let legacy_preset = preset.clone();
+        let legacy_profile_selection = profile_selection.clone();
         let quit_otel = self.session_telemetry.clone();
         let items = vec![
             SelectionItem {
@@ -258,6 +271,7 @@ impl ChatWidget {
                     );
                     tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
                         preset: preset.clone(),
+                        profile_selection: profile_selection.clone(),
                     });
                 })],
                 dismiss_on_select: true,
@@ -274,6 +288,7 @@ impl ChatWidget {
                     );
                     tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
                         preset: legacy_preset.clone(),
+                        profile_selection: legacy_profile_selection.clone(),
                     });
                 })],
                 dismiss_on_select: true,
@@ -305,10 +320,19 @@ impl ChatWidget {
     }
 
     #[cfg(not(target_os = "windows"))]
-    pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {}
+    pub(crate) fn open_windows_sandbox_enable_prompt(
+        &mut self,
+        _preset: ApprovalPreset,
+        _profile_selection: Option<PermissionProfileSelection>,
+    ) {
+    }
 
     #[cfg(target_os = "windows")]
-    pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: ApprovalPreset) {
+    pub(crate) fn open_windows_sandbox_fallback_prompt(
+        &mut self,
+        preset: ApprovalPreset,
+        profile_selection: Option<PermissionProfileSelection>,
+    ) {
         use ratatui_macros::line;
 
         let mut lines = Vec::new();
@@ -328,6 +352,8 @@ impl ChatWidget {
 
         let elevated_preset = preset.clone();
         let legacy_preset = preset;
+        let elevated_profile_selection = profile_selection.clone();
+        let legacy_profile_selection = profile_selection;
         let quit_otel = self.session_telemetry.clone();
         let items = vec![
             SelectionItem {
@@ -344,6 +370,7 @@ impl ChatWidget {
                         );
                         tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
                             preset: preset.clone(),
+                            profile_selection: elevated_profile_selection.clone(),
                         });
                     }
                 })],
@@ -364,6 +391,7 @@ impl ChatWidget {
                         );
                         tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
                             preset: preset.clone(),
+                            profile_selection: legacy_profile_selection.clone(),
                         });
                     }
                 })],
@@ -396,7 +424,12 @@ impl ChatWidget {
     }
 
     #[cfg(not(target_os = "windows"))]
-    pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: ApprovalPreset) {}
+    pub(crate) fn open_windows_sandbox_fallback_prompt(
+        &mut self,
+        _preset: ApprovalPreset,
+        _profile_selection: Option<PermissionProfileSelection>,
+    ) {
+    }
 
     #[cfg(target_os = "windows")]
     pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) {
@@ -406,7 +439,7 @@ impl ChatWidget {
                 .into_iter()
                 .find(|preset| preset.id == "auto")
         {
-            self.open_windows_sandbox_enable_prompt(preset);
+            self.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);
         }
     }
 

From f5ab488cb7ac75dd71d5e4cb53b88f4f7ed089fa Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Wed, 20 May 2026 08:49:54 -0700
Subject: [PATCH 13/15] fix(tui): keep profile picker symbols in split base

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/tui/src/chatwidget.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 0ba974d88e4..1b24daac26c 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -256,6 +256,7 @@ fn queued_message_edit_hint_binding(
 
 use crate::app_event::AppEvent;
 use crate::app_event::ExitMode;
+use crate::app_event::PermissionProfileSelection;
 use crate::app_event::RateLimitRefreshOrigin;
 #[cfg(target_os = "windows")]
 use crate::app_event::WindowsSandboxEnableMode;
@@ -459,6 +460,7 @@ use unicode_segmentation::UnicodeSegmentation;
 
 const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally";
 const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls";
+const AUTO_REVIEW_DESCRIPTION: &str = "Same workspace-write permissions as Default, but eligible `on-request` approvals are routed through the auto-reviewer subagent.";
 const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
 const DEFAULT_STATUS_LINE_ITEMS: [&str; 2] = ["model-with-reasoning", "current-dir"];
 const MAX_AGENT_COPY_HISTORY: usize = 32;

From 82e21da745c75bfecb474df50650297204daf547 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Wed, 20 May 2026 10:42:39 -0700
Subject: [PATCH 14/15] fix(tui): address permission profile review feedback

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/tui/src/app/config_persistence.rs    | 18 ++++++++++
 codex-rs/tui/src/app/event_dispatch.rs        | 34 +++++++++++++++++--
 codex-rs/tui/src/app/tests.rs                 |  6 ++--
 codex-rs/tui/src/chatwidget/settings.rs       |  4 ++-
 .../tui/src/chatwidget/tests/permissions.rs   | 13 ++++---
 5 files changed, 65 insertions(+), 10 deletions(-)

diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs
index cdcce127b13..8d6d392ec4c 100644
--- a/codex-rs/tui/src/app/config_persistence.rs
+++ b/codex-rs/tui/src/app/config_persistence.rs
@@ -34,6 +34,7 @@ impl App {
             .codex_home(self.config.codex_home.to_path_buf())
             .cli_overrides(self.cli_kv_overrides.clone())
             .harness_overrides(overrides)
+            .loader_overrides(self.loader_overrides.clone())
             .build()
             .await
             .wrap_err_with(|| {
@@ -41,6 +42,23 @@ impl App {
             })
     }
 
+    #[cfg(target_os = "windows")]
+    pub(super) async fn permission_profile_for_windows_setup(
+        &self,
+        preset: &ApprovalPreset,
+        profile_selection: Option<&PermissionProfileSelection>,
+    ) -> Result<PermissionProfile> {
+        match profile_selection {
+            Some(selection) => Ok(self
+                .rebuild_config_for_permission_profile(selection.profile_id.as_str())
+                .await?
+                .permissions
+                .permission_profile()
+                .clone()),
+            None => Ok(preset.permission_profile.clone()),
+        }
+    }
+
     pub(super) async fn apply_permission_profile_selection(
         &mut self,
         selection: PermissionProfileSelection,
diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs
index 61f1fe5fcdb..cd6bd0b5c91 100644
--- a/codex-rs/tui/src/app/event_dispatch.rs
+++ b/codex-rs/tui/src/app/event_dispatch.rs
@@ -885,7 +885,22 @@ impl App {
             } => {
                 #[cfg(target_os = "windows")]
                 {
-                    let permission_profile = preset.permission_profile.clone();
+                    let permission_profile = match self
+                        .permission_profile_for_windows_setup(&preset, profile_selection.as_ref())
+                        .await
+                    {
+                        Ok(permission_profile) => permission_profile,
+                        Err(err) => {
+                            tracing::warn!(
+                                error = %err,
+                                "failed to resolve permission profile for elevated Windows sandbox setup"
+                            );
+                            self.chat_widget.add_error_message(format!(
+                                "Failed to prepare Windows sandbox for the selected permission profile: {err}"
+                            ));
+                            return Ok(AppRunControl::Continue);
+                        }
+                    };
                     let policy_cwd = self.config.cwd.clone();
                     let command_cwd = policy_cwd.clone();
                     let env_map: std::collections::HashMap<String, String> =
@@ -994,7 +1009,22 @@ impl App {
             } => {
                 #[cfg(target_os = "windows")]
                 {
-                    let permission_profile = preset.permission_profile.clone();
+                    let permission_profile = match self
+                        .permission_profile_for_windows_setup(&preset, profile_selection.as_ref())
+                        .await
+                    {
+                        Ok(permission_profile) => permission_profile,
+                        Err(err) => {
+                            tracing::warn!(
+                                error = %err,
+                                "failed to resolve permission profile for legacy Windows sandbox setup"
+                            );
+                            self.chat_widget.add_error_message(format!(
+                                "Failed to prepare Windows sandbox for the selected permission profile: {err}"
+                            ));
+                            return Ok(AppRunControl::Continue);
+                        }
+                    };
                     let policy_cwd = self.config.cwd.clone();
                     let command_cwd = policy_cwd.clone();
                     let env_map: std::collections::HashMap<String, String> =
diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs
index 6a99d599827..c0050db2858 100644
--- a/codex-rs/tui/src/app/tests.rs
+++ b/codex-rs/tui/src/app/tests.rs
@@ -1629,11 +1629,12 @@ async fn reset_memories_clears_local_memory_directories() -> Result<()> {
 }
 
 #[tokio::test]
-async fn apply_permission_profile_selection_ignores_legacy_harness_overrides() -> Result<()> {
+async fn apply_permission_profile_selection_preserves_loader_overrides() -> Result<()> {
     let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
     let codex_home = tempdir()?;
+    let selected_config = codex_home.path().join("work.config.toml");
     std::fs::write(
-        codex_home.path().join("config.toml"),
+        &selected_config,
         r#"
 default_permissions = "locked-down"
 
@@ -1642,6 +1643,7 @@ default_permissions = "locked-down"
 "#,
     )?;
     app.config.codex_home = codex_home.path().to_path_buf().abs();
+    app.loader_overrides.user_config_path = Some(selected_config.abs());
     app.harness_overrides.sandbox_mode = Some(SandboxMode::WorkspaceWrite);
     app.harness_overrides.permission_profile = Some(PermissionProfile::workspace_write());
 
diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs
index cf1d902017c..eb3fc4eefd9 100644
--- a/codex-rs/tui/src/chatwidget/settings.rs
+++ b/codex-rs/tui/src/chatwidget/settings.rs
@@ -42,7 +42,9 @@ impl ChatWidget {
                     profile,
                     active_permission_profile,
                 ),
-            )
+            )?;
+        self.refresh_status_surfaces();
+        Ok(())
     }
 
     pub(crate) fn set_permission_network(
diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs
index ae58271dff3..df59db17cbf 100644
--- a/codex-rs/tui/src/chatwidget/tests/permissions.rs
+++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs
@@ -166,7 +166,9 @@ async fn full_access_confirmation_popup_snapshot() {
         .into_iter()
         .find(|preset| preset.id == "full-access")
         .expect("full access preset");
-    chat.open_full_access_confirmation(preset, /*return_to_permissions*/ false);
+    chat.open_full_access_confirmation(
+        preset, /*return_to_permissions*/ false, /*profile_selection*/ None,
+    );
 
     let popup = render_bottom_popup(&chat, /*width*/ 80);
     assert_chatwidget_snapshot!("full_access_confirmation_popup", popup);
@@ -181,7 +183,7 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
         .into_iter()
         .find(|preset| preset.id == "auto")
         .expect("auto preset");
-    chat.open_windows_sandbox_enable_prompt(preset);
+    chat.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);
 
     let popup = render_bottom_popup(&chat, /*width*/ 120);
     assert!(
@@ -799,8 +801,9 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation()
             AppEvent::OpenFullAccessConfirmation {
                 preset,
                 return_to_permissions,
+                profile_selection,
             } => {
-                open_confirmation_event = Some((preset, return_to_permissions));
+                open_confirmation_event = Some((preset, return_to_permissions, profile_selection));
             }
             _ => {}
         }
@@ -811,9 +814,9 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation()
             "did not expect history cell before confirming full access"
         );
     }
-    let (preset, return_to_permissions) =
+    let (preset, return_to_permissions, profile_selection) =
         open_confirmation_event.expect("expected full access confirmation event");
-    chat.open_full_access_confirmation(preset, return_to_permissions);
+    chat.open_full_access_confirmation(preset, return_to_permissions, profile_selection);
 
     let popup = render_bottom_popup(&chat, /*width*/ 80);
     assert!(

From 3ee4233c79fd8b10fa3b4dd88cc1c00b511b4256 Mon Sep 17 00:00:00 2001
From: viyatb-oai <viyatb@openai.com>
Date: Wed, 20 May 2026 11:18:34 -0700
Subject: [PATCH 15/15] fix(tui): repair windows permission setup build

Co-authored-by: Codex noreply@openai.com
---
 codex-rs/tui/src/app/config_persistence.rs    | 2 ++
 codex-rs/tui/src/chatwidget/slash_dispatch.rs | 5 ++++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs
index 8d6d392ec4c..d4fdebad89d 100644
--- a/codex-rs/tui/src/app/config_persistence.rs
+++ b/codex-rs/tui/src/app/config_persistence.rs
@@ -5,6 +5,8 @@
 //! loop.
 
 use super::*;
+#[cfg(target_os = "windows")]
+use codex_utils_approval_presets::ApprovalPreset;
 
 impl App {
     pub(super) async fn rebuild_config_for_cwd(&self, cwd: PathBuf) -> Result<Config> {
diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs
index aa9e5112f80..b479718304e 100644
--- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs
+++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs
@@ -297,7 +297,10 @@ impl ChatWidget {
                         &[],
                     );
                     self.app_event_tx
-                        .send(AppEvent::BeginWindowsSandboxElevatedSetup { preset });
+                        .send(AppEvent::BeginWindowsSandboxElevatedSetup {
+                            preset,
+                            profile_selection: None,
+                        });
                 }
                 #[cfg(not(target_os = "windows"))]
                 {
