mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
Promote Windows Sandbox (#11341)
1. Move Windows Sandbox NUX to right after trust directory screen 2. Don't offer read-only as an option in Sandbox NUX. Elevated/Legacy/Quit 3. Don't allow new untrusted directories. It's trust or quit 4. move experimental sandbox features to `[windows] sandbox="elevated|unelevatd"` 5. Copy tweaks = elevated -> default, non-elevated -> non-admin
This commit is contained in:
@@ -1800,14 +1800,23 @@ impl CodexMessageProcessor {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Persist windows sandbox feature.
|
||||
// Persist Windows sandbox mode.
|
||||
// TODO: persist default config in general.
|
||||
let mut request_overrides = request_overrides.unwrap_or_default();
|
||||
if cfg!(windows) && self.config.features.enabled(Feature::WindowsSandbox) {
|
||||
request_overrides.insert(
|
||||
"features.experimental_windows_sandbox".to_string(),
|
||||
serde_json::json!(true),
|
||||
);
|
||||
if cfg!(windows) {
|
||||
match WindowsSandboxLevel::from_config(&self.config) {
|
||||
WindowsSandboxLevel::Elevated => {
|
||||
request_overrides
|
||||
.insert("windows.sandbox".to_string(), serde_json::json!("elevated"));
|
||||
}
|
||||
WindowsSandboxLevel::RestrictedToken => {
|
||||
request_overrides.insert(
|
||||
"windows.sandbox".to_string(),
|
||||
serde_json::json!("unelevated"),
|
||||
);
|
||||
}
|
||||
WindowsSandboxLevel::Disabled => {}
|
||||
}
|
||||
}
|
||||
|
||||
let cloud_requirements = self.current_cloud_requirements();
|
||||
@@ -2933,13 +2942,22 @@ impl CodexMessageProcessor {
|
||||
read_history_cwd_from_state_db(&self.config, source_thread_id, rollout_path.as_path())
|
||||
.await;
|
||||
|
||||
// Persist windows sandbox feature.
|
||||
// Persist Windows sandbox mode.
|
||||
let mut cli_overrides = cli_overrides.unwrap_or_default();
|
||||
if cfg!(windows) && self.config.features.enabled(Feature::WindowsSandbox) {
|
||||
cli_overrides.insert(
|
||||
"features.experimental_windows_sandbox".to_string(),
|
||||
serde_json::json!(true),
|
||||
);
|
||||
if cfg!(windows) {
|
||||
match WindowsSandboxLevel::from_config(&self.config) {
|
||||
WindowsSandboxLevel::Elevated => {
|
||||
cli_overrides
|
||||
.insert("windows.sandbox".to_string(), serde_json::json!("elevated"));
|
||||
}
|
||||
WindowsSandboxLevel::RestrictedToken => {
|
||||
cli_overrides.insert(
|
||||
"windows.sandbox".to_string(),
|
||||
serde_json::json!("unelevated"),
|
||||
);
|
||||
}
|
||||
WindowsSandboxLevel::Disabled => {}
|
||||
}
|
||||
}
|
||||
let request_overrides = if cli_overrides.is_empty() {
|
||||
None
|
||||
@@ -3855,13 +3873,24 @@ impl CodexMessageProcessor {
|
||||
include_apply_patch_tool,
|
||||
} = overrides;
|
||||
|
||||
// Persist windows sandbox feature.
|
||||
// Persist Windows sandbox mode.
|
||||
let mut request_overrides = request_overrides.unwrap_or_default();
|
||||
if cfg!(windows) && self.config.features.enabled(Feature::WindowsSandbox) {
|
||||
request_overrides.insert(
|
||||
"features.experimental_windows_sandbox".to_string(),
|
||||
serde_json::json!(true),
|
||||
);
|
||||
if cfg!(windows) {
|
||||
match WindowsSandboxLevel::from_config(&self.config) {
|
||||
WindowsSandboxLevel::Elevated => {
|
||||
request_overrides.insert(
|
||||
"windows.sandbox".to_string(),
|
||||
serde_json::json!("elevated"),
|
||||
);
|
||||
}
|
||||
WindowsSandboxLevel::RestrictedToken => {
|
||||
request_overrides.insert(
|
||||
"windows.sandbox".to_string(),
|
||||
serde_json::json!("unelevated"),
|
||||
);
|
||||
}
|
||||
WindowsSandboxLevel::Disabled => {}
|
||||
}
|
||||
}
|
||||
|
||||
let typesafe_overrides = ConfigOverrides {
|
||||
@@ -4034,13 +4063,24 @@ impl CodexMessageProcessor {
|
||||
include_apply_patch_tool,
|
||||
} = overrides;
|
||||
|
||||
// Persist windows sandbox feature.
|
||||
// Persist Windows sandbox mode.
|
||||
let mut cli_overrides = cli_overrides.unwrap_or_default();
|
||||
if cfg!(windows) && self.config.features.enabled(Feature::WindowsSandbox) {
|
||||
cli_overrides.insert(
|
||||
"features.experimental_windows_sandbox".to_string(),
|
||||
serde_json::json!(true),
|
||||
);
|
||||
if cfg!(windows) {
|
||||
match WindowsSandboxLevel::from_config(&self.config) {
|
||||
WindowsSandboxLevel::Elevated => {
|
||||
cli_overrides.insert(
|
||||
"windows.sandbox".to_string(),
|
||||
serde_json::json!("elevated"),
|
||||
);
|
||||
}
|
||||
WindowsSandboxLevel::RestrictedToken => {
|
||||
cli_overrides.insert(
|
||||
"windows.sandbox".to_string(),
|
||||
serde_json::json!("unelevated"),
|
||||
);
|
||||
}
|
||||
WindowsSandboxLevel::Disabled => {}
|
||||
}
|
||||
}
|
||||
let request_overrides = if cli_overrides.is_empty() {
|
||||
None
|
||||
|
||||
@@ -331,6 +331,14 @@
|
||||
},
|
||||
"web_search": {
|
||||
"$ref": "#/definitions/WebSearchMode"
|
||||
},
|
||||
"windows": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WindowsToml"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1144,6 +1152,22 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WindowsSandboxModeToml": {
|
||||
"enum": [
|
||||
"elevated",
|
||||
"unelevated"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WindowsToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/WindowsSandboxModeToml"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WireApi": {
|
||||
"description": "Wire protocol that the provider speaks.",
|
||||
"oneOf": [
|
||||
@@ -1636,6 +1660,15 @@
|
||||
],
|
||||
"description": "Controls the web search tool mode: disabled, cached, or live."
|
||||
},
|
||||
"windows": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WindowsToml"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Windows-specific configuration."
|
||||
},
|
||||
"windows_wsl_setup_acknowledged": {
|
||||
"description": "Tracks whether the Windows onboarding screen has been acknowledged.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -819,6 +819,44 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_windows_sandbox_mode(mut self, mode: &str) -> Self {
|
||||
let segments = if let Some(profile) = self.profile.as_ref() {
|
||||
vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"windows".to_string(),
|
||||
"sandbox".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["windows".to_string(), "sandbox".to_string()]
|
||||
};
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments,
|
||||
value: value(mode),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_legacy_windows_sandbox_keys(mut self) -> Self {
|
||||
for key in [
|
||||
"experimental_windows_sandbox",
|
||||
"elevated_windows_sandbox",
|
||||
"enable_experimental_windows_sandbox",
|
||||
] {
|
||||
let mut segments = vec!["features".to_string(), key.to_string()];
|
||||
if let Some(profile) = self.profile.as_ref() {
|
||||
segments = vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"features".to_string(),
|
||||
key.to_string(),
|
||||
];
|
||||
}
|
||||
self.edits.push(ConfigEdit::ClearPath { segments });
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_edits<I>(mut self, edits: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = ConfigEdit>,
|
||||
|
||||
@@ -19,6 +19,8 @@ use crate::config::types::ShellEnvironmentPolicyToml;
|
||||
use crate::config::types::SkillsConfig;
|
||||
use crate::config::types::Tui;
|
||||
use crate::config::types::UriBasedFileOpener;
|
||||
use crate::config::types::WindowsSandboxModeToml;
|
||||
use crate::config::types::WindowsToml;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
@@ -45,6 +47,7 @@ use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||
use codex_app_server_protocol::Tools;
|
||||
use codex_app_server_protocol::UserSavedConfig;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
@@ -161,10 +164,6 @@ pub struct Config {
|
||||
/// for either of approval_policy or sandbox_mode.
|
||||
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
|
||||
|
||||
/// On Windows, indicates that a previously configured workspace-write sandbox
|
||||
/// was coerced to read-only because native auto mode is unsupported.
|
||||
pub forced_auto_mode_downgraded_on_windows: bool,
|
||||
|
||||
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||
|
||||
/// When `true`, `AgentReasoning` events emitted by the backend will be
|
||||
@@ -342,6 +341,10 @@ pub struct Config {
|
||||
/// Settings for ghost snapshots (used for undo).
|
||||
pub ghost_snapshot: GhostSnapshotConfig,
|
||||
|
||||
/// Effective Windows sandbox mode derived from `[windows].sandbox` or
|
||||
/// legacy feature keys.
|
||||
pub windows_sandbox_mode: Option<WindowsSandboxModeToml>,
|
||||
|
||||
/// Centralized feature flags; source of truth for feature gating.
|
||||
pub features: Features,
|
||||
|
||||
@@ -1037,6 +1040,10 @@ pub struct ConfigToml {
|
||||
/// OTEL configuration.
|
||||
pub otel: Option<crate::config::types::OtelConfigToml>,
|
||||
|
||||
/// Windows-specific configuration.
|
||||
#[serde(default)]
|
||||
pub windows: Option<WindowsToml>,
|
||||
|
||||
/// Tracks whether the Windows onboarding screen has been acknowledged.
|
||||
pub windows_wsl_setup_acknowledged: Option<bool>,
|
||||
|
||||
@@ -1139,12 +1146,6 @@ pub struct GhostSnapshotToml {
|
||||
pub disable_warnings: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SandboxPolicyResolution {
|
||||
pub policy: SandboxPolicy,
|
||||
pub forced_auto_mode_downgraded_on_windows: bool,
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
/// Derive the effective sandbox policy from the configuration.
|
||||
fn derive_sandbox_policy(
|
||||
@@ -1154,7 +1155,7 @@ impl ConfigToml {
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
resolved_cwd: &Path,
|
||||
sandbox_policy_constraint: Option<&Constrained<SandboxPolicy>>,
|
||||
) -> SandboxPolicyResolution {
|
||||
) -> SandboxPolicy {
|
||||
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
|
||||
|| profile_sandbox_mode.is_some()
|
||||
|| self.sandbox_mode.is_some();
|
||||
@@ -1162,10 +1163,19 @@ impl ConfigToml {
|
||||
.or(profile_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
.or_else(|| {
|
||||
// if no sandbox_mode is set, but user has marked directory as trusted or untrusted, use WorkspaceWrite
|
||||
// If no sandbox_mode is set but this directory has a trust decision,
|
||||
// default to workspace-write except on unsandboxed Windows where we
|
||||
// default to read-only.
|
||||
self.get_active_project(resolved_cwd).and_then(|p| {
|
||||
if p.is_trusted() || p.is_untrusted() {
|
||||
Some(SandboxMode::WorkspaceWrite)
|
||||
if cfg!(target_os = "windows")
|
||||
&& windows_sandbox_level
|
||||
== codex_protocol::config_types::WindowsSandboxLevel::Disabled
|
||||
{
|
||||
Some(SandboxMode::ReadOnly)
|
||||
} else {
|
||||
Some(SandboxMode::WorkspaceWrite)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -1190,8 +1200,7 @@ impl ConfigToml {
|
||||
},
|
||||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
};
|
||||
let mut forced_auto_mode_downgraded_on_windows = false;
|
||||
let mut downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| {
|
||||
let downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| {
|
||||
if cfg!(target_os = "windows")
|
||||
// If the experimental Windows sandbox is enabled, do not force a downgrade.
|
||||
&& windows_sandbox_level
|
||||
@@ -1199,7 +1208,6 @@ impl ConfigToml {
|
||||
&& matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. })
|
||||
{
|
||||
*policy = SandboxPolicy::new_read_only_policy();
|
||||
forced_auto_mode_downgraded_on_windows = true;
|
||||
}
|
||||
};
|
||||
if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) {
|
||||
@@ -1216,10 +1224,7 @@ impl ConfigToml {
|
||||
sandbox_policy = constraint.get().clone();
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
SandboxPolicyResolution {
|
||||
policy: sandbox_policy,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
}
|
||||
sandbox_policy
|
||||
}
|
||||
|
||||
/// Resolves the cwd to an existing project, or returns None if ConfigToml
|
||||
@@ -1438,6 +1443,7 @@ impl Config {
|
||||
};
|
||||
|
||||
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
|
||||
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
|
||||
let resolved_cwd = {
|
||||
use std::env;
|
||||
|
||||
@@ -1467,11 +1473,12 @@ impl Config {
|
||||
|| config_profile.sandbox_mode.is_some()
|
||||
|| cfg.sandbox_mode.is_some();
|
||||
|
||||
let windows_sandbox_level = WindowsSandboxLevel::from_features(&features);
|
||||
let SandboxPolicyResolution {
|
||||
policy: mut sandbox_policy,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
} = cfg.derive_sandbox_policy(
|
||||
let windows_sandbox_level = match windows_sandbox_mode {
|
||||
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
|
||||
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
|
||||
None => WindowsSandboxLevel::from_features(&features),
|
||||
};
|
||||
let mut sandbox_policy = cfg.derive_sandbox_policy(
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
@@ -1711,7 +1718,6 @@ impl Config {
|
||||
enforce_residency: enforce_residency.value,
|
||||
network,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
shell_environment_policy,
|
||||
notify: cfg.notify,
|
||||
user_instructions,
|
||||
@@ -1776,6 +1782,7 @@ impl Config {
|
||||
web_search_mode: constrained_web_search_mode.value,
|
||||
use_experimental_unified_exec_tool,
|
||||
ghost_snapshot,
|
||||
windows_sandbox_mode,
|
||||
features,
|
||||
suppress_unstable_features_warning: cfg
|
||||
.suppress_unstable_features_warning
|
||||
@@ -1881,21 +1888,29 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn set_windows_sandbox_enabled(&mut self, value: bool) {
|
||||
if value {
|
||||
self.features.enable(Feature::WindowsSandbox);
|
||||
self.forced_auto_mode_downgraded_on_windows = false;
|
||||
self.windows_sandbox_mode = if value {
|
||||
Some(WindowsSandboxModeToml::Unelevated)
|
||||
} else if matches!(
|
||||
self.windows_sandbox_mode,
|
||||
Some(WindowsSandboxModeToml::Unelevated)
|
||||
) {
|
||||
None
|
||||
} else {
|
||||
self.features.disable(Feature::WindowsSandbox);
|
||||
}
|
||||
self.windows_sandbox_mode
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_windows_elevated_sandbox_enabled(&mut self, value: bool) {
|
||||
if value {
|
||||
self.features.enable(Feature::WindowsSandboxElevated);
|
||||
self.forced_auto_mode_downgraded_on_windows = false;
|
||||
self.windows_sandbox_mode = if value {
|
||||
Some(WindowsSandboxModeToml::Elevated)
|
||||
} else if matches!(
|
||||
self.windows_sandbox_mode,
|
||||
Some(WindowsSandboxModeToml::Elevated)
|
||||
) {
|
||||
None
|
||||
} else {
|
||||
self.features.disable(Feature::WindowsSandboxElevated);
|
||||
}
|
||||
self.windows_sandbox_mode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2076,13 +2091,7 @@ network_access = false # This should be ignored.
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::DangerFullAccess,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(resolution, SandboxPolicy::DangerFullAccess);
|
||||
|
||||
let sandbox_read_only = r#"
|
||||
sandbox_mode = "read-only"
|
||||
@@ -2101,13 +2110,7 @@ network_access = true # This should be ignored.
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(resolution, SandboxPolicy::ReadOnly);
|
||||
|
||||
let writable_root = test_absolute_path("/my/workspace");
|
||||
let sandbox_workspace_write = format!(
|
||||
@@ -2135,24 +2138,15 @@ exclude_slash_tmp = true
|
||||
None,
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(resolution, SandboxPolicy::ReadOnly);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root.clone()],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root.clone()],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2185,24 +2179,15 @@ trust_level = "trusted"
|
||||
None,
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(resolution, SandboxPolicy::ReadOnly);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![writable_root],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2365,10 +2350,6 @@ trust_level = "trusted"
|
||||
|
||||
let expected_backend = AbsolutePathBuf::try_from(backend).unwrap();
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
config.forced_auto_mode_downgraded_on_windows,
|
||||
"expected workspace-write request to be downgraded on Windows"
|
||||
);
|
||||
match config.sandbox_policy.get() {
|
||||
&SandboxPolicy::ReadOnly => {}
|
||||
other => panic!("expected read-only policy on Windows, got {other:?}"),
|
||||
@@ -2701,13 +2682,11 @@ profile = "project"
|
||||
config.sandbox_policy.get(),
|
||||
SandboxPolicy::ReadOnly
|
||||
));
|
||||
assert!(config.forced_auto_mode_downgraded_on_windows);
|
||||
} else {
|
||||
assert!(matches!(
|
||||
config.sandbox_policy.get(),
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
));
|
||||
assert!(!config.forced_auto_mode_downgraded_on_windows);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -4026,7 +4005,6 @@ model_verbosity = "high"
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
network: None,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -4069,6 +4047,7 @@ model_verbosity = "high"
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("o3".to_string()),
|
||||
active_project: ProjectConfig { trust_level: None },
|
||||
windows_sandbox_mode: None,
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
notices: Default::default(),
|
||||
check_for_update_on_startup: true,
|
||||
@@ -4132,7 +4111,6 @@ model_verbosity = "high"
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
network: None,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -4175,6 +4153,7 @@ model_verbosity = "high"
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("gpt3".to_string()),
|
||||
active_project: ProjectConfig { trust_level: None },
|
||||
windows_sandbox_mode: None,
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
notices: Default::default(),
|
||||
check_for_update_on_startup: true,
|
||||
@@ -4236,7 +4215,6 @@ model_verbosity = "high"
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
network: None,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -4279,6 +4257,7 @@ model_verbosity = "high"
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("zdr".to_string()),
|
||||
active_project: ProjectConfig { trust_level: None },
|
||||
windows_sandbox_mode: None,
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
notices: Default::default(),
|
||||
check_for_update_on_startup: true,
|
||||
@@ -4326,7 +4305,6 @@ model_verbosity = "high"
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
network: None,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -4369,6 +4347,7 @@ model_verbosity = "high"
|
||||
suppress_unstable_features_warning: false,
|
||||
active_profile: Some("gpt5".to_string()),
|
||||
active_project: ProjectConfig { trust_level: None },
|
||||
windows_sandbox_mode: None,
|
||||
windows_wsl_setup_acknowledged: false,
|
||||
notices: Default::default(),
|
||||
check_for_update_on_startup: true,
|
||||
@@ -4671,15 +4650,13 @@ trust_level = "untrusted"
|
||||
// Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade)
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
matches!(resolution.policy, SandboxPolicy::ReadOnly),
|
||||
"Expected ReadOnly on Windows, got {:?}",
|
||||
resolution.policy
|
||||
matches!(resolution, SandboxPolicy::ReadOnly),
|
||||
"Expected ReadOnly on Windows, got {resolution:?}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
matches!(resolution.policy, SandboxPolicy::WorkspaceWrite { .. }),
|
||||
"Expected WorkspaceWrite for untrusted project, got {:?}",
|
||||
resolution.policy
|
||||
matches!(resolution, SandboxPolicy::WorkspaceWrite { .. }),
|
||||
"Expected WorkspaceWrite for untrusted project, got {resolution:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4722,7 +4699,7 @@ trust_level = "untrusted"
|
||||
Some(&constrained),
|
||||
);
|
||||
|
||||
assert_eq!(resolution.policy, SandboxPolicy::DangerFullAccess);
|
||||
assert_eq!(resolution, SandboxPolicy::DangerFullAccess);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4764,21 +4741,9 @@ trust_level = "untrusted"
|
||||
);
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(resolution, SandboxPolicy::ReadOnly);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::new_workspace_write_policy(),
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(resolution, SandboxPolicy::new_workspace_write_policy());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::config::types::Personality;
|
||||
use crate::config::types::WindowsToml;
|
||||
use crate::protocol::AskForApproval;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -40,6 +41,8 @@ pub struct ConfigProfile {
|
||||
pub tools_view_image: Option<bool>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub analytics: Option<crate::config::types::AnalyticsConfigToml>,
|
||||
#[serde(default)]
|
||||
pub windows: Option<WindowsToml>,
|
||||
/// Optional feature toggles scoped to this profile.
|
||||
#[serde(default)]
|
||||
// Injects known feature keys into the schema and forbids unknown keys.
|
||||
|
||||
@@ -24,6 +24,19 @@ use serde::de::Error as SerdeError;
|
||||
|
||||
pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WindowsSandboxModeToml {
|
||||
Elevated,
|
||||
Unelevated,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct WindowsToml {
|
||||
pub sandbox: Option<WindowsSandboxModeToml>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum McpServerDisabledReason {
|
||||
Unknown,
|
||||
|
||||
@@ -493,13 +493,13 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::WindowsSandbox,
|
||||
key: "experimental_windows_sandbox",
|
||||
stage: Stage::UnderDevelopment,
|
||||
stage: Stage::Removed,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::WindowsSandboxElevated,
|
||||
key: "elevated_windows_sandbox",
|
||||
stage: Stage::UnderDevelopment,
|
||||
stage: Stage::Removed,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config::profile::ConfigProfile;
|
||||
use crate::config::types::WindowsSandboxModeToml;
|
||||
use crate::features::Feature;
|
||||
use crate::features::Features;
|
||||
use crate::features::FeaturesToml;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -19,7 +24,11 @@ pub trait WindowsSandboxLevelExt {
|
||||
|
||||
impl WindowsSandboxLevelExt for WindowsSandboxLevel {
|
||||
fn from_config(config: &Config) -> WindowsSandboxLevel {
|
||||
Self::from_features(&config.features)
|
||||
match config.windows_sandbox_mode {
|
||||
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
|
||||
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
|
||||
None => Self::from_features(&config.features),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_features(features: &Features) -> WindowsSandboxLevel {
|
||||
@@ -42,6 +51,66 @@ pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandbo
|
||||
WindowsSandboxLevel::from_features(features)
|
||||
}
|
||||
|
||||
pub fn resolve_windows_sandbox_mode(
|
||||
cfg: &ConfigToml,
|
||||
profile: &ConfigProfile,
|
||||
) -> Option<WindowsSandboxModeToml> {
|
||||
if let Some(mode) = legacy_windows_sandbox_mode(profile.features.as_ref()) {
|
||||
return Some(mode);
|
||||
}
|
||||
if legacy_windows_sandbox_keys_present(profile.features.as_ref()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
profile
|
||||
.windows
|
||||
.as_ref()
|
||||
.and_then(|windows| windows.sandbox)
|
||||
.or_else(|| cfg.windows.as_ref().and_then(|windows| windows.sandbox))
|
||||
.or_else(|| legacy_windows_sandbox_mode(cfg.features.as_ref()))
|
||||
}
|
||||
|
||||
fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool {
|
||||
let Some(entries) = features.map(|features| &features.entries) else {
|
||||
return false;
|
||||
};
|
||||
entries.contains_key(Feature::WindowsSandboxElevated.key())
|
||||
|| entries.contains_key(Feature::WindowsSandbox.key())
|
||||
|| entries.contains_key("enable_experimental_windows_sandbox")
|
||||
}
|
||||
|
||||
pub fn legacy_windows_sandbox_mode(
|
||||
features: Option<&FeaturesToml>,
|
||||
) -> Option<WindowsSandboxModeToml> {
|
||||
let entries = features.map(|features| &features.entries)?;
|
||||
legacy_windows_sandbox_mode_from_entries(entries)
|
||||
}
|
||||
|
||||
pub fn legacy_windows_sandbox_mode_from_entries(
|
||||
entries: &BTreeMap<String, bool>,
|
||||
) -> Option<WindowsSandboxModeToml> {
|
||||
if entries
|
||||
.get(Feature::WindowsSandboxElevated.key())
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(WindowsSandboxModeToml::Elevated);
|
||||
}
|
||||
if entries
|
||||
.get(Feature::WindowsSandbox.key())
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
|| entries
|
||||
.get("enable_experimental_windows_sandbox")
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(WindowsSandboxModeToml::Unelevated)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool {
|
||||
codex_windows_sandbox::sandbox_setup_is_complete(codex_home)
|
||||
@@ -114,11 +183,42 @@ pub fn run_elevated_setup(
|
||||
anyhow::bail!("elevated Windows sandbox setup is only supported on Windows")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn run_legacy_setup_preflight(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
codex_home: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
codex_windows_sandbox::run_windows_sandbox_legacy_preflight(
|
||||
policy,
|
||||
policy_cwd,
|
||||
codex_home,
|
||||
command_cwd,
|
||||
env_map,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn run_legacy_setup_preflight(
|
||||
_policy: &SandboxPolicy,
|
||||
_policy_cwd: &Path,
|
||||
_command_cwd: &Path,
|
||||
_env_map: &HashMap<String, String>,
|
||||
_codex_home: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
anyhow::bail!("legacy Windows sandbox setup is only supported on Windows")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::types::WindowsToml;
|
||||
use crate::features::Features;
|
||||
use crate::features::FeaturesToml;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn elevated_flag_works_by_itself() {
|
||||
@@ -163,4 +263,86 @@ mod tests {
|
||||
WindowsSandboxLevel::Elevated
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_mode_prefers_elevated() {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("experimental_windows_sandbox".to_string(), true);
|
||||
entries.insert("elevated_windows_sandbox".to_string(), true);
|
||||
|
||||
assert_eq!(
|
||||
legacy_windows_sandbox_mode_from_entries(&entries),
|
||||
Some(WindowsSandboxModeToml::Elevated)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_mode_supports_alias_key() {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("enable_experimental_windows_sandbox".to_string(), true);
|
||||
|
||||
assert_eq!(
|
||||
legacy_windows_sandbox_mode_from_entries(&entries),
|
||||
Some(WindowsSandboxModeToml::Unelevated)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_windows_sandbox_mode_prefers_profile_windows() {
|
||||
let cfg = ConfigToml {
|
||||
windows: Some(WindowsToml {
|
||||
sandbox: Some(WindowsSandboxModeToml::Unelevated),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let profile = ConfigProfile {
|
||||
windows: Some(WindowsToml {
|
||||
sandbox: Some(WindowsSandboxModeToml::Elevated),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_windows_sandbox_mode(&cfg, &profile),
|
||||
Some(WindowsSandboxModeToml::Elevated)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_windows_sandbox_mode_falls_back_to_legacy_keys() {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("experimental_windows_sandbox".to_string(), true);
|
||||
let cfg = ConfigToml {
|
||||
features: Some(FeaturesToml { entries }),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_windows_sandbox_mode(&cfg, &ConfigProfile::default()),
|
||||
Some(WindowsSandboxModeToml::Unelevated)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_windows_sandbox_mode_profile_legacy_false_blocks_top_level_legacy_true() {
|
||||
let mut profile_entries = BTreeMap::new();
|
||||
profile_entries.insert("experimental_windows_sandbox".to_string(), false);
|
||||
let profile = ConfigProfile {
|
||||
features: Some(FeaturesToml {
|
||||
entries: profile_entries,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut cfg_entries = BTreeMap::new();
|
||||
cfg_entries.insert("experimental_windows_sandbox".to_string(), true);
|
||||
let cfg = ConfigToml {
|
||||
features: Some(FeaturesToml {
|
||||
entries: cfg_entries,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(resolve_windows_sandbox_mode(&cfg, &profile), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,6 +944,7 @@ impl App {
|
||||
session_selection: SessionSelection,
|
||||
feedback: codex_feedback::CodexFeedback,
|
||||
is_first_run: bool,
|
||||
should_prompt_windows_sandbox_nux_at_startup: bool,
|
||||
) -> Result<AppExitInfo> {
|
||||
use tokio_stream::StreamExt;
|
||||
let (app_event_tx, mut app_event_rx) = unbounded_channel();
|
||||
@@ -1107,7 +1108,8 @@ impl App {
|
||||
}
|
||||
};
|
||||
|
||||
chat_widget.maybe_prompt_windows_sandbox_enable();
|
||||
chat_widget
|
||||
.maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup);
|
||||
|
||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -1788,6 +1790,42 @@ impl App {
|
||||
let _ = preset;
|
||||
}
|
||||
}
|
||||
AppEvent::BeginWindowsSandboxLegacySetup { preset } => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let policy = preset.sandbox.clone();
|
||||
let policy_cwd = self.config.cwd.clone();
|
||||
let command_cwd = policy_cwd.clone();
|
||||
let env_map: std::collections::HashMap<String, String> =
|
||||
std::env::vars().collect();
|
||||
let codex_home = self.config.codex_home.clone();
|
||||
let tx = self.app_event_tx.clone();
|
||||
|
||||
self.chat_widget.show_windows_sandbox_setup_status();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Err(err) = codex_core::windows_sandbox::run_legacy_setup_preflight(
|
||||
&policy,
|
||||
policy_cwd.as_path(),
|
||||
command_cwd.as_path(),
|
||||
&env_map,
|
||||
codex_home.as_path(),
|
||||
) {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"failed to preflight non-admin Windows sandbox setup"
|
||||
);
|
||||
}
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
preset,
|
||||
mode: WindowsSandboxEnableMode::Legacy,
|
||||
});
|
||||
});
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = preset;
|
||||
}
|
||||
}
|
||||
AppEvent::EnableWindowsSandboxForAgentMode { preset, mode } => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -1800,33 +1838,26 @@ impl App {
|
||||
);
|
||||
}
|
||||
let profile = self.active_profile.as_deref();
|
||||
let feature_key = Feature::WindowsSandbox.key();
|
||||
let elevated_key = Feature::WindowsSandboxElevated.key();
|
||||
let elevated_enabled = matches!(mode, WindowsSandboxEnableMode::Elevated);
|
||||
let mut builder =
|
||||
ConfigEditsBuilder::new(&self.config.codex_home).with_profile(profile);
|
||||
if elevated_enabled {
|
||||
builder = builder.set_feature_enabled(elevated_key, true);
|
||||
} else {
|
||||
builder = builder
|
||||
.set_feature_enabled(feature_key, true)
|
||||
.set_feature_enabled(elevated_key, false);
|
||||
}
|
||||
let builder = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_windows_sandbox_mode(if elevated_enabled {
|
||||
"elevated"
|
||||
} else {
|
||||
"unelevated"
|
||||
})
|
||||
.clear_legacy_windows_sandbox_keys();
|
||||
match builder.apply().await {
|
||||
Ok(()) => {
|
||||
if elevated_enabled {
|
||||
self.config.set_windows_sandbox_enabled(false);
|
||||
self.config.set_windows_elevated_sandbox_enabled(true);
|
||||
self.chat_widget
|
||||
.set_feature_enabled(Feature::WindowsSandboxElevated, true);
|
||||
} else {
|
||||
self.config.set_windows_sandbox_enabled(true);
|
||||
self.config.set_windows_elevated_sandbox_enabled(false);
|
||||
self.chat_widget
|
||||
.set_feature_enabled(Feature::WindowsSandbox, true);
|
||||
self.chat_widget
|
||||
.set_feature_enabled(Feature::WindowsSandboxElevated, false);
|
||||
}
|
||||
self.chat_widget.clear_forced_auto_mode_downgrade();
|
||||
self.chat_widget
|
||||
.set_windows_sandbox_mode(self.config.windows_sandbox_mode);
|
||||
let windows_sandbox_level =
|
||||
WindowsSandboxLevel::from_config(&self.config);
|
||||
if let Some((sample_paths, extra_count, failed_scan)) =
|
||||
@@ -1871,17 +1902,15 @@ impl App {
|
||||
.send(AppEvent::UpdateAskForApprovalPolicy(preset.approval));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateSandboxPolicy(preset.sandbox.clone()));
|
||||
self.chat_widget.add_info_message(
|
||||
match mode {
|
||||
WindowsSandboxEnableMode::Elevated => {
|
||||
"Enabled elevated agent sandbox.".to_string()
|
||||
}
|
||||
WindowsSandboxEnableMode::Legacy => {
|
||||
"Enabled non-elevated agent sandbox.".to_string()
|
||||
}
|
||||
},
|
||||
None,
|
||||
);
|
||||
let _ = mode;
|
||||
self.chat_widget.add_plain_history_lines(vec![
|
||||
Line::from(vec!["• ".dim(), "Sandbox ready".into()]),
|
||||
Line::from(vec![
|
||||
" ".into(),
|
||||
"Codex can now safely edit files and execute commands in your computer"
|
||||
.dark_gray(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1989,21 +2018,17 @@ impl App {
|
||||
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
|
||||
| codex_core::protocol::SandboxPolicy::ReadOnly
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
let policy_for_chat = policy.clone();
|
||||
|
||||
if let Err(err) = self.config.sandbox_policy.set(policy.clone()) {
|
||||
if let Err(err) = self.config.sandbox_policy.set(policy) {
|
||||
tracing::warn!(%err, "failed to set sandbox policy on app config");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to set sandbox policy: {err}"));
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|
||||
|| WindowsSandboxLevel::from_config(&self.config)
|
||||
!= WindowsSandboxLevel::Disabled
|
||||
{
|
||||
self.config.forced_auto_mode_downgraded_on_windows = false;
|
||||
}
|
||||
if let Err(err) = self.chat_widget.set_sandbox_policy(policy) {
|
||||
if let Err(err) = self.chat_widget.set_sandbox_policy(policy_for_chat) {
|
||||
tracing::warn!(%err, "failed to set sandbox policy on chat config");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to set sandbox policy: {err}"));
|
||||
|
||||
@@ -212,6 +212,12 @@ pub(crate) enum AppEvent {
|
||||
preset: ApprovalPreset,
|
||||
},
|
||||
|
||||
/// Begin the non-elevated Windows sandbox setup flow.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
BeginWindowsSandboxLegacySetup {
|
||||
preset: ApprovalPreset,
|
||||
},
|
||||
|
||||
/// Enable the Windows sandbox feature and switch to Agent mode.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
EnableWindowsSandboxForAgentMode {
|
||||
|
||||
@@ -821,7 +821,7 @@ mod tests {
|
||||
}];
|
||||
let footer_note = Line::from(vec![
|
||||
"Note: ".dim(),
|
||||
"Use /setup-elevated-sandbox".cyan(),
|
||||
"Use /setup-default-sandbox".cyan(),
|
||||
" to allow network access.".dim(),
|
||||
]);
|
||||
let view = ListSelectionView::new(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/list_selection_view.rs
|
||||
assertion_line: 683
|
||||
assertion_line: 828
|
||||
expression: "render_lines_with_width(&view, 40)"
|
||||
---
|
||||
|
||||
@@ -9,6 +9,6 @@ expression: "render_lines_with_width(&view, 40)"
|
||||
› 1. Read Only (current) Codex can
|
||||
read files
|
||||
|
||||
Note: Use /setup-elevated-sandbox to
|
||||
Note: Use /setup-default-sandbox to
|
||||
allow network access.
|
||||
Press enter to confirm or esc to go ba
|
||||
|
||||
@@ -51,6 +51,7 @@ use codex_chatgpt::connectors;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConstraintResult;
|
||||
use codex_core::config::types::Notifications;
|
||||
use codex_core::config::types::WindowsSandboxModeToml;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
@@ -5286,7 +5287,7 @@ impl ChatWidget {
|
||||
let is_current =
|
||||
Self::preset_matches_current(current_approval, current_sandbox, &preset);
|
||||
let name = if preset.id == "auto" && windows_degraded_sandbox_enabled {
|
||||
"Default (non-elevated sandbox)".to_string()
|
||||
"Default (non-admin sandbox)".to_string()
|
||||
} else {
|
||||
preset.label.to_string()
|
||||
};
|
||||
@@ -5370,8 +5371,8 @@ 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(),
|
||||
"The non-admin sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To upgrade to the default sandbox, run ".dim(),
|
||||
"/setup-default-sandbox".cyan(),
|
||||
".".dim(),
|
||||
]
|
||||
.into()
|
||||
@@ -5714,59 +5715,24 @@ impl ChatWidget {
|
||||
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)
|
||||
});
|
||||
self.otel_manager
|
||||
.counter("codex.windows_sandbox.elevated_prompt_shown", 1, &[]);
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(*Box::new(
|
||||
Paragraph::new(vec![
|
||||
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"],
|
||||
line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more <https://developers.openai.com/codex/windows>"],
|
||||
])
|
||||
.wrap(Wrap { trim: false }),
|
||||
));
|
||||
|
||||
let stay_label = if stay_full_access {
|
||||
"Stay in Agent Full Access".to_string()
|
||||
} else {
|
||||
"Stay in Read-Only".to_string()
|
||||
};
|
||||
let mut 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()
|
||||
};
|
||||
stay_actions.insert(
|
||||
0,
|
||||
Box::new({
|
||||
let otel = self.otel_manager.clone();
|
||||
move |_tx| {
|
||||
otel.counter("codex.windows_sandbox.elevated_prompt_decline", 1, &[]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let accept_otel = self.otel_manager.clone();
|
||||
let legacy_otel = self.otel_manager.clone();
|
||||
let legacy_preset = preset.clone();
|
||||
let quit_otel = self.otel_manager.clone();
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Set up agent sandbox (requires elevation)".to_string(),
|
||||
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", 1, &[]);
|
||||
@@ -5778,9 +5744,24 @@ impl ChatWidget {
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: stay_label,
|
||||
name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(),
|
||||
description: None,
|
||||
actions: stay_actions,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
legacy_otel.counter("codex.windows_sandbox.fallback_use_legacy", 1, &[]);
|
||||
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
|
||||
preset: legacy_preset.clone(),
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Quit".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
quit_otel.counter("codex.windows_sandbox.elevated_prompt_decline", 1, &[]);
|
||||
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -5808,23 +5789,16 @@ impl ChatWidget {
|
||||
|
||||
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 mut lines = Vec::new();
|
||||
lines.push(line!["Use Non-Elevated Sandbox?".bold()]);
|
||||
lines.push(line![
|
||||
"Couldn't set up your sandbox with Administrator permissions".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."
|
||||
"You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected."
|
||||
]);
|
||||
lines.push(line![
|
||||
"Learn more: https://developers.openai.com/codex/windows"
|
||||
"Learn more <https://developers.openai.com/codex/windows>"
|
||||
]);
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
@@ -5832,34 +5806,10 @@ impl ChatWidget {
|
||||
|
||||
let elevated_preset = preset.clone();
|
||||
let legacy_preset = preset;
|
||||
let stay_label = if stay_full_access {
|
||||
"Stay in Agent Full Access".to_string()
|
||||
} else {
|
||||
"Stay in Read-Only".to_string()
|
||||
};
|
||||
let mut 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()
|
||||
};
|
||||
stay_actions.insert(
|
||||
0,
|
||||
Box::new({
|
||||
let otel = self.otel_manager.clone();
|
||||
move |_tx| {
|
||||
otel.counter("codex.windows_sandbox.fallback_stay_current", 1, &[]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
let quit_otel = self.otel_manager.clone();
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: "Try elevated agent sandbox setup again".to_string(),
|
||||
name: "Try setting up admin sandbox again".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new({
|
||||
let otel = self.otel_manager.clone();
|
||||
@@ -5875,16 +5825,15 @@ impl ChatWidget {
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Use non-elevated agent sandbox".to_string(),
|
||||
name: "Use Codex with non-admin sandbox".to_string(),
|
||||
description: None,
|
||||
actions: vec![Box::new({
|
||||
let otel = self.otel_manager.clone();
|
||||
let preset = legacy_preset;
|
||||
move |tx| {
|
||||
otel.counter("codex.windows_sandbox.fallback_use_legacy", 1, &[]);
|
||||
tx.send(AppEvent::EnableWindowsSandboxForAgentMode {
|
||||
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
|
||||
preset: preset.clone(),
|
||||
mode: WindowsSandboxEnableMode::Legacy,
|
||||
});
|
||||
}
|
||||
})],
|
||||
@@ -5892,9 +5841,12 @@ impl ChatWidget {
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: stay_label,
|
||||
name: "Quit".to_string(),
|
||||
description: None,
|
||||
actions: stay_actions,
|
||||
actions: vec![Box::new(move |tx| {
|
||||
quit_otel.counter("codex.windows_sandbox.fallback_stay_current", 1, &[]);
|
||||
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -5918,8 +5870,8 @@ 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
|
||||
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) {
|
||||
if show_now
|
||||
&& WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled
|
||||
&& let Some(preset) = builtin_approval_presets()
|
||||
.into_iter()
|
||||
@@ -5930,7 +5882,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {}
|
||||
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) {
|
||||
@@ -5942,7 +5894,10 @@ impl ChatWidget {
|
||||
);
|
||||
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.set_status(
|
||||
"Setting up sandbox...".to_string(),
|
||||
Some("Hang tight, this may take a few minutes".to_string()),
|
||||
);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -5960,15 +5915,6 @@ impl ChatWidget {
|
||||
#[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;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn clear_forced_auto_mode_downgrade(&mut self) {}
|
||||
|
||||
/// Set the approval policy in the widget's config copy.
|
||||
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
||||
if let Err(err) = self.config.approval_policy.set(policy) {
|
||||
@@ -5977,21 +5923,25 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
/// Set the sandbox policy in the widget's config copy.
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly)
|
||||
|| WindowsSandboxLevel::from_config(&self.config) != WindowsSandboxLevel::Disabled;
|
||||
|
||||
self.config.sandbox_policy.set(policy)?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if should_clear_downgrade {
|
||||
self.config.forced_auto_mode_downgraded_on_windows = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option<WindowsSandboxModeToml>) {
|
||||
self.config.windows_sandbox_mode = mode;
|
||||
#[cfg(target_os = "windows")]
|
||||
self.bottom_pane.set_windows_degraded_sandbox_active(
|
||||
codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED
|
||||
&& matches!(
|
||||
WindowsSandboxLevel::from_config(&self.config),
|
||||
WindowsSandboxLevel::RestrictedToken
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) {
|
||||
if enabled {
|
||||
|
||||
@@ -8,7 +8,7 @@ expression: popup
|
||||
› 1. Read Only (current) Codex can read files in the current
|
||||
workspace. Approval is required to edit
|
||||
files or access the internet.
|
||||
2. Default (non-elevated sandbox) Codex can read and edit files in the
|
||||
2. Default (non-admin sandbox) Codex can read and edit files in the
|
||||
current workspace, and run commands.
|
||||
Approval is required to access the
|
||||
internet or edit other files.
|
||||
@@ -17,7 +17,7 @@ expression: popup
|
||||
asking for approval. Exercise caution
|
||||
when using.
|
||||
|
||||
The non-elevated sandbox protects your files and prevents network access under
|
||||
The non-admin 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 /setup-elevated-sandbox.
|
||||
upgrade to the default sandbox, run /setup-default-sandbox.
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
@@ -4161,9 +4161,18 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() {
|
||||
chat.open_approvals_popup();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
insta::with_settings!({ snapshot_suffix => "windows_degraded" }, {
|
||||
assert_snapshot!("approvals_selection_popup", popup);
|
||||
});
|
||||
assert!(
|
||||
popup.contains("Default (non-admin sandbox)"),
|
||||
"expected degraded sandbox label in approvals popup: {popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("/setup-default-sandbox"),
|
||||
"expected setup hint in approvals popup: {popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("non-admin sandbox"),
|
||||
"expected degraded sandbox note in approvals popup: {popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4216,8 +4225,12 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
|
||||
|
||||
let popup = render_bottom_popup(&chat, 120);
|
||||
assert!(
|
||||
popup.contains("requires elevation"),
|
||||
"expected auto mode prompt to mention elevation, popup: {popup}"
|
||||
popup.contains("requires Administrator permissions"),
|
||||
"expected auto mode prompt to mention Administrator permissions, popup: {popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Use non-admin sandbox"),
|
||||
"expected auto mode prompt to include non-admin fallback option, popup: {popup}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4228,22 +4241,40 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() {
|
||||
|
||||
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();
|
||||
chat.maybe_prompt_windows_sandbox_enable(true);
|
||||
|
||||
let popup = render_bottom_popup(&chat, 120);
|
||||
assert!(
|
||||
popup.contains("requires elevation"),
|
||||
"expected startup prompt to explain elevation: {popup}"
|
||||
popup.contains("requires Administrator permissions"),
|
||||
"expected startup prompt to mention Administrator permissions: {popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Set up agent sandbox"),
|
||||
"expected startup prompt to offer agent sandbox setup: {popup}"
|
||||
popup.contains("Set up default sandbox"),
|
||||
"expected startup prompt to offer default sandbox setup: {popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Stay in"),
|
||||
"expected startup prompt to offer staying in current mode: {popup}"
|
||||
popup.contains("Use non-admin sandbox"),
|
||||
"expected startup prompt to offer non-admin fallback: {popup}"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Quit"),
|
||||
"expected startup prompt to offer quit action: {popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tokio::test]
|
||||
async fn startup_does_not_prompt_for_windows_sandbox_when_not_requested() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.set_feature_enabled(Feature::WindowsSandbox, false);
|
||||
chat.set_feature_enabled(Feature::WindowsSandboxElevated, false);
|
||||
chat.maybe_prompt_windows_sandbox_enable(false);
|
||||
|
||||
assert!(
|
||||
chat.bottom_pane.no_modal_or_popup_active(),
|
||||
"expected no startup sandbox NUX popup when startup trigger is false"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,7 @@ async fn run_ratatui_app(
|
||||
let should_show_trust_screen_flag = should_show_trust_screen(&initial_config);
|
||||
let should_show_onboarding =
|
||||
should_show_onboarding(login_status, &initial_config, should_show_trust_screen_flag);
|
||||
let mut trust_decision_was_made = false;
|
||||
|
||||
let config = if should_show_onboarding {
|
||||
let show_login_screen = should_show_login_screen(login_status, &initial_config);
|
||||
@@ -499,6 +500,7 @@ async fn run_ratatui_app(
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
trust_decision_was_made = onboarding_result.directory_trust_decision.is_some();
|
||||
// If this onboarding run included the login step, always refresh cloud requirements and
|
||||
// rebuild config. This avoids missing newly available cloud requirements due to login
|
||||
// status detection edge cases.
|
||||
@@ -674,6 +676,9 @@ async fn run_ratatui_app(
|
||||
set_default_client_residency_requirement(config.enforce_residency.value());
|
||||
let active_profile = config.active_profile.clone();
|
||||
let should_show_trust_screen = should_show_trust_screen(&config);
|
||||
let should_prompt_windows_sandbox_nux_at_startup = cfg!(target_os = "windows")
|
||||
&& trust_decision_was_made
|
||||
&& WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled;
|
||||
|
||||
let Cli {
|
||||
prompt,
|
||||
@@ -697,6 +702,7 @@ async fn run_ratatui_app(
|
||||
session_selection,
|
||||
feedback,
|
||||
should_show_trust_screen, // Proxy to: is it a first run in this directory?
|
||||
should_prompt_windows_sandbox_nux_at_startup,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -881,12 +887,6 @@ 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")
|
||||
&& 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;
|
||||
}
|
||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
||||
// Respect explicit approval/sandbox overrides made by the user.
|
||||
return false;
|
||||
@@ -941,7 +941,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn windows_skips_trust_prompt_without_sandbox() -> std::io::Result<()> {
|
||||
async fn windows_shows_trust_prompt_without_sandbox() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = build_config(&temp_dir).await?;
|
||||
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
|
||||
@@ -949,17 +949,10 @@ mod tests {
|
||||
config.set_windows_sandbox_enabled(false);
|
||||
|
||||
let should_show = should_show_trust_screen(&config);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
!should_show,
|
||||
"Windows trust prompt should always be skipped on native Windows"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
should_show,
|
||||
"Non-Windows should still show trust prompt when project is untrusted"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
should_show,
|
||||
"Trust prompt should be shown when project trust is undecided"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::git_info::get_git_repo_root;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
@@ -82,7 +85,7 @@ impl OnboardingScreen {
|
||||
let cwd = config.cwd.clone();
|
||||
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
||||
let forced_login_method = config.forced_login_method;
|
||||
let codex_home = config.codex_home;
|
||||
let codex_home = config.codex_home.clone();
|
||||
let cli_auth_credentials_store_mode = config.cli_auth_credentials_store_mode;
|
||||
let mut steps: Vec<Step> = Vec::new();
|
||||
steps.push(Step::Welcome(WelcomeWidget::new(
|
||||
@@ -109,18 +112,18 @@ impl OnboardingScreen {
|
||||
animations_enabled: config.animations,
|
||||
}))
|
||||
}
|
||||
let is_git_repo = get_git_repo_root(&cwd).is_some();
|
||||
let highlighted = if is_git_repo {
|
||||
TrustDirectorySelection::Trust
|
||||
} else {
|
||||
// Default to not trusting the directory if it's not a git repo.
|
||||
TrustDirectorySelection::DontTrust
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_windows_create_sandbox_hint =
|
||||
WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let show_windows_create_sandbox_hint = false;
|
||||
let highlighted = TrustDirectorySelection::Trust;
|
||||
if show_trust_screen {
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
codex_home,
|
||||
is_git_repo,
|
||||
show_windows_create_sandbox_hint,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
highlighted,
|
||||
error: None,
|
||||
@@ -253,6 +256,16 @@ impl KeyboardHandler for OnboardingScreen {
|
||||
if let Some(active_step) = self.current_steps_mut().into_iter().last() {
|
||||
active_step.handle_key_event(key_event);
|
||||
}
|
||||
if self.steps.iter().any(|step| {
|
||||
if let Step::TrustDirectory(widget) = step {
|
||||
widget.should_quit()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
self.should_exit = true;
|
||||
self.is_done = true;
|
||||
}
|
||||
}
|
||||
self.request_frame.schedule_frame();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
source: tui/src/onboarding/trust_directory.rs
|
||||
assertion_line: 218
|
||||
expression: terminal.backend()
|
||||
---
|
||||
> You are running Codex in /workspace/project
|
||||
> You are in /workspace/project
|
||||
|
||||
Since this folder is version controlled, you may wish to allow Codex
|
||||
to work in this folder without asking for approval.
|
||||
Do you trust the contents of this directory? Working with untrusted
|
||||
contents comes with higher risk of prompt injection.
|
||||
|
||||
› 1. Yes, allow Codex to work in this folder without asking for
|
||||
approval
|
||||
2. No, ask me to approve edits and commands
|
||||
› 1. Yes, continue
|
||||
2. No, quit
|
||||
|
||||
Press enter to continue
|
||||
|
||||
@@ -27,7 +27,8 @@ use super::onboarding_screen::StepState;
|
||||
pub(crate) struct TrustDirectoryWidget {
|
||||
pub codex_home: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub is_git_repo: bool,
|
||||
pub show_windows_create_sandbox_hint: bool,
|
||||
pub should_quit: bool,
|
||||
pub selection: Option<TrustDirectorySelection>,
|
||||
pub highlighted: TrustDirectorySelection,
|
||||
pub error: Option<String>,
|
||||
@@ -36,7 +37,7 @@ pub(crate) struct TrustDirectoryWidget {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TrustDirectorySelection {
|
||||
Trust,
|
||||
DontTrust,
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl WidgetRef for &TrustDirectoryWidget {
|
||||
@@ -45,44 +46,24 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||||
|
||||
column.push(Line::from(vec![
|
||||
"> ".into(),
|
||||
"You are running Codex in ".bold(),
|
||||
"You are in ".bold(),
|
||||
self.cwd.to_string_lossy().to_string().into(),
|
||||
]));
|
||||
column.push("");
|
||||
|
||||
let guidance = if self.is_git_repo {
|
||||
"Since this folder is version controlled, you may wish to allow Codex to work in this folder without asking for approval."
|
||||
} else {
|
||||
"Since this folder is not version controlled, we recommend requiring approval of all edits and commands."
|
||||
};
|
||||
|
||||
column.push(
|
||||
Paragraph::new(guidance.to_string())
|
||||
Paragraph::new(
|
||||
"Do you trust the contents of this directory? Working with untrusted contents comes with higher risk of prompt injection.".to_string(),
|
||||
)
|
||||
.wrap(Wrap { trim: true })
|
||||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||||
);
|
||||
column.push("");
|
||||
|
||||
let mut options: Vec<(&str, TrustDirectorySelection)> = Vec::new();
|
||||
if self.is_git_repo {
|
||||
options.push((
|
||||
"Yes, allow Codex to work in this folder without asking for approval",
|
||||
TrustDirectorySelection::Trust,
|
||||
));
|
||||
options.push((
|
||||
"No, ask me to approve edits and commands",
|
||||
TrustDirectorySelection::DontTrust,
|
||||
));
|
||||
} else {
|
||||
options.push((
|
||||
"Allow Codex to work in this folder without asking for approval",
|
||||
TrustDirectorySelection::Trust,
|
||||
));
|
||||
options.push((
|
||||
"Require approval of edits and commands",
|
||||
TrustDirectorySelection::DontTrust,
|
||||
));
|
||||
}
|
||||
let options: Vec<(&str, TrustDirectorySelection)> = vec![
|
||||
("Yes, continue", TrustDirectorySelection::Trust),
|
||||
("No, quit", TrustDirectorySelection::Quit),
|
||||
];
|
||||
|
||||
for (idx, (text, selection)) in options.iter().enumerate() {
|
||||
column.push(selection_option_row(
|
||||
@@ -108,7 +89,11 @@ impl WidgetRef for &TrustDirectoryWidget {
|
||||
Line::from(vec![
|
||||
"Press ".dim(),
|
||||
key_hint::plain(KeyCode::Enter).into(),
|
||||
" to continue".dim(),
|
||||
if self.show_windows_create_sandbox_hint {
|
||||
" to continue and create a sandbox...".dim()
|
||||
} else {
|
||||
" to continue".dim()
|
||||
},
|
||||
])
|
||||
.inset(Insets::tlbr(0, 2, 0, 0)),
|
||||
);
|
||||
@@ -128,13 +113,13 @@ impl KeyboardHandler for TrustDirectoryWidget {
|
||||
self.highlighted = TrustDirectorySelection::Trust;
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.highlighted = TrustDirectorySelection::DontTrust;
|
||||
self.highlighted = TrustDirectorySelection::Quit;
|
||||
}
|
||||
KeyCode::Char('1') | KeyCode::Char('y') => self.handle_trust(),
|
||||
KeyCode::Char('2') | KeyCode::Char('n') => self.handle_dont_trust(),
|
||||
KeyCode::Char('2') | KeyCode::Char('n') => self.handle_quit(),
|
||||
KeyCode::Enter => match self.highlighted {
|
||||
TrustDirectorySelection::Trust => self.handle_trust(),
|
||||
TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
|
||||
TrustDirectorySelection::Quit => self.handle_quit(),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
@@ -143,9 +128,10 @@ impl KeyboardHandler for TrustDirectoryWidget {
|
||||
|
||||
impl StepStateProvider for TrustDirectoryWidget {
|
||||
fn get_step_state(&self) -> StepState {
|
||||
match self.selection {
|
||||
Some(_) => StepState::Complete,
|
||||
None => StepState::InProgress,
|
||||
if self.selection.is_some() || self.should_quit {
|
||||
StepState::Complete
|
||||
} else {
|
||||
StepState::InProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,19 +148,13 @@ impl TrustDirectoryWidget {
|
||||
self.selection = Some(TrustDirectorySelection::Trust);
|
||||
}
|
||||
|
||||
fn handle_dont_trust(&mut self) {
|
||||
self.highlighted = TrustDirectorySelection::DontTrust;
|
||||
let target =
|
||||
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
|
||||
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Untrusted) {
|
||||
tracing::error!("Failed to set project untrusted: {e:?}");
|
||||
self.error = Some(format!(
|
||||
"Failed to set untrusted for {}: {e}",
|
||||
target.display()
|
||||
));
|
||||
}
|
||||
fn handle_quit(&mut self) {
|
||||
self.highlighted = TrustDirectorySelection::Quit;
|
||||
self.should_quit = true;
|
||||
}
|
||||
|
||||
self.selection = Some(TrustDirectorySelection::DontTrust);
|
||||
pub fn should_quit(&self) -> bool {
|
||||
self.should_quit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,9 +178,10 @@ mod tests {
|
||||
let mut widget = TrustDirectoryWidget {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
cwd: PathBuf::from("."),
|
||||
is_git_repo: false,
|
||||
show_windows_create_sandbox_hint: false,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
highlighted: TrustDirectorySelection::DontTrust,
|
||||
highlighted: TrustDirectorySelection::Quit,
|
||||
error: None,
|
||||
};
|
||||
|
||||
@@ -213,7 +194,7 @@ mod tests {
|
||||
|
||||
let press = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
|
||||
widget.handle_key_event(press);
|
||||
assert_eq!(widget.selection, Some(TrustDirectorySelection::DontTrust));
|
||||
assert!(widget.should_quit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -222,7 +203,8 @@ mod tests {
|
||||
let widget = TrustDirectoryWidget {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
cwd: PathBuf::from("/workspace/project"),
|
||||
is_git_repo: true,
|
||||
show_windows_create_sandbox_hint: false,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
highlighted: TrustDirectorySelection::Trust,
|
||||
error: None,
|
||||
|
||||
@@ -15,7 +15,7 @@ pub enum SlashCommand {
|
||||
Model,
|
||||
Approvals,
|
||||
Permissions,
|
||||
#[strum(serialize = "setup-elevated-sandbox")]
|
||||
#[strum(serialize = "setup-default-sandbox")]
|
||||
ElevateSandbox,
|
||||
Experimental,
|
||||
Skills,
|
||||
@@ -77,7 +77,7 @@ impl SlashCommand {
|
||||
SlashCommand::Agent => "switch the active agent thread",
|
||||
SlashCommand::Approvals => "choose what Codex is allowed to do",
|
||||
SlashCommand::Permissions => "choose what Codex is allowed to do",
|
||||
SlashCommand::ElevateSandbox => "set up elevated agent sandbox",
|
||||
SlashCommand::ElevateSandbox => "set up default agent sandbox",
|
||||
SlashCommand::Experimental => "toggle experimental features",
|
||||
SlashCommand::Mcp => "list configured MCP tools",
|
||||
SlashCommand::Apps => "manage apps",
|
||||
|
||||
@@ -115,6 +115,8 @@ pub use token::get_current_token_for_restriction;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::run_windows_sandbox_capture;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::run_windows_sandbox_legacy_preflight;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows_impl::CaptureResult;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use winutil::string_from_sid_bytes;
|
||||
@@ -130,6 +132,8 @@ pub use stub::apply_world_writable_scan_and_denies;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::run_windows_sandbox_capture;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::run_windows_sandbox_legacy_preflight;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::CaptureResult;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -502,6 +506,50 @@ mod windows_impl {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_windows_sandbox_legacy_preflight(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
codex_home: &Path,
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let is_workspace_write = matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||
if !is_workspace_write {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_codex_home_exists(codex_home)?;
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let psid_generic =
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace) }.expect("valid workspace SID");
|
||||
let ws_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
|
||||
let psid_workspace =
|
||||
unsafe { convert_string_sid_to_sid(&ws_sid) }.expect("valid workspace SID");
|
||||
let current_dir = cwd.to_path_buf();
|
||||
let AllowDenyPaths { allow, deny } =
|
||||
compute_allow_paths(sandbox_policy, sandbox_policy_cwd, ¤t_dir, env_map);
|
||||
let canonical_cwd = canonicalize_path(¤t_dir);
|
||||
|
||||
unsafe {
|
||||
for p in &allow {
|
||||
let psid = if is_command_cwd_root(p, &canonical_cwd) {
|
||||
psid_workspace
|
||||
} else {
|
||||
psid_generic
|
||||
};
|
||||
let _ = add_allow_ace(p, psid);
|
||||
}
|
||||
for p in &deny {
|
||||
let _ = add_deny_write_ace(p, psid_generic);
|
||||
}
|
||||
allow_null_device(psid_generic);
|
||||
allow_null_device(psid_workspace);
|
||||
let _ = protect_workspace_codex_dir(¤t_dir, psid_workspace);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_apply_network_block;
|
||||
@@ -570,4 +618,14 @@ mod stub {
|
||||
) -> Result<()> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
|
||||
pub fn run_windows_sandbox_legacy_preflight(
|
||||
_sandbox_policy: &SandboxPolicy,
|
||||
_sandbox_policy_cwd: &Path,
|
||||
_codex_home: &Path,
|
||||
_cwd: &Path,
|
||||
_env_map: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
bail!("Windows sandbox is only available on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user