artifacts: honor filesystem policy carveouts in path checks

This commit is contained in:
Michael Bolin
2026-03-04 01:10:16 -08:00
parent 03b0581a3a
commit 65344a607a
4 changed files with 146 additions and 48 deletions
@@ -0,0 +1,123 @@
use codex_protocol::protocol::FileSystemSandboxPolicy;
use std::path::Path;
pub(super) fn path_is_readable(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
path: &Path,
) -> bool {
if file_system_sandbox_policy
.get_unreadable_roots_with_cwd(cwd)
.iter()
.any(|root| path.starts_with(root.as_path()))
{
return false;
}
if file_system_sandbox_policy.has_full_disk_read_access() {
return true;
}
file_system_sandbox_policy
.get_readable_roots_with_cwd(cwd)
.iter()
.any(|root| path.starts_with(root.as_path()))
}
pub(super) fn path_is_writable(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
cwd: &Path,
path: &Path,
) -> bool {
if file_system_sandbox_policy
.get_unreadable_roots_with_cwd(cwd)
.iter()
.any(|root| path.starts_with(root.as_path()))
{
return false;
}
if file_system_sandbox_policy.has_full_disk_write_access() {
return true;
}
file_system_sandbox_policy
.get_writable_roots_with_cwd(cwd)
.iter()
.any(|root| root.is_path_writable(path))
}
#[cfg(test)]
mod tests {
use super::path_is_readable;
use super::path_is_writable;
use codex_protocol::protocol::FileSystemAccessMode;
use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::FileSystemSpecialPathKind;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn unreadable_subpaths_override_root_read_access() {
let cwd = TempDir::new().expect("tempdir");
let readable = cwd.path().join("readable.txt");
let blocked = cwd.path().join("blocked.txt");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath {
kind: FileSystemSpecialPathKind::Root,
subpath: None,
},
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath {
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
subpath: Some(PathBuf::from("blocked.txt")),
},
},
access: FileSystemAccessMode::None,
},
]);
assert_eq!(path_is_readable(&policy, cwd.path(), &readable), true);
assert_eq!(path_is_readable(&policy, cwd.path(), &blocked), false);
}
#[test]
fn unreadable_subpaths_override_writable_roots() {
let cwd = TempDir::new().expect("tempdir");
let allowed = cwd.path().join("allowed.txt");
let blocked = cwd.path().join("blocked.txt");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath {
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
subpath: None,
},
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath {
kind: FileSystemSpecialPathKind::CurrentWorkingDirectory,
subpath: Some(PathBuf::from("blocked.txt")),
},
},
access: FileSystemAccessMode::None,
},
]);
assert_eq!(path_is_writable(&policy, cwd.path(), &allowed), true);
assert_eq!(path_is_writable(&policy, cwd.path(), &blocked), false);
}
}
+1
View File
@@ -1,5 +1,6 @@
pub(crate) mod agent_jobs;
pub mod apply_patch;
mod artifact_path_access;
mod dynamic;
mod grep_files;
mod js_repl;
@@ -19,6 +19,7 @@ use crate::path_utils::resolve_symlink_write_paths;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::artifact_path_access;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
@@ -111,8 +112,16 @@ async fn authorize_path_access(
PathAccessKind::Write => effective_write_path(&access.path),
};
let allowed = match access.kind {
PathAccessKind::Read => path_is_readable(turn, &effective_path),
PathAccessKind::Write => path_is_writable(turn, &effective_path),
PathAccessKind::Read => artifact_path_access::path_is_readable(
&turn.file_system_sandbox_policy,
&turn.cwd,
&effective_path,
),
PathAccessKind::Write => artifact_path_access::path_is_writable(
&turn.file_system_sandbox_policy,
&turn.cwd,
&effective_path,
),
};
if allowed {
return Ok(());
@@ -188,28 +197,6 @@ async fn authorize_path_access(
)))
}
fn path_is_readable(turn: &TurnContext, path: &Path) -> bool {
if turn.sandbox_policy.has_full_disk_read_access() {
return true;
}
turn.sandbox_policy
.get_readable_roots_with_cwd(&turn.cwd)
.iter()
.any(|root| path.starts_with(root.as_path()))
}
fn path_is_writable(turn: &TurnContext, path: &Path) -> bool {
if turn.sandbox_policy.has_full_disk_write_access() {
return true;
}
turn.sandbox_policy
.get_writable_roots_with_cwd(&turn.cwd)
.iter()
.any(|root| root.is_path_writable(path))
}
fn effective_read_path(path: &Path) -> PathBuf {
normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path))
}
@@ -20,6 +20,7 @@ use crate::path_utils::resolve_symlink_write_paths;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::artifact_path_access;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
@@ -100,8 +101,16 @@ async fn authorize_path_access(
PathAccessKind::Write => effective_write_path(&access.path),
};
let allowed = match access.kind {
PathAccessKind::Read => path_is_readable(turn, &effective_path),
PathAccessKind::Write => path_is_writable(turn, &effective_path),
PathAccessKind::Read => artifact_path_access::path_is_readable(
&turn.file_system_sandbox_policy,
&turn.cwd,
&effective_path,
),
PathAccessKind::Write => artifact_path_access::path_is_writable(
&turn.file_system_sandbox_policy,
&turn.cwd,
&effective_path,
),
};
if allowed {
return Ok(());
@@ -173,28 +182,6 @@ async fn authorize_path_access(
)))
}
fn path_is_readable(turn: &TurnContext, path: &Path) -> bool {
if turn.sandbox_policy.has_full_disk_read_access() {
return true;
}
turn.sandbox_policy
.get_readable_roots_with_cwd(&turn.cwd)
.iter()
.any(|root| path.starts_with(root.as_path()))
}
fn path_is_writable(turn: &TurnContext, path: &Path) -> bool {
if turn.sandbox_policy.has_full_disk_write_access() {
return true;
}
turn.sandbox_policy
.get_writable_roots_with_cwd(&turn.cwd)
.iter()
.any(|root| root.is_path_writable(path))
}
fn effective_read_path(path: &Path) -> PathBuf {
normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path))
}