Compare commits

...

4 Commits

Author SHA1 Message Date
Dylan Hurd
db3b2b286a clippy, fix visibility 2026-02-17 15:17:57 -08:00
Dylan Hurd
4dd25dda04 rm approval-presets 2026-02-17 15:17:31 -08:00
Dylan Hurd
3399b9645c fix(tui) Consolidate Permissions logic 2026-02-17 15:17:31 -08:00
Dylan Hurd
f45a4bfce1 fix(tui) remove config check for trusted setting 2026-02-17 15:17:31 -08:00
14 changed files with 317 additions and 326 deletions

8
codex-rs/Cargo.lock generated
View File

@@ -2290,7 +2290,6 @@ dependencies = [
"codex-protocol",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-utils-elapsed",
@@ -2364,13 +2363,6 @@ dependencies = [
"ts-rs",
]
[[package]]
name = "codex-utils-approval-presets"
version = "0.0.0"
dependencies = [
"codex-core",
]
[[package]]
name = "codex-utils-cache"
version = "0.0.0"

View File

@@ -55,7 +55,6 @@ members = [
"utils/sandbox-summary",
"utils/sanitizer",
"utils/sleep-inhibitor",
"utils/approval-presets",
"utils/oss",
"utils/fuzzy-match",
"codex-client",
@@ -117,7 +116,6 @@ codex-state = { path = "state" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-tui = { path = "tui" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
codex-utils-approval-presets = { path = "utils/approval-presets" }
codex-utils-cache = { path = "utils/cache" }
codex-utils-cargo-bin = { path = "utils/cargo-bin" }
codex-utils-cli = { path = "utils/cli" }

View File

@@ -181,10 +181,6 @@ pub struct Config {
/// using backend-specific headers or URLs to enforce this.
pub enforce_residency: Constrained<Option<ResidencyRequirement>>,
/// True if the user passed in an override or set a value in config.toml
/// for either of approval_policy or sandbox_mode.
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
/// When `true`, `AgentReasoning` events emitted by the backend will be
/// suppressed from the frontend output. This can reduce visual noise when
/// users are only interested in the final agent responses.
@@ -1543,9 +1539,6 @@ impl Config {
let active_project = cfg
.get_active_project(&resolved_cwd)
.unwrap_or(ProjectConfig { trust_level: None });
let sandbox_mode_was_explicit = sandbox_mode.is_some()
|| config_profile.sandbox_mode.is_some()
|| cfg.sandbox_mode.is_some();
let windows_sandbox_level = match windows_sandbox_mode {
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
@@ -1566,9 +1559,6 @@ impl Config {
}
}
}
let approval_policy_was_explicit = approval_policy_override.is_some()
|| config_profile.approval_policy.is_some()
|| cfg.approval_policy.is_some();
let mut approval_policy = approval_policy_override
.or(config_profile.approval_policy)
.or(cfg.approval_policy)
@@ -1581,9 +1571,7 @@ impl Config {
AskForApproval::default()
}
});
if !approval_policy_was_explicit
&& let Err(err) = requirements.approval_policy.can_set(&approval_policy)
{
if let Err(err) = requirements.approval_policy.can_set(&approval_policy) {
tracing::warn!(
error = %err,
"default approval policy is disallowed by requirements; falling back to required default"
@@ -1592,10 +1580,6 @@ impl Config {
}
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
.unwrap_or(WebSearchMode::Cached);
// TODO(dylan): We should be able to leverage ConfigLayerStack so that
// we can reliably check this at every config level.
let did_user_set_custom_approval_policy_or_sandbox_mode =
approval_policy_was_explicit || sandbox_mode_was_explicit;
let mut model_providers = built_in_model_providers();
// Merge user-defined providers into the built-in list.
@@ -1823,7 +1807,6 @@ impl Config {
macos_seatbelt_profile_extensions: None,
},
enforce_residency: enforce_residency.value,
did_user_set_custom_approval_policy_or_sandbox_mode,
notify: cfg.notify,
user_instructions,
base_instructions,
@@ -2807,7 +2790,6 @@ profile = "project"
config.permissions.sandbox_policy.get(),
&SandboxPolicy::DangerFullAccess
));
assert!(config.did_user_set_custom_approval_policy_or_sandbox_mode);
Ok(())
}
@@ -4172,7 +4154,6 @@ model_verbosity = "high"
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
@@ -4285,7 +4266,6 @@ model_verbosity = "high"
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
@@ -4396,7 +4376,6 @@ model_verbosity = "high"
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
@@ -4493,7 +4472,6 @@ model_verbosity = "high"
macos_seatbelt_profile_extensions: None,
},
enforce_residency: Constrained::allow_any(None),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
@@ -4560,24 +4538,6 @@ model_verbosity = "high"
Ok(())
}
#[test]
fn test_did_user_set_custom_approval_policy_or_sandbox_mode_defaults_no() -> anyhow::Result<()>
{
let fixture = create_test_fixture()?;
let config = Config::load_from_base_config_with_overrides(
fixture.cfg.clone(),
ConfigOverrides {
..Default::default()
},
fixture.codex_home(),
)?;
assert!(config.did_user_set_custom_approval_policy_or_sandbox_mode);
Ok(())
}
#[test]
fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()>
{

View File

@@ -39,7 +39,6 @@ codex-login = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
codex-state = { workspace = true }
codex-utils-approval-presets = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cli = { workspace = true }
codex-utils-elapsed = { workspace = true }

View File

@@ -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,
@@ -2358,9 +2354,6 @@ impl App {
));
}
}
AppEvent::OpenApprovalsPopup => {
self.chat_widget.open_approvals_popup();
}
AppEvent::OpenAgentPicker => {
self.open_agent_picker().await;
}

