mirror of
https://github.com/openai/codex.git
synced 2026-11-11 22:33:20 +00:00
## What changed - Return the number of directories flagged by successful Windows sandbox world-writable scans. - Record that count in the `codex.windows_sandbox.world_writable_scan_flagged_directories` histogram for startup and warning-triggered scans. - Tag each measurement with a `success` or `error` result, using a count of zero when the scan fails. GitOrigin-RevId: 076160f36b1d62fa737f4b2d0318774e6c44a4b1
517 lines
20 KiB
Rust
517 lines
20 KiB
Rust
//! Windows sandbox prompts and warning surfaces for `ChatWidget`.
|
|
|
|
use super::*;
|
|
|
|
impl ChatWidget {
|
|
#[cfg(any(target_os = "windows", test))]
|
|
pub(crate) fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool {
|
|
self.config
|
|
.config_layer_stack
|
|
.requirements()
|
|
.windows_sandbox_mode
|
|
.can_set(&Some(mode))
|
|
.is_ok()
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
pub(super) fn elevated_windows_sandbox_setup_required(&self) -> bool {
|
|
crate::windows_sandbox::level_from_config(&self.config) == WindowsSandboxLevel::Elevated
|
|
&& self
|
|
.config
|
|
.config_layer_stack
|
|
.requirements()
|
|
.windows_sandbox_mode
|
|
.source
|
|
.is_some()
|
|
&& !crate::windows_sandbox::sandbox_setup_is_complete(self.config.codex_home.as_path())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {
|
|
if self
|
|
.config
|
|
.notices
|
|
.hide_world_writable_warning
|
|
.unwrap_or(false)
|
|
{
|
|
return None;
|
|
}
|
|
let cwd = self.config.cwd.clone();
|
|
let workspace_roots = self.config.effective_workspace_roots();
|
|
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
|
|
let permission_profile = self.config.permissions.effective_permission_profile();
|
|
let Ok(permissions) =
|
|
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
|
|
&permission_profile,
|
|
workspace_roots.as_slice(),
|
|
)
|
|
else {
|
|
return None;
|
|
};
|
|
let result = codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions(
|
|
self.config.codex_home.as_path(),
|
|
cwd.as_path(),
|
|
&env_map,
|
|
&permissions,
|
|
Some(self.config.codex_home.as_path()),
|
|
);
|
|
crate::windows_sandbox::record_world_writable_scan_result(&self.session_telemetry, &result);
|
|
match result {
|
|
Ok(_) => None,
|
|
Err(_) => Some((Vec::new(), 0, true)),
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
#[allow(dead_code)]
|
|
pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {
|
|
None
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
pub(crate) fn open_world_writable_warning_confirmation(
|
|
&mut self,
|
|
preset: Option<ApprovalPreset>,
|
|
profile_selection: Option<PermissionProfileSelection>,
|
|
sample_paths: Vec<String>,
|
|
extra_count: usize,
|
|
failed_scan: bool,
|
|
) {
|
|
let (approval, permission_profile, active_permission_profile) = match &preset {
|
|
Some(p) => (
|
|
Some(AskForApproval::from(p.approval)),
|
|
Some(p.permission_profile.clone()),
|
|
Some(p.active_permission_profile.clone()),
|
|
),
|
|
None => (None, None, None),
|
|
};
|
|
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
|
|
let describe_profile = |profile: &PermissionProfile| {
|
|
if matches!(profile, PermissionProfile::Disabled) {
|
|
"Full Access mode"
|
|
} else if profile
|
|
.file_system_sandbox_policy()
|
|
.can_write_path_with_cwd(self.config.cwd.as_path(), self.config.cwd.as_path())
|
|
{
|
|
"Agent mode"
|
|
} else {
|
|
"Read-Only mode"
|
|
}
|
|
};
|
|
let mode_label = preset
|
|
.as_ref()
|
|
.map(|p| describe_profile(&p.permission_profile))
|
|
.unwrap_or_else(|| {
|
|
describe_profile(&self.config.permissions.effective_permission_profile())
|
|
});
|
|
let info_line = if failed_scan {
|
|
Line::from(vec![
|
|
"We couldn't complete the world-writable scan, so protections cannot be verified. "
|
|
.into(),
|
|
format!("The Windows sandbox cannot guarantee protection in {mode_label}.").red(),
|
|
])
|
|
} else {
|
|
Line::from(vec![
|
|
"The Windows sandbox cannot protect writes to folders that are writable by Everyone.".into(),
|
|
" Consider removing write access for Everyone from the following folders:".into(),
|
|
])
|
|
};
|
|
header_children.push(Box::new(
|
|
Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }),
|
|
));
|
|
|
|
if !sample_paths.is_empty() {
|
|
// Show up to three examples and optionally an "and X more" line.
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
lines.push(Line::from(""));
|
|
for p in &sample_paths {
|
|
lines.push(Line::from(format!(" - {p}")));
|
|
}
|
|
if extra_count > 0 {
|
|
lines.push(Line::from(format!("and {extra_count} more")));
|
|
}
|
|
header_children.push(Box::new(Paragraph::new(lines).wrap(Wrap { trim: false })));
|
|
}
|
|
let header = ColumnRenderable::with(header_children);
|
|
|
|
// Build actions ensuring acknowledgement happens before applying the
|
|
// new permission profile, so downstream policy-change hooks don't
|
|
// re-trigger the warning.
|
|
let mut accept_actions: Vec<SelectionAction> = Vec::new();
|
|
// Suppress the immediate re-scan only when a preset will be applied via
|
|
// /permissions, to avoid duplicate warnings from the ensuing policy change.
|
|
if preset.is_some() {
|
|
accept_actions.push(Box::new(|tx| {
|
|
tx.send(AppEvent::SkipNextWorldWritableScan);
|
|
}));
|
|
}
|
|
if let Some(selection) = profile_selection.clone() {
|
|
accept_actions.extend(Self::permission_profile_selection_actions(selection));
|
|
} else if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) = (
|
|
approval,
|
|
permission_profile.clone(),
|
|
active_permission_profile.clone(),
|
|
) {
|
|
accept_actions.extend(Self::approval_preset_actions(
|
|
approval,
|
|
permission_profile,
|
|
active_permission_profile,
|
|
mode_label.to_string(),
|
|
ApprovalsReviewer::User,
|
|
));
|
|
}
|
|
|
|
let mut accept_and_remember_actions: Vec<SelectionAction> = Vec::new();
|
|
accept_and_remember_actions.push(Box::new(|tx| {
|
|
tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));
|
|
tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);
|
|
}));
|
|
if let Some(selection) = profile_selection {
|
|
accept_and_remember_actions
|
|
.extend(Self::permission_profile_selection_actions(selection));
|
|
} else if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) =
|
|
(approval, permission_profile, active_permission_profile)
|
|
{
|
|
accept_and_remember_actions.extend(Self::approval_preset_actions(
|
|
approval,
|
|
permission_profile,
|
|
active_permission_profile,
|
|
mode_label.to_string(),
|
|
ApprovalsReviewer::User,
|
|
));
|
|
}
|
|
|
|
let items = vec![
|
|
SelectionItem {
|
|
name: "Continue".to_string(),
|
|
description: Some(format!("Apply {mode_label} for this session")),
|
|
actions: accept_actions,
|
|
dismiss_on_select: true,
|
|
require_explicit_confirmation: true,
|
|
..Default::default()
|
|
},
|
|
SelectionItem {
|
|
name: "Continue and don't warn again".to_string(),
|
|
description: Some(format!("Enable {mode_label} and remember this choice")),
|
|
actions: accept_and_remember_actions,
|
|
dismiss_on_select: true,
|
|
require_explicit_confirmation: true,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
|
footer_hint: Some(standard_popup_hint_line()),
|
|
items,
|
|
header: Box::new(header),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
#[cfg(all(not(target_os = "windows"), not(test)))]
|
|
pub(crate) fn open_world_writable_warning_confirmation(
|
|
&mut self,
|
|
_preset: Option<ApprovalPreset>,
|
|
_profile_selection: Option<PermissionProfileSelection>,
|
|
_sample_paths: Vec<String>,
|
|
_extra_count: usize,
|
|
_failed_scan: bool,
|
|
) {
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
pub(crate) fn open_windows_sandbox_enable_prompt(
|
|
&mut self,
|
|
preset: ApprovalPreset,
|
|
profile_selection: Option<PermissionProfileSelection>,
|
|
) {
|
|
use ratatui_macros::line;
|
|
|
|
self.session_telemetry.counter(
|
|
"codex.windows_sandbox.elevated_prompt_shown",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
|
|
let allow_unelevated =
|
|
self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated);
|
|
let setup_choice_is_required =
|
|
!allow_unelevated || self.elevated_windows_sandbox_setup_required();
|
|
let mut header = ColumnRenderable::new();
|
|
header.push(*Box::new(
|
|
Paragraph::new(if allow_unelevated {
|
|
vec![
|
|
line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more <https://developers.openai.com/codex/windows>"],
|
|
]
|
|
} else {
|
|
vec![
|
|
line!["Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control network access."],
|
|
line!["Learn more <https://developers.openai.com/codex/windows>"],
|
|
]
|
|
})
|
|
.wrap(Wrap { trim: false }),
|
|
));
|
|
|
|
let accept_otel = self.session_telemetry.clone();
|
|
let legacy_otel = self.session_telemetry.clone();
|
|
let legacy_preset = preset.clone();
|
|
let legacy_profile_selection = profile_selection.clone();
|
|
let quit_otel = self.session_telemetry.clone();
|
|
let retry_preset = preset.clone();
|
|
let retry_profile_selection = profile_selection.clone();
|
|
let mut items = vec![SelectionItem {
|
|
name: "Set up default sandbox (requires Administrator permissions)".to_string(),
|
|
description: None,
|
|
actions: vec![Box::new(move |tx| {
|
|
accept_otel.counter(
|
|
"codex.windows_sandbox.elevated_prompt_accept",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
|
|
preset: preset.clone(),
|
|
profile_selection: profile_selection.clone(),
|
|
});
|
|
})],
|
|
dismiss_on_select: true,
|
|
..Default::default()
|
|
}];
|
|
if allow_unelevated {
|
|
items.push(SelectionItem {
|
|
name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(),
|
|
description: None,
|
|
actions: vec![Box::new(move |tx| {
|
|
legacy_otel.counter(
|
|
"codex.windows_sandbox.elevated_prompt_use_legacy",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
|
|
preset: legacy_preset.clone(),
|
|
profile_selection: legacy_profile_selection.clone(),
|
|
});
|
|
})],
|
|
dismiss_on_select: true,
|
|
require_explicit_confirmation: true,
|
|
..Default::default()
|
|
});
|
|
}
|
|
items.push(SelectionItem {
|
|
name: "Quit".to_string(),
|
|
description: None,
|
|
actions: vec![Box::new(move |tx| {
|
|
quit_otel.counter(
|
|
"codex.windows_sandbox.elevated_prompt_quit",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
|
|
})],
|
|
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),
|
|
on_cancel: setup_choice_is_required.then(|| {
|
|
Box::new(move |tx: &AppEventSender| {
|
|
tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
|
|
preset: retry_preset.clone(),
|
|
profile_selection: retry_profile_selection.clone(),
|
|
});
|
|
}) as _
|
|
}),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
#[cfg(all(not(target_os = "windows"), not(test)))]
|
|
pub(crate) fn open_windows_sandbox_enable_prompt(
|
|
&mut self,
|
|
_preset: ApprovalPreset,
|
|
_profile_selection: Option<PermissionProfileSelection>,
|
|
) {
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", test))]
|
|
pub(crate) fn open_windows_sandbox_fallback_prompt(
|
|
&mut self,
|
|
preset: ApprovalPreset,
|
|
profile_selection: Option<PermissionProfileSelection>,
|
|
) {
|
|
use ratatui_macros::line;
|
|
|
|
let allow_unelevated =
|
|
self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated);
|
|
let setup_choice_is_required =
|
|
!allow_unelevated || self.elevated_windows_sandbox_setup_required();
|
|
let mut lines = Vec::new();
|
|
lines.push(line![
|
|
"Couldn't set up your sandbox with Administrator permissions".bold()
|
|
]);
|
|
lines.push(line![""]);
|
|
if allow_unelevated {
|
|
lines.push(line![
|
|
"You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected."
|
|
]);
|
|
} else {
|
|
lines.push(line![
|
|
"Your organization requires the default sandbox before Codex can continue."
|
|
]);
|
|
}
|
|
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 retry_preset = elevated_preset.clone();
|
|
let retry_profile_selection = profile_selection.clone();
|
|
let elevated_profile_selection = profile_selection.clone();
|
|
let legacy_profile_selection = profile_selection;
|
|
let quit_otel = self.session_telemetry.clone();
|
|
let mut items = vec![SelectionItem {
|
|
name: "Try setting up admin sandbox again".to_string(),
|
|
description: None,
|
|
actions: vec![Box::new({
|
|
let otel = self.session_telemetry.clone();
|
|
let preset = elevated_preset;
|
|
move |tx| {
|
|
otel.counter(
|
|
"codex.windows_sandbox.fallback_retry_elevated",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
|
|
preset: preset.clone(),
|
|
profile_selection: elevated_profile_selection.clone(),
|
|
});
|
|
}
|
|
})],
|
|
dismiss_on_select: true,
|
|
..Default::default()
|
|
}];
|
|
if allow_unelevated {
|
|
items.push(SelectionItem {
|
|
name: "Use Codex with non-admin sandbox".to_string(),
|
|
description: None,
|
|
actions: vec![Box::new({
|
|
let otel = self.session_telemetry.clone();
|
|
let preset = legacy_preset;
|
|
move |tx| {
|
|
otel.counter(
|
|
"codex.windows_sandbox.fallback_use_legacy",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
|
|
preset: preset.clone(),
|
|
profile_selection: legacy_profile_selection.clone(),
|
|
});
|
|
}
|
|
})],
|
|
dismiss_on_select: true,
|
|
require_explicit_confirmation: true,
|
|
..Default::default()
|
|
});
|
|
}
|
|
items.push(SelectionItem {
|
|
name: "Quit".to_string(),
|
|
description: None,
|
|
actions: vec![Box::new(move |tx| {
|
|
quit_otel.counter(
|
|
"codex.windows_sandbox.fallback_prompt_quit",
|
|
/*inc*/ 1,
|
|
&[],
|
|
);
|
|
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
|
|
})],
|
|
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),
|
|
on_cancel: setup_choice_is_required.then(|| {
|
|
Box::new(move |tx: &AppEventSender| {
|
|
tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {
|
|
preset: retry_preset.clone(),
|
|
profile_selection: retry_profile_selection.clone(),
|
|
});
|
|
}) as _
|
|
}),
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
#[cfg(all(not(target_os = "windows"), not(test)))]
|
|
pub(crate) fn open_windows_sandbox_fallback_prompt(
|
|
&mut self,
|
|
_preset: ApprovalPreset,
|
|
_profile_selection: Option<PermissionProfileSelection>,
|
|
) {
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) {
|
|
let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config);
|
|
let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled
|
|
|| self.elevated_windows_sandbox_setup_required();
|
|
if show_now
|
|
&& setup_is_required
|
|
&& let Some(preset) = builtin_approval_presets()
|
|
.into_iter()
|
|
.find(|preset| preset.id == "auto")
|
|
{
|
|
self.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, _show_now: bool) {}
|
|
|
|
#[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(
|
|
/*enabled*/ false,
|
|
Some("Input disabled until setup completes.".to_string()),
|
|
);
|
|
self.bottom_pane.ensure_status_indicator();
|
|
self.bottom_pane
|
|
.set_interrupt_hint_visible(/*visible*/ false);
|
|
self.set_status(
|
|
"Setting up sandbox...".to_string(),
|
|
Some("Hang tight, this may take a few minutes".to_string()),
|
|
StatusDetailsCapitalization::CapitalizeFirst,
|
|
STATUS_DETAILS_DEFAULT_MAX_LINES,
|
|
);
|
|
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(/*enabled*/ true, /*placeholder*/ 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) {}
|
|
}
|