mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
fix(tui) Consolidate Permissions logic
This commit is contained in:
@@ -1747,12 +1747,8 @@ impl App {
|
||||
AppEvent::OpenAllModelsPopup { models } => {
|
||||
self.chat_widget.open_all_models_popup(models);
|
||||
}
|
||||
AppEvent::OpenFullAccessConfirmation {
|
||||
preset,
|
||||
return_to_permissions,
|
||||
} => {
|
||||
self.chat_widget
|
||||
.open_full_access_confirmation(preset, return_to_permissions);
|
||||
AppEvent::OpenFullAccessConfirmation { preset } => {
|
||||
self.chat_widget.open_full_access_confirmation(preset);
|
||||
}
|
||||
AppEvent::OpenWorldWritableWarningConfirmation {
|
||||
preset,
|
||||
|
||||
@@ -16,11 +16,11 @@ use codex_core::protocol::RateLimitSnapshot;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_utils_approval_presets::ApprovalPreset;
|
||||
|
||||
use crate::bottom_pane::ApprovalRequest;
|
||||
use crate::bottom_pane::StatusLineItem;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::permissions::PermissionsPreset;
|
||||
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
@@ -171,8 +171,7 @@ pub(crate) enum AppEvent {
|
||||
|
||||
/// Open the confirmation prompt before enabling full access mode.
|
||||
OpenFullAccessConfirmation {
|
||||
preset: ApprovalPreset,
|
||||
return_to_permissions: bool,
|
||||
preset: PermissionsPreset,
|
||||
},
|
||||
|
||||
/// Open the Windows world-writable directories warning.
|
||||
@@ -181,7 +180,7 @@ pub(crate) enum AppEvent {
|
||||
/// policy change and only acknowledges/dismisses the warning.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
OpenWorldWritableWarningConfirmation {
|
||||
preset: Option<ApprovalPreset>,
|
||||
preset: Option<PermissionsPreset>,
|
||||
/// Up to 3 sample world-writable directories to display in the warning.
|
||||
sample_paths: Vec<String>,
|
||||
/// If there are more than `sample_paths`, this carries the remaining count.
|
||||
@@ -193,25 +192,25 @@ pub(crate) enum AppEvent {
|
||||
/// Prompt to enable the Windows sandbox feature before using Agent mode.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
OpenWindowsSandboxEnablePrompt {
|
||||
preset: ApprovalPreset,
|
||||
preset: PermissionsPreset,
|
||||
},
|
||||
|
||||
/// Open the Windows sandbox fallback prompt after declining or failing elevation.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
OpenWindowsSandboxFallbackPrompt {
|
||||
preset: ApprovalPreset,
|
||||
preset: PermissionsPreset,
|
||||
},
|
||||
|
||||
/// Begin the elevated Windows sandbox setup flow.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
BeginWindowsSandboxElevatedSetup {
|
||||
preset: ApprovalPreset,
|
||||
preset: PermissionsPreset,
|
||||
},
|
||||
|
||||
/// Begin the non-elevated Windows sandbox setup flow.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
BeginWindowsSandboxLegacySetup {
|
||||
preset: ApprovalPreset,
|
||||
preset: PermissionsPreset,
|
||||
},
|
||||
|
||||
/// Begin a non-elevated grant of read access for an additional directory.
|
||||
@@ -230,7 +229,7 @@ pub(crate) enum AppEvent {
|
||||
/// Enable the Windows sandbox feature and switch to Agent mode.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
EnableWindowsSandboxForAgentMode {
|
||||
preset: ApprovalPreset,
|
||||
preset: PermissionsPreset,
|
||||
mode: WindowsSandboxEnableMode,
|
||||
},
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ use std::time::Instant;
|
||||
|
||||
use crate::bottom_pane::StatusLineItem;
|
||||
use crate::bottom_pane::StatusLineSetupView;
|
||||
use crate::permissions::PermissionsPreset;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::permissions::builtin_permissions_presets;
|
||||
use crate::permissions::visible_permissions_options;
|
||||
use crate::permissions::windows_degraded_sandbox_enabled;
|
||||
use crate::status::RateLimitWindowDisplay;
|
||||
use crate::status::format_directory_display;
|
||||
use crate::status::format_tokens_compact;
|
||||
@@ -242,8 +247,6 @@ use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_utils_approval_presets::ApprovalPreset;
|
||||
use codex_utils_approval_presets::builtin_approval_presets;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally";
|
||||
@@ -3338,7 +3341,7 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(preset) = builtin_approval_presets()
|
||||
let Some(preset) = builtin_permissions_presets()
|
||||
.into_iter()
|
||||
.find(|preset| preset.id == "auto")
|
||||
else {
|
||||
@@ -5335,125 +5338,14 @@ impl ChatWidget {
|
||||
|
||||
/// 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");
|
||||
let current_approval = self.config.permissions.approval_policy.value();
|
||||
let current_sandbox = self.config.permissions.sandbox_policy.get();
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
|
||||
#[cfg(target_os = "windows")]
|
||||
let windows_degraded_sandbox_enabled =
|
||||
matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let windows_degraded_sandbox_enabled = false;
|
||||
let items: Vec<SelectionItem> = visible_permissions_options(&self.config);
|
||||
|
||||
let windows_degraded_sandbox_enabled = windows_degraded_sandbox_enabled(&self.config);
|
||||
let show_elevate_sandbox_hint = codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
&& windows_degraded_sandbox_enabled
|
||||
&& 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 {
|
||||
"Default (non-admin sandbox)".to_string()
|
||||
} else {
|
||||
preset.label.to_string()
|
||||
};
|
||||
let description = Some(preset.description.replace(" (Identical to Agent mode)", ""));
|
||||
let disabled_reason = match self
|
||||
.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.can_set(&preset.approval)
|
||||
{
|
||||
Ok(()) => None,
|
||||
Err(err) => Some(err.to_string()),
|
||||
};
|
||||
let requires_confirmation = preset.id == "full-access"
|
||||
&& !self
|
||||
.config
|
||||
.notices
|
||||
.hide_full_access_warning
|
||||
.unwrap_or(false);
|
||||
let actions: Vec<SelectionAction> = if requires_confirmation {
|
||||
let preset_clone = preset.clone();
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenFullAccessConfirmation {
|
||||
preset: preset_clone.clone(),
|
||||
return_to_permissions: !include_read_only,
|
||||
});
|
||||
})]
|
||||
} else if preset.id == "auto" {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if WindowsSandboxLevel::from_config(&self.config)
|
||||
== WindowsSandboxLevel::Disabled
|
||||
{
|
||||
let preset_clone = preset.clone();
|
||||
if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
&& codex_core::windows_sandbox::sandbox_setup_is_complete(
|
||||
self.config.codex_home.as_path(),
|
||||
)
|
||||
{
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset_clone.clone(),
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
});
|
||||
})]
|
||||
} else {
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
|
||||
preset: preset_clone.clone(),
|
||||
});
|
||||
})]
|
||||
}
|
||||
} else if let Some((sample_paths, extra_count, failed_scan)) =
|
||||
self.world_writable_warning_details()
|
||||
{
|
||||
let preset_clone = preset.clone();
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
|
||||
preset: Some(preset_clone.clone()),
|
||||
sample_paths: sample_paths.clone(),
|
||||
extra_count,
|
||||
failed_scan,
|
||||
});
|
||||
})]
|
||||
} else {
|
||||
Self::approval_preset_actions(
|
||||
preset.approval,
|
||||
preset.sandbox.clone(),
|
||||
name.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Self::approval_preset_actions(
|
||||
preset.approval,
|
||||
preset.sandbox.clone(),
|
||||
name.clone(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Self::approval_preset_actions(preset.approval, preset.sandbox.clone(), name.clone())
|
||||
};
|
||||
items.push(SelectionItem {
|
||||
name,
|
||||
description,
|
||||
is_current,
|
||||
actions,
|
||||
dismiss_on_select: true,
|
||||
disabled_reason,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
&& items
|
||||
.iter()
|
||||
.any(|item| item.name == "Default (non-admin sandbox)");
|
||||
|
||||
let footer_note = show_elevate_sandbox_hint.then(|| {
|
||||
vec![
|
||||
@@ -5519,14 +5411,6 @@ impl ChatWidget {
|
||||
})]
|
||||
}
|
||||
|
||||
fn preset_matches_current(
|
||||
current_approval: AskForApproval,
|
||||
current_sandbox: &SandboxPolicy,
|
||||
preset: &ApprovalPreset,
|
||||
) -> bool {
|
||||
current_approval == preset.approval && *current_sandbox == preset.sandbox
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {
|
||||
if self
|
||||
@@ -5557,14 +5441,7 @@ impl ChatWidget {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn open_full_access_confirmation(
|
||||
&mut self,
|
||||
preset: ApprovalPreset,
|
||||
return_to_permissions: bool,
|
||||
) {
|
||||
let selected_name = preset.label.to_string();
|
||||
let approval = preset.approval;
|
||||
let sandbox = preset.sandbox;
|
||||
pub(crate) fn open_full_access_confirmation(&mut self, preset: PermissionsPreset) {
|
||||
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![
|
||||
@@ -5579,25 +5456,27 @@ impl ChatWidget {
|
||||
));
|
||||
let header = ColumnRenderable::with(header_children);
|
||||
|
||||
let mut accept_actions =
|
||||
Self::approval_preset_actions(approval, sandbox.clone(), selected_name.clone());
|
||||
let mut accept_actions = Self::approval_preset_actions(
|
||||
preset.approval,
|
||||
preset.sandbox.clone(),
|
||||
preset.label.to_string(),
|
||||
);
|
||||
accept_actions.push(Box::new(|tx| {
|
||||
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
|
||||
}));
|
||||
|
||||
let mut accept_and_remember_actions =
|
||||
Self::approval_preset_actions(approval, sandbox, selected_name);
|
||||
let mut accept_and_remember_actions = Self::approval_preset_actions(
|
||||
preset.approval,
|
||||
preset.sandbox.clone(),
|
||||
preset.label.to_string(),
|
||||
);
|
||||
accept_and_remember_actions.push(Box::new(|tx| {
|
||||
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
|
||||
tx.send(AppEvent::PersistFullAccessWarningAcknowledged);
|
||||
}));
|
||||
|
||||
let deny_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
if return_to_permissions {
|
||||
tx.send(AppEvent::OpenPermissionsPopup);
|
||||
} else {
|
||||
tx.send(AppEvent::OpenApprovalsPopup);
|
||||
}
|
||||
tx.send(AppEvent::OpenPermissionsPopup);
|
||||
})];
|
||||
|
||||
let items = vec![
|
||||
@@ -5635,7 +5514,7 @@ impl ChatWidget {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn open_world_writable_warning_confirmation(
|
||||
&mut self,
|
||||
preset: Option<ApprovalPreset>,
|
||||
preset: Option<PermissionsPreset>,
|
||||
sample_paths: Vec<String>,
|
||||
extra_count: usize,
|
||||
failed_scan: bool,
|
||||
@@ -5744,7 +5623,7 @@ impl ChatWidget {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_world_writable_warning_confirmation(
|
||||
&mut self,
|
||||
_preset: Option<ApprovalPreset>,
|
||||
_preset: Option<PermissionsPreset>,
|
||||
_sample_paths: Vec<String>,
|
||||
_extra_count: usize,
|
||||
_failed_scan: bool,
|
||||
@@ -5752,7 +5631,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) {
|
||||
pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: PermissionsPreset) {
|
||||
use ratatui_macros::line;
|
||||
|
||||
if !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED {
|
||||
@@ -5864,10 +5743,10 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {}
|
||||
pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: PermissionsPreset) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: ApprovalPreset) {
|
||||
pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: PermissionsPreset) {
|
||||
use ratatui_macros::line;
|
||||
|
||||
let mut lines = Vec::new();
|
||||
@@ -5943,13 +5822,13 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: ApprovalPreset) {}
|
||||
pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: PermissionsPreset) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) {
|
||||
if show_now
|
||||
&& WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled
|
||||
&& let Some(preset) = builtin_approval_presets()
|
||||
&& let Some(preset) = builtin_permissions_presets()
|
||||
.into_iter()
|
||||
.find(|preset| preset.id == "auto")
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 3092
|
||||
expression: popup
|
||||
---
|
||||
Update Model Permissions
|
||||
|
||||
› 1. 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.
|
||||
edit other files. (Identical to Agent mode)
|
||||
2. Full Access Codex can edit files outside this workspace and access the
|
||||
internet without asking for approval. Exercise caution when
|
||||
using.
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::bottom_pane::FeedbackAudience;
|
||||
use crate::bottom_pane::LocalImageAttachment;
|
||||
use crate::bottom_pane::MentionBinding;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use crate::permissions::builtin_permissions_presets;
|
||||
use crate::test_backend::VT100Backend;
|
||||
use crate::tui::FrameRequester;
|
||||
use assert_matches::assert_matches;
|
||||
@@ -91,7 +92,6 @@ use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_approval_presets::builtin_approval_presets;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
@@ -5064,10 +5064,11 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn preset_matching_requires_exact_workspace_write_settings() {
|
||||
let preset = builtin_approval_presets()
|
||||
let preset = builtin_permissions_presets()
|
||||
.into_iter()
|
||||
.find(|p| p.id == "auto")
|
||||
.expect("auto preset exists");
|
||||
let mut config = test_config().await;
|
||||
let current_sandbox = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::try_from("C:\\extra").unwrap()],
|
||||
read_only_access: Default::default(),
|
||||
@@ -5075,13 +5076,28 @@ async fn preset_matching_requires_exact_workspace_write_settings() {
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.unwrap();
|
||||
config
|
||||
.permissions
|
||||
.sandbox_policy
|
||||
.set(current_sandbox)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!ChatWidget::preset_matches_current(AskForApproval::OnRequest, ¤t_sandbox, &preset),
|
||||
!preset.to_selection_item(&config).is_current,
|
||||
"WorkspaceWrite with extra roots should not match the Default preset"
|
||||
);
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.unwrap();
|
||||
assert!(
|
||||
!ChatWidget::preset_matches_current(AskForApproval::Never, ¤t_sandbox, &preset),
|
||||
!preset.to_selection_item(&config).is_current,
|
||||
"approval mismatch should prevent matching the preset"
|
||||
);
|
||||
}
|
||||
@@ -5090,11 +5106,11 @@ async fn preset_matching_requires_exact_workspace_write_settings() {
|
||||
async fn full_access_confirmation_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
let preset = builtin_approval_presets()
|
||||
let preset = builtin_permissions_presets()
|
||||
.into_iter()
|
||||
.find(|preset| preset.id == "full-access")
|
||||
.find(|selection_item| selection_item.id == "full-access")
|
||||
.expect("full access preset");
|
||||
chat.open_full_access_confirmation(preset, false);
|
||||
chat.open_full_access_confirmation(preset);
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
assert_snapshot!("full_access_confirmation_popup", popup);
|
||||
@@ -5105,7 +5121,7 @@ async fn full_access_confirmation_popup_snapshot() {
|
||||
async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
let preset = builtin_approval_presets()
|
||||
let preset = builtin_permissions_presets()
|
||||
.into_iter()
|
||||
.find(|preset| preset.id == "auto")
|
||||
.expect("auto preset");
|
||||
@@ -5624,11 +5640,8 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation()
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
cells_before_confirmation.push(cell.display_lines(80));
|
||||
}
|
||||
AppEvent::OpenFullAccessConfirmation {
|
||||
preset,
|
||||
return_to_permissions,
|
||||
} => {
|
||||
open_confirmation_event = Some((preset, return_to_permissions));
|
||||
AppEvent::OpenFullAccessConfirmation { preset } => {
|
||||
open_confirmation_event = Some(preset);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -5639,9 +5652,8 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation()
|
||||
"did not expect history cell before confirming full access"
|
||||
);
|
||||
}
|
||||
let (preset, return_to_permissions) =
|
||||
open_confirmation_event.expect("expected full access confirmation event");
|
||||
chat.open_full_access_confirmation(preset, return_to_permissions);
|
||||
let preset = open_confirmation_event.expect("expected full access confirmation event");
|
||||
chat.open_full_access_confirmation(preset.clone());
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
assert!(
|
||||
|
||||
@@ -91,6 +91,7 @@ mod notifications;
|
||||
pub mod onboarding;
|
||||
mod oss_selection;
|
||||
mod pager_overlay;
|
||||
mod permissions;
|
||||
pub mod public_widgets;
|
||||
mod render;
|
||||
mod resume_picker;
|
||||
|
||||
243
codex-rs/tui/src/permissions/mod.rs
Normal file
243
codex-rs/tui/src/permissions/mod.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use crate::app_event::AppEvent;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::SelectionAction;
|
||||
use crate::bottom_pane::SelectionItem;
|
||||
use crate::history_cell;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol_config_types::WindowsSandboxLevel;
|
||||
|
||||
/// A simple preset pairing an approval policy with a sandbox policy.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PermissionsPreset {
|
||||
/// Stable identifier for the preset.
|
||||
pub id: &'static str,
|
||||
/// Display label shown in UIs.
|
||||
pub label: &'static str,
|
||||
/// Short human description shown next to the label in UIs.
|
||||
pub description: &'static str,
|
||||
/// Approval policy to apply.
|
||||
pub approval: AskForApproval,
|
||||
/// Sandbox policy to apply.
|
||||
pub sandbox: SandboxPolicy,
|
||||
}
|
||||
|
||||
/// Built-in list of approval presets that pair approval and sandbox policy.
|
||||
pub fn builtin_permissions_presets() -> Vec<PermissionsPreset> {
|
||||
vec![
|
||||
PermissionsPreset {
|
||||
id: "read-only",
|
||||
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,
|
||||
sandbox: SandboxPolicy::new_read_only_policy(),
|
||||
},
|
||||
PermissionsPreset {
|
||||
id: "auto",
|
||||
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(),
|
||||
},
|
||||
PermissionsPreset {
|
||||
id: "full-access",
|
||||
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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn visible_permissions_options(config: &Config) -> Vec<SelectionItem> {
|
||||
builtin_permissions_presets()
|
||||
.into_iter()
|
||||
.filter(|preset| preset.is_visible(config))
|
||||
.map(|preset| preset.to_selection_item(config))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl PermissionsPreset {
|
||||
pub(crate) fn is_visible(&self, config: &Config) -> bool {
|
||||
match self.id {
|
||||
"read-only" => cfg!(target_os = "windows"),
|
||||
"auto" => {
|
||||
!cfg!(target_os = "windows")
|
||||
|| codex_core::windows_sandbox::windows_sandbox_level_from_config(config)
|
||||
== WindowsSandboxLevel::Disabled
|
||||
}
|
||||
"full-access" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_selection_item(&self, config: &Config) -> SelectionItem {
|
||||
let name = if self.id == "auto" && windows_degraded_sandbox_enabled(config) {
|
||||
"Default (non-admin sandbox)".to_string()
|
||||
} else {
|
||||
self.label.to_string()
|
||||
};
|
||||
|
||||
SelectionItem {
|
||||
name,
|
||||
description: Some(self.description.to_string()),
|
||||
is_current: self.is_current(config),
|
||||
actions: self.actions(config),
|
||||
dismiss_on_select: true,
|
||||
disabled_reason: self.disabled_reason(config),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_current(&self, config: &Config) -> bool {
|
||||
self.approval == config.permissions.approval_policy.value()
|
||||
&& self.sandbox == *config.permissions.sandbox_policy.get()
|
||||
}
|
||||
|
||||
fn disabled_reason(&self, config: &Config) -> Option<String> {
|
||||
let disabled_sandbox_reason = match config.permissions.sandbox_policy.can_set(&self.sandbox)
|
||||
{
|
||||
Ok(()) => None,
|
||||
Err(err) => Some(err.to_string()),
|
||||
};
|
||||
if disabled_sandbox_reason.is_some() {
|
||||
return disabled_sandbox_reason;
|
||||
}
|
||||
|
||||
let disabled_approval_reason =
|
||||
match config.permissions.approval_policy.can_set(&self.approval) {
|
||||
Ok(()) => None,
|
||||
Err(err) => Some(err.to_string()),
|
||||
};
|
||||
if disabled_approval_reason.is_some() {
|
||||
return disabled_approval_reason;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn actions(&self, config: &Config) -> Vec<SelectionAction> {
|
||||
let requires_full_access_confirmation =
|
||||
self.id == "full-access" && !config.notices.hide_full_access_warning.unwrap_or(false);
|
||||
if requires_full_access_confirmation {
|
||||
let preset = self.clone();
|
||||
return vec![Box::new(move |tx: &AppEventSender| {
|
||||
tx.send(AppEvent::OpenFullAccessConfirmation {
|
||||
preset: preset.clone(),
|
||||
});
|
||||
})];
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(actions) = windows_permissions_actions(self, config) {
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
let approval = self.approval;
|
||||
let sandbox = self.sandbox.clone();
|
||||
let label = self.label.to_string();
|
||||
vec![Box::new(move |tx: &AppEventSender| {
|
||||
let sandbox_clone = sandbox.clone();
|
||||
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: Some(approval),
|
||||
sandbox_policy: Some(sandbox_clone.clone()),
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
|
||||
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone));
|
||||
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_info_event(format!("Permissions updated to {label}"), None),
|
||||
)));
|
||||
})]
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle windows-specific actions for auto preset. Returns Some when it should take precedence over the approval preset actions.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_permissions_actions(
|
||||
preset: &PermissionsPreset,
|
||||
config: &Config,
|
||||
) -> Option<Vec<SelectionAction>> {
|
||||
if preset.id != "auto" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if codex_core::windows_sandbox::windows_sandbox_level_from_config(config)
|
||||
== WindowsSandboxLevel::Disabled
|
||||
{
|
||||
let preset_clone = preset.clone();
|
||||
if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
&& codex_core::windows_sandbox::sandbox_setup_is_complete(config.codex_home.as_path())
|
||||
{
|
||||
Some(vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset_clone.clone(),
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
});
|
||||
})])
|
||||
} else {
|
||||
Some(vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
|
||||
preset: preset_clone.clone(),
|
||||
});
|
||||
})])
|
||||
}
|
||||
} else if let Some((sample_paths, extra_count, failed_scan)) =
|
||||
world_writable_warning_details(config)
|
||||
{
|
||||
let preset_clone = preset.clone();
|
||||
Some(vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
|
||||
preset: Some(preset_clone.clone()),
|
||||
sample_paths: sample_paths.clone(),
|
||||
extra_count,
|
||||
failed_scan,
|
||||
});
|
||||
})])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn windows_degraded_sandbox_enabled(config: &Config) -> bool {
|
||||
let windows_sandbox_level =
|
||||
codex_core::windows_sandbox::windows_sandbox_level_from_config(config);
|
||||
matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn world_writable_warning_details(config: &Config) -> Option<(Vec<String>, usize, bool)> {
|
||||
if config.notices.hide_world_writable_warning.unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let cwd = config.cwd.clone();
|
||||
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
|
||||
match codex_windows_sandbox::apply_world_writable_scan_and_denies(
|
||||
config.codex_home.as_path(),
|
||||
cwd.as_path(),
|
||||
&env_map,
|
||||
config.permissions.sandbox_policy.get(),
|
||||
Some(config.codex_home.as_path()),
|
||||
) {
|
||||
Ok(_) => None,
|
||||
Err(_) => Some((Vec::new(), 0, true)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn windows_degraded_sandbox_enabled(_config: &Config) -> bool {
|
||||
false
|
||||
}
|
||||
Reference in New Issue
Block a user