Add executor-context filesystem permission helpers (#43939)

## Why

Filesystem policies for remote execution need to interpret paths, home directories, and temporary directories using the execution host's context.

## What changed

- Add context-based read-denial matching, read-access checks, glob resolution, and `PathUri` workspace-write constructors without consulting local paths or environment variables.
- Share managed read-denial validation through `DenyReadValidator` and use it in core configuration, preserving required-entry checks, concrete grant validation, and constraint diagnostics.
- Reject invalid or unresolvable denial paths when constructing a context-based matcher, and apply `/tmp` denials according to the execution host's path convention.
- Preserve workspace-root symbols and protected metadata entries when materializing URI-based writable roots.

## Testing

Add regression tests for POSIX, Windows, and UNC path matching; conflicting read and write grants; malformed or unresolvable denials; `/tmp` handling; and workspace metadata protection.

GitOrigin-RevId: aa2b174288d1172ab52ff485f58d306236d27652
This commit is contained in:
Sean Huang
2026-09-08 22:52:05 +00:00
committed by copyberry
parent 1032738aa0
commit c53f342fec
7 changed files with 596 additions and 35 deletions
+51 -33
View File
@@ -116,10 +116,12 @@ 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::DenyReadValidator;
use codex_protocol::permissions::DenyReadViolation;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
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;
@@ -4101,49 +4103,65 @@ impl Config {
}) = filesystem_requirements.as_ref()
&& let Some(managed_file_system_policy) = managed_deny_read_policy.as_ref()
{
let managed_deny_matcher =
ReadDenyMatcher::try_new_for_local_paths(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 cwd = PathUri::from_abs_path(&resolved_cwd);
let user_home_dir = PathUri::from_host_native_path("~").ok();
let temporary_directories = std::env::var_os("TMPDIR")
.filter(|path| !path.is_empty())
.and_then(|path| AbsolutePathBuf::from_absolute_path(PathBuf::from(path)).ok())
.map(PathUri::from)
.into_iter()
.collect::<Vec<_>>();
let context = FileSystemSandboxPolicyContext {
cwd: &cwd,
workspace_roots: std::slice::from_ref(&cwd),
user_home_dir: user_home_dir.as_ref(),
temporary_directories: Some(&temporary_directories),
};
let validator = DenyReadValidator::new(managed_file_system_policy, &context)
.map_err(std::io::Error::other)?;
let requirement_source = requirement_source.clone();
constrained_permission_profile
.value
.add_validator(move |permission_profile| {
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 mut file_system_policy = permission_profile.file_system_sandbox_policy();
// Preserve the native conversion boundary before the shared URI checks.
// Mandatory entries are Deny entries, so their identity stays unchanged.
file_system_policy.entries.retain_mut(|entry| {
if entry.access.can_read()
&& let FileSystemPath::Path { path } = &mut entry.path
{
let Ok(native_path) = path.to_abs_path() else {
return false;
};
let path = path.to_abs_path().ok()?;
managed_deny_matcher
.as_ref()
.is_some_and(|matcher| matcher.is_local_path_read_denied(path.as_path()))
.then_some(path)
});
if missing_required_deny || violating_root.is_some() {
return Err(ConstraintError::InvalidValue {
*path = PathUri::from(native_path);
}
true
});
let context = FileSystemSandboxPolicyContext {
cwd: &cwd,
workspace_roots: std::slice::from_ref(&cwd),
user_home_dir: user_home_dir.as_ref(),
temporary_directories: Some(&temporary_directories),
};
validator.validate(&file_system_policy, &context).map_err(|violation| {
let candidate = match violation {
DenyReadViolation::MissingRequiredDeny => "missing managed deny".to_string(),
DenyReadViolation::ReadablePath(path) => path.to_abs_path().map_or_else(
|_| path.to_string(),
|path| path.to_string_lossy().into_owned(),
),
};
ConstraintError::InvalidValue {
field_name: "permissions.filesystem",
candidate: violating_root
.map_or_else(|| "missing managed deny".to_string(), |path| {
path.to_string_lossy().into_owned()
}),
candidate,
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,