use anyhow::Context; use anyhow::Result; use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::FileMetadata; use codex_exec_server::ReadDirectoryEntry; use codex_exec_server::RemoveOptions; 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 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; 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_path(&file_path)?, /*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_path(&directory_path)?, /*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 ; "local")] #[test_case(FileSystemImplementation::Remote ; "remote")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn file_system_create_directory_creates_nested_directories( implementation: FileSystemImplementation, ) -> Result<()> { let context = create_file_system_context(implementation).await?; let file_system = context.file_system; let tmp = TempDir::new()?; let nested_dir = tmp.path().join("source").join("nested"); file_system .create_directory( &PathUri::from_path(&nested_dir)?, CreateDirectoryOptions { recursive: true }, /*sandbox*/ None, ) .await .with_context(|| format!("mode={implementation}"))?; assert!(nested_dir.is_dir()); 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_writes_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"); file_system .write_file( &PathUri::from_path(&file_path)?, b"hello from trait".to_vec(), /*sandbox*/ None, ) .await .with_context(|| format!("mode={implementation}"))?; assert_eq!(std::fs::read(file_path)?, b"hello from trait"); 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_path(&source_dir)?; let joined_nested = source_dir_uri.join("nested/note.txt")?; assert_eq!( joined_nested, PathUri::from_path(source_dir.join("nested").join("note.txt"))? ); let joined_parent = joined_nested.parent(); assert_eq!( joined_parent, Some(PathUri::from_path(source_dir.join("nested"))?) ); let joined_parent_traversal = source_dir_uri.join("../outside")?; assert_eq!( joined_parent_traversal, PathUri::from_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_path(&file_path)?, /*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_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_path(&file_path)?, /*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_path(&source_file)?, &PathUri::from_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_path(&source_dir)?, &PathUri::from_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_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_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_path(&directory_path)?, RemoveOptions { recursive: true, force: true, }, /*sandbox*/ None, ) .await .with_context(|| format!("mode={implementation}"))?; 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_path(&missing_parent_path)?, b"hello from trait".to_vec(), /*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_path(&source_dir)?, &PathUri::from_path(tmp.path().join("dest"))?, CopyOptions { recursive: false }, /*sandbox*/ None, ) .await; let error = match error { Ok(()) => panic!("copy should fail"), Err(error) => error, }; 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_path(&file_path)?, 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_path(&file_path)?, Some(&sandbox)) .await .with_context(|| format!("mode={implementation}"))?; assert_eq!(contents, b"sandboxed hello"); 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_path(alias_dir.join("nested").join("note.txt"))?; let expected_path = PathUri::from_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_path(alias_dir.join("nested").join("note.txt"))?; let expected_path = PathUri::from_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")] #[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 file_system_policy = effective_file_system_sandbox_policy( &sandbox.permissions.file_system_sandbox_policy(), Some(&additional_permissions), ); let network_policy = effective_network_sandbox_policy( sandbox.permissions.network_sandbox_policy(), Some(&additional_permissions), ); sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement( sandbox.permissions.enforcement(), &file_system_policy, network_policy, ); file_system .write_file( &PathUri::from_path(&file_path)?, b"created".to_vec(), 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_path(&source_dir)?, &PathUri::from_path(source_dir.join("nested").join("copy"))?, CopyOptions { recursive: true }, /*sandbox*/ None, ) .await; let error = match error { Ok(()) => panic!("copy should fail"), Err(error) => error, }; 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(()) }