tui/exec: show effective workspace roots in summaries (#22612)

## Why

This PR builds on [#22611](https://github.com/openai/codex/pull/22611).

After `runtimeWorkspaceRoots` moved onto thread state, the user-facing
summaries were still inconsistent about which roots they showed. In
particular, `/status` and the exec startup summary could under-report
extra workspace roots from `--add-dir` or from profile-defined
`workspace_roots`, which made the new model look incorrect even when the
permissions themselves were right.

## What Changed

- switched the TUI status surfaces to summarize against
`Config::effective_workspace_roots()`
- updated the exec human-output summary to render from the effective
permission profile instead of the raw constrained profile
- added focused regressions for both the TUI and exec code paths so
extra workspace roots stay visible in user-facing summaries

## Verification

Targeted coverage for this follow-up lives in:
- `codex-rs/tui/src/status/tests.rs`
- `codex-rs/exec/src/event_processor_with_human_output_tests.rs`

The added regressions verify that:
- status output includes profile-defined workspace roots in the
effective permissions summary
- exec startup output includes runtime workspace roots instead of
collapsing back to `cwd` only
This commit is contained in:
Michael Bolin
2026-05-14 23:10:45 -07:00
committed by GitHub
parent 8a5306ff88
commit 8adb6032cc
5 changed files with 120 additions and 11 deletions

View File

@@ -420,6 +420,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()),
@@ -434,9 +435,9 @@ fn config_summary_entries(
(
"sandbox",
summarize_permission_profile(
&config.permissions.effective_permission_profile(),
&permission_profile,
&config.cwd,
config.permissions.user_visible_workspace_roots(),
config.effective_workspace_roots().as_slice(),
),
),
];

View File

@@ -2,12 +2,17 @@ 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 codex_utils_sandbox_summary::summarize_permission_profile;
@@ -15,6 +20,7 @@ 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::reasoning_text;
use super::should_print_final_message_to_stdout;
@@ -168,6 +174,71 @@ 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_name = extra_root
.file_name()
.expect("extra root should have file name")
.to_string_lossy()
.to_string();
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);
let sandbox_summary = summary_entries
.iter()
.find_map(|(key, value)| (*key == "sandbox").then_some(value))
.expect("sandbox summary entry");
assert!(
sandbox_summary.starts_with("workspace-write [workdir, ")
&& sandbox_summary.contains(&expected_extra_root_name),
"expected runtime workspace root in sandbox summary: {summary_entries:?}"
);
}
#[test]
fn final_message_from_turn_items_uses_latest_agent_message() {
let message = final_message_from_turn_items(&[

View File

@@ -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)")
{

View File

@@ -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.user_visible_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(|| "<unknown>".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(),

View File

@@ -463,6 +463,40 @@ 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_from_session_snapshot_with_profile_workspace_roots(
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")),
vec![profile_root.clone()],
)
.expect("set permission profile");
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");