mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Elevated sandbox NUX (#8789)
Elevated Sandbox NUX: * prompt for elevated sandbox setup when agent mode is selected (via /approvals or at startup) * prompt for degraded sandbox if elevated setup is declined or fails * introduce /elevate-sandbox command to upgrade from degraded experience.
This commit is contained in:
1 parent
bdfdebcfa1
commit
6372ba9d5f
22 files changed
+1110
-184
No files matched your search
+307
-18
@@ -10,6 +10,7 @@ use codex_backend_client::Client as BackendClient;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConstraintResult;
|
||||
use codex_core::config::types::Notifications;
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::git_info::current_branch_name;
|
||||
use codex_core::git_info::local_git_branches;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
@@ -83,6 +84,9 @@ use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
use crate::app_event::WindowsSandboxFallbackReason;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::ApprovalRequest;
|
||||
use crate::bottom_pane::BottomPane;
|
||||
@@ -1570,6 +1574,45 @@ impl ChatWidget {
|
||||
SlashCommand::Approvals => {
|
||||
self.open_approvals_popup();
|
||||
}
|
||||
SlashCommand::ElevateSandbox => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox()
|
||||
.is_some()
|
||||
&& !codex_core::is_windows_elevated_sandbox_enabled();
|
||||
if !windows_degraded_sandbox_enabled
|
||||
|| !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
{
|
||||
// This command should not be visible/recognized outside degraded mode,
|
||||
// but guard anyway in case something dispatches it directly.
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(preset) = builtin_approval_presets()
|
||||
.into_iter()
|
||||
.find(|preset| preset.id == "auto")
|
||||
else {
|
||||
// Avoid panicking in interactive UI; treat this as a recoverable
|
||||
// internal error.
|
||||
self.add_error_message(
|
||||
"Internal error: missing the 'auto' approval preset.".to_string(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = self.config.approval_policy.can_set(&preset.approval) {
|
||||
self.add_error_message(err.to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
self.app_event_tx
|
||||
.send(AppEvent::BeginWindowsSandboxElevatedSetup { preset });
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// Not supported; on non-Windows this command should never be reachable.
|
||||
};
|
||||
}
|
||||
SlashCommand::Quit | SlashCommand::Exit => {
|
||||
self.request_exit();
|
||||
}
|
||||
@@ -2594,10 +2637,25 @@ impl ChatWidget {
|
||||
let current_sandbox = self.config.sandbox_policy.get();
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox().is_some()
|
||||
&& !codex_core::is_windows_elevated_sandbox_enabled();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let windows_degraded_sandbox_enabled = false;
|
||||
|
||||
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() {
|
||||
let is_current =
|
||||
Self::preset_matches_current(current_approval, current_sandbox, &preset);
|
||||
let name = preset.label.to_string();
|
||||
let name = if preset.id == "auto" && windows_degraded_sandbox_enabled {
|
||||
"Agent (non-elevated sandbox)".to_string()
|
||||
} else {
|
||||
preset.label.to_string()
|
||||
};
|
||||
let description_text = preset.description;
|
||||
let description = Some(description_text.to_string());
|
||||
let requires_confirmation = preset.id == "full-access"
|
||||
@@ -2618,11 +2676,24 @@ impl ChatWidget {
|
||||
{
|
||||
if codex_core::get_platform_sandbox().is_none() {
|
||||
let preset_clone = preset.clone();
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
|
||||
preset: preset_clone.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()
|
||||
{
|
||||
@@ -2656,8 +2727,18 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
let footer_note = show_elevate_sandbox_hint.then(|| {
|
||||
vec![
|
||||
"The non-elevated sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To upgrade to the elevated sandbox, run ".dim(),
|
||||
"/setup-elevated-sandbox".cyan(),
|
||||
".".dim(),
|
||||
]
|
||||
.into()
|
||||
});
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some("Select Approval Mode".to_string()),
|
||||
footer_note,
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: Box::new(()),
|
||||
@@ -2915,36 +2996,106 @@ impl ChatWidget {
|
||||
pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) {
|
||||
use ratatui_macros::line;
|
||||
|
||||
if !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED {
|
||||
// Legacy flow (pre-NUX): explain the experimental sandbox and let the user enable it
|
||||
// directly (no elevation prompts).
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(*Box::new(
|
||||
Paragraph::new(vec![
|
||||
line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()],
|
||||
line!["Learn more: https://developers.openai.com/codex/windows"],
|
||||
])
|
||||
.wrap(Wrap { trim: false }),
|
||||
));
|
||||
|
||||
let preset_clone = preset;
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Enable experimental sandbox".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset_clone.clone(),
|
||||
mode: WindowsSandboxEnableMode::Legacy,
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Go back".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::OpenApprovalsPopup);
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: None,
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: Box::new(header),
|
||||
..Default::default()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let current_approval = self.config.approval_policy.value();
|
||||
let current_sandbox = self.config.sandbox_policy.get();
|
||||
let presets = builtin_approval_presets();
|
||||
let stay_full_access = presets
|
||||
.iter()
|
||||
.find(|preset| preset.id == "full-access")
|
||||
.is_some_and(|preset| {
|
||||
Self::preset_matches_current(current_approval, current_sandbox, preset)
|
||||
});
|
||||
let stay_actions = if stay_full_access {
|
||||
Vec::new()
|
||||
} else {
|
||||
presets
|
||||
.iter()
|
||||
.find(|preset| preset.id == "read-only")
|
||||
.map(|preset| {
|
||||
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let stay_label = if stay_full_access {
|
||||
"Stay in Agent Full Access".to_string()
|
||||
} else {
|
||||
"Stay in Read-Only".to_string()
|
||||
};
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(*Box::new(
|
||||
Paragraph::new(vec![
|
||||
line!["Agent mode on Windows uses an experimental sandbox to limit network and filesystem access.".bold()],
|
||||
line![
|
||||
"Learn more: https://developers.openai.com/codex/windows"
|
||||
],
|
||||
line!["Set Up Agent Sandbox".bold()],
|
||||
line![""],
|
||||
line!["Agent mode uses an experimental Windows sandbox that protects your files and prevents network access by default."],
|
||||
line!["Learn more: https://developers.openai.com/codex/windows"],
|
||||
])
|
||||
.wrap(Wrap { trim: false }),
|
||||
));
|
||||
|
||||
let preset_clone = preset;
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Enable experimental sandbox".to_string(),
|
||||
name: "Set up agent sandbox (requires elevation)".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: preset_clone.clone(),
|
||||
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
|
||||
preset: preset.clone(),
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Go back".to_string(),
|
||||
name: stay_label,
|
||||
description: None,
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::OpenApprovalsPopup);
|
||||
})],
|
||||
actions: stay_actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -2962,6 +3113,107 @@ impl ChatWidget {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn open_windows_sandbox_fallback_prompt(
|
||||
&mut self,
|
||||
preset: ApprovalPreset,
|
||||
reason: WindowsSandboxFallbackReason,
|
||||
) {
|
||||
use ratatui_macros::line;
|
||||
|
||||
let _ = reason;
|
||||
|
||||
let current_approval = self.config.approval_policy.value();
|
||||
let current_sandbox = self.config.sandbox_policy.get();
|
||||
let presets = builtin_approval_presets();
|
||||
let stay_full_access = presets
|
||||
.iter()
|
||||
.find(|preset| preset.id == "full-access")
|
||||
.is_some_and(|preset| {
|
||||
Self::preset_matches_current(current_approval, current_sandbox, preset)
|
||||
});
|
||||
let stay_actions = if stay_full_access {
|
||||
Vec::new()
|
||||
} else {
|
||||
presets
|
||||
.iter()
|
||||
.find(|preset| preset.id == "read-only")
|
||||
.map(|preset| {
|
||||
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let stay_label = if stay_full_access {
|
||||
"Stay in Agent Full Access".to_string()
|
||||
} else {
|
||||
"Stay in Read-Only".to_string()
|
||||
};
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push(line!["Use Non-Elevated Sandbox?".bold()]);
|
||||
lines.push(line![""]);
|
||||
lines.push(line![
|
||||
"Elevation failed. You can also use a non-elevated sandbox, which protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected."
|
||||
]);
|
||||
lines.push(line![
|
||||
"Learn more: https://developers.openai.com/codex/windows"
|
||||
]);
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(*Box::new(Paragraph::new(lines).wrap(Wrap { trim: false })));
|
||||
|
||||
let elevated_preset = preset.clone();
|
||||
let legacy_preset = preset;
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Try elevated agent sandbox setup again".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
|
||||
preset: elevated_preset.clone(),
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Use non-elevated agent sandbox".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset: legacy_preset.clone(),
|
||||
mode: WindowsSandboxEnableMode::Legacy,
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: stay_label,
|
||||
description: None,
|
||||
actions: stay_actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: None,
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: Box::new(header),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_windows_sandbox_fallback_prompt(
|
||||
&mut self,
|
||||
_preset: ApprovalPreset,
|
||||
_reason: WindowsSandboxFallbackReason,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {
|
||||
if self.config.forced_auto_mode_downgraded_on_windows
|
||||
@@ -2977,6 +3229,34 @@ impl ChatWidget {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn show_windows_sandbox_setup_status(&mut self) {
|
||||
// While elevated sandbox setup runs, prevent typing so the user doesn't
|
||||
// accidentally queue messages that will run under an unexpected mode.
|
||||
self.bottom_pane.set_composer_input_enabled(
|
||||
false,
|
||||
Some("Input disabled until setup completes.".to_string()),
|
||||
);
|
||||
self.bottom_pane.ensure_status_indicator();
|
||||
self.bottom_pane.set_interrupt_hint_visible(false);
|
||||
self.set_status_header("Setting up agent sandbox. This can take a minute.".to_string());
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn show_windows_sandbox_setup_status(&mut self) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {
|
||||
self.bottom_pane.set_composer_input_enabled(true, None);
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn clear_forced_auto_mode_downgrade(&mut self) {
|
||||
self.config.forced_auto_mode_downgraded_on_windows = false;
|
||||
@@ -3009,6 +3289,15 @@ impl ChatWidget {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) {
|
||||
if enabled {
|
||||
self.config.features.enable(feature);
|
||||
} else {
|
||||
self.config.features.disable(feature);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {
|
||||
self.config.notices.hide_full_access_warning = Some(acknowledged);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user