mirror of
https://github.com/openai/codex.git
synced 2026-08-26 13:38:49 +00:00
## What changed - Make `ExecutorFileSystem::walk` a required backend operation instead of providing a fallback built from directory reads and metadata requests. - Implement bounded local walks on a blocking task with cancellation, symlink cycle detection, deterministic ordering, error collection, and response-size limits. - Have remote filesystems use the server's walk operation directly. ## Testing - Cover local and remote handling of invalid roots and limits, directory symlinks, non-UTF-8 names, cancellation, sandbox contexts, and response budgets. GitOrigin-RevId: 7499bf05080c3f9965a5eb7ffd593de604d62c2a
1143 lines
39 KiB
Rust
1143 lines
39 KiB
Rust
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use codex_exec_server::CopyOptions;
|
|
use codex_exec_server::CreateDirectoryOptions;
|
|
#[cfg(unix)]
|
|
use codex_exec_server::ExecServerRuntimePaths;
|
|
#[cfg(unix)]
|
|
use codex_exec_server::ExecutorFileSystem;
|
|
use codex_exec_server::FILE_READ_CHUNK_SIZE;
|
|
use codex_exec_server::FileMetadata;
|
|
#[cfg(unix)]
|
|
use codex_exec_server::LocalFileSystem;
|
|
use codex_exec_server::ReadDirectoryEntry;
|
|
use codex_exec_server::RemoveOptions;
|
|
use codex_exec_server::WalkEntry;
|
|
use codex_exec_server::WalkEntryKind;
|
|
use codex_exec_server::WalkOptions;
|
|
use codex_exec_server::WalkOutcome;
|
|
use codex_exec_server::WriteFileOptions;
|
|
use codex_file_system::MAX_WALK_DEPTH;
|
|
use codex_file_system::MAX_WALK_DIRECTORIES;
|
|
use codex_file_system::MAX_WALK_ENTRIES;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::models::FileSystemPermissions;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
|
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
|
|
use codex_utils_path_uri::PathUri;
|
|
use futures::TryStreamExt;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
use test_case::test_case;
|
|
|
|
use super::support::FileSystemImplementation;
|
|
use super::support::absolute_path;
|
|
use super::support::create_file_system_context;
|
|
#[cfg(windows)]
|
|
use super::support::is_unsupported_restricted_token_host;
|
|
use super::support::read_only_sandbox;
|
|
use super::support::workspace_write_sandbox;
|
|
|
|
#[test]
|
|
fn sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths() -> Result<()> {
|
|
let tmp = TempDir::new()?;
|
|
let writable_dir = tmp.path().join("writable");
|
|
let git_dir = writable_dir.join(".git");
|
|
std::fs::create_dir_all(&git_dir)?;
|
|
|
|
let sandbox = workspace_write_sandbox(writable_dir.clone());
|
|
let permissions: PermissionProfile = sandbox.permissions.try_into()?;
|
|
let policy = permissions.file_system_sandbox_policy();
|
|
let cwd = absolute_path(writable_dir.clone());
|
|
let writable_roots = policy.get_writable_roots_with_cwd(cwd.as_path());
|
|
let writable_dir = absolute_path(std::fs::canonicalize(writable_dir)?);
|
|
let git_dir = absolute_path(std::fs::canonicalize(git_dir)?);
|
|
let Some(writable_root) = writable_roots
|
|
.iter()
|
|
.find(|writable_root| writable_root.root == writable_dir)
|
|
else {
|
|
panic!("writable root should be preserved");
|
|
};
|
|
|
|
assert!(writable_root.read_only_subpaths.contains(&git_dir));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_get_metadata_reports_files_and_directories(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
let directory_path = tmp.path().join("notes");
|
|
std::fs::write(&file_path, "hello")?;
|
|
std::fs::create_dir(&directory_path)?;
|
|
|
|
let file_metadata = file_system
|
|
.get_metadata(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
Default::default(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
file_metadata,
|
|
FileMetadata {
|
|
is_directory: false,
|
|
is_file: true,
|
|
is_symlink: false,
|
|
size: 5,
|
|
created_at_ms: file_metadata.created_at_ms,
|
|
modified_at_ms: file_metadata.modified_at_ms,
|
|
}
|
|
);
|
|
assert!(file_metadata.modified_at_ms > 0);
|
|
|
|
let directory_metadata = file_system
|
|
.get_metadata(
|
|
&PathUri::from_host_native_path(&directory_path)?,
|
|
Default::default(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
directory_metadata,
|
|
FileMetadata {
|
|
is_directory: true,
|
|
is_file: false,
|
|
is_symlink: false,
|
|
size: std::fs::metadata(&directory_path)?.len(),
|
|
created_at_ms: directory_metadata.created_at_ms,
|
|
modified_at_ms: directory_metadata.modified_at_ms,
|
|
}
|
|
);
|
|
assert!(directory_metadata.modified_at_ms > 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local, true, false ; "local_follow")]
|
|
#[test_case(FileSystemImplementation::Local, false, false ; "local_no_follow")]
|
|
#[test_case(FileSystemImplementation::Remote, true, false ; "remote_follow")]
|
|
#[test_case(FileSystemImplementation::Remote, false, false ; "remote_no_follow")]
|
|
#[cfg_attr(any(target_os = "linux", windows), test_case(FileSystemImplementation::Local, false, true ; "local_sandboxed_no_follow"))]
|
|
#[cfg_attr(any(target_os = "linux", windows), test_case(FileSystemImplementation::Remote, false, true ; "remote_sandboxed_no_follow"))]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_create_directory_creates_nested_directories(
|
|
implementation: FileSystemImplementation,
|
|
follow_symlinks: bool,
|
|
sandboxed: bool,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let root = tmp.path().canonicalize()?;
|
|
let nested_dir = root.join("source").join("nested");
|
|
let sandbox = sandboxed.then(|| workspace_write_sandbox(root));
|
|
|
|
let result = file_system
|
|
.create_directory(
|
|
&PathUri::from_host_native_path(&nested_dir)?,
|
|
CreateDirectoryOptions {
|
|
recursive: true,
|
|
follow_symlinks,
|
|
},
|
|
sandbox.as_ref(),
|
|
)
|
|
.await;
|
|
#[cfg(windows)]
|
|
if is_unsupported_restricted_token_host(&result) {
|
|
return Ok(());
|
|
}
|
|
result.with_context(|| format!("mode={implementation}, sandboxed={sandboxed}"))?;
|
|
assert!(nested_dir.is_dir());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local, true, false ; "local_follow")]
|
|
#[test_case(FileSystemImplementation::Local, false, false ; "local_no_follow")]
|
|
#[test_case(FileSystemImplementation::Remote, true, false ; "remote_follow")]
|
|
#[test_case(FileSystemImplementation::Remote, false, false ; "remote_no_follow")]
|
|
#[cfg_attr(any(target_os = "linux", windows), test_case(FileSystemImplementation::Local, false, true ; "local_sandboxed_no_follow"))]
|
|
#[cfg_attr(any(target_os = "linux", windows), test_case(FileSystemImplementation::Remote, false, true ; "remote_sandboxed_no_follow"))]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_write_file_writes_bytes(
|
|
implementation: FileSystemImplementation,
|
|
follow_symlinks: bool,
|
|
sandboxed: bool,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let root = tmp.path().canonicalize()?;
|
|
let file_path = root.join("note.txt");
|
|
let sandbox = sandboxed.then(|| workspace_write_sandbox(root.clone()));
|
|
let result = file_system
|
|
.write_file(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
b"hello from trait".to_vec(),
|
|
WriteFileOptions { follow_symlinks },
|
|
sandbox.as_ref(),
|
|
)
|
|
.await;
|
|
#[cfg(windows)]
|
|
if is_unsupported_restricted_token_host(&result) {
|
|
return Ok(());
|
|
}
|
|
result.with_context(|| format!("mode={implementation}, sandboxed={sandboxed}"))?;
|
|
assert_eq!(std::fs::read(file_path)?, b"hello from trait");
|
|
|
|
let file_path = root.join("existing.txt");
|
|
std::fs::write(&file_path, b"before")?;
|
|
file_system
|
|
.write_file(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
b"after".to_vec(),
|
|
WriteFileOptions { follow_symlinks },
|
|
sandbox.as_ref(),
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}, sandboxed={sandboxed}"))?;
|
|
assert_eq!(std::fs::read(file_path)?, b"after");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn path_uri_join_and_parent_preserve_lexical_paths() -> Result<()> {
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let source_dir_uri = PathUri::from_host_native_path(&source_dir)?;
|
|
let joined_nested = source_dir_uri.join("nested/note.txt")?;
|
|
assert_eq!(
|
|
joined_nested,
|
|
PathUri::from_host_native_path(source_dir.join("nested").join("note.txt"))?
|
|
);
|
|
let joined_parent = joined_nested.parent();
|
|
assert_eq!(
|
|
joined_parent,
|
|
Some(PathUri::from_host_native_path(source_dir.join("nested"))?)
|
|
);
|
|
let joined_parent_traversal = source_dir_uri.join("../outside")?;
|
|
assert_eq!(
|
|
joined_parent_traversal,
|
|
PathUri::from_host_native_path(source_dir.join("../outside"))?
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_file_returns_bytes(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
std::fs::write(&file_path, "hello from trait")?;
|
|
|
|
let contents = file_system
|
|
.read_file(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
Default::default(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, b"hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_file_stream_returns_bounded_chunks(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("blocks.bin");
|
|
let contents = (0..FILE_READ_CHUNK_SIZE * 2 + 17)
|
|
.map(|index| (index % 251) as u8)
|
|
.collect::<Vec<_>>();
|
|
std::fs::write(&file_path, &contents)?;
|
|
|
|
let path = PathUri::from_host_native_path(file_path)?;
|
|
let sandbox = read_only_sandbox(tmp.path().to_path_buf());
|
|
for sandbox in [None, Some(&sandbox)] {
|
|
let chunks = file_system
|
|
.read_file_stream(&path, sandbox)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?
|
|
.try_collect::<Vec<_>>()
|
|
.await?;
|
|
|
|
assert!(
|
|
chunks
|
|
.iter()
|
|
.all(|chunk| !chunk.is_empty() && chunk.len() <= FILE_READ_CHUNK_SIZE)
|
|
);
|
|
assert_eq!(
|
|
chunks
|
|
.iter()
|
|
.flat_map(|chunk| chunk.iter().copied())
|
|
.collect::<Vec<_>>(),
|
|
contents
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_file_text_returns_string(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("note.txt");
|
|
std::fs::write(&file_path, "hello from trait")?;
|
|
|
|
let contents = file_system
|
|
.read_file_text(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
Default::default(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, "hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_copies_file(implementation: FileSystemImplementation) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_file = tmp.path().join("source.txt");
|
|
let copied_file = tmp.path().join("copy.txt");
|
|
std::fs::write(&source_file, "hello from trait")?;
|
|
|
|
file_system
|
|
.copy(
|
|
&PathUri::from_host_native_path(&source_file)?,
|
|
&PathUri::from_host_native_path(&copied_file)?,
|
|
CopyOptions { recursive: false },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(std::fs::read_to_string(copied_file)?, "hello from trait");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_copies_directory_recursively(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
let nested_file = nested_dir.join("note.txt");
|
|
let copied_dir = tmp.path().join("copied");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(&nested_file, "hello from trait")?;
|
|
|
|
file_system
|
|
.copy(
|
|
&PathUri::from_host_native_path(&source_dir)?,
|
|
&PathUri::from_host_native_path(&copied_dir)?,
|
|
CopyOptions { recursive: true },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
std::fs::read_to_string(copied_dir.join("nested").join("note.txt"))?,
|
|
"hello from trait"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_read_directory_lists_entries(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
std::fs::create_dir_all(source_dir.join("nested"))?;
|
|
std::fs::write(source_dir.join("root.txt"), "hello")?;
|
|
|
|
let mut entries = file_system
|
|
.read_directory(
|
|
&PathUri::from_host_native_path(&source_dir)?,
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
entries.sort_by(|left, right| left.file_name.cmp(&right.file_name));
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
ReadDirectoryEntry {
|
|
file_name: "nested".to_string(),
|
|
is_directory: true,
|
|
is_file: false,
|
|
},
|
|
ReadDirectoryEntry {
|
|
file_name: "root.txt".to_string(),
|
|
is_directory: false,
|
|
is_file: true,
|
|
},
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_walk_returns_a_bounded_tree(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(source_dir.join("root.txt"), "root")?;
|
|
std::fs::write(nested_dir.join("note.txt"), "nested")?;
|
|
|
|
let source_uri = PathUri::from_host_native_path(&source_dir)?;
|
|
let outcome = file_system
|
|
.walk(
|
|
&source_uri,
|
|
WalkOptions {
|
|
max_depth: 4,
|
|
max_directories: 10,
|
|
max_entries: 10,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
},
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
outcome,
|
|
WalkOutcome {
|
|
entries: vec![
|
|
WalkEntry {
|
|
path: PathUri::from_host_native_path(&nested_dir)?,
|
|
kind: WalkEntryKind::Directory,
|
|
},
|
|
WalkEntry {
|
|
path: PathUri::from_host_native_path(source_dir.join("root.txt"))?,
|
|
kind: WalkEntryKind::File,
|
|
},
|
|
WalkEntry {
|
|
path: PathUri::from_host_native_path(nested_dir.join("note.txt"))?,
|
|
kind: WalkEntryKind::File,
|
|
},
|
|
],
|
|
errors: Vec::new(),
|
|
truncated: false,
|
|
}
|
|
);
|
|
|
|
let root_entries = vec![
|
|
WalkEntry {
|
|
path: PathUri::from_host_native_path(&nested_dir)?,
|
|
kind: WalkEntryKind::Directory,
|
|
},
|
|
WalkEntry {
|
|
path: PathUri::from_host_native_path(source_dir.join("root.txt"))?,
|
|
kind: WalkEntryKind::File,
|
|
},
|
|
];
|
|
let shallow = file_system
|
|
.walk(
|
|
&source_uri,
|
|
WalkOptions {
|
|
max_depth: 0,
|
|
max_directories: 10,
|
|
max_entries: 10,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
},
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
shallow,
|
|
WalkOutcome {
|
|
entries: root_entries.clone(),
|
|
errors: Vec::new(),
|
|
truncated: false,
|
|
}
|
|
);
|
|
|
|
let directory_bounded = file_system
|
|
.walk(
|
|
&source_uri,
|
|
WalkOptions {
|
|
max_depth: 4,
|
|
max_directories: 1,
|
|
max_entries: 10,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
},
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
directory_bounded,
|
|
WalkOutcome {
|
|
entries: root_entries,
|
|
errors: Vec::new(),
|
|
truncated: true,
|
|
}
|
|
);
|
|
|
|
let bounded = file_system
|
|
.walk(
|
|
&source_uri,
|
|
WalkOptions {
|
|
max_depth: 4,
|
|
max_directories: 10,
|
|
max_entries: 1,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
},
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
bounded,
|
|
WalkOutcome {
|
|
entries: vec![WalkEntry {
|
|
path: PathUri::from_host_native_path(&nested_dir)?,
|
|
kind: WalkEntryKind::Directory,
|
|
}],
|
|
errors: Vec::new(),
|
|
truncated: true,
|
|
}
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_walk_handles_invalid_roots_and_limits(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
let tmp = TempDir::new()?;
|
|
let file_path = tmp.path().join("file.txt");
|
|
std::fs::write(&file_path, "contents")?;
|
|
let missing = PathUri::from_host_native_path(tmp.path().join("missing"))?;
|
|
let options = WalkOptions {
|
|
max_depth: 8,
|
|
max_directories: 100,
|
|
max_entries: 100,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
};
|
|
|
|
let outcome = file_system
|
|
.walk(
|
|
&PathUri::from_host_native_path(file_path)?,
|
|
options,
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(outcome, WalkOutcome::default());
|
|
|
|
let error = file_system
|
|
.walk(&missing, options, /*sandbox*/ None)
|
|
.await
|
|
.expect_err("a missing root must fail");
|
|
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
|
|
|
|
let zero_limit_message = "filesystem walk limits must be greater than zero";
|
|
let excessive_limit_message = format!(
|
|
"filesystem walk limits exceed maximums: depth={MAX_WALK_DEPTH}, directories={MAX_WALK_DIRECTORIES}, entries={MAX_WALK_ENTRIES}"
|
|
);
|
|
for (max_depth, max_directories, max_entries, message) in [
|
|
(8, 0, 100, zero_limit_message),
|
|
(8, 100, 0, zero_limit_message),
|
|
(
|
|
MAX_WALK_DEPTH + 1,
|
|
100,
|
|
100,
|
|
excessive_limit_message.as_str(),
|
|
),
|
|
(
|
|
8,
|
|
MAX_WALK_DIRECTORIES + 1,
|
|
100,
|
|
excessive_limit_message.as_str(),
|
|
),
|
|
(
|
|
8,
|
|
100,
|
|
MAX_WALK_ENTRIES + 1,
|
|
excessive_limit_message.as_str(),
|
|
),
|
|
] {
|
|
let options = WalkOptions {
|
|
max_depth,
|
|
max_directories,
|
|
max_entries,
|
|
..options
|
|
};
|
|
// Invalid limits take precedence over the missing root.
|
|
let error = file_system
|
|
.walk(&missing, options, /*sandbox*/ None)
|
|
.await
|
|
.expect_err("invalid walk limits must fail");
|
|
assert_eq!(
|
|
(error.kind(), error.to_string()),
|
|
(std::io::ErrorKind::InvalidInput, message.to_owned()),
|
|
"mode={implementation}, options={options:?}",
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_walk_honors_read_sandbox(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let file_path = source_dir.join("note.txt");
|
|
std::fs::create_dir_all(&source_dir)?;
|
|
std::fs::write(&file_path, "sandboxed")?;
|
|
let sandbox = read_only_sandbox(source_dir.clone());
|
|
|
|
let outcome = file_system
|
|
.walk(
|
|
&PathUri::from_host_native_path(&source_dir)?,
|
|
WalkOptions {
|
|
max_depth: 1,
|
|
max_directories: 2,
|
|
max_entries: 2,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
},
|
|
Some(&sandbox),
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
outcome,
|
|
WalkOutcome {
|
|
entries: vec![WalkEntry {
|
|
path: PathUri::from_host_native_path(file_path)?,
|
|
kind: WalkEntryKind::File,
|
|
}],
|
|
errors: Vec::new(),
|
|
truncated: false,
|
|
}
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_remove_removes_directory(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let directory_path = tmp.path().join("remove-me");
|
|
std::fs::create_dir_all(directory_path.join("nested"))?;
|
|
|
|
file_system
|
|
.remove(
|
|
&PathUri::from_host_native_path(&directory_path)?,
|
|
RemoveOptions {
|
|
recursive: true,
|
|
force: true,
|
|
follow_symlinks: true,
|
|
},
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert!(!directory_path.exists());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local, false ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote, false ; "remote")]
|
|
#[cfg_attr(any(target_os = "linux", windows), test_case(FileSystemImplementation::Local, true ; "local_sandboxed"))]
|
|
#[cfg_attr(any(target_os = "linux", windows), test_case(FileSystemImplementation::Remote, true ; "remote_sandboxed"))]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_remove_no_follow_removes_file_and_empty_directory(
|
|
implementation: FileSystemImplementation,
|
|
sandboxed: bool,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
let tmp = TempDir::new()?;
|
|
let root = tmp.path().canonicalize()?;
|
|
let sandbox = sandboxed.then(|| workspace_write_sandbox(root.clone()));
|
|
let options = RemoveOptions {
|
|
recursive: false,
|
|
force: false,
|
|
follow_symlinks: false,
|
|
};
|
|
|
|
let file_path = root.join("remove-me.txt");
|
|
std::fs::write(&file_path, b"remove")?;
|
|
let result = file_system
|
|
.remove(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
options,
|
|
sandbox.as_ref(),
|
|
)
|
|
.await;
|
|
#[cfg(windows)]
|
|
if is_unsupported_restricted_token_host(&result) {
|
|
return Ok(());
|
|
}
|
|
result.with_context(|| format!("mode={implementation}, sandboxed={sandboxed}"))?;
|
|
assert!(!file_path.exists());
|
|
|
|
let directory_path = root.join("remove-me");
|
|
std::fs::create_dir(&directory_path)?;
|
|
file_system
|
|
.remove(
|
|
&PathUri::from_host_native_path(&directory_path)?,
|
|
options,
|
|
sandbox.as_ref(),
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}, sandboxed={sandboxed}"))?;
|
|
assert!(!directory_path.exists());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_write_file_reports_missing_parent(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let missing_parent_path = tmp.path().join("missing").join("note.txt");
|
|
|
|
let error = match file_system
|
|
.write_file(
|
|
&PathUri::from_host_native_path(&missing_parent_path)?,
|
|
b"hello from trait".to_vec(),
|
|
Default::default(),
|
|
/*sandbox*/ None,
|
|
)
|
|
.await
|
|
{
|
|
Ok(()) => anyhow::bail!("write should fail when parent directory is absent"),
|
|
Err(error) => error,
|
|
};
|
|
assert_eq!(
|
|
error.kind(),
|
|
std::io::ErrorKind::NotFound,
|
|
"mode={implementation}"
|
|
);
|
|
assert!(!missing_parent_path.exists(), "mode={implementation}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_rejects_directory_without_recursive(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
std::fs::create_dir_all(&source_dir)?;
|
|
|
|
let error = file_system
|
|
.copy(
|
|
&PathUri::from_host_native_path(&source_dir)?,
|
|
&PathUri::from_host_native_path(tmp.path().join("dest"))?,
|
|
CopyOptions { recursive: false },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await;
|
|
let error = error.expect_err("copying a directory without recursion should fail");
|
|
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"fs/copy requires recursive: true when sourcePath is a directory"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_sandboxed_metadata_and_read_allow_readable_root(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let allowed_dir = tmp.path().join("allowed");
|
|
let file_path = allowed_dir.join("note.txt");
|
|
std::fs::create_dir_all(&allowed_dir)?;
|
|
std::fs::write(&file_path, "sandboxed hello")?;
|
|
let sandbox = read_only_sandbox(allowed_dir);
|
|
|
|
let metadata = file_system
|
|
.get_metadata(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
Default::default(),
|
|
Some(&sandbox),
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(
|
|
metadata,
|
|
FileMetadata {
|
|
is_directory: false,
|
|
is_file: true,
|
|
is_symlink: false,
|
|
size: 15,
|
|
created_at_ms: metadata.created_at_ms,
|
|
modified_at_ms: metadata.modified_at_ms,
|
|
}
|
|
);
|
|
|
|
let contents = file_system
|
|
.read_file(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
Default::default(),
|
|
Some(&sandbox),
|
|
)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, b"sandboxed hello");
|
|
|
|
let chunks = file_system
|
|
.read_file_stream(&PathUri::from_host_native_path(&file_path)?, Some(&sandbox))
|
|
.await
|
|
.with_context(|| format!("stream mode={implementation}"))?
|
|
.try_collect::<Vec<_>>()
|
|
.await?;
|
|
assert_eq!(chunks.concat(), b"sandboxed hello");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn sandboxed_file_operations_cannot_read_helper_siblings() -> Result<()> {
|
|
let helper_paths = crate::common::exec_server::test_codex_helper_paths()?;
|
|
let root = TempDir::new()?;
|
|
let runtime_dir = root.path().join("runtime");
|
|
let workspace = root.path().join("workspace");
|
|
std::fs::create_dir(&runtime_dir)?;
|
|
std::fs::create_dir(&workspace)?;
|
|
|
|
let helper = runtime_dir.join("codex-test-helper");
|
|
std::fs::hard_link(&helper_paths.codex_exe, &helper)
|
|
.or_else(|_| std::fs::copy(&helper_paths.codex_exe, &helper).map(|_| ()))?;
|
|
let linux_sandbox = if helper_paths.codex_linux_sandbox_exe.is_some() {
|
|
let alias = runtime_dir.join("codex-linux-sandbox");
|
|
std::fs::hard_link(&helper, &alias)
|
|
.or_else(|_| std::fs::copy(&helper, &alias).map(|_| ()))?;
|
|
Some(alias)
|
|
} else {
|
|
None
|
|
};
|
|
let file_system =
|
|
LocalFileSystem::with_runtime_paths(ExecServerRuntimePaths::new(helper, linux_sandbox)?);
|
|
|
|
let sibling = runtime_dir.join("credentials.json");
|
|
std::fs::write(&sibling, "secret")?;
|
|
let escaping_link = workspace.join("credentials-link");
|
|
std::os::unix::fs::symlink(&sibling, &escaping_link)?;
|
|
let sandbox = workspace_write_sandbox(workspace.clone());
|
|
let allowed_file = workspace.join("allowed.txt");
|
|
std::fs::write(&allowed_file, b"allowed")?;
|
|
let allowed_contents = file_system
|
|
.read_file(
|
|
&PathUri::from_host_native_path(&allowed_file)?,
|
|
Default::default(),
|
|
Some(&sandbox),
|
|
)
|
|
.await?;
|
|
assert_eq!(allowed_contents, b"allowed");
|
|
|
|
#[cfg(target_os = "macos")]
|
|
assert!(
|
|
file_system
|
|
.read_directory(
|
|
&PathUri::from_host_native_path("/Applications")?,
|
|
Some(&sandbox)
|
|
)
|
|
.await
|
|
.is_err(),
|
|
"filesystem helpers should not inherit the normal process sandbox's /Applications access"
|
|
);
|
|
|
|
let sibling_uri = PathUri::from_host_native_path(&sibling)?;
|
|
let destination = PathUri::from_host_native_path(workspace.join("copied.json"))?;
|
|
|
|
for path in [sibling, escaping_link] {
|
|
let path = PathUri::from_host_native_path(path)?;
|
|
assert!(
|
|
file_system
|
|
.read_file(&path, Default::default(), Some(&sandbox))
|
|
.await
|
|
.is_err(),
|
|
"sandboxed read unexpectedly accessed helper sibling {path}"
|
|
);
|
|
assert!(
|
|
file_system
|
|
.read_file_stream(&path, Some(&sandbox))
|
|
.await
|
|
.is_err(),
|
|
"sandboxed streaming unexpectedly accessed helper sibling {path}"
|
|
);
|
|
}
|
|
assert!(
|
|
file_system
|
|
.copy(
|
|
&sibling_uri,
|
|
&destination,
|
|
CopyOptions { recursive: false },
|
|
Some(&sandbox),
|
|
)
|
|
.await
|
|
.is_err(),
|
|
"sandboxed copy unexpectedly accessed helper sibling {sibling_uri}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn assert_canonicalize_resolves_directory_alias(
|
|
implementation: FileSystemImplementation,
|
|
create_directory_alias: impl FnOnce(&Path, &Path) -> Result<()>,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
let file_path = nested_dir.join("note.txt");
|
|
let alias_dir = tmp.path().join("source-alias");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(&file_path, "canonical hello")?;
|
|
create_directory_alias(&source_dir, &alias_dir)?;
|
|
|
|
let requested_path = PathUri::from_host_native_path(alias_dir.join("nested").join("note.txt"))?;
|
|
let expected_path = PathUri::from_host_native_path(std::fs::canonicalize(&file_path)?)?;
|
|
assert_ne!(requested_path, expected_path);
|
|
|
|
let canonical_path = file_system
|
|
.canonicalize(&requested_path, /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(canonical_path, expected_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn assert_sandboxed_canonicalize_resolves_directory_alias(
|
|
implementation: FileSystemImplementation,
|
|
create_directory_alias: impl FnOnce(&Path, &Path) -> Result<()>,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
let nested_dir = source_dir.join("nested");
|
|
let file_path = nested_dir.join("note.txt");
|
|
let alias_dir = tmp.path().join("source-alias");
|
|
std::fs::create_dir_all(&nested_dir)?;
|
|
std::fs::write(&file_path, "sandboxed canonical hello")?;
|
|
create_directory_alias(&source_dir, &alias_dir)?;
|
|
let sandbox = read_only_sandbox(tmp.path().to_path_buf());
|
|
|
|
let requested_path = PathUri::from_host_native_path(alias_dir.join("nested").join("note.txt"))?;
|
|
let expected_path = PathUri::from_host_native_path(std::fs::canonicalize(&file_path)?)?;
|
|
assert_ne!(requested_path, expected_path);
|
|
|
|
let canonical_path = file_system
|
|
.canonicalize(&requested_path, Some(&sandbox))
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(canonical_path, expected_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Verifies that effective additional permissions extend a read-only sandbox with a writable root.
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[cfg_attr(
|
|
windows,
|
|
ignore = "Windows restricted-token sandbox cannot enforce split writable roots"
|
|
)]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_sandboxed_write_allows_additional_write_root(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let readable_dir = tmp.path().join("readable");
|
|
let writable_dir = tmp.path().join("writable");
|
|
let file_path = writable_dir.join("note.txt");
|
|
std::fs::create_dir_all(&readable_dir)?;
|
|
std::fs::create_dir_all(&writable_dir)?;
|
|
|
|
let mut sandbox = read_only_sandbox(readable_dir);
|
|
let additional_permissions = AdditionalPermissionProfile {
|
|
network: None,
|
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
|
/*read*/ None,
|
|
Some(vec![absolute_path(writable_dir)]),
|
|
)),
|
|
};
|
|
let native_permissions: PermissionProfile = sandbox.permissions.clone().try_into()?;
|
|
let file_system_policy = effective_file_system_sandbox_policy(
|
|
&native_permissions.file_system_sandbox_policy(),
|
|
Some(&additional_permissions),
|
|
);
|
|
let network_policy = effective_network_sandbox_policy(
|
|
native_permissions.network_sandbox_policy(),
|
|
Some(&additional_permissions),
|
|
);
|
|
sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
native_permissions.enforcement(),
|
|
&file_system_policy,
|
|
network_policy,
|
|
)
|
|
.into();
|
|
|
|
file_system
|
|
.write_file(
|
|
&PathUri::from_host_native_path(&file_path)?,
|
|
b"created".to_vec(),
|
|
Default::default(),
|
|
Some(&sandbox),
|
|
)
|
|
.await
|
|
.with_context(|| format!("write file through additional root mode={implementation}"))?;
|
|
assert_eq!(std::fs::read(&file_path)?, b"created");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn file_system_copy_rejects_copying_directory_into_descendant(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let file_system = context.file_system;
|
|
|
|
let tmp = TempDir::new()?;
|
|
let source_dir = tmp.path().join("source");
|
|
std::fs::create_dir_all(source_dir.join("nested"))?;
|
|
|
|
let error = file_system
|
|
.copy(
|
|
&PathUri::from_host_native_path(&source_dir)?,
|
|
&PathUri::from_host_native_path(source_dir.join("nested").join("copy"))?,
|
|
CopyOptions { recursive: true },
|
|
/*sandbox*/ None,
|
|
)
|
|
.await;
|
|
let error = error.expect_err("copying a directory into itself should fail");
|
|
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"fs/copy cannot copy a directory to itself or one of its descendants"
|
|
);
|
|
|
|
Ok(())
|
|
}
|