From 08df3a31f3a68feeeabe7fa253ba32ba2c31ba96 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 22 Apr 2026 22:36:00 -0700 Subject: [PATCH] tui: sync session permission profiles --- codex-rs/tui/src/app/config_persistence.rs | 28 +++++++++--- codex-rs/tui/src/chatwidget.rs | 17 ++++++++ .../src/chatwidget/tests/history_replay.rs | 43 ++++++++++++++++++- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 1ea89d8847..6461a055cd 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -72,13 +72,15 @@ impl App { "Failed to carry forward approval policy override: {err}" )); } - if let Some(policy) = self.runtime_sandbox_policy_override.as_ref() - && let Err(err) = config.permissions.sandbox_policy.set(policy.clone()) - { - tracing::warn!(%err, "failed to carry forward sandbox policy override"); - self.chat_widget.add_error_message(format!( - "Failed to carry forward sandbox policy override: {err}" - )); + if let Some(policy) = self.runtime_sandbox_policy_override.as_ref() { + if let Err(err) = config.permissions.sandbox_policy.set(policy.clone()) { + tracing::warn!(%err, "failed to carry forward sandbox policy override"); + self.chat_widget.add_error_message(format!( + "Failed to carry forward sandbox policy override: {err}" + )); + } else { + sync_runtime_permissions_from_legacy_sandbox_policy(config); + } } } @@ -117,6 +119,7 @@ impl App { .add_error_message(format!("{user_message_prefix}: {err}")); return false; } + sync_runtime_permissions_from_legacy_sandbox_policy(config); true } @@ -534,6 +537,17 @@ impl App { } } +fn sync_runtime_permissions_from_legacy_sandbox_policy(config: &mut Config) { + let sandbox_policy = config.permissions.sandbox_policy.get(); + config.permissions.file_system_sandbox_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + sandbox_policy, + &config.cwd, + ); + config.permissions.network_sandbox_policy = + codex_protocol::permissions::NetworkSandboxPolicy::from(sandbox_policy); +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 91a2e7555e..fe38125b15 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -139,6 +139,7 @@ use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::UserMessageItem; use codex_protocol::models::MessagePhase; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg; @@ -2119,6 +2120,12 @@ impl ChatWidget { display: SessionConfiguredDisplay, fork_parent_title: Option, ) { + let permission_profile = event.permission_profile.clone().unwrap_or_else(|| { + PermissionProfile::from_legacy_sandbox_policy(&event.sandbox_policy, &event.cwd) + }); + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + self.last_agent_markdown = None; self.agent_turn_markdowns.clear(); self.visible_user_turn_count = 0; @@ -2156,6 +2163,8 @@ impl ChatWidget { self.config.permissions.sandbox_policy = Constrained::allow_only(event.sandbox_policy.clone()); } + self.config.permissions.file_system_sandbox_policy = file_system_sandbox_policy; + self.config.permissions.network_sandbox_policy = network_sandbox_policy; self.config.approvals_reviewer = event.approvals_reviewer; self.status_line_project_root_name_cache = None; let forked_from_id = event.forked_from_id; @@ -9764,6 +9773,14 @@ impl ChatWidget { #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { self.config.permissions.sandbox_policy.set(policy)?; + let sandbox_policy = self.config.permissions.sandbox_policy.get(); + self.config.permissions.file_system_sandbox_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + sandbox_policy, + &self.config.cwd, + ); + self.config.permissions.network_sandbox_policy = + codex_protocol::permissions::NetworkSandboxPolicy::from(sandbox_policy); Ok(()) } diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 3e2f5b4d74..4a017a4bcb 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -1,4 +1,10 @@ use super::*; +use codex_protocol::protocol::FileSystemAccessMode; +use codex_protocol::protocol::FileSystemPath; +use codex_protocol::protocol::FileSystemSandboxEntry; +use codex_protocol::protocol::FileSystemSandboxPolicy; +use codex_protocol::protocol::FileSystemSpecialPath; +use codex_protocol::protocol::NetworkSandboxPolicy; use pretty_assertions::assert_eq; #[tokio::test] @@ -252,6 +258,25 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { let expected_sandbox = SandboxPolicy::new_read_only_policy(); let expected_cwd = test_path_buf("/home/user/sub-agent").abs(); + let expected_file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/.secret".to_string(), + }, + access: FileSystemAccessMode::None, + }, + ]); + let expected_permission_profile = + codex_protocol::models::PermissionProfile::from_runtime_permissions( + &expected_file_system_policy, + NetworkSandboxPolicy::Restricted, + ); let configured = codex_protocol::protocol::SessionConfiguredEvent { session_id: ThreadId::new(), forked_from_id: None, @@ -262,7 +287,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: expected_sandbox.clone(), - permission_profile: None, + permission_profile: Some(expected_permission_profile.clone()), cwd: expected_cwd.clone(), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -285,7 +310,23 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { chat.config_ref().permissions.sandbox_policy.get(), &expected_sandbox ); + assert_eq!( + chat.config_ref().permissions.permission_profile(), + expected_permission_profile + ); assert_eq!(&chat.config_ref().cwd, &expected_cwd); + + let updated_sandbox = SandboxPolicy::new_workspace_write_policy(); + chat.set_sandbox_policy(updated_sandbox.clone()) + .expect("set sandbox policy"); + assert_eq!( + chat.config_ref().permissions.permission_profile(), + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &updated_sandbox, + &expected_cwd + ), + "local sandbox changes should replace SessionConfigured profile-derived runtime permissions" + ); } #[tokio::test]