feat(tui): Add confirmation prompt for enabling full access approvals (#4980)
## Summary Adds a confirmation screen when a user attempts to select Full Access via the `/approvals` flow in the TUI. If the user selects the remember option, the preference is persisted to config.toml as `full_access_warning_acknowledged`, so they will not be prompted again. ## Testing - [x] Adds snapshot test coverage for the approvals flow and the confirmation flow <img width="865" height="187" alt="Screenshot 2025-10-08 at 6 04 59 PM" src="https://github.com/user-attachments/assets/fd1dac62-28b0-4835-ba91-5da6dc5ec4c4" /> ------ https://chatgpt.com/codex/tasks/task_i_68e6c5c458088322a28efa3207058180 --------- Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Co-authored-by: Fouad Matin <fouad@openai.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use crate::config_types::DEFAULT_OTEL_ENVIRONMENT;
|
|||||||
use crate::config_types::History;
|
use crate::config_types::History;
|
||||||
use crate::config_types::McpServerConfig;
|
use crate::config_types::McpServerConfig;
|
||||||
use crate::config_types::McpServerTransportConfig;
|
use crate::config_types::McpServerTransportConfig;
|
||||||
|
use crate::config_types::Notice;
|
||||||
use crate::config_types::Notifications;
|
use crate::config_types::Notifications;
|
||||||
use crate::config_types::OtelConfig;
|
use crate::config_types::OtelConfig;
|
||||||
use crate::config_types::OtelConfigToml;
|
use crate::config_types::OtelConfigToml;
|
||||||
@@ -242,6 +243,9 @@ pub struct Config {
|
|||||||
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
||||||
pub windows_wsl_setup_acknowledged: bool,
|
pub windows_wsl_setup_acknowledged: bool,
|
||||||
|
|
||||||
|
/// Collection of various notices we show the user
|
||||||
|
pub notices: Notice,
|
||||||
|
|
||||||
/// When true, disables burst-paste detection for typed input entirely.
|
/// When true, disables burst-paste detection for typed input entirely.
|
||||||
/// All characters are inserted as they are received, and no buffering
|
/// All characters are inserted as they are received, and no buffering
|
||||||
/// or placeholder replacement will occur for fast keypress bursts.
|
/// or placeholder replacement will occur for fast keypress bursts.
|
||||||
@@ -557,6 +561,54 @@ pub fn set_windows_wsl_setup_acknowledged(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persist the acknowledgement flag for the full access warning prompt.
|
||||||
|
pub fn set_hide_full_access_warning(codex_home: &Path, acknowledged: bool) -> anyhow::Result<()> {
|
||||||
|
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||||
|
let mut doc = match std::fs::read_to_string(config_path.clone()) {
|
||||||
|
Ok(s) => s.parse::<DocumentMut>()?,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let notices_table = load_or_create_top_level_table(&mut doc, Notice::TABLE_KEY)?;
|
||||||
|
|
||||||
|
notices_table["hide_full_access_warning"] = toml_edit::value(acknowledged);
|
||||||
|
|
||||||
|
std::fs::create_dir_all(codex_home)?;
|
||||||
|
let tmp_file = NamedTempFile::new_in(codex_home)?;
|
||||||
|
std::fs::write(tmp_file.path(), doc.to_string())?;
|
||||||
|
tmp_file.persist(config_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_or_create_top_level_table<'a>(
|
||||||
|
doc: &'a mut DocumentMut,
|
||||||
|
key: &str,
|
||||||
|
) -> anyhow::Result<&'a mut toml_edit::Table> {
|
||||||
|
let mut created_table = false;
|
||||||
|
|
||||||
|
let root = doc.as_table_mut();
|
||||||
|
let needs_table =
|
||||||
|
!root.contains_key(key) || root.get(key).and_then(|item| item.as_table()).is_none();
|
||||||
|
if needs_table {
|
||||||
|
root.insert(key, toml_edit::table());
|
||||||
|
created_table = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(table) = doc[key].as_table_mut() else {
|
||||||
|
return Err(anyhow::anyhow!(format!(
|
||||||
|
"table [{key}] missing after initialization"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
if created_table {
|
||||||
|
table.set_implicit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(table)
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_profile_table<'a>(
|
fn ensure_profile_table<'a>(
|
||||||
doc: &'a mut DocumentMut,
|
doc: &'a mut DocumentMut,
|
||||||
profile_name: &str,
|
profile_name: &str,
|
||||||
@@ -832,6 +884,10 @@ pub struct ConfigToml {
|
|||||||
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
||||||
pub windows_wsl_setup_acknowledged: Option<bool>,
|
pub windows_wsl_setup_acknowledged: Option<bool>,
|
||||||
|
|
||||||
|
/// Collection of in-product notices (different from notifications)
|
||||||
|
/// See [`crate::config_types::Notices`] for more details
|
||||||
|
pub notice: Option<Notice>,
|
||||||
|
|
||||||
/// Legacy, now use features
|
/// Legacy, now use features
|
||||||
pub experimental_instructions_file: Option<PathBuf>,
|
pub experimental_instructions_file: Option<PathBuf>,
|
||||||
pub experimental_use_exec_command_tool: Option<bool>,
|
pub experimental_use_exec_command_tool: Option<bool>,
|
||||||
@@ -1247,6 +1303,7 @@ impl Config {
|
|||||||
active_profile: active_profile_name,
|
active_profile: active_profile_name,
|
||||||
active_project,
|
active_project,
|
||||||
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
|
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
|
||||||
|
notices: cfg.notice.unwrap_or_default(),
|
||||||
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false),
|
||||||
tui_notifications: cfg
|
tui_notifications: cfg
|
||||||
.tui
|
.tui
|
||||||
@@ -2330,6 +2387,7 @@ model_verbosity = "high"
|
|||||||
active_profile: Some("o3".to_string()),
|
active_profile: Some("o3".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
|
notices: Default::default(),
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
@@ -2396,6 +2454,7 @@ model_verbosity = "high"
|
|||||||
active_profile: Some("gpt3".to_string()),
|
active_profile: Some("gpt3".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
|
notices: Default::default(),
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
@@ -2477,6 +2536,7 @@ model_verbosity = "high"
|
|||||||
active_profile: Some("zdr".to_string()),
|
active_profile: Some("zdr".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
|
notices: Default::default(),
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
@@ -2544,6 +2604,7 @@ model_verbosity = "high"
|
|||||||
active_profile: Some("gpt5".to_string()),
|
active_profile: Some("gpt5".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
|
notices: Default::default(),
|
||||||
disable_paste_burst: false,
|
disable_paste_burst: false,
|
||||||
tui_notifications: Default::default(),
|
tui_notifications: Default::default(),
|
||||||
otel: OtelConfig::default(),
|
otel: OtelConfig::default(),
|
||||||
|
|||||||
@@ -322,6 +322,20 @@ pub struct Tui {
|
|||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Settings for notices we display to users via the tui and app-server clients
|
||||||
|
/// (primarily the Codex IDE extension). NOTE: these are different from
|
||||||
|
/// notifications - notices are warnings, NUX screens, acknowledgements, etc.
|
||||||
|
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Notice {
|
||||||
|
/// Tracks whether the user has acknowledged the full access warning prompt.
|
||||||
|
pub hide_full_access_warning: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notice {
|
||||||
|
/// used by set_hide_full_access_warning until we refactor config updates
|
||||||
|
pub(crate) const TABLE_KEY: &'static str = "notice";
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||||
pub struct SandboxWorkspaceWrite {
|
pub struct SandboxWorkspaceWrite {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use codex_core::AuthManager;
|
|||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::persist_model_selection;
|
use codex_core::config::persist_model_selection;
|
||||||
|
use codex_core::config::set_hide_full_access_warning;
|
||||||
use codex_core::model_family::find_family_for_model;
|
use codex_core::model_family::find_family_for_model;
|
||||||
use codex_core::protocol::SessionSource;
|
use codex_core::protocol::SessionSource;
|
||||||
use codex_core::protocol::TokenUsage;
|
use codex_core::protocol::TokenUsage;
|
||||||
@@ -334,6 +335,9 @@ impl App {
|
|||||||
AppEvent::OpenReasoningPopup { model, presets } => {
|
AppEvent::OpenReasoningPopup { model, presets } => {
|
||||||
self.chat_widget.open_reasoning_popup(model, presets);
|
self.chat_widget.open_reasoning_popup(model, presets);
|
||||||
}
|
}
|
||||||
|
AppEvent::OpenFullAccessConfirmation { preset } => {
|
||||||
|
self.chat_widget.open_full_access_confirmation(preset);
|
||||||
|
}
|
||||||
AppEvent::PersistModelSelection { model, effort } => {
|
AppEvent::PersistModelSelection { model, effort } => {
|
||||||
let profile = self.active_profile.as_deref();
|
let profile = self.active_profile.as_deref();
|
||||||
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
||||||
@@ -379,6 +383,23 @@ impl App {
|
|||||||
AppEvent::UpdateSandboxPolicy(policy) => {
|
AppEvent::UpdateSandboxPolicy(policy) => {
|
||||||
self.chat_widget.set_sandbox_policy(policy);
|
self.chat_widget.set_sandbox_policy(policy);
|
||||||
}
|
}
|
||||||
|
AppEvent::UpdateFullAccessWarningAcknowledged(ack) => {
|
||||||
|
self.chat_widget.set_full_access_warning_acknowledged(ack);
|
||||||
|
}
|
||||||
|
AppEvent::PersistFullAccessWarningAcknowledged => {
|
||||||
|
if let Err(err) = set_hide_full_access_warning(&self.config.codex_home, true) {
|
||||||
|
tracing::error!(
|
||||||
|
error = %err,
|
||||||
|
"failed to persist full access warning acknowledgement"
|
||||||
|
);
|
||||||
|
self.chat_widget.add_error_message(format!(
|
||||||
|
"Failed to save full access confirmation preference: {err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppEvent::OpenApprovalsPopup => {
|
||||||
|
self.chat_widget.open_approvals_popup();
|
||||||
|
}
|
||||||
AppEvent::OpenReviewBranchPicker(cwd) => {
|
AppEvent::OpenReviewBranchPicker(cwd) => {
|
||||||
self.chat_widget.show_review_branch_picker(&cwd).await;
|
self.chat_widget.show_review_branch_picker(&cwd).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_common::approval_presets::ApprovalPreset;
|
||||||
use codex_common::model_presets::ModelPreset;
|
use codex_common::model_presets::ModelPreset;
|
||||||
use codex_core::protocol::ConversationPathResponseEvent;
|
use codex_core::protocol::ConversationPathResponseEvent;
|
||||||
use codex_core::protocol::Event;
|
use codex_core::protocol::Event;
|
||||||
@@ -67,12 +68,26 @@ pub(crate) enum AppEvent {
|
|||||||
presets: Vec<ModelPreset>,
|
presets: Vec<ModelPreset>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Open the confirmation prompt before enabling full access mode.
|
||||||
|
OpenFullAccessConfirmation {
|
||||||
|
preset: ApprovalPreset,
|
||||||
|
},
|
||||||
|
|
||||||
/// Update the current approval policy in the running app and widget.
|
/// Update the current approval policy in the running app and widget.
|
||||||
UpdateAskForApprovalPolicy(AskForApproval),
|
UpdateAskForApprovalPolicy(AskForApproval),
|
||||||
|
|
||||||
/// Update the current sandbox policy in the running app and widget.
|
/// Update the current sandbox policy in the running app and widget.
|
||||||
UpdateSandboxPolicy(SandboxPolicy),
|
UpdateSandboxPolicy(SandboxPolicy),
|
||||||
|
|
||||||
|
/// Update whether the full access warning prompt has been acknowledged.
|
||||||
|
UpdateFullAccessWarningAcknowledged(bool),
|
||||||
|
|
||||||
|
/// Persist the acknowledgement flag for the full access warning prompt.
|
||||||
|
PersistFullAccessWarningAcknowledged,
|
||||||
|
|
||||||
|
/// Re-open the approval presets popup.
|
||||||
|
OpenApprovalsPopup,
|
||||||
|
|
||||||
/// Forwarded conversation history snapshot from the current conversation.
|
/// Forwarded conversation history snapshot from the current conversation.
|
||||||
ConversationHistory(ConversationPathResponseEvent),
|
ConversationHistory(ConversationPathResponseEvent),
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,13 @@ use ratatui::buffer::Buffer;
|
|||||||
use ratatui::layout::Constraint;
|
use ratatui::layout::Constraint;
|
||||||
use ratatui::layout::Layout;
|
use ratatui::layout::Layout;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::Color;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::WidgetRef;
|
||||||
|
use ratatui::widgets::Wrap;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
@@ -85,6 +88,7 @@ use crate::history_cell::HistoryCell;
|
|||||||
use crate::history_cell::McpToolCallCell;
|
use crate::history_cell::McpToolCallCell;
|
||||||
use crate::markdown::append_markdown;
|
use crate::markdown::append_markdown;
|
||||||
use crate::render::renderable::ColumnRenderable;
|
use crate::render::renderable::ColumnRenderable;
|
||||||
|
use crate::render::renderable::Renderable;
|
||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
use crate::status::RateLimitSnapshotDisplay;
|
use crate::status::RateLimitSnapshotDisplay;
|
||||||
use crate::text_formatting::truncate_text;
|
use crate::text_formatting::truncate_text;
|
||||||
@@ -1819,22 +1823,24 @@ impl ChatWidget {
|
|||||||
for preset in presets.into_iter() {
|
for preset in presets.into_iter() {
|
||||||
let is_current =
|
let is_current =
|
||||||
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
||||||
let approval = preset.approval;
|
|
||||||
let sandbox = preset.sandbox.clone();
|
|
||||||
let name = preset.label.to_string();
|
let name = preset.label.to_string();
|
||||||
let description = Some(preset.description.to_string());
|
let description = Some(preset.description.to_string());
|
||||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
let requires_confirmation = preset.id == "full-access"
|
||||||
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
&& !self
|
||||||
cwd: None,
|
.config
|
||||||
approval_policy: Some(approval),
|
.notices
|
||||||
sandbox_policy: Some(sandbox.clone()),
|
.hide_full_access_warning
|
||||||
model: None,
|
.unwrap_or(false);
|
||||||
effort: None,
|
let actions: Vec<SelectionAction> = if requires_confirmation {
|
||||||
summary: None,
|
let preset_clone = preset.clone();
|
||||||
}));
|
vec![Box::new(move |tx| {
|
||||||
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
|
tx.send(AppEvent::OpenFullAccessConfirmation {
|
||||||
tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone()));
|
preset: preset_clone.clone(),
|
||||||
})];
|
});
|
||||||
|
})]
|
||||||
|
} else {
|
||||||
|
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
||||||
|
};
|
||||||
items.push(SelectionItem {
|
items.push(SelectionItem {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -1853,6 +1859,89 @@ impl ChatWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn approval_preset_actions(
|
||||||
|
approval: AskForApproval,
|
||||||
|
sandbox: SandboxPolicy,
|
||||||
|
) -> Vec<SelectionAction> {
|
||||||
|
vec![Box::new(move |tx| {
|
||||||
|
let sandbox_clone = sandbox.clone();
|
||||||
|
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||||
|
cwd: None,
|
||||||
|
approval_policy: Some(approval),
|
||||||
|
sandbox_policy: Some(sandbox_clone.clone()),
|
||||||
|
model: None,
|
||||||
|
effort: None,
|
||||||
|
summary: None,
|
||||||
|
}));
|
||||||
|
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
|
||||||
|
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone));
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) {
|
||||||
|
let approval = preset.approval;
|
||||||
|
let sandbox = preset.sandbox;
|
||||||
|
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![
|
||||||
|
"When Codex runs with full access, it can edit any file on your computer and run commands with network, without your approval. "
|
||||||
|
.into(),
|
||||||
|
"Exercise caution when enabling full access. This significantly increases the risk of data loss, leaks, or unexpected behavior."
|
||||||
|
.fg(Color::Red),
|
||||||
|
]);
|
||||||
|
header_children.push(Box::new(title_line));
|
||||||
|
header_children.push(Box::new(
|
||||||
|
Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }),
|
||||||
|
));
|
||||||
|
let header = ColumnRenderable::with(header_children);
|
||||||
|
|
||||||
|
let mut accept_actions = Self::approval_preset_actions(approval, sandbox.clone());
|
||||||
|
accept_actions.push(Box::new(|tx| {
|
||||||
|
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut accept_and_remember_actions = Self::approval_preset_actions(approval, sandbox);
|
||||||
|
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(|tx| {
|
||||||
|
tx.send(AppEvent::OpenApprovalsPopup);
|
||||||
|
})];
|
||||||
|
|
||||||
|
let items = vec![
|
||||||
|
SelectionItem {
|
||||||
|
name: "Yes, continue anyway".to_string(),
|
||||||
|
description: Some("Apply full access for this session".to_string()),
|
||||||
|
actions: accept_actions,
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
SelectionItem {
|
||||||
|
name: "Yes, and don't ask again".to_string(),
|
||||||
|
description: Some("Enable full access and remember this choice".to_string()),
|
||||||
|
actions: accept_and_remember_actions,
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
SelectionItem {
|
||||||
|
name: "Cancel".to_string(),
|
||||||
|
description: Some("Go back without enabling full access".to_string()),
|
||||||
|
actions: deny_actions,
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
|
items,
|
||||||
|
header: Box::new(header),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the approval policy in the widget's config copy.
|
/// Set the approval policy in the widget's config copy.
|
||||||
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
||||||
self.config.approval_policy = policy;
|
self.config.approval_policy = policy;
|
||||||
@@ -1863,6 +1952,10 @@ impl ChatWidget {
|
|||||||
self.config.sandbox_policy = policy;
|
self.config.sandbox_policy = policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {
|
||||||
|
self.config.notices.hide_full_access_warning = Some(acknowledged);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the reasoning effort in the widget's config copy.
|
/// Set the reasoning effort in the widget's config copy.
|
||||||
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||||
self.config.model_reasoning_effort = effort;
|
self.config.model_reasoning_effort = effort;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests.rs
|
||||||
|
expression: popup
|
||||||
|
---
|
||||||
|
Select Approval Mode
|
||||||
|
|
||||||
|
› 1. Read Only (current) Codex can read files and answer questions. Codex
|
||||||
|
requires approval to make edits, run commands, or
|
||||||
|
access network
|
||||||
|
2. Auto Codex can read files, make edits, and run commands
|
||||||
|
in the workspace. Codex requires approval to work
|
||||||
|
outside the workspace or access network
|
||||||
|
3. Full Access Codex can read files, make edits, and run commands
|
||||||
|
with network access, without approval. Exercise
|
||||||
|
caution
|
||||||
|
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests.rs
|
||||||
|
expression: popup
|
||||||
|
---
|
||||||
|
Enable full access?
|
||||||
|
When Codex runs with full access, it can edit any file on your computer and
|
||||||
|
run commands with network, without your approval. Exercise caution when
|
||||||
|
enabling full access. This significantly increases the risk of data loss,
|
||||||
|
leaks, or unexpected behavior.
|
||||||
|
|
||||||
|
› 1. Yes, continue anyway Apply full access for this session
|
||||||
|
2. Yes, and don't ask again Enable full access and remember this choice
|
||||||
|
3. Cancel Go back without enabling full access
|
||||||
|
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
@@ -4,6 +4,7 @@ use crate::app_event_sender::AppEventSender;
|
|||||||
use crate::test_backend::VT100Backend;
|
use crate::test_backend::VT100Backend;
|
||||||
use crate::tui::FrameRequester;
|
use crate::tui::FrameRequester;
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
|
use codex_common::approval_presets::builtin_approval_presets;
|
||||||
use codex_core::AuthManager;
|
use codex_core::AuthManager;
|
||||||
use codex_core::CodexAuth;
|
use codex_core::CodexAuth;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
@@ -1087,6 +1088,31 @@ fn model_selection_popup_snapshot() {
|
|||||||
assert_snapshot!("model_selection_popup", popup);
|
assert_snapshot!("model_selection_popup", popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn approvals_selection_popup_snapshot() {
|
||||||
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||||
|
|
||||||
|
chat.config.notices.hide_full_access_warning = None;
|
||||||
|
chat.open_approvals_popup();
|
||||||
|
|
||||||
|
let popup = render_bottom_popup(&chat, 80);
|
||||||
|
assert_snapshot!("approvals_selection_popup", popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_access_confirmation_popup_snapshot() {
|
||||||
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||||
|
|
||||||
|
let preset = builtin_approval_presets()
|
||||||
|
.into_iter()
|
||||||
|
.find(|preset| preset.id == "full-access")
|
||||||
|
.expect("full access preset");
|
||||||
|
chat.open_full_access_confirmation(preset);
|
||||||
|
|
||||||
|
let popup = render_bottom_popup(&chat, 80);
|
||||||
|
assert_snapshot!("full_access_confirmation_popup", popup);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn model_reasoning_selection_popup_snapshot() {
|
fn model_reasoning_selection_popup_snapshot() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||||
|
|||||||
Reference in New Issue
Block a user