From 40ad7be2b5f2684e839328980c2569d6b842ae2f Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 20 May 2026 14:55:21 -0700 Subject: [PATCH] core: refresh active permission profiles at runtime (#22931) ## Why Once a named permission profile is selected, runtime state has to keep that profile identity intact instead of collapsing back to anonymous effective permissions. The session refresh path also needs to rebuild profile-derived network proxy state so active profile switches take effect consistently. ## What changed - Preserve the active permission profile through session updates. - Rebuild profile-derived runtime/network configuration when the active profile changes. - Keep the runtime path aligned with the current session configuration APIs. - Tighten the affected tests, including the Windows delete-pending memory-file case that was intermittently tripping CI. ## Stack 1. **This PR**: runtime/session/network propagation for active permission profiles. 2. [#23708](https://github.com/openai/codex/pull/23708): TUI selection plumbing and guardrail flow. 3. [#21559](https://github.com/openai/codex/pull/21559): profile-aware `/permissions` menu and custom profile display. image --- codex-rs/core/src/config/mod.rs | 104 +++++++++++--- codex-rs/core/src/guardian/review.rs | 3 +- codex-rs/core/src/session/mod.rs | 39 +++++- codex-rs/core/src/session/session.rs | 34 ++++- codex-rs/core/src/session/tests.rs | 131 +++++++++++++++++- 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 | 7 + codex-rs/tui/src/app/thread_settings.rs | 1 + codex-rs/tui/src/app_command.rs | 4 + .../tui/src/chatwidget/permission_popups.rs | 8 ++ codex-rs/tui/src/chatwidget/rate_limits.rs | 1 + codex-rs/tui/src/chatwidget/service_tiers.rs | 1 + codex-rs/tui/src/chatwidget/settings.rs | 1 + .../tui/src/chatwidget/settings_popups.rs | 1 + .../tui/src/chatwidget/tests/permissions.rs | 1 + .../src/chatwidget/windows_sandbox_prompts.rs | 19 ++- 20 files changed, 324 insertions(+), 45 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 434969e2c..ff09f26c2 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>, + permission_profile: &PermissionProfile, +) -> std::io::Result> { + 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,53 @@ 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> { + 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}" + ), + ) + })?; + 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() + }; + + 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 2908492dd..7df7a9692 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, ) -> (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 b710e095e..55fd5444c 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 88958b39e..7ae5ff241 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,33 @@ impl SessionConfiguration { updates.profile_workspace_roots.clone().unwrap_or_default(), Some(¤t_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_from_session_snapshot( + PermissionProfileSnapshot::active( + permission_profile, + 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 +913,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 +1005,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 a08243701..e669becc4 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,114 @@ 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 { + description: None, + extends: None, + workspace_roots: None, + 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 { + description: None, + extends: None, + workspace_roots: None, + 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 { + features: Some(toml::from_str("network_proxy = true").expect("valid features")), + 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().clone()), + 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 +4476,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 +6305,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 3338d0a55..818a8b7bb 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 afba409a0..59def9c7a 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, + pub(crate) network_proxy: ArcSwapOption, + pub(crate) network_proxy_audit_metadata: NetworkProxyAuditMetadata, + pub(crate) managed_network_requirements_configured: bool, pub(crate) network_approval: Arc, pub(crate) state_db: Option, pub(crate) live_thread: Option, diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 80ddfee89..9cb6b5f0f 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 1f594d64c..3955b5ab0 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 ce0eb0ff9..51615f6a9 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 0a2f382a8..92e824aec 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()), 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()), 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()), 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, @@ -5276,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, )), diff --git a/codex-rs/tui/src/app/thread_settings.rs b/codex-rs/tui/src/app/thread_settings.rs index bf9282787..27c435af3 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/app_command.rs b/codex-rs/tui/src/app_command.rs index 0b6484d01..2ecf17f5a 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, approval_policy: Option, approvals_reviewer: Option, + permission_profile: Option, active_permission_profile: Option, windows_sandbox_level: Option, model: Option, @@ -172,6 +174,7 @@ impl AppCommand { cwd: Option, approval_policy: Option, approvals_reviewer: Option, + permission_profile: Option, active_permission_profile: Option, windows_sandbox_level: Option, model: Option, @@ -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/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs index a82f3a6cd..dc5d6c187 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 544611f4f..df45a11c7 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/service_tiers.rs b/codex-rs/tui/src/chatwidget/service_tiers.rs index a60af0a69..fd11048cb 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/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index 0434f7e38..0df36f6bf 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, diff --git a/codex-rs/tui/src/chatwidget/settings_popups.rs b/codex-rs/tui/src/chatwidget/settings_popups.rs index 2dcfefb73..357223a6c 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, diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 71483bd39..ae58271df 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, )), diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index d8ff59da4..3b71d467b 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> = 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,