Compare commits

...

3 Commits

Author SHA1 Message Date
Ahmed Ibrahim
c37276d45d fix 2026-01-14 09:19:01 -08:00
Ahmed Ibrahim
03ce86346b Persist full access when warning hidden 2026-01-13 10:51:27 -08:00
Ahmed Ibrahim
74425ba6ad Persist approval preset selection 2026-01-13 10:39:03 -08:00
7 changed files with 259 additions and 30 deletions

View File

@@ -1,7 +1,9 @@
use crate::config::CONFIG_TOML_FILE;
use crate::config::types::McpServerConfig;
use crate::config::types::Notice;
use crate::protocol::AskForApproval;
use anyhow::Context;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;
use std::collections::BTreeMap;
@@ -22,6 +24,10 @@ pub enum ConfigEdit {
model: Option<String>,
effort: Option<ReasoningEffort>,
},
/// Update the active (or default) approval policy.
SetApprovalPolicy { policy: Option<AskForApproval> },
/// Update the active (or default) sandbox mode.
SetSandboxMode { mode: Option<SandboxMode> },
/// Toggle the acknowledgement flag under `[notice]`.
SetNoticeHideFullAccessWarning(bool),
/// Toggle the Windows world-writable directories warning acknowledgement flag.
@@ -265,6 +271,12 @@ impl ConfigDocument {
);
mutated
}),
ConfigEdit::SetApprovalPolicy { policy } => Ok(self.write_profile_value(
&["approval_policy"],
policy.map(|policy| value(policy.to_string())),
)),
ConfigEdit::SetSandboxMode { mode } => Ok(self
.write_profile_value(&["sandbox_mode"], mode.map(|mode| value(mode.to_string())))),
ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value(
Scope::Global,
&[Notice::TABLE_KEY, "hide_full_access_warning"],
@@ -596,6 +608,19 @@ impl ConfigEditsBuilder {
self
}
pub fn set_approval_policy(mut self, policy: AskForApproval) -> Self {
self.edits.push(ConfigEdit::SetApprovalPolicy {
policy: Some(policy),
});
self
}
pub fn set_sandbox_mode(mut self, mode: SandboxMode) -> Self {
self.edits
.push(ConfigEdit::SetSandboxMode { mode: Some(mode) });
self
}
pub fn set_hide_full_access_warning(mut self, acknowledged: bool) -> Self {
self.edits
.push(ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged));

View File

@@ -39,10 +39,12 @@ use codex_core::protocol::EventMsg;
use codex_core::protocol::FinalOutput;
use codex_core::protocol::ListSkillsResponseEvent;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::SessionSource;
use codex_core::protocol::SkillErrorInfo;
use codex_core::protocol::TokenUsage;
use codex_protocol::ThreadId;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -1069,6 +1071,43 @@ impl App {
}
}
}
AppEvent::PersistApprovalSelection { approval, sandbox } => {
let profile = self.active_profile.as_deref();
let Some(sandbox_mode) = Self::sandbox_mode_for_policy(&sandbox) else {
tracing::warn!(
"Skipping approval persistence for unsupported sandbox policy: {sandbox:?}"
);
self.chat_widget.add_error_message(
"Failed to save approval preference: unsupported sandbox policy."
.to_string(),
);
return Ok(true);
};
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_approval_policy(approval)
.set_sandbox_mode(sandbox_mode)
.apply()
.await
{
Ok(()) => {}
Err(err) => {
tracing::error!(
error = %err,
"failed to persist approval selection"
);
if let Some(profile) = profile {
self.chat_widget.add_error_message(format!(
"Failed to save approvals for profile `{profile}`: {err}"
));
} else {
self.chat_widget.add_error_message(format!(
"Failed to save approval preferences: {err}"
));
}
}
}
}
AppEvent::UpdateAskForApprovalPolicy(policy) => {
self.chat_widget.set_approval_policy(policy);
}
@@ -1310,6 +1349,15 @@ impl App {
(!model.starts_with("codex-auto-")).then(|| Self::reasoning_label(reasoning_effort))
}
fn sandbox_mode_for_policy(policy: &SandboxPolicy) -> Option<SandboxMode> {
match policy {
SandboxPolicy::ReadOnly => Some(SandboxMode::ReadOnly),
SandboxPolicy::WorkspaceWrite { .. } => Some(SandboxMode::WorkspaceWrite),
SandboxPolicy::DangerFullAccess => Some(SandboxMode::DangerFullAccess),
SandboxPolicy::ExternalSandbox { .. } => None,
}
}
pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
self.chat_widget.token_usage()
}

View File