View File

@@ -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,
},
@@ -278,9 +277,6 @@ pub(crate) enum AppEvent {
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
SkipNextWorldWritableScan,
/// Re-open the approval presets popup.
OpenApprovalsPopup,
/// Open the skills list popup.
OpenSkillsList,

View File

@@ -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 {
@@ -5328,132 +5331,16 @@ impl ChatWidget {
);
}
/// Open the permissions popup (alias for /permissions).
pub(crate) fn open_approvals_popup(&mut self) {
self.open_permissions_popup();
}
/// 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 +5406,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 +5436,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 +5451,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 +5509,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 +5618,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 +5626,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 {
@@ -5785,7 +5659,7 @@ impl ChatWidget {
name: "Go back".to_string(),
description: None,
actions: vec![Box::new(|tx| {
tx.send(AppEvent::OpenApprovalsPopup);
tx.send(AppEvent::OpenPermissionsPopup);
})],
dismiss_on_select: true,
..Default::default()
@@ -5864,10 +5738,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 +5817,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")
{

View File

@@ -1,6 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 3092
expression: popup
---
Update Model Permissions

View File

@@ -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;
@@ -3076,7 +3076,7 @@ async fn ctrl_d_quits_without_prompt() {
async fn ctrl_d_with_modal_open_does_not_quit() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
chat.open_approvals_popup();
chat.open_permissions_popup();
chat.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL));
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
@@ -5024,7 +5024,7 @@ async fn approvals_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.config.notices.hide_full_access_warning = None;
chat.open_approvals_popup();
chat.open_permissions_popup();
let popup = render_bottom_popup(&chat, 80);
#[cfg(target_os = "windows")]
@@ -5045,7 +5045,7 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() {
chat.set_feature_enabled(Feature::WindowsSandbox, true);
chat.set_feature_enabled(Feature::WindowsSandboxElevated, false);
chat.open_approvals_popup();
chat.open_permissions_popup();
let popup = render_bottom_popup(&chat, 80);
assert!(
@@ -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, &current_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, &current_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");
@@ -5383,7 +5399,7 @@ async fn approvals_popup_shows_disabled_presets() {
)),
})
.expect("construct constrained approval policy");
chat.open_approvals_popup();
chat.open_permissions_popup();
let width = 80;
let height = chat.desired_height(width);
@@ -5416,7 +5432,7 @@ async fn approvals_popup_navigation_skips_disabled() {
_ => Err(invalid_value(candidate.to_string(), "[on-request]")),
})
.expect("construct constrained approval policy");
chat.open_approvals_popup();
chat.open_permissions_popup();
// The approvals popup is the active bottom-pane view; drive navigation via chat handle_key_event.
// Start selected at idx 0 (enabled), move down twice; the disabled option should be skipped
@@ -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);
let popup = render_bottom_popup(&chat, 80);
assert!(

View File

@@ -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;
@@ -925,15 +926,8 @@ async fn load_config_or_exit_with_fallback_cwd(
}
}
/// Determine if user has configured a sandbox / approval policy,
/// or if the current cwd project is already trusted. If not, we need to
/// show the trust screen.
/// Determine if the user has decided whether to trust the current directory.
fn should_show_trust_screen(config: &Config) -> bool {
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
// Respect explicit approval/sandbox overrides made by the user.
return false;
}
// otherwise, show only if no trust decision has been made
config.active_project.trust_level.is_none()
}
@@ -986,7 +980,6 @@ mod tests {
async fn windows_shows_trust_prompt_without_sandbox() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let mut config = build_config(&temp_dir).await?;
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
config.active_project = ProjectConfig { trust_level: None };
config.set_windows_sandbox_enabled(false);
@@ -1002,7 +995,6 @@ mod tests {
async fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let mut config = build_config(&temp_dir).await?;
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
config.active_project = ProjectConfig { trust_level: None };
config.set_windows_sandbox_enabled(true);
@@ -1025,7 +1017,6 @@ mod tests {
use codex_protocol::config_types::TrustLevel;
let temp_dir = TempDir::new()?;
let mut config = build_config(&temp_dir).await?;
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
config.active_project = ProjectConfig {
trust_level: Some(TrustLevel::Untrusted),
};

View File

@@ -0,0 +1,240 @@
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;
#[cfg(target_os = "windows")]
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.",
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(PermissionsPreset::is_visible)
.map(|preset| preset.to_selection_item(config))
.collect()
}
impl PermissionsPreset {
fn is_visible(&self) -> bool {
matches!(self.id, "auto" | "full-access")
|| (cfg!(target_os = "windows") && matches!(self.id, "read-only"))
}
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 = if self.id == "auto" && windows_degraded_sandbox_enabled(config) {
"Default (non-admin sandbox)".to_string()
} else {
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
}

View File

@@ -1,6 +0,0 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "approval-presets",
crate_name = "codex_utils_approval_presets",
)

View File

@@ -1,11 +0,0 @@
[package]
name = "codex-utils-approval-presets"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
codex-core = { workspace = true }

View File

@@ -1,46 +0,0 @@
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
/// A simple preset pairing an approval policy with a sandbox policy.
#[derive(Debug, Clone)]
pub struct ApprovalPreset {
/// 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.
///
/// Keep this UI-agnostic so it can be reused by both TUI and MCP server.
pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
vec![
ApprovalPreset {
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(),
},
ApprovalPreset {
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(),
},
ApprovalPreset {
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,
},
]
}