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

## Why

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

## What

Move `ExecutorFileSystem` APIs to use `PathUri` instead of
`AbsolutePathBuf`. Future changes will convert higher-level parts of
exec-server.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-11 11:44:18 -07:00
committed by GitHub
Unverified
parent 4a05d3b282
commit b2a4e3be27
52 changed files with 1126 additions and 495 deletions
+11
View File
@@ -2004,6 +2004,7 @@ dependencies = [
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-utils-json-to-toml",
"codex-utils-path-uri",
"codex-utils-pty",
"codex-web-search-extension",
"codex-windows-sandbox",
@@ -2183,6 +2184,7 @@ dependencies = [
"codex-exec-server",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-path-uri",
"pretty_assertions",
"similar",
"tempfile",
@@ -2515,6 +2517,7 @@ dependencies = [
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-path",
"codex-utils-path-uri",
"core-foundation 0.9.4",
"dns-lookup",
"dunce",
@@ -2634,6 +2637,7 @@ dependencies = [
"codex-utils-image",
"codex-utils-output-truncation",
"codex-utils-path",
"codex-utils-path-uri",
"codex-utils-plugins",
"codex-utils-pty",
"codex-utils-stream-parser",
@@ -2768,6 +2772,7 @@ dependencies = [
"codex-skills",
"codex-utils-absolute-path",
"codex-utils-output-truncation",
"codex-utils-path-uri",
"codex-utils-plugins",
"dirs",
"dunce",
@@ -2848,6 +2853,7 @@ dependencies = [
"codex-shell-command",
"codex-test-binary-support",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"codex-utils-pty",
"codex-utils-rustls-provider",
"ctor 0.6.3",
@@ -3004,6 +3010,7 @@ dependencies = [
"async-trait",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"serde",
]
@@ -3027,6 +3034,7 @@ dependencies = [
"codex-file-system",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"futures",
"gix",
"once_cell",
@@ -3822,6 +3830,7 @@ dependencies = [
"codex-protocol",
"codex-tools",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"codex-utils-string",
"pretty_assertions",
"schemars 0.8.22",
@@ -4203,6 +4212,7 @@ dependencies = [
"codex-exec-server",
"codex-login",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"serde",
"serde_json",
"tempfile",
@@ -4592,6 +4602,7 @@ dependencies = [
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-path-uri",
"ctor 0.6.3",
"futures",
"notify",
+1
View File
@@ -237,6 +237,7 @@ codex-utils-json-to-toml = { path = "utils/json-to-toml" }
codex-utils-oss = { path = "utils/oss" }
codex-utils-output-truncation = { path = "utils/output-truncation" }
codex-utils-path = { path = "utils/path-utils" }
codex-utils-path-uri = { path = "utils/path-uri" }
codex-utils-plugins = { path = "utils/plugins" }
codex-utils-pty = { path = "utils/pty" }
codex-utils-rustls-provider = { path = "utils/rustls-provider" }
+1
View File
@@ -76,6 +76,7 @@ codex-thread-store = { workspace = true }
codex-tools = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-json-to-toml = { workspace = true }
codex-utils-path-uri = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive"] }
futures = { workspace = true }
@@ -29,6 +29,7 @@ use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions;
use codex_utils_path_uri::PathUri;
use std::io;
use std::sync::Arc;
@@ -64,9 +65,10 @@ impl FsRequestProcessor {
&self,
params: FsReadFileParams,
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = self
.file_system()?
.read_file(&params.path, /*sandbox*/ None)
.read_file(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsReadFileResponse {
@@ -83,8 +85,9 @@ impl FsRequestProcessor {
"fs/writeFile requires valid base64 dataBase64: {err}"
))
})?;
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system()?
.write_file(&params.path, bytes, /*sandbox*/ None)
.write_file(&path, bytes, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
@@ -94,9 +97,10 @@ impl FsRequestProcessor {
&self,
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system()?
.create_directory(
&params.path,
&path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
@@ -111,9 +115,10 @@ impl FsRequestProcessor {
&self,
params: FsGetMetadataParams,
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let metadata = self
.file_system()?
.get_metadata(&params.path, /*sandbox*/ None)
.get_metadata(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
@@ -129,9 +134,10 @@ impl FsRequestProcessor {
&self,
params: FsReadDirectoryParams,
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let entries = self
.file_system()?
.read_directory(&params.path, /*sandbox*/ None)
.read_directory(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsReadDirectoryResponse {
@@ -150,9 +156,10 @@ impl FsRequestProcessor {
&self,
params: FsRemoveParams,
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system()?
.remove(
&params.path,
&path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
@@ -168,10 +175,13 @@ impl FsRequestProcessor {
&self,
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
let source_path = PathUri::from_abs_path(&params.source_path).map_err(map_fs_error)?;
let destination_path =
PathUri::from_abs_path(&params.destination_path).map_err(map_fs_error)?;
self.file_system()?
.copy(
&params.source_path,
&params.destination_path,
&source_path,
&destination_path,
CopyOptions {
recursive: params.recursive,
},
+1
View File
@@ -20,6 +20,7 @@ workspace = true
anyhow = { workspace = true }
codex-exec-server = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
similar = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
+13 -1
View File
@@ -21,6 +21,7 @@ use crate::parser::Hunk;
use crate::parser::ParseError;
use crate::parser::parse_patch;
use crate::unified_diff_from_chunks;
use codex_utils_path_uri::PathUri;
use std::str::Utf8Error;
use tree_sitter::LanguageError;
@@ -185,7 +186,18 @@ pub async fn verify_apply_patch_args(
);
}
Hunk::DeleteFile { .. } => {
let content = match fs.read_file_text(&path, sandbox).await {
let path_uri = match PathUri::from_abs_path(&path) {
Ok(path_uri) => path_uri,
Err(e) => {
return MaybeApplyPatchVerified::CorrectnessError(
ApplyPatchError::IoError(IoError {
context: format!("Failed to read {}", path.display()),
source: e,
}),
);
}
};
let content = match fs.read_file_text(&path_uri, sandbox).await {
Ok(content) => content,
Err(e) => {
return MaybeApplyPatchVerified::CorrectnessError(
+32 -12
View File
@@ -16,6 +16,7 @@ use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::RemoveOptions;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
pub use parser::Hunk;
pub use parser::ParseError;
use parser::ParseError::*;
@@ -390,6 +391,7 @@ async fn apply_hunks_to_files(
for hunk in hunks {
let affected_path = hunk.path().to_path_buf();
let path_abs = hunk.resolve_path(cwd);
let path_uri = PathUri::from_abs_path(&path_abs)?;
match hunk {
Hunk::AddFile { contents, .. } => {
let overwritten_content =
@@ -415,7 +417,7 @@ async fn apply_hunks_to_files(
}
Hunk::DeleteFile { .. } => {
note_existing_path_delta_support(&path_abs, fs, sandbox, &mut delta.exact).await;
let deleted_content = fs.read_file_text(&path_abs, sandbox).await.ok();
let deleted_content = fs.read_file_text(&path_uri, sandbox).await.ok();
if deleted_content.is_none() {
delta.exact = false;
}
@@ -424,7 +426,7 @@ async fn apply_hunks_to_files(
.with_context(|| format!("Failed to delete file {}", path_abs.display()))?;
if let Err(error) = fs
.remove(
&path_abs,
&path_uri,
RemoveOptions {
recursive: false,
force: false,
@@ -488,7 +490,7 @@ async fn apply_hunks_to_files(
})?;
if let Err(error) = fs
.remove(
&path_abs,
&path_uri,
RemoveOptions {
recursive: false,
force: false,
@@ -521,7 +523,7 @@ async fn apply_hunks_to_files(
modified.push(affected_path);
} else {
try_write!(
fs.write_file(&path_abs, new_contents.clone().into_bytes(), sandbox)
fs.write_file(&path_uri, new_contents.clone().into_bytes(), sandbox)
.await
.with_context(|| format!(
"Failed to write file {}",
@@ -554,7 +556,8 @@ async fn ensure_not_directory(
fs: &dyn ExecutorFileSystem,
sandbox: Option<&FileSystemSandboxContext>,
) -> io::Result<()> {
let metadata = fs.get_metadata(path, sandbox).await?;
let path_uri = PathUri::from_abs_path(path)?;
let metadata = fs.get_metadata(&path_uri, sandbox).await?;
if metadata.is_directory {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -570,9 +573,12 @@ async fn remove_failure_was_side_effect_free(
fs: &dyn ExecutorFileSystem,
sandbox: Option<&FileSystemSandboxContext>,
) -> bool {
let Ok(path_uri) = PathUri::from_abs_path(path) else {
return false;
};
match expected_content {
Some(expected_content) => fs
.read_file_text(path, sandbox)
.read_file_text(&path_uri, sandbox)
.await
.is_ok_and(|content| content == expected_content),
None => false,
@@ -586,7 +592,14 @@ async fn read_optional_file_text_for_delta(
exact: &mut bool,
) -> Option<String> {
note_existing_path_delta_support(path, fs, sandbox, exact).await;
match fs.read_file_text(path, sandbox).await {
let path_uri = match PathUri::from_abs_path(path) {
Ok(path_uri) => path_uri,
Err(_) => {
*exact = false;
return None;
}
};
match fs.read_file_text(&path_uri, sandbox).await {
Ok(content) => Some(content),
Err(source) if source.kind() == io::ErrorKind::NotFound => None,
Err(_) => {
@@ -602,7 +615,11 @@ async fn note_existing_path_delta_support(
sandbox: Option<&FileSystemSandboxContext>,
exact: &mut bool,
) {
match fs.get_metadata(path, sandbox).await {
let Ok(path_uri) = PathUri::from_abs_path(path) else {
*exact = false;
return;
};
match fs.get_metadata(&path_uri, sandbox).await {
Ok(metadata) if metadata.is_file && !metadata.is_symlink => {}
Ok(_) => *exact = false,
Err(source) if source.kind() == io::ErrorKind::NotFound => {}
@@ -616,12 +633,14 @@ async fn write_file_with_missing_parent_retry(
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> anyhow::Result<()> {
match fs.write_file(path_abs, contents.clone(), sandbox).await {
let path_uri = PathUri::from_abs_path(path_abs)?;
match fs.write_file(&path_uri, contents.clone(), sandbox).await {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => {
if let Some(parent_abs) = path_abs.parent() {
let parent_uri = PathUri::from_abs_path(&parent_abs)?;
fs.create_directory(
&parent_abs,
&parent_uri,
CreateDirectoryOptions { recursive: true },
sandbox,
)
@@ -633,7 +652,7 @@ async fn write_file_with_missing_parent_retry(
)
})?;
}
fs.write_file(path_abs, contents, sandbox)
fs.write_file(&path_uri, contents, sandbox)
.await
.with_context(|| format!("Failed to write file {}", path_abs.display()))?;
Ok(())
@@ -657,7 +676,8 @@ async fn derive_new_contents_from_chunks(
fs: &dyn ExecutorFileSystem,
sandbox: Option<&FileSystemSandboxContext>,
) -> std::result::Result<AppliedPatch, ApplyPatchError> {
let original_contents = fs.read_file_text(path_abs, sandbox).await.map_err(|err| {
let path_uri = PathUri::from_abs_path(path_abs)?;
let original_contents = fs.read_file_text(&path_uri, sandbox).await.map_err(|err| {
ApplyPatchError::IoError(IoError {
context: format!("Failed to read file to update {}", path_abs.display()),
source: err,
+1
View File
@@ -25,6 +25,7 @@ codex-network-proxy = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
dunce = { workspace = true }
futures = { workspace = true, features = ["alloc", "std"] }
gethostname = { workspace = true }
+3 -1
View File
@@ -10,6 +10,7 @@ use crate::strict_config::config_error_from_ignored_toml_value_fields;
use codex_file_system::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
use std::io;
use std::path::Path;
use std::path::PathBuf;
@@ -106,7 +107,8 @@ pub(super) async fn read_config_from_path(
log_missing_as_info: bool,
strict_config: bool,
) -> io::Result<Option<TomlValue>> {
match fs.read_file_text(path, /*sandbox*/ None).await {
let path_uri = PathUri::from_abs_path(path)?;
match fs.read_file_text(&path_uri, /*sandbox*/ None).await {
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
Ok(value) => {
if strict_config {
+21 -8
View File
@@ -40,6 +40,7 @@ use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::AskForApproval;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
use dunce::canonicalize as normalize_path;
use serde::Deserialize;
use std::io;
@@ -471,7 +472,8 @@ async fn load_config_toml_for_required_layer(
strict_config: bool,
create_entry: impl FnOnce(TomlValue) -> ConfigLayerEntry,
) -> io::Result<ConfigLayerEntry> {
let toml_value = match fs.read_file_text(toml_file, /*sandbox*/ None).await {
let toml_file_uri = PathUri::from_abs_path(toml_file)?;
let toml_value = match fs.read_file_text(&toml_file_uri, /*sandbox*/ None).await {
Ok(contents) => {
let config_parent = toml_file.as_path().parent().ok_or_else(|| {
io::Error::new(
@@ -566,8 +568,9 @@ pub async fn load_requirements_toml(
fs: &dyn ExecutorFileSystem,
requirements_toml_file: &AbsolutePathBuf,
) -> io::Result<Option<RequirementsLayerEntry>> {
let requirements_toml_file_uri = PathUri::from_abs_path(requirements_toml_file)?;
match fs
.read_file_text(requirements_toml_file, /*sandbox*/ None)
.read_file_text(&requirements_toml_file_uri, /*sandbox*/ None)
.await
{
Ok(contents) => {
@@ -1135,8 +1138,9 @@ async fn find_project_root(
for ancestor in cwd.ancestors() {
for marker in project_root_markers {
let marker_path = ancestor.join(marker);
let marker_path_uri = PathUri::from_abs_path(&marker_path)?;
if fs
.get_metadata(&marker_path, /*sandbox*/ None)
.get_metadata(&marker_path_uri, /*sandbox*/ None)
.await
.is_ok()
{
@@ -1151,14 +1155,20 @@ async fn find_git_checkout_root(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
) -> Option<AbsolutePathBuf> {
let base = match fs.get_metadata(cwd, /*sandbox*/ None).await {
let cwd_uri = PathUri::from_abs_path(cwd).ok()?;
let base = match fs.get_metadata(&cwd_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_directory => cwd.clone(),
_ => cwd.parent()?,
};
for dir in base.ancestors() {
let dot_git = dir.join(".git");
if fs.get_metadata(&dot_git, /*sandbox*/ None).await.is_ok() {
let dot_git_uri = PathUri::from_abs_path(&dot_git).ok()?;
if fs
.get_metadata(&dot_git_uri, /*sandbox*/ None)
.await
.is_ok()
{
return Some(dir);
}
}
@@ -1206,8 +1216,9 @@ async fn load_project_layers(
let mut startup_warnings = Vec::new();
for dir in dirs {
let dot_codex_abs = dir.join(".codex");
let dot_codex_uri = PathUri::from_abs_path(&dot_codex_abs)?;
if !fs
.get_metadata(&dot_codex_abs, /*sandbox*/ None)
.get_metadata(&dot_codex_uri, /*sandbox*/ None)
.await
.map(|metadata| metadata.is_directory)
.unwrap_or(false)
@@ -1224,7 +1235,8 @@ async fn load_project_layers(
continue;
}
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE);
match fs.read_file_text(&config_file, /*sandbox*/ None).await {
let config_file_uri = PathUri::from_abs_path(&config_file)?;
match fs.read_file_text(&config_file_uri, /*sandbox*/ None).await {
Ok(contents) => {
let config: TomlValue = match toml::from_str(&contents) {
Ok(config) => config,
@@ -1327,8 +1339,9 @@ async fn merge_root_checkout_project_hooks(
return Ok(config);
};
let hooks_config_file = hooks_config_folder.join(CONFIG_TOML_FILE);
let hooks_config_file_uri = PathUri::from_abs_path(&hooks_config_file)?;
let root_config = match fs
.read_file_text(&hooks_config_file, /*sandbox*/ None)
.read_file_text(&hooks_config_file_uri, /*sandbox*/ None)
.await
{
Ok(contents) => {
+15 -24
View File
@@ -7,8 +7,8 @@ use codex_file_system::FileSystemResult;
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::ReadDirectoryEntry;
use codex_file_system::RemoveOptions;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::path::Path;
use tempfile::tempdir;
struct TestFileSystem;
@@ -17,35 +17,26 @@ struct TestFileSystem;
impl ExecutorFileSystem for TestFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
path.canonicalize()
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
Ok(base_path.join(path))
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
Ok(path.parent())
) -> FileSystemResult<PathUri> {
let path = path.to_abs_path()?;
let canonicalized = path.canonicalize()?;
PathUri::from_abs_path(&canonicalized)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
let path = path.to_abs_path()?;
tokio::fs::read(path.as_path()).await
}
async fn write_file(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_contents: Vec<u8>,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -54,7 +45,7 @@ impl ExecutorFileSystem for TestFileSystem {
async fn create_directory(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_create_directory_options: CreateDirectoryOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -63,7 +54,7 @@ impl ExecutorFileSystem for TestFileSystem {
async fn get_metadata(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
unimplemented!("test filesystem only supports reads")
@@ -71,7 +62,7 @@ impl ExecutorFileSystem for TestFileSystem {
async fn read_directory(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
unimplemented!("test filesystem only supports reads")
@@ -79,7 +70,7 @@ impl ExecutorFileSystem for TestFileSystem {
async fn remove(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_remove_options: RemoveOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -88,8 +79,8 @@ impl ExecutorFileSystem for TestFileSystem {
async fn copy(
&self,
_source_path: &AbsolutePathBuf,
_destination_path: &AbsolutePathBuf,
_source_path: &PathUri,
_destination_path: &PathUri,
_copy_options: CopyOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
+1
View File
@@ -26,6 +26,7 @@ codex-protocol = { workspace = true }
codex-skills = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-output-truncation = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-plugins = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
+16 -4
View File
@@ -13,6 +13,7 @@ use codex_exec_server::LOCAL_FS;
use codex_otel::SessionTelemetry;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL;
#[derive(Debug, Default)]
@@ -75,10 +76,21 @@ pub async fn build_skill_injections(
let fs = loaded_skills
.and_then(|outcome| outcome.file_system_for_skill(skill))
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
match fs
.read_file_text(&skill.path_to_skills_md, /*sandbox*/ None)
.await
{
// Skill metadata may point at a file that is absent, but a path the host
// cannot represent means the configured skill cannot be loaded correctly.
let path = match PathUri::from_abs_path(&skill.path_to_skills_md) {
Ok(path) => path,
Err(err) => {
emit_skill_injected_metric(otel, skill, "error");
result.warnings.push(format!(
"Failed to load skill {name} at {path}: {err:#}",
name = skill.name,
path = skill.path_to_skills_md.display()
));
continue;
}
};
match fs.read_file_text(&path, /*sandbox*/ None).await {
Ok(contents) => {
emit_skill_injected_metric(otel, skill, "ok");
invocations.push(SkillInvocation {
+80 -10
View File
@@ -19,6 +19,7 @@ use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
use codex_utils_plugins::PluginSkillRoot;
use codex_utils_plugins::plugin_namespace_for_skill_path;
use dirs::home_dir;
@@ -374,7 +375,17 @@ async fn repo_agents_skill_roots(
let mut roots = Vec::new();
for dir in dirs {
let agents_skills = dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME);
match fs.get_metadata(&agents_skills, /*sandbox*/ None).await {
let agents_skills_uri = match PathUri::from_abs_path(&agents_skills) {
Ok(path) => path,
Err(err) => {
tracing::warn!(
"failed to convert repo skills root {} to URI: {err:#}",
agents_skills.display()
);
continue;
}
};
match fs.get_metadata(&agents_skills_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_directory => roots.push(SkillRoot {
path: agents_skills,
scope: SkillScope::Repo,
@@ -429,7 +440,17 @@ async fn find_project_root(
for ancestor in cwd.ancestors() {
for marker in project_root_markers {
let marker_path = ancestor.join(marker);
match fs.get_metadata(&marker_path, /*sandbox*/ None).await {
let marker_path_uri = match PathUri::from_abs_path(&marker_path) {
Ok(path) => path,
Err(err) => {
tracing::warn!(
"failed to convert project root marker {} to URI: {err:#}",
marker_path.display()
);
continue;
}
};
match fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await {
Ok(_) => return ancestor,
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
@@ -475,8 +496,12 @@ async fn canonicalize_for_skill_identity(
fs: &dyn ExecutorFileSystem,
path: &AbsolutePathBuf,
) -> AbsolutePathBuf {
fs.canonicalize(path, /*sandbox*/ None)
let Ok(path_uri) = PathUri::from_abs_path(path) else {
return path.clone();
};
fs.canonicalize(&path_uri, /*sandbox*/ None)
.await
.and_then(|path| path.to_abs_path())
.unwrap_or_else(|_| path.clone())
}
@@ -494,7 +519,17 @@ async fn discover_skills_under_root(
None => None,
};
match fs.get_metadata(&root, /*sandbox*/ None).await {
let root_uri = match PathUri::from_abs_path(&root) {
Ok(path) => path,
Err(err) => {
tracing::warn!(
"failed to convert skills root {} to URI: {err:#}",
root.display()
);
return;
}
};
match fs.get_metadata(&root_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_directory => {}
Ok(_) => return,
Err(err) if err.kind() == io::ErrorKind::NotFound => return,
@@ -536,7 +571,17 @@ async fn discover_skills_under_root(
let mut truncated_by_dir_limit = false;
while let Some((dir, depth)) = queue.pop_front() {
let entries = match fs.read_directory(&dir, /*sandbox*/ None).await {
let dir_uri = match PathUri::from_abs_path(&dir) {
Ok(path) => path,
Err(e) => {
tracing::warn!(
"failed to convert skills dir {} to URI: {e:#}",
dir.display()
);
continue;
}
};
let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await {
Ok(entries) => entries,
Err(e) => {
error!("failed to read skills dir {}: {e:#}", dir.display());
@@ -551,7 +596,17 @@ async fn discover_skills_under_root(
}
let path = dir.join(&file_name);
let metadata = match fs.get_metadata(&path, /*sandbox*/ None).await {
let path_uri = match PathUri::from_abs_path(&path) {
Ok(path) => path,
Err(e) => {
tracing::warn!(
"failed to convert skills path {} to URI: {e:#}",
path.display()
);
continue;
}
};
let metadata = match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
Ok(metadata) => metadata,
Err(e) => {
error!("failed to stat skills path {}: {e:#}", path.display());
@@ -563,7 +618,7 @@ async fn discover_skills_under_root(
if !follow_symlinks {
continue;
}
match fs.read_directory(&path, /*sandbox*/ None).await {
match fs.read_directory(&path_uri, /*sandbox*/ None).await {
Ok(_) => {
let resolved_dir = canonicalize_for_skill_identity(fs, &path).await;
enqueue_dir(
@@ -635,8 +690,9 @@ async fn parse_skill_file(
plugin_id: Option<&str>,
plugin_root: Option<&AbsolutePathBuf>,
) -> Result<SkillMetadata, SkillParseError> {
let path_uri = PathUri::from_abs_path(path).map_err(SkillParseError::Read)?;
let contents = fs
.read_file_text(path, /*sandbox*/ None)
.read_file_text(&path_uri, /*sandbox*/ None)
.await
.map_err(SkillParseError::Read)?;
@@ -730,7 +786,18 @@ async fn load_skill_metadata(
let metadata_path = skill_dir
.join(SKILLS_METADATA_DIR)
.join(SKILLS_METADATA_FILENAME);
match fs.get_metadata(&metadata_path, /*sandbox*/ None).await {
let metadata_path_uri = match PathUri::from_abs_path(&metadata_path) {
Ok(path) => path,
Err(error) => {
tracing::warn!(
"ignoring {path}: failed to convert {label} path to URI: {error}",
path = metadata_path.display(),
label = SKILLS_METADATA_FILENAME
);
return LoadedSkillMetadata::default();
}
};
match fs.get_metadata(&metadata_path_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_file => {}
Ok(_) => return LoadedSkillMetadata::default(),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
@@ -746,7 +813,10 @@ async fn load_skill_metadata(
}
}
let contents = match fs.read_file_text(&metadata_path, /*sandbox*/ None).await {
let contents = match fs
.read_file_text(&metadata_path_uri, /*sandbox*/ None)
.await
{
Ok(contents) => contents,
Err(error) => {
tracing::warn!(
+3 -2
View File
@@ -9,6 +9,7 @@ use codex_exec_server::LOCAL_FS;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
#[derive(Debug, Clone, PartialEq)]
pub struct SkillMetadata {
@@ -152,8 +153,8 @@ impl HostLoadedSkills {
.outcome
.file_system_for_skill(skill)
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
fs.read_file_text(&skill.path_to_skills_md, /*sandbox*/ None)
.await
let path = PathUri::from_abs_path(&skill.path_to_skills_md)?;
fs.read_file_text(&path, /*sandbox*/ None).await
}
}
+1
View File
@@ -70,6 +70,7 @@ codex-utils-image = { workspace = true }
codex-utils-home-dir = { workspace = true }
codex-utils-output-truncation = { workspace = true }
codex-utils-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-plugins = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-string = { workspace = true }
+26 -10
View File
@@ -26,6 +26,7 @@ use codex_exec_server::ExecutorFileSystem;
use codex_features::Feature;
use codex_prompts::HIERARCHICAL_AGENTS_MESSAGE;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::io;
use toml::Value as TomlValue;
use tracing::error;
@@ -58,7 +59,19 @@ impl<'a> AgentsMdManager<'a> {
let base = codex_dir?;
for candidate in [LOCAL_AGENTS_MD_FILENAME, DEFAULT_AGENTS_MD_FILENAME] {
let path = base.join(candidate);
let data = match fs.read_file(&path, /*sandbox*/ None).await {
// A missing global instructions file is normal, but an unrepresentable
// configured path means Codex cannot honor the workspace configuration.
let path_uri = match PathUri::from_abs_path(&path) {
Ok(path_uri) => path_uri,
Err(err) => {
startup_warnings.push(format!(
"Failed to read global AGENTS.md instructions from `{}`: {err}",
path.display()
));
continue;
}
};
let data = match fs.read_file(&path_uri, /*sandbox*/ None).await {
Ok(data) => data,
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) if err.kind() == io::ErrorKind::IsADirectory => continue,
@@ -149,14 +162,15 @@ impl<'a> AgentsMdManager<'a> {
break;
}
match fs.get_metadata(&p, /*sandbox*/ None).await {
let path_uri = PathUri::from_abs_path(&p)?;
match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
Ok(metadata) if !metadata.is_file => continue,
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
}
let mut data = match fs.read_file(&p, /*sandbox*/ None).await {
let mut data = match fs.read_file(&path_uri, /*sandbox*/ None).await {
Ok(data) => data,
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
@@ -231,12 +245,13 @@ impl<'a> AgentsMdManager<'a> {
for ancestor in dir.ancestors() {
for marker in &project_root_markers {
let marker_path = ancestor.join(marker);
let marker_exists = match fs.get_metadata(&marker_path, /*sandbox*/ None).await
{
Ok(_) => true,
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
Err(err) => return Err(err),
};
let marker_path_uri = PathUri::from_abs_path(&marker_path)?;
let marker_exists =
match fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await {
Ok(_) => true,
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
Err(err) => return Err(err),
};
if marker_exists {
project_root = Some(ancestor.clone());
break;
@@ -272,7 +287,8 @@ impl<'a> AgentsMdManager<'a> {
for d in search_dirs {
for name in &candidate_filenames {
let candidate = d.join(name);
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
let candidate_uri = PathUri::from_abs_path(&candidate)?;
match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {
Ok(md) if md.is_file => {
found.push(candidate);
break;
+7 -3
View File
@@ -7,6 +7,7 @@ use codex_config::config_toml::ConfigToml;
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
@@ -319,7 +320,8 @@ async fn read_resolved_agent_role_file(
path: &AbsolutePathBuf,
role_name_hint: Option<&str>,
) -> std::io::Result<ResolvedAgentRoleFile> {
let contents = fs.read_file_text(path, /*sandbox*/ None).await?;
let path_uri = PathUri::from_abs_path(path)?;
let contents = fs.read_file_text(&path_uri, /*sandbox*/ None).await?;
let config_base_dir = path.parent().unwrap_or_else(|| path.clone());
parse_agent_role_file_contents(
&contents,
@@ -391,8 +393,9 @@ async fn validate_agent_role_config_file(
return Ok(());
};
let config_file_uri = PathUri::from_abs_path(config_file)?;
let metadata = fs
.get_metadata(config_file, /*sandbox*/ None)
.get_metadata(&config_file_uri, /*sandbox*/ None)
.await
.map_err(|e| {
std::io::Error::new(
@@ -522,7 +525,8 @@ async fn collect_agent_role_files(
let mut files = Vec::new();
let mut dirs = vec![dir.clone()];
while let Some(dir) = dirs.pop() {
let entries = match fs.read_directory(&dir, /*sandbox*/ None).await {
let dir_uri = PathUri::from_abs_path(&dir)?;
let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(err) => return Err(err),
+3 -1
View File
@@ -105,6 +105,7 @@ use codex_protocol::protocol::SandboxPolicy;
pub use codex_thread_store::ExtraConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
use rmcp::model::ElicitationCapability;
use rmcp::model::FormElicitationCapability;
use rmcp::model::UrlElicitationCapability;
@@ -3667,8 +3668,9 @@ impl Config {
return Ok(None);
};
let path_uri = PathUri::from_abs_path(path)?;
let contents = fs
.read_file_text(path, /*sandbox*/ None)
.read_file_text(&path_uri, /*sandbox*/ None)
.await
.map_err(|e| {
std::io::Error::new(
@@ -27,6 +27,7 @@ use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_path_uri::PathUri;
pub struct ViewImageHandler {
options: ViewImageToolOptions,
@@ -146,9 +147,15 @@ impl ViewImageHandler {
let abs_path = cwd.join(path);
let sandbox = turn.file_system_sandbox_context(/*additional_permissions*/ None, &cwd);
let fs = turn_environment.environment.get_filesystem();
let path_uri = PathUri::from_abs_path(&abs_path).map_err(|error| {
FunctionCallError::RespondToModel(format!(
"unable to locate image at `{}`: {error}",
abs_path.display()
))
})?;
let metadata = fs
.get_metadata(&abs_path, Some(&sandbox))
.get_metadata(&path_uri, Some(&sandbox))
.await
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
@@ -164,7 +171,7 @@ impl ViewImageHandler {
)));
}
let file_bytes = fs
.read_file(&abs_path, Some(&sandbox))
.read_file(&path_uri, Some(&sandbox))
.await
.map_err(|error| {
FunctionCallError::RespondToModel(format!(
+1
View File
@@ -28,6 +28,7 @@ codex-models-manager = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
codex-utils-path-uri = { workspace = true }
ctor = { workspace = true }
futures = { workspace = true }
notify = { workspace = true }
+26 -7
View File
@@ -43,6 +43,7 @@ use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::future::BoxFuture;
use serde_json::Value;
use tempfile::TempDir;
@@ -135,10 +136,11 @@ pub async fn test_env() -> Result<TestEnv> {
let environment =
codex_exec_server::Environment::create_for_tests(Some(websocket_url.clone()))?;
let cwd = remote_aware_cwd_path();
let cwd_uri = PathUri::from_path(&cwd)?;
environment
.get_filesystem()
.create_directory(
&cwd,
&cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -907,35 +909,45 @@ impl TestCodexHarness {
) -> Result<()> {
let abs_path = self.path_abs(rel);
if let Some(parent) = abs_path.parent() {
let parent_uri = PathUri::from_path(&parent)?;
self.test
.fs()
.create_directory(
&parent,
&parent_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
}
let abs_path_uri = PathUri::from_path(&abs_path)?;
self.test
.fs()
.write_file(&abs_path, contents.as_ref().to_vec(), /*sandbox*/ None)
.write_file(
&abs_path_uri,
contents.as_ref().to_vec(),
/*sandbox*/ None,
)
.await?;
Ok(())
}
pub async fn read_file_text(&self, rel: impl AsRef<Path>) -> Result<String> {
let path = self.path_abs(rel);
let path_uri = PathUri::from_path(&path)?;
Ok(self
.test
.fs()
.read_file_text(&self.path_abs(rel), /*sandbox*/ None)
.read_file_text(&path_uri, /*sandbox*/ None)
.await?)
}
pub async fn create_dir_all(&self, rel: impl AsRef<Path>) -> Result<()> {
let path = self.path_abs(rel);
let path_uri = PathUri::from_path(&path)?;
self.test
.fs()
.create_directory(
&self.path_abs(rel),
&path_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -948,10 +960,11 @@ impl TestCodexHarness {
}
pub async fn remove_abs_path(&self, path: &AbsolutePathBuf) -> Result<()> {
let path_uri = PathUri::from_abs_path(path)?;
self.test
.fs()
.remove(
path,
&path_uri,
RemoveOptions {
recursive: false,
force: true,
@@ -963,7 +976,13 @@ impl TestCodexHarness {
}
pub async fn abs_path_exists(&self, path: &AbsolutePathBuf) -> Result<bool> {
match self.test.fs().get_metadata(path, /*sandbox*/ None).await {
let path_uri = PathUri::from_abs_path(path)?;
match self
.test
.fs()
.get_metadata(&path_uri, /*sandbox*/ None)
.await
{
Ok(_) => Ok(true),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
Err(err) => Err(err.into()),
+37 -14
View File
@@ -7,6 +7,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt;
use core_test_support::create_directory_symlink;
use core_test_support::load_default_config_for_test;
@@ -112,10 +113,12 @@ async fn agents_override_is_preferred_over_agents_md() -> Result<()> {
agents_instructions(test_codex().with_workspace_setup(|cwd, fs| async move {
let agents_md = cwd.join("AGENTS.md");
let override_md = cwd.join("AGENTS.override.md");
fs.write_file(&agents_md, b"base doc".to_vec(), /*sandbox*/ None)
let agents_md_uri = PathUri::from_path(&agents_md)?;
let override_md_uri = PathUri::from_path(&override_md)?;
fs.write_file(&agents_md_uri, b"base doc".to_vec(), /*sandbox*/ None)
.await?;
fs.write_file(
&override_md,
&override_md_uri,
b"override doc".to_vec(),
/*sandbox*/ None,
)
@@ -146,14 +149,20 @@ async fn configured_fallback_is_used_when_agents_candidate_is_directory() -> Res
.with_workspace_setup(|cwd, fs| async move {
let agents_dir = cwd.join("AGENTS.md");
let fallback = cwd.join("WORKFLOW.md");
let agents_dir_uri = PathUri::from_path(&agents_dir)?;
let fallback_uri = PathUri::from_path(&fallback)?;
fs.create_directory(
&agents_dir,
&agents_dir_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
fs.write_file(&fallback, b"fallback doc".to_vec(), /*sandbox*/ None)
.await?;
fs.write_file(
&fallback_uri,
b"fallback doc".to_vec(),
/*sandbox*/ None,
)
.await?;
Ok::<(), anyhow::Error>(())
}),
)
@@ -183,23 +192,35 @@ async fn agents_docs_are_concatenated_from_project_root_to_cwd() -> Result<()> {
let root_agents = root.join("AGENTS.md");
let git_marker = root.join(".git");
let nested_agents = nested.join("AGENTS.md");
let nested_uri = PathUri::from_path(&nested)?;
let root_agents_uri = PathUri::from_path(&root_agents)?;
let git_marker_uri = PathUri::from_path(&git_marker)?;
let nested_agents_uri = PathUri::from_path(&nested_agents)?;
fs.create_directory(
&nested,
&nested_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
fs.write_file(&root_agents, b"root doc".to_vec(), /*sandbox*/ None)
.await?;
fs.write_file(
&git_marker,
&root_agents_uri,
b"root doc".to_vec(),
/*sandbox*/ None,
)
.await?;
fs.write_file(
&git_marker_uri,
b"gitdir: /tmp/mock-git-dir\n".to_vec(),
/*sandbox*/ None,
)
.await?;
fs.write_file(&nested_agents, b"child doc".to_vec(), /*sandbox*/ None)
.await?;
fs.write_file(
&nested_agents_uri,
b"child doc".to_vec(),
/*sandbox*/ None,
)
.await?;
Ok::<(), anyhow::Error>(())
}),
)
@@ -314,8 +335,9 @@ async fn selected_environment_sources_match_model_visible_instructions() -> Resu
let mut builder = test_codex()
.with_home(home)
.with_workspace_setup(|cwd, fs| async move {
let agents_md_uri = PathUri::from_path(cwd.join("AGENTS.md"))?;
fs.write_file(
&cwd.join("AGENTS.md"),
&agents_md_uri,
b"project doc".to_vec(),
/*sandbox*/ None,
)
@@ -367,8 +389,9 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re
let mut builder = test_codex()
.with_home(Arc::clone(&home))
.with_workspace_setup(|cwd, fs| async move {
let agents_md_uri = PathUri::from_path(cwd.join("AGENTS.md"))?;
fs.write_file(
&cwd.join("AGENTS.md"),
&agents_md_uri,
PROJECT_INSTRUCTIONS.as_bytes().to_vec(),
/*sandbox*/ None,
)
@@ -392,7 +415,7 @@ async fn fresh_thread_composes_global_before_project_and_reports_sources() -> Re
)?;
test.fs()
.write_file(
&project_source,
&PathUri::from_path(&project_source)?,
NEW_PROJECT_INSTRUCTIONS.as_bytes().to_vec(),
/*sandbox*/ None,
)
+18 -13
View File
@@ -38,6 +38,7 @@ use codex_protocol::user_input::UserInput;
#[cfg(target_os = "linux")]
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt;
use core_test_support::assert_regex_match;
use core_test_support::get_remote_test_env;
@@ -1329,25 +1330,24 @@ async fn apply_patch_turn_diff_paths_stay_repo_relative_when_session_cwd_is_nest
config.cwd = config.cwd.join("subdir");
})
.with_workspace_setup(|cwd, fs| async move {
let cwd_uri = PathUri::from_path(&cwd)?;
fs.create_directory(
&cwd,
&cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
let repo_root = cwd.parent().expect("nested cwd should have parent");
let git_uri = PathUri::from_path(repo_root.join(".git"))?;
let repo_file_uri = PathUri::from_path(repo_root.join("repo.txt"))?;
fs.write_file(
&repo_root.join(".git"),
&git_uri,
b"gitdir: /tmp/fake-worktree\n".to_vec(),
/*sandbox*/ None,
)
.await?;
fs.write_file(
&repo_root.join("repo.txt"),
b"before\n".to_vec(),
/*sandbox*/ None,
)
.await?;
fs.write_file(&repo_file_uri, b"before\n".to_vec(), /*sandbox*/ None)
.await?;
Ok(())
})
})
@@ -1582,10 +1582,11 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
))
.abs();
let shared_cwd_uri = PathUri::from_path(&shared_cwd)?;
let _ = fs::remove_dir_all(shared_cwd.as_path());
test.fs()
.remove(
&shared_cwd,
&shared_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -1596,7 +1597,7 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
fs::create_dir_all(shared_cwd.as_path())?;
test.fs()
.create_directory(
&shared_cwd,
&shared_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -1683,7 +1684,10 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
assert_eq!(fs::read_to_string(shared_cwd.join(file_name))?, "local\n");
assert_eq!(
test.fs()
.read_file_text(&shared_cwd.join(file_name), /*sandbox*/ None)
.read_file_text(
&PathUri::from_path(shared_cwd.join(file_name))?,
/*sandbox*/ None,
)
.await?,
"remote\n"
);
@@ -1710,7 +1714,7 @@ index 0000000000000000000000000000000000000000..9c998f7b995a7327177b38a90d138517
let _ = fs::remove_dir_all(shared_cwd.as_path());
test.fs()
.remove(
&shared_cwd,
&shared_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -1851,8 +1855,9 @@ async fn apply_patch_clears_aggregated_diff_after_inexact_delta() -> Result<()>
let harness = apply_patch_harness_with(|builder| {
builder.with_workspace_setup(|cwd, fs| async move {
let binary_path_uri = PathUri::from_path(cwd.join("binary.dat"))?;
fs.write_file(
&cwd.join("binary.dat"),
&binary_path_uri,
vec![0xff, 0xfe, 0xfd],
/*sandbox*/ None,
)
@@ -1,4 +1,5 @@
use codex_features::Feature;
use codex_utils_path_uri::PathUri;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
@@ -27,7 +28,8 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
})
.with_workspace_setup(|cwd, fs| async move {
let agents_md = cwd.join("AGENTS.md");
fs.write_file(&agents_md, b"be nice".to_vec(), /*sandbox*/ None)
let agents_md_uri = PathUri::from_path(&agents_md)?;
fs.write_file(&agents_md_uri, b"be nice".to_vec(), /*sandbox*/ None)
.await?;
Ok::<(), anyhow::Error>(())
});
+57 -33
View File
@@ -28,6 +28,7 @@ use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::get_remote_test_env;
@@ -155,19 +156,20 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
let file_system = test_env.environment().get_filesystem();
let file_path_abs = remote_test_file_path().abs();
let file_path_uri = PathUri::from_path(&file_path_abs)?;
let payload = b"remote-test-env-ok".to_vec();
file_system
.write_file(&file_path_abs, payload.clone(), /*sandbox*/ None)
.write_file(&file_path_uri, payload.clone(), /*sandbox*/ None)
.await?;
let actual = file_system
.read_file(&file_path_abs, /*sandbox*/ None)
.read_file(&file_path_uri, /*sandbox*/ None)
.await?;
assert_eq!(actual, payload);
file_system
.remove(
&file_path_abs,
&file_path_uri,
RemoveOptions {
recursive: false,
force: true,
@@ -295,16 +297,18 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
))
.abs();
let remote_marker_name = "marker.txt";
let remote_cwd_uri = PathUri::from_path(&remote_cwd)?;
let remote_marker_uri = PathUri::from_path(remote_cwd.join(remote_marker_name))?;
test.fs()
.create_directory(
&remote_cwd,
&remote_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
test.fs()
.write_file(
&remote_cwd.join(remote_marker_name),
&remote_marker_uri,
b"remote-routing".to_vec(),
/*sandbox*/ None,
)
@@ -338,7 +342,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
test.fs()
.remove(
&remote_cwd,
&remote_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -390,9 +394,10 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
let local_write_root = local_cwd.path().join(relative_write_root);
let local_target_path = local_cwd.path().join(relative_target_path);
fs::create_dir(&local_write_root)?;
let remote_write_root_uri = PathUri::from_path(&remote_write_root)?;
test.fs()
.create_directory(
&remote_write_root,
&remote_write_root_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -527,7 +532,10 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
);
assert_eq!(
test.fs()
.read_file_text(&remote_target_path, /*sandbox*/ None)
.read_file_text(
&PathUri::from_path(&remote_target_path)?,
/*sandbox*/ None,
)
.await?,
"remote-request-permissions-ok"
);
@@ -538,7 +546,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
test.fs()
.remove(
&remote_cwd,
&PathUri::from_abs_path(&remote_cwd)?,
RemoveOptions {
recursive: true,
force: true,
@@ -567,9 +575,10 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
))
.abs();
let remote_cwd_uri = PathUri::from_path(&remote_cwd)?;
test.fs()
.create_directory(
&remote_cwd,
&remote_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -610,7 +619,10 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
let remote_contents = test
.fs()
.read_file_text(&remote_cwd.join(file_name), /*sandbox*/ None)
.read_file_text(
&PathUri::from_path(remote_cwd.join(file_name))?,
/*sandbox*/ None,
)
.await?;
assert_eq!(remote_contents, "patched remote freeform\n");
assert!(
@@ -620,7 +632,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
test.fs()
.remove(
&remote_cwd,
&remote_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -651,9 +663,10 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
))
.abs();
let remote_cwd_uri = PathUri::from_path(&remote_cwd)?;
test.fs()
.create_directory(
&remote_cwd,
&remote_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -664,10 +677,11 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
))
.abs();
let target_path_uri = PathUri::from_path(&target_path)?;
let _ = fs::remove_file(&target_path);
test.fs()
.remove(
&target_path,
&target_path_uri,
RemoveOptions {
recursive: false,
force: true,
@@ -771,7 +785,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
.await;
assert_eq!(
test.fs()
.read_file_text(&target_path, /*sandbox*/ None)
.read_file_text(&target_path_uri, /*sandbox*/ None)
.await?,
"remote\n"
);
@@ -785,7 +799,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
wait_for_completion_without_patch_approval(&test).await;
assert_eq!(
test.fs()
.read_file_text(&target_path, /*sandbox*/ None)
.read_file_text(&target_path_uri, /*sandbox*/ None)
.await?,
"remote updated\n"
);
@@ -793,7 +807,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
let _ = fs::remove_file(&target_path);
test.fs()
.remove(
&target_path,
&target_path_uri,
RemoveOptions {
recursive: false,
force: true,
@@ -803,7 +817,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
.await?;
test.fs()
.remove(
&remote_cwd,
&remote_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -832,9 +846,10 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
))
.abs();
let remote_cwd_uri = PathUri::from_path(&remote_cwd)?;
test.fs()
.create_directory(
&remote_cwd,
&remote_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -885,7 +900,10 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
let remote_contents = test
.fs()
.read_file_text(&remote_cwd.join(file_name), /*sandbox*/ None)
.read_file_text(
&PathUri::from_path(remote_cwd.join(file_name))?,
/*sandbox*/ None,
)
.await?;
assert_eq!(remote_contents, "patched remote exec\n");
assert!(
@@ -895,7 +913,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
test.fs()
.remove(
&remote_cwd,
&remote_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -919,16 +937,18 @@ async fn remote_test_env_sandboxed_read_allows_readable_root() -> Result<()> {
let allowed_dir = PathBuf::from(format!("/tmp/codex-remote-readable-{}", std::process::id()));
let file_path = allowed_dir.join("note.txt");
let allowed_dir_uri = PathUri::from_path(&allowed_dir)?;
let file_path_uri = PathUri::from_path(&file_path)?;
file_system
.create_directory(
&absolute_path(allowed_dir.clone()),
&allowed_dir_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
file_system
.write_file(
&absolute_path(file_path.clone()),
&file_path_uri,
b"sandboxed hello".to_vec(),
/*sandbox*/ None,
)
@@ -936,13 +956,13 @@ async fn remote_test_env_sandboxed_read_allows_readable_root() -> Result<()> {
let sandbox = read_only_sandbox(allowed_dir.clone());
let contents = file_system
.read_file(&absolute_path(file_path.clone()), Some(&sandbox))
.read_file(&file_path_uri, Some(&sandbox))
.await?;
assert_eq!(contents, b"sandboxed hello");
file_system
.remove(
&absolute_path(allowed_dir),
&allowed_dir_uri,
RemoveOptions {
recursive: true,
force: true,
@@ -976,7 +996,8 @@ async fn remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape() -
secret = secret_path.display(),
))?;
let requested_path = absolute_path(allowed_dir.join("link").join("..").join("secret.txt"));
let requested_path =
PathUri::from_path(allowed_dir.join("link").join("..").join("secret.txt"))?;
let sandbox = read_only_sandbox(allowed_dir.clone());
let error = match file_system.read_file(&requested_path, Some(&sandbox)).await {
Ok(_) => anyhow::bail!("read should fail after path normalization"),
@@ -1023,7 +1044,7 @@ async fn remote_test_env_remove_removes_symlink_not_target() -> Result<()> {
let sandbox = workspace_write_sandbox(allowed_dir.clone());
file_system
.remove(
&absolute_path(symlink_path.clone()),
&PathUri::from_path(&symlink_path)?,
RemoveOptions {
recursive: false,
force: false,
@@ -1033,18 +1054,21 @@ async fn remote_test_env_remove_removes_symlink_not_target() -> Result<()> {
.await?;
let symlink_exists = file_system
.get_metadata(&absolute_path(symlink_path), /*sandbox*/ None)
.get_metadata(
&PathUri::from_abs_path(&absolute_path(symlink_path))?,
/*sandbox*/ None,
)
.await
.is_ok();
assert!(!symlink_exists);
let outside = file_system
.read_file_text(&absolute_path(outside_file.clone()), /*sandbox*/ None)
.read_file_text(&PathUri::from_path(&outside_file)?, /*sandbox*/ None)
.await?;
assert_eq!(outside, "outside");
file_system
.remove(
&absolute_path(root),
&PathUri::from_path(&root)?,
RemoveOptions {
recursive: true,
force: true,
@@ -1085,8 +1109,8 @@ async fn remote_test_env_copy_preserves_symlink_source() -> Result<()> {
let sandbox = workspace_write_sandbox(allowed_dir.clone());
file_system
.copy(
&absolute_path(source_symlink),
&absolute_path(copied_symlink.clone()),
&PathUri::from_path(&source_symlink)?,
&PathUri::from_path(&copied_symlink)?,
CopyOptions { recursive: false },
Some(&sandbox),
)
@@ -1117,7 +1141,7 @@ async fn remote_test_env_copy_preserves_symlink_source() -> Result<()> {
file_system
.remove(
&absolute_path(root),
&PathUri::from_path(&root)?,
RemoveOptions {
recursive: true,
force: true,
+4 -1
View File
@@ -48,6 +48,7 @@ use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use codex_utils_cargo_bin::cargo_bin;
use codex_utils_path_uri::PathUri;
use core_test_support::assert_regex_match;
use core_test_support::remote_env_env_var;
use core_test_support::responses;
@@ -625,8 +626,10 @@ async fn stdio_server_uses_configured_cwd_before_runtime_fallback() -> anyhow::R
let fixture = test_codex()
.with_workspace_setup(|cwd, fs| async move {
let configured_cwd = cwd.join("mcp-configured-cwd");
let configured_cwd_uri = PathUri::from_path(&configured_cwd)?;
fs.create_directory(
&cwd.join("mcp-configured-cwd"),
&configured_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
+5 -2
View File
@@ -9,6 +9,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
@@ -29,15 +30,17 @@ async fn write_repo_skill(
body: &str,
) -> Result<()> {
let skill_dir = cwd.join(".agents").join("skills").join(name);
let skill_dir_uri = PathUri::from_path(&skill_dir)?;
fs.create_directory(
&skill_dir,
&skill_dir_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
let contents = format!("---\nname: {name}\ndescription: {description}\n---\n\n{body}\n");
let path = skill_dir.join("SKILL.md");
fs.write_file(&path, contents.into_bytes(), /*sandbox*/ None)
let path_uri = PathUri::from_path(&path)?;
fs.write_file(&path_uri, contents.into_bytes(), /*sandbox*/ None)
.await?;
Ok(())
}
+3 -1
View File
@@ -17,6 +17,7 @@ use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathUri;
use core_test_support::TempDirExt;
use core_test_support::assert_regex_match;
use core_test_support::managed_network_requirements_loader;
@@ -228,9 +229,10 @@ async fn create_workspace_directory(
rel_path: impl AsRef<std::path::Path>,
) -> Result<std::path::PathBuf> {
let abs_path = test.config.cwd.join(rel_path.as_ref());
let abs_path_uri = PathUri::from_path(&abs_path)?;
test.fs()
.create_directory(
&abs_path,
&abs_path_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
+12 -6
View File
@@ -29,6 +29,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::get_remote_test_env;
@@ -134,9 +135,10 @@ fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>>
async fn create_workspace_directory(test: &TestCodex, rel_path: &str) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
let abs_path_uri = PathUri::from_path(&abs_path)?;
test.fs()
.create_directory(
&abs_path,
&abs_path_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -151,16 +153,18 @@ async fn write_workspace_file(
) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
if let Some(parent) = abs_path.parent() {
let parent_uri = PathUri::from_path(&parent)?;
test.fs()
.create_directory(
&parent,
&parent_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
}
let abs_path_uri = PathUri::from_path(&abs_path)?;
test.fs()
.write_file(&abs_path, contents, /*sandbox*/ None)
.write_file(&abs_path_uri, contents, /*sandbox*/ None)
.await?;
Ok(abs_path.into_path_buf())
}
@@ -607,16 +611,18 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
))
.abs();
let image_path = remote_cwd.join("remote.png");
let remote_cwd_uri = PathUri::from_path(&remote_cwd)?;
test.fs()
.create_directory(
&remote_cwd,
&remote_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
let png = png_bytes(/*width*/ 1, /*height*/ 1, [0, 255, 0, 255])?;
let image_path_uri = PathUri::from_path(&image_path)?;
test.fs()
.write_file(&image_path, png, /*sandbox*/ None)
.write_file(&image_path_uri, png, /*sandbox*/ None)
.await?;
let remote_selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
@@ -675,7 +681,7 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
test.fs()
.remove(
&remote_cwd,
&remote_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
+1
View File
@@ -24,6 +24,7 @@ codex-protocol = { workspace = true }
codex-sandboxing = { workspace = true }
codex-shell-command = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
futures = { workspace = true }
+1
View File
@@ -882,6 +882,7 @@ mod tests {
std::env::current_exe().expect("current exe").as_path(),
)
.expect("absolute current exe");
let path = codex_utils_path_uri::PathUri::from_abs_path(&path).expect("path URI");
let sandbox = crate::FileSystemSandboxContext::from_permission_profile(
codex_protocol::models::PermissionProfile::from_runtime_permissions(
&codex_protocol::permissions::FileSystemSandboxPolicy::restricted(Vec::new()),
+90 -21
View File
@@ -189,8 +189,10 @@ pub(crate) async fn run_direct_request(
let file_system = DirectFileSystem;
match request {
FsHelperRequest::ReadFile(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let data = file_system
.read_file(&params.path, /*sandbox*/ None)
.read_file(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::ReadFile(FsReadFileResponse {
@@ -198,21 +200,25 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::WriteFile(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
invalid_request(format!(
"{FS_WRITE_FILE_METHOD} requires valid base64 dataBase64: {err}"
))
})?;
file_system
.write_file(&params.path, bytes, /*sandbox*/ None)
.write_file(&path, bytes, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::WriteFile(FsWriteFileResponse {}))
}
FsHelperRequest::CreateDirectory(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
file_system
.create_directory(
&params.path,
&path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
@@ -225,8 +231,10 @@ pub(crate) async fn run_direct_request(
))
}
FsHelperRequest::GetMetadata(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let metadata = file_system
.get_metadata(&params.path, /*sandbox*/ None)
.get_metadata(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
Ok(FsHelperPayload::GetMetadata(FsGetMetadataResponse {
@@ -238,17 +246,22 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::Canonicalize(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let path = file_system
.canonicalize(&params.path, /*sandbox*/ None)
.canonicalize(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?;
let path = path.to_abs_path().map_err(map_fs_error)?;
Ok(FsHelperPayload::Canonicalize(FsCanonicalizeResponse {
path,
}))
}
FsHelperRequest::ReadDirectory(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let entries = file_system
.read_directory(&params.path, /*sandbox*/ None)
.read_directory(&path, /*sandbox*/ None)
.await
.map_err(map_fs_error)?
.into_iter()
@@ -263,9 +276,11 @@ pub(crate) async fn run_direct_request(
}))
}
FsHelperRequest::Remove(params) => {
let path =
codex_utils_path_uri::PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
file_system
.remove(
&params.path,
&path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
@@ -277,10 +292,15 @@ pub(crate) async fn run_direct_request(
Ok(FsHelperPayload::Remove(FsRemoveResponse {}))
}
FsHelperRequest::Copy(params) => {
let source_path = codex_utils_path_uri::PathUri::from_abs_path(&params.source_path)
.map_err(map_fs_error)?;
let destination_path =
codex_utils_path_uri::PathUri::from_abs_path(&params.destination_path)
.map_err(map_fs_error)?;
file_system
.copy(
&params.source_path,
&params.destination_path,
&source_path,
&destination_path,
CopyOptions {
recursive: params.recursive,
},
@@ -305,23 +325,72 @@ fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
#[cfg(test)]
mod tests {
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
use super::*;
#[test]
fn helper_requests_use_fs_method_names() -> serde_json::Result<()> {
assert_eq!(
serde_json::to_value(FsHelperRequest::WriteFile(FsWriteFileParams {
path: std::env::current_dir()
.expect("cwd")
.join("file")
.as_path()
.try_into()
.expect("absolute path"),
fn helper_protocol_keeps_native_absolute_paths() -> serde_json::Result<()> {
let local_path =
AbsolutePathBuf::from_absolute_path(std::env::current_dir().expect("cwd").join("file"))
.expect("absolute path");
#[cfg(not(windows))]
let paths = [local_path];
#[cfg(windows)]
let paths = [
local_path,
AbsolutePathBuf::from_absolute_path(r"\\server\share\file").expect("absolute UNC path"),
];
for path in paths {
let expected_path = path.to_string_lossy().into_owned();
let request = serde_json::to_value(FsHelperRequest::WriteFile(FsWriteFileParams {
path: path.clone(),
data_base64: String::new(),
sandbox: None,
}))?["operation"],
FS_WRITE_FILE_METHOD,
);
}))?;
assert_eq!(
request,
json!({
"operation": FS_WRITE_FILE_METHOD,
"params": {
"path": expected_path.as_str(),
"dataBase64": "",
"sandbox": null,
},
}),
);
let request_path = request["params"]["path"]
.as_str()
.expect("request path should be a string");
assert_eq!(request_path, expected_path);
assert!(!request_path.starts_with("file:"));
let response = serde_json::to_value(FsHelperResponse::Ok(
FsHelperPayload::Canonicalize(FsCanonicalizeResponse { path }),
))?;
assert_eq!(
response,
json!({
"status": "ok",
"payload": {
"operation": FS_CANONICALIZE_METHOD,
"response": {
"path": expected_path.as_str(),
},
},
}),
);
let response_path = response["payload"]["response"]["path"]
.as_str()
.expect("canonicalize response path should be a string");
assert_eq!(response_path, expected_path);
assert!(!response_path.starts_with("file:"));
}
Ok(())
}
}
+47 -69
View File
@@ -1,5 +1,6 @@
use async_trait::async_trait;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
@@ -81,28 +82,16 @@ impl LocalFileSystem {
impl ExecutorFileSystem for LocalFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
file_system.canonicalize(path, sandbox).await
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
self.unsandboxed.join(base_path, path).await
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
self.unsandboxed.parent(path).await
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
@@ -111,7 +100,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -121,7 +110,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -131,7 +120,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
@@ -140,7 +129,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let (file_system, sandbox) = self.file_system_for(sandbox)?;
@@ -149,7 +138,7 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -159,8 +148,8 @@ impl ExecutorFileSystem for LocalFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -175,28 +164,16 @@ impl ExecutorFileSystem for LocalFileSystem {
impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
reject_platform_sandbox_context(sandbox)?;
self.file_system.canonicalize(path, /*sandbox*/ None).await
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
self.file_system.join(base_path, path).await
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
self.file_system.parent(path).await
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_platform_sandbox_context(sandbox)?;
@@ -205,7 +182,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -217,7 +194,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -229,7 +206,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_platform_sandbox_context(sandbox)?;
@@ -238,7 +215,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_platform_sandbox_context(sandbox)?;
@@ -249,7 +226,7 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -261,8 +238,8 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -282,31 +259,23 @@ impl ExecutorFileSystem for UnsandboxedFileSystem {
impl ExecutorFileSystem for DirectFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
reject_sandbox_context(sandbox)?;
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
Ok(base_path.join(path))
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
Ok(path.parent())
let path = path.to_abs_path()?;
let canonicalized =
AbsolutePathBuf::from_absolute_path(tokio::fs::canonicalize(path.as_path()).await?)?;
PathUri::from_abs_path(&canonicalized)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
if metadata.len() > MAX_READ_FILE_BYTES {
return Err(io::Error::new(
@@ -319,21 +288,23 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
tokio::fs::write(path.as_path(), contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
if options.recursive {
tokio::fs::create_dir_all(path.as_path()).await?;
} else {
@@ -344,10 +315,11 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
Ok(FileMetadata {
@@ -361,10 +333,11 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
let mut entries = Vec::new();
let mut read_dir = tokio::fs::read_dir(path.as_path()).await?;
while let Some(entry) = read_dir.next_entry().await? {
@@ -382,11 +355,12 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let path = path.to_abs_path()?;
match tokio::fs::symlink_metadata(path.as_path()).await {
Ok(metadata) => {
let file_type = metadata.file_type();
@@ -408,14 +382,14 @@ impl ExecutorFileSystem for DirectFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
reject_sandbox_context(sandbox)?;
let source_path = source_path.to_path_buf();
let destination_path = destination_path.to_path_buf();
let source_path = source_path.to_abs_path()?.into_path_buf();
let destination_path = destination_path.to_abs_path()?.into_path_buf();
tokio::task::spawn_blocking(move || -> FileSystemResult<()> {
let metadata = std::fs::symlink_metadata(source_path.as_path())?;
let file_type = metadata.file_type();
@@ -576,6 +550,10 @@ fn system_time_to_unix_ms(time: SystemTime) -> i64 {
.unwrap_or(0)
}
#[cfg(all(test, any(unix, windows)))]
#[path = "local_file_system_path_uri_tests.rs"]
mod path_uri_tests;
#[cfg(all(test, unix))]
mod tests {
use super::*;
@@ -0,0 +1,27 @@
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::io;
use super::*;
#[tokio::test]
async fn direct_file_system_rejects_non_native_uri_as_invalid_input() {
let error = DirectFileSystem
.read_file(&non_native_uri(), /*sandbox*/ None)
.await
.expect_err("non-native URI should be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
fn non_native_uri() -> PathUri {
#[cfg(unix)]
let uri = "file://server/share/file.txt";
#[cfg(windows)]
let uri = "file:///usr/local/file.txt";
match PathUri::parse(uri) {
Ok(uri) => uri,
Err(err) => panic!("valid non-native URI should parse: {err}"),
}
}
+35 -51
View File
@@ -1,8 +1,7 @@
use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use codex_utils_path_uri::PathUri;
use tokio::io;
use tracing::trace;
@@ -20,8 +19,6 @@ use crate::protocol::FsCanonicalizeParams;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
use crate::protocol::FsJoinParams;
use crate::protocol::FsParentParams;
use crate::protocol::FsReadDirectoryParams;
use crate::protocol::FsReadFileParams;
use crate::protocol::FsRemoveParams;
@@ -45,58 +42,33 @@ impl RemoteFileSystem {
impl ExecutorFileSystem for RemoteFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
trace!("remote fs canonicalize");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_canonicalize(FsCanonicalizeParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
.map_err(map_remote_error)?;
Ok(response.path)
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
trace!("remote fs join");
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_join(FsJoinParams {
base_path: base_path.clone(),
path: path.to_path_buf(),
})
.await
.map_err(map_remote_error)?;
Ok(response.path)
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
trace!("remote fs parent");
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_parent(FsParentParams { path: path.clone() })
.await
.map_err(map_remote_error)?;
Ok(response.path)
PathUri::from_abs_path(&response.path)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
trace!("remote fs read_file");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_read_file(FsReadFileParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
@@ -111,15 +83,16 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs write_file");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_write_file(FsWriteFileParams {
path: path.clone(),
path,
data_base64: STANDARD.encode(contents),
sandbox: remote_sandbox_context(sandbox),
})
@@ -130,15 +103,16 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs create_directory");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
path,
recursive: Some(options.recursive),
sandbox: remote_sandbox_context(sandbox),
})
@@ -149,14 +123,15 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
trace!("remote fs get_metadata");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
@@ -172,14 +147,15 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
trace!("remote fs read_directory");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
path,
sandbox: remote_sandbox_context(sandbox),
})
.await
@@ -197,15 +173,16 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs remove");
let path = path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_remove(FsRemoveParams {
path: path.clone(),
path,
recursive: Some(options.recursive),
force: Some(options.force),
sandbox: remote_sandbox_context(sandbox),
@@ -217,17 +194,19 @@ impl ExecutorFileSystem for RemoteFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs copy");
let source_path = source_path.to_abs_path()?;
let destination_path = destination_path.to_abs_path()?;
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
source_path,
destination_path,
recursive: options.recursive,
sandbox: remote_sandbox_context(sandbox),
})
@@ -261,6 +240,10 @@ fn map_remote_error(error: ExecServerError) -> io::Error {
}
}
#[cfg(all(test, any(unix, windows)))]
#[path = "remote_file_system_path_uri_tests.rs"]
mod path_uri_tests;
#[cfg(test)]
mod tests {
use codex_protocol::models::PermissionProfile;
@@ -270,6 +253,7 @@ mod tests {
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use super::*;
@@ -0,0 +1,225 @@
#![allow(clippy::expect_used)]
#[cfg(windows)]
use codex_app_server_protocol::JSONRPCMessage;
#[cfg(windows)]
use codex_app_server_protocol::JSONRPCResponse;
#[cfg(windows)]
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
#[cfg(windows)]
use futures::SinkExt;
#[cfg(windows)]
use futures::StreamExt;
use pretty_assertions::assert_eq;
use tokio::io;
#[cfg(windows)]
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
#[cfg(windows)]
use tokio::sync::oneshot;
#[cfg(windows)]
use tokio::time::Duration;
#[cfg(windows)]
use tokio::time::timeout;
#[cfg(windows)]
use tokio_tungstenite::WebSocketStream;
#[cfg(windows)]
use tokio_tungstenite::accept_async;
#[cfg(windows)]
use tokio_tungstenite::tungstenite::Message;
use super::*;
use crate::client_api::ExecServerTransportParams;
#[cfg(windows)]
use crate::protocol::FS_READ_FILE_METHOD;
#[cfg(windows)]
use crate::protocol::FsReadFileParams;
#[cfg(windows)]
use crate::protocol::FsReadFileResponse;
#[cfg(windows)]
use crate::protocol::INITIALIZE_METHOD;
#[cfg(windows)]
use crate::protocol::INITIALIZED_METHOD;
#[cfg(windows)]
use crate::protocol::InitializeResponse;
#[tokio::test]
async fn non_native_uri_is_rejected_before_connecting() {
let file_system = RemoteFileSystem::new(LazyRemoteExecServerClient::new(
ExecServerTransportParams::websocket_url("not a websocket URL".to_string()),
));
let error = file_system
.read_file(&non_native_uri(), /*sandbox*/ None)
.await
.expect_err("non-native URI should be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
#[cfg(windows)]
#[tokio::test]
async fn remote_file_system_sends_explicit_windows_native_paths() {
let (websocket_url, captured_paths, server) = record_read_file_paths(2).await;
let file_system = RemoteFileSystem::new(LazyRemoteExecServerClient::new(
ExecServerTransportParams::websocket_url(websocket_url),
));
let paths = vec![
(
PathUri::parse("file:///C:/Users/Alice/src/main.rs").expect("valid drive URI"),
absolute_windows_path(r"C:\Users\Alice\src\main.rs"),
),
(
PathUri::parse("file://server/share/src/main.rs").expect("valid UNC URI"),
absolute_windows_path(r"\\server\share\src\main.rs"),
),
];
let expected_paths = paths
.iter()
.map(|(_, expected_path)| expected_path.clone())
.collect::<Vec<_>>();
for (path, _) in paths {
assert_eq!(
file_system
.read_file(&path, /*sandbox*/ None)
.await
.expect("remote read should succeed"),
Vec::<u8>::new()
);
}
assert_eq!(
captured_paths.await.expect("captured paths"),
expected_paths
);
server.await.expect("recording server should succeed");
}
fn non_native_uri() -> PathUri {
#[cfg(unix)]
let uri = "file://server/share/file.txt";
#[cfg(windows)]
let uri = "file:///usr/local/file.txt";
PathUri::parse(uri).expect("valid non-native URI")
}
#[cfg(windows)]
fn absolute_windows_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path_checked(path).expect("absolute Windows path")
}
#[cfg(windows)]
async fn record_read_file_paths(
expected_requests: usize,
) -> (
String,
oneshot::Receiver<Vec<AbsolutePathBuf>>,
tokio::task::JoinHandle<()>,
) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
let websocket_url = format!("ws://{}", listener.local_addr().expect("listener address"));
let (captured_paths_tx, captured_paths_rx) = oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("listener should accept");
let mut websocket = accept_async(stream)
.await
.expect("websocket handshake should succeed");
complete_websocket_initialize(&mut websocket).await;
let mut captured_paths = Vec::with_capacity(expected_requests);
for _ in 0..expected_requests {
let request = match read_jsonrpc_websocket(&mut websocket).await {
JSONRPCMessage::Request(request) if request.method == FS_READ_FILE_METHOD => {
request
}
other => panic!("expected fs/readFile request, got {other:?}"),
};
let params: FsReadFileParams =
serde_json::from_value(request.params.expect("fs/readFile params should exist"))
.expect("fs/readFile params should deserialize");
captured_paths.push(params.path);
write_jsonrpc_websocket(
&mut websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(FsReadFileResponse {
data_base64: String::new(),
})
.expect("fs/readFile response should serialize"),
}),
)
.await;
}
captured_paths_tx
.send(captured_paths)
.expect("captured paths receiver should stay open");
});
(websocket_url, captured_paths_rx, server)
}
#[cfg(windows)]
async fn complete_websocket_initialize(websocket: &mut WebSocketStream<TcpStream>) {
let request = match read_jsonrpc_websocket(websocket).await {
JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
other => panic!("expected initialize request, got {other:?}"),
};
write_jsonrpc_websocket(
websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(InitializeResponse {
session_id: "session-1".to_string(),
})
.expect("initialize response should serialize"),
}),
)
.await;
match read_jsonrpc_websocket(websocket).await {
JSONRPCMessage::Notification(notification) if notification.method == INITIALIZED_METHOD => {
}
other => panic!("expected initialized notification, got {other:?}"),
}
}
#[cfg(windows)]
async fn read_jsonrpc_websocket(websocket: &mut WebSocketStream<TcpStream>) -> JSONRPCMessage {
loop {
match timeout(Duration::from_secs(1), websocket.next())
.await
.expect("json-rpc websocket read should not time out")
.expect("websocket should stay open")
.expect("websocket frame should read")
{
Message::Text(text) => {
return serde_json::from_str(text.as_ref())
.expect("json-rpc text frame should parse");
}
Message::Binary(bytes) => {
return serde_json::from_slice(bytes.as_ref())
.expect("json-rpc binary frame should parse");
}
Message::Ping(_) | Message::Pong(_) => {}
other => panic!("expected json-rpc websocket frame, got {other:?}"),
}
}
}
#[cfg(windows)]
async fn write_jsonrpc_websocket(
websocket: &mut WebSocketStream<TcpStream>,
message: JSONRPCMessage,
) {
let encoded = serde_json::to_string(&message).expect("json-rpc should serialize");
websocket
.send(Message::Text(encoded.into()))
.await
.expect("json-rpc websocket frame should write");
}
@@ -2,8 +2,7 @@ use async_trait::async_trait;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use codex_utils_path_uri::PathUri;
use tokio::io;
use crate::CopyOptions;
@@ -55,39 +54,27 @@ impl SandboxedFileSystem {
impl ExecutorFileSystem for SandboxedFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
) -> FileSystemResult<PathUri> {
let sandbox = require_platform_sandbox(sandbox)?;
let response = self
.run_sandboxed(
sandbox,
FsHelperRequest::Canonicalize(FsCanonicalizeParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
.await?
.expect_canonicalize()
.map_err(map_sandbox_error)?;
Ok(response.path)
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
Ok(base_path.join(path))
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
Ok(path.parent())
PathUri::from_abs_path(&response.path)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
let sandbox = require_platform_sandbox(sandbox)?;
@@ -95,7 +82,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
.run_sandboxed(
sandbox,
FsHelperRequest::ReadFile(FsReadFileParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
@@ -112,7 +99,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -120,7 +107,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::WriteFile(FsWriteFileParams {
path: path.clone(),
path: path.to_abs_path()?,
data_base64: STANDARD.encode(contents),
sandbox: None,
}),
@@ -133,7 +120,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -141,7 +128,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::CreateDirectory(FsCreateDirectoryParams {
path: path.clone(),
path: path.to_abs_path()?,
recursive: Some(options.recursive),
sandbox: None,
}),
@@ -154,7 +141,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
let sandbox = require_platform_sandbox(sandbox)?;
@@ -162,7 +149,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
.run_sandboxed(
sandbox,
FsHelperRequest::GetMetadata(FsGetMetadataParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
@@ -180,7 +167,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
let sandbox = require_platform_sandbox(sandbox)?;
@@ -188,7 +175,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
.run_sandboxed(
sandbox,
FsHelperRequest::ReadDirectory(FsReadDirectoryParams {
path: path.clone(),
path: path.to_abs_path()?,
sandbox: None,
}),
)
@@ -208,7 +195,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
remove_options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -216,7 +203,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::Remove(FsRemoveParams {
path: path.clone(),
path: path.to_abs_path()?,
recursive: Some(remove_options.recursive),
force: Some(remove_options.force),
sandbox: None,
@@ -230,8 +217,8 @@ impl ExecutorFileSystem for SandboxedFileSystem {
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -239,8 +226,8 @@ impl ExecutorFileSystem for SandboxedFileSystem {
self.run_sandboxed(
sandbox,
FsHelperRequest::Copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
source_path: source_path.to_abs_path()?,
destination_path: destination_path.to_abs_path()?,
recursive: options.recursive,
sandbox: None,
}),
@@ -272,3 +259,7 @@ fn map_sandbox_error(error: JSONRPCErrorError) -> io::Error {
_ => io::Error::other(error.message),
}
}
#[cfg(all(test, any(unix, windows)))]
#[path = "sandboxed_file_system_path_uri_tests.rs"]
mod path_uri_tests;
@@ -0,0 +1,43 @@
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::io;
use super::*;
#[tokio::test]
async fn sandboxed_file_system_rejects_non_native_uri_as_invalid_input() {
let runtime_paths = ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let file_system = SandboxedFileSystem::new(runtime_paths);
let sandbox = FileSystemSandboxContext::from_permission_profile(
PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(Vec::new()),
NetworkSandboxPolicy::Restricted,
),
);
let error = file_system
.read_file(&non_native_uri(), Some(&sandbox))
.await
.expect_err("non-native URI should be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
fn non_native_uri() -> PathUri {
#[cfg(unix)]
let uri = "file://server/share/file.txt";
#[cfg(windows)]
let uri = "file:///usr/local/file.txt";
match PathUri::parse(uri) {
Ok(uri) => uri,
Err(err) => panic!("valid non-native URI should parse: {err}"),
}
}
@@ -3,6 +3,7 @@ use std::io;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_utils_path_uri::PathUri;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
@@ -52,9 +53,10 @@ impl FileSystemHandler {
&self,
params: FsReadFileParams,
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = self
.file_system
.read_file(&params.path, params.sandbox.as_ref())
.read_file(&path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsReadFileResponse {
@@ -66,13 +68,14 @@ impl FileSystemHandler {
&self,
params: FsWriteFileParams,
) -> Result<FsWriteFileResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let bytes = STANDARD.decode(params.data_base64).map_err(|err| {
invalid_request(format!(
"{FS_WRITE_FILE_METHOD} requires valid base64 dataBase64: {err}"
))
})?;
self.file_system
.write_file(&params.path, bytes, params.sandbox.as_ref())
.write_file(&path, bytes, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
@@ -83,9 +86,10 @@ impl FileSystemHandler {
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
let recursive = params.recursive.unwrap_or(true);
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system
.create_directory(
&params.path,
&path,
CreateDirectoryOptions { recursive },
params.sandbox.as_ref(),
)
@@ -98,9 +102,10 @@ impl FileSystemHandler {
&self,
params: FsGetMetadataParams,
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let metadata = self
.file_system
.get_metadata(&params.path, params.sandbox.as_ref())
.get_metadata(&path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
@@ -116,11 +121,13 @@ impl FileSystemHandler {
&self,
params: FsCanonicalizeParams,
) -> Result<FsCanonicalizeResponse, JSONRPCErrorError> {
let requested_path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let path = self
.file_system
.canonicalize(&params.path, params.sandbox.as_ref())
.canonicalize(&requested_path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?;
let path = path.to_abs_path().map_err(map_fs_error)?;
Ok(FsCanonicalizeResponse { path })
}
@@ -128,11 +135,8 @@ impl FileSystemHandler {
&self,
params: FsJoinParams,
) -> Result<FsJoinResponse, JSONRPCErrorError> {
let path = self
.file_system
.join(&params.base_path, &params.path)
.await
.map_err(map_fs_error)?;
// TODO(anp): remove and migrate callers to PathUri.
let path = params.base_path.join(params.path);
Ok(FsJoinResponse { path })
}
@@ -140,11 +144,8 @@ impl FileSystemHandler {
&self,
params: FsParentParams,
) -> Result<FsParentResponse, JSONRPCErrorError> {
let path = self
.file_system
.parent(&params.path)
.await
.map_err(map_fs_error)?;
// TODO(anp): remove and migrate callers to PathUri.
let path = params.path.parent();
Ok(FsParentResponse { path })
}
@@ -152,9 +153,10 @@ impl FileSystemHandler {
&self,
params: FsReadDirectoryParams,
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
let entries = self
.file_system
.read_directory(&params.path, params.sandbox.as_ref())
.read_directory(&path, params.sandbox.as_ref())
.await
.map_err(map_fs_error)?
.into_iter()
@@ -173,9 +175,10 @@ impl FileSystemHandler {
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
let recursive = params.recursive.unwrap_or(true);
let force = params.force.unwrap_or(true);
let path = PathUri::from_abs_path(&params.path).map_err(map_fs_error)?;
self.file_system
.remove(
&params.path,
&path,
RemoveOptions { recursive, force },
params.sandbox.as_ref(),
)
@@ -188,10 +191,13 @@ impl FileSystemHandler {
&self,
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
let source_path = PathUri::from_abs_path(&params.source_path).map_err(map_fs_error)?;
let destination_path =
PathUri::from_abs_path(&params.destination_path).map_err(map_fs_error)?;
self.file_system
.copy(
&params.source_path,
&params.destination_path,
&source_path,
&destination_path,
CopyOptions {
recursive: params.recursive,
},
@@ -262,6 +268,24 @@ mod tests {
.await
.expect("write file");
let canonicalized = handler
.canonicalize(FsCanonicalizeParams {
path: path.clone(),
sandbox: Some(FileSystemSandboxContext::from_legacy_sandbox_policy(
sandbox_policy.clone(),
sandbox_cwd.clone(),
)),
})
.await
.expect("canonicalize file");
assert_eq!(
canonicalized.path,
AbsolutePathBuf::from_absolute_path(
std::fs::canonicalize(path.as_path()).expect("canonical path"),
)
.expect("absolute canonical path"),
);
let response = handler
.read_file(FsReadFileParams {
path,
@@ -276,4 +300,34 @@ mod tests {
assert_eq!(response.data_base64, STANDARD.encode("ok"));
}
}
#[tokio::test]
async fn protocol_join_and_parent_remain_native_path_operations() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let runtime_paths = ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let handler = FileSystemHandler::new(runtime_paths);
let base_path =
AbsolutePathBuf::from_absolute_path(temp_dir.path()).expect("absolute tempdir");
let joined = handler
.join(FsJoinParams {
base_path: base_path.clone(),
path: "nested/file.txt".into(),
})
.await
.expect("join path");
assert_eq!(joined.path, base_path.join("nested/file.txt"));
let parent = handler
.parent(FsParentParams {
path: joined.path.clone(),
})
.await
.expect("parent path");
assert_eq!(parent.path, joined.path.parent());
}
}
@@ -9,6 +9,7 @@ use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::path::Path;
use tempfile::TempDir;
@@ -61,7 +62,7 @@ async fn file_system_get_metadata_reports_files_and_directories(
std::fs::create_dir(&directory_path)?;
let file_metadata = file_system
.get_metadata(&absolute_path(&file_path), /*sandbox*/ None)
.get_metadata(&PathUri::from_path(&file_path)?, /*sandbox*/ None)
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(file_metadata.is_directory, false);
@@ -70,7 +71,7 @@ async fn file_system_get_metadata_reports_files_and_directories(
assert!(file_metadata.modified_at_ms > 0);
let directory_metadata = file_system
.get_metadata(&absolute_path(&directory_path), /*sandbox*/ None)
.get_metadata(&PathUri::from_path(&directory_path)?, /*sandbox*/ None)
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(directory_metadata.is_directory, true);
@@ -95,7 +96,7 @@ async fn file_system_create_directory_creates_nested_directories(
file_system
.create_directory(
&absolute_path(&nested_dir),
&PathUri::from_path(&nested_dir)?,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
@@ -119,7 +120,7 @@ async fn file_system_write_file_writes_bytes(
let file_path = tmp.path().join("note.txt");
file_system
.write_file(
&absolute_path(&file_path),
&PathUri::from_path(&file_path)?,
b"hello from trait".to_vec(),
/*sandbox*/ None,
)
@@ -130,42 +131,26 @@ async fn file_system_write_file_writes_bytes(
Ok(())
}
#[test_case(FileSystemImplementation::Local ; "local")]
#[test_case(FileSystemImplementation::Remote ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_join_and_parent_preserve_lexical_paths(
implementation: FileSystemImplementation,
) -> Result<()> {
let context = create_file_system_context(implementation).await?;
let file_system = context.file_system;
#[test]
fn path_uri_join_and_parent_preserve_lexical_paths() -> Result<()> {
let tmp = TempDir::new()?;
let source_dir = tmp.path().join("source");
let joined_nested = file_system
.join(&absolute_path(&source_dir), Path::new("nested/note.txt"))
.await
.with_context(|| format!("mode={implementation}"))?;
let source_dir_uri = PathUri::from_path(&source_dir)?;
let joined_nested = source_dir_uri.join("nested/note.txt")?;
assert_eq!(
joined_nested,
absolute_path(source_dir.join("nested").join("note.txt"))
PathUri::from_path(source_dir.join("nested").join("note.txt"))?
);
let joined_parent = file_system
.parent(&joined_nested)
.await
.with_context(|| format!("mode={implementation}"))?;
let joined_parent = joined_nested.parent();
assert_eq!(
joined_parent,
Some(absolute_path(source_dir.join("nested")))
Some(PathUri::from_path(source_dir.join("nested"))?)
);
let joined_parent_traversal = file_system
.join(&absolute_path(&source_dir), Path::new("../outside"))
.await
.with_context(|| format!("mode={implementation}"))?;
let joined_parent_traversal = source_dir_uri.join("../outside")?;
assert_eq!(
joined_parent_traversal,
absolute_path(source_dir.join("../outside"))
PathUri::from_path(source_dir.join("../outside"))?
);
Ok(())
}
@@ -183,7 +168,7 @@ async fn file_system_read_file_returns_bytes(
std::fs::write(&file_path, "hello from trait")?;
let contents = file_system
.read_file(&absolute_path(&file_path), /*sandbox*/ None)
.read_file(&PathUri::from_path(&file_path)?, /*sandbox*/ None)
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(contents, b"hello from trait");
@@ -205,7 +190,7 @@ async fn file_system_read_file_text_returns_string(
std::fs::write(&file_path, "hello from trait")?;
let contents = file_system
.read_file_text(&absolute_path(&file_path), /*sandbox*/ None)
.read_file_text(&PathUri::from_path(&file_path)?, /*sandbox*/ None)
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(contents, "hello from trait");
@@ -227,8 +212,8 @@ async fn file_system_copy_copies_file(implementation: FileSystemImplementation)
file_system
.copy(
&absolute_path(&source_file),
&absolute_path(&copied_file),
&PathUri::from_path(&source_file)?,
&PathUri::from_path(&copied_file)?,
CopyOptions { recursive: false },
/*sandbox*/ None,
)
@@ -258,8 +243,8 @@ async fn file_system_copy_copies_directory_recursively(
file_system
.copy(
&absolute_path(&source_dir),
&absolute_path(&copied_dir),
&PathUri::from_path(&source_dir)?,
&PathUri::from_path(&copied_dir)?,
CopyOptions { recursive: true },
/*sandbox*/ None,
)
@@ -288,7 +273,7 @@ async fn file_system_read_directory_lists_entries(
std::fs::write(source_dir.join("root.txt"), "hello")?;
let mut entries = file_system
.read_directory(&absolute_path(&source_dir), /*sandbox*/ None)
.read_directory(&PathUri::from_path(&source_dir)?, /*sandbox*/ None)
.await
.with_context(|| format!("mode={implementation}"))?;
entries.sort_by(|left, right| left.file_name.cmp(&right.file_name));
@@ -326,7 +311,7 @@ async fn file_system_remove_removes_directory(
file_system
.remove(
&absolute_path(&directory_path),
&PathUri::from_path(&directory_path)?,
RemoveOptions {
recursive: true,
force: true,
@@ -354,7 +339,7 @@ async fn file_system_write_file_reports_missing_parent(
let error = match file_system
.write_file(
&absolute_path(&missing_parent_path),
&PathUri::from_path(&missing_parent_path)?,
b"hello from trait".to_vec(),
/*sandbox*/ None,
)
@@ -388,8 +373,8 @@ async fn file_system_copy_rejects_directory_without_recursive(
let error = file_system
.copy(
&absolute_path(&source_dir),
&absolute_path(tmp.path().join("dest")),
&PathUri::from_path(&source_dir)?,
&PathUri::from_path(tmp.path().join("dest"))?,
CopyOptions { recursive: false },
/*sandbox*/ None,
)
@@ -424,7 +409,7 @@ async fn file_system_sandboxed_read_allows_readable_root(
let sandbox = read_only_sandbox(allowed_dir);
let contents = file_system
.read_file(&absolute_path(&file_path), Some(&sandbox))
.read_file(&PathUri::from_path(&file_path)?, Some(&sandbox))
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(contents, b"sandboxed hello");
@@ -448,8 +433,8 @@ pub(crate) async fn assert_canonicalize_resolves_directory_alias(
std::fs::write(&file_path, "canonical hello")?;
create_directory_alias(&source_dir, &alias_dir)?;
let requested_path = absolute_path(alias_dir.join("nested").join("note.txt"));
let expected_path = absolute_path(std::fs::canonicalize(&file_path)?);
let requested_path = PathUri::from_path(alias_dir.join("nested").join("note.txt"))?;
let expected_path = PathUri::from_path(std::fs::canonicalize(&file_path)?)?;
assert_ne!(requested_path, expected_path);
let canonical_path = file_system
@@ -478,8 +463,8 @@ pub(crate) async fn assert_sandboxed_canonicalize_resolves_directory_alias(
create_directory_alias(&source_dir, &alias_dir)?;
let sandbox = read_only_sandbox(tmp.path().to_path_buf());
let requested_path = absolute_path(alias_dir.join("nested").join("note.txt"));
let expected_path = absolute_path(std::fs::canonicalize(&file_path)?);
let requested_path = PathUri::from_path(alias_dir.join("nested").join("note.txt"))?;
let expected_path = PathUri::from_path(std::fs::canonicalize(&file_path)?)?;
assert_ne!(requested_path, expected_path);
let canonical_path = file_system
@@ -532,7 +517,7 @@ async fn file_system_sandboxed_write_allows_additional_write_root(
file_system
.write_file(
&absolute_path(&file_path),
&PathUri::from_path(&file_path)?,
b"created".to_vec(),
Some(&sandbox),
)
@@ -558,8 +543,8 @@ async fn file_system_copy_rejects_copying_directory_into_descendant(
let error = file_system
.copy(
&absolute_path(&source_dir),
&absolute_path(source_dir.join("nested").join("copy")),
&PathUri::from_path(&source_dir)?,
&PathUri::from_path(source_dir.join("nested").join("copy"))?,
CopyOptions { recursive: true },
/*sandbox*/ None,
)
@@ -1,5 +1,4 @@
use std::fmt;
use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
@@ -71,8 +70,7 @@ pub(crate) async fn create_file_system_context(
}
}
pub(crate) fn absolute_path(path: impl AsRef<Path>) -> AbsolutePathBuf {
let path = path.as_ref().to_path_buf();
pub(crate) fn absolute_path(path: std::path::PathBuf) -> AbsolutePathBuf {
assert!(
path.is_absolute(),
"path must be absolute: {}",
+31 -27
View File
@@ -22,6 +22,7 @@ use codex_exec_server::CreateDirectoryOptions;
#[cfg(target_os = "linux")]
use codex_exec_server::Environment;
use codex_exec_server::RemoveOptions;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use test_case::test_case;
@@ -30,7 +31,6 @@ use test_case::test_case;
use crate::common::exec_server::exec_server_with_env;
use crate::support::FileSystemImplementation;
use crate::support::absolute_path;
use crate::support::create_file_system_context;
use crate::support::read_only_sandbox;
use crate::support::workspace_write_sandbox;
@@ -185,7 +185,7 @@ async fn sandboxed_file_system_helper_finds_bwrap_on_preserved_path() -> Result<
file_system
.write_file(
&absolute_path(&file_path),
&PathUri::from_path(&file_path)?,
b"written through fs helper".to_vec(),
Some(&sandbox),
)
@@ -219,7 +219,7 @@ async fn file_system_get_metadata_reports_symlink_targets(
let symlink_path = tmp.path().join("note-link.txt");
symlink(&file_path, &symlink_path)?;
let symlink_metadata = file_system
.get_metadata(&absolute_path(&symlink_path), /*sandbox*/ None)
.get_metadata(&PathUri::from_path(&symlink_path)?, /*sandbox*/ None)
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(symlink_metadata.is_directory, false);
@@ -232,7 +232,10 @@ async fn file_system_get_metadata_reports_symlink_targets(
let dir_symlink_path = tmp.path().join("notes-link");
symlink(&dir_path, &dir_symlink_path)?;
let dir_symlink_metadata = file_system
.get_metadata(&absolute_path(&dir_symlink_path), /*sandbox*/ None)
.get_metadata(
&PathUri::from_path(&dir_symlink_path)?,
/*sandbox*/ None,
)
.await
.with_context(|| format!("mode={implementation}"))?;
assert_eq!(dir_symlink_metadata.is_directory, true);
@@ -257,7 +260,7 @@ async fn file_system_sandboxed_write_rejects_unwritable_path(
let sandbox = read_only_sandbox(tmp.path().to_path_buf());
let error = match file_system
.write_file(
&absolute_path(&blocked_path),
&PathUri::from_path(&blocked_path)?,
b"nope".to_vec(),
Some(&sandbox),
)
@@ -293,7 +296,7 @@ async fn file_system_sandboxed_write_allows_explicit_alias_roots(
file_system
.write_file(
&absolute_path(&file_path),
&PathUri::from_path(&file_path)?,
b"created".to_vec(),
Some(&sandbox),
)
@@ -324,7 +327,7 @@ async fn file_system_sandboxed_read_rejects_symlink_escape(
let requested_path = allowed_dir.join("link").join("secret.txt");
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system
.read_file(&absolute_path(&requested_path), Some(&sandbox))
.read_file(&PathUri::from_path(&requested_path)?, Some(&sandbox))
.await
{
Ok(_) => anyhow::bail!("read should be blocked"),
@@ -353,13 +356,14 @@ async fn file_system_sandboxed_read_rejects_symlink_parent_dotdot_escape(
std::fs::write(&secret_path, "nope")?;
symlink(&outside_dir, allowed_dir.join("link"))?;
let requested_path = absolute_path(allowed_dir.join("link").join("..").join("secret.txt"));
let requested_path =
PathUri::from_path(allowed_dir.join("link").join("..").join("secret.txt"))?;
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system.read_file(&requested_path, Some(&sandbox)).await {
Ok(_) => anyhow::bail!("read should fail after path normalization"),
Err(error) => error,
};
// AbsolutePathBuf normalizes `link/../secret.txt` to
// PathUri's native path constructor normalizes `link/../secret.txt` to
// `allowed/secret.txt` before the request reaches the filesystem layer.
// Depending on whether the platform/runtime resolves that normalized path
// through a top-level symlink alias, the request can surface as either
@@ -389,7 +393,7 @@ async fn file_system_sandboxed_write_rejects_symlink_escape(
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.write_file(
&absolute_path(&requested_path),
&PathUri::from_path(&requested_path)?,
b"nope".to_vec(),
Some(&sandbox),
)
@@ -427,7 +431,7 @@ async fn file_system_sandboxed_write_preserves_existing_hard_link(
let sandbox = workspace_write_sandbox(allowed_dir);
file_system
.write_file(
&absolute_path(&hard_link),
&PathUri::from_path(&hard_link)?,
b"updated through existing hard link\n".to_vec(),
Some(&sandbox),
)
@@ -473,7 +477,7 @@ async fn file_system_create_directory_rejects_symlink_escape(
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.create_directory(
&absolute_path(&requested_path),
&PathUri::from_path(&requested_path)?,
CreateDirectoryOptions { recursive: false },
Some(&sandbox),
)
@@ -508,7 +512,7 @@ async fn file_system_read_directory_rejects_symlink_escape(
let requested_path = allowed_dir.join("link");
let sandbox = read_only_sandbox(allowed_dir);
let error = match file_system
.read_directory(&absolute_path(&requested_path), Some(&sandbox))
.read_directory(&PathUri::from_path(&requested_path)?, Some(&sandbox))
.await
{
Ok(_) => anyhow::bail!("read_directory should be blocked"),
@@ -540,8 +544,8 @@ async fn file_system_copy_rejects_symlink_escape_destination(
let sandbox = workspace_write_sandbox(allowed_dir.clone());
let error = match file_system
.copy(
&absolute_path(allowed_dir.join("source.txt")),
&absolute_path(&requested_destination),
&PathUri::from_path(allowed_dir.join("source.txt"))?,
&PathUri::from_path(&requested_destination)?,
CopyOptions { recursive: false },
Some(&sandbox),
)
@@ -578,7 +582,7 @@ async fn file_system_remove_removes_symlink_not_target(
let sandbox = workspace_write_sandbox(allowed_dir);
file_system
.remove(
&absolute_path(&symlink_path),
&PathUri::from_path(&symlink_path)?,
RemoveOptions {
recursive: false,
force: false,
@@ -618,8 +622,8 @@ async fn file_system_copy_preserves_symlink_source(
let sandbox = workspace_write_sandbox(allowed_dir.clone());
file_system
.copy(
&absolute_path(&source_symlink),
&absolute_path(&copied_symlink),
&PathUri::from_path(&source_symlink)?,
&PathUri::from_path(&copied_symlink)?,
CopyOptions { recursive: false },
Some(&sandbox),
)
@@ -655,7 +659,7 @@ async fn file_system_remove_rejects_symlink_escape(
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.remove(
&absolute_path(&requested_path),
&PathUri::from_path(&requested_path)?,
RemoveOptions {
recursive: false,
force: false,
@@ -696,8 +700,8 @@ async fn file_system_copy_rejects_symlink_escape_source(
let sandbox = workspace_write_sandbox(allowed_dir);
let error = match file_system
.copy(
&absolute_path(&requested_source),
&absolute_path(&requested_destination),
&PathUri::from_path(&requested_source)?,
&PathUri::from_path(&requested_destination)?,
CopyOptions { recursive: false },
Some(&sandbox),
)
@@ -730,8 +734,8 @@ async fn file_system_copy_preserves_symlinks_in_recursive_copy(
file_system
.copy(
&absolute_path(&source_dir),
&absolute_path(&copied_dir),
&PathUri::from_path(&source_dir)?,
&PathUri::from_path(&copied_dir)?,
CopyOptions { recursive: true },
/*sandbox*/ None,
)
@@ -776,8 +780,8 @@ async fn file_system_copy_ignores_unknown_special_files_in_recursive_copy(
file_system
.copy(
&absolute_path(&source_dir),
&absolute_path(&copied_dir),
&PathUri::from_path(&source_dir)?,
&PathUri::from_path(&copied_dir)?,
CopyOptions { recursive: true },
/*sandbox*/ None,
)
@@ -815,8 +819,8 @@ async fn file_system_copy_rejects_standalone_fifo_source(
let error = file_system
.copy(
&absolute_path(&fifo_path),
&absolute_path(tmp.path().join("copied")),
&PathUri::from_path(&fifo_path)?,
&PathUri::from_path(tmp.path().join("copied"))?,
CopyOptions { recursive: false },
/*sandbox*/ None,
)
+1
View File
@@ -21,6 +21,7 @@ codex-mcp = { workspace = true }
codex-protocol = { workspace = true }
codex-tools = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-string = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+8 -1
View File
@@ -10,6 +10,7 @@ use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use crate::catalog::SkillAuthority;
use crate::catalog::SkillCatalog;
@@ -130,9 +131,15 @@ impl SkillProvider for ExecutorSkillProvider {
"executor skill resource references unavailable environment `{environment_id}`"
)));
};
let resource_path = PathUri::from_abs_path(resource_path).map_err(|err| {
SkillProviderError::new(format!(
"failed to read executor skill resource {}: {err}",
request.resource.as_str()
))
})?;
let contents = environment
.get_filesystem()
.read_file_text(resource_path, /*sandbox*/ None)
.read_file_text(&resource_path, /*sandbox*/ None)
.await
.map_err(|err| {
SkillProviderError::new(format!(
@@ -1,5 +1,4 @@
use std::io;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
@@ -24,6 +23,7 @@ use codex_skills_extension::ExecutorSkillProvider;
use codex_skills_extension::provider::SkillListQuery;
use codex_skills_extension::provider::SkillProvider;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
const SKILL_CONTENTS: &str =
@@ -60,34 +60,23 @@ impl SyntheticFileSystem {
impl ExecutorFileSystem for SyntheticFileSystem {
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf> {
if path == &self.alias_root {
return Ok(self.canonical_root.clone());
) -> FileSystemResult<PathUri> {
let path = path.to_abs_path()?;
if path == self.alias_root {
return PathUri::from_abs_path(&self.canonical_root);
}
self.metadata(path)?;
Ok(path.clone())
}
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf> {
Ok(base_path.join(path))
}
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>> {
Ok(path.parent())
self.metadata(&path)?;
PathUri::from_abs_path(&path)
}
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
if path == &self.canonical_root.join("skill/SKILL.md") {
if path.to_abs_path()? == self.canonical_root.join("skill/SKILL.md") {
Ok(SKILL_CONTENTS.as_bytes().to_vec())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
@@ -96,7 +85,7 @@ impl ExecutorFileSystem for SyntheticFileSystem {
async fn write_file(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_contents: Vec<u8>,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -105,7 +94,7 @@ impl ExecutorFileSystem for SyntheticFileSystem {
async fn create_directory(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_options: CreateDirectoryOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -114,24 +103,25 @@ impl ExecutorFileSystem for SyntheticFileSystem {
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
self.metadata(path)
self.metadata(&path.to_abs_path()?)
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
if path == &self.canonical_root {
let path = path.to_abs_path()?;
if path == self.canonical_root {
Ok(vec![ReadDirectoryEntry {
file_name: "skill".to_string(),
is_directory: true,
is_file: false,
}])
} else if path == &self.canonical_root.join("skill") {
} else if path == self.canonical_root.join("skill") {
Ok(vec![ReadDirectoryEntry {
file_name: "SKILL.md".to_string(),
is_directory: false,
@@ -144,7 +134,7 @@ impl ExecutorFileSystem for SyntheticFileSystem {
async fn remove(
&self,
_path: &AbsolutePathBuf,
_path: &PathUri,
_options: RemoveOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
@@ -153,8 +143,8 @@ impl ExecutorFileSystem for SyntheticFileSystem {
async fn copy(
&self,
_source_path: &AbsolutePathBuf,
_destination_path: &AbsolutePathBuf,
_source_path: &PathUri,
_destination_path: &PathUri,
_options: CopyOptions,
_sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
+1
View File
@@ -11,6 +11,7 @@ workspace = true
async-trait = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[lib]
+12 -21
View File
@@ -9,6 +9,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::io;
use std::path::Path;
@@ -136,30 +137,20 @@ pub trait ExecutorFileSystem: Send + Sync {
/// Resolves a path within this filesystem.
async fn canonicalize(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<AbsolutePathBuf>;
/// Lexically joins a path onto an existing bound path.
async fn join(
&self,
base_path: &AbsolutePathBuf,
path: &Path,
) -> FileSystemResult<AbsolutePathBuf>;
/// Returns the parent directory of a bound path.
async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult<Option<AbsolutePathBuf>>;
) -> FileSystemResult<PathUri>;
async fn read_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>>;
/// Reads a file and decodes it as UTF-8 text.
async fn read_file_text(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<String> {
let bytes = self.read_file(path, sandbox).await?;
@@ -168,41 +159,41 @@ pub trait ExecutorFileSystem: Send + Sync {
async fn write_file(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
contents: Vec<u8>,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()>;
async fn create_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
create_directory_options: CreateDirectoryOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()>;
async fn get_metadata(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata>;
async fn read_directory(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
async fn remove(
&self,
path: &AbsolutePathBuf,
path: &PathUri,
remove_options: RemoveOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()>;
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
source_path: &PathUri,
destination_path: &PathUri,
copy_options: CopyOptions,
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()>;
+1
View File
@@ -14,6 +14,7 @@ chrono = { workspace = true }
codex-file-system = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
futures = { workspace = true, features = ["alloc"] }
gix = { workspace = true }
once_cell = { workspace = true }
+15 -4
View File
@@ -6,6 +6,7 @@ use std::path::PathBuf;
use codex_file_system::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::future::join_all;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -46,7 +47,8 @@ pub async fn get_git_repo_root_with_fs(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
) -> Option<AbsolutePathBuf> {
let base = match fs.get_metadata(cwd, /*sandbox*/ None).await {
let cwd_uri = PathUri::from_abs_path(cwd).ok()?;
let base = match fs.get_metadata(&cwd_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_directory => cwd.clone(),
_ => cwd.parent()?,
};
@@ -803,8 +805,9 @@ pub async fn resolve_root_git_project_for_trust(
) -> Option<AbsolutePathBuf> {
let repo_root = get_git_repo_root_with_fs(fs, cwd).await?;
let dot_git = repo_root.join(".git");
let dot_git_uri = PathUri::from_abs_path(&dot_git).ok()?;
if fs
.get_metadata(&dot_git, /*sandbox*/ None)
.get_metadata(&dot_git_uri, /*sandbox*/ None)
.await
.ok()?
.is_directory
@@ -812,7 +815,10 @@ pub async fn resolve_root_git_project_for_trust(
return Some(repo_root);
}
let git_dir_s = fs.read_file_text(&dot_git, /*sandbox*/ None).await.ok()?;
let git_dir_s = fs
.read_file_text(&dot_git_uri, /*sandbox*/ None)
.await
.ok()?;
let git_dir_rel = git_dir_s.trim().strip_prefix("gitdir:")?.trim();
if git_dir_rel.is_empty() {
return None;
@@ -853,7 +859,12 @@ async fn find_ancestor_git_entry_with_fs(
) -> Option<(AbsolutePathBuf, AbsolutePathBuf)> {
for dir in base_dir.ancestors() {
let dot_git = dir.join(".git");
if fs.get_metadata(&dot_git, /*sandbox*/ None).await.is_ok() {
let dot_git_uri = PathUri::from_abs_path(&dot_git).ok()?;
if fs
.get_metadata(&dot_git_uri, /*sandbox*/ None)
.await
.is_ok()
{
return Some((dir, dot_git));
}
}
+1
View File
@@ -17,6 +17,7 @@ workspace = true
codex-exec-server = { workspace = true }
codex-login = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -2,6 +2,7 @@
use codex_exec_server::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::path::Path;
use std::path::PathBuf;
@@ -29,7 +30,8 @@ async fn plugin_manifest_name(
let mut manifest_path = None;
for relative_path in DISCOVERABLE_PLUGIN_MANIFEST_PATHS {
let candidate = plugin_root.join(relative_path);
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
let candidate_uri = PathUri::from_abs_path(&candidate).ok()?;
match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {
Ok(metadata) if metadata.is_file => {
manifest_path = Some(candidate);
break;
@@ -38,8 +40,9 @@ async fn plugin_manifest_name(
}
}
let manifest_path = manifest_path?;
let manifest_path_uri = PathUri::from_abs_path(&manifest_path).ok()?;
let contents = fs
.read_file_text(&manifest_path, /*sandbox*/ None)
.read_file_text(&manifest_path_uri, /*sandbox*/ None)
.await
.ok()?;
let RawPluginManifestName { name: raw_name } = serde_json::from_str(&contents).ok()?;