mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## What changed Add `EnvironmentAccess` and `FileSystemEnvironmentAccessor` to expose filesystem operations with a captured sandbox configuration, without allowing consumers to extract the filesystem or select another sandbox. Include a text-reading helper and an explicit unrestricted constructor. Provide opaque cache keys that compare filesystem identity and captured permissions without keeping the filesystem alive. Allow opened read streams to outlive the accessor. Export the new APIs through `codex-exec-server` and add `Environment::filesystem_ref()` for borrowing the shared filesystem. ## Testing Add local and remote coverage for text reads through `EnvironmentAccess`, streams surviving accessor disposal, and cache keys distinguishing changed permissions or a replacement filesystem. GitOrigin-RevId: 4f6787ed9ba0fb94adea2f31716c4d3132fed07d
1371 lines
47 KiB
Rust
1371 lines
47 KiB
Rust
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use codex_exec_server::CopyOptions;
|
|
use codex_exec_server::CreateDirectoryOptions;
|
|
use codex_exec_server::EnvironmentAccess;
|
|
use codex_exec_server::EnvironmentAccessExt;
|
|
#[cfg(unix)]
|
|
use codex_exec_server::ExecServerRuntimePaths;
|
|
use codex_exec_server::ExecutorFileSystem;
|
|
use codex_exec_server::FILE_READ_CHUNK_SIZE;
|
|
use codex_exec_server::FileMetadata;
|
|
use codex_exec_server::FileSystemEnvironmentAccessor;
|
|
use codex_exec_server::FileSystemSandboxContext;
|
|
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_protocol::permissions::FileSystemAccessMode;
|
|
use codex_protocol::permissions::FileSystemPath;
|
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
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 std::sync::Arc;
|
|
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 policy = sandbox.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 file_path = PathUri::from_host_native_path(file_path)?;
|
|
|
|
let contents = file_system
|
|
.read_file_text(&file_path, Default::default(), /*sandbox*/ None)
|
|
.await
|
|
.with_context(|| format!("mode={implementation}"))?;
|
|
assert_eq!(contents, "hello from trait");
|
|
|
|
let accessor = FileSystemEnvironmentAccessor::unrestricted(&file_system);
|
|
let access: &dyn EnvironmentAccess = &accessor;
|
|
assert_eq!(
|
|
access
|
|
.read_file_text(&file_path, Default::default())
|
|
.await?,
|
|
contents
|
|
);
|
|
|
|
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(())
|
|
}
|
|
|
|
/// Full-disk read permission allows all read APIs without a helper; mutations still require one.
|
|
#[tokio::test]
|
|
async fn file_system_full_disk_read_skips_sandbox_only_for_reads() -> Result<()> {
|
|
let file_system = LocalFileSystem::unsandboxed();
|
|
let tmp = TempDir::new()?;
|
|
let file = tmp.path().join("note.txt");
|
|
std::fs::write(&file, b"note")?;
|
|
let file_uri = PathUri::from_host_native_path(&file)?;
|
|
let root_uri = PathUri::from_host_native_path(tmp.path())?;
|
|
let destination = PathUri::from_host_native_path(tmp.path().join("destination"))?;
|
|
let sandbox = FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::read_only(),
|
|
root_uri.clone(),
|
|
);
|
|
|
|
assert_eq!(
|
|
file_system.canonicalize(&file_uri, Some(&sandbox)).await?,
|
|
PathUri::from_host_native_path(std::fs::canonicalize(&file)?)?
|
|
);
|
|
assert_eq!(
|
|
file_system
|
|
.read_file(&file_uri, Default::default(), Some(&sandbox))
|
|
.await?,
|
|
b"note"
|
|
);
|
|
assert_eq!(
|
|
file_system
|
|
.read_file_stream(&file_uri, Some(&sandbox))
|
|
.await?
|
|
.try_collect::<Vec<_>>()
|
|
.await?
|
|
.concat(),
|
|
b"note"
|
|
);
|
|
let metadata = file_system
|
|
.get_metadata(&file_uri, Default::default(), Some(&sandbox))
|
|
.await?;
|
|
assert_eq!(
|
|
metadata,
|
|
FileMetadata {
|
|
is_directory: false,
|
|
is_file: true,
|
|
is_symlink: false,
|
|
size: 4,
|
|
created_at_ms: metadata.created_at_ms,
|
|
modified_at_ms: metadata.modified_at_ms,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
file_system
|
|
.read_directory(&root_uri, Some(&sandbox))
|
|
.await?,
|
|
vec![ReadDirectoryEntry {
|
|
file_name: "note.txt".to_string(),
|
|
is_directory: false,
|
|
is_file: true,
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
file_system
|
|
.walk(
|
|
&root_uri,
|
|
WalkOptions {
|
|
max_depth: 1,
|
|
max_directories: 2,
|
|
max_entries: 2,
|
|
follow_directory_symlinks: false,
|
|
prune_hidden_directories: false,
|
|
},
|
|
Some(&sandbox),
|
|
)
|
|
.await?,
|
|
WalkOutcome {
|
|
entries: vec![WalkEntry {
|
|
path: file_uri.clone(),
|
|
kind: WalkEntryKind::File,
|
|
}],
|
|
errors: Vec::new(),
|
|
truncated: false,
|
|
}
|
|
);
|
|
|
|
let mutations = [
|
|
file_system
|
|
.write_file(
|
|
&file_uri,
|
|
b"changed".to_vec(),
|
|
Default::default(),
|
|
Some(&sandbox),
|
|
)
|
|
.await,
|
|
file_system
|
|
.create_directory(
|
|
&destination,
|
|
CreateDirectoryOptions {
|
|
recursive: false,
|
|
follow_symlinks: true,
|
|
},
|
|
Some(&sandbox),
|
|
)
|
|
.await,
|
|
file_system
|
|
.remove(
|
|
&file_uri,
|
|
RemoveOptions {
|
|
recursive: false,
|
|
force: false,
|
|
follow_symlinks: true,
|
|
},
|
|
Some(&sandbox),
|
|
)
|
|
.await,
|
|
file_system
|
|
.copy(
|
|
&file_uri,
|
|
&destination,
|
|
CopyOptions { recursive: false },
|
|
Some(&sandbox),
|
|
)
|
|
.await,
|
|
];
|
|
for mutation in mutations {
|
|
let error = mutation.expect_err("restricted writes must require the sandbox");
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"sandboxed filesystem operations require configured runtime paths"
|
|
);
|
|
}
|
|
assert_eq!(std::fs::read(&file)?, b"note");
|
|
assert!(!tmp.path().join("destination").exists());
|
|
Ok(())
|
|
}
|
|
|
|
/// A stream already opened under one environment remains usable after its accessor is dropped;
|
|
/// cache keys still distinguish a new filesystem or changed captured permissions.
|
|
#[test_case(FileSystemImplementation::Local ; "local")]
|
|
#[test_case(FileSystemImplementation::Remote ; "remote")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn environment_accessor_stream_outlives_permissions_snapshot(
|
|
implementation: FileSystemImplementation,
|
|
) -> Result<()> {
|
|
let context = create_file_system_context(implementation).await?;
|
|
let tmp = TempDir::new()?;
|
|
let file = tmp.path().join("note.txt");
|
|
std::fs::write(&file, b"opened in the previous turn")?;
|
|
let cwd = PathUri::from_host_native_path(tmp.path())?;
|
|
let file = PathUri::from_host_native_path(file)?;
|
|
let sandbox = FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::read_only(),
|
|
cwd.clone(),
|
|
);
|
|
|
|
let (stream, key) = {
|
|
let access = FileSystemEnvironmentAccessor::new(&context.file_system, sandbox.clone());
|
|
(access.read_file_stream(&file).await?, access.cache_key())
|
|
};
|
|
let same = FileSystemEnvironmentAccessor::new(&context.file_system, sandbox.clone());
|
|
assert_eq!(key, same.cache_key());
|
|
let changed = FileSystemEnvironmentAccessor::new(
|
|
&context.file_system,
|
|
FileSystemSandboxContext::from_permission_profile(PermissionProfile::Disabled, cwd),
|
|
);
|
|
assert_ne!(key, changed.cache_key());
|
|
let other_file_system: Arc<dyn ExecutorFileSystem> = Arc::new(LocalFileSystem::unsandboxed());
|
|
let replacement = FileSystemEnvironmentAccessor::new(&other_file_system, sandbox);
|
|
assert_ne!(key, replacement.cache_key());
|
|
assert_eq!(
|
|
stream.try_collect::<Vec<_>>().await?.concat(),
|
|
b"opened in the previous turn"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// A read deny keeps both ordinary and streaming reads from falling back to direct access.
|
|
#[tokio::test]
|
|
async fn file_system_restricted_reads_require_sandbox() -> Result<()> {
|
|
let file_system = LocalFileSystem::unsandboxed();
|
|
let tmp = TempDir::new()?;
|
|
let file = tmp.path().join("note.txt");
|
|
std::fs::write(&file, b"note")?;
|
|
let file_uri = PathUri::from_host_native_path(&file)?;
|
|
let sandbox = FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::from_runtime_permissions(
|
|
&FileSystemSandboxPolicy::restricted(vec![
|
|
FileSystemSandboxEntry::new(
|
|
FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
FileSystemAccessMode::Read,
|
|
),
|
|
FileSystemSandboxEntry::new(absolute_path(file).into(), FileSystemAccessMode::Deny),
|
|
]),
|
|
NetworkSandboxPolicy::Restricted,
|
|
),
|
|
PathUri::from_host_native_path(tmp.path())?,
|
|
);
|
|
|
|
let read = file_system
|
|
.read_file(&file_uri, Default::default(), Some(&sandbox))
|
|
.await
|
|
.map(|_| ());
|
|
let stream = file_system
|
|
.read_file_stream(&file_uri, Some(&sandbox))
|
|
.await
|
|
.map(|_| ());
|
|
for result in [read, stream] {
|
|
let error = result.expect_err("restricted reads must require the sandbox");
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"sandboxed filesystem operations require configured runtime paths"
|
|
);
|
|
}
|
|
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 permissions = &sandbox.permissions;
|
|
let file_system_policy = effective_file_system_sandbox_policy(
|
|
&permissions.file_system_sandbox_policy(),
|
|
Some(&additional_permissions),
|
|
);
|
|
let network_policy = effective_network_sandbox_policy(
|
|
permissions.network_sandbox_policy(),
|
|
Some(&additional_permissions),
|
|
);
|
|
sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
permissions.enforcement(),
|
|
&file_system_policy,
|
|
network_policy,
|
|
);
|
|
|
|
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(())
|
|
}
|