mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Run exec-server fs operations through sandbox helper (#17294)
## Summary - run exec-server filesystem RPCs requiring sandboxing through a `codex-fs` arg0 helper over stdin/stdout - keep direct local filesystem execution for `DangerFullAccess` and external sandbox policies - remove the standalone exec-server binary path in favor of top-level arg0 dispatch/runtime paths - add sandbox escape regression coverage for local and remote filesystem paths ## Validation - `just fmt` - `git diff --check` - remote devbox: `cd codex-rs && bazel test --bes_backend= --bes_results_url= //codex-rs/exec-server:all` (6/6 passed) --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExecServerArgs {
|
||||
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default).
|
||||
#[arg(
|
||||
long = "listen",
|
||||
value_name = "URL",
|
||||
default_value = codex_exec_server::DEFAULT_LISTEN_URL
|
||||
)]
|
||||
listen: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let args = ExecServerArgs::parse();
|
||||
codex_exec_server::run_main_with_listen_url(&args.listen).await
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use tokio::sync::OnceCell;
|
||||
|
||||
use crate::ExecServerClient;
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::RemoteExecServerConnectArgs;
|
||||
use crate::file_system::ExecutorFileSystem;
|
||||
use crate::local_file_system::LocalFileSystem;
|
||||
@@ -21,6 +22,7 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
#[derive(Debug)]
|
||||
pub struct EnvironmentManager {
|
||||
exec_server_url: Option<String>,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
disabled: bool,
|
||||
current_environment: OnceCell<Option<Arc<Environment>>>,
|
||||
}
|
||||
@@ -34,9 +36,19 @@ impl Default for EnvironmentManager {
|
||||
impl EnvironmentManager {
|
||||
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub fn new(exec_server_url: Option<String>) -> Self {
|
||||
Self::new_with_runtime_paths(exec_server_url, /*local_runtime_paths*/ None)
|
||||
}
|
||||
|
||||
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value and local
|
||||
/// runtime paths used when creating local filesystem helpers.
|
||||
pub fn new_with_runtime_paths(
|
||||
exec_server_url: Option<String>,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
|
||||
Self {
|
||||
exec_server_url,
|
||||
local_runtime_paths,
|
||||
disabled,
|
||||
current_environment: OnceCell::new(),
|
||||
}
|
||||
@@ -44,7 +56,18 @@ impl EnvironmentManager {
|
||||
|
||||
/// Builds a manager from process environment variables.
|
||||
pub fn from_env() -> Self {
|
||||
Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
|
||||
Self::from_env_with_runtime_paths(/*local_runtime_paths*/ None)
|
||||
}
|
||||
|
||||
/// Builds a manager from process environment variables and local runtime
|
||||
/// paths used when creating local filesystem helpers.
|
||||
pub fn from_env_with_runtime_paths(
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
Self::new_with_runtime_paths(
|
||||
std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok(),
|
||||
local_runtime_paths,
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a manager from the currently selected environment, or from the
|
||||
@@ -53,11 +76,13 @@ impl EnvironmentManager {
|
||||
match environment {
|
||||
Some(environment) => Self {
|
||||
exec_server_url: environment.exec_server_url().map(str::to_owned),
|
||||
local_runtime_paths: environment.local_runtime_paths().cloned(),
|
||||
disabled: false,
|
||||
current_environment: OnceCell::new(),
|
||||
},
|
||||
None => Self {
|
||||
exec_server_url: None,
|
||||
local_runtime_paths: None,
|
||||
disabled: true,
|
||||
current_environment: OnceCell::new(),
|
||||
},
|
||||
@@ -82,7 +107,11 @@ impl EnvironmentManager {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Arc::new(
|
||||
Environment::create(self.exec_server_url.clone()).await?,
|
||||
Environment::create_with_runtime_paths(
|
||||
self.exec_server_url.clone(),
|
||||
self.local_runtime_paths.clone(),
|
||||
)
|
||||
.await?,
|
||||
)))
|
||||
}
|
||||
})
|
||||
@@ -101,6 +130,7 @@ pub struct Environment {
|
||||
exec_server_url: Option<String>,
|
||||
remote_exec_server_client: Option<ExecServerClient>,
|
||||
exec_backend: Arc<dyn ExecBackend>,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
}
|
||||
|
||||
impl Default for Environment {
|
||||
@@ -109,6 +139,7 @@ impl Default for Environment {
|
||||
exec_server_url: None,
|
||||
remote_exec_server_client: None,
|
||||
exec_backend: Arc::new(LocalProcess::default()),
|
||||
local_runtime_paths: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +155,15 @@ impl std::fmt::Debug for Environment {
|
||||
impl Environment {
|
||||
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub async fn create(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
|
||||
Self::create_with_runtime_paths(exec_server_url, /*local_runtime_paths*/ None).await
|
||||
}
|
||||
|
||||
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value and
|
||||
/// local runtime paths used when creating local filesystem helpers.
|
||||
pub async fn create_with_runtime_paths(
|
||||
exec_server_url: Option<String>,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
|
||||
if disabled {
|
||||
return Err(ExecServerError::Protocol(
|
||||
@@ -157,6 +197,7 @@ impl Environment {
|
||||
exec_server_url,
|
||||
remote_exec_server_client,
|
||||
exec_backend,
|
||||
local_runtime_paths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,6 +210,10 @@ impl Environment {
|
||||
self.exec_server_url.as_deref()
|
||||
}
|
||||
|
||||
pub fn local_runtime_paths(&self) -> Option<&ExecServerRuntimePaths> {
|
||||
self.local_runtime_paths.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
|
||||
Arc::clone(&self.exec_backend)
|
||||
}
|
||||
@@ -176,7 +221,10 @@ impl Environment {
|
||||
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
|
||||
match self.remote_exec_server_client.clone() {
|
||||
Some(client) => Arc::new(RemoteFileSystem::new(client)),
|
||||
None => Arc::new(LocalFileSystem),
|
||||
None => match self.local_runtime_paths.clone() {
|
||||
Some(runtime_paths) => Arc::new(LocalFileSystem::with_runtime_paths(runtime_paths)),
|
||||
None => Arc::new(LocalFileSystem::unsandboxed()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +242,7 @@ mod tests {
|
||||
|
||||
use super::Environment;
|
||||
use super::EnvironmentManager;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -246,6 +295,31 @@ mod tests {
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_carries_local_runtime_paths() {
|
||||
let runtime_paths = ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
let manager = EnvironmentManager::new_with_runtime_paths(
|
||||
/*exec_server_url*/ None,
|
||||
Some(runtime_paths.clone()),
|
||||
);
|
||||
|
||||
let environment = manager
|
||||
.current()
|
||||
.await
|
||||
.expect("get current environment")
|
||||
.expect("local environment");
|
||||
|
||||
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
|
||||
assert_eq!(
|
||||
EnvironmentManager::from_environment(Some(&environment)).local_runtime_paths,
|
||||
Some(runtime_paths)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_environment_manager_has_no_current_environment() {
|
||||
let manager = EnvironmentManager::new(Some("none".to_string()));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use tokio::io;
|
||||
@@ -34,86 +36,95 @@ pub struct ReadDirectoryEntry {
|
||||
pub is_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileSystemSandboxContext {
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
#[serde(default)]
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
#[serde(default)]
|
||||
pub use_legacy_landlock: bool,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
}
|
||||
|
||||
impl FileSystemSandboxContext {
|
||||
pub fn new(sandbox_policy: SandboxPolicy) -> Self {
|
||||
Self {
|
||||
sandbox_policy,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
additional_permissions: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_run_in_sandbox(&self) -> bool {
|
||||
matches!(
|
||||
self.sandbox_policy,
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub type FileSystemResult<T> = io::Result<T>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait ExecutorFileSystem: Send + Sync {
|
||||
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>>;
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>>;
|
||||
|
||||
/// Reads a file and decodes it as UTF-8 text.
|
||||
async fn read_file_text(&self, path: &AbsolutePathBuf) -> FileSystemResult<String> {
|
||||
let bytes = self.read_file(path).await?;
|
||||
async fn read_file_text(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<String> {
|
||||
let bytes = self.read_file(path, sandbox).await?;
|
||||
String::from_utf8(bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
|
||||
}
|
||||
|
||||
async fn read_file_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<u8>>;
|
||||
|
||||
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()>;
|
||||
|
||||
async fn write_file_with_sandbox_policy(
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()>;
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
) -> FileSystemResult<()>;
|
||||
|
||||
async fn create_directory_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
create_directory_options: CreateDirectoryOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()>;
|
||||
|
||||
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata>;
|
||||
|
||||
async fn get_metadata_with_sandbox_policy(
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata>;
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
|
||||
|
||||
async fn read_directory_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
|
||||
|
||||
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()>;
|
||||
|
||||
async fn remove_with_sandbox_policy(
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
remove_options: RemoveOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()>;
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
) -> FileSystemResult<()>;
|
||||
|
||||
async fn copy_with_sandbox_policy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
copy_options: CopyOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::io;
|
||||
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::RemoveOptions;
|
||||
use crate::local_file_system::DirectFileSystem;
|
||||
use crate::protocol::FS_COPY_METHOD;
|
||||
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
|
||||
use crate::protocol::FS_GET_METADATA_METHOD;
|
||||
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::FsCopyParams;
|
||||
use crate::protocol::FsCopyResponse;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsCreateDirectoryResponse;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
use crate::protocol::FsGetMetadataResponse;
|
||||
use crate::protocol::FsReadDirectoryEntry;
|
||||
use crate::protocol::FsReadDirectoryParams;
|
||||
use crate::protocol::FsReadDirectoryResponse;
|
||||
use crate::protocol::FsReadFileParams;
|
||||
use crate::protocol::FsReadFileResponse;
|
||||
use crate::protocol::FsRemoveParams;
|
||||
use crate::protocol::FsRemoveResponse;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
use crate::protocol::FsWriteFileResponse;
|
||||
use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_request;
|
||||
use crate::rpc::not_found;
|
||||
|
||||
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/readFile")]
|
||||
ReadFile(FsReadFileParams),
|
||||
#[serde(rename = "fs/writeFile")]
|
||||
WriteFile(FsWriteFileParams),
|
||||
#[serde(rename = "fs/createDirectory")]
|
||||
CreateDirectory(FsCreateDirectoryParams),
|
||||
#[serde(rename = "fs/getMetadata")]
|
||||
GetMetadata(FsGetMetadataParams),
|
||||
#[serde(rename = "fs/readDirectory")]
|
||||
ReadDirectory(FsReadDirectoryParams),
|
||||
#[serde(rename = "fs/remove")]
|
||||
Remove(FsRemoveParams),
|
||||
#[serde(rename = "fs/copy")]
|
||||
Copy(FsCopyParams),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "status", content = "payload", rename_all = "camelCase")]
|
||||
pub(crate) enum FsHelperResponse {
|
||||
Ok(FsHelperPayload),
|
||||
Error(JSONRPCErrorError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "operation", content = "response")]
|
||||
pub(crate) enum FsHelperPayload {
|
||||
#[serde(rename = "fs/readFile")]
|
||||
ReadFile(FsReadFileResponse),
|
||||
#[serde(rename = "fs/writeFile")]
|
||||
WriteFile(FsWriteFileResponse),
|
||||
#[serde(rename = "fs/createDirectory")]
|
||||
CreateDirectory(FsCreateDirectoryResponse),
|
||||
#[serde(rename = "fs/getMetadata")]
|
||||
GetMetadata(FsGetMetadataResponse),
|
||||
#[serde(rename = "fs/readDirectory")]
|
||||
ReadDirectory(FsReadDirectoryResponse),
|
||||
#[serde(rename = "fs/remove")]
|
||||
Remove(FsRemoveResponse),
|
||||
#[serde(rename = "fs/copy")]
|
||||
Copy(FsCopyResponse),
|
||||
}
|
||||
|
||||
impl FsHelperPayload {
|
||||
fn operation(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ReadFile(_) => FS_READ_FILE_METHOD,
|
||||
Self::WriteFile(_) => FS_WRITE_FILE_METHOD,
|
||||
Self::CreateDirectory(_) => FS_CREATE_DIRECTORY_METHOD,
|
||||
Self::GetMetadata(_) => FS_GET_METADATA_METHOD,
|
||||
Self::ReadDirectory(_) => FS_READ_DIRECTORY_METHOD,
|
||||
Self::Remove(_) => FS_REMOVE_METHOD,
|
||||
Self::Copy(_) => FS_COPY_METHOD,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_read_file(self) -> Result<FsReadFileResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::ReadFile(response) => Ok(response),
|
||||
other => Err(unexpected_response(FS_READ_FILE_METHOD, other.operation())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_write_file(self) -> Result<FsWriteFileResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::WriteFile(response) => Ok(response),
|
||||
other => Err(unexpected_response(FS_WRITE_FILE_METHOD, other.operation())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_create_directory(
|
||||
self,
|
||||
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::CreateDirectory(response) => Ok(response),
|
||||
other => Err(unexpected_response(
|
||||
FS_CREATE_DIRECTORY_METHOD,
|
||||
other.operation(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_get_metadata(self) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::GetMetadata(response) => Ok(response),
|
||||
other => Err(unexpected_response(
|
||||
FS_GET_METADATA_METHOD,
|
||||
other.operation(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_read_directory(
|
||||
self,
|
||||
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::ReadDirectory(response) => Ok(response),
|
||||
other => Err(unexpected_response(
|
||||
FS_READ_DIRECTORY_METHOD,
|
||||
other.operation(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_remove(self) -> Result<FsRemoveResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::Remove(response) => Ok(response),
|
||||
other => Err(unexpected_response(FS_REMOVE_METHOD, other.operation())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expect_copy(self) -> Result<FsCopyResponse, JSONRPCErrorError> {
|
||||
match self {
|
||||
Self::Copy(response) => Ok(response),
|
||||
other => Err(unexpected_response(FS_COPY_METHOD, other.operation())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unexpected_response(expected: &str, actual: &str) -> JSONRPCErrorError {
|
||||
internal_error(format!(
|
||||
"unexpected fs sandbox helper response: expected {expected}, got {actual}"
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn run_direct_request(
|
||||
request: FsHelperRequest,
|
||||
) -> Result<FsHelperPayload, JSONRPCErrorError> {
|
||||
let file_system = DirectFileSystem;
|
||||
match request {
|
||||
FsHelperRequest::ReadFile(params) => {
|
||||
let data = file_system
|
||||
.read_file(¶ms.path, /*sandbox*/ None)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsHelperPayload::ReadFile(FsReadFileResponse {
|
||||
data_base64: STANDARD.encode(data),
|
||||
}))
|
||||
}
|
||||
FsHelperRequest::WriteFile(params) => {
|
||||
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(¶ms.path, bytes, /*sandbox*/ None)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsHelperPayload::WriteFile(FsWriteFileResponse {}))
|
||||
}
|
||||
FsHelperRequest::CreateDirectory(params) => {
|
||||
file_system
|
||||
.create_directory(
|
||||
¶ms.path,
|
||||
CreateDirectoryOptions {
|
||||
recursive: params.recursive.unwrap_or(true),
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsHelperPayload::CreateDirectory(
|
||||
FsCreateDirectoryResponse {},
|
||||
))
|
||||
}
|
||||
FsHelperRequest::GetMetadata(params) => {
|
||||
let metadata = file_system
|
||||
.get_metadata(¶ms.path, /*sandbox*/ None)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsHelperPayload::GetMetadata(FsGetMetadataResponse {
|
||||
is_directory: metadata.is_directory,
|
||||
is_file: metadata.is_file,
|
||||
created_at_ms: metadata.created_at_ms,
|
||||
modified_at_ms: metadata.modified_at_ms,
|
||||
}))
|
||||
}
|
||||
FsHelperRequest::ReadDirectory(params) => {
|
||||
let entries = file_system
|
||||
.read_directory(¶ms.path, /*sandbox*/ None)
|
||||
.await
|
||||
.map_err(map_fs_error)?
|
||||
.into_iter()
|
||||
.map(|entry| FsReadDirectoryEntry {
|
||||
file_name: entry.file_name,
|
||||
is_directory: entry.is_directory,
|
||||
is_file: entry.is_file,
|
||||
})
|
||||
.collect();
|
||||
Ok(FsHelperPayload::ReadDirectory(FsReadDirectoryResponse {
|
||||
entries,
|
||||
}))
|
||||
}
|
||||
FsHelperRequest::Remove(params) => {
|
||||
file_system
|
||||
.remove(
|
||||
¶ms.path,
|
||||
RemoveOptions {
|
||||
recursive: params.recursive.unwrap_or(true),
|
||||
force: params.force.unwrap_or(true),
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsHelperPayload::Remove(FsRemoveResponse {}))
|
||||
}
|
||||
FsHelperRequest::Copy(params) => {
|
||||
file_system
|
||||
.copy(
|
||||
¶ms.source_path,
|
||||
¶ms.destination_path,
|
||||
CopyOptions {
|
||||
recursive: params.recursive,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsHelperPayload::Copy(FsCopyResponse {}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
|
||||
match err.kind() {
|
||||
io::ErrorKind::NotFound => not_found(err.to_string()),
|
||||
io::ErrorKind::InvalidInput | io::ErrorKind::PermissionDenied => {
|
||||
invalid_request(err.to_string())
|
||||
}
|
||||
_ => internal_error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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"),
|
||||
data_base64: String::new(),
|
||||
sandbox: None,
|
||||
}))?["operation"],
|
||||
FS_WRITE_FILE_METHOD,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use std::error::Error;
|
||||
|
||||
use tokio::io;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::fs_helper::FsHelperRequest;
|
||||
use crate::fs_helper::FsHelperResponse;
|
||||
use crate::fs_helper::run_direct_request;
|
||||
|
||||
pub fn main() -> ! {
|
||||
let exit_code = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(runtime) => match runtime.block_on(run_main()) {
|
||||
Ok(()) => 0,
|
||||
Err(err) => {
|
||||
eprintln!("fs sandbox helper failed: {err}");
|
||||
1
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("failed to start fs sandbox helper runtime: {err}");
|
||||
1
|
||||
}
|
||||
};
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
async fn run_main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let mut input = Vec::new();
|
||||
io::stdin().read_to_end(&mut input).await?;
|
||||
let request: FsHelperRequest = serde_json::from_slice(&input)?;
|
||||
let response = match run_direct_request(request).await {
|
||||
Ok(payload) => FsHelperResponse::Ok(payload),
|
||||
Err(error) => FsHelperResponse::Error(error),
|
||||
};
|
||||
let mut stdout = io::stdout();
|
||||
stdout
|
||||
.write_all(serde_json::to_string(&response)?.as_bytes())
|
||||
.await?;
|
||||
stdout.write_all(b"\n").await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::canonicalize_preserving_symlinks;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::fs_helper::CODEX_FS_HELPER_ARG1;
|
||||
use crate::fs_helper::FsHelperPayload;
|
||||
use crate::fs_helper::FsHelperRequest;
|
||||
use crate::fs_helper::FsHelperResponse;
|
||||
use crate::local_file_system::current_sandbox_cwd;
|
||||
use crate::local_file_system::resolve_existing_path;
|
||||
use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
use crate::protocol::FsReadDirectoryParams;
|
||||
use crate::protocol::FsReadFileParams;
|
||||
use crate::protocol::FsRemoveParams;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct FileSystemSandboxRunner {
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
}
|
||||
|
||||
impl FileSystemSandboxRunner {
|
||||
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self { runtime_paths }
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
&self,
|
||||
sandbox: &FileSystemSandboxContext,
|
||||
request: FsHelperRequest,
|
||||
) -> Result<FsHelperPayload, JSONRPCErrorError> {
|
||||
let request_sandbox_policy =
|
||||
normalize_sandbox_policy_root_aliases(sandbox.sandbox_policy.clone());
|
||||
let helper_sandbox_policy = normalize_sandbox_policy_root_aliases(
|
||||
sandbox_policy_with_helper_runtime_defaults(&sandbox.sandbox_policy),
|
||||
);
|
||||
let cwd = current_sandbox_cwd().map_err(io_error)?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd.as_path())
|
||||
.map_err(|err| invalid_request(format!("current directory is not absolute: {err}")))?;
|
||||
let request_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
&request_sandbox_policy,
|
||||
cwd.as_path(),
|
||||
);
|
||||
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
&helper_sandbox_policy,
|
||||
cwd.as_path(),
|
||||
);
|
||||
let request = resolve_request_paths(request, &request_file_system_policy, &cwd)?;
|
||||
let network_policy = NetworkSandboxPolicy::Restricted;
|
||||
let command = self.sandbox_exec_request(
|
||||
&helper_sandbox_policy,
|
||||
&file_system_policy,
|
||||
network_policy,
|
||||
&cwd,
|
||||
sandbox,
|
||||
)?;
|
||||
let request_json = serde_json::to_vec(&request).map_err(json_error)?;
|
||||
run_command(command, request_json).await
|
||||
}
|
||||
|
||||
fn sandbox_exec_request(
|
||||
&self,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
network_policy: NetworkSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
sandbox_context: &FileSystemSandboxContext,
|
||||
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
||||
let helper = &self.runtime_paths.codex_self_exe;
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
file_system_policy,
|
||||
network_policy,
|
||||
SandboxablePreference::Auto,
|
||||
sandbox_context.windows_sandbox_level,
|
||||
/*has_managed_network_requirements*/ false,
|
||||
);
|
||||
let command = SandboxCommand {
|
||||
program: helper.as_path().as_os_str().to_owned(),
|
||||
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
additional_permissions: Some(
|
||||
self.helper_permissions(sandbox_context.additional_permissions.as_ref()),
|
||||
),
|
||||
};
|
||||
sandbox_manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command,
|
||||
policy: sandbox_policy,
|
||||
file_system_policy,
|
||||
network_policy,
|
||||
sandbox,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
||||
})
|
||||
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))
|
||||
}
|
||||
|
||||
fn helper_permissions(
|
||||
&self,
|
||||
additional_permissions: Option<&PermissionProfile>,
|
||||
) -> PermissionProfile {
|
||||
PermissionProfile {
|
||||
network: None,
|
||||
file_system: additional_permissions
|
||||
.and_then(|permissions| permissions.file_system.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_request_paths(
|
||||
request: FsHelperRequest,
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> Result<FsHelperRequest, JSONRPCErrorError> {
|
||||
match request {
|
||||
FsHelperRequest::ReadFile(FsReadFileParams { path, sandbox }) => {
|
||||
let path = resolve_sandbox_path(&path, PreserveTerminalSymlink::No)?;
|
||||
ensure_path_access(file_system_policy, cwd, &path, FileSystemAccessMode::Read)?;
|
||||
Ok(FsHelperRequest::ReadFile(FsReadFileParams {
|
||||
path,
|
||||
sandbox,
|
||||
}))
|
||||
}
|
||||
FsHelperRequest::WriteFile(FsWriteFileParams {
|
||||
path,
|
||||
data_base64,
|
||||
sandbox,
|
||||
}) => Ok(FsHelperRequest::WriteFile(FsWriteFileParams {
|
||||
path: {
|
||||
let path = resolve_sandbox_path(&path, PreserveTerminalSymlink::No)?;
|
||||
ensure_path_access(file_system_policy, cwd, &path, FileSystemAccessMode::Write)?;
|
||||
path
|
||||
},
|
||||
data_base64,
|
||||
sandbox,
|
||||
})),
|
||||
FsHelperRequest::CreateDirectory(FsCreateDirectoryParams {
|
||||
path,
|
||||
recursive,
|
||||
sandbox,
|
||||
}) => Ok(FsHelperRequest::CreateDirectory(FsCreateDirectoryParams {
|
||||
path: {
|
||||
let path = resolve_sandbox_path(&path, PreserveTerminalSymlink::No)?;
|
||||
ensure_path_access(file_system_policy, cwd, &path, FileSystemAccessMode::Write)?;
|
||||
path
|
||||
},
|
||||
recursive,
|
||||
sandbox,
|
||||
})),
|
||||
FsHelperRequest::GetMetadata(FsGetMetadataParams { path, sandbox }) => {
|
||||
let path = resolve_sandbox_path(&path, PreserveTerminalSymlink::No)?;
|
||||
ensure_path_access(file_system_policy, cwd, &path, FileSystemAccessMode::Read)?;
|
||||
Ok(FsHelperRequest::GetMetadata(FsGetMetadataParams {
|
||||
path,
|
||||
sandbox,
|
||||
}))
|
||||
}
|
||||
FsHelperRequest::ReadDirectory(FsReadDirectoryParams { path, sandbox }) => {
|
||||
let path = resolve_sandbox_path(&path, PreserveTerminalSymlink::No)?;
|
||||
ensure_path_access(file_system_policy, cwd, &path, FileSystemAccessMode::Read)?;
|
||||
Ok(FsHelperRequest::ReadDirectory(FsReadDirectoryParams {
|
||||
path,
|
||||
sandbox,
|
||||
}))
|
||||
}
|
||||
FsHelperRequest::Remove(FsRemoveParams {
|
||||
path,
|
||||
recursive,
|
||||
force,
|
||||
sandbox,
|
||||
}) => Ok(FsHelperRequest::Remove(FsRemoveParams {
|
||||
path: {
|
||||
let path = resolve_sandbox_path(&path, PreserveTerminalSymlink::Yes)?;
|
||||
ensure_path_access(file_system_policy, cwd, &path, FileSystemAccessMode::Write)?;
|
||||
path
|
||||
},
|
||||
recursive,
|
||||
force,
|
||||
sandbox,
|
||||
})),
|
||||
FsHelperRequest::Copy(FsCopyParams {
|
||||
source_path,
|
||||
destination_path,
|
||||
recursive,
|
||||
sandbox,
|
||||
}) => Ok(FsHelperRequest::Copy(FsCopyParams {
|
||||
source_path: {
|
||||
let source_path = resolve_sandbox_path(&source_path, PreserveTerminalSymlink::Yes)?;
|
||||
ensure_path_access(
|
||||
file_system_policy,
|
||||
cwd,
|
||||
&source_path,
|
||||
FileSystemAccessMode::Read,
|
||||
)?;
|
||||
source_path
|
||||
},
|
||||
destination_path: {
|
||||
let destination_path =
|
||||
resolve_sandbox_path(&destination_path, PreserveTerminalSymlink::No)?;
|
||||
ensure_path_access(
|
||||
file_system_policy,
|
||||
cwd,
|
||||
&destination_path,
|
||||
FileSystemAccessMode::Write,
|
||||
)?;
|
||||
destination_path
|
||||
},
|
||||
recursive,
|
||||
sandbox,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PreserveTerminalSymlink {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
fn resolve_sandbox_path(
|
||||
path: &AbsolutePathBuf,
|
||||
preserve_terminal_symlink: PreserveTerminalSymlink,
|
||||
) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
|
||||
if matches!(preserve_terminal_symlink, PreserveTerminalSymlink::Yes)
|
||||
&& std::fs::symlink_metadata(path.as_path())
|
||||
.map(|metadata| metadata.file_type().is_symlink())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(normalize_top_level_alias(path.clone()));
|
||||
}
|
||||
|
||||
let resolved = resolve_existing_path(path.as_path()).map_err(io_error)?;
|
||||
absolute_path(resolved)
|
||||
}
|
||||
|
||||
fn normalize_sandbox_policy_root_aliases(sandbox_policy: SandboxPolicy) -> SandboxPolicy {
|
||||
let mut sandbox_policy = sandbox_policy;
|
||||
match &mut sandbox_policy {
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted { readable_roots, .. },
|
||||
..
|
||||
} => {
|
||||
normalize_root_aliases(readable_roots);
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
read_only_access,
|
||||
..
|
||||
} => {
|
||||
normalize_root_aliases(writable_roots);
|
||||
if let ReadOnlyAccess::Restricted { readable_roots, .. } = read_only_access {
|
||||
normalize_root_aliases(readable_roots);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
sandbox_policy
|
||||
}
|
||||
|
||||
fn normalize_root_aliases(paths: &mut Vec<AbsolutePathBuf>) {
|
||||
for path in paths {
|
||||
*path = normalize_top_level_alias(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_top_level_alias(path: AbsolutePathBuf) -> AbsolutePathBuf {
|
||||
let raw_path = path.to_path_buf();
|
||||
for ancestor in raw_path.ancestors() {
|
||||
if std::fs::symlink_metadata(ancestor).is_err() {
|
||||
continue;
|
||||
}
|
||||
let Ok(normalized_ancestor) = canonicalize_preserving_symlinks(ancestor) else {
|
||||
continue;
|
||||
};
|
||||
if normalized_ancestor == ancestor {
|
||||
continue;
|
||||
}
|
||||
let Ok(suffix) = raw_path.strip_prefix(ancestor) else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(normalized_path) =
|
||||
AbsolutePathBuf::from_absolute_path(normalized_ancestor.join(suffix))
|
||||
{
|
||||
return normalized_path;
|
||||
}
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn absolute_path(path: PathBuf) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
|
||||
AbsolutePathBuf::from_absolute_path(path.as_path())
|
||||
.map_err(|err| invalid_request(format!("resolved sandbox path is not absolute: {err}")))
|
||||
}
|
||||
|
||||
fn ensure_path_access(
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
path: &AbsolutePathBuf,
|
||||
required_access: FileSystemAccessMode,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let actual_access = file_system_policy.resolve_access_with_cwd(path.as_path(), cwd.as_path());
|
||||
let permitted = match required_access {
|
||||
FileSystemAccessMode::Read => actual_access.can_read(),
|
||||
FileSystemAccessMode::Write => actual_access.can_write(),
|
||||
FileSystemAccessMode::None => true,
|
||||
};
|
||||
if permitted {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(invalid_request(format!(
|
||||
"{} is not permitted by filesystem sandbox",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
async fn run_command(
|
||||
command: SandboxExecRequest,
|
||||
request_json: Vec<u8>,
|
||||
) -> Result<FsHelperPayload, JSONRPCErrorError> {
|
||||
let mut child = spawn_command(command)?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| internal_error("failed to open fs sandbox helper stdin".to_string()))?;
|
||||
stdin.write_all(&request_json).await.map_err(io_error)?;
|
||||
stdin.shutdown().await.map_err(io_error)?;
|
||||
drop(stdin);
|
||||
|
||||
let output = child.wait_with_output().await.map_err(io_error)?;
|
||||
if !output.status.success() {
|
||||
return Err(internal_error(format!(
|
||||
"fs sandbox helper failed with status {status}: {stderr}",
|
||||
status = output.status,
|
||||
stderr = String::from_utf8_lossy(&output.stderr).trim()
|
||||
)));
|
||||
}
|
||||
let response: FsHelperResponse = serde_json::from_slice(&output.stdout).map_err(json_error)?;
|
||||
match response {
|
||||
FsHelperResponse::Ok(payload) => Ok(payload),
|
||||
FsHelperResponse::Error(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_command(
|
||||
SandboxExecRequest {
|
||||
command: argv,
|
||||
cwd,
|
||||
env,
|
||||
arg0,
|
||||
..
|
||||
}: SandboxExecRequest,
|
||||
) -> Result<tokio::process::Child, JSONRPCErrorError> {
|
||||
let Some((program, args)) = argv.split_first() else {
|
||||
return Err(invalid_request("fs sandbox command was empty".to_string()));
|
||||
};
|
||||
let mut command = Command::new(program);
|
||||
#[cfg(unix)]
|
||||
if let Some(arg0) = arg0 {
|
||||
command.arg0(arg0);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = arg0;
|
||||
command.args(args);
|
||||
command.current_dir(cwd.as_path());
|
||||
command.env_clear();
|
||||
command.envs(env);
|
||||
command.stdin(std::process::Stdio::piped());
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
command.stderr(std::process::Stdio::piped());
|
||||
command.spawn().map_err(io_error)
|
||||
}
|
||||
|
||||
fn sandbox_policy_with_helper_runtime_defaults(sandbox_policy: &SandboxPolicy) -> SandboxPolicy {
|
||||
let mut sandbox_policy = sandbox_policy.clone();
|
||||
match &mut sandbox_policy {
|
||||
SandboxPolicy::ReadOnly { access, .. } => enable_platform_defaults(access),
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
read_only_access, ..
|
||||
} => enable_platform_defaults(read_only_access),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {}
|
||||
}
|
||||
sandbox_policy
|
||||
}
|
||||
|
||||
fn enable_platform_defaults(access: &mut ReadOnlyAccess) {
|
||||
if let ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults,
|
||||
..
|
||||
} = access
|
||||
{
|
||||
*include_platform_defaults = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn io_error(err: std::io::Error) -> JSONRPCErrorError {
|
||||
internal_error(err.to_string())
|
||||
}
|
||||
|
||||
fn json_error(err: serde_json::Error) -> JSONRPCErrorError {
|
||||
internal_error(format!(
|
||||
"failed to encode or decode fs sandbox helper message: {err}"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
|
||||
use super::FileSystemSandboxRunner;
|
||||
use super::sandbox_policy_with_helper_runtime_defaults;
|
||||
|
||||
#[test]
|
||||
fn helper_sandbox_policy_enables_platform_defaults_for_read_only_access() {
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
};
|
||||
|
||||
let updated = sandbox_policy_with_helper_runtime_defaults(&sandbox_policy);
|
||||
|
||||
assert_eq!(
|
||||
updated,
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn helper_sandbox_policy_enables_platform_defaults_for_workspace_read_access() {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
|
||||
let updated = sandbox_policy_with_helper_runtime_defaults(&sandbox_policy);
|
||||
|
||||
assert_eq!(
|
||||
updated,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: Vec::new(),
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn helper_permissions_strip_network_grants() {
|
||||
let codex_self_exe = std::env::current_exe().expect("current exe");
|
||||
let runtime_paths = ExecServerRuntimePaths::new(
|
||||
codex_self_exe.clone(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
let runner = FileSystemSandboxRunner::new(runtime_paths);
|
||||
let readable = AbsolutePathBuf::from_absolute_path(
|
||||
codex_self_exe.parent().expect("current exe parent"),
|
||||
)
|
||||
.expect("absolute readable path");
|
||||
let writable = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path())
|
||||
.expect("absolute writable path");
|
||||
|
||||
let permissions = runner.helper_permissions(Some(&PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![readable.clone()]),
|
||||
write: Some(vec![writable.clone()]),
|
||||
}),
|
||||
}));
|
||||
|
||||
assert_eq!(permissions.network, None);
|
||||
assert_eq!(
|
||||
permissions
|
||||
.file_system
|
||||
.as_ref()
|
||||
.and_then(|fs| fs.write.clone()),
|
||||
Some(vec![writable])
|
||||
);
|
||||
assert_eq!(
|
||||
permissions
|
||||
.file_system
|
||||
.as_ref()
|
||||
.and_then(|fs| fs.read.clone()),
|
||||
Some(vec![readable])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ mod client_api;
|
||||
mod connection;
|
||||
mod environment;
|
||||
mod file_system;
|
||||
mod fs_helper;
|
||||
mod fs_helper_main;
|
||||
mod fs_sandbox;
|
||||
mod local_file_system;
|
||||
mod local_process;
|
||||
mod process;
|
||||
@@ -11,6 +14,8 @@ mod protocol;
|
||||
mod remote_file_system;
|
||||
mod remote_process;
|
||||
mod rpc;
|
||||
mod runtime_paths;
|
||||
mod sandboxed_file_system;
|
||||
mod server;
|
||||
|
||||
pub use client::ExecServerClient;
|
||||
@@ -25,9 +30,13 @@ pub use file_system::CreateDirectoryOptions;
|
||||
pub use file_system::ExecutorFileSystem;
|
||||
pub use file_system::FileMetadata;
|
||||
pub use file_system::FileSystemResult;
|
||||
pub use file_system::FileSystemSandboxContext;
|
||||
pub use file_system::ReadDirectoryEntry;
|
||||
pub use file_system::RemoveOptions;
|
||||
pub use fs_helper::CODEX_FS_HELPER_ARG1;
|
||||
pub use fs_helper_main::main as run_fs_helper_main;
|
||||
pub use local_file_system::LOCAL_FS;
|
||||
pub use local_file_system::LocalFileSystem;
|
||||
pub use process::ExecBackend;
|
||||
pub use process::ExecProcess;
|
||||
pub use process::StartedExecProcess;
|
||||
@@ -62,7 +71,7 @@ pub use protocol::TerminateResponse;
|
||||
pub use protocol::WriteParams;
|
||||
pub use protocol::WriteResponse;
|
||||
pub use protocol::WriteStatus;
|
||||
pub use runtime_paths::ExecServerRuntimePaths;
|
||||
pub use server::DEFAULT_LISTEN_URL;
|
||||
pub use server::ExecServerListenUrlParseError;
|
||||
pub use server::run_main;
|
||||
pub use server::run_main_with_listen_url;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -13,23 +10,240 @@ use tokio::io;
|
||||
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::FileMetadata;
|
||||
use crate::FileSystemResult;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
use crate::sandboxed_file_system::SandboxedFileSystem;
|
||||
|
||||
const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024;
|
||||
|
||||
pub static LOCAL_FS: LazyLock<Arc<dyn ExecutorFileSystem>> =
|
||||
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem) });
|
||||
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { Arc::new(LocalFileSystem::unsandboxed()) });
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct LocalFileSystem;
|
||||
pub(crate) struct DirectFileSystem;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct UnsandboxedFileSystem {
|
||||
file_system: DirectFileSystem,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LocalFileSystem {
|
||||
unsandboxed: UnsandboxedFileSystem,
|
||||
sandboxed: Option<SandboxedFileSystem>,
|
||||
}
|
||||
|
||||
impl LocalFileSystem {
|
||||
pub fn unsandboxed() -> Self {
|
||||
Self {
|
||||
unsandboxed: UnsandboxedFileSystem::default(),
|
||||
sandboxed: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_runtime_paths(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
unsandboxed: UnsandboxedFileSystem::default(),
|
||||
sandboxed: Some(SandboxedFileSystem::new(runtime_paths)),
|
||||
}
|
||||
}
|
||||
|
||||
fn sandboxed(&self) -> io::Result<&SandboxedFileSystem> {
|
||||
self.sandboxed.as_ref().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"sandboxed filesystem operations require configured runtime paths",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn file_system_for<'a>(
|
||||
&'a self,
|
||||
sandbox: Option<&'a FileSystemSandboxContext>,
|
||||
) -> io::Result<(
|
||||
&'a dyn ExecutorFileSystem,
|
||||
Option<&'a FileSystemSandboxContext>,
|
||||
)> {
|
||||
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
||||
Ok((self.sandboxed()?, sandbox))
|
||||
} else {
|
||||
Ok((&self.unsandboxed, sandbox))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for LocalFileSystem {
|
||||
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>> {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.read_file(path, sandbox).await
|
||||
}
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.write_file(path, contents, sandbox).await
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.create_directory(path, options, sandbox).await
|
||||
}
|
||||
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.get_metadata(path, sandbox).await
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.read_directory(path, sandbox).await
|
||||
}
|
||||
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system.remove(path, options, sandbox).await
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let (file_system, sandbox) = self.file_system_for(sandbox)?;
|
||||
file_system
|
||||
.copy(source_path, destination_path, options, sandbox)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for UnsandboxedFileSystem {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system.read_file(path, /*sandbox*/ None).await
|
||||
}
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.write_file(path, contents, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.create_directory(path, options, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system.get_metadata(path, /*sandbox*/ None).await
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.read_directory(path, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.remove(path, options, /*sandbox*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_platform_sandbox_context(sandbox)?;
|
||||
self.file_system
|
||||
.copy(
|
||||
source_path,
|
||||
destination_path,
|
||||
options,
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for DirectFileSystem {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let metadata = tokio::fs::metadata(path.as_path()).await?;
|
||||
if metadata.len() > MAX_READ_FILE_BYTES {
|
||||
return Err(io::Error::new(
|
||||
@@ -40,34 +254,23 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
tokio::fs::read(path.as_path()).await
|
||||
}
|
||||
|
||||
async fn read_file_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
enforce_read_access(path, sandbox_policy)?;
|
||||
self.read_file(path).await
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
|
||||
tokio::fs::write(path.as_path(), contents).await
|
||||
}
|
||||
|
||||
async fn write_file_with_sandbox_policy(
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_write_access(path, sandbox_policy)?;
|
||||
self.write_file(path, contents).await
|
||||
reject_sandbox_context(sandbox)?;
|
||||
tokio::fs::write(path.as_path(), contents).await
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
if options.recursive {
|
||||
tokio::fs::create_dir_all(path.as_path()).await?;
|
||||
} else {
|
||||
@@ -76,17 +279,12 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_directory_with_sandbox_policy(
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
create_directory_options: CreateDirectoryOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_write_access(path, sandbox_policy)?;
|
||||
self.create_directory(path, create_directory_options).await
|
||||
}
|
||||
|
||||
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
let metadata = tokio::fs::metadata(path.as_path()).await?;
|
||||
Ok(FileMetadata {
|
||||
is_directory: metadata.is_dir(),
|
||||
@@ -96,19 +294,12 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_metadata_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
enforce_read_access(path, sandbox_policy)?;
|
||||
self.get_metadata(path).await
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
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? {
|
||||
@@ -122,16 +313,13 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn read_directory_with_sandbox_policy(
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
enforce_read_access(path, sandbox_policy)?;
|
||||
self.read_directory(path).await
|
||||
}
|
||||
|
||||
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
reject_sandbox_context(sandbox)?;
|
||||
match tokio::fs::symlink_metadata(path.as_path()).await {
|
||||
Ok(metadata) => {
|
||||
let file_type = metadata.file_type();
|
||||
@@ -151,22 +339,14 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
remove_options: RemoveOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_write_access_preserving_leaf(path, sandbox_policy)?;
|
||||
self.remove(path, remove_options).await
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
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();
|
||||
tokio::task::spawn_blocking(move || -> FileSystemResult<()> {
|
||||
@@ -211,164 +391,26 @@ impl ExecutorFileSystem for LocalFileSystem {
|
||||
.await
|
||||
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
|
||||
}
|
||||
|
||||
async fn copy_with_sandbox_policy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
copy_options: CopyOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_copy_source_read_access(source_path, sandbox_policy)?;
|
||||
enforce_write_access(destination_path, sandbox_policy)?;
|
||||
self.copy(source_path, destination_path, copy_options).await
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_read_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_read_path_with_cwd,
|
||||
"read",
|
||||
AccessPathMode::ResolveAll,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_write_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_write_path_with_cwd,
|
||||
"write",
|
||||
AccessPathMode::ResolveAll,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_write_access_preserving_leaf(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_write_path_with_cwd,
|
||||
"write",
|
||||
AccessPathMode::PreserveLeaf,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_copy_source_read_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
let path_mode = match std::fs::symlink_metadata(path.as_path()) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => AccessPathMode::PreserveLeaf,
|
||||
_ => AccessPathMode::ResolveAll,
|
||||
};
|
||||
enforce_access_for_current_dir(
|
||||
path,
|
||||
sandbox_policy,
|
||||
FileSystemSandboxPolicy::can_read_path_with_cwd,
|
||||
"read",
|
||||
path_mode,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
fn enforce_read_access_for_cwd(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
) -> FileSystemResult<()> {
|
||||
enforce_access_for_cwd(
|
||||
path,
|
||||
sandbox_policy,
|
||||
sandbox_cwd,
|
||||
FileSystemSandboxPolicy::can_read_path_with_cwd,
|
||||
"read",
|
||||
AccessPathMode::ResolveAll,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_access_for_current_dir(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
|
||||
access_kind: &str,
|
||||
path_mode: AccessPathMode,
|
||||
) -> FileSystemResult<()> {
|
||||
let Some(sandbox_policy) = sandbox_policy else {
|
||||
return Ok(());
|
||||
};
|
||||
let cwd = current_sandbox_cwd()?;
|
||||
enforce_access(
|
||||
path,
|
||||
sandbox_policy,
|
||||
cwd.as_path(),
|
||||
is_allowed,
|
||||
access_kind,
|
||||
path_mode,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
fn enforce_access_for_cwd(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
|
||||
access_kind: &str,
|
||||
path_mode: AccessPathMode,
|
||||
) -> FileSystemResult<()> {
|
||||
let Some(sandbox_policy) = sandbox_policy else {
|
||||
return Ok(());
|
||||
};
|
||||
let cwd = resolve_existing_path(sandbox_cwd.as_path())?;
|
||||
enforce_access(
|
||||
path,
|
||||
sandbox_policy,
|
||||
cwd.as_path(),
|
||||
is_allowed,
|
||||
access_kind,
|
||||
path_mode,
|
||||
)
|
||||
}
|
||||
|
||||
fn enforce_access(
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
|
||||
access_kind: &str,
|
||||
path_mode: AccessPathMode,
|
||||
) -> FileSystemResult<()> {
|
||||
let resolved_path = resolve_path_for_access_check(path.as_path(), path_mode)?;
|
||||
let file_system_policy =
|
||||
canonicalize_file_system_policy_paths(FileSystemSandboxPolicy::from(sandbox_policy))?;
|
||||
if is_allowed(&file_system_policy, resolved_path.as_path(), sandbox_cwd) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
||||
if sandbox.is_some() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"fs/{access_kind} is not permitted for path {}",
|
||||
path.as_path().display()
|
||||
),
|
||||
))
|
||||
"direct filesystem operations do not accept sandbox context",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum AccessPathMode {
|
||||
ResolveAll,
|
||||
PreserveLeaf,
|
||||
fn reject_platform_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> {
|
||||
if sandbox.is_some_and(FileSystemSandboxContext::should_run_in_sandbox) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"sandboxed filesystem operations require configured runtime paths",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {
|
||||
@@ -395,28 +437,11 @@ fn destination_is_same_or_descendant_of_source(
|
||||
destination: &Path,
|
||||
) -> io::Result<bool> {
|
||||
let source = std::fs::canonicalize(source)?;
|
||||
let destination = resolve_path_for_access_check(destination, AccessPathMode::ResolveAll)?;
|
||||
let destination = resolve_existing_path(destination)?;
|
||||
Ok(destination.starts_with(&source))
|
||||
}
|
||||
|
||||
fn resolve_path_for_access_check(path: &Path, path_mode: AccessPathMode) -> io::Result<PathBuf> {
|
||||
match path_mode {
|
||||
AccessPathMode::ResolveAll => resolve_existing_path(path),
|
||||
AccessPathMode::PreserveLeaf => preserve_leaf_path_for_access_check(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_leaf_path_for_access_check(path: &Path) -> io::Result<PathBuf> {
|
||||
let Some(file_name) = path.file_name() else {
|
||||
return resolve_existing_path(path);
|
||||
};
|
||||
let parent = path.parent().unwrap_or_else(|| Path::new("/"));
|
||||
let mut resolved_parent = resolve_existing_path(parent)?;
|
||||
resolved_parent.push(file_name);
|
||||
Ok(resolved_parent)
|
||||
}
|
||||
|
||||
fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
||||
pub(crate) fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
||||
let mut unresolved_suffix = Vec::new();
|
||||
let mut existing_path = path;
|
||||
while !existing_path.exists() {
|
||||
@@ -437,33 +462,12 @@ fn resolve_existing_path(path: &Path) -> io::Result<PathBuf> {
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn current_sandbox_cwd() -> io::Result<PathBuf> {
|
||||
pub(crate) fn current_sandbox_cwd() -> io::Result<PathBuf> {
|
||||
let cwd = std::env::current_dir()
|
||||
.map_err(|err| io::Error::other(format!("failed to read current dir: {err}")))?;
|
||||
resolve_existing_path(cwd.as_path())
|
||||
}
|
||||
|
||||
fn canonicalize_file_system_policy_paths(
|
||||
mut file_system_policy: FileSystemSandboxPolicy,
|
||||
) -> io::Result<FileSystemSandboxPolicy> {
|
||||
for entry in &mut file_system_policy.entries {
|
||||
if let FileSystemPath::Path { path } = &mut entry.path {
|
||||
*path = canonicalize_absolute_path(path)?;
|
||||
}
|
||||
}
|
||||
Ok(file_system_policy)
|
||||
}
|
||||
|
||||
fn canonicalize_absolute_path(path: &AbsolutePathBuf) -> io::Result<AbsolutePathBuf> {
|
||||
let resolved = resolve_existing_path(path.as_path())?;
|
||||
AbsolutePathBuf::from_absolute_path(resolved.as_path()).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("path must stay absolute after canonicalization: {err}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn copy_symlink(source: &Path, target: &Path) -> io::Result<()> {
|
||||
let link_target = std::fs::read_link(source)?;
|
||||
#[cfg(unix)]
|
||||
@@ -508,29 +512,11 @@ fn system_time_to_unix_ms(time: SystemTime) -> i64 {
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
fn absolute_path(path: PathBuf) -> AbsolutePathBuf {
|
||||
match AbsolutePathBuf::try_from(path) {
|
||||
Ok(path) => path,
|
||||
Err(err) => panic!("absolute path: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_only_sandbox_policy(readable_roots: Vec<PathBuf>) -> SandboxPolicy {
|
||||
SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: false,
|
||||
readable_roots: readable_roots.into_iter().map(absolute_path).collect(),
|
||||
},
|
||||
network_access: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_for_access_check_rejects_symlink_parent_dotdot_escape() -> io::Result<()> {
|
||||
fn resolve_existing_path_handles_symlink_parent_dotdot_escape() -> io::Result<()> {
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let allowed_dir = temp_dir.path().join("allowed");
|
||||
let outside_dir = temp_dir.path().join("outside");
|
||||
@@ -538,13 +524,12 @@ mod tests {
|
||||
std::fs::create_dir_all(&outside_dir)?;
|
||||
symlink(&outside_dir, allowed_dir.join("link"))?;
|
||||
|
||||
let resolved = resolve_path_for_access_check(
|
||||
let resolved = resolve_existing_path(
|
||||
allowed_dir
|
||||
.join("link")
|
||||
.join("..")
|
||||
.join("secret.txt")
|
||||
.as_path(),
|
||||
AccessPathMode::ResolveAll,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
@@ -553,29 +538,6 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforce_read_access_uses_explicit_sandbox_cwd() -> io::Result<()> {
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let workspace_dir = temp_dir.path().join("workspace");
|
||||
let other_dir = temp_dir.path().join("other");
|
||||
let note_path = workspace_dir.join("note.txt");
|
||||
std::fs::create_dir_all(&workspace_dir)?;
|
||||
std::fs::create_dir_all(&other_dir)?;
|
||||
std::fs::write(¬e_path, "hello")?;
|
||||
|
||||
let sandbox_policy = read_only_sandbox_policy(vec![]);
|
||||
let sandbox_cwd = absolute_path(workspace_dir);
|
||||
let other_cwd = absolute_path(other_dir);
|
||||
let note_path = absolute_path(note_path);
|
||||
|
||||
enforce_read_access_for_cwd(¬e_path, Some(&sandbox_policy), &sandbox_cwd)?;
|
||||
|
||||
let error = enforce_read_access_for_cwd(¬e_path, Some(&sandbox_policy), &other_cwd)
|
||||
.expect_err("read should be rejected outside provided cwd");
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::FileSystemSandboxContext;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -141,7 +141,7 @@ pub struct TerminateResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsReadFileParams {
|
||||
pub path: AbsolutePathBuf,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -155,7 +155,7 @@ pub struct FsReadFileResponse {
|
||||
pub struct FsWriteFileParams {
|
||||
pub path: AbsolutePathBuf,
|
||||
pub data_base64: String,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -167,7 +167,7 @@ pub struct FsWriteFileResponse {}
|
||||
pub struct FsCreateDirectoryParams {
|
||||
pub path: AbsolutePathBuf,
|
||||
pub recursive: Option<bool>,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -178,7 +178,7 @@ pub struct FsCreateDirectoryResponse {}
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsGetMetadataParams {
|
||||
pub path: AbsolutePathBuf,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -194,7 +194,7 @@ pub struct FsGetMetadataResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FsReadDirectoryParams {
|
||||
pub path: AbsolutePathBuf,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -217,7 +217,7 @@ pub struct FsRemoveParams {
|
||||
pub path: AbsolutePathBuf,
|
||||
pub recursive: Option<bool>,
|
||||
pub force: Option<bool>,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -230,7 +230,7 @@ pub struct FsCopyParams {
|
||||
pub source_path: AbsolutePathBuf,
|
||||
pub destination_path: AbsolutePathBuf,
|
||||
pub recursive: bool,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub sandbox: Option<FileSystemSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use tokio::io;
|
||||
use tracing::trace;
|
||||
@@ -13,6 +12,7 @@ use crate::ExecServerError;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::FileMetadata;
|
||||
use crate::FileSystemResult;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
use crate::protocol::FsCopyParams;
|
||||
@@ -40,13 +40,17 @@ impl RemoteFileSystem {
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for RemoteFileSystem {
|
||||
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>> {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
trace!("remote fs read_file");
|
||||
let response = self
|
||||
.client
|
||||
.fs_read_file(FsReadFileParams {
|
||||
path: path.clone(),
|
||||
sandbox_policy: None,
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
@@ -58,53 +62,18 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_file_with_sandbox_policy(
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
trace!("remote fs read_file_with_sandbox_policy");
|
||||
let response = self
|
||||
.client
|
||||
.fs_read_file(FsReadFileParams {
|
||||
path: path.clone(),
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
STANDARD.decode(response.data_base64).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("remote fs/readFile returned invalid base64 dataBase64: {err}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs write_file");
|
||||
self.client
|
||||
.fs_write_file(FsWriteFileParams {
|
||||
path: path.clone(),
|
||||
data_base64: STANDARD.encode(contents),
|
||||
sandbox_policy: None,
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_file_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs write_file_with_sandbox_policy");
|
||||
self.client
|
||||
.fs_write_file(FsWriteFileParams {
|
||||
path: path.clone(),
|
||||
data_base64: STANDARD.encode(contents),
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
@@ -115,66 +84,31 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs create_directory");
|
||||
self.client
|
||||
.fs_create_directory(FsCreateDirectoryParams {
|
||||
path: path.clone(),
|
||||
recursive: Some(options.recursive),
|
||||
sandbox_policy: None,
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_directory_with_sandbox_policy(
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
create_directory_options: CreateDirectoryOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs create_directory_with_sandbox_policy");
|
||||
self.client
|
||||
.fs_create_directory(FsCreateDirectoryParams {
|
||||
path: path.clone(),
|
||||
recursive: Some(create_directory_options.recursive),
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
trace!("remote fs get_metadata");
|
||||
let response = self
|
||||
.client
|
||||
.fs_get_metadata(FsGetMetadataParams {
|
||||
path: path.clone(),
|
||||
sandbox_policy: None,
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(FileMetadata {
|
||||
is_directory: response.is_directory,
|
||||
is_file: response.is_file,
|
||||
created_at_ms: response.created_at_ms,
|
||||
modified_at_ms: response.modified_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_metadata_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
trace!("remote fs get_metadata_with_sandbox_policy");
|
||||
let response = self
|
||||
.client
|
||||
.fs_get_metadata(FsGetMetadataParams {
|
||||
path: path.clone(),
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
@@ -189,13 +123,14 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
trace!("remote fs read_directory");
|
||||
let response = self
|
||||
.client
|
||||
.fs_read_directory(FsReadDirectoryParams {
|
||||
path: path.clone(),
|
||||
sandbox_policy: None,
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
@@ -210,58 +145,19 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn read_directory_with_sandbox_policy(
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
trace!("remote fs read_directory_with_sandbox_policy");
|
||||
let response = self
|
||||
.client
|
||||
.fs_read_directory(FsReadDirectoryParams {
|
||||
path: path.clone(),
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(response
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| ReadDirectoryEntry {
|
||||
file_name: entry.file_name,
|
||||
is_directory: entry.is_directory,
|
||||
is_file: entry.is_file,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
|
||||
options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs remove");
|
||||
self.client
|
||||
.fs_remove(FsRemoveParams {
|
||||
path: path.clone(),
|
||||
recursive: Some(options.recursive),
|
||||
force: Some(options.force),
|
||||
sandbox_policy: None,
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_with_sandbox_policy(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
remove_options: RemoveOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs remove_with_sandbox_policy");
|
||||
self.client
|
||||
.fs_remove(FsRemoveParams {
|
||||
path: path.clone(),
|
||||
recursive: Some(remove_options.recursive),
|
||||
force: Some(remove_options.force),
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
@@ -273,6 +169,7 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs copy");
|
||||
self.client
|
||||
@@ -280,27 +177,7 @@ impl ExecutorFileSystem for RemoteFileSystem {
|
||||
source_path: source_path.clone(),
|
||||
destination_path: destination_path.clone(),
|
||||
recursive: options.recursive,
|
||||
sandbox_policy: None,
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy_with_sandbox_policy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
copy_options: CopyOptions,
|
||||
sandbox_policy: Option<&SandboxPolicy>,
|
||||
) -> FileSystemResult<()> {
|
||||
trace!("remote fs copy_with_sandbox_policy");
|
||||
self.client
|
||||
.fs_copy(FsCopyParams {
|
||||
source_path: source_path.clone(),
|
||||
destination_path: destination_path.clone(),
|
||||
recursive: copy_options.recursive,
|
||||
sandbox_policy: sandbox_policy.cloned(),
|
||||
sandbox: sandbox.cloned(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_remote_error)?;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
/// Runtime paths needed by exec-server child processes.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExecServerRuntimePaths {
|
||||
/// Stable path to the Codex executable used to launch hidden helper modes.
|
||||
pub codex_self_exe: AbsolutePathBuf,
|
||||
/// Path to the Linux sandbox helper alias used when the platform sandbox
|
||||
/// needs to re-enter Codex by argv0.
|
||||
pub codex_linux_sandbox_exe: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
impl ExecServerRuntimePaths {
|
||||
pub fn from_optional_paths(
|
||||
codex_self_exe: Option<PathBuf>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
) -> std::io::Result<Self> {
|
||||
let codex_self_exe = codex_self_exe.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Codex executable path is not configured",
|
||||
)
|
||||
})?;
|
||||
Self::new(codex_self_exe, codex_linux_sandbox_exe)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
codex_self_exe: PathBuf,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
codex_self_exe: absolute_path(codex_self_exe)?,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.map(absolute_path).transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn absolute_path(path: PathBuf) -> std::io::Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::from_absolute_path(path.as_path())
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
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 tokio::io;
|
||||
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::FileMetadata;
|
||||
use crate::FileSystemResult;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::ReadDirectoryEntry;
|
||||
use crate::RemoveOptions;
|
||||
use crate::fs_helper::FsHelperPayload;
|
||||
use crate::fs_helper::FsHelperRequest;
|
||||
use crate::fs_sandbox::FileSystemSandboxRunner;
|
||||
use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
use crate::protocol::FsGetMetadataParams;
|
||||
use crate::protocol::FsReadDirectoryParams;
|
||||
use crate::protocol::FsReadFileParams;
|
||||
use crate::protocol::FsRemoveParams;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SandboxedFileSystem {
|
||||
sandbox_runner: FileSystemSandboxRunner,
|
||||
}
|
||||
|
||||
impl SandboxedFileSystem {
|
||||
pub fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
sandbox_runner: FileSystemSandboxRunner::new(runtime_paths),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_sandboxed(
|
||||
&self,
|
||||
sandbox: &FileSystemSandboxContext,
|
||||
request: FsHelperRequest,
|
||||
) -> FileSystemResult<FsHelperPayload> {
|
||||
self.sandbox_runner
|
||||
.run(sandbox, request)
|
||||
.await
|
||||
.map_err(map_sandbox_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutorFileSystem for SandboxedFileSystem {
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<u8>> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
let response = self
|
||||
.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::ReadFile(FsReadFileParams {
|
||||
path: path.clone(),
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_read_file()
|
||||
.map_err(map_sandbox_error)?;
|
||||
STANDARD.decode(response.data_base64).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("fs/readFile returned invalid base64 dataBase64: {err}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
contents: Vec<u8>,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
self.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::WriteFile(FsWriteFileParams {
|
||||
path: path.clone(),
|
||||
data_base64: STANDARD.encode(contents),
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_write_file()
|
||||
.map_err(map_sandbox_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
options: CreateDirectoryOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
self.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::CreateDirectory(FsCreateDirectoryParams {
|
||||
path: path.clone(),
|
||||
recursive: Some(options.recursive),
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_create_directory()
|
||||
.map_err(map_sandbox_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_metadata(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<FileMetadata> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
let response = self
|
||||
.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::GetMetadata(FsGetMetadataParams {
|
||||
path: path.clone(),
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_get_metadata()
|
||||
.map_err(map_sandbox_error)?;
|
||||
Ok(FileMetadata {
|
||||
is_directory: response.is_directory,
|
||||
is_file: response.is_file,
|
||||
created_at_ms: response.created_at_ms,
|
||||
modified_at_ms: response.modified_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_directory(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
let response = self
|
||||
.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::ReadDirectory(FsReadDirectoryParams {
|
||||
path: path.clone(),
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_read_directory()
|
||||
.map_err(map_sandbox_error)?;
|
||||
Ok(response
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| ReadDirectoryEntry {
|
||||
file_name: entry.file_name,
|
||||
is_directory: entry.is_directory,
|
||||
is_file: entry.is_file,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn remove(
|
||||
&self,
|
||||
path: &AbsolutePathBuf,
|
||||
remove_options: RemoveOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
self.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::Remove(FsRemoveParams {
|
||||
path: path.clone(),
|
||||
recursive: Some(remove_options.recursive),
|
||||
force: Some(remove_options.force),
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_remove()
|
||||
.map_err(map_sandbox_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy(
|
||||
&self,
|
||||
source_path: &AbsolutePathBuf,
|
||||
destination_path: &AbsolutePathBuf,
|
||||
options: CopyOptions,
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<()> {
|
||||
let sandbox = require_platform_sandbox(sandbox)?;
|
||||
self.run_sandboxed(
|
||||
sandbox,
|
||||
FsHelperRequest::Copy(FsCopyParams {
|
||||
source_path: source_path.clone(),
|
||||
destination_path: destination_path.clone(),
|
||||
recursive: options.recursive,
|
||||
sandbox: None,
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.expect_copy()
|
||||
.map_err(map_sandbox_error)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn require_platform_sandbox(
|
||||
sandbox: Option<&FileSystemSandboxContext>,
|
||||
) -> FileSystemResult<&FileSystemSandboxContext> {
|
||||
sandbox
|
||||
.filter(|sandbox| sandbox.should_run_in_sandbox())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"sandboxed filesystem operations require ReadOnly or WorkspaceWrite sandbox policy",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_sandbox_error(error: JSONRPCErrorError) -> io::Error {
|
||||
match error.code {
|
||||
-32004 => io::Error::new(io::ErrorKind::NotFound, error.message),
|
||||
-32600 => io::Error::new(io::ErrorKind::InvalidInput, error.message),
|
||||
_ => io::Error::other(error.message),
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,11 @@ pub(crate) use handler::ExecServerHandler;
|
||||
pub use transport::DEFAULT_LISTEN_URL;
|
||||
pub use transport::ExecServerListenUrlParseError;
|
||||
|
||||
pub async fn run_main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
run_main_with_listen_url(DEFAULT_LISTEN_URL).await
|
||||
}
|
||||
use crate::ExecServerRuntimePaths;
|
||||
|
||||
pub async fn run_main_with_listen_url(
|
||||
pub async fn run_main(
|
||||
listen_url: &str,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
transport::run_transport(listen_url).await
|
||||
transport::run_transport(listen_url, runtime_paths).await
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
use crate::CopyOptions;
|
||||
use crate::CreateDirectoryOptions;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::RemoveOptions;
|
||||
use crate::local_file_system::LocalFileSystem;
|
||||
use crate::protocol::FS_WRITE_FILE_METHOD;
|
||||
use crate::protocol::FsCopyParams;
|
||||
use crate::protocol::FsCopyResponse;
|
||||
use crate::protocol::FsCreateDirectoryParams;
|
||||
@@ -28,19 +30,25 @@ use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_request;
|
||||
use crate::rpc::not_found;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FileSystemHandler {
|
||||
file_system: LocalFileSystem,
|
||||
}
|
||||
|
||||
impl FileSystemHandler {
|
||||
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
file_system: LocalFileSystem::with_runtime_paths(runtime_paths),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file(
|
||||
&self,
|
||||
params: FsReadFileParams,
|
||||
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
|
||||
let bytes = self
|
||||
.file_system
|
||||
.read_file_with_sandbox_policy(¶ms.path, params.sandbox_policy.as_ref())
|
||||
.read_file(¶ms.path, params.sandbox.as_ref())
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsReadFileResponse {
|
||||
@@ -54,11 +62,11 @@ impl FileSystemHandler {
|
||||
) -> Result<FsWriteFileResponse, JSONRPCErrorError> {
|
||||
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
|
||||
invalid_request(format!(
|
||||
"fs/writeFile requires valid base64 dataBase64: {err}"
|
||||
"{FS_WRITE_FILE_METHOD} requires valid base64 dataBase64: {err}"
|
||||
))
|
||||
})?;
|
||||
self.file_system
|
||||
.write_file_with_sandbox_policy(¶ms.path, bytes, params.sandbox_policy.as_ref())
|
||||
.write_file(¶ms.path, bytes, params.sandbox.as_ref())
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsWriteFileResponse {})
|
||||
@@ -68,13 +76,12 @@ impl FileSystemHandler {
|
||||
&self,
|
||||
params: FsCreateDirectoryParams,
|
||||
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
|
||||
let recursive = params.recursive.unwrap_or(true);
|
||||
self.file_system
|
||||
.create_directory_with_sandbox_policy(
|
||||
.create_directory(
|
||||
¶ms.path,
|
||||
CreateDirectoryOptions {
|
||||
recursive: params.recursive.unwrap_or(true),
|
||||
},
|
||||
params.sandbox_policy.as_ref(),
|
||||
CreateDirectoryOptions { recursive },
|
||||
params.sandbox.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
@@ -87,7 +94,7 @@ impl FileSystemHandler {
|
||||
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
|
||||
let metadata = self
|
||||
.file_system
|
||||
.get_metadata_with_sandbox_policy(¶ms.path, params.sandbox_policy.as_ref())
|
||||
.get_metadata(¶ms.path, params.sandbox.as_ref())
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsGetMetadataResponse {
|
||||
@@ -104,33 +111,30 @@ impl FileSystemHandler {
|
||||
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
|
||||
let entries = self
|
||||
.file_system
|
||||
.read_directory_with_sandbox_policy(¶ms.path, params.sandbox_policy.as_ref())
|
||||
.read_directory(¶ms.path, params.sandbox.as_ref())
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsReadDirectoryResponse {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|entry| FsReadDirectoryEntry {
|
||||
file_name: entry.file_name,
|
||||
is_directory: entry.is_directory,
|
||||
is_file: entry.is_file,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.map_err(map_fs_error)?
|
||||
.into_iter()
|
||||
.map(|entry| FsReadDirectoryEntry {
|
||||
file_name: entry.file_name,
|
||||
is_directory: entry.is_directory,
|
||||
is_file: entry.is_file,
|
||||
})
|
||||
.collect();
|
||||
Ok(FsReadDirectoryResponse { entries })
|
||||
}
|
||||
|
||||
pub(crate) async fn remove(
|
||||
&self,
|
||||
params: FsRemoveParams,
|
||||
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
|
||||
let recursive = params.recursive.unwrap_or(true);
|
||||
let force = params.force.unwrap_or(true);
|
||||
self.file_system
|
||||
.remove_with_sandbox_policy(
|
||||
.remove(
|
||||
¶ms.path,
|
||||
RemoveOptions {
|
||||
recursive: params.recursive.unwrap_or(true),
|
||||
force: params.force.unwrap_or(true),
|
||||
},
|
||||
params.sandbox_policy.as_ref(),
|
||||
RemoveOptions { recursive, force },
|
||||
params.sandbox.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
@@ -142,13 +146,13 @@ impl FileSystemHandler {
|
||||
params: FsCopyParams,
|
||||
) -> Result<FsCopyResponse, JSONRPCErrorError> {
|
||||
self.file_system
|
||||
.copy_with_sandbox_policy(
|
||||
.copy(
|
||||
¶ms.source_path,
|
||||
¶ms.destination_path,
|
||||
CopyOptions {
|
||||
recursive: params.recursive,
|
||||
},
|
||||
params.sandbox_policy.as_ref(),
|
||||
params.sandbox.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
@@ -157,11 +161,68 @@ impl FileSystemHandler {
|
||||
}
|
||||
|
||||
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
not_found(err.to_string())
|
||||
} else if err.kind() == io::ErrorKind::InvalidInput {
|
||||
invalid_request(err.to_string())
|
||||
} else {
|
||||
internal_error(err.to_string())
|
||||
match err.kind() {
|
||||
io::ErrorKind::NotFound => not_found(err.to_string()),
|
||||
io::ErrorKind::InvalidInput | io::ErrorKind::PermissionDenied => {
|
||||
invalid_request(err.to_string())
|
||||
}
|
||||
_ => internal_error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::protocol::FsReadFileParams;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_platform_sandbox_policies_do_not_require_configured_sandbox_helper() {
|
||||
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);
|
||||
|
||||
for (file_name, sandbox_policy) in [
|
||||
("danger.txt", SandboxPolicy::DangerFullAccess),
|
||||
(
|
||||
"external.txt",
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
},
|
||||
),
|
||||
] {
|
||||
let path =
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path().join(file_name).as_path())
|
||||
.expect("absolute path");
|
||||
|
||||
handler
|
||||
.write_file(FsWriteFileParams {
|
||||
path: path.clone(),
|
||||
data_base64: STANDARD.encode("ok"),
|
||||
sandbox: Some(FileSystemSandboxContext::new(sandbox_policy.clone())),
|
||||
})
|
||||
.await
|
||||
.expect("write file");
|
||||
|
||||
let response = handler
|
||||
.read_file(FsReadFileParams {
|
||||
path,
|
||||
sandbox: Some(FileSystemSandboxContext::new(sandbox_policy)),
|
||||
})
|
||||
.await
|
||||
.expect("read file");
|
||||
|
||||
assert_eq!(response.data_base64, STANDARD.encode("ok"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
use crate::protocol::FsCopyParams;
|
||||
@@ -48,12 +49,13 @@ impl ExecServerHandler {
|
||||
pub(crate) fn new(
|
||||
session_registry: Arc<SessionRegistry>,
|
||||
notifications: RpcNotificationSender,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_registry,
|
||||
notifications,
|
||||
session: StdMutex::new(None),
|
||||
file_system: FileSystemHandler::default(),
|
||||
file_system: FileSystemHandler::new(runtime_paths),
|
||||
initialize_requested: AtomicBool::new(false),
|
||||
initialized: AtomicBool::new(false),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::ExecServerHandler;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::InitializeParams;
|
||||
@@ -64,12 +65,21 @@ fn windows_command_processor() -> String {
|
||||
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
|
||||
}
|
||||
|
||||
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths")
|
||||
}
|
||||
|
||||
async fn initialized_handler() -> Arc<ExecServerHandler> {
|
||||
let (outgoing_tx, _outgoing_rx) = mpsc::channel(16);
|
||||
let registry = SessionRegistry::new();
|
||||
let handler = Arc::new(ExecServerHandler::new(
|
||||
registry,
|
||||
RpcNotificationSender::new(outgoing_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
let initialize_response = handler
|
||||
.initialize(InitializeParams {
|
||||
@@ -147,6 +157,7 @@ async fn long_poll_read_fails_after_session_resume() {
|
||||
let first_handler = Arc::new(ExecServerHandler::new(
|
||||
Arc::clone(®istry),
|
||||
RpcNotificationSender::new(first_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
let initialize_response = first_handler
|
||||
.initialize(InitializeParams {
|
||||
@@ -187,6 +198,7 @@ async fn long_poll_read_fails_after_session_resume() {
|
||||
let second_handler = Arc::new(ExecServerHandler::new(
|
||||
registry,
|
||||
RpcNotificationSender::new(second_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
second_handler
|
||||
.initialize(InitializeParams {
|
||||
@@ -219,6 +231,7 @@ async fn active_session_resume_is_rejected() {
|
||||
let first_handler = Arc::new(ExecServerHandler::new(
|
||||
Arc::clone(®istry),
|
||||
RpcNotificationSender::new(first_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
let initialize_response = first_handler
|
||||
.initialize(InitializeParams {
|
||||
@@ -232,6 +245,7 @@ async fn active_session_resume_is_rejected() {
|
||||
let second_handler = Arc::new(ExecServerHandler::new(
|
||||
registry,
|
||||
RpcNotificationSender::new(second_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
let err = second_handler
|
||||
.initialize(InitializeParams {
|
||||
@@ -259,6 +273,7 @@ async fn output_and_exit_are_retained_after_notification_receiver_closes() {
|
||||
let handler = Arc::new(ExecServerHandler::new(
|
||||
SessionRegistry::new(),
|
||||
RpcNotificationSender::new(outgoing_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
handler
|
||||
.initialize(InitializeParams {
|
||||
|
||||
@@ -4,6 +4,7 @@ use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::connection::CHANNEL_CAPACITY;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::connection::JsonRpcConnectionEvent;
|
||||
@@ -19,28 +20,43 @@ use crate::server::session_registry::SessionRegistry;
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ConnectionProcessor {
|
||||
session_registry: Arc<SessionRegistry>,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
}
|
||||
|
||||
impl ConnectionProcessor {
|
||||
pub(crate) fn new() -> Self {
|
||||
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
session_registry: SessionRegistry::new(),
|
||||
runtime_paths,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run_connection(&self, connection: JsonRpcConnection) {
|
||||
run_connection(connection, Arc::clone(&self.session_registry)).await;
|
||||
run_connection(
|
||||
connection,
|
||||
Arc::clone(&self.session_registry),
|
||||
self.runtime_paths.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_connection(connection: JsonRpcConnection, session_registry: Arc<SessionRegistry>) {
|
||||
async fn run_connection(
|
||||
connection: JsonRpcConnection,
|
||||
session_registry: Arc<SessionRegistry>,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) {
|
||||
let router = Arc::new(build_router());
|
||||
let (json_outgoing_tx, mut incoming_rx, mut disconnected_rx, connection_tasks) =
|
||||
connection.into_parts();
|
||||
let (outgoing_tx, mut outgoing_rx) =
|
||||
mpsc::channel::<RpcServerOutboundMessage>(CHANNEL_CAPACITY);
|
||||
let notifications = RpcNotificationSender::new(outgoing_tx.clone());
|
||||
let handler = Arc::new(ExecServerHandler::new(session_registry, notifications));
|
||||
let handler = Arc::new(ExecServerHandler::new(
|
||||
session_registry,
|
||||
notifications,
|
||||
runtime_paths,
|
||||
));
|
||||
|
||||
let outbound_task = tokio::spawn(async move {
|
||||
while let Some(message) = outgoing_rx.recv().await {
|
||||
@@ -184,6 +200,7 @@ mod tests {
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::run_connection;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::protocol::EXEC_METHOD;
|
||||
@@ -298,10 +315,18 @@ mod tests {
|
||||
let (server_writer, client_reader) = duplex(1 << 20);
|
||||
let connection =
|
||||
JsonRpcConnection::from_stdio(server_reader, server_writer, label.to_string());
|
||||
let task = tokio::spawn(run_connection(connection, registry));
|
||||
let task = tokio::spawn(run_connection(connection, registry, test_runtime_paths()));
|
||||
(client_writer, BufReader::new(client_reader).lines(), task)
|
||||
}
|
||||
|
||||
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths")
|
||||
}
|
||||
|
||||
async fn send_request<P: Serialize>(
|
||||
writer: &mut DuplexStream,
|
||||
id: i64,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::io::Write as _;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::server::processor::ConnectionProcessor;
|
||||
|
||||
@@ -48,19 +49,22 @@ pub(crate) fn parse_listen_url(
|
||||
|
||||
pub(crate) async fn run_transport(
|
||||
listen_url: &str,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let bind_address = parse_listen_url(listen_url)?;
|
||||
run_websocket_listener(bind_address).await
|
||||
run_websocket_listener(bind_address, runtime_paths).await
|
||||
}
|
||||
|
||||
async fn run_websocket_listener(
|
||||
bind_address: SocketAddr,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let listener = TcpListener::bind(bind_address).await?;
|
||||
let local_addr = listener.local_addr()?;
|
||||
let processor = ConnectionProcessor::new();
|
||||
let processor = ConnectionProcessor::new(runtime_paths);
|
||||
tracing::info!("codex-exec-server listening on ws://{local_addr}");
|
||||
println!("ws://{local_addr}");
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
loop {
|
||||
let (stream, peer_addr) = listener.accept().await?;
|
||||
|
||||
Reference in New Issue
Block a user