@@ -86,6 +86,12 @@ pub(crate) enum AppEvent {
effort: Option<ReasoningEffort>,
},
/// Persist the selected approval policy and sandbox policy to the appropriate config.
PersistApprovalSelection {
approval: AskForApproval,
sandbox: SandboxPolicy,
},
/// Open the reasoning selection popup after picking a model.
OpenReasoningPopup {
model: ModelPreset,

View File

@@ -3012,6 +3012,7 @@ impl ChatWidget {
.notices
.hide_full_access_warning
.unwrap_or(false);
let persist_preset = self.should_persist_approval_preset(&preset);
let actions: Vec<SelectionAction> = if requires_confirmation {
let preset_clone = preset.clone();
vec![Box::new(move |tx| {
@@ -3055,15 +3056,27 @@ impl ChatWidget {
});
})]
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
persist_preset,
)
}
}
#[cfg(not(target_os = "windows"))]
{
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
persist_preset,
)
}
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
persist_preset,
)
};
items.push(SelectionItem {
name,
@@ -3117,22 +3130,40 @@ impl ChatWidget {
fn approval_preset_actions(
approval: AskForApproval,
sandbox: SandboxPolicy,
persist: bool,
) -> Vec<SelectionAction> {
vec![Box::new(move |tx| {
let sandbox_clone = sandbox.clone();
let sandbox_for_update = sandbox.clone();
let sandbox_for_persist = sandbox.clone();
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
cwd: None,
approval_policy: Some(approval),
sandbox_policy: Some(sandbox_clone.clone()),
sandbox_policy: Some(sandbox_for_update.clone()),
model: None,
effort: None,
summary: None,
}));
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone));
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_for_update));
if persist {
tx.send(AppEvent::PersistApprovalSelection {
approval,
sandbox: sandbox_for_persist,
});
}
})]
}
fn should_persist_approval_preset(&self, preset: &ApprovalPreset) -> bool {
if preset.id != "full-access" {
return true;
}
self.config
.notices
.hide_full_access_warning
.unwrap_or(false)
}
fn preset_matches_current(
current_approval: AskForApproval,
current_sandbox: &SandboxPolicy,
@@ -3202,12 +3233,10 @@ impl ChatWidget {
));
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 accept_actions = Self::approval_preset_actions(approval, sandbox.clone(), false);
let mut accept_and_remember_actions = Self::approval_preset_actions(approval, sandbox);
let mut accept_and_remember_actions =
Self::approval_preset_actions(approval, sandbox, true);
accept_and_remember_actions.push(Box::new(|tx| {
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
tx.send(AppEvent::PersistFullAccessWarningAcknowledged);
@@ -3312,8 +3341,15 @@ impl ChatWidget {
tx.send(AppEvent::SkipNextWorldWritableScan);
}));
}
let persist_approval = preset
.as_ref()
.is_some_and(|preset| self.should_persist_approval_preset(preset));
if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) {
accept_actions.extend(Self::approval_preset_actions(approval, sandbox));
accept_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
persist_approval,
));
}
let mut accept_and_remember_actions: Vec<SelectionAction> = Vec::new();
@@ -3322,7 +3358,11 @@ impl ChatWidget {
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
}));
if let (Some(approval), Some(sandbox)) = (approval, sandbox) {
accept_and_remember_actions.extend(Self::approval_preset_actions(approval, sandbox));
accept_and_remember_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
persist_approval,
));
}
let items = vec![
@@ -3427,7 +3467,11 @@ impl ChatWidget {
.iter()
.find(|preset| preset.id == "read-only")
.map(|preset| {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
self.should_persist_approval_preset(preset),
)
})
.unwrap_or_default()
};
@@ -3507,7 +3551,11 @@ impl ChatWidget {
.iter()
.find(|preset| preset.id == "read-only")
.map(|preset| {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
self.should_persist_approval_preset(preset),
)
})
.unwrap_or_default()
};

View File

@@ -56,11 +56,13 @@ use codex_core::protocol::EventMsg;
use codex_core::protocol::FinalOutput;
use codex_core::protocol::ListSkillsResponseEvent;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::SessionSource;
use codex_core::protocol::SkillErrorInfo;
use codex_core::protocol::TokenUsage;
use codex_core::terminal::terminal_info;
use codex_protocol::ThreadId;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -1845,6 +1847,43 @@ impl App {
}
}
}
AppEvent::PersistApprovalSelection { approval, sandbox } => {
let profile = self.active_profile.as_deref();
let Some(sandbox_mode) = Self::sandbox_mode_for_policy(&sandbox) else {
tracing::warn!(
"Skipping approval persistence for unsupported sandbox policy: {sandbox:?}"
);
self.chat_widget.add_error_message(
"Failed to save approval preference: unsupported sandbox policy."
.to_string(),
);
return Ok(true);
};
match ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_approval_policy(approval)
.set_sandbox_mode(sandbox_mode)
.apply()
.await
{
Ok(()) => {}
Err(err) => {
tracing::error!(
error = %err,
"failed to persist approval selection"
);
if let Some(profile) = profile {
self.chat_widget.add_error_message(format!(
"Failed to save approvals for profile `{profile}`: {err}"
));
} else {
self.chat_widget.add_error_message(format!(
"Failed to save approval preferences: {err}"
));
}
}
}
}
AppEvent::UpdateAskForApprovalPolicy(policy) => {
self.chat_widget.set_approval_policy(policy);
}
@@ -2050,6 +2089,15 @@ impl App {
(!model.starts_with("codex-auto-")).then(|| Self::reasoning_label(reasoning_effort))
}
fn sandbox_mode_for_policy(policy: &SandboxPolicy) -> Option<SandboxMode> {
match policy {
SandboxPolicy::ReadOnly => Some(SandboxMode::ReadOnly),
SandboxPolicy::WorkspaceWrite { .. } => Some(SandboxMode::WorkspaceWrite),
SandboxPolicy::DangerFullAccess => Some(SandboxMode::DangerFullAccess),
SandboxPolicy::ExternalSandbox { .. } => None,
}
}
pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
self.chat_widget.token_usage()
}

