mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
1 Commits
capture-sh
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f30780df3 |
@@ -136,6 +136,8 @@ pub struct Config {
|
||||
/// TUI notifications preference. When set, the TUI will send OSC 9 notifications on approvals
|
||||
/// and turn completions when not focused.
|
||||
pub tui_notifications: Notifications,
|
||||
/// Whether to suppress the confirmation dialog before enabling the Full Access preset in the TUI.
|
||||
pub skip_full_access_warning: bool,
|
||||
|
||||
/// The directory that should be treated as the current working directory
|
||||
/// for the session. All relative paths inside the business-logic layer are
|
||||
@@ -1181,6 +1183,11 @@ impl Config {
|
||||
.as_ref()
|
||||
.map(|t| t.notifications.clone())
|
||||
.unwrap_or_default(),
|
||||
skip_full_access_warning: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|t| t.skip_full_access_warning)
|
||||
.unwrap_or(false),
|
||||
otel: {
|
||||
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
|
||||
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
|
||||
@@ -1355,6 +1362,7 @@ persistence = "none"
|
||||
let tui = parsed.tui.expect("config should include tui section");
|
||||
|
||||
assert_eq!(tui.notifications, Notifications::Enabled(false));
|
||||
assert!(!tui.skip_full_access_warning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2081,6 +2089,7 @@ model_verbosity = "high"
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
skip_full_access_warning: false,
|
||||
otel: OtelConfig::default(),
|
||||
},
|
||||
o3_profile_config
|
||||
@@ -2144,6 +2153,7 @@ model_verbosity = "high"
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
skip_full_access_warning: false,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -2222,6 +2232,7 @@ model_verbosity = "high"
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
skip_full_access_warning: false,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -2286,6 +2297,7 @@ model_verbosity = "high"
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
skip_full_access_warning: false,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -2399,7 +2411,10 @@ mod notifications_tests {
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
struct TuiTomlTest {
|
||||
#[serde(default)]
|
||||
notifications: Notifications,
|
||||
#[serde(default)]
|
||||
skip_full_access_warning: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
@@ -2415,6 +2430,7 @@ mod notifications_tests {
|
||||
"#;
|
||||
let parsed: RootTomlTest = toml::from_str(toml).expect("deserialize notifications=true");
|
||||
assert_matches!(parsed.tui.notifications, Notifications::Enabled(true));
|
||||
assert!(!parsed.tui.skip_full_access_warning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2429,5 +2445,17 @@ mod notifications_tests {
|
||||
parsed.tui.notifications,
|
||||
Notifications::Custom(ref v) if v == &vec!["foo".to_string()]
|
||||
);
|
||||
assert!(!parsed.tui.skip_full_access_warning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_full_access_warning_true() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
skip_full_access_warning = true
|
||||
"#;
|
||||
let parsed: RootTomlTest =
|
||||
toml::from_str(toml).expect("deserialize skip_full_access_warning");
|
||||
assert!(parsed.tui.skip_full_access_warning);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,10 @@ pub struct Tui {
|
||||
/// Defaults to `false`.
|
||||
#[serde(default)]
|
||||
pub notifications: Notifications,
|
||||
|
||||
/// Skip the confirmation warning shown before enabling the dangerous Full Access preset.
|
||||
#[serde(default)]
|
||||
pub skip_full_access_warning: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_core::AuthManager;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::persist_model_selection;
|
||||
use codex_core::config_edit::persist_overrides;
|
||||
use codex_core::model_family::find_family_for_model;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
@@ -368,9 +369,56 @@ impl App {
|
||||
}
|
||||
AppEvent::UpdateAskForApprovalPolicy(policy) => {
|
||||
self.chat_widget.set_approval_policy(policy);
|
||||
self.config.approval_policy = policy;
|
||||
}
|
||||
AppEvent::UpdateSandboxPolicy(policy) => {
|
||||
self.chat_widget.set_sandbox_policy(policy);
|
||||
self.chat_widget.set_sandbox_policy(policy.clone());
|
||||
self.config.sandbox_policy = policy;
|
||||
}
|
||||
AppEvent::OpenFullAccessWarning { approval, sandbox } => {
|
||||
self.chat_widget.open_full_access_warning(approval, sandbox);
|
||||
}
|
||||
AppEvent::UpdateSkipFullAccessWarning(skip) => {
|
||||
self.chat_widget.set_skip_full_access_warning(skip);
|
||||
self.config.skip_full_access_warning = skip;
|
||||
}
|
||||
AppEvent::PersistSkipFullAccessWarning { skip } => {
|
||||
let value = if skip { "true" } else { "false" };
|
||||
match persist_overrides(
|
||||
&self.config.codex_home,
|
||||
self.active_profile.as_deref(),
|
||||
&[(&["tui", "skip_full_access_warning"], value)],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if skip {
|
||||
self.chat_widget.add_info_message(
|
||||
"We'll remember that Full Access is trusted on this machine."
|
||||
.to_string(),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
self.chat_widget.add_info_message(
|
||||
"Full Access will require confirmation again.".to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
"failed to persist skip_full_access_warning preference",
|
||||
);
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to save Full Access warning preference: {err}",
|
||||
));
|
||||
if skip {
|
||||
self.chat_widget.set_skip_full_access_warning(false);
|
||||
self.config.skip_full_access_warning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::OpenReviewBranchPicker(cwd) => {
|
||||
self.chat_widget.show_review_branch_picker(&cwd).await;
|
||||
|
||||
@@ -73,6 +73,20 @@ pub(crate) enum AppEvent {
|
||||
/// Update the current sandbox policy in the running app and widget.
|
||||
UpdateSandboxPolicy(SandboxPolicy),
|
||||
|
||||
/// Show the confirmation warning before enabling the Full Access preset.
|
||||
OpenFullAccessWarning {
|
||||
approval: AskForApproval,
|
||||
sandbox: SandboxPolicy,
|
||||
},
|
||||
|
||||
/// Update whether the Full Access warning should be skipped.
|
||||
UpdateSkipFullAccessWarning(bool),
|
||||
|
||||
/// Persist the Full Access warning preference to config.toml.
|
||||
PersistSkipFullAccessWarning {
|
||||
skip: bool,
|
||||
},
|
||||
|
||||
/// Forwarded conversation history snapshot from the current conversation.
|
||||
ConversationHistory(ConversationPathResponseEvent),
|
||||
|
||||
|
||||
@@ -53,8 +53,12 @@ use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -1767,6 +1771,91 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
fn approval_preset_actions(
|
||||
&self,
|
||||
approval: AskForApproval,
|
||||
sandbox: SandboxPolicy,
|
||||
) -> Vec<SelectionAction> {
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: Some(approval),
|
||||
sandbox_policy: Some(sandbox.clone()),
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
|
||||
tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone()));
|
||||
})]
|
||||
}
|
||||
|
||||
pub(crate) fn open_full_access_warning(
|
||||
&mut self,
|
||||
approval: AskForApproval,
|
||||
sandbox: SandboxPolicy,
|
||||
) {
|
||||
let warning_lines = vec![
|
||||
Line::from(
|
||||
"Full access gives Codex full control over this machine."
|
||||
.bold()
|
||||
.red(),
|
||||
),
|
||||
Line::from(
|
||||
"Codex can edit files, run any command, and access the network without asking.",
|
||||
),
|
||||
Line::from("Only continue if you completely trust this session."),
|
||||
];
|
||||
let header = Paragraph::new(warning_lines).wrap(Wrap { trim: true });
|
||||
|
||||
let accept_once_actions = self.approval_preset_actions(approval, sandbox.clone());
|
||||
|
||||
let mut accept_and_skip_actions = self.approval_preset_actions(approval, sandbox);
|
||||
accept_and_skip_actions.push(Box::new(|tx| {
|
||||
tx.send(AppEvent::UpdateSkipFullAccessWarning(true));
|
||||
}));
|
||||
accept_and_skip_actions.push(Box::new(|tx| {
|
||||
tx.send(AppEvent::PersistSkipFullAccessWarning { skip: true });
|
||||
}));
|
||||
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Accept full access".to_string(),
|
||||
description: Some(
|
||||
"Enable full access for this session while keeping future warnings."
|
||||
.to_string(),
|
||||
),
|
||||
actions: accept_once_actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Accept and don't ask again".to_string(),
|
||||
description: Some(
|
||||
"Enable full access and remember this choice for future sessions.".to_string(),
|
||||
),
|
||||
actions: accept_and_skip_actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Deny".to_string(),
|
||||
description: Some("Cancel and keep your current approvals preset.".to_string()),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some("Enable Full Access?".to_string()),
|
||||
subtitle: Some("Full access is dangerous and bypasses approval prompts.".to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: Box::new(header),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy).
|
||||
pub(crate) fn open_approvals_popup(&mut self) {
|
||||
let current_approval = self.config.approval_policy;
|
||||
@@ -1780,18 +1869,18 @@ impl ChatWidget {
|
||||
let sandbox = preset.sandbox.clone();
|
||||
let name = preset.label.to_string();
|
||||
let description = Some(preset.description.to_string());
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: Some(approval),
|
||||
sandbox_policy: Some(sandbox.clone()),
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
|
||||
tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone()));
|
||||
})];
|
||||
let requires_warning =
|
||||
preset.id == "full-access" && !self.config.skip_full_access_warning;
|
||||
let actions: Vec<SelectionAction> = if requires_warning {
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenFullAccessWarning {
|
||||
approval,
|
||||
sandbox: sandbox.clone(),
|
||||
});
|
||||
})]
|
||||
} else {
|
||||
self.approval_preset_actions(approval, sandbox.clone())
|
||||
};
|
||||
items.push(SelectionItem {
|
||||
name,
|
||||
description,
|
||||
@@ -1820,6 +1909,10 @@ impl ChatWidget {
|
||||
self.config.sandbox_policy = policy;
|
||||
}
|
||||
|
||||
pub(crate) fn set_skip_full_access_warning(&mut self, skip: bool) {
|
||||
self.config.skip_full_access_warning = skip;
|
||||
}
|
||||
|
||||
/// Set the reasoning effort in the widget's config copy.
|
||||
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||
self.config.model_reasoning_effort = effort;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: popup
|
||||
---
|
||||
Full access gives Codex full control over this machine.
|
||||
Codex can edit files, run any command, and access the network without
|
||||
asking.
|
||||
Only continue if you completely trust this session.
|
||||
Enable Full Access?
|
||||
Full access is dangerous and bypasses approval prompts.
|
||||
|
||||
› 1. Accept full access Enable full access for this session while
|
||||
keeping future warnings.
|
||||
2. Accept and don't ask again Enable full access and remember this choice
|
||||
for future sessions.
|
||||
3. Deny Cancel and keep your current approvals
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -31,6 +31,7 @@ use codex_core::protocol::ReviewFinding;
|
||||
use codex_core::protocol::ReviewLineRange;
|
||||
use codex_core::protocol::ReviewOutputEvent;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_core::protocol::TaskStartedEvent;
|
||||
@@ -1057,6 +1058,16 @@ fn model_reasoning_selection_popup_snapshot() {
|
||||
assert_snapshot!("model_reasoning_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_access_warning_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
chat.open_full_access_warning(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
assert_snapshot!("full_access_warning_popup", popup);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_popup_escape_returns_to_model_popup() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
@@ -763,6 +763,10 @@ notifications = true
|
||||
# You can optionally filter to specific notification types.
|
||||
# Available types are "agent-turn-complete" and "approval-requested".
|
||||
notifications = [ "agent-turn-complete", "approval-requested" ]
|
||||
|
||||
# Skip the confirmation dialog when selecting the "Full Access" approvals preset in the TUI.
|
||||
# Defaults to false to keep the extra warning enabled.
|
||||
skip_full_access_warning = true
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
Reference in New Issue
Block a user