tools: remove unused experimental list_dir tool (#21170)

## Why
`list_dir` still carries a full spec/handler/test path, but nothing in
the current model catalog advertises it via
`experimental_supported_tools`. That leaves us maintaining an
environment-backed tool surface that is effectively unused.

## What changed
- delete the `list_dir` handler and its tests from `codex-core`
- remove the `list_dir` spec builder, handler kind, and registry wiring
from `codex-tools`
- clean up the remaining internal README and registry tests so they no
longer mention the removed tool
This commit is contained in:
jif-oai
2026-05-05 13:11:07 +02:00
committed by GitHub
Unverified
parent 9d579813bb
commit 70807730f5
11 changed files with 2 additions and 734 deletions
@@ -1,294 +0,0 @@
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::fs::FileType;
use std::path::Path;
use std::path::PathBuf;
use codex_protocol::permissions::ReadDenyMatcher;
use codex_utils_string::take_bytes_at_char_boundary;
use serde::Deserialize;
use tokio::fs;
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
pub struct ListDirHandler;
const DENY_READ_POLICY_MESSAGE: &str =
"access denied: reading this path is blocked by filesystem deny_read policy";
const MAX_ENTRY_LENGTH: usize = 500;
const INDENTATION_SPACES: usize = 2;
fn default_offset() -> usize {
1
}
fn default_limit() -> usize {
25
}
fn default_depth() -> usize {
2
}
#[derive(Deserialize)]
struct ListDirArgs {
dir_path: String,
#[serde(default = "default_offset")]
offset: usize,
#[serde(default = "default_limit")]
limit: usize,
#[serde(default = "default_depth")]
depth: usize,
}
impl ToolHandler for ListDirHandler {
type Output = FunctionToolOutput;
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation { payload, turn, .. } = invocation;
let arguments = match payload {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(
"list_dir handler received unsupported payload".to_string(),
));
}
};
let args: ListDirArgs = parse_arguments(&arguments)?;
let ListDirArgs {
dir_path,
offset,
limit,
depth,
} = args;
if offset == 0 {
return Err(FunctionCallError::RespondToModel(
"offset must be a 1-indexed entry number".to_string(),
));
}
if limit == 0 {
return Err(FunctionCallError::RespondToModel(
"limit must be greater than zero".to_string(),
));
}
if depth == 0 {
return Err(FunctionCallError::RespondToModel(
"depth must be greater than zero".to_string(),
));
}
let path = PathBuf::from(&dir_path);
if !path.is_absolute() {
return Err(FunctionCallError::RespondToModel(
"dir_path must be an absolute path".to_string(),
));
}
let file_system_sandbox_policy = turn.file_system_sandbox_policy();
let read_deny_matcher = ReadDenyMatcher::new(&file_system_sandbox_policy, &turn.cwd);
if read_deny_matcher
.as_ref()
.is_some_and(|matcher| matcher.is_read_denied(&path))
{
return Err(FunctionCallError::RespondToModel(format!(
"{DENY_READ_POLICY_MESSAGE}: `{}`",
path.display()
)));
}
let entries =
list_dir_slice_with_policy(&path, offset, limit, depth, read_deny_matcher.as_ref())
.await?;
let mut output = Vec::with_capacity(entries.len() + 1);
output.push(format!("Absolute path: {}", path.display()));
output.extend(entries);
Ok(FunctionToolOutput::from_text(output.join("\n"), Some(true)))
}
}
async fn list_dir_slice_with_policy(
path: &Path,
offset: usize,
limit: usize,
depth: usize,
read_deny_matcher: Option<&ReadDenyMatcher>,
) -> Result<Vec<String>, FunctionCallError> {
let mut entries = Vec::new();
collect_entries(path, Path::new(""), depth, read_deny_matcher, &mut entries).await?;
if entries.is_empty() {
return Ok(Vec::new());
}
entries.sort_unstable_by(|a, b| a.name.cmp(&b.name));
let start_index = offset - 1;
if start_index >= entries.len() {
return Err(FunctionCallError::RespondToModel(
"offset exceeds directory entry count".to_string(),
));
}
let remaining_entries = entries.len() - start_index;
let capped_limit = limit.min(remaining_entries);
let end_index = start_index + capped_limit;
let selected_entries = &entries[start_index..end_index];
let mut formatted = Vec::with_capacity(selected_entries.len());
for entry in selected_entries {
formatted.push(format_entry_line(entry));
}
if end_index < entries.len() {
formatted.push(format!("More than {capped_limit} entries found"));
}
Ok(formatted)
}
async fn collect_entries(
dir_path: &Path,
relative_prefix: &Path,
depth: usize,
read_deny_matcher: Option<&ReadDenyMatcher>,
entries: &mut Vec<DirEntry>,
) -> Result<(), FunctionCallError> {
let mut queue = VecDeque::new();
queue.push_back((dir_path.to_path_buf(), relative_prefix.to_path_buf(), depth));
while let Some((current_dir, prefix, remaining_depth)) = queue.pop_front() {
let mut read_dir = fs::read_dir(&current_dir).await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to read directory: {err}"))
})?;
let mut dir_entries = Vec::new();
while let Some(entry) = read_dir.next_entry().await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to read directory: {err}"))
})? {
let entry_path = entry.path();
if let Some(read_deny_matcher) = read_deny_matcher
&& read_deny_matcher.is_read_denied(&entry_path)
{
continue;
}
let file_type = entry.file_type().await.map_err(|err| {
FunctionCallError::RespondToModel(format!("failed to inspect entry: {err}"))
})?;
let file_name = entry.file_name();
let relative_path = if prefix.as_os_str().is_empty() {
PathBuf::from(&file_name)
} else {
prefix.join(&file_name)
};
let display_name = format_entry_component(&file_name);
let display_depth = prefix.components().count();
let sort_key = format_entry_name(&relative_path);
let kind = DirEntryKind::from(&file_type);
dir_entries.push((
entry_path,
relative_path,
kind,
DirEntry {
name: sort_key,
display_name,
depth: display_depth,
kind,
},
));
}
dir_entries.sort_unstable_by(|a, b| a.3.name.cmp(&b.3.name));
for (entry_path, relative_path, kind, dir_entry) in dir_entries {
if kind == DirEntryKind::Directory && remaining_depth > 1 {
queue.push_back((entry_path, relative_path, remaining_depth - 1));
}
entries.push(dir_entry);
}
}
Ok(())
}
fn format_entry_name(path: &Path) -> String {
let normalized = path.to_string_lossy().replace("\\", "/");
if normalized.len() > MAX_ENTRY_LENGTH {
take_bytes_at_char_boundary(&normalized, MAX_ENTRY_LENGTH).to_string()
} else {
normalized
}
}
fn format_entry_component(name: &OsStr) -> String {
let normalized = name.to_string_lossy();
if normalized.len() > MAX_ENTRY_LENGTH {
take_bytes_at_char_boundary(&normalized, MAX_ENTRY_LENGTH).to_string()
} else {
normalized.to_string()
}
}
fn format_entry_line(entry: &DirEntry) -> String {
let indent = " ".repeat(entry.depth * INDENTATION_SPACES);
let mut name = entry.display_name.clone();
match entry.kind {
DirEntryKind::Directory => name.push('/'),
DirEntryKind::Symlink => name.push('@'),
DirEntryKind::Other => name.push('?'),
DirEntryKind::File => {}
}
format!("{indent}{name}")
}
#[derive(Clone)]
struct DirEntry {
name: String,
display_name: String,
depth: usize,
kind: DirEntryKind,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DirEntryKind {
Directory,
File,
Symlink,
Other,
}
impl From<&FileType> for DirEntryKind {
fn from(file_type: &FileType) -> Self {
if file_type.is_symlink() {
DirEntryKind::Symlink
} else if file_type.is_dir() {
DirEntryKind::Directory
} else if file_type.is_file() {
DirEntryKind::File
} else {
DirEntryKind::Other
}
}
}
#[cfg(test)]
#[path = "list_dir_tests.rs"]
mod tests;
@@ -1,331 +0,0 @@
use super::*;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::ReadDenyMatcher;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
async fn list_dir_slice(
path: &Path,
offset: usize,
limit: usize,
depth: usize,
) -> Result<Vec<String>, FunctionCallError> {
list_dir_slice_with_policy(path, offset, limit, depth, /*read_deny_matcher*/ None).await
}
#[tokio::test]
async fn lists_directory_entries() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
let sub_dir = dir_path.join("nested");
tokio::fs::create_dir(&sub_dir)
.await
.expect("create sub dir");
let deeper_dir = sub_dir.join("deeper");
tokio::fs::create_dir(&deeper_dir)
.await
.expect("create deeper dir");
tokio::fs::write(dir_path.join("entry.txt"), b"content")
.await
.expect("write file");
tokio::fs::write(sub_dir.join("child.txt"), b"child")
.await
.expect("write child");
tokio::fs::write(deeper_dir.join("grandchild.txt"), b"grandchild")
.await
.expect("write grandchild");
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let link_path = dir_path.join("link");
symlink(dir_path.join("entry.txt"), &link_path).expect("create symlink");
}
let entries = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 20, /*depth*/ 3,
)
.await
.expect("list directory");
#[cfg(unix)]
let expected = vec![
"entry.txt".to_string(),
"link@".to_string(),
"nested/".to_string(),
" child.txt".to_string(),
" deeper/".to_string(),
" grandchild.txt".to_string(),
];
#[cfg(not(unix))]
let expected = vec![
"entry.txt".to_string(),
"nested/".to_string(),
" child.txt".to_string(),
" deeper/".to_string(),
" grandchild.txt".to_string(),
];
assert_eq!(entries, expected);
}
#[tokio::test]
async fn errors_when_offset_exceeds_entries() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
tokio::fs::create_dir(dir_path.join("nested"))
.await
.expect("create sub dir");
let err = list_dir_slice(
dir_path, /*offset*/ 10, /*limit*/ 1, /*depth*/ 2,
)
.await
.expect_err("offset exceeds entries");
assert_eq!(
err,
FunctionCallError::RespondToModel("offset exceeds directory entry count".to_string())
);
}
#[tokio::test]
async fn respects_depth_parameter() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
let nested = dir_path.join("nested");
let deeper = nested.join("deeper");
tokio::fs::create_dir(&nested).await.expect("create nested");
tokio::fs::create_dir(&deeper).await.expect("create deeper");
tokio::fs::write(dir_path.join("root.txt"), b"root")
.await
.expect("write root");
tokio::fs::write(nested.join("child.txt"), b"child")
.await
.expect("write nested");
tokio::fs::write(deeper.join("grandchild.txt"), b"deep")
.await
.expect("write deeper");
let entries_depth_one = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 10, /*depth*/ 1,
)
.await
.expect("list depth 1");
assert_eq!(
entries_depth_one,
vec!["nested/".to_string(), "root.txt".to_string(),]
);
let entries_depth_two = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 20, /*depth*/ 2,
)
.await
.expect("list depth 2");
assert_eq!(
entries_depth_two,
vec![
"nested/".to_string(),
" child.txt".to_string(),
" deeper/".to_string(),
"root.txt".to_string(),
]
);
let entries_depth_three = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 30, /*depth*/ 3,
)
.await
.expect("list depth 3");
assert_eq!(
entries_depth_three,
vec![
"nested/".to_string(),
" child.txt".to_string(),
" deeper/".to_string(),
" grandchild.txt".to_string(),
"root.txt".to_string(),
]
);
}
#[tokio::test]
async fn paginates_in_sorted_order() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
let dir_a = dir_path.join("a");
let dir_b = dir_path.join("b");
tokio::fs::create_dir(&dir_a).await.expect("create a");
tokio::fs::create_dir(&dir_b).await.expect("create b");
tokio::fs::write(dir_a.join("a_child.txt"), b"a")
.await
.expect("write a child");
tokio::fs::write(dir_b.join("b_child.txt"), b"b")
.await
.expect("write b child");
let first_page = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 2, /*depth*/ 2,
)
.await
.expect("list page one");
assert_eq!(
first_page,
vec![
"a/".to_string(),
" a_child.txt".to_string(),
"More than 2 entries found".to_string()
]
);
let second_page = list_dir_slice(
dir_path, /*offset*/ 3, /*limit*/ 2, /*depth*/ 2,
)
.await
.expect("list page two");
assert_eq!(
second_page,
vec!["b/".to_string(), " b_child.txt".to_string()]
);
}
#[tokio::test]
async fn handles_large_limit_without_overflow() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
tokio::fs::write(dir_path.join("alpha.txt"), b"alpha")
.await
.expect("write alpha");
tokio::fs::write(dir_path.join("beta.txt"), b"beta")
.await
.expect("write beta");
tokio::fs::write(dir_path.join("gamma.txt"), b"gamma")
.await
.expect("write gamma");
let entries = list_dir_slice(dir_path, /*offset*/ 2, usize::MAX, /*depth*/ 1)
.await
.expect("list without overflow");
assert_eq!(
entries,
vec!["beta.txt".to_string(), "gamma.txt".to_string(),]
);
}
#[tokio::test]
async fn indicates_truncated_results() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
for idx in 0..40 {
let file = dir_path.join(format!("file_{idx:02}.txt"));
tokio::fs::write(file, b"content")
.await
.expect("write file");
}
let entries = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 25, /*depth*/ 1,
)
.await
.expect("list directory");
assert_eq!(entries.len(), 26);
assert_eq!(
entries.last(),
Some(&"More than 25 entries found".to_string())
);
}
#[tokio::test]
async fn truncation_respects_sorted_order() -> anyhow::Result<()> {
let temp = tempdir()?;
let dir_path = temp.path();
let nested = dir_path.join("nested");
let deeper = nested.join("deeper");
tokio::fs::create_dir(&nested).await?;
tokio::fs::create_dir(&deeper).await?;
tokio::fs::write(dir_path.join("root.txt"), b"root").await?;
tokio::fs::write(nested.join("child.txt"), b"child").await?;
tokio::fs::write(deeper.join("grandchild.txt"), b"deep").await?;
let entries_depth_three = list_dir_slice(
dir_path, /*offset*/ 1, /*limit*/ 3, /*depth*/ 3,
)
.await?;
assert_eq!(
entries_depth_three,
vec![
"nested/".to_string(),
" child.txt".to_string(),
" deeper/".to_string(),
"More than 3 entries found".to_string()
]
);
Ok(())
}
#[tokio::test]
async fn hides_denied_entries_and_prunes_denied_subtrees() {
let temp = tempdir().expect("create tempdir");
let dir_path = temp.path();
let visible_dir = dir_path.join("visible");
let denied_dir = dir_path.join("private");
tokio::fs::create_dir(&visible_dir)
.await
.expect("create visible dir");
tokio::fs::create_dir(&denied_dir)
.await
.expect("create denied dir");
tokio::fs::write(visible_dir.join("ok.txt"), b"ok")
.await
.expect("write visible file");
tokio::fs::write(denied_dir.join("secret.txt"), b"secret")
.await
.expect("write denied file");
tokio::fs::write(dir_path.join("top_secret.txt"), b"secret")
.await
.expect("write denied top-level file");
let policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: denied_dir.try_into().expect("absolute denied dir"),
},
access: FileSystemAccessMode::None,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: dir_path
.join("top_secret.txt")
.try_into()
.expect("absolute denied file"),
},
access: FileSystemAccessMode::None,
},
]);
let read_deny_matcher = ReadDenyMatcher::new(&policy, dir_path);
let entries = list_dir_slice_with_policy(
dir_path,
/*offset*/ 1,
/*limit*/ 20,
/*depth*/ 3,
read_deny_matcher.as_ref(),
)
.await
.expect("list directory");
assert_eq!(
entries,
vec!["visible/".to_string(), " ok.txt".to_string(),]
);
}
-2
View File
@@ -2,7 +2,6 @@ pub(crate) mod agent_jobs;
pub(crate) mod apply_patch;
mod dynamic;
mod goal;
mod list_dir;
mod mcp;
mod mcp_resource;
pub(crate) mod multi_agents;
@@ -38,7 +37,6 @@ use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::protocol::AskForApproval;
pub use dynamic::DynamicToolHandler;
pub use goal::GoalHandler;
pub use list_dir::ListDirHandler;
pub use mcp::McpHandler;
pub use mcp_resource::McpResourceHandler;
pub use plan::PlanHandler;
-4
View File
@@ -78,7 +78,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
use crate::tools::handlers::CodeModeWaitHandler;
use crate::tools::handlers::DynamicToolHandler;
use crate::tools::handlers::GoalHandler;
use crate::tools::handlers::ListDirHandler;
use crate::tools::handlers::McpHandler;
use crate::tools::handlers::McpResourceHandler;
use crate::tools::handlers::PlanHandler;
@@ -223,9 +222,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
ToolHandlerKind::ListAgentsV2 => {
builder.register_handler(handler.name, Arc::new(ListAgentsHandlerV2));
}
ToolHandlerKind::ListDir => {
builder.register_handler(handler.name, Arc::new(ListDirHandler));
}
ToolHandlerKind::Mcp => {
builder.register_handler(handler.name, mcp_handler.clone());
}
+1 -1
View File
@@ -22,7 +22,7 @@ schema and Responses API tool primitives that no longer need to live in
- `ResponsesApiNamespace`
- `ResponsesApiNamespaceTool`
- code-mode `ToolSpec` adapters and `exec` / `wait` spec builders
- MCP resource, `list_dir`, and `test_sync_tool` spec builders
- MCP resource and `test_sync_tool` spec builders
- local host tool spec builders for shell/exec/request-permissions/view-image
- collaboration and agent-job `ToolSpec` builders for spawn/send/wait/close,
`request_user_input`, and CSV fanout/reporting
-1
View File
@@ -150,7 +150,6 @@ pub use tool_spec::create_image_generation_tool;
pub use tool_spec::create_local_shell_tool;
pub use tool_spec::create_tools_json_for_responses_api;
pub use tool_spec::create_web_search_tool;
pub use utility_tool::create_list_dir_tool;
pub use utility_tool::create_test_sync_tool;
pub use view_image::ViewImageToolOptions;
pub use view_image::create_view_image_tool;
-15
View File
@@ -32,7 +32,6 @@ use crate::create_followup_task_tool;
use crate::create_get_goal_tool;
use crate::create_image_generation_tool;
use crate::create_list_agents_tool;
use crate::create_list_dir_tool;
use crate::create_list_mcp_resource_templates_tool;
use crate::create_list_mcp_resources_tool;
use crate::create_local_shell_tool;
@@ -348,20 +347,6 @@ pub fn build_tool_registry_plan(
plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch);
}
if config.environment_mode.has_environment()
&& config
.experimental_supported_tools
.iter()
.any(|tool| tool == "list_dir")
{
plan.push_spec(
create_list_dir_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
plan.register_handler("list_dir", ToolHandlerKind::ListDir);
}
if config
.experimental_supported_tools
.iter()
@@ -535,7 +535,7 @@ fn disabled_environment_omits_environment_backed_tools() {
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
@@ -546,9 +546,6 @@ fn disabled_environment_omits_environment_backed_tools() {
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_environment_mode(ToolEnvironmentMode::None);
tools_config
.experimental_supported_tools
.push("list_dir".to_string());
let (tools, _) = build_specs(
&tools_config,
/*mcp_tools*/ None,
@@ -559,7 +556,6 @@ fn disabled_environment_omits_environment_backed_tools() {
assert_lacks_tool_name(&tools, "exec_command");
assert_lacks_tool_name(&tools, "write_stdin");
assert_lacks_tool_name(&tools, "apply_patch");
assert_lacks_tool_name(&tools, "list_dir");
assert_lacks_tool_name(&tools, VIEW_IMAGE_TOOL_NAME);
}
@@ -20,7 +20,6 @@ pub enum ToolHandlerKind {
FollowupTaskV2,
Goal,
ListAgentsV2,
ListDir,
Mcp,
McpResource,
Plan,
-36
View File
@@ -3,42 +3,6 @@ use crate::ResponsesApiTool;
use crate::ToolSpec;
use std::collections::BTreeMap;
pub fn create_list_dir_tool() -> ToolSpec {
let properties = BTreeMap::from([
(
"dir_path".to_string(),
JsonSchema::string(Some("Absolute path to the directory to list.".to_string())),
),
(
"offset".to_string(),
JsonSchema::number(Some(
"The entry number to start listing from. Must be 1 or greater.".to_string(),
)),
),
(
"limit".to_string(),
JsonSchema::number(Some("The maximum number of entries to return.".to_string())),
),
(
"depth".to_string(),
JsonSchema::number(Some(
"The maximum directory depth to traverse. Must be 1 or greater.".to_string(),
)),
),
]);
ToolSpec::Function(ResponsesApiTool {
name: "list_dir".to_string(),
description:
"Lists entries in a local directory with 1-indexed entry numbers and simple type labels."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(properties, Some(vec!["dir_path".to_string()]), Some(false.into())),
output_schema: None,
})
}
pub fn create_test_sync_tool() -> ToolSpec {
let barrier_properties = BTreeMap::from([
(
-44
View File
@@ -3,50 +3,6 @@ use crate::JsonSchema;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[test]
fn list_dir_tool_matches_expected_spec() {
assert_eq!(
create_list_dir_tool(),
ToolSpec::Function(ResponsesApiTool {
name: "list_dir".to_string(),
description:
"Lists entries in a local directory with 1-indexed entry numbers and simple type labels."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(BTreeMap::from([
(
"depth".to_string(),
JsonSchema::number(Some(
"The maximum directory depth to traverse. Must be 1 or greater."
.to_string(),
)),
),
(
"dir_path".to_string(),
JsonSchema::string(Some(
"Absolute path to the directory to list.".to_string(),
)),
),
(
"limit".to_string(),
JsonSchema::number(Some(
"The maximum number of entries to return.".to_string(),
)),
),
(
"offset".to_string(),
JsonSchema::number(Some(
"The entry number to start listing from. Must be 1 or greater."
.to_string(),
)),
),
]), Some(vec!["dir_path".to_string()]), Some(false.into())),
output_schema: None,
})
);
}
#[test]
fn test_sync_tool_matches_expected_spec() {
assert_eq!(