permissions: constrain requirements as profiles

This commit is contained in:
Michael Bolin
2026-04-26 17:43:45 -07:00
parent 4937cc02d1
commit 434fcb6fd7
7 changed files with 200 additions and 205 deletions
+21 -14
View File
@@ -27,6 +27,7 @@ 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;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -585,8 +586,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
@@ -598,13 +599,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()
);
@@ -913,9 +916,11 @@ allowed_sandbox_modes = ["read-only"]
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
assert_eq!(
config_requirements
.sandbox_policy
.can_set(&SandboxPolicy::new_workspace_write_policy()),
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(),
@@ -1191,8 +1196,10 @@ async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow::
assert!(
layers
.requirements()
.sandbox_policy
.can_set(&SandboxPolicy::new_workspace_write_policy())
.permission_profile
.can_set(&PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy()
))
.is_ok()
);
+34 -23
View File
@@ -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") {
@@ -6318,7 +6318,7 @@ trust_level = "untrusted"
/*profile_sandbox_mode*/ None,
WindowsSandboxLevel::Disabled,
Some(&active_project),
/*sandbox_policy_constraint*/ None,
/*permission_profile_constraint*/ None,
)
.await;
@@ -6339,8 +6339,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();
@@ -6356,14 +6356,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,
})
}
@@ -6379,7 +6379,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(())
}
@@ -6401,18 +6401,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(
+39 -95
View File
@@ -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<SandboxPolicy>,
cwd: AbsolutePathBuf,
) -> std::io::Result<Constrained<PermissionProfile>> {
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 {
@@ -1734,7 +1717,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: _,
@@ -1906,9 +1889,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();
@@ -1935,7 +1916,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,
@@ -1952,19 +1933,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(|| {
@@ -1993,7 +1966,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,
@@ -2009,19 +1982,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();
@@ -2031,7 +1996,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 {
@@ -2055,9 +2020,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()
@@ -2356,8 +2319,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,
@@ -2371,17 +2333,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)?;
@@ -2399,9 +2366,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(
@@ -2419,13 +2386,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,
@@ -2451,17 +2412,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,
..
@@ -2474,28 +2431,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,
@@ -2508,7 +2452,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,