mirror of
https://github.com/openai/codex.git
synced 2026-11-03 16:23:51 +00:00
tui: pass active permission profiles through app commands (#22891)
## Why This continues the permissions migration by keeping the TUI command boundary aligned with the app-server protocol direction from #22795: callers should select a permission profile by id instead of passing a concrete `PermissionProfile` value around as the turn configuration. `AppCommand` is internal to the TUI, but it is the path that eventually becomes `thread/turn/start`, so carrying concrete profile details there made it too easy for UI code to keep relying on the old whole-profile replacement model. ## What changed - `AppCommand::UserTurn` and `AppCommand::OverrideTurnContext` now carry `Option<ActivePermissionProfile>` instead of `PermissionProfile`. - Composer submissions copy the active permission profile id from the current session snapshot; legacy snapshots intentionally submit no active profile id. - Permission preset UI events now carry only the active built-in profile id. The app derives the concrete built-in `PermissionProfile` internally only when updating its local config/status snapshot. - Permission presets expose their built-in active profile id, and preset selection preserves that id in both the immediate turn override and the local TUI config snapshot. - Turn routing sends `TurnPermissionsOverride::ActiveProfile` when an active id is present, and only falls back to the legacy sandbox projection for the remaining runtime override path. ## How to review Start with `codex-rs/tui/src/app_command.rs` to verify the command shape no longer exposes `PermissionProfile`. Then read `codex-rs/tui/src/app/thread_routing.rs` to verify the app-server turn-start conversion: active ids go through as ids, while the legacy sandbox fallback is still constrained to the existing runtime override case. Finally, check `codex-rs/tui/src/chatwidget/permission_popups.rs`, `codex-rs/tui/src/app/event_dispatch.rs`, `codex-rs/tui/src/app/config_persistence.rs`, and `codex-rs/utils/approval-presets/src/lib.rs` to see how preset selections stay id-only across TUI events while the local display/config mirror still gets a concrete built-in profile. ## Verification Latest local verification after the id-only `AppEvent` cleanup: - `cargo check -p codex-tui --tests` - `cargo test -p codex-tui permissions_selection_sends_approvals_reviewer_in_override_turn_context` - `cargo test -p codex-tui update_feature_flags_enabling_guardian` - `cargo test -p codex-utils-approval-presets` - `just fmt` - `just fix -p codex-tui -p codex-utils-approval-presets` Earlier in the same PR, before the final event-shape cleanup: - `cargo test -p codex-tui turn_permissions_` - `cargo test -p codex-tui submission_` - `cargo test -p codex-tui session_configured_syncs_widget_config_permissions_and_cwd` - `RUST_MIN_STACK=16777216 cargo test -p codex-tui`
This commit is contained in:
1 parent
8543e39885
commit
bbb5c2811d
21 files changed
+243
-114
No files matched your search
@@ -105,24 +105,41 @@ impl App {
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn try_set_permission_profile_on_config(
|
||||
pub(super) fn try_set_builtin_active_permission_profile_on_config(
|
||||
&mut self,
|
||||
config: &mut Config,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: ActivePermissionProfile,
|
||||
user_message_prefix: &str,
|
||||
log_message: &str,
|
||||
) -> bool {
|
||||
) -> Option<PermissionProfile> {
|
||||
let Some(permission_profile) =
|
||||
builtin_permission_profile_for_active_permission_profile(&active_permission_profile)
|
||||
else {
|
||||
tracing::warn!(
|
||||
id = %active_permission_profile.id,
|
||||
"{log_message}: unsupported active permission profile"
|
||||
);
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"{user_message_prefix}: unsupported active permission profile `{}`",
|
||||
active_permission_profile.id
|
||||
));
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Err(err) = config
|
||||
.permissions
|
||||
.set_permission_profile(permission_profile)
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
permission_profile.clone(),
|
||||
Some(active_permission_profile),
|
||||
)
|
||||
{
|
||||
tracing::warn!(error = %err, "{log_message}");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("{user_message_prefix}: {err}"));
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
|
||||
true
|
||||
Some(permission_profile)
|
||||
}
|
||||
|
||||
pub(super) async fn update_feature_flags(&mut self, updates: Vec<(Feature, bool)>) {
|
||||
@@ -149,6 +166,7 @@ impl App {
|
||||
let mut approval_policy_override = None;
|
||||
let mut approvals_reviewer_override = None;
|
||||
let mut permission_profile_override = None;
|
||||
let mut active_permission_profile_override = None;
|
||||
let mut feature_updates_to_apply = Vec::with_capacity(updates.len());
|
||||
// Auto-Review owns `approvals_reviewer`, but disabling the feature
|
||||
// from inside a profile should not silently clear a value configured at
|
||||
@@ -240,14 +258,16 @@ impl App {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if !self.try_set_permission_profile_on_config(
|
||||
&mut feature_config,
|
||||
auto_review_preset.permission_profile.clone(),
|
||||
"Failed to enable Auto-review",
|
||||
"failed to set auto-review permission profile on staged config",
|
||||
) {
|
||||
let Some(permission_profile) = self
|
||||
.try_set_builtin_active_permission_profile_on_config(
|
||||
&mut feature_config,
|
||||
auto_review_preset.active_permission_profile.clone(),
|
||||
"Failed to enable Auto-review",
|
||||
"failed to set auto-review permission profile on staged config",
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
feature_edits.extend([
|
||||
ConfigEdit::SetPath {
|
||||
segments: scoped_segments("approval_policy"),
|
||||
@@ -259,7 +279,9 @@ impl App {
|
||||
},
|
||||
]);
|
||||
approval_policy_override = Some(auto_review_preset.approval_policy);
|
||||
permission_profile_override = Some(auto_review_preset.permission_profile.clone());
|
||||
permission_profile_override = Some(permission_profile);
|
||||
active_permission_profile_override =
|
||||
Some(auto_review_preset.active_permission_profile.clone());
|
||||
}
|
||||
next_config = feature_config;
|
||||
feature_updates_to_apply.push((feature, effective_enabled));
|
||||
@@ -305,7 +327,10 @@ impl App {
|
||||
if let Some(permission_profile) = permission_profile_override_value.as_ref()
|
||||
&& let Err(err) = self
|
||||
.chat_widget
|
||||
.set_permission_profile(permission_profile.clone())
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
permission_profile.clone(),
|
||||
active_permission_profile_override.clone(),
|
||||
)
|
||||
{
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
@@ -333,7 +358,7 @@ impl App {
|
||||
/*cwd*/ None,
|
||||
approval_policy_override,
|
||||
approvals_reviewer_override,
|
||||
permission_profile_override,
|
||||
active_permission_profile_override,
|
||||
/*windows_sandbox_level*/ None,
|
||||
/*model*/ None,
|
||||
/*effort*/ None,
|
||||
@@ -360,7 +385,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),
|
||||
/*model*/ None,
|
||||
|
||||
@@ -1111,7 +1111,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),
|
||||
/*model*/ None,
|
||||
@@ -1136,7 +1136,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),
|
||||
/*model*/ None,
|
||||
@@ -1150,9 +1150,10 @@ impl App {
|
||||
self.app_event_tx.send(AppEvent::UpdateAskForApprovalPolicy(
|
||||
AskForApproval::from(preset.approval),
|
||||
));
|
||||
self.app_event_tx.send(AppEvent::UpdatePermissionProfile(
|
||||
preset.permission_profile.clone(),
|
||||
));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateActivePermissionProfile(
|
||||
preset.active_permission_profile.clone(),
|
||||
));
|
||||
let _ = mode;
|
||||
self.chat_widget.add_plain_history_lines(vec![
|
||||
Line::from(vec!["• ".dim(), "Sandbox ready".into()]),
|
||||
@@ -1400,25 +1401,30 @@ impl App {
|
||||
self.sync_active_thread_permission_settings_to_cached_session()
|
||||
.await;
|
||||
}
|
||||
AppEvent::UpdatePermissionProfile(permission_profile) => {
|
||||
AppEvent::UpdateActivePermissionProfile(active_permission_profile) => {
|
||||
let mut config = self.config.clone();
|
||||
let Some(permission_profile) = self
|
||||
.try_set_builtin_active_permission_profile_on_config(
|
||||
&mut config,
|
||||
active_permission_profile.clone(),
|
||||
"Failed to set permission profile",
|
||||
"failed to set active permission profile on app config",
|
||||
)
|
||||
else {
|
||||
return Ok(AppRunControl::Continue);
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let permission_profile_is_managed_restricted =
|
||||
managed_filesystem_sandbox_is_restricted(&permission_profile);
|
||||
let permission_profile_for_chat = permission_profile.clone();
|
||||
|
||||
let mut config = self.config.clone();
|
||||
if !self.try_set_permission_profile_on_config(
|
||||
&mut config,
|
||||
permission_profile,
|
||||
"Failed to set permission profile",
|
||||
"failed to set permission profile on app config",
|
||||
) {
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
self.config = config;
|
||||
if let Err(err) = self
|
||||
.chat_widget
|
||||
.set_permission_profile(permission_profile_for_chat)
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
permission_profile_for_chat,
|
||||
Some(active_permission_profile),
|
||||
)
|
||||
{
|
||||
tracing::warn!(%err, "failed to set permission profile on chat config");
|
||||
self.chat_widget
|
||||
|
||||
@@ -1640,7 +1640,18 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
|
||||
.config_ref()
|
||||
.permissions
|
||||
.permission_profile(),
|
||||
&auto_review.permission_profile
|
||||
&auto_review.permission_profile()
|
||||
);
|
||||
assert_eq!(
|
||||
app.config.permissions.active_permission_profile(),
|
||||
Some(auto_review.active_permission_profile.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
app.chat_widget
|
||||
.config_ref()
|
||||
.permissions
|
||||
.active_permission_profile(),
|
||||
Some(auto_review.active_permission_profile.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
app.chat_widget.config_ref().approvals_reviewer,
|
||||
@@ -1649,7 +1660,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
|
||||
assert_eq!(app.runtime_approval_policy_override, None);
|
||||
assert_eq!(
|
||||
app.runtime_permission_profile_override,
|
||||
Some(auto_review.permission_profile.clone())
|
||||
Some(auto_review.permission_profile())
|
||||
);
|
||||
assert_eq!(
|
||||
op_rx.try_recv(),
|
||||
@@ -1657,7 +1668,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.clone()),
|
||||
active_permission_profile: Some(auto_review.active_permission_profile.clone()),
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
@@ -1719,7 +1730,10 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor
|
||||
app.chat_widget
|
||||
.set_approval_policy(AskForApproval::OnRequest);
|
||||
app.chat_widget
|
||||
.set_permission_profile(PermissionProfile::workspace_write())?;
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
PermissionProfile::workspace_write(),
|
||||
/*active_profile*/ None,
|
||||
)?;
|
||||
|
||||
app.update_feature_flags(vec![(Feature::GuardianApproval, false)])
|
||||
.await;
|
||||
@@ -1747,7 +1761,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,
|
||||
effort: None,
|
||||
@@ -1817,7 +1831,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review
|
||||
.config_ref()
|
||||
.permissions
|
||||
.permission_profile(),
|
||||
&auto_review.permission_profile
|
||||
&auto_review.permission_profile()
|
||||
);
|
||||
assert_eq!(
|
||||
op_rx.try_recv(),
|
||||
@@ -1825,7 +1839,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.clone()),
|
||||
active_permission_profile: Some(auto_review.active_permission_profile.clone()),
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
@@ -1882,7 +1896,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,
|
||||
effort: None,
|
||||
@@ -1941,7 +1955,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.clone()),
|
||||
active_permission_profile: Some(auto_review.active_permission_profile.clone()),
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
@@ -2028,7 +2042,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,
|
||||
effort: None,
|
||||
@@ -3139,7 +3153,10 @@ async fn side_fork_config_inherits_parent_thread_runtime_settings() {
|
||||
app.chat_widget
|
||||
.set_approval_policy(AskForApproval::OnRequest);
|
||||
app.chat_widget
|
||||
.set_permission_profile(parent_permission_profile.clone())
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
parent_permission_profile.clone(),
|
||||
/*active_profile*/ None,
|
||||
)
|
||||
.expect("test permission profile should be accepted");
|
||||
app.chat_widget
|
||||
.set_approvals_reviewer(ApprovalsReviewer::AutoReview);
|
||||
|
||||
@@ -510,7 +510,7 @@ impl App {
|
||||
cwd,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
@@ -590,7 +590,7 @@ impl App {
|
||||
approvals_reviewer.unwrap_or(config.approvals_reviewer);
|
||||
let permissions_override = Self::turn_permissions_override_from_config(
|
||||
config,
|
||||
permission_profile,
|
||||
active_permission_profile.as_ref(),
|
||||
self.runtime_permission_profile_override.as_ref(),
|
||||
);
|
||||
app_server
|
||||
@@ -698,16 +698,14 @@ impl App {
|
||||
|
||||
fn turn_permissions_override_from_config(
|
||||
config: &Config,
|
||||
permission_profile: &PermissionProfile,
|
||||
active_permission_profile: Option<&ActivePermissionProfile>,
|
||||
runtime_permission_profile_override: Option<&PermissionProfile>,
|
||||
) -> TurnPermissionsOverride {
|
||||
let effective_permission_profile = config.permissions.effective_permission_profile();
|
||||
if &effective_permission_profile == permission_profile
|
||||
&& let Some(active_permission_profile) = config.permissions.active_permission_profile()
|
||||
{
|
||||
return TurnPermissionsOverride::ActiveProfile(active_permission_profile);
|
||||
if let Some(active_permission_profile) = active_permission_profile {
|
||||
return TurnPermissionsOverride::ActiveProfile(active_permission_profile.clone());
|
||||
}
|
||||
|
||||
let effective_permission_profile = config.permissions.effective_permission_profile();
|
||||
let runtime_permission_profile_override =
|
||||
runtime_permission_profile_override.map(|profile| {
|
||||
profile
|
||||
@@ -718,9 +716,9 @@ impl App {
|
||||
});
|
||||
if runtime_permission_profile_override
|
||||
.as_ref()
|
||||
.is_some_and(|profile| profile == permission_profile)
|
||||
.is_some_and(|profile| profile == &effective_permission_profile)
|
||||
{
|
||||
return TurnPermissionsOverride::LegacySandbox(permission_profile.clone());
|
||||
return TurnPermissionsOverride::LegacySandbox(effective_permission_profile);
|
||||
}
|
||||
|
||||
TurnPermissionsOverride::Preserve
|
||||
@@ -1506,12 +1504,12 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn turn_permissions_use_active_profile_when_available() {
|
||||
let config = config_with_workspace_profile().await;
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
let active_permission_profile = config.permissions.active_permission_profile();
|
||||
|
||||
assert_eq!(
|
||||
App::turn_permissions_override_from_config(
|
||||
&config,
|
||||
&permission_profile,
|
||||
active_permission_profile.as_ref(),
|
||||
/*runtime_permission_profile_override*/ None,
|
||||
),
|
||||
TurnPermissionsOverride::ActiveProfile(ActivePermissionProfile::new(
|
||||
@@ -1527,12 +1525,10 @@ mod tests {
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::read_only())
|
||||
.expect("read-only profile should be allowed");
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
|
||||
assert_eq!(
|
||||
App::turn_permissions_override_from_config(
|
||||
&config,
|
||||
&permission_profile,
|
||||
&config, /*active_permission_profile*/ None,
|
||||
/*runtime_permission_profile_override*/ None,
|
||||
),
|
||||
TurnPermissionsOverride::Preserve
|
||||
@@ -1552,7 +1548,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
App::turn_permissions_override_from_config(
|
||||
&config,
|
||||
&effective_permission_profile,
|
||||
/*active_permission_profile*/ None,
|
||||
Some(&permission_profile),
|
||||
),
|
||||
TurnPermissionsOverride::LegacySandbox(effective_permission_profile)
|
||||
|
||||
@@ -131,6 +131,7 @@ mod tests {
|
||||
use codex_app_server_protocol::PermissionProfileFileSystemPermissions;
|
||||
use codex_app_server_protocol::PermissionProfileNetworkPermissions;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
@@ -197,9 +198,14 @@ mod tests {
|
||||
codex_config::Constrained::allow_any(AskForApproval::OnRequest.to_core());
|
||||
app.config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
let expected_permission_profile = PermissionProfile::workspace_write();
|
||||
let expected_active_permission_profile =
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
|
||||
app.chat_widget.handle_thread_session(main_session.clone());
|
||||
app.chat_widget
|
||||
.set_permission_profile(expected_permission_profile.clone())
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
expected_permission_profile.clone(),
|
||||
Some(expected_active_permission_profile.clone()),
|
||||
)
|
||||
.expect("set widget permission profile");
|
||||
app.config
|
||||
.permissions
|
||||
@@ -213,6 +219,7 @@ mod tests {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::AutoReview,
|
||||
permission_profile: expected_permission_profile,
|
||||
active_permission_profile: Some(expected_active_permission_profile),
|
||||
..main_session
|
||||
};
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user