diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 52fb24f13e..a91ae892f5 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1,8 +1,8 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; @@ -84,7 +84,7 @@ impl std::ops::DerefMut for ConstrainedWithSource { pub struct ConfigRequirements { pub approval_policy: ConstrainedWithSource, pub approvals_reviewer: ConstrainedWithSource, - pub sandbox_policy: ConstrainedWithSource, + pub permission_profile: ConstrainedWithSource, pub web_search_mode: ConstrainedWithSource, pub feature_requirements: Option>, pub managed_hooks: Option>, @@ -110,8 +110,8 @@ impl Default for ConfigRequirements { Constrained::allow_any_from_default(), /*source*/ None, ), - sandbox_policy: ConstrainedWithSource::new( - Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permission_profile: ConstrainedWithSource::new( + Constrained::allow_any(PermissionProfile::read_only()), /*source*/ None, ), web_search_mode: ConstrainedWithSource::new( @@ -967,15 +967,8 @@ impl TryFrom for ConfigRequirements { ), }; - // 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::new_read_only_policy(); - let sandbox_policy = match allowed_sandbox_modes { + let default_permission_profile = PermissionProfile::read_only(); + let permission_profile = match allowed_sandbox_modes { Some(Sourced { value: modes, source: requirement_source, @@ -984,23 +977,15 @@ impl TryFrom for ConfigRequirements { return Err(ConstraintError::InvalidValue { field_name: "allowed_sandbox_modes", candidate: format!("{modes:?}"), - allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(), + allowed: "must include 'read-only' to allow any PermissionProfile" + .to_string(), requirement_source, }); }; let requirement_source_for_error = requirement_source.clone(); - let constrained = 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 - } - }; + let constrained = Constrained::new(default_permission_profile, move |candidate| { + let mode = sandbox_mode_requirement_for_permission_profile(candidate); if modes.contains(&mode) { Ok(()) } else { @@ -1014,12 +999,10 @@ impl TryFrom for ConfigRequirements { })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } - None => { - ConstrainedWithSource::new( - Constrained::allow_any(default_sandbox_policy), - /*source*/ None, - ) - } + None => ConstrainedWithSource::new( + Constrained::allow_any(default_permission_profile), + /*source*/ None, + ), }; let exec_policy = match rules { Some(Sourced { value, source }) => { @@ -1145,7 +1128,7 @@ impl TryFrom for ConfigRequirements { Ok(ConfigRequirements { approval_policy, approvals_reviewer, - sandbox_policy, + permission_profile, web_search_mode, feature_requirements, managed_hooks, @@ -1159,6 +1142,29 @@ impl TryFrom for ConfigRequirements { } } +pub fn sandbox_mode_requirement_for_permission_profile( + permission_profile: &PermissionProfile, +) -> SandboxModeRequirement { + match permission_profile { + PermissionProfile::Disabled => SandboxModeRequirement::DangerFullAccess, + PermissionProfile::External { .. } => SandboxModeRequirement::ExternalSandbox, + PermissionProfile::Managed { .. } => { + let file_system_policy = permission_profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + SandboxModeRequirement::DangerFullAccess + } else if file_system_policy + .entries + .iter() + .any(|entry| entry.access.can_write()) + { + SandboxModeRequirement::WorkspaceWrite + } else { + SandboxModeRequirement::ReadOnly + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1168,6 +1174,7 @@ mod tests { use codex_execpolicy::Evaluation; use codex_execpolicy::RuleMatch; use codex_protocol::protocol::NetworkAccess; + use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use pretty_assertions::assert_eq; @@ -1183,6 +1190,10 @@ mod tests { )?) } + fn profile_from_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) + } + fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources { let ConfigRequirementsToml { allowed_approval_policies, @@ -1724,8 +1735,10 @@ allowed_approvals_reviewers = ["user"] ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -1803,7 +1816,7 @@ allowed_approvals_reviewers = ["user"] Some(source_location.clone()) ); assert_eq!( - requirements.sandbox_policy.source, + requirements.permission_profile.source, Some(source_location.clone()) ); assert_eq!( @@ -1869,8 +1882,10 @@ allowed_approvals_reviewers = ["user"] ); assert!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_read_only_policy()) + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy() + )) .is_ok() ); @@ -1952,25 +1967,30 @@ allowed_approvals_reviewers = ["user"] let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; assert!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_read_only_policy()) + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy() + )) .is_ok() ); + let workspace_write_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; 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, - }) + .permission_profile + .can_set(&profile_from_sandbox_policy(&workspace_write_policy)) .is_ok() ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -1980,10 +2000,12 @@ allowed_approvals_reviewers = ["user"] ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Restricted, - }), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Restricted, + } + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "ExternalSandbox".into(), @@ -2064,21 +2086,24 @@ allowed_approvals_reviewers = ["user"] let requirements = ConfigRequirements::try_from(requirements_with_sources)?; let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; + let workspace_write_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; 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, - }) + .permission_profile + .can_set(&profile_from_sandbox_policy(&workspace_write_policy)) .is_ok() ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -2108,8 +2133,10 @@ allowed_approvals_reviewers = ["user"] assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -2147,8 +2174,10 @@ allowed_approvals_reviewers = ["user"] assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_workspace_write_policy()), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "WorkspaceWrite".into(), diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 92ff18b45a..2821de5a8b 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -47,6 +47,7 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; @@ -647,7 +648,7 @@ impl ConfigToml { profile_sandbox_mode: Option, windows_sandbox_level: WindowsSandboxLevel, active_project: Option<&ProjectConfig>, - sandbox_policy_constraint: Option<&crate::Constrained>, + permission_profile_constraint: Option<&crate::Constrained>, ) -> SandboxPolicy { let sandbox_mode_was_explicit = sandbox_mode_override.is_some() || profile_sandbox_mode.is_some() @@ -707,14 +708,16 @@ impl ConfigToml { downgrade_workspace_write_if_unsupported(&mut sandbox_policy); } if !sandbox_mode_was_explicit - && let Some(constraint) = sandbox_policy_constraint - && let Err(err) = constraint.can_set(&sandbox_policy) + && let Some(constraint) = permission_profile_constraint + && let Err(err) = constraint.can_set(&PermissionProfile::from_legacy_sandbox_policy( + &sandbox_policy, + )) { tracing::warn!( error = %err, "default sandbox policy is disallowed by requirements; falling back to required default" ); - sandbox_policy = constraint.get().clone(); + sandbox_policy = SandboxPolicy::new_read_only_policy(); downgrade_workspace_write_if_unsupported(&mut sandbox_policy); } sandbox_policy diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index eb0e7713fb..d628fbf04f 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -53,6 +53,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::sandbox_mode_requirement_for_permission_profile; pub use constraint::Constrained; pub use constraint::ConstraintError; pub use constraint::ConstraintResult; diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 3a77c16189..cc465d42b1 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -27,8 +27,8 @@ use codex_config::version_for_toml; use codex_exec_server::LOCAL_FS; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -#[cfg(target_os = "macos")] use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -577,8 +577,8 @@ allowed_sandbox_modes = ["read-only"] AskForApproval::Never ); assert_eq!( - *state.requirements().sandbox_policy.get(), - SandboxPolicy::new_read_only_policy() + state.requirements().permission_profile.get(), + &PermissionProfile::read_only() ); assert!( state @@ -590,13 +590,15 @@ allowed_sandbox_modes = ["read-only"] assert!( state .requirements() - .sandbox_policy - .can_set(&SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) + .permission_profile + .can_set(&PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + )) .is_err() ); @@ -867,6 +869,55 @@ allowed_approval_policies = ["on-request"] Ok(()) } +#[tokio::test(flavor = "current_thread")] +async fn system_remote_sandbox_config_keeps_cloud_sandbox_modes() -> anyhow::Result<()> { + let tmp = tempdir()?; + let requirements_file = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_file, + r#" +[[remote_sandbox_config]] +hostname_patterns = ["*"] +allowed_sandbox_modes = ["read-only", "workspace-write"] +"#, + ) + .await?; + + let cloud_source = RequirementSource::CloudRequirements; + let mut config_requirements_toml = ConfigRequirementsWithSources::default(); + config_requirements_toml.merge_unset_fields( + cloud_source.clone(), + toml::from_str( + r#" +allowed_sandbox_modes = ["read-only"] +"#, + )?, + ); + load_requirements_toml( + LOCAL_FS.as_ref(), + &mut config_requirements_toml, + &AbsolutePathBuf::try_from(requirements_file)?, + ) + .await?; + let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?; + + assert_eq!( + config_requirements.permission_profile.can_set( + &PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy() + ) + ), + Err(ConstraintError::InvalidValue { + field_name: "sandbox_mode", + candidate: "WorkspaceWrite".into(), + allowed: "[ReadOnly]".into(), + requirement_source: cloud_source, + }) + ); + + Ok(()) +} + #[tokio::test(flavor = "current_thread")] async fn load_requirements_toml_resolves_deny_read_against_parent() -> anyhow::Result<()> { let tmp = tempdir()?; @@ -1088,6 +1139,54 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result Ok(()) } +#[tokio::test] +async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow::Result<()> { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + + let requirements: ConfigRequirementsToml = toml::from_str( + r#" + allowed_sandbox_modes = ["read-only"] + + [[remote_sandbox_config]] + hostname_patterns = ["*"] + allowed_sandbox_modes = ["read-only", "workspace-write"] + "#, + )?; + let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(requirements)) }); + let layers = load_config_layers_state( + LOCAL_FS.as_ref(), + &codex_home, + Some(cwd), + &[] as &[(String, TomlValue)], + LoaderOverrides::default(), + cloud_requirements, + &codex_config::NoopThreadConfigLoader, + ) + .await?; + + assert_eq!( + layers.requirements_toml().allowed_sandbox_modes, + Some(vec![ + codex_config::SandboxModeRequirement::ReadOnly, + codex_config::SandboxModeRequirement::WorkspaceWrite, + ]) + ); + assert!( + layers + .requirements() + .permission_profile + .can_set(&PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy() + )) + .is_ok() + ); + + Ok(()) +} + #[tokio::test] async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyhow::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index a55e444e99..d0ea8980bf 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1636,7 +1636,7 @@ network_access = false # This should be ignored. /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; assert_eq!(resolution, SandboxPolicy::DangerFullAccess); @@ -1657,7 +1657,7 @@ network_access = true # This should be ignored. /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); @@ -1689,7 +1689,7 @@ trust_level = "trusted" /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; if cfg!(target_os = "windows") { @@ -1729,7 +1729,7 @@ exclude_slash_tmp = true /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; if cfg!(target_os = "windows") { @@ -6316,7 +6316,7 @@ trust_level = "untrusted" /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, Some(&active_project), - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; @@ -6337,8 +6337,8 @@ trust_level = "untrusted" } #[tokio::test] -async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults() --> anyhow::Result<()> { +async fn derive_sandbox_policy_falls_back_to_read_only_for_implicit_defaults() -> anyhow::Result<()> +{ let project_dir = TempDir::new()?; let project_path = project_dir.path().to_path_buf(); let project_key = project_path.to_string_lossy().to_string(); @@ -6354,14 +6354,14 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau let active_project = ProjectConfig { trust_level: Some(TrustLevel::Trusted), }; - let constrained = Constrained::new(SandboxPolicy::DangerFullAccess, |candidate| { - if matches!(candidate, SandboxPolicy::DangerFullAccess) { + let constrained = Constrained::new(PermissionProfile::read_only(), |candidate| { + if candidate == &PermissionProfile::read_only() { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: format!("{candidate:?}"), - allowed: "[DangerFullAccess]".to_string(), + allowed: "[ReadOnly]".to_string(), requirement_source: RequirementSource::Unknown, }) } @@ -6377,7 +6377,7 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau ) .await; - assert_eq!(resolution, SandboxPolicy::DangerFullAccess); + assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); Ok(()) } @@ -6399,18 +6399,29 @@ async fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallb let active_project = ProjectConfig { trust_level: Some(TrustLevel::Trusted), }; - let constrained = Constrained::new(SandboxPolicy::new_workspace_write_policy(), |candidate| { - if matches!(candidate, SandboxPolicy::WorkspaceWrite { .. }) { - Ok(()) - } else { - Err(ConstraintError::InvalidValue { - field_name: "sandbox_mode", - candidate: format!("{candidate:?}"), - allowed: "[WorkspaceWrite]".to_string(), - requirement_source: RequirementSource::Unknown, - }) - } - })?; + let constrained = Constrained::new( + PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + |candidate| { + if matches!( + candidate, + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { entries, .. }, + .. + } if entries + .iter() + .any(|entry| entry.access.can_write()) + ) { + Ok(()) + } else { + Err(ConstraintError::InvalidValue { + field_name: "sandbox_mode", + candidate: format!("{candidate:?}"), + allowed: "[WorkspaceWrite]".to_string(), + requirement_source: RequirementSource::Unknown, + }) + } + }, + )?; let resolution = cfg .derive_sandbox_policy( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 9a7a9ca79d..9635034dcc 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -19,6 +19,7 @@ use codex_config::LoaderOverrides; use codex_config::McpServerIdentity; use codex_config::McpServerRequirement; use codex_config::ResidencyRequirement; +use codex_config::SandboxModeRequirement; use codex_config::Sourced; use codex_config::ThreadConfigLoader; use codex_config::config_toml::ConfigToml; @@ -30,6 +31,7 @@ use codex_config::config_toml::validate_model_providers; use codex_config::loader::load_config_layers_state; use codex_config::loader::project_trust_key; use codex_config::profile_toml::ConfigProfile; +use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::DEFAULT_OTEL_ENVIRONMENT; @@ -295,25 +297,6 @@ impl Permissions { } } -fn constrained_permission_profile_from_sandbox_projection( - initial_value: PermissionProfile, - sandbox_constraint: Constrained, - cwd: AbsolutePathBuf, -) -> std::io::Result> { - Constrained::new(initial_value, move |candidate| { - let (file_system_sandbox_policy, network_sandbox_policy) = - candidate.to_runtime_permissions(); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - candidate, - &file_system_sandbox_policy, - network_sandbox_policy, - cwd.as_path(), - ); - sandbox_constraint.can_set(&sandbox_policy) - }) - .map_err(std::io::Error::from) -} - /// Configured thread persistence backend. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum ThreadStoreConfig { @@ -1709,7 +1692,7 @@ impl Config { let ConfigRequirements { approval_policy: mut constrained_approval_policy, approvals_reviewer: mut constrained_approvals_reviewer, - sandbox_policy: mut constrained_sandbox_policy, + permission_profile: mut constrained_permission_profile, web_search_mode: mut constrained_web_search_mode, feature_requirements, managed_hooks: _, @@ -1881,9 +1864,7 @@ impl Config { let ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) = if let Some(mut permission_profile) = permission_profile { let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); @@ -1910,7 +1891,7 @@ impl Config { } else { NetworkProxyConfig::default() }; - let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, network_sandbox_policy, @@ -1927,19 +1908,11 @@ impl Config { &file_system_sandbox_policy, network_sandbox_policy, ); - sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &permission_profile, - &file_system_sandbox_policy, - network_sandbox_policy, - resolved_cwd.as_path(), - ); } ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) } else if profiles_are_active { let permissions = cfg.permissions.as_ref().ok_or_else(|| { @@ -1968,7 +1941,7 @@ impl Config { &file_system_sandbox_policy, network_sandbox_policy, ); - let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, network_sandbox_policy, @@ -1984,19 +1957,11 @@ impl Config { &file_system_sandbox_policy, network_sandbox_policy, ); - sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &permission_profile, - &file_system_sandbox_policy, - network_sandbox_policy, - resolved_cwd.as_path(), - ); } ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) } else { let configured_network_proxy_config = NetworkProxyConfig::default(); @@ -2006,7 +1971,7 @@ impl Config { config_profile.sandbox_mode, windows_sandbox_level, Some(&active_project), - Some(&constrained_sandbox_policy), + Some(&constrained_permission_profile), ) .await; if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy { @@ -2030,9 +1995,7 @@ impl Config { ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) }; let approval_policy_was_explicit = approval_policy_override.is_some() @@ -2324,8 +2287,7 @@ impl Config { .map(AbsolutePathBuf::to_path_buf) .or_else(|| resolve_sqlite_home_env(&resolved_cwd)) .unwrap_or_else(|| codex_home.to_path_buf()); - let original_sandbox_policy = sandbox_policy.clone(); - + let original_permission_profile = permission_profile.clone(); apply_requirement_constrained_value( "approval_policy", approval_policy, @@ -2339,17 +2301,22 @@ impl Config { && !filesystem_requirements.deny_read.is_empty() { let requirement_source = filesystem_requirements_source.clone(); - constrained_sandbox_policy + constrained_permission_profile .value - .add_validator(move |policy| match policy { - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Ok(()), - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - Err(ConstraintError::InvalidValue { - field_name: "sandbox_mode", - candidate: policy.to_string(), - allowed: "[read-only, workspace-write]".to_string(), - requirement_source: requirement_source.clone(), - }) + .add_validator(move |permission_profile| { + let mode = sandbox_mode_requirement_for_permission_profile(permission_profile); + match mode { + SandboxModeRequirement::ReadOnly + | SandboxModeRequirement::WorkspaceWrite => Ok(()), + SandboxModeRequirement::DangerFullAccess + | SandboxModeRequirement::ExternalSandbox => { + Err(ConstraintError::InvalidValue { + field_name: "sandbox_mode", + candidate: format!("{mode:?}"), + allowed: "[read-only, workspace-write]".to_string(), + requirement_source: requirement_source.clone(), + }) + } } }) .map_err(std::io::Error::from)?; @@ -2367,9 +2334,9 @@ impl Config { &mut startup_warnings, )?; apply_requirement_constrained_value( - "sandbox_mode", - sandbox_policy, - &mut constrained_sandbox_policy, + "permission_profile", + permission_profile, + &mut constrained_permission_profile, &mut startup_warnings, )?; apply_requirement_constrained_value( @@ -2387,13 +2354,7 @@ impl Config { None => (None, None), }; let has_network_requirements = network_requirements.is_some(); - let network_permission_profile = if *constrained_sandbox_policy.get() - == original_sandbox_policy - { - permission_profile.clone() - } else { - PermissionProfile::from_legacy_sandbox_policy(constrained_sandbox_policy.get()) - }; + let network_permission_profile = constrained_permission_profile.get().clone(); let network = NetworkProxySpec::from_config_and_constraints( configured_network_proxy_config, network_requirements, @@ -2419,17 +2380,13 @@ impl Config { zsh_path.as_ref(), main_execve_wrapper_exe.as_ref(), ); - let effective_sandbox_policy = constrained_sandbox_policy.value.get().clone(); - let mut effective_file_system_sandbox_policy = - if effective_sandbox_policy == original_sandbox_policy { - file_system_sandbox_policy - } else { - FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( - &effective_sandbox_policy, - resolved_cwd.as_path(), - &file_system_sandbox_policy, - ) - }; + let effective_permission_profile = constrained_permission_profile.value.get().clone(); + let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) = + effective_permission_profile.to_runtime_permissions(); + if effective_permission_profile != original_permission_profile { + effective_file_system_sandbox_policy + .preserve_deny_read_restrictions_from(&file_system_sandbox_policy); + } if let Some(Sourced { value: filesystem_requirements, .. @@ -2442,28 +2399,15 @@ impl Config { } let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy .with_additional_readable_roots(resolved_cwd.as_path(), &helper_readable_roots); - let effective_network_sandbox_policy = - if effective_sandbox_policy == original_sandbox_policy { - network_sandbox_policy - } else { - NetworkSandboxPolicy::from(&effective_sandbox_policy) - }; - let effective_enforcement = if effective_sandbox_policy == original_sandbox_policy { - permission_profile.enforcement() - } else { - SandboxEnforcement::from_legacy_sandbox_policy(&effective_sandbox_policy) - }; let effective_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( - effective_enforcement, + effective_permission_profile.enforcement(), &effective_file_system_sandbox_policy, effective_network_sandbox_policy, ); - let constrained_permission_profile = - constrained_permission_profile_from_sandbox_projection( - effective_permission_profile, - constrained_sandbox_policy.value.clone(), - resolved_cwd.clone(), - )?; + constrained_permission_profile + .value + .set(effective_permission_profile) + .map_err(std::io::Error::from)?; let config = Self { model, service_tier, @@ -2476,7 +2420,7 @@ impl Config { startup_warnings, permissions: Permissions { approval_policy: constrained_approval_policy.value, - permission_profile: constrained_permission_profile, + permission_profile: constrained_permission_profile.value, network, allow_login_shell, shell_environment_policy, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 1c48c44918..dde85d9392 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -126,7 +126,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { requirement_lines.push(requirement_line( "allowed_sandbox_modes", value, - requirements.sandbox_policy.source.as_ref(), + requirements.permission_profile.source.as_ref(), )); } @@ -531,8 +531,8 @@ mod tests { use codex_config::WebSearchModeRequirement; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::WebSearchMode; + use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use ratatui::text::Line; use std::collections::BTreeMap; @@ -622,8 +622,8 @@ mod tests { Constrained::allow_any(ApprovalsReviewer::AutoReview), Some(RequirementSource::LegacyManagedConfigTomlFromMdm), ), - sandbox_policy: ConstrainedWithSource::new( - Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permission_profile: ConstrainedWithSource::new( + Constrained::allow_any(PermissionProfile::read_only()), Some(RequirementSource::SystemRequirementsToml { file: requirements_file.clone(), }),