[codex] migrate ExecutorFileSystem paths to PathUri (#27424)

## Why

We're moving exec-server to use PathUri for its internal path
representations.

## What

Move `ExecutorFileSystem` APIs to use `PathUri` instead of
`AbsolutePathBuf`. Future changes will convert higher-level parts of
exec-server.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-11 11:44:18 -07:00
committed by GitHub
Unverified
parent 4a05d3b282
commit b2a4e3be27
52 changed files with 1126 additions and 495 deletions
+1
View File
@@ -882,6 +882,7 @@ mod tests {
std::env::current_exe().expect("current exe").as_path(),
)
.expect("absolute current exe");
let path = codex_utils_path_uri::PathUri::from_abs_path(&path).expect("path URI");
let sandbox = crate::FileSystemSandboxContext::from_permission_profile(
codex_protocol::models::PermissionProfile::from_runtime_permissions(
&codex_protocol::permissions::FileSystemSandboxPolicy::restricted(Vec::new()),
+90 -21
View File
@@ -189,8 +189,10 @@ pub(crate) async fn run_direct_request(
let file_system = DirectFileSystem;
match request {
FsHelperRequest::ReadFile(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let data = file_system
.read_file(&params.path, /*sandbox*/ None)
.read_file(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::ReadFile(FsReadFileResponse {
@@ -198,21 +200,25 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::WriteFile(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
invalid_request(format!(
"{FS_WRITE_FILE_METHOD} requires valid base64 dataBase64: {err}"
))
})?;
file_system
.write_file(&params.path, bytes, /*sandbox*/ None)
.write_file(&path, bytes, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::WriteFile(FsWriteFileResponse {}))
}
FsHelperRequest::CreateDirectory(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
file_system
.create_directory(
&params.path,
&path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
@@ -225,8 +231,10 @@ pub(crate) async fn run_direct_request(
))
}
FsHelperRequest::GetMetadata(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let metadata = file_system
.get_metadata(&params.path, /*sandbox*/ None)
.get_metadata(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::GetMetadata(FsGetMetadataResponse {
@@ -238,17 +246,22 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::Canonicalize(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let path = file_system
.canonicalize(&params.path, /*sandbox*/ None)
.canonicalize(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
let path = path.to_abs_path().map_err(map_fs_error)?;
Ok(FsHelperPayload::Canonicalize(FsCanonicalizeResponse {
path,
}))
}
FsHelperRequest::ReadDirectory(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let entries = file_system
.read_directory(&params.path, /*sandbox*/ None)
.read_directory(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?
.into_iter()
@@ -263,9 +276,11 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::Remove(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
file_system
.remove(
&params.path,
&path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
@@ -277,10 +292,15 @@ pub(crate) async fn run_direct_request(
Ok(FsHelperPayload::Remove(FsRemoveResponse {}))
}
FsHelperRequest::Copy(params) => {
let source_path = codex_utils_path_uri::PathUri::from_abs_path(&params.source_path)
.map_err(map_fs_error)?;
let destination_path =
codex_utils_path_uri::PathUri::from_abs_path(&params.destination_path)
.map_err(map_fs_error)?;
file_system
.copy(
&params.source_path,
&params.destination_path,
&source_path,
&destination_path,
CopyOptions {
recursive: params.recursive,
},
@@ -305,23 +325,72 @@ fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
#[cfg(test)]
mod tests {
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
use super::*;
#[test]
fn helper_requests_use_fs_method_names() -> serde_json::Result<()> {
assert_eq!(
serde_json::to_value(FsHelperRequest::WriteFile(FsWriteFileParams {
path: std::env::current_dir()
.expect("cwd")
.join("file")
.as_path()
.try_into()
.expect("absolute path"),
fn helper_protocol_keeps_native_absolute_paths() -> serde_json::Result<()> {
let local_path =
AbsolutePathBuf::from_absolute_path(std::env::current_dir().expect("cwd").join("file"))
.expect("absolute path");
#[cfg(not(windows))]
let paths = [local_path];
#[cfg(windows)]
let paths = [
local_path,
AbsolutePathBuf::from_absolute_path(r"\\server\share\file").expect("absolute UNC path"),
];
for path in paths {
let expected_path = path.to_string_lossy().into_owned();
let request = serde_json::to_value(FsHelperRequest::WriteFile(FsWriteFileParams {
path: path.clone(),
data_base64: String::new(),
sandbox: None,
}))?["operation"],
FS_WRITE_FILE_METHOD,
);
}))?;
assert_eq!(
request,
json!({
"operation": FS_WRITE_FILE_METHOD,
"params": {
"path": expected_path.as_str(),
"dataBase64": "",
"sandbox": null,
},
}),
);
let request_path = request["params"]["path"]
.as_str()
.expect("request path should be a string");
assert_eq!(request_path, expected_path);
assert!(!request_path.starts_with("file:"));
let response = serde_json::to_value(FsHelperResponse::Ok(
FsHelperPayload::Canonicalize(FsCanonicalizeResponse { path }),
))?;
assert_eq!(
response,
json!({
"status": "ok",
"payload": {
"operation": FS_CANONICALIZE_METHOD,
"response": {
"path": expected_path.as_str(),
},
},
}),
);
let response_path = response["payload"]["response"]["path"]
.as_str()
.expect("canonicalize response path should be a string");
assert_eq!(response_path, expected_path);
assert!(!response_path.starts_with("file:"));
}
Ok(())
}
}
+47 -69
View File
@@ -1,5 +1,6 @@
use async_trait::async_trait;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
@@ -81,28 +82,16 @@ impl LocalFileSystem {
impl ExecutorFileSystem for LocalFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.canonicalize(path, sandbox).await
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
self.unsandboxed.join(base_path, path).await
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
self.unsandboxed.parent(path).await
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
@@ -111,7 +100,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -121,7 +110,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -131,7 +120,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
@@ -140,7 +129,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
@@ -149,7 +138,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -159,8 +148,8 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -175,28 +164,16 @@ impl ExecutorFileSystem for LocalFileSystem {
impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
reject_platform_sandbox_context(sandbox)?;
self.file_system.canonicalize(path, /*sandbox*/ None).await
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
self.file_system.join(base_path, path).await
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
self.file_system.parent(path).await
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_platform_sandbox_context(sandbox)?;
@@ -205,7 +182,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -217,7 +194,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -229,7 +206,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_platform_sandbox_context(sandbox)?;
@@ -238,7 +215,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_platform_sandbox_context(sandbox)?;
@@ -249,7 +226,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -261,8 +238,8 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -282,31 +259,23 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
impl ExecutorFileSystem for DirectFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
reject_sandbox_context(sandbox)?;
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
Ok(base_path.join(path))
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
Ok(path.parent())
let path = path.to_abs_path()?;
let canonicalized =
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)?;
PathUri::from_abs_path(&canonicalized)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
if metadata.len() > MAX_READ_FILE_BYTES {
return Err(io::Error::new(
@@ -319,21 +288,23 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
tokio::fs::write(path.as_path(), contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
if options.recursive {
tokio::fs::create_dir_all(path.as_path()).await?;
} else {
@@ -344,10 +315,11 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
Ok(FileMetadata {
@@ -361,10 +333,11 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let mut entries = Vec::new();
let mut read_dir = tokio::fs::read_dir(path.as_path()).await?;
while let Some(entry) = read_dir.next_entry().await? {
@@ -382,11 +355,12 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
match tokio::fs::symlink_metadata(path.as_path()).await {
Ok(metadata) => {
let file_type = metadata.file_type();
@@ -408,14 +382,14 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let source_path = source_path.to_path_buf();
let destination_path = destination_path.to_path_buf();
let source_path = source_path.to_abs_path()?.into_path_buf();
let destination_path = destination_path.to_abs_path()?.into_path_buf();
tokio::task::spawn_blocking(move || -> FileSystemResult<()> {
let metadata = std::fs::symlink_metadata(source_path.as_path())?;
let file_type = metadata.file_type();
@@ -576,6 +550,10 @@ fn system_time_to_unix_ms(time: SystemTime) -> i64 {
.unwrap_or(0)
}
#[cfg(all(test, any(unix, windows)))]
#[path = "local_file_system_path_uri_tests.rs"]
mod path_uri_tests;
#[cfg(all(test, unix))]
mod tests {
use super::*;
@@ -0,0 +1,27 @@
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::io;
use super::*;
#[tokio::test]
async fn direct_file_system_rejects_non_native_uri_as_invalid_input() {
let error = DirectFileSystem
.read_file(&non_native_uri(), /*sandbox*/ None)
.await
.expect_err("non-native URI should be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
fn non_native_uri() -> PathUri {
#[cfg(unix)]
let uri = "file://server/share/file.txt";
#[cfg(windows)]
let uri = "file:///usr/local/file.txt";
match PathUri::parse(uri) {
Ok(uri) => uri,
Err(err) => panic!("valid non-native URI should parse: {err}"),
}
}
+35 -51
View File
@@ -1,8 +1,7 @@
use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use codex_utils_path_uri::PathUri;
use tokio::io;
use tracing::trace;
@@ -20,8 +19,6 @@ use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
use crate::protocol::FsJoinParams;
use crate::protocol::FsParentParams;
use crate::protocol::FsReadDirectoryParams;
use crate::protocol::FsReadFileParams;
use crate::protocol::FsRemoveParams;
@@ -45,58 +42,33 @@ impl RemoteFileSystem {
impl ExecutorFileSystem for RemoteFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
trace!("remote fs canonicalize");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_canonicalize(FsCanonicalizeParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
.map_err(map_remote_error)?;
Ok(response.path)
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
trace!("remote fs join");
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_join(FsJoinParams {
base_path: base_path.clone(),
path: path.to_path_buf(),
})
.await
.map_err(map_remote_error)?;
Ok(response.path)
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
trace!("remote fs parent");
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_parent(FsParentParams { path: path.clone() })
.await
.map_err(map_remote_error)?;
Ok(response.path)
PathUri::from_abs_path(&response.path)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
trace!("remote fs read_file");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_read_file(FsReadFileParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
@@ -111,15 +83,16 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs write_file");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_write_file(FsWriteFileParams {
path: path.clone(),
path,
data_base64: STANDARD.encode(contents),
sandbox: remote_sandbox_context(sandbox),
})
@@ -130,15 +103,16 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs create_directory");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
path,
recursive: Some(options.recursive),
sandbox: remote_sandbox_context(sandbox),
})
@@ -149,14 +123,15 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
trace!("remote fs get_metadata");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
@@ -172,14 +147,15 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
trace!("remote fs read_directory");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
@@ -197,15 +173,16 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs remove");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_remove(FsRemoveParams {
path: path.clone(),
path,
recursive: Some(options.recursive),
force: Some(options.force),
sandbox: remote_sandbox_context(sandbox),
@@ -217,17 +194,19 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs copy");
let source_path = source_path.to_abs_path()?;
let destination_path = destination_path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
source_path,
destination_path,
recursive: options.recursive,
sandbox: remote_sandbox_context(sandbox),
})
@@ -261,6 +240,10 @@ fn map_remote_error(error: ExecServerError) -> io::Error {
}
}
#[cfg(all(test, any(unix, windows)))]
#[path = "remote_file_system_path_uri_tests.rs"]
mod path_uri_tests;
#[cfg(test)]
mod tests {
use codex_protocol::models::PermissionProfile;
@@ -270,6 +253,7 @@ mod tests {
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use super::*;
@@ -0,0 +1,225 @@
#![allow(clippy::expect_used)]
#[cfg(windows)]
use codex_app_server_protocol::JSONRPCMessage;
#[cfg(windows)]
use codex_app_server_protocol::JSONRPCResponse;
#[cfg(windows)]
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
#[cfg(windows)]
use futures::SinkExt;
#[cfg(windows)]
use futures::StreamExt;
use pretty_assertions::assert_eq;
use tokio::io;
#[cfg(windows)]
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
#[cfg(windows)]
use tokio::sync::oneshot;
#[cfg(windows)]
use tokio::time::Duration;
#[cfg(windows)]
use tokio::time::timeout;
#[cfg(windows)]
use tokio_tungstenite::WebSocketStream;
#[cfg(windows)]
use tokio_tungstenite::accept_async;
#[cfg(windows)]
use tokio_tungstenite::tungstenite::Message;
use super::*;
use crate::client_api::ExecServerTransportParams;
#[cfg(windows)]
use crate::protocol::FS_READ_FILE_METHOD;
#[cfg(windows)]
use crate::protocol::FsReadFileParams;
#[cfg(windows)]
use crate::protocol::FsReadFileResponse;
#[cfg(windows)]
use crate::protocol::INITIALIZE_METHOD;
#[cfg(windows)]
use crate::protocol::INITIALIZED_METHOD;
#[cfg(windows)]
use crate::protocol::InitializeResponse;
#[tokio::test]
async fn non_native_uri_is_rejected_before_connecting() {
let file_system = RemoteFileSystem::new(LazyRemoteExecServerClient::new(
ExecServerTransportParams::websocket_url("not a websocket URL".to_string()),
));
let error = file_system
.read_file(&non_native_uri(), /*sandbox*/ None)
.await
.expect_err("non-native URI should be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
#[cfg(windows)]
#[tokio::test]
async fn remote_file_system_sends_explicit_windows_native_paths() {
let (websocket_url, captured_paths, server) = record_read_file_paths(2).await;
let file_system = RemoteFileSystem::new(LazyRemoteExecServerClient::new(
ExecServerTransportParams::websocket_url(websocket_url),
));
let paths = vec![
(
PathUri::parse("file:///C:/Users/Alice/src/main.rs").expect("valid drive URI"),
absolute_windows_path(r"C:\Users\Alice\src\main.rs"),
),
(
PathUri::parse("file://server/share/src/main.rs").expect("valid UNC URI"),
absolute_windows_path(r"\\server\share\src\main.rs"),
),
];
let expected_paths = paths
.iter()
.map(|(_, expected_path)| expected_path.clone())
.collect::<Vec<_>>();
for (path, _) in paths {
assert_eq!(
file_system
.read_file(&path, /*sandbox*/ None)
.await
.expect("remote read should succeed"),
Vec::<u8>::new()
);
}
assert_eq!(
captured_paths.await.expect("captured paths"),
expected_paths
);
server.await.expect("recording server should succeed");
}
fn non_native_uri() -> PathUri {
#[cfg(unix)]
let uri = "file://server/share/file.txt";
#[cfg(windows)]
let uri = "file:///usr/local/file.txt";
PathUri::parse(uri).expect("valid non-native URI")
}
#[cfg(windows)]
fn absolute_windows_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute Windows path")
}
#[cfg(windows)]
async fn record_read_file_paths(
expected_requests: usize,
) -> (
String,
oneshot::Receiver<Vec<AbsolutePathBuf>>,
tokio::task::JoinHandle<()>,
) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
let websocket_url = format!("ws://{}", listener.local_addr().expect("listener address"));
let (captured_paths_tx, captured_paths_rx) = oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("listener should accept");
let mut websocket = accept_async(stream)
.await
.expect("websocket handshake should succeed");
complete_websocket_initialize(&mut websocket).await;
let mut captured_paths = Vec::with_capacity(expected_requests);
for _ in 0..expected_requests {
let request = match read_jsonrpc_websocket(&mut websocket).await {
JSONRPCMessage::Request(request) if request.method == FS_READ_FILE_METHOD => {
request
}
other => panic!("expected fs/readFile request, got {other:?}"),
};
let params: FsReadFileParams =
serde_json::from_value(request.params.expect("fs/readFile params should exist"))
.expect("fs/readFile params should deserialize");
captured_paths.push(params.path);
write_jsonrpc_websocket(
&mut websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(FsReadFileResponse {
data_base64: String::new(),
})
.expect("fs/readFile response should serialize"),
}),
)
.await;
}
captured_paths_tx
.send(captured_paths)
.expect("captured paths receiver should stay open");
});
(websocket_url, captured_paths_rx, server)
}
#[cfg(windows)]
async fn complete_websocket_initialize(websocket: &mut WebSocketStream<TcpStream>) {
let request = match read_jsonrpc_websocket(websocket).await {
JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
other => panic!("expected initialize request, got {other:?}"),
};
write_jsonrpc_websocket(
websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(InitializeResponse {
session_id: "session-1".to_string(),
})
.expect("initialize response should serialize"),
}),
)
.await;
match read_jsonrpc_websocket(websocket).await {
JSONRPCMessage::Notification(notification) if notification.method == INITIALIZED_METHOD => {
}
other => panic!("expected initialized notification, got {other:?}"),
}
}
#[cfg(windows)]
async fn read_jsonrpc_websocket(websocket: &mut WebSocketStream<TcpStream>) -> JSONRPCMessage {
loop {
match timeout(Duration::from_secs(1), websocket.next())
.await
.expect("json-rpc websocket read should not time out")
.expect("websocket should stay open")
.expect("websocket frame should read")
{
Message::Text(text) => {
return serde_json::from_str(text.as_ref())
.expect("json-rpc text frame should parse");
}
Message::Binary(bytes) => {
return serde_json::from_slice(bytes.as_ref())
.expect("json-rpc binary frame should parse");
}
Message::Ping(_) | Message::Pong(_) => {}
other => panic!("expected json-rpc websocket frame, got {other:?}"),
}
}
}
#[cfg(windows)]
async fn write_jsonrpc_websocket(
websocket: &mut WebSocketStream<TcpStream>,
message: JSONRPCMessage,
) {
let encoded = serde_json::to_string(&message).expect("json-rpc should serialize");
websocket
.send(Message::Text(encoded.into()))
.await
.expect("json-rpc websocket frame should write");
}
@@ -2,8 +2,7 @@ use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use codex_utils_path_uri::PathUri;
use tokio::io;
use crate::CopyOptions;
@@ -55,39 +54,27 @@ impl SandboxedFileSystem {
impl ExecutorFileSystem for SandboxedFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
let sandbox = require_platform_sandbox(sandbox)?;
let response = self
.run_sandboxed(
sandbox,
FsHelperRequest::Canonicalize(FsCanonicalizeParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
.await?
.expect_canonicalize()
.map_err(map_sandbox_error)?;
Ok(response.path)
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
Ok(base_path.join(path))
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
Ok(path.parent())
PathUri::from_abs_path(&response.path)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
let sandbox = require_platform_sandbox(sandbox)?;
@@ -95,7 +82,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
.run_sandboxed(
sandbox,
FsHelperRequest::ReadFile(FsReadFileParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
@@ -112,7 +99,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -120,7 +107,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::WriteFile(FsWriteFileParams {
path: path.clone(),
path: path.to_abs_path()?,
data_base64: STANDARD.encode(contents),
sandbox: None,
}),
@@ -133,7 +120,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -141,7 +128,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::CreateDirectory(FsCreateDirectoryParams {
path: path.clone(),
path: path.to_abs_path()?,
recursive: Some(options.recursive),
sandbox: None,
}),
@@ -154,7 +141,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
let sandbox = require_platform_sandbox(sandbox)?;
@@ -162,7 +149,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
.run_sandboxed(
sandbox,
FsHelperRequest::GetMetadata(FsGetMetadataParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
@@ -180,7 +167,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let sandbox = require_platform_sandbox(sandbox)?;
@@ -188,7 +175,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
.run_sandboxed(
sandbox,
FsHelperRequest::ReadDirectory(FsReadDirectoryParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
@@ -208,7 +195,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
remove_options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -216,7 +203,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::Remove(FsRemoveParams {
path: path.clone(),
path: path.to_abs_path()?,
recursive: Some(remove_options.recursive),
force: Some(remove_options.force),
sandbox: None,
@@ -230,8 +217,8 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -239,8 +226,8 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::Copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
source_path: source_path.to_abs_path()?,
destination_path: destination_path.to_abs_path()?,
recursive: options.recursive,
sandbox: None,
}),
@@ -272,3 +259,7 @@ fn map_sandbox_error(error: JSONRPCErrorError) -> io::Error {
_ => io::Error::other(error.message),
}
}
#[cfg(all(test, any(unix, windows)))]
#[path = "sandboxed_file_system_path_uri_tests.rs"]
mod path_uri_tests;
@@ -0,0 +1,43 @@
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::io;
use super::*;
#[tokio::test]
async fn sandboxed_file_system_rejects_non_native_uri_as_invalid_input() {
let runtime_paths = ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let file_system = SandboxedFileSystem::new(runtime_paths);
let sandbox = FileSystemSandboxContext::from_permission_profile(
PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(Vec::new()),
NetworkSandboxPolicy::Restricted,
),
);
let error = file_system
.read_file(&non_native_uri(), Some(&sandbox))
.await
.expect_err("non-native URI should be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
fn non_native_uri() -> PathUri {
#[cfg(unix)]
let uri = "file://server/share/file.txt";
#[cfg(windows)]
let uri = "file:///usr/local/file.txt";
match PathUri::parse(uri) {
Ok(uri) => uri,
Err(err) => panic!("valid non-native URI should parse: {err}"),
}
}
@@ -3,6 +3,7 @@ use std::io;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_utils_path_uri::PathUri;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
@@ -52,9 +53,10 @@ impl FileSystemHandler {
&self,
params: FsReadFileParams,
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = self
.file_system
.read_file(&params.path, params.sandbox.as_ref())
.read_file(&path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsReadFileResponse {
@@ -66,13 +68,14 @@ impl FileSystemHandler {
&self,
params: FsWriteFileParams,
) -> Result<FsWriteFileResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
invalid_request(format!(
"{FS_WRITE_FILE_METHOD} requires valid base64 dataBase64: {err}"
))
})?;
self.file_system
.write_file(&params.path, bytes, params.sandbox.as_ref())
.write_file(&path, bytes, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
@@ -83,9 +86,10 @@ impl FileSystemHandler {
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
let recursive = params.recursive.unwrap_or(true);
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system
.create_directory(
&params.path,
&path,
CreateDirectoryOptions { recursive },
params.sandbox.as_ref(),
)
@@ -98,9 +102,10 @@ impl FileSystemHandler {
&self,
params: FsGetMetadataParams,
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let metadata = self
.file_system
.get_metadata(&params.path, params.sandbox.as_ref())
.get_metadata(&path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
@@ -116,11 +121,13 @@ impl FileSystemHandler {
&self,
params: FsCanonicalizeParams,
) -> Result<FsCanonicalizeResponse, JSONRPCErrorError> {
let requested_path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let path = self
.file_system
.canonicalize(&params.path, params.sandbox.as_ref())
.canonicalize(&requested_path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
let path = path.to_abs_path().map_err(map_fs_error)?;
Ok(FsCanonicalizeResponse { path })
}
@@ -128,11 +135,8 @@ impl FileSystemHandler {
&self,
params: FsJoinParams,
) -> Result<FsJoinResponse, JSONRPCErrorError> {
let path = self
.file_system
.join(&params.base_path, &params.path)
.await
.map_err(map_fs_error)?;
// TODO(anp): remove and migrate callers to PathUri.
let path = params.base_path.join(params.path);
Ok(FsJoinResponse { path })
}
@@ -140,11 +144,8 @@ impl FileSystemHandler {
&self,
params: FsParentParams,
) -> Result<FsParentResponse, JSONRPCErrorError> {
let path = self
.file_system
.parent(&params.path)
.await
.map_err(map_fs_error)?;
// TODO(anp): remove and migrate callers to PathUri.
let path = params.path.parent();
Ok(FsParentResponse { path })
}
@@ -152,9 +153,10 @@ impl FileSystemHandler {
&self,
params: FsReadDirectoryParams,
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let entries = self
.file_system
.read_directory(&params.path, params.sandbox.as_ref())
.read_directory(&path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?
.into_iter()
@@ -173,9 +175,10 @@ impl FileSystemHandler {
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
let recursive = params.recursive.unwrap_or(true);
let force = params.force.unwrap_or(true);
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system
.remove(
&params.path,
&path,
RemoveOptions { recursive, force },
params.sandbox.as_ref(),
)
@@ -188,10 +191,13 @@ impl FileSystemHandler {
&self,
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
let source_path = PathUri::from_abs_path(&params.source_path).map_err(map_fs_error)?;
let destination_path =
PathUri::from_abs_path(&params.destination_path).map_err(map_fs_error)?;
self.file_system
.copy(
&params.source_path,
&params.destination_path,
&source_path,
&destination_path,
CopyOptions {
recursive: params.recursive,
},
@@ -262,6 +268,24 @@ mod tests {
.await
.expect("write file");
let canonicalized = handler
.canonicalize(FsCanonicalizeParams {
path: path.clone(),
sandbox: Some(FileSystemSandboxContext::from_legacy_sandbox_policy(
sandbox_policy.clone(),
sandbox_cwd.clone(),
)),
})
.await
.expect("canonicalize file");
assert_eq!(
canonicalized.path,
AbsolutePathBuf::from_absolute_path(
std::fs::canonicalize(path.as_path()).expect("canonical path"),
)
.expect("absolute canonical path"),
);
let response = handler
.read_file(FsReadFileParams {
path,
@@ -276,4 +300,34 @@ mod tests {
assert_eq!(response.data_base64, STANDARD.encode("ok"));
}
}
#[tokio::test]
async fn protocol_join_and_parent_remain_native_path_operations() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let runtime_paths = ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let handler = FileSystemHandler::new(runtime_paths);
let base_path =
AbsolutePathBuf::from_absolute_path(temp_dir.path()).expect("absolute tempdir");
let joined = handler
.join(FsJoinParams {
base_path: base_path.clone(),
path: "nested/file.txt".into(),
})
.await
.expect("join path");
assert_eq!(joined.path, base_path.join("nested/file.txt"));
let parent = handler
.parent(FsParentParams {
path: joined.path.clone(),
})
.await
.expect("parent path");
assert_eq!(parent.path, joined.path.parent());
}
}