feat: support allowed_sandbox_modes in requirements.toml

This commit is contained in:
Michael Bolin
2025-12-19 09:35:51 -08:00
parent dcc01198e2
commit 159b0d088b
25 changed files with 244 additions and 100 deletions
+7 -7
View File
@@ -416,7 +416,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
@@ -452,7 +452,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;
@@ -527,7 +527,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(),
@@ -644,7 +644,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(),
);
@@ -694,7 +694,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,
@@ -711,7 +711,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(),
};
@@ -898,7 +898,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,7 +81,11 @@ 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) {
@@ -62,7 +98,46 @@ 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`?
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,
})
}
}
+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
}
}