Constrain Windows sandbox requirements (#23766)

# Why

Managed requirements can already constrain sandbox policy choices, but
Windows sandbox implementation selection was still resolved
independently from those requirements. That left the TUI able to
continue through the unelevated fallback even when an organization wants
to require the elevated Windows sandbox implementation.

# What

- Add `[windows].allowed_sandbox_implementations` requirements support
for the Windows `elevated` and `unelevated` implementations.
- Apply that allowlist during core config resolution so disallowed
configured or feature-selected Windows sandbox implementations fall back
to an allowed implementation with the existing requirements warning
path.
- Reuse the existing TUI Windows setup prompts to block disallowed
unelevated continuation, keep required elevated setup in front of the
user, and refuse to persist a TUI-selected Windows sandbox mode that
requirements disallow.

# Semantics

| Allowed | Selected | Effective |
| --- | --- | --- |
| `["elevated"]` | `unelevated` / unset | `elevated` |
| `["unelevated"]` | `elevated` / unset | `unelevated` |
| `["elevated", "unelevated"]` | `elevated` | `elevated` |
| `["elevated", "unelevated"]` | `unelevated` | `unelevated` |
| `["elevated", "unelevated"]` | unset | `elevated` |

Availability is handled by interactive setup surfaces after allowlist
resolution. If the effective elevated implementation is not ready,
elevated-only requirements block on setup. When unelevated is also
allowed, the UI may offer the existing unelevated fallback.

## TUI Screens

If elevated setup is not already complete:
```
  Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control
  network access.
  Learn more <https://developers.openai.com/codex/windows>

› 1. Set up default sandbox (requires Administrator permissions)
  2. Quit
```

If admin setup fails under `["elevated"]`:
```
  Couldn't set up your sandbox with Administrator permissions

  Your organization requires the default sandbox before Codex can continue.
  Learn more <https://developers.openai.com/codex/windows>

› 1. Try setting up admin sandbox again
  2. Quit
```

# Next Steps


- extend the requirements/readout surface, such as
`configRequirements/read`, so clients can inspect the loaded
`[windows].allowed_sandbox_implementations` requirement instead of
inferring it from Windows setup state
- consider extending `windowsSandbox/readiness` as well
- update the App startup guide, setup flow, and banner surfaces so an
elevated-only requirement omits any continue-unelevated escape hatch and
blocks startup until a permitted implementation is ready;
- preserve the existing unelevated fallback path when requirements allow
it, including the `["unelevated"]` case where elevated is disallowed
This commit is contained in:
Abhinav
2026-05-29 16:31:33 -07:00
committed by GitHub
Unverified
parent 8e5f561697
commit a5a94ee5a7
25 changed files with 821 additions and 143 deletions
@@ -7809,6 +7809,15 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/v2/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
@@ -4178,6 +4178,15 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
@@ -121,6 +121,15 @@
"null"
]
},
"allowedWindowsSandboxImplementations": {
"items": {
"$ref": "#/definitions/WindowsSandboxSetupMode"
},
"type": [
"array",
"null"
]
},
"computerUse": {
"anyOf": [
{
@@ -485,6 +494,13 @@
"live"
],
"type": "string"
},
"WindowsSandboxSetupMode": {
"enum": [
"elevated",
"unelevated"
],
"type": "string"
}
},
"properties": {
@@ -6,5 +6,6 @@ import type { AskForApproval } from "./AskForApproval";
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
@@ -1,6 +1,7 @@
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::SandboxMode;
use super::WindowsSandboxSetupMode;
use super::shared::default_enabled;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
@@ -358,6 +359,7 @@ pub struct ConfigRequirements {
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub allowed_windows_sandbox_implementations: Option<Vec<WindowsSandboxSetupMode>>,
pub allowed_permissions: Option<Vec<String>>,
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
pub allow_managed_hooks_only: Option<bool>,
@@ -1670,6 +1670,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
}]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
allowed_windows_sandbox_implementations: None,
allowed_permissions: None,
allowed_web_search_modes: None,
allow_managed_hooks_only: None,
@@ -30,6 +30,7 @@ use codex_app_server_protocol::NetworkRequirements;
use codex_app_server_protocol::NetworkUnixSocketPermission;
use codex_app_server_protocol::SandboxMode;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_chatgpt::connectors;
use codex_config::ConfigRequirementsToml;
use codex_config::HookEventsToml;
@@ -420,6 +421,23 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.filter_map(map_sandbox_mode_requirement_to_api)
.collect()
}),
allowed_windows_sandbox_implementations: requirements.windows.and_then(|windows| {
windows
.allowed_sandbox_implementations
.map(|implementations| {
implementations
.into_iter()
.map(|implementation| match implementation {
codex_config::types::WindowsSandboxModeToml::Elevated => {
WindowsSandboxSetupMode::Elevated
}
codex_config::types::WindowsSandboxModeToml::Unelevated => {
WindowsSandboxSetupMode::Unelevated
}
})
.collect()
})
}),
allowed_permissions: requirements.allowed_permissions,
allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| {
let mut normalized = modes
@@ -634,8 +652,10 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
#[cfg(test)]
mod tests {
use super::map_requirements_toml_to_api;
use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_config::ComputerUseRequirementsToml;
use codex_config::ConfigRequirementsToml;
use codex_config::WindowsRequirementsToml;
use pretty_assertions::assert_eq;
#[test]
@@ -687,4 +707,25 @@ mod tests {
Some(false)
);
}
#[test]
fn requirements_api_includes_allowed_windows_sandbox_implementations() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
windows: Some(WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![
codex_config::types::WindowsSandboxModeToml::Elevated,
codex_config::types::WindowsSandboxModeToml::Unelevated,
]),
}),
..ConfigRequirementsToml::default()
});
assert_eq!(
mapped.allowed_windows_sandbox_implementations,
Some(vec![
WindowsSandboxSetupMode::Elevated,
WindowsSandboxSetupMode::Unelevated,
])
);
}
}
@@ -41,6 +41,29 @@ impl WindowsSandboxRequestProcessor {
request_id: &ConnectionRequestId,
params: WindowsSandboxSetupStartParams,
) -> Result<(), JSONRPCErrorError> {
// Validate requirements before acknowledging setup so callers do not get a
// `started` response for a Windows sandbox mode that cannot be persisted.
let command_cwd = params
.cwd
.map(PathBuf::from)
.unwrap_or_else(|| self.config.cwd.to_path_buf());
let config = self
.config_manager
.load_for_cwd(
/*request_overrides*/ None,
ConfigOverrides {
cwd: Some(command_cwd.clone()),
..Default::default()
},
Some(command_cwd.clone()),
)
.await
.map_err(|err| config_load_error(&err))?;
let setup_mode = resolve_allowed_windows_sandbox_setup_mode(
config.config_layer_stack.requirements(),
params.mode,
)?;
self.outgoing
.send_response(
request_id.clone(),
@@ -48,46 +71,22 @@ impl WindowsSandboxRequestProcessor {
)
.await;
let mode = match params.mode {
WindowsSandboxSetupMode::Elevated => CoreWindowsSandboxSetupMode::Elevated,
WindowsSandboxSetupMode::Unelevated => CoreWindowsSandboxSetupMode::Unelevated,
};
let config = Arc::clone(&self.config);
let config_manager = self.config_manager.clone();
let command_cwd = params
.cwd
.map(PathBuf::from)
.unwrap_or_else(|| config.cwd.to_path_buf());
let outgoing = Arc::clone(&self.outgoing);
let connection_id = request_id.connection_id;
tokio::spawn(async move {
let derived_config = config_manager
.load_for_cwd(
/*request_overrides*/ None,
ConfigOverrides {
cwd: Some(command_cwd.clone()),
..Default::default()
},
Some(command_cwd.clone()),
)
.await;
let setup_result = match derived_config {
Ok(config) => {
let setup_request = WindowsSandboxSetupRequest {
mode,
permission_profile: config.permissions.effective_permission_profile(),
workspace_roots: config.effective_workspace_roots(),
command_cwd,
env_map: std::env::vars().collect(),
codex_home: config.codex_home.to_path_buf(),
};
codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await
}
Err(err) => Err(err.into()),
let setup_request = WindowsSandboxSetupRequest {
mode: setup_mode,
permission_profile: config.permissions.effective_permission_profile(),
workspace_roots: config.effective_workspace_roots(),
command_cwd,
env_map: std::env::vars().collect(),
codex_home: config.codex_home.to_path_buf(),
};
let setup_result =
codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await;
let notification = WindowsSandboxSetupCompletedNotification {
mode: match mode {
mode: match setup_mode {
CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxSetupMode::Elevated,
CoreWindowsSandboxSetupMode::Unelevated => WindowsSandboxSetupMode::Unelevated,
},
@@ -105,6 +104,28 @@ impl WindowsSandboxRequestProcessor {
}
}
/// Resolves the requested API mode after checking that managed requirements allow it.
fn resolve_allowed_windows_sandbox_setup_mode(
requirements: &codex_config::ConfigRequirements,
requested_mode: WindowsSandboxSetupMode,
) -> Result<CoreWindowsSandboxSetupMode, JSONRPCErrorError> {
let (setup_mode, config_mode) = match requested_mode {
WindowsSandboxSetupMode::Elevated => (
CoreWindowsSandboxSetupMode::Elevated,
codex_config::types::WindowsSandboxModeToml::Elevated,
),
WindowsSandboxSetupMode::Unelevated => (
CoreWindowsSandboxSetupMode::Unelevated,
codex_config::types::WindowsSandboxModeToml::Unelevated,
),
};
requirements
.windows_sandbox_mode
.can_set(&Some(config_mode))
.map_err(|err| invalid_request(format!("invalid Windows sandbox setup mode: {err}")))?;
Ok(setup_mode)
}
fn determine_windows_sandbox_readiness(config: &Config) -> WindowsSandboxReadinessResponse {
if !cfg!(windows) {
return WindowsSandboxReadinessResponse {
@@ -140,6 +161,34 @@ fn determine_windows_sandbox_readiness_from_state(
#[cfg(test)]
mod tests {
use super::*;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use codex_config::ConfigRequirements;
use codex_config::Constrained;
use codex_config::ConstrainedWithSource;
use codex_config::types::WindowsSandboxModeToml;
#[test]
fn resolve_allowed_windows_sandbox_setup_mode_rejects_disallowed_mode() {
let requirements = ConfigRequirements {
windows_sandbox_mode: ConstrainedWithSource::new(
Constrained::allow_only(Some(WindowsSandboxModeToml::Elevated)),
/*source*/ None,
),
..Default::default()
};
let err = resolve_allowed_windows_sandbox_setup_mode(
&requirements,
WindowsSandboxSetupMode::Unelevated,
)
.expect_err("unelevated setup should be rejected");
assert_eq!(err.code, INVALID_REQUEST_ERROR_CODE);
assert!(
err.message.contains("invalid Windows sandbox setup mode"),
"{err:?}"
);
}
#[test]
fn determine_windows_sandbox_readiness_reports_not_configured_when_disabled() {
+16
View File
@@ -1222,6 +1222,7 @@ mod tests {
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1308,6 +1309,7 @@ mod tests {
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1345,6 +1347,7 @@ mod tests {
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1399,6 +1402,7 @@ mod tests {
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1582,6 +1586,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1666,6 +1671,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1748,6 +1754,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -1958,6 +1965,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2002,6 +2010,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2066,6 +2075,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2126,6 +2136,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2188,6 +2199,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2251,6 +2263,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2318,6 +2331,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2411,6 +2425,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
@@ -2450,6 +2465,7 @@ command = "sample-mcp"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
+138
View File
@@ -20,6 +20,7 @@ use crate::ConstraintError;
use crate::ManagedHooksRequirementsToml;
use crate::mcp_types::AppToolApproval;
use crate::permissions_toml::PermissionProfileToml;
use crate::types::WindowsSandboxModeToml;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequirementSource {
@@ -87,6 +88,7 @@ pub struct ConfigRequirements {
pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
pub windows_sandbox_mode: ConstrainedWithSource<Option<WindowsSandboxModeToml>>,
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
pub allow_managed_hooks_only: Option<Sourced<bool>>,
pub allow_appshots: Option<Sourced<bool>>,
@@ -120,6 +122,10 @@ impl Default for ConfigRequirements {
Constrained::allow_any(PermissionProfile::read_only()),
/*source*/ None,
),
windows_sandbox_mode: ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
web_search_mode: ConstrainedWithSource::new(
Constrained::allow_any(WebSearchMode::Cached),
/*source*/ None,
@@ -648,6 +654,17 @@ impl ComputerUseRequirementsToml {
}
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct WindowsRequirementsToml {
pub allowed_sandbox_implementations: Option<Vec<WindowsSandboxModeToml>>,
}
impl WindowsRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_sandbox_implementations.is_none()
}
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct FeatureRequirementsToml {
#[serde(flatten)]
@@ -754,6 +771,7 @@ pub struct ConfigRequirementsToml {
pub allow_managed_hooks_only: Option<bool>,
pub allow_appshots: Option<bool>,
pub computer_use: Option<ComputerUseRequirementsToml>,
pub windows: Option<WindowsRequirementsToml>,
#[serde(rename = "features", alias = "feature_requirements")]
pub feature_requirements: Option<FeatureRequirementsToml>,
pub hooks: Option<ManagedHooksRequirementsToml>,
@@ -806,6 +824,7 @@ pub struct ConfigRequirementsWithSources {
pub allow_managed_hooks_only: Option<Sourced<bool>>,
pub allow_appshots: Option<Sourced<bool>>,
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
pub windows: Option<Sourced<WindowsRequirementsToml>>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
@@ -846,6 +865,7 @@ impl ConfigRequirementsWithSources {
allow_managed_hooks_only: _,
allow_appshots: _,
computer_use: _,
windows: _,
feature_requirements: _,
hooks: _,
mcp_servers: _,
@@ -879,6 +899,7 @@ impl ConfigRequirementsWithSources {
allow_managed_hooks_only,
allow_appshots,
computer_use,
windows,
feature_requirements,
hooks,
mcp_servers,
@@ -910,6 +931,7 @@ impl ConfigRequirementsWithSources {
allow_managed_hooks_only,
allow_appshots,
computer_use,
windows,
feature_requirements,
hooks,
mcp_servers,
@@ -931,6 +953,7 @@ impl ConfigRequirementsWithSources {
allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value),
allow_appshots: allow_appshots.map(|sourced| sourced.value),
computer_use: computer_use.map(|sourced| sourced.value),
windows: windows.map(|sourced| sourced.value),
feature_requirements: feature_requirements.map(|sourced| sourced.value),
hooks: hooks.map(|sourced| sourced.value),
mcp_servers: mcp_servers.map(|sourced| sourced.value),
@@ -1021,6 +1044,10 @@ impl ConfigRequirementsToml {
.computer_use
.as_ref()
.is_none_or(ComputerUseRequirementsToml::is_empty)
&& self
.windows
.as_ref()
.is_none_or(WindowsRequirementsToml::is_empty)
&& self
.feature_requirements
.as_ref()
@@ -1065,6 +1092,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
allow_managed_hooks_only,
allow_appshots,
computer_use,
windows,
feature_requirements,
hooks,
mcp_servers,
@@ -1174,6 +1202,44 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
/*source*/ None,
),
};
let windows_sandbox_mode = match windows {
Some(Sourced {
value:
WindowsRequirementsToml {
allowed_sandbox_implementations: Some(implementations),
},
source: requirement_source,
}) => {
if implementations.is_empty() {
return Err(ConstraintError::empty_field(
"windows.allowed_sandbox_implementations",
));
}
// Prefer elevated when both Windows sandbox implementations are allowed.
let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) {
WindowsSandboxModeToml::Elevated
} else {
WindowsSandboxModeToml::Unelevated
};
let requirement_source_for_error = requirement_source.clone();
let constrained =
Constrained::new(Some(initial_value), move |candidate| match candidate {
Some(candidate) if implementations.contains(candidate) => Ok(()),
_ => Err(ConstraintError::InvalidValue {
field_name: "windows.sandbox",
candidate: format!("{candidate:?}"),
allowed: format!("{implementations:?}"),
requirement_source: requirement_source_for_error.clone(),
}),
})?;
ConstrainedWithSource::new(constrained, Some(requirement_source))
}
Some(_) | None => ConstrainedWithSource::new(
Constrained::allow_any(/*initial_value*/ None),
/*source*/ None,
),
};
let exec_policy = match rules {
Some(Sourced { value, source }) => {
let policy = value.to_requirements_policy().map_err(|err| {
@@ -1299,6 +1365,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
approval_policy,
approvals_reviewer,
permission_profile,
windows_sandbox_mode,
web_search_mode,
allow_managed_hooks_only,
allow_appshots,
@@ -1374,6 +1441,7 @@ mod tests {
allow_managed_hooks_only,
allow_appshots,
computer_use,
windows,
feature_requirements,
hooks,
mcp_servers,
@@ -1401,6 +1469,7 @@ mod tests {
allow_appshots: allow_appshots
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)),
windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feature_requirements: feature_requirements
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
@@ -1581,6 +1650,7 @@ mod tests {
allow_managed_hooks_only: Some(true),
allow_appshots: Some(false),
computer_use: Some(computer_use.clone()),
windows: None,
feature_requirements: Some(feature_requirements.clone()),
hooks: None,
mcp_servers: None,
@@ -1621,6 +1691,7 @@ mod tests {
)),
allow_appshots: Some(Sourced::new(/*value*/ false, enforce_source.clone(),)),
computer_use: Some(Sourced::new(computer_use, enforce_source.clone())),
windows: None,
feature_requirements: Some(Sourced::new(
feature_requirements,
enforce_source.clone(),
@@ -1667,6 +1738,7 @@ mod tests {
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
@@ -1718,6 +1790,7 @@ mod tests {
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
@@ -2357,6 +2430,71 @@ allowed_approvals_reviewers = ["user"]
Ok(())
}
#[test]
fn deserialize_allowed_windows_sandbox_implementations() -> Result<()> {
let toml_str = r#"
[windows]
allowed_sandbox_implementations = ["elevated"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
assert_eq!(
requirements.windows_sandbox_mode.value(),
Some(WindowsSandboxModeToml::Elevated)
);
assert!(
requirements
.windows_sandbox_mode
.can_set(&Some(WindowsSandboxModeToml::Elevated))
.is_ok()
);
assert!(
requirements
.windows_sandbox_mode
.can_set(&Some(WindowsSandboxModeToml::Unelevated))
.is_err()
);
assert!(requirements.windows_sandbox_mode.can_set(&None).is_err());
Ok(())
}
#[test]
fn empty_allowed_windows_sandbox_implementations_is_rejected() -> Result<()> {
let toml_str = r#"
[windows]
allowed_sandbox_implementations = []
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
assert_eq!(
ConfigRequirements::try_from(with_unknown_source(config)),
Err(ConstraintError::EmptyField {
field_name: "windows.allowed_sandbox_implementations".to_string(),
})
);
Ok(())
}
#[test]
fn allowed_windows_sandbox_implementations_prefer_elevated_fallback() -> Result<()> {
let toml_str = r#"
[windows]
allowed_sandbox_implementations = ["unelevated", "elevated"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
assert_eq!(
requirements.windows_sandbox_mode.value(),
Some(WindowsSandboxModeToml::Elevated)
);
Ok(())
}
#[test]
fn deserialize_legacy_allowed_approvals_reviewer() -> Result<()> {
let toml_str = r#"
+1
View File
@@ -62,6 +62,7 @@ pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use config_requirements::WindowsRequirementsToml;
pub use config_requirements::sandbox_mode_requirement_for_permission_profile;
pub use constraint::Constrained;
pub use constraint::ConstraintError;
@@ -1103,6 +1103,7 @@ allowed_approval_policies = ["on-request"]
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
@@ -1164,6 +1165,7 @@ allowed_approval_policies = ["on-request"]
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
@@ -1374,6 +1376,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
+43
View File
@@ -8195,6 +8195,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
@@ -8918,6 +8919,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
feature_requirements: None,
hooks: None,
mcp_servers: None,
@@ -8945,6 +8947,47 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
Ok(())
}
#[tokio::test]
async fn windows_sandbox_mode_falls_back_when_disallowed_by_requirements() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"[windows]
sandbox = "unelevated"
"#,
)?;
let requirements = codex_config::ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![
codex_config::types::WindowsSandboxModeToml::Elevated,
]),
}),
..Default::default()
};
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_requirements(CloudRequirementsLoader::new(async move {
Ok(Some(requirements))
}))
.build()
.await?;
assert_eq!(
config.permissions.windows_sandbox_mode,
Some(codex_config::types::WindowsSandboxModeToml::Elevated)
);
assert!(
config.startup_warnings.iter().any(|warning| warning
.contains("Configured value for `windows.sandbox` is disallowed by requirements")),
"{:?}",
config.startup_warnings
);
Ok(())
}
#[tokio::test]
async fn danger_full_access_with_never_is_rejected_when_requirements_force_read_only()
-> std::io::Result<()> {
+25 -3
View File
@@ -2457,6 +2457,7 @@ impl Config {
approval_policy: mut constrained_approval_policy,
approvals_reviewer: mut constrained_approvals_reviewer,
permission_profile: mut constrained_permission_profile,
windows_sandbox_mode: mut constrained_windows_sandbox_mode,
web_search_mode: mut constrained_web_search_mode,
allow_managed_hooks_only: _,
allow_appshots: _,
@@ -2568,7 +2569,28 @@ impl Config {
&mut startup_warnings,
)?;
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg);
let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg);
// Keep the configured mode separate so a requirement-constrained mode
// does not look like it was explicitly selected in config.
let selected_windows_sandbox_mode = configured_windows_sandbox_mode.or_else(|| {
match WindowsSandboxLevel::from_features(&features) {
WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated),
WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated),
WindowsSandboxLevel::Disabled => None,
}
});
apply_requirement_constrained_value(
"windows.sandbox",
selected_windows_sandbox_mode,
&mut constrained_windows_sandbox_mode,
&mut startup_warnings,
)?;
let effective_windows_sandbox_mode = *constrained_windows_sandbox_mode.get();
let windows_sandbox_mode = if constrained_windows_sandbox_mode.source.is_some() {
effective_windows_sandbox_mode
} else {
configured_windows_sandbox_mode
};
let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg);
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({
use std::env;
@@ -2626,10 +2648,10 @@ impl Config {
));
}
let windows_sandbox_level = match windows_sandbox_mode {
let windows_sandbox_level = match effective_windows_sandbox_mode {
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
None => WindowsSandboxLevel::from_features(&features),
None => WindowsSandboxLevel::Disabled,
};
let memories_config: MemoriesConfig = cfg.memories.clone().unwrap_or_default().into();
let memories_root = memory_root(&codex_home);
+52 -5
View File
@@ -5,6 +5,8 @@
use super::resize_reflow::trailing_run_start;
use super::*;
#[cfg(target_os = "windows")]
use codex_config::types::WindowsSandboxModeToml;
const SHUTDOWN_FIRST_EXIT_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 2);
@@ -867,6 +869,19 @@ impl App {
preset,
profile_selection,
} => {
#[cfg(any(target_os = "windows", test))]
if !self.chat_widget.windows_sandbox_mode_allowed(
codex_config::types::WindowsSandboxModeToml::Elevated,
) {
tracing::warn!(
"refusing to set up elevated Windows sandbox mode disallowed by requirements"
);
self.chat_widget.add_info_message(
"That Windows sandbox option is disallowed by requirements.".to_string(),
/*hint*/ None,
);
return Ok(AppRunControl::Continue);
}
#[cfg(target_os = "windows")]
{
let setup_permissions = match self
@@ -977,6 +992,19 @@ impl App {
preset,
profile_selection,
} => {
#[cfg(any(target_os = "windows", test))]
if !self.chat_widget.windows_sandbox_mode_allowed(
codex_config::types::WindowsSandboxModeToml::Unelevated,
) {
tracing::warn!(
"refusing to set up unelevated Windows sandbox mode disallowed by requirements"
);
self.chat_widget.add_info_message(
"That Windows sandbox option is disallowed by requirements.".to_string(),
/*hint*/ None,
);
return Ok(AppRunControl::Continue);
}
#[cfg(target_os = "windows")]
{
let setup_permissions = match self
@@ -1109,7 +1137,23 @@ impl App {
&[("result", "success")],
);
}
let elevated_enabled = matches!(mode, WindowsSandboxEnableMode::Elevated);
let selected_mode = match mode {
WindowsSandboxEnableMode::Elevated => WindowsSandboxModeToml::Elevated,
WindowsSandboxEnableMode::Legacy => WindowsSandboxModeToml::Unelevated,
};
let elevated_enabled = selected_mode == WindowsSandboxModeToml::Elevated;
if !self.chat_widget.windows_sandbox_mode_allowed(selected_mode) {
tracing::warn!(
?selected_mode,
"refusing to persist Windows sandbox mode disallowed by requirements"
);
self.chat_widget.add_info_message(
"That Windows sandbox option is disallowed by requirements."
.to_string(),
/*hint*/ None,
);
return Ok(AppRunControl::Continue);
}
let edits =
crate::config_update::build_windows_sandbox_mode_edits(elevated_enabled);
match crate::config_update::write_config_batch(
@@ -1184,8 +1228,9 @@ impl App {
/*personality*/ None,
),
));
self.apply_permission_profile_selection(selection).await;
let _ = mode;
if self.apply_permission_profile_selection(selection).await {
self.chat_widget.submit_initial_user_message_if_pending();
}
self.chat_widget.add_plain_history_lines(vec![
Line::from(vec!["".dim(), "Sandbox ready".into()]),
Line::from(vec![
@@ -1219,7 +1264,6 @@ impl App {
.send(AppEvent::UpdateActivePermissionProfile(
preset.active_permission_profile.clone(),
));
let _ = mode;
self.chat_widget.add_plain_history_lines(vec![
Line::from(vec!["".dim(), "Sandbox ready".into()]),
Line::from(vec![
@@ -1462,6 +1506,7 @@ impl App {
Some(RuntimePermissionProfileOverride::from_config(&self.config));
self.sync_active_thread_permission_settings_to_cached_session()
.await;
self.chat_widget.submit_initial_user_message_if_pending();
// If a managed filesystem sandbox is active, run the Windows
// world-writable scan.
@@ -1498,7 +1543,9 @@ impl App {
}
}
AppEvent::SelectPermissionProfile(selection) => {
self.apply_permission_profile_selection(selection).await;
if self.apply_permission_profile_selection(selection).await {
self.chat_widget.submit_initial_user_message_if_pending();
}
}
AppEvent::UpdateApprovalsReviewer(policy) => {
self.config.approvals_reviewer = policy;
+3 -3
View File
@@ -63,7 +63,7 @@ use crate::legacy_core::config::Config;
use crate::legacy_core::config::Constrained;
use crate::legacy_core::config::ConstraintResult;
use crate::legacy_core::config::PermissionProfileSnapshot;
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", test))]
use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt;
use crate::mention_codec::LinkedMention;
use crate::mention_codec::encode_history_mentions;
@@ -153,7 +153,7 @@ 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")]
#[cfg(any(target_os = "windows", test))]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
@@ -261,7 +261,7 @@ use crate::app_event::AppEvent;
use crate::app_event::ExitMode;
use crate::app_event::PermissionProfileSelection;
use crate::app_event::RateLimitRefreshOrigin;
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", test))]
use crate::app_event::WindowsSandboxEnableMode;
use crate::app_event_sender::AppEventSender;
use crate::auto_review_denials;
@@ -8,6 +8,13 @@ impl ChatWidget {
}
pub(crate) fn submit_initial_user_message_if_pending(&mut self) {
if self.suppress_initial_user_message_submit {
return;
}
#[cfg(any(target_os = "windows", test))]
if self.elevated_windows_sandbox_setup_required() {
return;
}
if let Some(user_message) = self.initial_user_message.take() {
self.submit_user_message(user_message);
}
+1 -7
View File
@@ -135,13 +135,7 @@ impl ChatWidget {
if self.connectors_enabled() {
self.prefetch_connectors();
}
if let Some(user_message) = self.initial_user_message.take() {
if self.suppress_initial_user_message_submit {
self.initial_user_message = Some(user_message);
} else {
self.submit_user_message(user_message);
}
}
self.submit_initial_user_message_if_pending();
if display == SessionConfiguredDisplay::Normal
&& let Some(forked_from_id) = forked_from_id
{
@@ -0,0 +1,12 @@
---
source: tui/src/chatwidget/tests/permissions.rs
expression: "render_bottom_popup(&chat, 120)"
---
Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control
network access.
Learn more <https://developers.openai.com/codex/windows>
1. Set up default sandbox (requires Administrator permissions)
2. Quit
Press enter to confirm or esc to go back
@@ -0,0 +1,13 @@
---
source: tui/src/chatwidget/tests/permissions.rs
expression: popup
---
Couldn't set up your sandbox with Administrator permissions
Your organization requires the default sandbox before Codex can continue.
Learn more <https://developers.openai.com/codex/windows>
1. Try setting up admin sandbox again
2. Quit
Press enter to confirm or esc to go back
-1
View File
@@ -123,7 +123,6 @@ pub(super) use codex_config::ConfigLayerStack;
pub(super) use codex_config::RequirementSource;
pub(super) use codex_config::types::ApprovalsReviewer;
pub(super) use codex_config::types::Notifications;
#[cfg(target_os = "windows")]
pub(super) use codex_config::types::WindowsSandboxModeToml;
pub(super) use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
pub(super) use codex_core_skills::model::SkillMetadata;
@@ -48,6 +48,25 @@ fn app_server_workspace_write_profile(extra_root: AbsolutePathBuf) -> Permission
}
}
fn windows_sandbox_requirements_stack(
allowed_sandbox_implementations: Vec<WindowsSandboxModeToml>,
) -> ConfigLayerStack {
let requirements_toml = codex_config::ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(allowed_sandbox_implementations),
}),
..Default::default()
};
let mut requirements_with_sources = codex_config::ConfigRequirementsWithSources::default();
requirements_with_sources
.merge_unset_fields(RequirementSource::Unknown, requirements_toml.clone());
let requirements = codex_config::ConfigRequirements::try_from(requirements_with_sources)
.expect("windows sandbox requirements");
ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
.expect("test config layer stack")
}
#[tokio::test]
async fn approvals_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -404,6 +423,148 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() {
);
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false);
chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false);
chat.config.config_layer_stack =
windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]);
chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true);
let popup = render_bottom_popup(&chat, /*width*/ 120);
assert!(
popup.contains("Your organization requires the default Codex agent sandbox"),
"expected required sandbox prompt copy: {popup}"
);
assert!(
!popup.contains("Use non-admin sandbox"),
"expected required sandbox prompt to hide non-admin fallback: {popup}"
);
}
#[tokio::test]
async fn windows_sandbox_required_enable_prompt_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.config.config_layer_stack =
windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]);
let preset = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
.expect("auto preset");
chat.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);
assert_chatwidget_snapshot!(
"windows_sandbox_required_enable_prompt",
render_bottom_popup(&chat, /*width*/ 120)
);
}
#[tokio::test]
async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevated_allowed() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated);
chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![
WindowsSandboxModeToml::Elevated,
WindowsSandboxModeToml::Unelevated,
]);
let preset = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
.expect("auto preset");
chat.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(matches!(
rx.try_recv(),
Ok(AppEvent::OpenWindowsSandboxEnablePrompt { .. })
));
}
#[tokio::test]
async fn required_windows_sandbox_setup_defers_configured_initial_prompt() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let initial_prompt = "fix required sandbox startup".to_string();
chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated);
chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![
WindowsSandboxModeToml::Elevated,
WindowsSandboxModeToml::Unelevated,
]);
chat.initial_user_message =
create_initial_user_message(Some(initial_prompt.clone()), Vec::new(), Vec::new());
chat.handle_thread_session(crate::session_state::ThreadSessionState {
thread_id: ThreadId::new(),
forked_from_id: None,
fork_parent_title: None,
thread_name: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
service_tier: None,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::workspace_write(),
active_permission_profile: None,
cwd: test_project_path().abs(),
runtime_workspace_roots: Vec::new(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
collaboration_mode: None,
personality: None,
message_history: None,
network_proxy: None,
rollout_path: Some(PathBuf::new()),
});
drain_insert_history(&mut rx);
assert!(chat.initial_user_message.is_some());
while let Ok(op) = op_rx.try_recv() {
assert!(
!matches!(op, Op::UserTurn { .. }),
"required sandbox setup should hold the configured initial prompt"
);
}
chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated));
chat.submit_initial_user_message_if_pending();
let Op::UserTurn { items, .. } = next_submit_op(&mut op_rx) else {
panic!("expected initial prompt submission after setup is no longer required");
};
assert_eq!(
items,
vec![UserInput::Text {
text: initial_prompt,
text_elements: Vec::new(),
}]
);
}
#[tokio::test]
async fn windows_sandbox_required_fallback_prompt_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.config.config_layer_stack =
windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]);
let preset = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
.expect("auto preset");
chat.open_windows_sandbox_fallback_prompt(preset, /*profile_selection*/ None);
let popup = render_bottom_popup(&chat, /*width*/ 120);
assert_chatwidget_snapshot!("windows_sandbox_required_fallback_prompt", popup);
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn startup_does_not_prompt_for_windows_sandbox_when_not_requested() {
@@ -3,6 +3,31 @@
use super::*;
impl ChatWidget {
#[cfg(any(target_os = "windows", test))]
pub(crate) fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool {
self.config
.config_layer_stack
.requirements()
.windows_sandbox_mode
.can_set(&Some(mode))
.is_ok()
}
#[cfg(any(target_os = "windows", test))]
pub(super) fn elevated_windows_sandbox_setup_required(&self) -> bool {
WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Elevated
&& self
.config
.config_layer_stack
.requirements()
.windows_sandbox_mode
.source
.is_some()
&& !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete(
self.config.codex_home.as_path(),
)
}
#[cfg(target_os = "windows")]
pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {
if self
@@ -193,7 +218,7 @@ impl ChatWidget {
) {
}
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", test))]
pub(crate) fn open_windows_sandbox_enable_prompt(
&mut self,
preset: ApprovalPreset,
@@ -255,11 +280,22 @@ impl ChatWidget {
&[],
);
let allow_unelevated =
self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated);
let setup_choice_is_required =
!allow_unelevated || self.elevated_windows_sandbox_setup_required();
let mut header = ColumnRenderable::new();
header.push(*Box::new(
Paragraph::new(vec![
line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more <https://developers.openai.com/codex/windows>"],
])
Paragraph::new(if allow_unelevated {
vec![
line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more <https://developers.openai.com/codex/windows>"],
]
} else {
vec![
line!["Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control network access."],
line!["Learn more <https://developers.openai.com/codex/windows>"],
]
})
.wrap(Wrap { trim: false }),
));
@@ -268,25 +304,27 @@ impl ChatWidget {
let legacy_preset = preset.clone();
let legacy_profile_selection = profile_selection.clone();
let quit_otel = self.session_telemetry.clone();
let items = vec![
SelectionItem {
name: "Set up default sandbox (requires Administrator permissions)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
accept_otel.counter(
"codex.windows_sandbox.elevated_prompt_accept",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
profile_selection: profile_selection.clone(),
});
})],
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
let retry_preset = preset.clone();
let retry_profile_selection = profile_selection.clone();
let mut items = vec![SelectionItem {
name: "Set up default sandbox (requires Administrator permissions)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
accept_otel.counter(
"codex.windows_sandbox.elevated_prompt_accept",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
profile_selection: profile_selection.clone(),
});
})],
dismiss_on_select: true,
..Default::default()
}];
if allow_unelevated {
items.push(SelectionItem {
name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
@@ -302,33 +340,41 @@ impl ChatWidget {
})],
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_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
..Default::default()
},
];
});
}
items.push(SelectionItem {
name: "Quit".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
quit_otel.counter(
"codex.windows_sandbox.elevated_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
..Default::default()
});
self.bottom_pane.show_selection_view(SelectionViewParams {
title: None,
footer_hint: Some(standard_popup_hint_line()),
items,
header: Box::new(header),
on_cancel: setup_choice_is_required.then(|| {
Box::new(move |tx: &AppEventSender| {
tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {
preset: retry_preset.clone(),
profile_selection: retry_profile_selection.clone(),
});
}) as _
}),
..Default::default()
});
}
#[cfg(not(target_os = "windows"))]
#[cfg(all(not(target_os = "windows"), not(test)))]
pub(crate) fn open_windows_sandbox_enable_prompt(
&mut self,
_preset: ApprovalPreset,
@@ -336,7 +382,7 @@ impl ChatWidget {
) {
}
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", test))]
pub(crate) fn open_windows_sandbox_fallback_prompt(
&mut self,
preset: ApprovalPreset,
@@ -344,14 +390,24 @@ impl ChatWidget {
) {
use ratatui_macros::line;
let allow_unelevated =
self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated);
let setup_choice_is_required =
!allow_unelevated || self.elevated_windows_sandbox_setup_required();
let mut lines = Vec::new();
lines.push(line![
"Couldn't set up your sandbox with Administrator permissions".bold()
]);
lines.push(line![""]);
lines.push(line![
"You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected."
]);
if allow_unelevated {
lines.push(line![
"You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected."
]);
} else {
lines.push(line![
"Your organization requires the default sandbox before Codex can continue."
]);
}
lines.push(line![
"Learn more <https://developers.openai.com/codex/windows>"
]);
@@ -361,32 +417,34 @@ impl ChatWidget {
let elevated_preset = preset.clone();
let legacy_preset = preset;
let retry_preset = elevated_preset.clone();
let retry_profile_selection = profile_selection.clone();
let elevated_profile_selection = profile_selection.clone();
let legacy_profile_selection = profile_selection;
let quit_otel = self.session_telemetry.clone();
let items = vec![
SelectionItem {
name: "Try setting up admin sandbox again".to_string(),
description: None,
actions: vec![Box::new({
let otel = self.session_telemetry.clone();
let preset = elevated_preset;
move |tx| {
otel.counter(
"codex.windows_sandbox.fallback_retry_elevated",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
profile_selection: elevated_profile_selection.clone(),
});
}
})],
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
let mut items = vec![SelectionItem {
name: "Try setting up admin sandbox again".to_string(),
description: None,
actions: vec![Box::new({
let otel = self.session_telemetry.clone();
let preset = elevated_preset;
move |tx| {
otel.counter(
"codex.windows_sandbox.fallback_retry_elevated",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
profile_selection: elevated_profile_selection.clone(),
});
}
})],
dismiss_on_select: true,
..Default::default()
}];
if allow_unelevated {
items.push(SelectionItem {
name: "Use Codex with non-admin sandbox".to_string(),
description: None,
actions: vec![Box::new({
@@ -406,33 +464,41 @@ impl ChatWidget {
})],
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.fallback_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
..Default::default()
},
];
});
}
items.push(SelectionItem {
name: "Quit".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
quit_otel.counter(
"codex.windows_sandbox.fallback_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
..Default::default()
});
self.bottom_pane.show_selection_view(SelectionViewParams {
title: None,
footer_hint: Some(standard_popup_hint_line()),
items,
header: Box::new(header),
on_cancel: setup_choice_is_required.then(|| {
Box::new(move |tx: &AppEventSender| {
tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {
preset: retry_preset.clone(),
profile_selection: retry_profile_selection.clone(),
});
}) as _
}),
..Default::default()
});
}
#[cfg(not(target_os = "windows"))]
#[cfg(all(not(target_os = "windows"), not(test)))]
pub(crate) fn open_windows_sandbox_fallback_prompt(
&mut self,
_preset: ApprovalPreset,
@@ -442,8 +508,11 @@ impl ChatWidget {
#[cfg(target_os = "windows")]
pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) {
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled
|| self.elevated_windows_sandbox_setup_required();
if show_now
&& WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled
&& setup_is_required
&& let Some(preset) = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
+2
View File
@@ -718,6 +718,7 @@ mod tests {
allow_managed_hooks_only: Some(true),
allow_appshots: Some(false),
computer_use: None,
windows: None,
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
feature_requirements: Some(FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
@@ -936,6 +937,7 @@ approval_policy = "never"
allow_managed_hooks_only: None,
allow_appshots: None,
computer_use: None,
windows: None,
guardian_policy_config: None,
feature_requirements: None,
hooks: None,
+27 -4
View File
@@ -11,6 +11,7 @@ use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;
use crate::legacy_core::config::resolve_oss_provider;
use crate::legacy_core::config::resolve_profile_v2_config_path;
use crate::legacy_core::format_exec_policy_error_with_source;
#[cfg(target_os = "windows")]
use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt;
use crate::session_resume::ResolveCwdOutcome;
use crate::session_resume::resolve_cwd_for_resume_or_fork;
@@ -51,6 +52,7 @@ use codex_login::enforce_login_restrictions;
use codex_protocol::ThreadId;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_rollout::StateDbHandle;
use codex_rollout::state_db;
@@ -1392,6 +1394,7 @@ async fn run_ratatui_app(
let should_show_trust_screen_flag =
!uses_remote_workspace && should_show_trust_screen(&initial_config);
#[cfg(target_os = "windows")]
let mut trust_decision_was_made = false;
let login_status = if initial_config.model_provider.requires_openai_auth {
let Some(app_server) = app_server.as_mut() else {
@@ -1437,7 +1440,10 @@ async fn run_ratatui_app(
exit_reason: ExitReason::UserRequested,
});
}
trust_decision_was_made = onboarding_result.directory_trust_persisted;
#[cfg(target_os = "windows")]
{
trust_decision_was_made = onboarding_result.directory_trust_persisted;
}
// 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.
@@ -1695,9 +1701,26 @@ async fn run_ratatui_app(
set_default_client_residency_requirement(config.enforce_residency.value());
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;
#[cfg(target_os = "windows")]
let windows_sandbox_level = WindowsSandboxLevel::from_config(&config);
#[cfg(target_os = "windows")]
let required_elevated_sandbox_needs_setup = windows_sandbox_level
== WindowsSandboxLevel::Elevated
&& config
.config_layer_stack
.requirements()
.windows_sandbox_mode
.source
.is_some()
&& !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete(
config.codex_home.as_path(),
);
#[cfg(target_os = "windows")]
let should_prompt_windows_sandbox_nux_at_startup = (trust_decision_was_made
&& windows_sandbox_level == WindowsSandboxLevel::Disabled)
|| required_elevated_sandbox_needs_setup;
#[cfg(not(target_os = "windows"))]
let should_prompt_windows_sandbox_nux_at_startup = false;
let Cli {
prompt,