diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index b6e9bfe40f..49aceb6217 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -423,6 +423,7 @@ fn config_summary_entries( config: &Config, session_configured_event: &SessionConfiguredEvent, ) -> Vec<(&'static str, String)> { + let permission_profile = config.permissions.effective_permission_profile(); let mut entries = vec![ ("workdir", config.cwd.display().to_string()), ("model", session_configured_event.model.clone()), @@ -436,10 +437,7 @@ fn config_summary_entries( ), ( "sandbox", - summarize_permission_profile( - &config.permissions.effective_permission_profile(), - config.cwd.as_path(), - ), + summarize_permission_profile(&permission_profile, config.cwd.as_path()), ), ]; if config.model_provider.wire_api == WireApi::Responses { diff --git a/codex-rs/exec/src/event_processor_with_human_output_tests.rs b/codex-rs/exec/src/event_processor_with_human_output_tests.rs index 479758f9a0..845b0001ff 100644 --- a/codex-rs/exec/src/event_processor_with_human_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_human_output_tests.rs @@ -2,18 +2,24 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; +use codex_core::config::ConfigBuilder; +use codex_protocol::SessionId; +use codex_protocol::ThreadId; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::SessionConfiguredEvent; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; use owo_colors::Style; use pretty_assertions::assert_eq; use super::EventProcessorWithHumanOutput; +use super::config_summary_entries; use super::final_message_from_turn_items; use super::paths_match_after_canonicalization; use super::reasoning_text; @@ -162,6 +168,70 @@ fn summarizes_managed_read_only_permission_profile() { ); } +#[tokio::test] +async fn config_summary_entries_include_runtime_workspace_roots() { + let codex_home = tempfile::tempdir().expect("create codex home"); + let cwd = tempfile::tempdir().expect("create cwd"); + let extra_root = tempfile::tempdir().expect("create extra root"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(cwd.path().to_path_buf())) + .build() + .await + .expect("build default config"); + let cwd = cwd.path().to_path_buf().abs(); + let extra_root = extra_root.path().to_path_buf().abs(); + let expected_extra_root = + std::fs::canonicalize(extra_root.as_path()).expect("canonicalize extra root"); + config.cwd = cwd.clone(); + config.workspace_roots = vec![cwd.clone(), extra_root]; + config + .permissions + .set_workspace_roots(config.workspace_roots.clone()); + config + .permissions + .set_permission_profile(PermissionProfile::workspace_write_with( + &[], + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + )) + .expect("set permission profile"); + + let session_configured_event = SessionConfiguredEvent { + session_id: SessionId::new(), + thread_id: ThreadId::new(), + forked_from_id: None, + thread_source: None, + thread_name: None, + model: "gpt-5.4".to_string(), + model_provider_id: config.model_provider_id.clone(), + service_tier: None, + approval_policy: AskForApproval::Never, + approvals_reviewer: config.approvals_reviewer, + permission_profile: config.permissions.effective_permission_profile(), + active_permission_profile: None, + cwd, + reasoning_effort: None, + initial_messages: None, + network_proxy: None, + rollout_path: None, + }; + + let summary_entries = config_summary_entries(&config, &session_configured_event); + assert!( + summary_entries.iter().any(|(key, value)| { + *key == "sandbox" + && *value + == format!( + "workspace-write [workdir, {}]", + expected_extra_root.display() + ) + }), + "expected runtime workspace root in sandbox summary: {summary_entries:?}" + ); +} + #[test] fn distinct_missing_paths_do_not_match_after_canonicalization() { assert!(!paths_match_after_canonicalization( diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 8dd5f327d3..e34fe4688a 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -902,11 +902,9 @@ fn permissions_display(config: &Config) -> String { } let permission_profile = config.permissions.effective_permission_profile(); - let summary = summarize_permission_profile( - &permission_profile, - &config.cwd, - config.permissions.workspace_roots(), - ); + let workspace_roots = config.effective_workspace_roots(); + let summary = + summarize_permission_profile(&permission_profile, &config.cwd, workspace_roots.as_slice()); if let Some(details) = summary.strip_prefix("read-only") && !details.contains("(network access enabled)") { diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 8196678ba3..3d30f800b6 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -256,7 +256,7 @@ impl StatusHistoryCell { ) -> (Self, StatusHistoryHandle) { let approval_policy = AskForApproval::from(config.permissions.approval_policy.value()); let permission_profile = config.permissions.effective_permission_profile(); - let workspace_roots = config.permissions.workspace_roots(); + let workspace_roots = config.effective_workspace_roots(); let mut config_entries = vec![ ("workdir", config.cwd.display().to_string()), ("model", model_name.to_string()), @@ -267,7 +267,11 @@ impl StatusHistoryCell { ), ( "sandbox", - summarize_permission_profile(&permission_profile, &config.cwd, workspace_roots), + summarize_permission_profile( + &permission_profile, + &config.cwd, + workspace_roots.as_slice(), + ), ), ]; if config.model_provider.wire_api == WireApi::Responses { @@ -291,8 +295,9 @@ impl StatusHistoryCell { .map(|(_, v)| v.clone()) .unwrap_or_else(|| "".to_string()); let active_permission_profile = config.permissions.active_permission_profile(); - let sandbox = status_permission_summary(&permission_profile, &config.cwd, workspace_roots); - let workspace_root_suffix = workspace_root_suffix(workspace_roots, &config.cwd); + let sandbox = + status_permission_summary(&permission_profile, &config.cwd, workspace_roots.as_slice()); + let workspace_root_suffix = workspace_root_suffix(workspace_roots.as_slice(), &config.cwd); let approval = status_approval_label(approval_policy, config.approvals_reviewer, &approval); let permissions = status_permissions_label( active_permission_profile.as_ref(), diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index e69f2da3c8..616ea15a9f 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -463,6 +463,42 @@ async fn status_permissions_workspace_roots_show_additional_directories() { ); } +#[tokio::test] +async fn status_permissions_workspace_roots_include_profile_defined_directories() { + let temp_home = TempDir::new().expect("temp home"); + let mut config = test_config(&temp_home).await; + set_workspace_cwd(&mut config, test_path_buf("/workspace/tests").abs()); + config + .permissions + .approval_policy + .set(AskForApproval::OnRequest.to_core()) + .expect("set approval policy"); + let profile_root = test_path_buf("/workspace/shared").abs(); + config + .permissions + .set_permission_profile_with_active_profile( + PermissionProfile::workspace_write_with( + std::slice::from_ref(&profile_root), + NetworkSandboxPolicy::Restricted, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ), + Some(ActivePermissionProfile::new(":workspace")), + ) + .expect("set permission profile"); + config + .permissions + .set_profile_workspace_roots(vec![profile_root.clone()]); + + assert_eq!( + permissions_text_for(&config), + Some(format!( + "Workspace [{}] (on-request)", + profile_root.display() + )) + ); +} + #[tokio::test] async fn status_permissions_broadened_workspace_profile_shows_builtin_label() { let temp_home = TempDir::new().expect("temp home");