View File

@@ -85,6 +85,12 @@ pub(crate) enum AppEvent {
effort: Option<ReasoningEffort>,
},
/// Persist the selected approval policy and sandbox policy to the appropriate config.
PersistApprovalSelection {
approval: AskForApproval,
sandbox: SandboxPolicy,
},
/// Open the reasoning selection popup after picking a model.
OpenReasoningPopup {
model: ModelPreset,

View File

@@ -2763,6 +2763,7 @@ impl ChatWidget {
.notices
.hide_full_access_warning
.unwrap_or(false);
let persist_preset = self.should_persist_approval_preset(&preset);
let actions: Vec<SelectionAction> = if requires_confirmation {
let preset_clone = preset.clone();
vec![Box::new(move |tx| {
@@ -2806,15 +2807,27 @@ impl ChatWidget {
});
})]
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
persist_preset,
)
}
}
#[cfg(not(target_os = "windows"))]
{
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
persist_preset,
)
}
} else {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
persist_preset,
)
};
items.push(SelectionItem {
name,
@@ -2848,22 +2861,40 @@ impl ChatWidget {
fn approval_preset_actions(
approval: AskForApproval,
sandbox: SandboxPolicy,
persist: bool,
) -> Vec<SelectionAction> {
vec![Box::new(move |tx| {
let sandbox_clone = sandbox.clone();
let sandbox_for_update = sandbox.clone();
let sandbox_for_persist = sandbox.clone();
tx.send(AppEvent::CodexOp(Op::OverrideTurnContext {
cwd: None,
approval_policy: Some(approval),
sandbox_policy: Some(sandbox_clone.clone()),
sandbox_policy: Some(sandbox_for_update.clone()),
model: None,
effort: None,
summary: None,
}));
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone));
tx.send(AppEvent::UpdateSandboxPolicy(sandbox_for_update));
if persist {
tx.send(AppEvent::PersistApprovalSelection {
approval,
sandbox: sandbox_for_persist,
});
}
})]
}
fn should_persist_approval_preset(&self, preset: &ApprovalPreset) -> bool {
if preset.id != "full-access" {
return true;
}
self.config
.notices
.hide_full_access_warning
.unwrap_or(false)
}
fn preset_matches_current(
current_approval: AskForApproval,
current_sandbox: &SandboxPolicy,
@@ -2933,12 +2964,10 @@ impl ChatWidget {
));
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 accept_actions = Self::approval_preset_actions(approval, sandbox.clone(), false);
let mut accept_and_remember_actions = Self::approval_preset_actions(approval, sandbox);
let mut accept_and_remember_actions =
Self::approval_preset_actions(approval, sandbox, true);
accept_and_remember_actions.push(Box::new(|tx| {
tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));
tx.send(AppEvent::PersistFullAccessWarningAcknowledged);
@@ -3043,8 +3072,15 @@ impl ChatWidget {
tx.send(AppEvent::SkipNextWorldWritableScan);
}));
}
let persist_approval = preset
.as_ref()
.is_some_and(|preset| self.should_persist_approval_preset(preset));
if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) {
accept_actions.extend(Self::approval_preset_actions(approval, sandbox));
accept_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
persist_approval,
));
}
let mut accept_and_remember_actions: Vec<SelectionAction> = Vec::new();
@@ -3053,7 +3089,11 @@ impl ChatWidget {
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
}));
if let (Some(approval), Some(sandbox)) = (approval, sandbox) {
accept_and_remember_actions.extend(Self::approval_preset_actions(approval, sandbox));
accept_and_remember_actions.extend(Self::approval_preset_actions(
approval,
sandbox,
persist_approval,
));
}
let items = vec![
@@ -3158,7 +3198,11 @@ impl ChatWidget {
.iter()
.find(|preset| preset.id == "read-only")
.map(|preset| {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
preset.id != "full-access",
)
})
.unwrap_or_default()
};
@@ -3238,7 +3282,11 @@ impl ChatWidget {
.iter()
.find(|preset| preset.id == "read-only")
.map(|preset| {
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
Self::approval_preset_actions(
preset.approval,
preset.sandbox.clone(),
preset.id != "full-access",
)
})
.unwrap_or_default()
};