From 038b78c915800990c229eaf7b875f194895891b6 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Wed, 21 Jan 2026 21:38:46 -0800 Subject: [PATCH] feat(tui) /permissions flow (#9561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds the `/permissions` command, with a (usually) shorter set of permissions. `/approvals` still exists, for backwards compatibility. Screenshot 2026-01-20 at 4 12 51 PM ## Testing - [x] updated unit tests - [x] Tested locally --- codex-rs/common/src/approval_presets.rs | 10 +++--- codex-rs/tui/src/app.rs | 11 ++++-- codex-rs/tui/src/app_event.rs | 4 +++ codex-rs/tui/src/chatwidget.rs | 35 ++++++++++++++++--- ...get__tests__approvals_selection_popup.snap | 17 +++++---- ...ts__approvals_selection_popup@windows.snap | 17 +++++---- ...vals_selection_popup@windows_degraded.snap | 20 +++++++---- codex-rs/tui/src/chatwidget/tests.rs | 4 +-- codex-rs/tui/src/history_cell.rs | 5 +++ codex-rs/tui/src/slash_command.rs | 3 ++ 10 files changed, 93 insertions(+), 33 deletions(-) diff --git a/codex-rs/common/src/approval_presets.rs b/codex-rs/common/src/approval_presets.rs index 1b673d1d9..cec67d258 100644 --- a/codex-rs/common/src/approval_presets.rs +++ b/codex-rs/common/src/approval_presets.rs @@ -24,21 +24,21 @@ pub fn builtin_approval_presets() -> Vec { ApprovalPreset { id: "read-only", label: "Read Only", - description: "Requires approval to edit files and run commands.", + description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.", approval: AskForApproval::OnRequest, sandbox: SandboxPolicy::ReadOnly, }, ApprovalPreset { id: "auto", - label: "Agent", - description: "Read and edit files, and run commands.", + 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, sandbox: SandboxPolicy::new_workspace_write_policy(), }, ApprovalPreset { id: "full-access", - label: "Agent (full access)", - description: "Codex can edit files outside this workspace and run commands with network access. Exercise caution when using.", + 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, sandbox: SandboxPolicy::DangerFullAccess, }, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 7d2636bf3..7f6b37f6c 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1060,8 +1060,12 @@ impl App { AppEvent::OpenAllModelsPopup { models } => { self.chat_widget.open_all_models_popup(models); } - AppEvent::OpenFullAccessConfirmation { preset } => { - self.chat_widget.open_full_access_confirmation(preset); + AppEvent::OpenFullAccessConfirmation { + preset, + return_to_permissions, + } => { + self.chat_widget + .open_full_access_confirmation(preset, return_to_permissions); } AppEvent::OpenWorldWritableWarningConfirmation { preset, @@ -1503,6 +1507,9 @@ impl App { } } } + AppEvent::OpenPermissionsPopup => { + self.chat_widget.open_permissions_popup(); + } AppEvent::OpenReviewBranchPicker(cwd) => { self.chat_widget.show_review_branch_picker(&cwd).await; } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 68053a1d2..42af4449c 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -125,6 +125,7 @@ pub(crate) enum AppEvent { /// Open the confirmation prompt before enabling full access mode. OpenFullAccessConfirmation { preset: ApprovalPreset, + return_to_permissions: bool, }, /// Open the Windows world-writable directories warning. @@ -227,6 +228,9 @@ pub(crate) enum AppEvent { /// Notify that the manage skills popup was closed. ManageSkillsClosed, + /// Re-open the permissions presets popup. + OpenPermissionsPopup, + /// Open the branch picker option from the review popup. OpenReviewBranchPicker(PathBuf), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index bab19cc94..4c4438c3c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2292,6 +2292,9 @@ impl ChatWidget { SlashCommand::Approvals => { self.open_approvals_popup(); } + SlashCommand::Permissions => { + self.open_permissions_popup(); + } SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { @@ -3565,6 +3568,16 @@ impl ChatWidget { /// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy). pub(crate) fn open_approvals_popup(&mut self) { + self.open_approval_mode_popup(true); + } + + /// Open a popup to choose the permissions mode (approval policy + sandbox policy). + pub(crate) fn open_permissions_popup(&mut self) { + let include_read_only = cfg!(target_os = "windows"); + self.open_approval_mode_popup(include_read_only); + } + + fn open_approval_mode_popup(&mut self, include_read_only: bool) { let current_approval = self.config.approval_policy.value(); let current_sandbox = self.config.sandbox_policy.get(); let mut items: Vec = Vec::new(); @@ -3581,10 +3594,13 @@ impl ChatWidget { && presets.iter().any(|preset| preset.id == "auto"); for preset in presets.into_iter() { + if !include_read_only && preset.id == "read-only" { + continue; + } let is_current = Self::preset_matches_current(current_approval, current_sandbox, &preset); let name = if preset.id == "auto" && windows_degraded_sandbox_enabled { - "Agent (non-elevated sandbox)".to_string() + "Default (non-elevated sandbox)".to_string() } else { preset.label.to_string() }; @@ -3604,6 +3620,7 @@ impl ChatWidget { vec![Box::new(move |tx| { tx.send(AppEvent::OpenFullAccessConfirmation { preset: preset_clone.clone(), + return_to_permissions: !include_read_only, }); })] } else if preset.id == "auto" { @@ -3673,7 +3690,7 @@ impl ChatWidget { }); self.bottom_pane.show_selection_view(SelectionViewParams { - title: Some("Select Approval Mode".to_string()), + title: Some("Update Model Permissions".to_string()), footer_note, footer_hint: Some(standard_popup_hint_line()), items, @@ -3773,7 +3790,11 @@ impl ChatWidget { None } - pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) { + pub(crate) fn open_full_access_confirmation( + &mut self, + preset: ApprovalPreset, + return_to_permissions: bool, + ) { let approval = preset.approval; let sandbox = preset.sandbox; let mut header_children: Vec> = Vec::new(); @@ -3801,8 +3822,12 @@ impl ChatWidget { tx.send(AppEvent::PersistFullAccessWarningAcknowledged); })); - let deny_actions: Vec = vec![Box::new(|tx| { - tx.send(AppEvent::OpenApprovalsPopup); + let deny_actions: Vec = vec![Box::new(move |tx| { + if return_to_permissions { + tx.send(AppEvent::OpenPermissionsPopup); + } else { + tx.send(AppEvent::OpenApprovalsPopup); + } })]; let items = vec![ diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap index 6758ec62c..5e372cc0a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap @@ -2,12 +2,17 @@ source: tui/src/chatwidget/tests.rs expression: popup --- - Select Approval Mode + Update Model Permissions -› 1. Read Only (current) Requires approval to edit files and run commands. - 2. Agent Read and edit files, and run commands. - 3. Agent (full access) Codex can edit files outside this workspace and run - commands with network access. Exercise caution when - using. +› 1. Read Only (current) Codex can read files in the current workspace. + Approval is required to edit files or access the + internet. + 2. Default 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) + 3. Full Access Codex can edit files outside this workspace and + access the internet without asking for approval. + Exercise caution when using. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap index ff6bbec55..87ec52926 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap @@ -3,12 +3,17 @@ source: tui/src/chatwidget/tests.rs assertion_line: 2654 expression: popup --- - Select Approval Mode + Update Model Permissions -› 1. Read Only (current) Requires approval to edit files and run commands. - 2. Agent Read and edit files, and run commands. - 3. Agent (full access) Codex can edit files outside this workspace and run - commands with network access. Exercise caution when - using. +› 1. Read Only (current) Codex can read files in the current workspace. + Approval is required to edit files or access the + internet. + 2. Default 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) + 3. Full Access Codex can edit files outside this workspace and + access the internet without asking for approval. + Exercise caution when using. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows_degraded.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows_degraded.snap index 3c023a831..29220fb1c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows_degraded.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows_degraded.snap @@ -3,14 +3,20 @@ source: tui/src/chatwidget/tests.rs assertion_line: 2003 expression: popup --- - Select Approval Mode + Update Model Permissions -› 1. Read Only (current) Requires approval to edit files and run - commands. - 2. Agent (non-elevated sandbox) Read and edit files, and run commands. - 3. Agent (full access) Codex can edit files outside this workspace - and run commands with network access. - Exercise caution when using. +› 1. Read Only (current) Codex can read files in the current + workspace. Approval is required to edit + files or access the internet. + 2. Default (non-elevated sandbox) 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) + 3. Full Access Codex can edit files outside this + workspace and access the internet without + asking for approval. Exercise caution + when using. The non-elevated sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index d0d37f0a6..e6d7e4a59 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2791,7 +2791,7 @@ async fn full_access_confirmation_popup_snapshot() { .into_iter() .find(|preset| preset.id == "full-access") .expect("full access preset"); - chat.open_full_access_confirmation(preset); + chat.open_full_access_confirmation(preset, false); let popup = render_bottom_popup(&chat, 80); assert_snapshot!("full_access_confirmation_popup", popup); @@ -3112,7 +3112,7 @@ async fn approvals_popup_navigation_skips_disabled() { .expect("render approvals popup after disabled selection"); let screen = terminal.backend().vt100().screen().contents(); assert!( - screen.contains("Select Approval Mode"), + screen.contains("Update Model Permissions"), "popup should remain open after selecting a disabled entry" ); assert!( diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 9ad40e3af..8bd4eda85 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -947,6 +947,11 @@ pub(crate) fn new_session_info( "/approvals".into(), " - choose what Codex can do without approval".dim(), ]), + Line::from(vec![ + " ".into(), + "/permissions".into(), + " - choose what Codex is allowed to do".dim(), + ]), Line::from(vec![ " ".into(), "/model".into(), diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index ab63d6c7c..f825f32fa 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -14,6 +14,7 @@ pub enum SlashCommand { // more frequently used commands should be listed first. Model, Approvals, + Permissions, #[strum(serialize = "setup-elevated-sandbox")] ElevateSandbox, Experimental, @@ -60,6 +61,7 @@ impl SlashCommand { SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Collab => "change collaboration mode (experimental)", SlashCommand::Approvals => "choose what Codex can do without approval", + SlashCommand::Permissions => "choose what Codex is allowed to do", SlashCommand::ElevateSandbox => "set up elevated agent sandbox", SlashCommand::Experimental => "toggle beta features", SlashCommand::Mcp => "list configured MCP tools", @@ -86,6 +88,7 @@ impl SlashCommand { // | SlashCommand::Undo | SlashCommand::Model | SlashCommand::Approvals + | SlashCommand::Permissions | SlashCommand::ElevateSandbox | SlashCommand::Experimental | SlashCommand::Review