mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -550,6 +550,24 @@ impl PermissionProfile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy workspace-write settings interpreted for executor-owned paths.
|
||||
pub fn workspace_write_with_path_uris(
|
||||
writable_roots: &[PathUri],
|
||||
network: NetworkSandboxPolicy,
|
||||
exclude_tmpdir_env_var: bool,
|
||||
exclude_slash_tmp: bool,
|
||||
) -> Self {
|
||||
let file_system = FileSystemSandboxPolicy::workspace_write_with_path_uris(
|
||||
writable_roots,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
);
|
||||
Self::Managed {
|
||||
file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn materialize_project_roots_with_workspace_roots(
|
||||
self,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
|
||||
@@ -24,8 +24,12 @@ use crate::protocol::NetworkAccess;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::protocol::WritableRoot;
|
||||
|
||||
mod deny_read_validator;
|
||||
mod target;
|
||||
mod windows_glob;
|
||||
|
||||
pub use deny_read_validator::DenyReadValidator;
|
||||
pub use deny_read_validator::DenyReadViolation;
|
||||
pub use windows_glob::WindowsDenyReadGlobScan;
|
||||
pub use windows_glob::windows_deny_read_glob_scan;
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Core's mandatory-denial validation, shared without resolving paths on the controller.
|
||||
//! Checks raw concrete grants; policy application and sandbox enforcement remain caller-owned.
|
||||
|
||||
use super::FileSystemAccessMode;
|
||||
use super::FileSystemPath;
|
||||
use super::FileSystemSandboxEntry;
|
||||
use super::FileSystemSandboxKind;
|
||||
use super::FileSystemSandboxPolicy;
|
||||
use super::FileSystemSandboxPolicyContext;
|
||||
use super::FileSystemSpecialPath;
|
||||
use super::InvalidDenyReadGlobBehavior;
|
||||
use super::PreparedReadDenyMatcher;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
/// Prepared mandatory rules reused when a caller replaces its selected profile.
|
||||
pub struct DenyReadValidator {
|
||||
required_entries: Vec<FileSystemSandboxEntry>,
|
||||
matcher: Option<PreparedReadDenyMatcher>,
|
||||
}
|
||||
|
||||
/// Details needed to retain the caller's existing sourced constraint diagnostics.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum DenyReadViolation {
|
||||
MissingRequiredDeny,
|
||||
ReadablePath(PathUri),
|
||||
}
|
||||
|
||||
impl DenyReadValidator {
|
||||
/// Prepares the strict matcher once, using the supplied executor facts.
|
||||
pub fn new(
|
||||
required: &FileSystemSandboxPolicy,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> Result<Self, String> {
|
||||
let matcher = has_context_read_denials(required, context)
|
||||
.then(|| {
|
||||
required
|
||||
.prepare_deny_read_matcher(context, InvalidDenyReadGlobBehavior::ReturnError)
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(Self {
|
||||
required_entries: required.entries.clone(),
|
||||
matcher,
|
||||
})
|
||||
}
|
||||
|
||||
/// Preserves Core's entry-presence and raw readable-path checks.
|
||||
///
|
||||
/// Symbolic entries are not expanded. An equally specific deny does not
|
||||
/// exempt a concrete readable grant from this validation. Native callers
|
||||
/// retain their native-path conversion boundary before passing the policy.
|
||||
pub fn validate(
|
||||
&self,
|
||||
selected: &FileSystemSandboxPolicy,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> Result<(), DenyReadViolation> {
|
||||
let missing_required_deny = self
|
||||
.required_entries
|
||||
.iter()
|
||||
.any(|entry| !selected.entries.contains(entry));
|
||||
let violating_root = selected
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access.can_read())
|
||||
.find_map(|entry| {
|
||||
let FileSystemPath::Path { path } = &entry.path else {
|
||||
return None;
|
||||
};
|
||||
if path.infer_path_convention() != context.cwd.infer_path_convention() {
|
||||
return None;
|
||||
}
|
||||
self.matcher
|
||||
.as_ref()
|
||||
.is_some_and(|matcher| {
|
||||
FileSystemSandboxPolicy::matches_prepared_read_deny(path, context, matcher)
|
||||
})
|
||||
.then_some(path)
|
||||
});
|
||||
// Core reports the first conflicting path even when a required entry is missing too.
|
||||
if let Some(path) = violating_root {
|
||||
return Err(DenyReadViolation::ReadablePath(path.clone()));
|
||||
}
|
||||
if missing_required_deny {
|
||||
return Err(DenyReadViolation::MissingRequiredDeny);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn has_context_read_denials(
|
||||
policy: &FileSystemSandboxPolicy,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> bool {
|
||||
policy.kind == FileSystemSandboxKind::Restricted
|
||||
&& policy.entries.iter().any(|entry| {
|
||||
entry.access == FileSystemAccessMode::Deny
|
||||
&& !matches!(&entry.path,
|
||||
FileSystemPath::Special { value: FileSystemSpecialPath::SlashTmp }
|
||||
if context.cwd.infer_path_convention() != Some(PathConvention::Posix)
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//! Executor-context policy operations that never consult controller paths or environment.
|
||||
|
||||
use super::FileSystemPath;
|
||||
use super::FileSystemSandboxPolicy;
|
||||
use super::FileSystemSandboxPolicyContext;
|
||||
use super::FileSystemSpecialPath;
|
||||
use super::InvalidDenyReadGlobBehavior;
|
||||
use super::ReadDenyMatcher;
|
||||
use super::deny_read_validator::DenyReadValidator;
|
||||
use super::deny_read_validator::has_context_read_denials;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
const INVALID_PATH: &str = "managed filesystem denial cannot be materialized safely";
|
||||
const SHADOWED: &str = "managed filesystem denial is shadowed by a selected profile grant";
|
||||
|
||||
impl ReadDenyMatcher {
|
||||
/// Builds a strict matcher using only the supplied execution-host paths.
|
||||
pub fn try_new_with_context(
|
||||
policy: &FileSystemSandboxPolicy,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> Result<Option<Self>, String> {
|
||||
if !has_context_read_denials(policy, context) {
|
||||
return Ok(None);
|
||||
}
|
||||
validate_context_paths(policy, context)?;
|
||||
let prepared =
|
||||
policy.prepare_deny_read_matcher(context, InvalidDenyReadGlobBehavior::ReturnError)?;
|
||||
Ok(Some(Self {
|
||||
native_cwd: None,
|
||||
user_home_dir: None,
|
||||
temporary_directories: Vec::new(),
|
||||
prepared,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystemSandboxPolicy {
|
||||
/// Checks exact-root read access using paths owned by the execution host.
|
||||
/// Deny glob enforcement is checked separately with [`ReadDenyMatcher`].
|
||||
pub fn can_read_path(
|
||||
&self,
|
||||
path: &PathUri,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> bool {
|
||||
self.resolve_access(path, context).can_read()
|
||||
}
|
||||
|
||||
/// Preserves workspace-root symbols and adds their executor-owned concrete paths.
|
||||
pub fn with_materialized_project_roots_for_path_uris(mut self, roots: &[PathUri]) -> Self {
|
||||
for entry in self
|
||||
.clone()
|
||||
.materialize_project_roots_with_path_uris(roots)
|
||||
.entries
|
||||
{
|
||||
if !self.entries.contains(&entry) {
|
||||
self.entries.push(entry);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds workspace-write defaults without inspecting the execution host's filesystem.
|
||||
pub fn workspace_write_with_path_uris(
|
||||
roots: &[PathUri],
|
||||
exclude_tmpdir_env_var: bool,
|
||||
exclude_slash_tmp: bool,
|
||||
) -> Self {
|
||||
Self::workspace_write(&[], exclude_tmpdir_env_var, exclude_slash_tmp)
|
||||
.with_materialized_project_roots_for_path_uris(roots)
|
||||
}
|
||||
|
||||
/// Resolves deny glob text using the execution host's convention and home.
|
||||
pub fn get_unreadable_globs_with_context(
|
||||
&self,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
self.deny_read_globs(context)
|
||||
}
|
||||
|
||||
/// Checks the existing Core rules for required entries and readable concrete paths.
|
||||
///
|
||||
/// Sandbox-mode selection and native glob expansion remain caller-owned.
|
||||
pub fn validate_managed_deny_read(
|
||||
&self,
|
||||
required: &Self,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> Result<(), String> {
|
||||
DenyReadValidator::new(required, context)?
|
||||
.validate(self, context)
|
||||
.map_err(|_| SHADOWED.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_context_paths(
|
||||
policy: &FileSystemSandboxPolicy,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> Result<(), String> {
|
||||
let convention = context.cwd.infer_path_convention().ok_or(INVALID_PATH)?;
|
||||
let valid = |path: &PathUri| {
|
||||
path.infer_path_convention() == Some(convention) && path.lexical_depth().is_some()
|
||||
};
|
||||
if !valid(context.cwd) || context.workspace_roots.iter().any(|path| !valid(path)) {
|
||||
return Err(INVALID_PATH.into());
|
||||
}
|
||||
for entry in &policy.entries {
|
||||
match &entry.path {
|
||||
FileSystemPath::Path { path } if !valid(path) => return Err(INVALID_PATH.into()),
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Tmpdir,
|
||||
} => {
|
||||
let directories = context.temporary_directories.ok_or(
|
||||
"executor temporary-directory metadata is unavailable for managed filesystem denials",
|
||||
)?;
|
||||
if directories.iter().any(|path| !valid(path)) {
|
||||
return Err(
|
||||
"executor temporary-directory metadata cannot be materialized safely"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
FileSystemPath::Special {
|
||||
value:
|
||||
FileSystemSpecialPath::ProjectRoots {
|
||||
subpath: Some(subpath),
|
||||
},
|
||||
} if context
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.any(|root| root.join(subpath).is_err()) =>
|
||||
{
|
||||
return Err(INVALID_PATH.into());
|
||||
}
|
||||
FileSystemPath::GlobPattern { pattern }
|
||||
if (pattern.starts_with("~/")
|
||||
|| convention == PathConvention::Windows && pattern.starts_with("~\\"))
|
||||
&& !context.user_home_dir.is_some_and(valid) =>
|
||||
{
|
||||
return Err(INVALID_PATH.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "target_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,269 @@
|
||||
//! Regression coverage for remote path matching and managed-denial enforcement.
|
||||
|
||||
use super::*;
|
||||
use crate::permissions::FileSystemAccessMode;
|
||||
use crate::permissions::FileSystemSandboxEntry;
|
||||
use crate::permissions::PROTECTED_METADATA_PATH_NAMES;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn uri(input: &str) -> PathUri {
|
||||
PathUri::parse(input).unwrap()
|
||||
}
|
||||
|
||||
fn deny(path: FileSystemPath) -> FileSystemSandboxEntry {
|
||||
FileSystemSandboxEntry::new(path, FileSystemAccessMode::Deny)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_denials_use_target_paths_and_native_glob_semantics() {
|
||||
for base in [
|
||||
"file:///work/repo",
|
||||
"file:///Users/agent/repo",
|
||||
"file:///C:/work/repo",
|
||||
"file://server/share/repo",
|
||||
] {
|
||||
let cwd = uri(base);
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: std::slice::from_ref(&cwd),
|
||||
user_home_dir: Some(&cwd),
|
||||
temporary_directories: Some(&[]),
|
||||
};
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
deny(FileSystemPath::Path {
|
||||
path: cwd.join("private").unwrap(),
|
||||
}),
|
||||
deny(FileSystemPath::GlobPattern {
|
||||
pattern: "~/secrets/*.key".into(),
|
||||
}),
|
||||
deny(FileSystemPath::GlobPattern {
|
||||
pattern: "nested/**/*.env".into(),
|
||||
}),
|
||||
]);
|
||||
let matcher = ReadDenyMatcher::try_new_with_context(&policy, &context)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
for (path, expected) in [
|
||||
("private", true),
|
||||
("private/file", true),
|
||||
("private-other/file", false),
|
||||
("secrets/token.key", true),
|
||||
("secrets/nested/token.key", false),
|
||||
("nested/.env", true),
|
||||
("nested/deep/token.env", true),
|
||||
("public/file", false),
|
||||
] {
|
||||
assert_eq!(
|
||||
matcher.is_read_denied_uri(&cwd.join(path).unwrap(), &context),
|
||||
expected,
|
||||
"{base}: {path}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
matcher.is_read_denied_uri(&cwd.join("PRIVATE/file").unwrap(), &context),
|
||||
cwd.infer_path_convention() == Some(PathConvention::Windows),
|
||||
);
|
||||
assert!(matcher.is_read_denied_uri(&uri("file:///C:/foreign%2Fpath"), &context));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_denials_reject_concrete_read_and_write_grants() {
|
||||
for base in [
|
||||
"file:///work/repo",
|
||||
"file:///C:/work/repo",
|
||||
"file://server/share/repo",
|
||||
] {
|
||||
let cwd = uri(base);
|
||||
let private = cwd.join("private").unwrap();
|
||||
let required = FileSystemSandboxPolicy::restricted(vec![deny(FileSystemPath::Path {
|
||||
path: private.clone(),
|
||||
})]);
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: std::slice::from_ref(&cwd),
|
||||
user_home_dir: None,
|
||||
temporary_directories: Some(&[]),
|
||||
};
|
||||
let mut policy = FileSystemSandboxPolicy::workspace_write_with_path_uris(
|
||||
&[],
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
policy.entries.extend(required.entries.clone());
|
||||
assert_eq!(
|
||||
policy.validate_managed_deny_read(&required, &context),
|
||||
Ok(())
|
||||
);
|
||||
for access in [FileSystemAccessMode::Read, FileSystemAccessMode::Write] {
|
||||
let mut carveout = policy.clone();
|
||||
carveout.entries.push(FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Path {
|
||||
path: private.join("carveout").unwrap(),
|
||||
},
|
||||
access,
|
||||
));
|
||||
assert_eq!(
|
||||
carveout.validate_managed_deny_read(&required, &context),
|
||||
Err(SHADOWED.into())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_denials_preserve_native_metadata_read_rejections() {
|
||||
for base in [
|
||||
"file:///executor/home",
|
||||
"file:///C:/Users/executor",
|
||||
"file://server/share/executor",
|
||||
] {
|
||||
let extra_root = uri(base);
|
||||
let cwd = extra_root.join("workspace").unwrap();
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: std::slice::from_ref(&cwd),
|
||||
user_home_dir: Some(&extra_root),
|
||||
temporary_directories: Some(&[]),
|
||||
};
|
||||
let legacy = FileSystemSandboxPolicy::workspace_write_with_path_uris(
|
||||
std::slice::from_ref(&extra_root),
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
for name in [".codex", ".git"] {
|
||||
let denied_root = extra_root.join(name).unwrap();
|
||||
let child = denied_root.join("reopened").unwrap();
|
||||
for (path, expected_root_access) in [
|
||||
(
|
||||
FileSystemPath::Path {
|
||||
path: denied_root.clone(),
|
||||
},
|
||||
FileSystemAccessMode::Deny,
|
||||
),
|
||||
(
|
||||
FileSystemPath::GlobPattern {
|
||||
pattern: denied_root.join("*").unwrap().inferred_native_path_string(),
|
||||
},
|
||||
FileSystemAccessMode::Read,
|
||||
),
|
||||
] {
|
||||
let required = FileSystemSandboxPolicy::restricted(vec![deny(path)]);
|
||||
let mut policy = legacy.clone();
|
||||
policy.preserve_deny_read_restrictions_from(&required);
|
||||
assert_eq!(
|
||||
(
|
||||
policy.resolve_access(&denied_root, &context),
|
||||
policy.validate_managed_deny_read(&required, &context),
|
||||
),
|
||||
(
|
||||
expected_root_access,
|
||||
if expected_root_access == FileSystemAccessMode::Deny {
|
||||
Err(SHADOWED.into())
|
||||
} else {
|
||||
Ok(())
|
||||
},
|
||||
),
|
||||
"{base}: {name}",
|
||||
);
|
||||
let matcher = ReadDenyMatcher::try_new_with_context(&required, &context)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(matcher.is_read_denied_uri(&child, &context));
|
||||
for access in [FileSystemAccessMode::Read, FileSystemAccessMode::Write] {
|
||||
let mut reopened = policy.clone();
|
||||
reopened.entries.push(FileSystemSandboxEntry::new(
|
||||
FileSystemPath::Path {
|
||||
path: child.clone(),
|
||||
},
|
||||
access,
|
||||
));
|
||||
assert_eq!(
|
||||
reopened.validate_managed_deny_read(&required, &context),
|
||||
Err(SHADOWED.into()),
|
||||
"{base}: {name}: {access}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_unresolvable_denials_fail_closed() {
|
||||
let cwd = uri("file:///work/repo");
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: &[],
|
||||
user_home_dir: None,
|
||||
temporary_directories: Some(&[]),
|
||||
};
|
||||
for pattern in ["~/secrets/**", "secrets/[z-a]"] {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![deny(FileSystemPath::GlobPattern {
|
||||
pattern: pattern.into(),
|
||||
})]);
|
||||
assert!(ReadDenyMatcher::try_new_with_context(&policy, &context).is_err());
|
||||
}
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![deny(FileSystemPath::Path {
|
||||
path: uri("file:///C:/private"),
|
||||
})]);
|
||||
assert!(ReadDenyMatcher::try_new_with_context(&policy, &context).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_tmp_denials_follow_the_executor_convention() {
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![deny(FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::SlashTmp,
|
||||
})]);
|
||||
for (base, active) in [("file:///work", true), ("file:///C:/work", false)] {
|
||||
let cwd = uri(base);
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: &[],
|
||||
user_home_dir: None,
|
||||
temporary_directories: None,
|
||||
};
|
||||
let matcher = ReadDenyMatcher::try_new_with_context(&policy, &context).unwrap();
|
||||
assert_eq!(matcher.is_some(), active);
|
||||
if let Some(matcher) = matcher {
|
||||
assert!(matcher.is_read_denied_uri(&cwd.join("/tmp/file").unwrap(), &context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_workspace_write_keeps_metadata_protected() {
|
||||
let cwd = uri("file:///C:/repo");
|
||||
let extra = uri("file:///D:/extra");
|
||||
let policy = FileSystemSandboxPolicy::workspace_write_with_path_uris(
|
||||
std::slice::from_ref(&extra),
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: std::slice::from_ref(&cwd),
|
||||
user_home_dir: None,
|
||||
temporary_directories: Some(&[]),
|
||||
};
|
||||
for name in PROTECTED_METADATA_PATH_NAMES {
|
||||
assert!(
|
||||
policy
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry::skip_missing_path(
|
||||
FileSystemPath::Path {
|
||||
path: extra.join(name).unwrap()
|
||||
},
|
||||
FileSystemAccessMode::Read,
|
||||
))
|
||||
);
|
||||
}
|
||||
for root in [cwd.clone(), extra] {
|
||||
assert!(policy.can_write_path(&root.join("file").unwrap(), &context));
|
||||
for name in PROTECTED_METADATA_PATH_NAMES {
|
||||
let path = root.join(name).unwrap().join("file").unwrap();
|
||||
assert!(policy.can_read_path(&path, &context));
|
||||
assert!(!policy.can_write_path(&path, &context));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Windows deny-glob scan bounds shared by policy validation and native ACL expansion.
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
|
||||
/// Literal scan root and maximum traversal depth for a Windows deny glob.
|
||||
pub struct WindowsDenyReadGlobScan<'a> {
|
||||
@@ -26,8 +27,8 @@ pub fn windows_deny_read_glob_scan(
|
||||
}
|
||||
None => (".", pattern),
|
||||
};
|
||||
let components = pattern_suffix
|
||||
.split(['/', '\\'])
|
||||
let components = PathConvention::Windows
|
||||
.path_segments(pattern_suffix)
|
||||
.filter(|component| !component.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
let max_depth = if components.contains(&"**") {
|
||||
|
||||
Reference in New Issue
Block a user