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:
starr-openai
2026-04-12 18:36:03 -07:00
committed by GitHub
co-authored by Codex
parent 7c1e41c8b6
commit d626dc3895
52 changed files with 2313 additions and 895 deletions
@@ -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
}
+77 -3
View File
@@ -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()));
+53 -42
View File
@@ -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<()>;
}
+299
View File
@@ -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(&params.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(&params.path, bytes, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::WriteFile(FsWriteFileResponse {}))
}
FsHelperRequest::CreateDirectory(params) => {
file_system
.create_directory(
&params.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(&params.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(&params.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(
&params.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(
&params.source_path,
&params.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(())
}
+546
View File
@@ -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])
);
}
}
+10 -1
View File
@@ -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;
+258 -296
View File
@@ -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(&note_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(&note_path, Some(&sandbox_policy), &sandbox_cwd)?;
let error = enforce_read_access_for_cwd(&note_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))]
+8 -8
View File
@@ -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)]
+27 -150
View File
@@ -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)?;
+43
View File
@@ -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),
}
}
+4 -5
View File
@@ -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(&params.path, params.sandbox_policy.as_ref())
.read_file(&params.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(&params.path, bytes, params.sandbox_policy.as_ref())
.write_file(&params.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(
&params.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(&params.path, params.sandbox_policy.as_ref())
.get_metadata(&params.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(&params.path, params.sandbox_policy.as_ref())
.read_directory(&params.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(
&params.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(
&params.source_path,
&params.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"));
}
}
}
+3 -1
View File
@@ -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(&registry),
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(&registry),
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 {
+30 -5
View File
@@ -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,
+7 -3
View File
@@ -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?;