Files
codex/codex-rs/core-plugins/src/provider_tests.rs
T
pakrym-oai 76d8f20241 [codex] Add size to internal filesystem metadata (#27927)
## Why

`ExecutorFileSystem::get_metadata` reports file kind and timestamps but
not size. Internal callers that need to enforce a size limit therefore
have to read the complete file first, which is especially wasteful for
remote filesystems.

This adds the missing internal metadata so consumers can reject
oversized files before transferring or buffering them. The field is
named `size`, matching VS Code's `FileStat.size` filesystem convention.

## What changed

- add `size: u64` to internal `FileMetadata`
- populate it from the underlying filesystem metadata
- carry it through sandbox-helper and remote exec-server responses
- cover files, directories, symlink targets, and sandboxed reads across
local and remote filesystem implementations

The new field is intentionally not exposed through the app-server API.

## Testing

- `just test -p codex-exec-server get_metadata`
- `just test -p codex-exec-server
file_system_sandboxed_metadata_and_read_allow_readable_root`
- `just test -p codex-core-plugins`
- `just test -p codex-skills-extension`
2026-06-12 12:12:08 -07:00

334 lines
10 KiB
Rust

use super::ExecutorPluginProvider;
use super::ExecutorPluginProviderError;
use super::resolve_plugin_root;
use crate::manifest::parse_plugin_manifest;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::ExecutorFileSystemFuture;
use codex_exec_server::FileMetadata;
use codex_exec_server::FileSystemResult;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::ReadDirectoryEntry;
use codex_exec_server::RemoveOptions;
use codex_plugin::PluginProvider;
use codex_plugin::ResolvedPlugin;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::fs;
use std::io;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use tempfile::tempdir;
const MANIFEST_CONTENTS: &str = r#"{
"name": "demo-plugin",
"version": " 1.2.3 ",
"description": "Demo plugin",
"skills": "./skills",
"mcpServers": "./.mcp.json",
"apps": "./.app.json",
"interface": {
"displayName": "Demo Plugin",
"composerIcon": "./assets/icon.svg"
}
}"#;
#[derive(Debug, PartialEq, Eq)]
enum FileSystemCall {
Metadata(AbsolutePathBuf),
Read(AbsolutePathBuf),
}
struct SyntheticPluginFileSystem {
plugin_root: AbsolutePathBuf,
manifest_path: AbsolutePathBuf,
calls: Mutex<Vec<FileSystemCall>>,
}
impl SyntheticPluginFileSystem {
fn unsupported<T>() -> FileSystemResult<T> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"operation is not used by plugin resolution",
))
}
}
impl ExecutorFileSystem for SyntheticPluginFileSystem {
fn canonicalize<'a>(
&'a self,
_path: &'a PathUri,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, PathUri> {
Box::pin(async { Self::unsupported() })
}
fn read_file<'a>(
&'a self,
path: &'a PathUri,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<u8>> {
Box::pin(async move {
let path = path.to_abs_path()?;
self.calls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(FileSystemCall::Read(path.clone()));
if path == self.manifest_path {
Ok(MANIFEST_CONTENTS.as_bytes().to_vec())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
}
})
}
fn write_file<'a>(
&'a self,
_path: &'a PathUri,
_contents: Vec<u8>,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(async { Self::unsupported() })
}
fn create_directory<'a>(
&'a self,
_path: &'a PathUri,
_options: CreateDirectoryOptions,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(async { Self::unsupported() })
}
fn get_metadata<'a>(
&'a self,
path: &'a PathUri,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
Box::pin(async move {
let path = path.to_abs_path()?;
self.calls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(FileSystemCall::Metadata(path.clone()));
let (is_directory, is_file) = if path == self.plugin_root {
(true, false)
} else if path == self.manifest_path {
(false, true)
} else {
return Err(io::Error::new(io::ErrorKind::NotFound, "not found"));
};
Ok(FileMetadata {
is_directory,
is_file,
is_symlink: false,
size: 0,
created_at_ms: 0,
modified_at_ms: 0,
})
})
}
fn read_directory<'a>(
&'a self,
_path: &'a PathUri,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, Vec<ReadDirectoryEntry>> {
Box::pin(async { Self::unsupported() })
}
fn remove<'a>(
&'a self,
_path: &'a PathUri,
_options: RemoveOptions,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(async { Self::unsupported() })
}
fn copy<'a>(
&'a self,
_source_path: &'a PathUri,
_destination_path: &'a PathUri,
_options: CopyOptions,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, ()> {
Box::pin(async { Self::unsupported() })
}
}
fn write_manifest(plugin_root: &Path, relative_path: &str, contents: &str) {
let manifest_path = plugin_root.join(relative_path);
fs::create_dir_all(manifest_path.parent().expect("manifest parent"))
.expect("create manifest parent");
fs::write(manifest_path, contents).expect("write manifest");
}
fn selected_root(id: &str, environment_id: &str, path: &Path) -> SelectedCapabilityRoot {
SelectedCapabilityRoot {
id: id.to_string(),
location: CapabilityRootLocation::Environment {
environment_id: environment_id.to_string(),
path: path.to_string_lossy().into_owned(),
},
}
}
#[tokio::test]
async fn plugin_root_resolution_uses_supplied_executor_file_system() {
let temp_dir = tempdir().expect("tempdir");
let plugin_root = temp_dir.path().join("executor-only-plugin");
assert!(!plugin_root.exists());
let plugin_root =
AbsolutePathBuf::from_absolute_path_checked(plugin_root).expect("absolute plugin root");
let manifest_path = plugin_root.join(".codex-plugin/plugin.json");
let parsed_manifest = parse_plugin_manifest(
plugin_root.as_path(),
manifest_path.as_path(),
MANIFEST_CONTENTS,
)
.expect("parse manifest");
let file_system = SyntheticPluginFileSystem {
plugin_root: plugin_root.clone(),
manifest_path: manifest_path.clone(),
calls: Mutex::new(Vec::new()),
};
let resolved = resolve_plugin_root(
&selected_root("selected-demo", "executor-test", plugin_root.as_path()),
plugin_root.clone(),
&file_system,
)
.await
.expect("resolve executor plugin");
assert_eq!(
resolved,
Some(
ResolvedPlugin::from_environment(
"selected-demo".to_string(),
"executor-test".to_string(),
plugin_root.clone(),
manifest_path.clone(),
parsed_manifest,
)
.expect("valid expected descriptor")
)
);
assert_eq!(
*file_system
.calls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![
FileSystemCall::Metadata(plugin_root),
FileSystemCall::Metadata(manifest_path.clone()),
FileSystemCall::Read(manifest_path),
]
);
}
#[tokio::test]
async fn standalone_capability_root_is_not_a_plugin() {
let temp_dir = tempdir().expect("tempdir");
let standalone_root = temp_dir.path().join("standalone-skill");
fs::create_dir_all(&standalone_root).expect("create standalone root");
let provider = ExecutorPluginProvider::new(Arc::new(EnvironmentManager::default_for_tests()));
let resolved = provider
.resolve(&selected_root(
"standalone",
LOCAL_ENVIRONMENT_ID,
&standalone_root,
))
.await
.expect("resolve standalone root");
assert_eq!(resolved, None);
}
#[tokio::test]
async fn unavailable_environment_does_not_fall_back_to_host_filesystem() {
let temp_dir = tempdir().expect("tempdir");
let plugin_root = temp_dir.path().join("host-plugin");
write_manifest(&plugin_root, ".codex-plugin/plugin.json", MANIFEST_CONTENTS);
let provider =
ExecutorPluginProvider::new(Arc::new(EnvironmentManager::without_environments()));
let err = provider
.resolve(&selected_root("host-plugin", "missing", &plugin_root))
.await
.expect_err("missing environment should fail");
assert_eq!(
err.to_string(),
"selected capability root `host-plugin` references unavailable environment `missing`"
);
}
#[tokio::test]
async fn malformed_preferred_manifest_does_not_fall_through_to_alternate() {
let temp_dir = tempdir().expect("tempdir");
let plugin_root = temp_dir.path().join("demo-plugin");
write_manifest(&plugin_root, ".codex-plugin/plugin.json", "{not-json");
write_manifest(
&plugin_root,
".claude-plugin/plugin.json",
MANIFEST_CONTENTS,
);
let expected_path =
AbsolutePathBuf::from_absolute_path_checked(plugin_root.join(".codex-plugin/plugin.json"))
.expect("absolute manifest path");
let provider = ExecutorPluginProvider::new(Arc::new(EnvironmentManager::default_for_tests()));
let err = provider
.resolve(&selected_root(
"selected-demo",
LOCAL_ENVIRONMENT_ID,
&plugin_root,
))
.await
.expect_err("malformed preferred manifest should fail");
let ExecutorPluginProviderError::ParseManifest {
root_id,
path,
source: _,
} = err
else {
panic!("expected parse error");
};
assert_eq!(
(root_id, path),
("selected-demo".to_string(), expected_path)
);
}
#[tokio::test]
async fn executor_root_must_be_an_explicit_absolute_path() {
let provider = ExecutorPluginProvider::new(Arc::new(EnvironmentManager::default_for_tests()));
let selected_root = SelectedCapabilityRoot {
id: "selected-demo".to_string(),
location: CapabilityRootLocation::Environment {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
path: "~/plugins/demo".to_string(),
},
};
let err = provider
.resolve(&selected_root)
.await
.expect_err("home-relative executor path should fail");
assert_eq!(
err.to_string(),
"selected capability root `selected-demo` has invalid path `~/plugins/demo`: executor path must be absolute"
);
}