remove sandbox globals. (#9797)

Threads sandbox updates through OverrideTurnContext for active turn
Passes computed sandbox type into safety/exec
This commit is contained in:
iceweasel-oai 2026-01-27 11:04:23 -08:00 committed by GitHub
parent 894923ed5d
commit c40ad65bd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 339 additions and 132 deletions

View file

@ -169,6 +169,7 @@ use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_feedback::CodexFeedback;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
@ -176,6 +177,7 @@ use codex_login::run_login_server;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::dynamic_tools::DynamicToolSpec as CoreDynamicToolSpec;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ResponseItem;
@ -1259,12 +1261,14 @@ impl CodexMessageProcessor {
let timeout_ms = params
.timeout_ms
.and_then(|timeout_ms| u64::try_from(timeout_ms).ok());
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
let exec_params = ExecParams {
command: params.command,
cwd,
expiration: timeout_ms.into(),
env,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level,
justification: None,
arg0: None,
};
@ -3887,6 +3891,7 @@ impl CodexMessageProcessor {
cwd: params.cwd,
approval_policy: params.approval_policy.map(AskForApproval::to_core),
sandbox_policy: params.sandbox_policy.map(|p| p.to_core()),
windows_sandbox_level: None,
model: params.model,
effort: params.effort.map(Some),
summary: params.summary,

View file

@ -42,6 +42,7 @@ pub(crate) async fn apply_patch(
turn_context.approval_policy,
&turn_context.sandbox_policy,
&turn_context.cwd,
turn_context.windows_sandbox_level,
) {
SafetyCheck::AutoApprove {
user_explicitly_approved,

View file

@ -174,11 +174,13 @@ use crate::turn_diff_tracker::TurnDiffTracker;
use crate::unified_exec::UnifiedExecProcessManager;
use crate::user_notification::UserNotification;
use crate::util::backoff;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use codex_async_utils::OrCancelExt;
use codex_otel::OtelManager;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ContentItem;
use codex_protocol::models::DeveloperInstructions;
use codex_protocol::models::ResponseInputItem;
@ -325,6 +327,7 @@ impl Codex {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
session_source,
@ -445,6 +448,7 @@ pub(crate) struct TurnContext {
pub(crate) personality: Option<Personality>,
pub(crate) approval_policy: AskForApproval,
pub(crate) sandbox_policy: SandboxPolicy,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
pub(crate) tools_config: ToolsConfig,
pub(crate) ghost_snapshot: GhostSnapshotConfig,
@ -496,6 +500,7 @@ pub(crate) struct SessionConfiguration {
approval_policy: Constrained<AskForApproval>,
/// How to sandbox commands executed in the system
sandbox_policy: Constrained<SandboxPolicy>,
windows_sandbox_level: WindowsSandboxLevel,
/// Working directory that should be treated as the *root* of the
/// session. All relative paths supplied by the model as well as the
@ -544,6 +549,9 @@ impl SessionConfiguration {
if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
next_configuration.sandbox_policy.set(sandbox_policy)?;
}
if let Some(windows_sandbox_level) = updates.windows_sandbox_level {
next_configuration.windows_sandbox_level = windows_sandbox_level;
}
if let Some(cwd) = updates.cwd.clone() {
next_configuration.cwd = cwd;
}
@ -556,6 +564,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) cwd: Option<PathBuf>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) sandbox_policy: Option<SandboxPolicy>,
pub(crate) windows_sandbox_level: Option<WindowsSandboxLevel>,
pub(crate) collaboration_mode: Option<CollaborationMode>,
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
pub(crate) final_output_json_schema: Option<Option<Value>>,
@ -620,6 +629,7 @@ impl Session {
personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.value(),
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
windows_sandbox_level: session_configuration.windows_sandbox_level,
shell_environment_policy: per_turn_config.shell_environment_policy.clone(),
tools_config,
ghost_snapshot: per_turn_config.ghost_snapshot.clone(),
@ -2191,6 +2201,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
cwd,
approval_policy,
sandbox_policy,
windows_sandbox_level,
model,
effort,
summary,
@ -2214,6 +2225,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
cwd,
approval_policy,
sandbox_policy,
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
personality,
@ -2430,6 +2442,7 @@ mod handlers {
cwd: Some(cwd),
approval_policy: Some(approval_policy),
sandbox_policy: Some(sandbox_policy),
windows_sandbox_level: None,
collaboration_mode,
reasoning_summary: Some(summary),
final_output_json_schema: Some(final_output_json_schema),
@ -2918,6 +2931,7 @@ async fn spawn_review_thread(
personality: parent_turn_context.personality,
approval_policy: parent_turn_context.approval_policy,
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
cwd: parent_turn_context.cwd.clone(),
final_output_json_schema: None,
@ -4062,6 +4076,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
session_source: SessionSource::Exec,
@ -4142,6 +4157,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
session_source: SessionSource::Exec,
@ -4406,6 +4422,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
session_source: SessionSource::Exec,
@ -4516,6 +4533,7 @@ mod tests {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy.clone(),
sandbox_policy: config.sandbox_policy.clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
session_source: SessionSource::Exec,
@ -5023,6 +5041,7 @@ mod tests {
expiration: timeout_ms.into(),
env: HashMap::new(),
sandbox_permissions,
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: Some("test".to_string()),
arg0: None,
};
@ -5033,6 +5052,7 @@ mod tests {
cwd: params.cwd.clone(),
expiration: timeout_ms.into(),
env: HashMap::new(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification.clone(),
arg0: None,
};

View file

@ -38,6 +38,7 @@ use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
use codex_protocol::config_types::AltScreenMode;
@ -49,6 +50,7 @@ use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::openai_models::ReasoningEffort;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf;
@ -1056,6 +1058,7 @@ impl ConfigToml {
&self,
sandbox_mode_override: Option<SandboxMode>,
profile_sandbox_mode: Option<SandboxMode>,
windows_sandbox_level: WindowsSandboxLevel,
resolved_cwd: &Path,
) -> SandboxPolicyResolution {
let resolved_sandbox_mode = sandbox_mode_override
@ -1094,7 +1097,7 @@ impl ConfigToml {
if cfg!(target_os = "windows")
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
// If the experimental Windows sandbox is enabled, do not force a downgrade.
&& crate::safety::get_platform_sandbox().is_none()
&& windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Disabled
{
sandbox_policy = SandboxPolicy::new_read_only_policy();
forced_auto_mode_downgraded_on_windows = true;
@ -1285,16 +1288,6 @@ impl Config {
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features);
#[cfg(target_os = "windows")]
{
// Base flag controls sandbox on/off; elevated only applies when base is enabled.
let sandbox_enabled = features.enabled(Feature::WindowsSandbox);
crate::safety::set_windows_sandbox_enabled(sandbox_enabled);
let elevated_enabled =
sandbox_enabled && features.enabled(Feature::WindowsSandboxElevated);
crate::safety::set_windows_elevated_sandbox_enabled(elevated_enabled);
}
let resolved_cwd = {
use std::env;
@ -1321,10 +1314,16 @@ impl Config {
.get_active_project(&resolved_cwd)
.unwrap_or(ProjectConfig { trust_level: None });
let windows_sandbox_level = WindowsSandboxLevel::from_features(&features);
let SandboxPolicyResolution {
policy: mut sandbox_policy,
forced_auto_mode_downgraded_on_windows,
} = cfg.derive_sandbox_policy(sandbox_mode, config_profile.sandbox_mode, &resolved_cwd);
} = cfg.derive_sandbox_policy(
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
&resolved_cwd,
);
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
for path in additional_writable_roots {
if !writable_roots.iter().any(|existing| existing == &path) {
@ -1667,7 +1666,6 @@ impl Config {
}
pub fn set_windows_sandbox_globally(&mut self, value: bool) {
crate::safety::set_windows_sandbox_enabled(value);
if value {
self.features.enable(Feature::WindowsSandbox);
} else {
@ -1677,7 +1675,6 @@ impl Config {
}
pub fn set_windows_elevated_sandbox_globally(&mut self, value: bool) {
crate::safety::set_windows_elevated_sandbox_enabled(value);
if value {
self.features.enable(Feature::WindowsSandboxElevated);
} else {
@ -1871,6 +1868,7 @@ network_access = false # This should be ignored.
let resolution = sandbox_full_access_cfg.derive_sandbox_policy(
sandbox_mode_override,
None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
);
assert_eq!(
@ -1894,6 +1892,7 @@ network_access = true # This should be ignored.
let resolution = sandbox_read_only_cfg.derive_sandbox_policy(
sandbox_mode_override,
None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
);
assert_eq!(
@ -1925,6 +1924,7 @@ exclude_slash_tmp = true
let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy(
sandbox_mode_override,
None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
);
if cfg!(target_os = "windows") {
@ -1973,6 +1973,7 @@ trust_level = "trusted"
let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy(
sandbox_mode_override,
None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
);
if cfg!(target_os = "windows") {
@ -4190,7 +4191,12 @@ trust_level = "untrusted"
let cfg = toml::from_str::<ConfigToml>(config_with_untrusted)
.expect("TOML deserialization should succeed");
let resolution = cfg.derive_sandbox_policy(None, None, &PathBuf::from("/tmp/test"));
let resolution = cfg.derive_sandbox_policy(
None,
None,
WindowsSandboxLevel::Disabled,
&PathBuf::from("/tmp/test"),
);
// Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade)
if cfg!(target_os = "windows") {

View file

@ -64,6 +64,7 @@ pub struct ExecParams {
pub expiration: ExecExpiration,
pub env: HashMap<String, String>,
pub sandbox_permissions: SandboxPermissions,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
pub justification: Option<String>,
pub arg0: Option<String>,
}
@ -141,11 +142,15 @@ pub async fn process_exec_tool_call(
codex_linux_sandbox_exe: &Option<PathBuf>,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
let windows_sandbox_level = params.windows_sandbox_level;
let sandbox_type = match &sandbox_policy {
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
SandboxType::None
}
_ => get_platform_sandbox().unwrap_or(SandboxType::None),
_ => get_platform_sandbox(
windows_sandbox_level != codex_protocol::config_types::WindowsSandboxLevel::Disabled,
)
.unwrap_or(SandboxType::None),
};
tracing::debug!("Sandbox type: {sandbox_type:?}");
@ -155,6 +160,7 @@ pub async fn process_exec_tool_call(
expiration,
env,
sandbox_permissions,
windows_sandbox_level,
justification,
arg0: _,
} = params;
@ -184,6 +190,7 @@ pub async fn process_exec_tool_call(
sandbox_type,
sandbox_cwd,
codex_linux_sandbox_exe.as_ref(),
windows_sandbox_level,
)
.map_err(CodexErr::from)?;
@ -202,6 +209,7 @@ pub(crate) async fn execute_exec_env(
env,
expiration,
sandbox,
windows_sandbox_level,
sandbox_permissions,
justification,
arg0,
@ -213,6 +221,7 @@ pub(crate) async fn execute_exec_env(
expiration,
env,
sandbox_permissions,
windows_sandbox_level,
justification,
arg0,
};
@ -229,7 +238,7 @@ async fn exec_windows_sandbox(
sandbox_policy: &SandboxPolicy,
) -> Result<RawExecToolCallOutput> {
use crate::config::find_codex_home;
use crate::safety::is_windows_elevated_sandbox_enabled;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_windows_sandbox::run_windows_sandbox_capture;
use codex_windows_sandbox::run_windows_sandbox_capture_elevated;
@ -238,6 +247,7 @@ async fn exec_windows_sandbox(
cwd,
env,
expiration,
windows_sandbox_level,
..
} = params;
// TODO(iceweasel-oai): run_windows_sandbox_capture should support all
@ -255,7 +265,7 @@ async fn exec_windows_sandbox(
"windows sandbox: failed to resolve codex_home: {err}"
)))
})?;
let use_elevated = is_windows_elevated_sandbox_enabled();
let use_elevated = matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated);
let spawn_res = tokio::task::spawn_blocking(move || {
if use_elevated {
run_windows_sandbox_capture_elevated(
@ -584,6 +594,7 @@ async fn exec(
env,
arg0,
expiration,
windows_sandbox_level: _,
..
} = params;
@ -965,6 +976,7 @@ mod tests {
expiration: 500.into(),
env,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
};
@ -1010,6 +1022,7 @@ mod tests {
expiration: ExecExpiration::Cancellation(cancel_token),
env,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
};

View file

@ -126,9 +126,6 @@ pub use exec_policy::ExecPolicyError;
pub use exec_policy::check_execpolicy_for_warnings;
pub use exec_policy::load_exec_policy;
pub use safety::get_platform_sandbox;
pub use safety::is_windows_elevated_sandbox_enabled;
pub use safety::set_windows_elevated_sandbox_enabled;
pub use safety::set_windows_sandbox_enabled;
pub use tools::spec::parse_tool_input_schema;
// Re-export the protocol types from the standalone `codex-protocol` crate so existing
// `codex_core::protocol::...` references continue to work across the workspace.

View file

@ -10,45 +10,7 @@ use crate::util::resolve_path;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "windows")]
use std::sync::atomic::AtomicBool;
#[cfg(target_os = "windows")]
use std::sync::atomic::Ordering;
#[cfg(target_os = "windows")]
static WINDOWS_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "windows")]
static WINDOWS_ELEVATED_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "windows")]
pub fn set_windows_sandbox_enabled(enabled: bool) {
WINDOWS_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed);
}
#[cfg(not(target_os = "windows"))]
#[allow(dead_code)]
pub fn set_windows_sandbox_enabled(_enabled: bool) {}
#[cfg(target_os = "windows")]
pub fn set_windows_elevated_sandbox_enabled(enabled: bool) {
WINDOWS_ELEVATED_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed);
}
#[cfg(not(target_os = "windows"))]
#[allow(dead_code)]
pub fn set_windows_elevated_sandbox_enabled(_enabled: bool) {}
#[cfg(target_os = "windows")]
pub fn is_windows_elevated_sandbox_enabled() -> bool {
WINDOWS_ELEVATED_SANDBOX_ENABLED.load(Ordering::Relaxed)
}
#[cfg(not(target_os = "windows"))]
#[allow(dead_code)]
pub fn is_windows_elevated_sandbox_enabled() -> bool {
false
}
use codex_protocol::config_types::WindowsSandboxLevel;
#[derive(Debug, PartialEq)]
pub enum SafetyCheck {
@ -67,6 +29,7 @@ pub fn assess_patch_safety(
policy: AskForApproval,
sandbox_policy: &SandboxPolicy,
cwd: &Path,
windows_sandbox_level: WindowsSandboxLevel,
) -> SafetyCheck {
if action.is_empty() {
return SafetyCheck::Reject {
@ -104,7 +67,7 @@ pub fn assess_patch_safety(
// Only autoapprove when we can actually enforce a sandbox. Otherwise
// fall back to asking the user because the patch may touch arbitrary
// paths outside the project.
match get_platform_sandbox() {
match get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) {
Some(sandbox_type) => SafetyCheck::AutoApprove {
sandbox_type,
user_explicitly_approved: false,
@ -122,19 +85,17 @@ pub fn assess_patch_safety(
}
}
pub fn get_platform_sandbox() -> Option<SandboxType> {
pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
if cfg!(target_os = "macos") {
Some(SandboxType::MacosSeatbelt)
} else if cfg!(target_os = "linux") {
Some(SandboxType::LinuxSeccomp)
} else if cfg!(target_os = "windows") {
#[cfg(target_os = "windows")]
{
if WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) {
return Some(SandboxType::WindowsRestrictedToken);
}
if windows_sandbox_enabled {
Some(SandboxType::WindowsRestrictedToken)
} else {
None
}
None
} else {
None
}
@ -277,7 +238,13 @@ mod tests {
};
assert_eq!(
assess_patch_safety(&add_inside, AskForApproval::OnRequest, &policy, &cwd),
assess_patch_safety(
&add_inside,
AskForApproval::OnRequest,
&policy,
&cwd,
WindowsSandboxLevel::Disabled
),
SafetyCheck::AutoApprove {
sandbox_type: SandboxType::None,
user_explicitly_approved: false,

View file

@ -21,6 +21,7 @@ use crate::seatbelt::create_seatbelt_command_args;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use crate::tools::sandboxing::SandboxablePreference;
use codex_protocol::config_types::WindowsSandboxLevel;
pub use codex_protocol::models::SandboxPermissions;
use std::collections::HashMap;
use std::path::Path;
@ -44,6 +45,7 @@ pub struct ExecEnv {
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub sandbox: SandboxType,
pub windows_sandbox_level: WindowsSandboxLevel,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub arg0: Option<String>,
@ -76,19 +78,26 @@ impl SandboxManager {
&self,
policy: &SandboxPolicy,
pref: SandboxablePreference,
windows_sandbox_level: WindowsSandboxLevel,
) -> SandboxType {
match pref {
SandboxablePreference::Forbid => SandboxType::None,
SandboxablePreference::Require => {
// Require a platform sandbox when available; on Windows this
// respects the experimental_windows_sandbox feature.
crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None)
crate::safety::get_platform_sandbox(
windows_sandbox_level != WindowsSandboxLevel::Disabled,
)
.unwrap_or(SandboxType::None)
}
SandboxablePreference::Auto => match policy {
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
SandboxType::None
}
_ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None),
_ => crate::safety::get_platform_sandbox(
windows_sandbox_level != WindowsSandboxLevel::Disabled,
)
.unwrap_or(SandboxType::None),
},
}
}
@ -100,6 +109,7 @@ impl SandboxManager {
sandbox: SandboxType,
sandbox_policy_cwd: &Path,
codex_linux_sandbox_exe: Option<&PathBuf>,
windows_sandbox_level: WindowsSandboxLevel,
) -> Result<ExecEnv, SandboxTransformError> {
let mut env = spec.env;
if !policy.has_full_network_access() {
@ -160,6 +170,7 @@ impl SandboxManager {
env,
expiration: spec.expiration,
sandbox,
windows_sandbox_level,
sandbox_permissions: spec.sandbox_permissions,
justification: spec.justification,
arg0: arg0_override,

View file

@ -109,6 +109,7 @@ impl SessionTask for UserShellCommandTask {
// should use that instead of an "arbitrarily large" timeout here.
expiration: USER_SHELL_TIMEOUT_MS.into(),
sandbox: SandboxType::None,
windows_sandbox_level: turn_context.windows_sandbox_level,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,

View file

@ -36,6 +36,7 @@ impl ShellHandler {
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification,
arg0: None,
}
@ -62,6 +63,7 @@ impl ShellCommandHandler {
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy),
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
windows_sandbox_level: turn_context.windows_sandbox_level,
justification: params.justification,
arg0: None,
}

View file

@ -88,19 +88,22 @@ impl ToolOrchestrator {
// 2) First attempt under the selected sandbox.
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None,
SandboxOverride::NoOverride => self
.sandbox
.select_initial(&turn_ctx.sandbox_policy, tool.sandbox_preference()),
SandboxOverride::NoOverride => self.sandbox.select_initial(
&turn_ctx.sandbox_policy,
tool.sandbox_preference(),
turn_ctx.windows_sandbox_level,
),
};
// Platform-specific flag gating is handled by SandboxManager::select_initial
// via crate::safety::get_platform_sandbox().
// via crate::safety::get_platform_sandbox(..).
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
policy: &turn_ctx.sandbox_policy,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
windows_sandbox_level: turn_ctx.windows_sandbox_level,
};
match tool.run(req, &initial_attempt, tool_ctx).await {
@ -151,6 +154,7 @@ impl ToolOrchestrator {
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: None,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
};
// Second attempt.

View file

@ -274,6 +274,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a Path,
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
}
impl<'a> SandboxAttempt<'a> {
@ -287,6 +288,7 @@ impl<'a> SandboxAttempt<'a> {
self.sandbox,
self.sandbox_cwd,
self.codex_linux_sandbox_exe,
self.windows_sandbox_level,
)
}
}

View file

@ -1,4 +1,8 @@
use crate::config::Config;
use crate::features::Feature;
use crate::features::Features;
use crate::protocol::SandboxPolicy;
use codex_protocol::config_types::WindowsSandboxLevel;
use std::collections::HashMap;
use std::path::Path;
@ -8,6 +12,36 @@ use std::path::Path;
/// prompts users to enable the legacy sandbox feature.
pub const ELEVATED_SANDBOX_NUX_ENABLED: bool = true;
pub trait WindowsSandboxLevelExt {
fn from_config(config: &Config) -> WindowsSandboxLevel;
fn from_features(features: &Features) -> WindowsSandboxLevel;
}
impl WindowsSandboxLevelExt for WindowsSandboxLevel {
fn from_config(config: &Config) -> WindowsSandboxLevel {
Self::from_features(&config.features)
}
fn from_features(features: &Features) -> WindowsSandboxLevel {
if !features.enabled(Feature::WindowsSandbox) {
return WindowsSandboxLevel::Disabled;
}
if features.enabled(Feature::WindowsSandboxElevated) {
WindowsSandboxLevel::Elevated
} else {
WindowsSandboxLevel::RestrictedToken
}
}
}
pub fn windows_sandbox_level_from_config(config: &Config) -> WindowsSandboxLevel {
WindowsSandboxLevel::from_config(config)
}
pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandboxLevel {
WindowsSandboxLevel::from_features(features)
}
#[cfg(target_os = "windows")]
pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool {
codex_windows_sandbox::sandbox_setup_is_complete(codex_home)

View file

@ -104,6 +104,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -185,6 +186,7 @@ async fn override_then_user_turn_uses_updated_collaboration_instructions() -> Re
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -238,6 +240,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -292,6 +295,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -316,6 +320,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -361,6 +366,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -385,6 +391,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -436,6 +443,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -491,6 +499,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,

View file

@ -10,6 +10,7 @@ use codex_core::exec::process_exec_tool_call;
use codex_core::protocol::SandboxPolicy;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
use codex_protocol::config_types::WindowsSandboxLevel;
use tempfile::TempDir;
use codex_core::error::Result;
@ -27,7 +28,7 @@ fn skip_test() -> bool {
#[expect(clippy::expect_used)]
async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput> {
let sandbox_type = get_platform_sandbox().expect("should be able to get sandbox type");
let sandbox_type = get_platform_sandbox(false).expect("should be able to get sandbox type");
assert_eq!(sandbox_type, SandboxType::MacosSeatbelt);
let params = ExecParams {
@ -36,6 +37,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
expiration: 1000.into(),
env: HashMap::new(),
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
};

View file

@ -29,6 +29,7 @@ async fn override_turn_context_does_not_persist_when_config_exists() {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some("o3".to_string()),
effort: Some(Some(ReasoningEffort::High)),
summary: None,
@ -64,6 +65,7 @@ async fn override_turn_context_does_not_create_config_file() {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some("o3".to_string()),
effort: Some(Some(ReasoningEffort::Medium)),
summary: None,

View file

@ -118,6 +118,7 @@ async fn override_turn_context_records_permissions_update() -> Result<()> {
cwd: None,
approval_policy: Some(AskForApproval::Never),
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -161,6 +162,7 @@ async fn override_turn_context_records_environment_update() -> Result<()> {
cwd: Some(new_cwd.path().to_path_buf()),
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -198,6 +200,7 @@ async fn override_turn_context_records_collaboration_update() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,

View file

@ -106,6 +106,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
cwd: None,
approval_policy: Some(AskForApproval::Never),
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -227,6 +228,7 @@ async fn resume_replays_permissions_messages() -> Result<()> {
cwd: None,
approval_policy: Some(AskForApproval::Never),
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -309,6 +311,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
cwd: None,
approval_policy: Some(AskForApproval::Never),
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,

View file

@ -210,6 +210,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -362,6 +363,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some(remote_slug.to_string()),
effort: None,
summary: None,

View file

@ -350,6 +350,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
cwd: None,
approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(new_policy.clone()),
windows_sandbox_level: None,
model: Some("o3".to_string()),
effort: Some(Some(ReasoningEffort::High)),
summary: Some(ReasoningSummary::Detailed),
@ -427,6 +428,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
cwd: None,
approval_policy: Some(AskForApproval::Never),
sandbox_policy: None,
windows_sandbox_level: None,
model: Some("gpt-5.1-codex".to_string()),
effort: Some(Some(ReasoningEffort::Low)),
summary: None,

View file

@ -138,6 +138,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some(REMOTE_MODEL_SLUG.to_string()),
effort: None,
summary: None,
@ -367,6 +368,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some(model.to_string()),
effort: None,
summary: None,

View file

@ -819,6 +819,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
cwd: Some(repo_path.to_path_buf()),
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,

View file

@ -10,6 +10,7 @@ use path_absolutize::Absolutize as _;
use codex_core::SandboxState;
use codex_core::exec::process_exec_tool_call;
use codex_core::protocol_config_types::WindowsSandboxLevel;
use codex_core::sandboxing::SandboxPermissions;
use tokio::process::Command;
use tokio_util::sync::CancellationToken;
@ -87,6 +88,7 @@ impl EscalateServer {
expiration: ExecExpiration::Cancellation(cancel_rx),
env,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
},

View file

@ -7,6 +7,7 @@ use codex_core::exec::ExecParams;
use codex_core::exec::process_exec_tool_call;
use codex_core::exec_env::create_env;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol_config_types::WindowsSandboxLevel;
use codex_core::sandboxing::SandboxPermissions;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@ -60,6 +61,7 @@ async fn run_cmd_output(
expiration: timeout_ms.into(),
env: create_env_from_core_vars(),
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
};
@ -177,6 +179,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
expiration: NETWORK_TIMEOUT_MS.into(),
env: create_env_from_core_vars(),
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
};

View file

@ -66,6 +66,18 @@ pub enum SandboxMode {
DangerFullAccess,
}
#[derive(
Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, JsonSchema, TS,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum WindowsSandboxLevel {
#[default]
Disabled,
RestrictedToken,
Elevated,
}
#[derive(
Debug,
Serialize,

View file

@ -16,6 +16,7 @@ use crate::approvals::ElicitationRequestEvent;
use crate::config_types::CollaborationMode;
use crate::config_types::Personality;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::config_types::WindowsSandboxLevel;
use crate::custom_prompts::CustomPrompt;
use crate::dynamic_tools::DynamicToolCallRequest;
use crate::dynamic_tools::DynamicToolResponse;
@ -158,6 +159,10 @@ pub enum Op {
#[serde(skip_serializing_if = "Option::is_none")]
sandbox_policy: Option<SandboxPolicy>,
/// Updated Windows sandbox mode for tool execution.
#[serde(skip_serializing_if = "Option::is_none")]
windows_sandbox_level: Option<WindowsSandboxLevel>,
/// Updated model slug. When set, the model info is derived
/// automatically.
#[serde(skip_serializing_if = "Option::is_none")]

View file

@ -42,7 +42,6 @@ use codex_core::config::ConfigOverrides;
use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config_loader::ConfigLayerStackOrdering;
#[cfg(target_os = "windows")]
use codex_core::features::Feature;
use codex_core::models_manager::manager::RefreshStrategy;
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
@ -58,9 +57,13 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::SessionSource;
use codex_core::protocol::SkillErrorInfo;
use codex_core::protocol::TokenUsage;
#[cfg(target_os = "windows")]
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_otel::OtelManager;
use codex_protocol::ThreadId;
use codex_protocol::config_types::Personality;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::items::TurnItem;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelUpgrade;
@ -1088,7 +1091,8 @@ impl App {
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
#[cfg(target_os = "windows")]
{
let should_check = codex_core::get_platform_sandbox().is_some()
let should_check = WindowsSandboxLevel::from_config(&app.config)
!= WindowsSandboxLevel::Disabled
&& matches!(
app.config.sandbox_policy.get(),
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
@ -1684,9 +1688,24 @@ impl App {
elevated_enabled,
);
self.chat_widget.clear_forced_auto_mode_downgrade();
let windows_sandbox_level =
WindowsSandboxLevel::from_config(&self.config);
if let Some((sample_paths, extra_count, failed_scan)) =
self.chat_widget.world_writable_warning_details()
{
self.app_event_tx.send(AppEvent::CodexOp(
Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: Some(windows_sandbox_level),
model: None,
effort: None,
summary: None,
collaboration_mode: None,
personality: None,
},
));
self.app_event_tx.send(
AppEvent::OpenWorldWritableWarningConfirmation {
preset: Some(preset.clone()),
@ -1701,6 +1720,7 @@ impl App {
cwd: None,
approval_policy: Some(preset.approval),
sandbox_policy: Some(preset.sandbox.clone()),
windows_sandbox_level: Some(windows_sandbox_level),
model: None,
effort: None,
summary: None,
@ -1839,7 +1859,8 @@ impl App {
}
#[cfg(target_os = "windows")]
if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some()
|| WindowsSandboxLevel::from_config(&self.config)
!= WindowsSandboxLevel::Disabled
{
self.config.forced_auto_mode_downgraded_on_windows = false;
}
@ -1861,7 +1882,8 @@ impl App {
return Ok(AppRunControl::Continue);
}
let should_check = codex_core::get_platform_sandbox().is_some()
let should_check = WindowsSandboxLevel::from_config(&self.config)
!= WindowsSandboxLevel::Disabled
&& policy_is_workspace_write_or_ro
&& !self.chat_widget.world_writable_warning_hidden();
if should_check {
@ -1885,6 +1907,12 @@ impl App {
if updates.is_empty() {
return Ok(AppRunControl::Continue);
}
let windows_sandbox_changed = updates.iter().any(|(feature, _)| {
matches!(
feature,
Feature::WindowsSandbox | Feature::WindowsSandboxElevated
)
});
let mut builder = ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(self.active_profile.as_deref());
for (feature, enabled) in &updates {
@ -1910,6 +1938,24 @@ impl App {
}
}
}
if windows_sandbox_changed {
#[cfg(target_os = "windows")]
{
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
self.app_event_tx
.send(AppEvent::CodexOp(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: Some(windows_sandbox_level),
model: None,
effort: None,
summary: None,
collaboration_mode: None,
personality: None,
}));
}
}
if let Err(err) = builder.apply().await {
tracing::error!(error = %err, "failed to persist feature flags");
self.chat_widget.add_error_message(format!(

View file

@ -178,6 +178,9 @@ pub(crate) enum AppEvent {
mode: WindowsSandboxEnableMode,
},
/// Update the Windows sandbox feature mode without changing approval presets.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
/// Update the current approval policy in the running app and widget.
UpdateAskForApprovalPolicy(AskForApproval),

View file

@ -273,6 +273,7 @@ pub(crate) struct ChatComposer {
config: ChatComposerConfig,
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
personality_command_enabled: bool,
windows_degraded_sandbox_active: bool,
}
#[derive(Clone, Debug)]
@ -358,6 +359,7 @@ impl ChatComposer {
config,
collaboration_mode_indicator: None,
personality_command_enabled: false,
windows_degraded_sandbox_active: false,
};
// Apply configuration via the setter to keep side-effects centralized.
this.set_disable_paste_burst(disable_paste_burst);
@ -405,6 +407,10 @@ impl ChatComposer {
fn image_paste_enabled(&self) -> bool {
self.config.image_paste_enabled
}
#[cfg(target_os = "windows")]
pub fn set_windows_degraded_sandbox_active(&mut self, enabled: bool) {
self.windows_degraded_sandbox_active = enabled;
}
fn layout_areas(&self, area: Rect) -> [Rect; 3] {
let footer_props = self.footer_props();
let footer_hint_height = self
@ -1728,6 +1734,7 @@ impl ChatComposer {
name,
self.collaboration_modes_enabled,
self.personality_command_enabled,
self.windows_degraded_sandbox_active,
)
.is_some();
let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:");
@ -1904,6 +1911,7 @@ impl ChatComposer {
name,
self.collaboration_modes_enabled,
self.personality_command_enabled,
self.windows_degraded_sandbox_active,
)
{
self.textarea.set_text_clearing_elements("");
@ -1931,6 +1939,7 @@ impl ChatComposer {
name,
self.collaboration_modes_enabled,
self.personality_command_enabled,
self.windows_degraded_sandbox_active,
)
&& cmd == SlashCommand::Review
{
@ -2391,6 +2400,7 @@ impl ChatComposer {
name,
self.collaboration_modes_enabled,
self.personality_command_enabled,
self.windows_degraded_sandbox_active,
) {
return true;
}
@ -2447,6 +2457,7 @@ impl ChatComposer {
CommandPopupFlags {
collaboration_modes_enabled,
personality_command_enabled,
windows_degraded_sandbox_active: self.windows_degraded_sandbox_active,
},
);
command_popup.on_composer_text_change(first_line.to_string());

View file

@ -33,6 +33,7 @@ pub(crate) struct CommandPopup {
pub(crate) struct CommandPopupFlags {
pub(crate) collaboration_modes_enabled: bool,
pub(crate) personality_command_enabled: bool,
pub(crate) windows_degraded_sandbox_active: bool,
}
impl CommandPopup {
@ -41,6 +42,7 @@ impl CommandPopup {
let builtins = slash_commands::builtins_for_input(
flags.collaboration_modes_enabled,
flags.personality_command_enabled,
flags.windows_degraded_sandbox_active,
);
// Exclude prompts that collide with builtin command names and sort by name.
let exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
@ -461,6 +463,7 @@ mod tests {
CommandPopupFlags {
collaboration_modes_enabled: true,
personality_command_enabled: true,
windows_degraded_sandbox_active: false,
},
);
popup.on_composer_text_change("/collab".to_string());
@ -478,6 +481,7 @@ mod tests {
CommandPopupFlags {
collaboration_modes_enabled: true,
personality_command_enabled: false,
windows_degraded_sandbox_active: false,
},
);
popup.on_composer_text_change("/pers".to_string());
@ -503,6 +507,7 @@ mod tests {
CommandPopupFlags {
collaboration_modes_enabled: true,
personality_command_enabled: true,
windows_degraded_sandbox_active: false,
},
);
popup.on_composer_text_change("/personality".to_string());

View file

@ -213,6 +213,12 @@ impl BottomPane {
self.request_redraw();
}
#[cfg(target_os = "windows")]
pub fn set_windows_degraded_sandbox_active(&mut self, enabled: bool) {
self.composer.set_windows_degraded_sandbox_active(enabled);
self.request_redraw();
}
pub fn set_collaboration_mode_indicator(
&mut self,
indicator: Option<CollaborationModeIndicator>,

View file

@ -8,20 +8,12 @@ use codex_common::fuzzy_match::fuzzy_match;
use crate::slash_command::SlashCommand;
use crate::slash_command::built_in_slash_commands;
/// Whether the Windows degraded-sandbox elevation flow is currently allowed.
pub(crate) fn windows_degraded_sandbox_active() -> bool {
cfg!(target_os = "windows")
&& codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
&& codex_core::get_platform_sandbox().is_some()
&& !codex_core::is_windows_elevated_sandbox_enabled()
}
/// Return the built-ins that should be visible/usable for the current input.
pub(crate) fn builtins_for_input(
collaboration_modes_enabled: bool,
personality_command_enabled: bool,
allow_elevate_sandbox: bool,
) -> Vec<(&'static str, SlashCommand)> {
let allow_elevate_sandbox = windows_degraded_sandbox_active();
built_in_slash_commands()
.into_iter()
.filter(|(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox)
@ -35,11 +27,16 @@ pub(crate) fn find_builtin_command(
name: &str,
collaboration_modes_enabled: bool,
personality_command_enabled: bool,
allow_elevate_sandbox: bool,
) -> Option<SlashCommand> {
builtins_for_input(collaboration_modes_enabled, personality_command_enabled)
.into_iter()
.find(|(command_name, _)| *command_name == name)
.map(|(_, cmd)| cmd)
builtins_for_input(
collaboration_modes_enabled,
personality_command_enabled,
allow_elevate_sandbox,
)
.into_iter()
.find(|(command_name, _)| *command_name == name)
.map(|(_, cmd)| cmd)
}
/// Whether any visible built-in fuzzily matches the provided prefix.
@ -47,8 +44,13 @@ pub(crate) fn has_builtin_prefix(
name: &str,
collaboration_modes_enabled: bool,
personality_command_enabled: bool,
allow_elevate_sandbox: bool,
) -> bool {
builtins_for_input(collaboration_modes_enabled, personality_command_enabled)
.into_iter()
.any(|(command_name, _)| fuzzy_match(command_name, name).is_some())
builtins_for_input(
collaboration_modes_enabled,
personality_command_enabled,
allow_elevate_sandbox,
)
.into_iter()
.any(|(command_name, _)| fuzzy_match(command_name, name).is_some())
}

View file

@ -88,6 +88,8 @@ use codex_core::protocol::WarningEvent;
use codex_core::protocol::WebSearchBeginEvent;
use codex_core::protocol::WebSearchEndEvent;
use codex_core::skills::model::SkillMetadata;
#[cfg(target_os = "windows")]
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_otel::OtelManager;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
@ -97,6 +99,8 @@ use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::Settings;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::local_image_label_text;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::request_user_input::RequestUserInputEvent;
@ -2057,6 +2061,14 @@ impl ChatWidget {
widget.config.features.enabled(Feature::CollaborationModes),
);
widget.sync_personality_command_enabled();
#[cfg(target_os = "windows")]
widget.bottom_pane.set_windows_degraded_sandbox_active(
codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
&& matches!(
WindowsSandboxLevel::from_config(&widget.config),
WindowsSandboxLevel::RestrictedToken
),
);
widget.update_collaboration_mode_indicator();
widget
@ -2308,6 +2320,14 @@ impl ChatWidget {
widget.config.features.enabled(Feature::CollaborationModes),
);
widget.sync_personality_command_enabled();
#[cfg(target_os = "windows")]
widget.bottom_pane.set_windows_degraded_sandbox_active(
codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
&& matches!(
WindowsSandboxLevel::from_config(&widget.config),
WindowsSandboxLevel::RestrictedToken
),
);
widget.update_collaboration_mode_indicator();
widget
@ -2563,9 +2583,9 @@ impl ChatWidget {
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();
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
let windows_degraded_sandbox_enabled =
matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken);
if !windows_degraded_sandbox_enabled
|| !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
{
@ -3340,6 +3360,7 @@ impl ChatWidget {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some(switch_model.clone()),
effort: Some(Some(default_effort)),
summary: None,
@ -3462,6 +3483,7 @@ impl ChatWidget {
effort: None,
summary: None,
collaboration_mode: None,
windows_sandbox_level: None,
personality: Some(personality),
}));
tx.send(AppEvent::UpdatePersonality(personality));
@ -3731,6 +3753,7 @@ impl ChatWidget {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some(model_for_action.clone()),
effort: Some(effort_for_action),
summary: None,
@ -3904,6 +3927,7 @@ impl ChatWidget {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some(model.clone()),
effort: Some(effort),
summary: None,
@ -3944,8 +3968,10 @@ impl ChatWidget {
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();
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
#[cfg(target_os = "windows")]
let windows_degraded_sandbox_enabled =
matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken);
#[cfg(not(target_os = "windows"))]
let windows_degraded_sandbox_enabled = false;
@ -3986,7 +4012,9 @@ impl ChatWidget {
} else if preset.id == "auto" {
#[cfg(target_os = "windows")]
{
if codex_core::get_platform_sandbox().is_none() {
if WindowsSandboxLevel::from_config(&self.config)
== WindowsSandboxLevel::Disabled
{
let preset_clone = preset.clone();
if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
&& codex_core::windows_sandbox::sandbox_setup_is_complete(
@ -4088,6 +4116,7 @@ impl ChatWidget {
cwd: None,
approval_policy: Some(approval),
sandbox_policy: Some(sandbox_clone.clone()),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@ -4591,7 +4620,7 @@ impl ChatWidget {
#[cfg(target_os = "windows")]
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {
if self.config.forced_auto_mode_downgraded_on_windows
&& codex_core::get_platform_sandbox().is_none()
&& WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled
&& let Some(preset) = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
@ -4651,7 +4680,7 @@ impl ChatWidget {
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> {
#[cfg(target_os = "windows")]
let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some();
|| WindowsSandboxLevel::from_config(&self.config) != WindowsSandboxLevel::Disabled;
self.config.sandbox_policy.set(policy)?;
@ -4685,6 +4714,19 @@ impl ChatWidget {
self.refresh_model_display();
self.request_redraw();
}
#[cfg(target_os = "windows")]
if matches!(
feature,
Feature::WindowsSandbox | Feature::WindowsSandboxElevated
) {
self.bottom_pane.set_windows_degraded_sandbox_active(
codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
&& matches!(
WindowsSandboxLevel::from_config(&self.config),
WindowsSandboxLevel::RestrictedToken
),
);
}
}
pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {

View file

@ -92,16 +92,6 @@ use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::unbounded_channel;
use toml::Value as TomlValue;
#[cfg(target_os = "windows")]
fn set_windows_sandbox_enabled(enabled: bool) {
codex_core::set_windows_sandbox_enabled(enabled);
}
#[cfg(target_os = "windows")]
fn set_windows_elevated_sandbox_enabled(enabled: bool) {
codex_core::set_windows_elevated_sandbox_enabled(enabled);
}
async fn test_config() -> Config {
// Use base defaults to avoid depending on host state.
let codex_home = std::env::temp_dir();
@ -3050,16 +3040,9 @@ async fn approvals_selection_popup_snapshot() {
async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
let was_sandbox_enabled = codex_core::get_platform_sandbox().is_some();
let was_elevated_enabled = codex_core::is_windows_elevated_sandbox_enabled();
chat.config.notices.hide_full_access_warning = None;
chat.config.features.enable(Feature::WindowsSandbox);
chat.config
.features
.disable(Feature::WindowsSandboxElevated);
set_windows_sandbox_enabled(true);
set_windows_elevated_sandbox_enabled(false);
chat.set_feature_enabled(Feature::WindowsSandbox, true);
chat.set_feature_enabled(Feature::WindowsSandboxElevated, false);
chat.open_approvals_popup();
@ -3067,10 +3050,6 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() {
insta::with_settings!({ snapshot_suffix => "windows_degraded" }, {
assert_snapshot!("approvals_selection_popup", popup);
});
// Avoid leaking sandbox global state into other tests.
set_windows_sandbox_enabled(was_sandbox_enabled);
set_windows_elevated_sandbox_enabled(was_elevated_enabled);
}
#[tokio::test]
@ -3133,7 +3112,8 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
async fn startup_prompts_for_windows_sandbox_when_agent_requested() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
set_windows_sandbox_enabled(false);
chat.set_feature_enabled(Feature::WindowsSandbox, false);
chat.set_feature_enabled(Feature::WindowsSandboxElevated, false);
chat.config.forced_auto_mode_downgraded_on_windows = true;
chat.maybe_prompt_windows_sandbox_enable();
@ -3151,8 +3131,6 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() {
popup.contains("Stay in"),
"expected startup prompt to offer staying in current mode: {popup}"
);
set_windows_sandbox_enabled(true);
}
#[tokio::test]

View file

@ -26,13 +26,14 @@ use codex_core::config::resolve_oss_provider;
use codex_core::config_loader::ConfigLoadError;
use codex_core::config_loader::format_config_error_with_source;
use codex_core::find_thread_path_by_id_str;
use codex_core::get_platform_sandbox;
use codex_core::path_utils;
use codex_core::protocol::AskForApproval;
use codex_core::read_session_meta_line;
use codex_core::terminal::Multiplexer;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_utils_absolute_path::AbsolutePathBuf;
@ -816,7 +817,9 @@ async fn load_config_or_exit_with_fallback_cwd(
/// or if the current cwd project is already trusted. If not, we need to
/// show the trust screen.
fn should_show_trust_screen(config: &Config) -> bool {
if cfg!(target_os = "windows") && get_platform_sandbox().is_none() {
if cfg!(target_os = "windows")
&& WindowsSandboxLevel::from_config(config) == WindowsSandboxLevel::Disabled
{
// If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access; skip the trust prompt entirely.
return false;
}