Preserve managed deny-read rules across permission updates (#40004)

## Why

Runtime permission updates must not weaken managed filesystem `deny_read`
requirements.

## What changed

- Retain managed deny-read rules separately and merge them into updated
  permission profiles.
- Reject permission profiles and legacy sandbox policies that conflict with a
  managed denied path.
- Apply the same constraint when `command/exec` handles a request-specific
  sandbox policy.

## Testing

- Cover thread permission updates with managed deny-read requirements.
- Cover `command/exec` enforcement for managed and user-defined denies,
  including conflicting policy and profile overrides.

GitOrigin-RevId: 5e387b9c1bf1650a21753a74a3338bd33df7d0ce
This commit is contained in:
sayan-oai authored and copyberry committed 2026-08-21 20:15:23 +00:00
1 parent 0f1a30b5c2
commit ad9e8097fd
4 files changed
+290 -33

No files matched your search

+99 -15
View File
@@ -114,8 +114,10 @@ use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::permissions::ReadDenyMatcher;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SandboxPolicy;
@@ -297,6 +299,8 @@ pub struct Permissions {
/// Constrained permission profile plus its selected profile identity, if
/// the profile came from a built-in or named config profile.
permission_profile_state: PermissionProfileState,
/// Managed deny-read rules retained independently from user-defined denies.
managed_deny_read_policy: Option<Arc<FileSystemSandboxPolicy>>,
/// Thread-scoped runtime workspace roots. Symbolic `:workspace_roots`
/// entries in the permission profile are materialized against these roots.
workspace_roots: Vec<AbsolutePathBuf>,
@@ -332,6 +336,7 @@ impl Permissions {
permission_profile_state: PermissionProfileState::from_constrained_legacy(
permission_profile,
)?,
managed_deny_read_policy: None,
workspace_roots: Vec::new(),
network: None,
allow_login_shell: true,
@@ -388,8 +393,10 @@ impl Permissions {
&self,
permission_profile: &PermissionProfile,
) -> ConstraintResult<()> {
let permission_profile =
self.permission_profile_preserving_managed_denied_reads(permission_profile.clone());
self.permission_profile_state
.can_set_legacy_permission_profile(permission_profile)
.can_set_legacy_permission_profile(&permission_profile)
}
pub fn set_workspace_roots(&mut self, workspace_roots: Vec<AbsolutePathBuf>) {
@@ -455,8 +462,7 @@ impl Permissions {
&file_system_sandbox_policy,
network_sandbox_policy,
);
self.permission_profile_state
.can_set_legacy_permission_profile(&permission_profile)
self.can_set_permission_profile(&permission_profile)
}
/// Set permissions from a legacy sandbox policy and keep every permission
@@ -496,9 +502,7 @@ impl Permissions {
],
};
self.permission_profile_state
.set_legacy_permission_profile(permission_profile)?;
Ok(())
self.set_permission_profile(permission_profile)
}
/// Set permissions from the canonical profile.
@@ -506,9 +510,34 @@ impl Permissions {
&mut self,
permission_profile: PermissionProfile,
) -> ConstraintResult<()> {
let permission_profile =
self.permission_profile_preserving_managed_denied_reads(permission_profile);
self.permission_profile_state
.set_legacy_permission_profile(permission_profile)
}
fn permission_profile_preserving_managed_denied_reads(
&self,
permission_profile: PermissionProfile,
) -> PermissionProfile {
let Some(managed_deny_read_policy) = self.managed_deny_read_policy.as_ref() else {
return permission_profile;
};
if matches!(
sandbox_mode_requirement_for_permission_profile(&permission_profile),
SandboxModeRequirement::DangerFullAccess | SandboxModeRequirement::ExternalSandbox
) {
return permission_profile;
}
let enforcement = permission_profile.enforcement();
let (mut file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
file_system_policy.preserve_deny_read_restrictions_from(managed_deny_read_policy);
PermissionProfile::from_runtime_permissions_with_enforcement(
enforcement,
&file_system_policy,
network_policy,
)
}
}
// A profile override only inherits the selected profile's proxy/allowlist config
@@ -3985,15 +4014,17 @@ impl Config {
effective_file_system_sandbox_policy
.preserve_deny_read_restrictions_from(&file_system_sandbox_policy);
}
if let Some(Sourced {
value: filesystem_requirements,
..
}) = filesystem_requirements.as_ref()
{
apply_managed_filesystem_constraints(
&mut effective_file_system_sandbox_policy,
filesystem_requirements,
);
let managed_deny_read_policy = filesystem_requirements
.as_ref()
.filter(|Sourced { value, .. }| !value.deny_read.is_empty())
.map(|Sourced { value, .. }| {
let mut policy = FileSystemSandboxPolicy::restricted(Vec::new());
apply_managed_filesystem_constraints(&mut policy, value);
Arc::new(policy)
});
if let Some(managed_deny_read_policy) = managed_deny_read_policy.as_ref() {
effective_file_system_sandbox_policy
.preserve_deny_read_restrictions_from(managed_deny_read_policy);
}
let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy
.with_additional_readable_roots(resolved_cwd.as_path(), &helper_readable_roots);
@@ -4006,6 +4037,58 @@ impl Config {
.value
.set(effective_permission_profile)
.map_err(std::io::Error::from)?;
if let Some(Sourced {
source: requirement_source,
..
}) = filesystem_requirements.as_ref()
&& let Some(managed_file_system_policy) = managed_deny_read_policy.as_ref()
{
let _initial_matcher =
ReadDenyMatcher::try_new(managed_file_system_policy, resolved_cwd.as_path())
.map_err(std::io::Error::other)?;
let managed_file_system_policy = Arc::clone(managed_file_system_policy);
let permission_cwd = resolved_cwd.clone();
let requirement_source = requirement_source.clone();
constrained_permission_profile
.value
.add_validator(move |permission_profile| {
let managed_deny_matcher =
ReadDenyMatcher::new(&managed_file_system_policy, permission_cwd.as_path());
let file_system_policy = permission_profile.file_system_sandbox_policy();
let missing_required_deny = managed_file_system_policy
.entries
.iter()
.any(|entry| !file_system_policy.entries.contains(entry));
let violating_root = file_system_policy
.entries
.iter()
.filter(|entry| entry.access.can_read())
.find_map(|entry| {
let FileSystemPath::Path { path } = &entry.path else {
return None;
};
let path = path.to_abs_path().ok()?;
managed_deny_matcher
.as_ref()
.is_some_and(|matcher| matcher.is_read_denied(path.as_path()))
.then_some(path)
});
if missing_required_deny || violating_root.is_some() {
return Err(ConstraintError::InvalidValue {
field_name: "permissions.filesystem",
candidate: violating_root
.map_or_else(|| "missing managed deny".to_string(), |path| {
path.to_string_lossy().into_owned()
}),
allowed: "all managed deny_read restrictions".to_string(),
requirement_source: requirement_source.clone(),
});
}
Ok(())
})
.map_err(std::io::Error::from)?;
}
let permission_profile_state = PermissionProfileState::from_constrained_active_profile(
constrained_permission_profile.value,
active_permission_profile,
@@ -4031,6 +4114,7 @@ impl Config {
permissions: Permissions {
approval_policy: constrained_approval_policy.value,
permission_profile_state,
managed_deny_read_policy,
workspace_roots,
network,
allow_login_shell,