feat: support allowed_sandbox_modes in requirements.toml (#8298)

This adds support for `allowed_sandbox_modes` in `requirements.toml` and
provides legacy support for constraining sandbox modes in
`managed_config.toml`. This is converted to `Constrained<SandboxPolicy>`
in `ConfigRequirements` and applied to `Config` such that constraints
are enforced throughout the harness.

Note that, because `managed_config.toml` is deprecated, we do not add
support for the new `external-sandbox` variant recently introduced in
https://github.com/openai/codex/pull/8290. As noted, that variant is not
supported in `config.toml` today, but can be configured programmatically
via app server.
This commit is contained in:
Michael Bolin
2025-12-19 13:09:20 -08:00
committed by GitHub
Unverified
parent ec3738b47e
commit dc61fc5f50
25 changed files with 345 additions and 96 deletions
@@ -1186,10 +1186,22 @@ impl CodexMessageProcessor {
arg0: None,
};
let effective_policy = params
.sandbox_policy
.map(|policy| policy.to_core())
.unwrap_or_else(|| self.config.sandbox_policy.clone());
let requested_policy = params.sandbox_policy.map(|policy| policy.to_core());
let effective_policy = match requested_policy {
Some(policy) => match self.config.sandbox_policy.can_set(&policy) {
Ok(()) => policy,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("invalid sandbox policy: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
},
None => self.config.sandbox_policy.get().clone(),
};
let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone();
let outgoing = self.outgoing.clone();
+3 -3
View File
@@ -140,7 +140,7 @@ async fn run_command_under_sandbox(
use codex_windows_sandbox::run_windows_sandbox_capture;
use codex_windows_sandbox::run_windows_sandbox_capture_elevated;
let policy_str = serde_json::to_string(&config.sandbox_policy)?;
let policy_str = serde_json::to_string(config.sandbox_policy.get())?;
let sandbox_cwd = sandbox_policy_cwd.clone();
let cwd_clone = cwd.clone();
@@ -216,7 +216,7 @@ async fn run_command_under_sandbox(
spawn_command_under_seatbelt(
command,
cwd,
&config.sandbox_policy,
config.sandbox_policy.get(),
sandbox_policy_cwd.as_path(),
stdio_policy,
env,
@@ -232,7 +232,7 @@ async fn run_command_under_sandbox(
codex_linux_sandbox_exe,
command,
cwd,
&config.sandbox_policy,
config.sandbox_policy.get(),
sandbox_policy_cwd.as_path(),
stdio_policy,
env,
+4 -1
View File
@@ -10,7 +10,10 @@ pub fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'sta
("model", model.to_string()),
("provider", config.model_provider_id.clone()),
("approval", config.approval_policy.value().to_string()),
("sandbox", summarize_sandbox_policy(&config.sandbox_policy)),
(
"sandbox",
summarize_sandbox_policy(config.sandbox_policy.get()),
),
];
if config.model_provider.wire_api == WireApi::Responses {
let reasoning_effort = config
+7 -7
View File
@@ -415,7 +415,7 @@ pub(crate) struct SessionConfiguration {
/// When to escalate for approval for execution
approval_policy: Constrained<AskForApproval>,
/// How to sandbox commands executed in the system
sandbox_policy: SandboxPolicy,
sandbox_policy: Constrained<SandboxPolicy>,
/// Working directory that should be treated as the *root* of the
/// session. All relative paths supplied by the model as well as the
@@ -451,7 +451,7 @@ impl SessionConfiguration {
next_configuration.approval_policy.set(approval_policy)?;
}
if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
next_configuration.sandbox_policy = sandbox_policy;
next_configuration.sandbox_policy.set(sandbox_policy)?;
}
if let Some(cwd) = updates.cwd.clone() {
next_configuration.cwd = cwd;
@@ -526,7 +526,7 @@ impl Session {
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration.user_instructions.clone(),
approval_policy: session_configuration.approval_policy.value(),
sandbox_policy: session_configuration.sandbox_policy.clone(),
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
shell_environment_policy: per_turn_config.shell_environment_policy.clone(),
tools_config,
ghost_snapshot: per_turn_config.ghost_snapshot.clone(),
@@ -643,7 +643,7 @@ impl Session {
config.model_context_window,
config.model_auto_compact_token_limit,
config.approval_policy.value(),
config.sandbox_policy.clone(),
config.sandbox_policy.get().clone(),
config.mcp_servers.keys().map(String::as_str).collect(),
config.active_profile.clone(),
);
@@ -693,7 +693,7 @@ impl Session {
model: session_configuration.model.clone(),
model_provider_id: config.model_provider_id.clone(),
approval_policy: session_configuration.approval_policy.value(),
sandbox_policy: session_configuration.sandbox_policy.clone(),
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
cwd: session_configuration.cwd.clone(),
reasoning_effort: session_configuration.model_reasoning_effort,
history_log_id,
@@ -710,7 +710,7 @@ impl Session {
// Construct sandbox_state before initialize() so it can be sent to each
// MCP server immediately after it becomes ready (avoiding blocking).
let sandbox_state = SandboxState {
sandbox_policy: session_configuration.sandbox_policy.clone(),
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: session_configuration.cwd.clone(),
};
@@ -891,7 +891,7 @@ impl Session {
if sandbox_policy_changed {
let sandbox_state = SandboxState {
sandbox_policy: per_turn_config.sandbox_policy.clone(),
sandbox_policy: per_turn_config.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: per_turn_config.cwd.clone(),
};
+25 -15
View File
@@ -113,7 +113,7 @@ pub struct Config {
/// Approval policy for executing commands.
pub approval_policy: Constrained<AskForApproval>,
pub sandbox_policy: SandboxPolicy,
pub sandbox_policy: Constrained<SandboxPolicy>,
/// True if the user passed in an override or set a value in config.toml
/// for either of approval_policy or sandbox_mode.
@@ -1235,11 +1235,15 @@ impl Config {
// Config.
let ConfigRequirements {
approval_policy: mut constrained_approval_policy,
sandbox_policy: mut constrained_sandbox_policy,
} = requirements;
constrained_approval_policy
.set(approval_policy)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
constrained_sandbox_policy
.set(sandbox_policy)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
let config = Self {
model,
@@ -1250,7 +1254,7 @@ impl Config {
model_provider,
cwd: resolved_cwd,
approval_policy: constrained_approval_policy,
sandbox_policy,
sandbox_policy: constrained_sandbox_policy,
did_user_set_custom_approval_policy_or_sandbox_mode,
forced_auto_mode_downgraded_on_windows,
shell_environment_policy,
@@ -1672,12 +1676,12 @@ trust_level = "trusted"
config.forced_auto_mode_downgraded_on_windows,
"expected workspace-write request to be downgraded on Windows"
);
match config.sandbox_policy {
SandboxPolicy::ReadOnly => {}
match config.sandbox_policy.get() {
&SandboxPolicy::ReadOnly => {}
other => panic!("expected read-only policy on Windows, got {other:?}"),
}
} else {
match config.sandbox_policy {
match config.sandbox_policy.get() {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
assert_eq!(
writable_roots
@@ -1809,8 +1813,8 @@ trust_level = "trusted"
)?;
assert!(matches!(
config.sandbox_policy,
SandboxPolicy::DangerFullAccess
config.sandbox_policy.get(),
&SandboxPolicy::DangerFullAccess
));
assert!(config.did_user_set_custom_approval_policy_or_sandbox_mode);
@@ -1846,11 +1850,14 @@ trust_level = "trusted"
)?;
if cfg!(target_os = "windows") {
assert!(matches!(config.sandbox_policy, SandboxPolicy::ReadOnly));
assert!(matches!(
config.sandbox_policy.get(),
SandboxPolicy::ReadOnly
));
assert!(config.forced_auto_mode_downgraded_on_windows);
} else {
assert!(matches!(
config.sandbox_policy,
config.sandbox_policy.get(),
SandboxPolicy::WorkspaceWrite { .. }
));
assert!(!config.forced_auto_mode_downgraded_on_windows);
@@ -3048,7 +3055,7 @@ model_verbosity = "high"
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::Never),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3123,7 +3130,7 @@ model_verbosity = "high"
model_provider_id: "openai-chat-completions".to_string(),
model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3213,7 +3220,7 @@ model_verbosity = "high"
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3289,7 +3296,7 @@ model_verbosity = "high"
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
sandbox_policy: SandboxPolicy::new_read_only_policy(),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
did_user_set_custom_approval_policy_or_sandbox_mode: true,
forced_auto_mode_downgraded_on_windows: false,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -3634,12 +3641,15 @@ trust_level = "untrusted"
// Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows)
if cfg!(target_os = "windows") {
assert!(
matches!(config.sandbox_policy, SandboxPolicy::ReadOnly),
matches!(config.sandbox_policy.get(), SandboxPolicy::ReadOnly),
"Expected ReadOnly on Windows"
);
} else {
assert!(
matches!(config.sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }),
matches!(
config.sandbox_policy.get(),
SandboxPolicy::WorkspaceWrite { .. }
),
"Expected WorkspaceWrite sandbox for untrusted project"
);
}
@@ -1,4 +1,6 @@
use codex_protocol::config_types::SandboxMode;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use serde::Deserialize;
use crate::config::Constrained;
@@ -9,12 +11,14 @@ use crate::config::ConstraintError;
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigRequirements {
pub approval_policy: Constrained<AskForApproval>,
pub sandbox_policy: Constrained<SandboxPolicy>,
}
impl Default for ConfigRequirements {
fn default() -> Self {
Self {
approval_policy: Constrained::allow_any_from_default(),
sandbox_policy: Constrained::allow_any(SandboxPolicy::ReadOnly),
}
}
}
@@ -23,6 +27,34 @@ impl Default for ConfigRequirements {
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsToml {
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
}
/// Currently, `external-sandbox` is not supported in config.toml, but it is
/// supported through programmatic use.
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum SandboxModeRequirement {
#[serde(rename = "read-only")]
ReadOnly,
#[serde(rename = "workspace-write")]
WorkspaceWrite,
#[serde(rename = "danger-full-access")]
DangerFullAccess,
#[serde(rename = "external-sandbox")]
ExternalSandbox,
}
impl From<SandboxMode> for SandboxModeRequirement {
fn from(mode: SandboxMode) -> Self {
match mode {
SandboxMode::ReadOnly => SandboxModeRequirement::ReadOnly,
SandboxMode::WorkspaceWrite => SandboxModeRequirement::WorkspaceWrite,
SandboxMode::DangerFullAccess => SandboxModeRequirement::DangerFullAccess,
}
}
}
impl ConfigRequirementsToml {
@@ -41,7 +73,7 @@ impl ConfigRequirementsToml {
};
}
fill_missing_take!(self, other, { allowed_approval_policies });
fill_missing_take!(self, other, { allowed_approval_policies, allowed_sandbox_modes });
}
}
@@ -49,12 +81,13 @@ impl TryFrom<ConfigRequirementsToml> for ConfigRequirements {
type Error = ConstraintError;
fn try_from(toml: ConfigRequirementsToml) -> Result<Self, Self::Error> {
let approval_policy: Constrained<AskForApproval> = match toml.allowed_approval_policies {
let ConfigRequirementsToml {
allowed_approval_policies,
allowed_sandbox_modes,
} = toml;
let approval_policy: Constrained<AskForApproval> = match allowed_approval_policies {
Some(policies) => {
let default_value = AskForApproval::default();
if policies.contains(&default_value) {
Constrained::allow_values(default_value, policies)?
} else if let Some(first) = policies.first() {
if let Some(first) = policies.first() {
Constrained::allow_values(*first, policies)?
} else {
return Err(ConstraintError::empty_field("allowed_approval_policies"));
@@ -62,7 +95,51 @@ impl TryFrom<ConfigRequirementsToml> for ConfigRequirements {
}
None => Constrained::allow_any_from_default(),
};
Ok(ConfigRequirements { approval_policy })
// TODO(gt): `ConfigRequirementsToml` should let the author specify the
// default `SandboxPolicy`? Should do this for `AskForApproval` too?
//
// Currently, we force ReadOnly as the default policy because two of
// the other variants (WorkspaceWrite, ExternalSandbox) require
// additional parameters. Ultimately, we should expand the config
// format to allow specifying those parameters.
let default_sandbox_policy = SandboxPolicy::ReadOnly;
let sandbox_policy: Constrained<SandboxPolicy> = match allowed_sandbox_modes {
Some(modes) => {
if !modes.contains(&SandboxModeRequirement::ReadOnly) {
return Err(ConstraintError::invalid_value(
"allowed_sandbox_modes",
"must include 'read-only' to allow any SandboxPolicy",
));
};
Constrained::new(default_sandbox_policy, move |candidate| {
let mode = match candidate {
SandboxPolicy::ReadOnly => SandboxModeRequirement::ReadOnly,
SandboxPolicy::WorkspaceWrite { .. } => {
SandboxModeRequirement::WorkspaceWrite
}
SandboxPolicy::DangerFullAccess => SandboxModeRequirement::DangerFullAccess,
SandboxPolicy::ExternalSandbox { .. } => {
SandboxModeRequirement::ExternalSandbox
}
};
if modes.contains(&mode) {
Ok(())
} else {
Err(ConstraintError::invalid_value(
format!("{candidate:?}"),
format!("{modes:?}"),
))
}
})?
}
None => Constrained::allow_any(default_sandbox_policy),
};
Ok(ConfigRequirements {
approval_policy,
sandbox_policy,
})
}
}
@@ -70,6 +147,8 @@ impl TryFrom<ConfigRequirementsToml> for ConfigRequirements {
mod tests {
use super::*;
use anyhow::Result;
use codex_protocol::protocol::NetworkAccess;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use toml::from_str;
@@ -104,4 +183,105 @@ mod tests {
);
Ok(())
}
#[test]
fn deserialize_allowed_approval_policies() -> Result<()> {
let toml_str = r#"
allowed_approval_policies = ["untrusted", "on-request"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements = ConfigRequirements::try_from(config)?;
assert_eq!(
requirements.approval_policy.value(),
AskForApproval::UnlessTrusted,
"currently, there is no way to specify the default value for approval policy in the toml, so it picks the first allowed value"
);
assert!(
requirements
.approval_policy
.can_set(&AskForApproval::UnlessTrusted)
.is_ok()
);
assert_eq!(
requirements
.approval_policy
.can_set(&AskForApproval::OnFailure),
Err(ConstraintError::InvalidValue {
candidate: "OnFailure".into(),
allowed: "[UnlessTrusted, OnRequest]".into(),
})
);
assert!(
requirements
.approval_policy
.can_set(&AskForApproval::OnRequest)
.is_ok()
);
assert_eq!(
requirements.approval_policy.can_set(&AskForApproval::Never),
Err(ConstraintError::InvalidValue {
candidate: "Never".into(),
allowed: "[UnlessTrusted, OnRequest]".into(),
})
);
assert!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::ReadOnly)
.is_ok()
);
Ok(())
}
#[test]
fn deserialize_allowed_sandbox_modes() -> Result<()> {
let toml_str = r#"
allowed_sandbox_modes = ["read-only", "workspace-write"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements = ConfigRequirements::try_from(config)?;
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
assert!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::ReadOnly)
.is_ok()
);
assert!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
.is_ok()
);
assert_eq!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::DangerFullAccess),
Err(ConstraintError::InvalidValue {
candidate: "DangerFullAccess".into(),
allowed: "[ReadOnly, WorkspaceWrite]".into(),
})
);
assert_eq!(
requirements
.sandbox_policy
.can_set(&SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Restricted,
}),
Err(ConstraintError::InvalidValue {
candidate: "ExternalSandbox { network_access: Restricted }".into(),
allowed: "[ReadOnly, WorkspaceWrite]".into(),
})
);
Ok(())
}
}
+9 -2
View File
@@ -14,6 +14,7 @@ use crate::config::CONFIG_TOML_FILE;
use crate::config_loader::config_requirements::ConfigRequirementsToml;
use crate::config_loader::layer_io::LoadedConfigLayers;
use codex_app_server_protocol::ConfigLayerSource;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::protocol::AskForApproval;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
@@ -238,17 +239,23 @@ async fn load_requirements_from_legacy_scheme(
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
struct LegacyManagedConfigToml {
approval_policy: Option<AskForApproval>,
sandbox_mode: Option<SandboxMode>,
}
impl From<LegacyManagedConfigToml> for ConfigRequirementsToml {
fn from(legacy: LegacyManagedConfigToml) -> Self {
let mut config_requirements_toml = ConfigRequirementsToml::default();
let LegacyManagedConfigToml { approval_policy } = legacy;
let LegacyManagedConfigToml {
approval_policy,
sandbox_mode,
} = legacy;
if let Some(approval_policy) = approval_policy {
config_requirements_toml.allowed_approval_policies = Some(vec![approval_policy]);
}
if let Some(sandbox_mode) = sandbox_mode {
config_requirements_toml.allowed_sandbox_modes = Some(vec![sandbox_mode.into()]);
}
config_requirements_toml
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ allowed_approval_policies = ["never", "on-request"]
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
assert_eq!(
config_requirements.approval_policy.value(),
AskForApproval::OnRequest
AskForApproval::Never
);
config_requirements
.approval_policy
+2 -2
View File
@@ -1464,7 +1464,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
let mut builder = test_codex().with_model(model).with_config(move |config| {
config.approval_policy = Constrained::allow_any(approval_policy);
config.sandbox_policy = sandbox_policy.clone();
config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
for feature in features {
config.features.enable(feature);
}
@@ -1570,7 +1570,7 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
let sandbox_policy_for_config = sandbox_policy.clone();
let mut builder = test_codex().with_config(move |config| {
config.approval_policy = Constrained::allow_any(approval_policy);
config.sandbox_policy = sandbox_policy_for_config;
config.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config);
});
let test = builder.build(&server).await?;
let allow_prefix_path = test.cwd.path().join("allow-prefix.txt");
+2 -2
View File
@@ -63,7 +63,7 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
// routes ExecApprovalRequest via the parent.
let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| {
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.sandbox_policy = SandboxPolicy::ReadOnly;
config.sandbox_policy = Constrained::allow_any(SandboxPolicy::ReadOnly);
});
let test = builder.build(&server).await.expect("build test codex");
@@ -140,7 +140,7 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() {
let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| {
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
// Use a restricted sandbox so patch approval is required
config.sandbox_policy = SandboxPolicy::ReadOnly;
config.sandbox_policy = Constrained::allow_any(SandboxPolicy::ReadOnly);
config.include_apply_patch_tool = true;
});
let test = builder.build(&server).await.expect("build test codex");
+1 -1
View File
@@ -935,7 +935,7 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() {
let TestCodex { codex, .. } = test_codex()
.with_config(|config| {
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.sandbox_policy = SandboxPolicy::DangerFullAccess;
config.sandbox_policy = Constrained::allow_any(SandboxPolicy::DangerFullAccess);
})
.build(&server)
.await
+2 -2
View File
@@ -605,7 +605,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
let default_cwd = config.cwd.clone();
let default_approval_policy = config.approval_policy.value();
let default_sandbox_policy = config.sandbox_policy.clone();
let default_sandbox_policy = config.sandbox_policy.get();
let default_model = session_configured.model;
let default_effort = config.model_reasoning_effort;
let default_summary = config.model_reasoning_summary;
@@ -695,7 +695,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
let default_cwd = config.cwd.clone();
let default_approval_policy = config.approval_policy.value();
let default_sandbox_policy = config.sandbox_policy.clone();
let default_sandbox_policy = config.sandbox_policy.get();
let default_model = session_configured.model;
let default_effort = config.model_reasoning_effort;
let default_summary = config.model_reasoning_summary;
+1 -1
View File
@@ -24,7 +24,7 @@ fn resume_history(
let turn_ctx = TurnContextItem {
cwd: config.cwd.clone(),
approval_policy: config.approval_policy.value(),
sandbox_policy: config.sandbox_policy.clone(),
sandbox_policy: config.sandbox_policy.get().clone(),
model: previous_model.to_string(),
effort: config.model_reasoning_effort,
summary: config.model_reasoning_summary,
+7 -2
View File
@@ -415,7 +415,10 @@ async fn shell_timeout_handles_background_grandchild_stdout() -> Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| {
config.sandbox_policy = SandboxPolicy::DangerFullAccess;
config
.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("set sandbox policy");
});
let test = builder.build(&server).await?;
@@ -508,7 +511,9 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex().with_config(|cfg| {
cfg.sandbox_policy = SandboxPolicy::DangerFullAccess;
cfg.sandbox_policy
.set(SandboxPolicy::DangerFullAccess)
.expect("set sandbox policy");
});
let test = builder.build(&server).await?;
+2 -2
View File
@@ -259,7 +259,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
let default_cwd = config.cwd.to_path_buf();
let default_approval_policy = config.approval_policy.value();
let default_sandbox_policy = config.sandbox_policy.clone();
let default_sandbox_policy = config.sandbox_policy.get();
let default_effort = config.model_reasoning_effort;
let default_summary = config.model_reasoning_summary;
@@ -411,7 +411,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
items,
cwd: default_cwd,
approval_policy: default_approval_policy,
sandbox_policy: default_sandbox_policy,
sandbox_policy: default_sandbox_policy.clone(),
model: default_model,
effort: default_effort,
summary: default_summary,
+17 -7
View File
@@ -453,7 +453,7 @@ impl App {
{
let should_check = codex_core::get_platform_sandbox().is_some()
&& matches!(
app.config.sandbox_policy,
app.config.sandbox_policy.get(),
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_core::protocol::SandboxPolicy::ReadOnly
)
@@ -467,7 +467,7 @@ impl App {
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let tx = app.app_event_tx.clone();
let logs_base_dir = app.config.codex_home.clone();
let sandbox_policy = app.config.sandbox_policy.clone();
let sandbox_policy = app.config.sandbox_policy.get().clone();
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx);
}
}
@@ -904,19 +904,29 @@ impl App {
AppEvent::UpdateSandboxPolicy(policy) => {
#[cfg(target_os = "windows")]
let policy_is_workspace_write_or_ro = matches!(
policy,
&policy,
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_core::protocol::SandboxPolicy::ReadOnly
);
self.config.sandbox_policy = policy.clone();
if let Err(err) = self.config.sandbox_policy.set(policy.clone()) {
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(true);
}
#[cfg(target_os = "windows")]
if !matches!(policy, codex_core::protocol::SandboxPolicy::ReadOnly)
if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some()
{
self.config.forced_auto_mode_downgraded_on_windows = false;
}
self.chat_widget.set_sandbox_policy(policy);
if let Err(err) = self.chat_widget.set_sandbox_policy(policy) {
tracing::warn!(%err, "failed to set sandbox policy on chat config");
self.chat_widget
.add_error_message(format!("Failed to set sandbox policy: {err}"));
return Ok(true);
}
// If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan.
#[cfg(target_os = "windows")]
@@ -936,7 +946,7 @@ impl App {
std::env::vars().collect();
let tx = self.app_event_tx.clone();
let logs_base_dir = self.config.codex_home.clone();
let sandbox_policy = self.config.sandbox_policy.clone();
let sandbox_policy = self.config.sandbox_policy.get().clone();
Self::spawn_world_writable_scan(
cwd,
env_map,
+10 -7
View File
@@ -8,6 +8,7 @@ use std::time::Duration;
use codex_app_server_protocol::AuthMode;
use codex_backend_client::Client as BackendClient;
use codex_core::config::Config;
use codex_core::config::ConstraintResult;
use codex_core::config::types::Notifications;
use codex_core::features::FEATURES;
use codex_core::features::Feature;
@@ -2725,12 +2726,12 @@ impl ChatWidget {
/// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy).
pub(crate) fn open_approvals_popup(&mut self) {
let current_approval = self.config.approval_policy.value();
let current_sandbox = self.config.sandbox_policy.clone();
let current_sandbox = self.config.sandbox_policy.get();
let mut items: Vec<SelectionItem> = Vec::new();
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
for preset in presets.into_iter() {
let is_current =
Self::preset_matches_current(current_approval, &current_sandbox, &preset);
Self::preset_matches_current(current_approval, current_sandbox, &preset);
let name = preset.label.to_string();
let description = Some(preset.description.to_string());
let disabled_reason = match self.config.approval_policy.can_set(&preset.approval) {
@@ -2879,7 +2880,7 @@ impl ChatWidget {
self.config.codex_home.as_path(),
cwd.as_path(),
&env_map,
&self.config.sandbox_policy,
self.config.sandbox_policy.get(),
Some(self.config.codex_home.as_path()),
) {
Ok(_) => None,
@@ -2978,7 +2979,7 @@ impl ChatWidget {
let mode_label = preset
.as_ref()
.map(|p| describe_policy(&p.sandbox))
.unwrap_or_else(|| describe_policy(&self.config.sandbox_policy));
.unwrap_or_else(|| describe_policy(self.config.sandbox_policy.get()));
let info_line = if failed_scan {
Line::from(vec![
"We couldn't complete the world-writable scan, so protections cannot be verified. "
@@ -3151,17 +3152,19 @@ impl ChatWidget {
}
/// Set the sandbox policy in the widget's config copy.
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) {
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> {
#[cfg(target_os = "windows")]
let should_clear_downgrade = !matches!(policy, SandboxPolicy::ReadOnly)
let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some();
self.config.sandbox_policy = policy;
self.config.sandbox_policy.set(policy)?;
#[cfg(target_os = "windows")]
if should_clear_downgrade {
self.config.forced_auto_mode_downgraded_on_windows = false;
}
Ok(())
}
pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) {
+1 -1
View File
@@ -217,7 +217,7 @@ pub async fn run_main(
let config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await;
if let Some(warning) = add_dir_warning_message(&cli.add_dir, &config.sandbox_policy) {
if let Some(warning) = add_dir_warning_message(&cli.add_dir, config.sandbox_policy.get()) {
#[allow(clippy::print_stderr)]
{
eprintln!("Error adding directories: {warning}");
+1 -1
View File
@@ -119,7 +119,7 @@ impl StatusHistoryCell {
.find(|(k, _)| *k == "approval")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "<unknown>".to_string());
let sandbox = match &config.sandbox_policy {
let sandbox = match config.sandbox_policy.get() {
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
SandboxPolicy::ReadOnly => "read-only".to_string(),
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
+9 -6
View File
@@ -90,12 +90,15 @@ async fn status_snapshot_includes_reasoning_details() {
config.model_provider_id = "openai".to_string();
config.model_reasoning_effort = Some(ReasoningEffort::High);
config.model_reasoning_summary = ReasoningSummary::Detailed;
config.sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
config
.sandbox_policy
.set(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
.expect("set sandbox policy");
config.cwd = PathBuf::from("/workspace/tests");
+17 -7
View File
@@ -510,7 +510,7 @@ impl App {
{
let should_check = codex_core::get_platform_sandbox().is_some()
&& matches!(
app.config.sandbox_policy,
app.config.sandbox_policy.get(),
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_core::protocol::SandboxPolicy::ReadOnly
)
@@ -524,7 +524,7 @@ impl App {
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
let tx = app.app_event_tx.clone();
let logs_base_dir = app.config.codex_home.clone();
let sandbox_policy = app.config.sandbox_policy.clone();
let sandbox_policy = app.config.sandbox_policy.get().clone();
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx);
}
}
@@ -1746,19 +1746,29 @@ impl App {
AppEvent::UpdateSandboxPolicy(policy) => {
#[cfg(target_os = "windows")]
let policy_is_workspace_write_or_ro = matches!(
policy,
&policy,
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_core::protocol::SandboxPolicy::ReadOnly
);
self.config.sandbox_policy = policy.clone();
if let Err(err) = self.config.sandbox_policy.set(policy.clone()) {
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(true);
}
#[cfg(target_os = "windows")]
if !matches!(policy, codex_core::protocol::SandboxPolicy::ReadOnly)
if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some()
{
self.config.forced_auto_mode_downgraded_on_windows = false;
}
self.chat_widget.set_sandbox_policy(policy);
if let Err(err) = self.chat_widget.set_sandbox_policy(policy) {
tracing::warn!(%err, "failed to set sandbox policy on chat config");
self.chat_widget
.add_error_message(format!("Failed to set sandbox policy: {err}"));
return Ok(true);
}
// If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan.
#[cfg(target_os = "windows")]
@@ -1778,7 +1788,7 @@ impl App {
std::env::vars().collect();
let tx = self.app_event_tx.clone();
let logs_base_dir = self.config.codex_home.clone();
let sandbox_policy = self.config.sandbox_policy.clone();
let sandbox_policy = self.config.sandbox_policy.get().clone();
Self::spawn_world_writable_scan(
cwd,
env_map,
+10 -7
View File
@@ -8,6 +8,7 @@ use std::time::Duration;
use codex_app_server_protocol::AuthMode;
use codex_backend_client::Client as BackendClient;
use codex_core::config::Config;
use codex_core::config::ConstraintResult;
use codex_core::config::types::Notifications;
use codex_core::git_info::current_branch_name;
use codex_core::git_info::local_git_branches;
@@ -2554,12 +2555,12 @@ impl ChatWidget {
/// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy).
pub(crate) fn open_approvals_popup(&mut self) {
let current_approval = self.config.approval_policy.value();
let current_sandbox = self.config.sandbox_policy.clone();
let current_sandbox = self.config.sandbox_policy.get();
let mut items: Vec<SelectionItem> = Vec::new();
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
for preset in presets.into_iter() {
let is_current =
Self::preset_matches_current(current_approval, &current_sandbox, &preset);
Self::preset_matches_current(current_approval, current_sandbox, &preset);
let name = preset.label.to_string();
let description_text = preset.description;
let description = Some(description_text.to_string());
@@ -2685,7 +2686,7 @@ impl ChatWidget {
self.config.codex_home.as_path(),
cwd.as_path(),
&env_map,
&self.config.sandbox_policy,
self.config.sandbox_policy.get(),
Some(self.config.codex_home.as_path()),
) {
Ok(_) => None,
@@ -2784,7 +2785,7 @@ impl ChatWidget {
let mode_label = preset
.as_ref()
.map(|p| describe_policy(&p.sandbox))
.unwrap_or_else(|| describe_policy(&self.config.sandbox_policy));
.unwrap_or_else(|| describe_policy(self.config.sandbox_policy.get()));
let info_line = if failed_scan {
Line::from(vec![
"We couldn't complete the world-writable scan, so protections cannot be verified. "
@@ -2957,17 +2958,19 @@ impl ChatWidget {
}
/// Set the sandbox policy in the widget's config copy.
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) {
pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> {
#[cfg(target_os = "windows")]
let should_clear_downgrade = !matches!(policy, SandboxPolicy::ReadOnly)
let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly)
|| codex_core::get_platform_sandbox().is_some();
self.config.sandbox_policy = policy;
self.config.sandbox_policy.set(policy)?;
#[cfg(target_os = "windows")]
if should_clear_downgrade {
self.config.forced_auto_mode_downgraded_on_windows = false;
}
Ok(())
}
pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {
+1 -1
View File
@@ -223,7 +223,7 @@ pub async fn run_main(
let config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await;
if let Some(warning) = add_dir_warning_message(&cli.add_dir, &config.sandbox_policy) {
if let Some(warning) = add_dir_warning_message(&cli.add_dir, config.sandbox_policy.get()) {
#[allow(clippy::print_stderr)]
{
eprintln!("Error adding directories: {warning}");
+1 -1
View File
@@ -119,7 +119,7 @@ impl StatusHistoryCell {
.find(|(k, _)| *k == "approval")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "<unknown>".to_string());
let sandbox = match &config.sandbox_policy {
let sandbox = match config.sandbox_policy.get() {
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
SandboxPolicy::ReadOnly => "read-only".to_string(),
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
+9 -6
View File
@@ -90,12 +90,15 @@ async fn status_snapshot_includes_reasoning_details() {
config.model_provider_id = "openai".to_string();
config.model_reasoning_effort = Some(ReasoningEffort::High);
config.model_reasoning_summary = ReasoningSummary::Detailed;
config.sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
config
.sandbox_policy
.set(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
.expect("set sandbox policy");
config.cwd = PathBuf::from("/workspace/tests");