Compare commits

...

2 Commits

Author SHA1 Message Date
Felipe Coury
d8db042f54 fix(config): implement test filesystem canonicalization 2026-05-30 19:31:38 -03:00
Felipe Coury
d9e6828eba feat(exec-server): canonicalize executor paths 2026-05-30 18:56:31 -03:00
12 changed files with 266 additions and 0 deletions

View File

@@ -14,6 +14,16 @@ struct TestFileSystem;
#[async_trait]
impl ExecutorFileSystem for TestFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
let path = tokio::fs::canonicalize(path.as_path()).await?;
AbsolutePathBuf::try_from(path)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
async fn read_file(
&self,
path: &AbsolutePathBuf,

View File

@@ -41,6 +41,7 @@ use crate::protocol::ExecExitedNotification;
use crate::protocol::ExecOutputDeltaNotification;
use crate::protocol::ExecParams;
use crate::protocol::ExecResponse;
use crate::protocol::FS_CANONICALIZE_METHOD;
use crate::protocol::FS_COPY_METHOD;
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
use crate::protocol::FS_GET_METADATA_METHOD;
@@ -48,6 +49,8 @@ use crate::protocol::FS_READ_DIRECTORY_METHOD;
use crate::protocol::FS_READ_FILE_METHOD;
use crate::protocol::FS_REMOVE_METHOD;
use crate::protocol::FS_WRITE_FILE_METHOD;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCanonicalizeResponse;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCopyResponse;
use crate::protocol::FsCreateDirectoryParams;
@@ -393,6 +396,13 @@ impl ExecServerClient {
self.call(FS_READ_FILE_METHOD, &params).await
}
pub async fn fs_canonicalize(
&self,
params: FsCanonicalizeParams,
) -> Result<FsCanonicalizeResponse, ExecServerError> {
self.call(FS_CANONICALIZE_METHOD, &params).await
}
pub async fn fs_write_file(
&self,
params: FsWriteFileParams,
@@ -912,6 +922,7 @@ mod tests {
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_utils_absolute_path::AbsolutePathBuf;
use futures::SinkExt;
use futures::StreamExt;
use pretty_assertions::assert_eq;
@@ -956,6 +967,9 @@ mod tests {
use crate::protocol::ExecExitedNotification;
use crate::protocol::ExecOutputDeltaNotification;
use crate::protocol::ExecOutputStream;
use crate::protocol::FS_CANONICALIZE_METHOD;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCanonicalizeResponse;
use crate::protocol::INITIALIZE_METHOD;
use crate::protocol::INITIALIZED_METHOD;
use crate::protocol::InitializeResponse;
@@ -1024,6 +1038,81 @@ mod tests {
.expect("json-rpc websocket frame should write");
}
#[tokio::test]
async fn fs_canonicalize_uses_remote_rpc() {
let (client_stdin, server_reader) = duplex(/*max_buf_size*/ 1 << 20);
let (mut server_writer, client_stdout) = duplex(/*max_buf_size*/ 1 << 20);
let expected_path = AbsolutePathBuf::current_dir().expect("current directory");
let expected_path_for_server = expected_path.clone();
let server = tokio::spawn(async move {
let mut lines = BufReader::new(server_reader).lines();
let initialize = read_jsonrpc_line(&mut lines).await;
let request = match initialize {
JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
other => panic!("expected initialize request, got {other:?}"),
};
write_jsonrpc_line(
&mut server_writer,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(InitializeResponse {
session_id: "session-1".to_string(),
})
.expect("initialize response should serialize"),
}),
)
.await;
let initialized = read_jsonrpc_line(&mut lines).await;
assert!(matches!(
initialized,
JSONRPCMessage::Notification(notification)
if notification.method == INITIALIZED_METHOD
));
let canonicalize = read_jsonrpc_line(&mut lines).await;
let request = match canonicalize {
JSONRPCMessage::Request(request) if request.method == FS_CANONICALIZE_METHOD => {
request
}
other => panic!("expected fs/canonicalize request, got {other:?}"),
};
write_jsonrpc_line(
&mut server_writer,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(FsCanonicalizeResponse {
path: expected_path_for_server,
})
.expect("canonicalize response should serialize"),
}),
)
.await;
});
let client = ExecServerClient::connect(
JsonRpcConnection::from_stdio(
client_stdout,
client_stdin,
"test-exec-server-client".to_string(),
),
ExecServerClientConnectOptions::default(),
)
.await
.expect("client should connect");
assert_eq!(
client
.fs_canonicalize(FsCanonicalizeParams {
path: expected_path.clone(),
sandbox: None,
})
.await
.expect("canonicalize should succeed"),
FsCanonicalizeResponse {
path: expected_path
}
);
server.await.expect("server task should finish");
}
async fn complete_websocket_initialize(
websocket: &mut WebSocketStream<TcpStream>,
session_id: &str,

View File

@@ -10,6 +10,7 @@ use crate::CreateDirectoryOptions;
use crate::ExecutorFileSystem;
use crate::RemoveOptions;
use crate::local_file_system::DirectFileSystem;
use crate::protocol::FS_CANONICALIZE_METHOD;
use crate::protocol::FS_COPY_METHOD;
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
use crate::protocol::FS_GET_METADATA_METHOD;
@@ -17,6 +18,8 @@ use crate::protocol::FS_READ_DIRECTORY_METHOD;
use crate::protocol::FS_READ_FILE_METHOD;
use crate::protocol::FS_REMOVE_METHOD;
use crate::protocol::FS_WRITE_FILE_METHOD;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCanonicalizeResponse;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCopyResponse;
use crate::protocol::FsCreateDirectoryParams;
@@ -41,6 +44,8 @@ pub const CODEX_FS_HELPER_ARG1: &str = "--codex-run-as-fs-helper";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "operation", content = "params")]
pub(crate) enum FsHelperRequest {
#[serde(rename = "fs/canonicalize")]
Canonicalize(FsCanonicalizeParams),
#[serde(rename = "fs/readFile")]
ReadFile(FsReadFileParams),
#[serde(rename = "fs/writeFile")]
@@ -67,6 +72,8 @@ pub(crate) enum FsHelperResponse {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "operation", content = "response")]
pub(crate) enum FsHelperPayload {
#[serde(rename = "fs/canonicalize")]
Canonicalize(FsCanonicalizeResponse),
#[serde(rename = "fs/readFile")]
ReadFile(FsReadFileResponse),
#[serde(rename = "fs/writeFile")]
@@ -86,6 +93,7 @@ pub(crate) enum FsHelperPayload {
impl FsHelperPayload {
fn operation(&self) -> &'static str {
match self {
Self::Canonicalize(_) => FS_CANONICALIZE_METHOD,
Self::ReadFile(_) => FS_READ_FILE_METHOD,
Self::WriteFile(_) => FS_WRITE_FILE_METHOD,
Self::CreateDirectory(_) => FS_CREATE_DIRECTORY_METHOD,
@@ -96,6 +104,16 @@ impl FsHelperPayload {
}
}
pub(crate) fn expect_canonicalize(self) -> Result<FsCanonicalizeResponse, JSONRPCErrorError> {
match self {
Self::Canonicalize(response) => Ok(response),
other => Err(unexpected_response(
FS_CANONICALIZE_METHOD,
other.operation(),
)),
}
}
pub(crate) fn expect_read_file(self) -> Result<FsReadFileResponse, JSONRPCErrorError> {
match self {
Self::ReadFile(response) => Ok(response),
@@ -170,6 +188,15 @@ pub(crate) async fn run_direct_request(
) -> Result<FsHelperPayload, JSONRPCErrorError> {
let file_system = DirectFileSystem;
match request {
FsHelperRequest::Canonicalize(params) => {
let path = file_system
.canonicalize(&params.path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::Canonicalize(FsCanonicalizeResponse {
path,
}))
}
FsHelperRequest::ReadFile(params) => {
let data = file_system
.read_file(&params.path, /*sandbox*/ None)

View File

@@ -62,6 +62,8 @@ pub use protocol::ExecOutputDeltaNotification;
pub use protocol::ExecOutputStream;
pub use protocol::ExecParams;
pub use protocol::ExecResponse;
pub use protocol::FsCanonicalizeParams;
pub use protocol::FsCanonicalizeResponse;
pub use protocol::FsCopyParams;
pub use protocol::FsCopyResponse;
pub use protocol::FsCreateDirectoryParams;

View File

@@ -79,6 +79,15 @@ impl LocalFileSystem {
#[async_trait]
impl ExecutorFileSystem for LocalFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.canonicalize(path, sandbox).await
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
@@ -152,6 +161,15 @@ impl ExecutorFileSystem for LocalFileSystem {
#[async_trait]
impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
reject_platform_sandbox_context(sandbox)?;
self.file_system.canonicalize(path, /*sandbox*/ None).await
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
@@ -238,6 +256,17 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
#[async_trait]
impl ExecutorFileSystem for DirectFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
reject_sandbox_context(sandbox)?;
let path = tokio::fs::canonicalize(path.as_path()).await?;
AbsolutePathBuf::try_from(path)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
@@ -519,6 +548,25 @@ mod tests {
use pretty_assertions::assert_eq;
use std::os::unix::fs::symlink;
#[tokio::test]
async fn direct_file_system_canonicalizes_symlinks() -> io::Result<()> {
let temp_dir = tempfile::TempDir::new()?;
let target = temp_dir.path().join("target");
let link = temp_dir.path().join("link");
std::fs::create_dir(&target)?;
symlink(&target, &link)?;
let link = AbsolutePathBuf::from_absolute_path(&link)?;
let target = AbsolutePathBuf::try_from(std::fs::canonicalize(&target)?)?;
assert_eq!(
DirectFileSystem
.canonicalize(&link, /*sandbox*/ None)
.await?,
target
);
Ok(())
}
#[test]
fn resolve_existing_path_handles_symlink_parent_dotdot_escape() -> io::Result<()> {
let temp_dir = tempfile::TempDir::new()?;

View File

@@ -20,6 +20,7 @@ pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output";
pub const EXEC_EXITED_METHOD: &str = "process/exited";
pub const EXEC_CLOSED_METHOD: &str = "process/closed";
pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
pub const FS_CANONICALIZE_METHOD: &str = "fs/canonicalize";
pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
@@ -170,6 +171,19 @@ pub struct FsReadFileResponse {
pub data_base64: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsCanonicalizeParams {
pub path: AbsolutePathBuf,
pub sandbox: Option<FileSystemSandboxContext>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsCanonicalizeResponse {
pub path: AbsolutePathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsWriteFileParams {

View File

@@ -15,6 +15,7 @@ use crate::FileSystemSandboxContext;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use crate::client::LazyRemoteExecServerClient;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
@@ -40,6 +41,23 @@ impl RemoteFileSystem {
#[async_trait]
impl ExecutorFileSystem for RemoteFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
trace!("remote fs canonicalize");
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_canonicalize(FsCanonicalizeParams {
path: path.clone(),
sandbox: remote_sandbox_context(sandbox),
})
.await
.map_err(map_remote_error)?;
Ok(response.path)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,

View File

@@ -17,6 +17,7 @@ use crate::RemoveOptions;
use crate::fs_helper::FsHelperPayload;
use crate::fs_helper::FsHelperRequest;
use crate::fs_sandbox::FileSystemSandboxRunner;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
@@ -51,6 +52,25 @@ impl SandboxedFileSystem {
#[async_trait]
impl ExecutorFileSystem for SandboxedFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
let sandbox = require_platform_sandbox(sandbox)?;
self.run_sandboxed(
sandbox,
FsHelperRequest::Canonicalize(FsCanonicalizeParams {
path: path.clone(),
sandbox: None,
}),
)
.await?
.expect_canonicalize()
.map(|response| response.path)
.map_err(map_sandbox_error)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,

View File

@@ -11,6 +11,8 @@ use crate::ExecutorFileSystem;
use crate::RemoveOptions;
use crate::local_file_system::LocalFileSystem;
use crate::protocol::FS_WRITE_FILE_METHOD;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCanonicalizeResponse;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCopyResponse;
use crate::protocol::FsCreateDirectoryParams;
@@ -42,6 +44,18 @@ impl FileSystemHandler {
}
}
pub(crate) async fn canonicalize(
&self,
params: FsCanonicalizeParams,
) -> Result<FsCanonicalizeResponse, JSONRPCErrorError> {
let path = self
.file_system
.canonicalize(&params.path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsCanonicalizeResponse { path })
}
pub(crate) async fn read_file(
&self,
params: FsReadFileParams,

View File

@@ -16,6 +16,8 @@ use crate::client::http_client::PendingReqwestHttpBodyStream;
use crate::client::http_client::ReqwestHttpRequestRunner;
use crate::protocol::ExecParams;
use crate::protocol::ExecResponse;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCanonicalizeResponse;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCopyResponse;
use crate::protocol::FsCreateDirectoryParams;
@@ -216,6 +218,14 @@ impl ExecServerHandler {
self.file_system.read_file(params).await
}
pub(crate) async fn fs_canonicalize(
&self,
params: FsCanonicalizeParams,
) -> Result<FsCanonicalizeResponse, JSONRPCErrorError> {
self.require_initialized_for("filesystem")?;
self.file_system.canonicalize(params).await
}
pub(crate) async fn fs_write_file(
&self,
params: FsWriteFileParams,

View File

@@ -5,6 +5,7 @@ use crate::protocol::EXEC_READ_METHOD;
use crate::protocol::EXEC_TERMINATE_METHOD;
use crate::protocol::EXEC_WRITE_METHOD;
use crate::protocol::ExecParams;
use crate::protocol::FS_CANONICALIZE_METHOD;
use crate::protocol::FS_COPY_METHOD;
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
use crate::protocol::FS_GET_METADATA_METHOD;
@@ -12,6 +13,7 @@ use crate::protocol::FS_READ_DIRECTORY_METHOD;
use crate::protocol::FS_READ_FILE_METHOD;
use crate::protocol::FS_REMOVE_METHOD;
use crate::protocol::FS_WRITE_FILE_METHOD;
use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
@@ -72,6 +74,12 @@ pub(crate) fn build_router() -> RpcRouter<ExecServerHandler> {
handler.terminate(params).await
},
);
router.request(
FS_CANONICALIZE_METHOD,
|handler: Arc<ExecServerHandler>, params: FsCanonicalizeParams| async move {
handler.fs_canonicalize(params).await
},
);
router.request(
FS_READ_FILE_METHOD,
|handler: Arc<ExecServerHandler>, params: FsReadFileParams| async move {

View File

@@ -133,6 +133,12 @@ pub type FileSystemResult<T> = io::Result<T>;
/// a remote environment.
#[async_trait]
pub trait ExecutorFileSystem: Send + Sync {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf>;
async fn read_file(
&self,
path: &AbsolutePathBuf,