diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 43ad290a45..47092cdeb0 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -234,14 +234,26 @@ impl CodexThread { .await .with_updates(model, effort, /*developer_instructions*/ None) }; + let clear_active_permission_profile = + permission_profile.is_none() && sandbox_policy.is_some(); + let permission_profile = match (permission_profile, sandbox_policy.as_ref()) { + (Some(permission_profile), _) => Some(permission_profile), + (None, Some(sandbox_policy)) => Some( + self.codex + .session + .permission_profile_from_legacy_sandbox_update(sandbox_policy, cwd.as_deref()) + .await, + ), + (None, None) => None, + }; let updates = SessionSettingsUpdate { cwd, approval_policy, approvals_reviewer, - sandbox_policy, permission_profile, active_permission_profile, + clear_active_permission_profile, windows_sandbox_level, collaboration_mode: Some(collaboration_mode), reasoning_summary: summary, diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 624331ea62..1fe52b200a 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -34,6 +34,7 @@ use crate::tasks::execute_user_shell_command; use codex_mcp::collect_mcp_snapshot_from_manager; use codex_mcp::compute_auth_statuses; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; @@ -50,6 +51,7 @@ use codex_protocol::protocol::RealtimeVoicesList; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SkillErrorInfo; use codex_protocol::protocol::SkillsListEntry; use codex_protocol::protocol::ThreadMemoryMode; @@ -71,6 +73,7 @@ use codex_protocol::user_input::UserInput; use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; use serde_json::Value; +use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tracing::debug; @@ -153,15 +156,23 @@ pub(super) async fn user_input_or_turn_inner( }, }) }); + let clear_active_permission_profile = permission_profile.is_none(); + let permission_profile = permission_profile_with_legacy_fallback( + sess, + Some(&sandbox_policy), + permission_profile, + Some(cwd.as_path()), + ) + .await; ( items, SessionSettingsUpdate { cwd: Some(cwd), approval_policy: Some(approval_policy), approvals_reviewer, - sandbox_policy: Some(sandbox_policy), permission_profile, active_permission_profile: None, + clear_active_permission_profile, windows_sandbox_level: None, collaboration_mode, reasoning_summary: summary, @@ -205,15 +216,24 @@ pub(super) async fn user_input_or_turn_inner( .with_updates(model, effort, /*developer_instructions*/ None), ) }; + let clear_active_permission_profile = + permission_profile.is_none() && sandbox_policy.is_some(); + let permission_profile = permission_profile_with_legacy_fallback( + sess, + sandbox_policy.as_ref(), + permission_profile, + cwd.as_deref(), + ) + .await; ( items, SessionSettingsUpdate { cwd, approval_policy, approvals_reviewer, - sandbox_policy, permission_profile, active_permission_profile, + clear_active_permission_profile, windows_sandbox_level, collaboration_mode, reasoning_summary: summary, @@ -294,6 +314,22 @@ pub(super) async fn user_input_or_turn_inner( } } +async fn permission_profile_with_legacy_fallback( + sess: &Session, + sandbox_policy: Option<&SandboxPolicy>, + permission_profile: Option, + cwd: Option<&Path>, +) -> Option { + match (permission_profile, sandbox_policy) { + (Some(permission_profile), _) => Some(permission_profile), + (None, Some(sandbox_policy)) => Some( + sess.permission_profile_from_legacy_sandbox_update(sandbox_policy, cwd) + .await, + ), + (None, None) => None, + } +} + async fn mirror_user_text_to_realtime(sess: &Arc, items: &[UserInput]) { let text = UserMessageItem::new(items).message(); if text.is_empty() { @@ -1039,6 +1075,15 @@ pub(super) async fn submission_loop( /*developer_instructions*/ None, ) }; + let clear_active_permission_profile = + permission_profile.is_none() && sandbox_policy.is_some(); + let permission_profile = permission_profile_with_legacy_fallback( + &sess, + sandbox_policy.as_ref(), + permission_profile, + cwd.as_deref(), + ) + .await; override_turn_context( &sess, sub.id.clone(), @@ -1046,8 +1091,8 @@ pub(super) async fn submission_loop( cwd, approval_policy, approvals_reviewer, - sandbox_policy, permission_profile, + clear_active_permission_profile, windows_sandbox_level, collaboration_mode: Some(collaboration_mode), reasoning_summary: summary, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e45db20848..6993766fb1 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -1354,6 +1354,17 @@ impl Session { state.session_configuration.apply(updates).map(|_| ()) } + pub(crate) async fn permission_profile_from_legacy_sandbox_update( + &self, + sandbox_policy: &SandboxPolicy, + cwd: Option<&Path>, + ) -> PermissionProfile { + let state = self.state.lock().await; + state + .session_configuration + .permission_profile_from_legacy_sandbox_update(sandbox_policy, cwd) + } + pub(crate) async fn set_session_startup_prewarm( &self, startup_prewarm: SessionStartupPrewarmHandle, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index d4305aed44..b906856778 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -126,6 +126,24 @@ impl SessionConfiguration { self.permission_profile.get().network_sandbox_policy() } + pub(super) fn permission_profile_from_legacy_sandbox_update( + &self, + sandbox_policy: &SandboxPolicy, + cwd: Option<&Path>, + ) -> PermissionProfile { + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( + sandbox_policy, + self.resolved_update_cwd(cwd).as_path(), + &self.file_system_sandbox_policy(), + ); + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(sandbox_policy), + ) + } + pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { ThreadConfigSnapshot { model: self.collaboration_mode.model().to_string(), @@ -191,19 +209,7 @@ impl SessionConfiguration { next_configuration.windows_sandbox_level = windows_sandbox_level; } - let absolute_cwd = updates - .cwd - .as_ref() - .map(|cwd| { - AbsolutePathBuf::relative_to_current_dir(normalize_for_native_workdir( - cwd.as_path(), - )) - .unwrap_or_else(|e| { - warn!("failed to normalize update cwd: {cwd:?}: {e}"); - self.cwd.clone() - }) - }) - .unwrap_or_else(|| self.cwd.clone()); + let absolute_cwd = self.resolved_update_cwd(updates.cwd.as_deref()); let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path(); next_configuration.cwd = absolute_cwd.clone(); @@ -214,35 +220,22 @@ impl SessionConfiguration { } if let Some(permission_profile) = updates.permission_profile.clone() { - let active_permission_profile = + let active_permission_profile = if updates.clear_active_permission_profile { + None + } else { updates.active_permission_profile.clone().or_else(|| { if permission_profile == self.permission_profile() { self.active_permission_profile.clone() } else { None } - }); + }) + }; next_configuration.set_permission_profile_projection( permission_profile, Some(¤t_file_system_sandbox_policy), )?; next_configuration.active_permission_profile = active_permission_profile; - } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( - &sandbox_policy, - &next_configuration.cwd, - ¤t_file_system_sandbox_policy, - ); - let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); - next_configuration.permission_profile.set( - PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), - &file_system_sandbox_policy, - network_sandbox_policy, - ), - )?; - next_configuration.active_permission_profile = None; } else if cwd_changed && file_system_policy_matches_legacy && file_system_policy_has_rebindable_project_root_write @@ -273,6 +266,17 @@ impl SessionConfiguration { Ok(next_configuration) } + fn resolved_update_cwd(&self, cwd: Option<&Path>) -> AbsolutePathBuf { + cwd.map(|cwd| { + AbsolutePathBuf::relative_to_current_dir(normalize_for_native_workdir(cwd)) + .unwrap_or_else(|e| { + warn!("failed to normalize update cwd: {cwd:?}: {e}"); + self.cwd.clone() + }) + }) + .unwrap_or_else(|| self.cwd.clone()) + } + fn set_permission_profile_projection( &mut self, permission_profile: PermissionProfile, @@ -301,9 +305,11 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) cwd: Option, pub(crate) approval_policy: Option, pub(crate) approvals_reviewer: Option, - pub(crate) sandbox_policy: Option, pub(crate) permission_profile: Option, pub(crate) active_permission_profile: Option, + /// Legacy sandbox updates are represented as permission profiles before + /// reaching this layer, but they should still clear any named profile. + pub(crate) clear_active_permission_profile: bool, pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option,