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:
Michael Bolin
2026-05-15 15:42:35 -07:00
committed by GitHub
Unverified
parent 8543e39885
commit bbb5c2811d
21 changed files with 243 additions and 114 deletions
+15 -2
View File
@@ -140,6 +140,8 @@ use codex_protocol::ThreadId;
use codex_protocol::config_types::Personality;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelAvailabilityNux;
use codex_protocol::openai_models::ModelPreset;
@@ -150,6 +152,7 @@ use codex_protocol::permissions::FileSystemSandboxKind;
use codex_rollout::StateDbHandle;
use codex_terminal_detection::user_agent;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_approval_presets::builtin_permission_profile_for_active_permission_profile;
use color_eyre::eyre::Result;
use color_eyre::eyre::WrapErr;
use crossterm::event::KeyCode;
@@ -327,7 +330,7 @@ fn default_exec_approval_decisions(
struct AutoReviewMode {
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
permission_profile: PermissionProfile,
active_permission_profile: ActivePermissionProfile,
}
/// Enabling the Auto-review experiment in the TUI should also switch the
@@ -338,7 +341,17 @@ fn auto_review_mode() -> AutoReviewMode {
AutoReviewMode {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
permission_profile: PermissionProfile::workspace_write(),
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
),
}
}
#[cfg(test)]
impl AutoReviewMode {
fn permission_profile(&self) -> PermissionProfile {
builtin_permission_profile_for_active_permission_profile(&self.active_permission_profile)
.expect("auto-review mode should use a built-in permission profile")
}
}
+42 -17
View File
@@ -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,
+22 -16
View File
@@ -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
+28 -11
View File
@@ -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);
+12 -16
View File
@@ -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)
+8 -1
View File
@@ -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!(
+7 -7
View File
@@ -16,7 +16,7 @@ use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use serde::Serialize;
@@ -41,7 +41,7 @@ pub(crate) enum AppCommand {
cwd: PathBuf,
approval_policy: AskForApproval,
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
model: String,
effort: Option<ReasoningEffortConfig>,
summary: Option<ReasoningSummaryConfig>,
@@ -54,7 +54,7 @@ pub(crate) enum AppCommand {
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
@@ -142,7 +142,7 @@ impl AppCommand {
items: Vec<UserInput>,
cwd: PathBuf,
approval_policy: AskForApproval,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
model: String,
effort: Option<ReasoningEffortConfig>,
summary: Option<ReasoningSummaryConfig>,
@@ -156,7 +156,7 @@ impl AppCommand {
cwd,
approval_policy,
approvals_reviewer: None,
permission_profile,
active_permission_profile,
model,
effort,
summary,
@@ -172,7 +172,7 @@ impl AppCommand {
cwd: Option<PathBuf>,
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
@@ -185,7 +185,7 @@ impl AppCommand {
cwd,
approval_policy,
approvals_reviewer,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
+3 -3
View File
@@ -43,7 +43,7 @@ use codex_features::Feature;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::Personality;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_realtime_webrtc::RealtimeWebrtcEvent;
use codex_realtime_webrtc::RealtimeWebrtcSessionHandle;
@@ -742,8 +742,8 @@ pub(crate) enum AppEvent {
/// Update the current approval policy in the running app and widget.
UpdateAskForApprovalPolicy(AskForApproval),
/// Update the current permission profile in the running app and widget.
UpdatePermissionProfile(PermissionProfile),
/// Update the current built-in active permission profile in the running app and widget.
UpdateActivePermissionProfile(ActivePermissionProfile),
/// Update the current approvals reviewer in the running app and widget.
UpdateApprovalsReviewer(ApprovalsReviewer),
+1
View File
@@ -442,6 +442,7 @@ use crate::workspace_command::WorkspaceCommandRunner;
use chrono::Local;
use codex_app_server_protocol::AskForApproval;
use codex_file_search::FileMatch;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelPreset;
@@ -336,12 +336,12 @@ impl ChatWidget {
None if self.config.notices.fast_default_opt_out == Some(true) => Some(None),
None => None,
};
let permission_profile = self.config.permissions.effective_permission_profile();
let active_permission_profile = self.config.permissions.active_permission_profile();
let op = AppCommand::user_turn(
items,
self.config.cwd.to_path_buf(),
AskForApproval::from(self.config.permissions.approval_policy.value()),
permission_profile,
active_permission_profile,
effective_mode.model().to_string(),
effective_mode.reasoning_effort(),
/*summary*/ None,
@@ -124,7 +124,7 @@ impl ChatWidget {
} else {
Self::approval_preset_actions(
preset_approval,
preset.permission_profile.clone(),
preset.active_permission_profile.clone(),
base_name.clone(),
ApprovalsReviewer::User,
)
@@ -134,7 +134,7 @@ impl ChatWidget {
{
Self::approval_preset_actions(
preset_approval,
preset.permission_profile.clone(),
preset.active_permission_profile.clone(),
base_name.clone(),
ApprovalsReviewer::User,
)
@@ -142,7 +142,7 @@ impl ChatWidget {
} else {
Self::approval_preset_actions(
preset_approval,
preset.permission_profile.clone(),
preset.active_permission_profile.clone(),
base_name.clone(),
ApprovalsReviewer::User,
)
@@ -180,7 +180,7 @@ impl ChatWidget {
),
actions: Self::approval_preset_actions(
preset_approval,
preset.permission_profile.clone(),
preset.active_permission_profile.clone(),
"Auto-review".to_string(),
ApprovalsReviewer::AutoReview,
),
@@ -308,17 +308,16 @@ impl ChatWidget {
pub(super) fn approval_preset_actions(
approval: AskForApproval,
permission_profile: PermissionProfile,
active_permission_profile: ActivePermissionProfile,
label: String,
approvals_reviewer: ApprovalsReviewer,
) -> Vec<SelectionAction> {
vec![Box::new(move |tx| {
let permission_profile_clone = permission_profile.clone();
tx.send(AppEvent::CodexOp(AppCommand::override_turn_context(
/*cwd*/ None,
Some(approval),
Some(approvals_reviewer),
Some(permission_profile_clone.clone()),
Some(active_permission_profile.clone()),
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
@@ -328,7 +327,9 @@ impl ChatWidget {
/*personality*/ None,
)));
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
tx.send(AppEvent::UpdatePermissionProfile(permission_profile_clone));
tx.send(AppEvent::UpdateActivePermissionProfile(
active_permission_profile.clone(),
));
tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer));
tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_info_event(
@@ -385,7 +386,6 @@ impl ChatWidget {
) {
let selected_name = preset.label.to_string();
let approval = AskForApproval::from(preset.approval);
let permission_profile = preset.permission_profile;
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
let title_line = Line::from("Enable full access?").bold();
let info_line = Line::from(vec![
@@ -402,7 +402,7 @@ impl ChatWidget {
let mut accept_actions = Self::approval_preset_actions(
approval,
permission_profile.clone(),
preset.active_permission_profile.clone(),
selected_name.clone(),
ApprovalsReviewer::User,
);
@@ -412,7 +412,7 @@ impl ChatWidget {
let mut accept_and_remember_actions = Self::approval_preset_actions(
approval,
permission_profile,
preset.active_permission_profile,
selected_name,
ApprovalsReviewer::User,
);
+1 -1
View File
@@ -285,7 +285,7 @@ impl ChatWidget {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
Some(switch_model_for_events.clone()),
Some(Some(default_effort)),
+1 -1
View File
@@ -107,7 +107,7 @@ impl ChatWidget {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
+5 -3
View File
@@ -17,13 +17,15 @@ impl ChatWidget {
}
}
/// Set the permission profile in the widget's config copy.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn set_permission_profile(
pub(crate) fn set_permission_profile_from_session_snapshot(
&mut self,
profile: PermissionProfile,
active_profile: Option<ActivePermissionProfile>,
) -> ConstraintResult<()> {
self.config.permissions.set_permission_profile(profile)?;
self.config
.permissions
.set_permission_profile_from_session_snapshot(profile, active_profile)?;
self.refresh_status_surfaces();
Ok(())
}
@@ -53,7 +53,7 @@ impl ChatWidget {
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
+2
View File
@@ -149,6 +149,8 @@ pub(super) use codex_protocol::config_types::CollaborationMode;
pub(super) use codex_protocol::config_types::ModeKind;
pub(super) use codex_protocol::config_types::Personality;
pub(super) use codex_protocol::config_types::ServiceTier;
pub(super) use codex_protocol::models::ActivePermissionProfile;
pub(super) use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
pub(super) use codex_protocol::models::FileSystemPermissions;
pub(super) use codex_protocol::models::MessagePhase;
pub(super) use codex_protocol::models::NetworkPermissions;
@@ -94,7 +94,7 @@ async fn submission_preserves_text_elements_and_local_images() {
}
#[tokio::test]
async fn submission_includes_configured_permission_profile() {
async fn submission_includes_configured_active_permission_profile() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let thread_id = ThreadId::new();
@@ -120,6 +120,7 @@ async fn submission_includes_configured_permission_profile() {
},
}
.into();
let expected_active_permission_profile = ActivePermissionProfile::new("custom");
let configured = crate::session_state::ThreadSessionState {
thread_id,
forked_from_id: None,
@@ -130,8 +131,8 @@ async fn submission_includes_configured_permission_profile() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: expected_permission_profile.clone(),
active_permission_profile: None,
permission_profile: expected_permission_profile,
active_permission_profile: Some(expected_active_permission_profile.clone()),
cwd: test_path_buf("/home/user/project").abs(),
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
@@ -150,17 +151,21 @@ async fn submission_includes_configured_permission_profile() {
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let permission_profile = match next_submit_op(&mut op_rx) {
let active_permission_profile = match next_submit_op(&mut op_rx) {
Op::UserTurn {
permission_profile, ..
} => permission_profile,
active_permission_profile,
..
} => active_permission_profile,
other => panic!("expected Op::UserTurn, got {other:?}"),
};
assert_eq!(permission_profile, expected_permission_profile);
assert_eq!(
active_permission_profile,
Some(expected_active_permission_profile)
);
}
#[tokio::test]
async fn submission_keeps_profile_when_legacy_projection_is_external() {
async fn submission_omits_active_permission_profile_for_legacy_snapshot() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let thread_id = ThreadId::new();
@@ -180,7 +185,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: expected_permission_profile.clone(),
permission_profile: expected_permission_profile,
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
runtime_workspace_roots: Vec::new(),
@@ -197,13 +202,14 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
.set_composer_text("submit".to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let permission_profile = match next_submit_op(&mut op_rx) {
let active_permission_profile = match next_submit_op(&mut op_rx) {
Op::UserTurn {
permission_profile, ..
} => permission_profile,
active_permission_profile,
..
} => active_permission_profile,
other => panic!("expected Op::UserTurn, got {other:?}"),
};
assert_eq!(permission_profile, expected_permission_profile);
assert_eq!(active_permission_profile, None);
}
#[tokio::test]
@@ -296,8 +296,11 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
assert_eq!(&chat.config_ref().cwd, &expected_cwd);
let updated_profile = PermissionProfile::workspace_write();
chat.set_permission_profile(updated_profile.clone())
.expect("set permission profile");
chat.set_permission_profile_from_session_snapshot(
updated_profile.clone(),
/*active_profile*/ None,
)
.expect("set permission profile");
assert_eq!(
chat.config_ref().permissions.permission_profile(),
&updated_profile,
@@ -742,7 +742,9 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context
cwd: None,
approval_policy: Some(AskForApproval::OnRequest),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
permission_profile: Some(PermissionProfile::workspace_write()),
active_permission_profile: Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
windows_sandbox_level: None,
model: None,
effort: None,
@@ -752,6 +754,20 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context
personality: None,
}
);
let active_permission_profile_update = std::iter::from_fn(|| rx.try_recv().ok())
.find_map(|event| match event {
AppEvent::UpdateActivePermissionProfile(active_permission_profile) => {
Some(active_permission_profile)
}
_ => None,
})
.expect("expected UpdateActivePermissionProfile event");
assert_eq!(
active_permission_profile_update,
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
);
}
#[tokio::test]
@@ -42,10 +42,10 @@ impl ChatWidget {
extra_count: usize,
failed_scan: bool,
) {
let (approval, permission_profile) = match &preset {
let (approval, active_permission_profile) = match &preset {
Some(p) => (
Some(AskForApproval::from(p.approval)),
Some(p.permission_profile.clone()),
Some(p.active_permission_profile.clone()),
),
None => (None, None),
};
@@ -110,10 +110,12 @@ impl ChatWidget {
tx.send(AppEvent::SkipNextWorldWritableScan);
}));
}
if let (Some(approval), Some(permission_profile)) = (approval, permission_profile.clone()) {
if let (Some(approval), Some(active_permission_profile)) =
(approval, active_permission_profile.clone())
{
accept_actions.extend(Self::approval_preset_actions(
approval,
permission_profile,
active_permission_profile,
mode_label.to_string(),
ApprovalsReviewer::User,
));
@@ -124,10 +126,12 @@ impl ChatWidget {
tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
}));
if let (Some(approval), Some(permission_profile)) = (approval, permission_profile) {
if let (Some(approval), Some(active_permission_profile)) =
(approval, active_permission_profile)
{
accept_and_remember_actions.extend(Self::approval_preset_actions(
approval,
permission_profile,
active_permission_profile,
mode_label.to_string(),
ApprovalsReviewer::User,
));
@@ -1,3 +1,7 @@
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -12,6 +16,8 @@ pub struct ApprovalPreset {
pub description: &'static str,
/// Approval policy to apply.
pub approval: AskForApproval,
/// Built-in permission profile selected by this preset.
pub active_permission_profile: ActivePermissionProfile,
/// Permission profile to apply.
pub permission_profile: PermissionProfile,
}
@@ -26,6 +32,9 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
label: "Read Only",
description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.",
approval: AskForApproval::OnRequest,
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
),
permission_profile: PermissionProfile::read_only(),
},
ApprovalPreset {
@@ -33,6 +42,9 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
label: "Default",
description: "Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files. (Identical to Agent mode)",
approval: AskForApproval::OnRequest,
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
),
permission_profile: PermissionProfile::workspace_write(),
},
ApprovalPreset {
@@ -40,7 +52,26 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
label: "Full Access",
description: "Codex can edit files outside this workspace and access the internet without asking for approval. Exercise caution when using.",
approval: AskForApproval::Never,
active_permission_profile: ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS,
),
permission_profile: PermissionProfile::Disabled,
},
]
}
/// Return the concrete profile for one of the built-in active profile ids.
pub fn builtin_permission_profile_for_active_permission_profile(
active_permission_profile: &ActivePermissionProfile,
) -> Option<PermissionProfile> {
if active_permission_profile.extends.is_some() {
return None;
}
match active_permission_profile.id.as_str() {
BUILT_IN_PERMISSION_PROFILE_READ_ONLY => Some(PermissionProfile::read_only()),
BUILT_IN_PERMISSION_PROFILE_WORKSPACE => Some(PermissionProfile::workspace_write()),
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS => Some(PermissionProfile::Disabled),
_ => None,
}
}