use runtime::{ edit_file, execute_bash, glob_search, grep_search, read_file, write_file, BashCommandInput, GrepSearchInput, }; use serde::Deserialize; use serde_json::{json, Value}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ToolManifestEntry { pub name: String, pub source: ToolSource, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolSource { Base, Conditional, } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ToolRegistry { entries: Vec, } impl ToolRegistry { #[must_use] pub fn new(entries: Vec) -> Self { Self { entries } } #[must_use] pub fn entries(&self) -> &[ToolManifestEntry] { &self.entries } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ToolSpec { pub name: &'static str, pub description: &'static str, pub input_schema: Value, } #[must_use] pub fn mvp_tool_specs() -> Vec { vec![ ToolSpec { name: "bash", description: "Execute a shell command in the current workspace.", input_schema: json!({ "type": "object", "properties": { "command": { "type": "string" }, "timeout": { "type": "integer", "minimum": 1 }, "description": { "type": "string" }, "run_in_background": { "type": "boolean" }, "dangerouslyDisableSandbox": { "type": "boolean" } }, "required": ["command"], "additionalProperties": false }), }, ToolSpec { name: "read_file", description: "Read a text file from the workspace.", input_schema: json!({ "type": "object", "properties": { "path": { "type": "string" }, "offset": { "type": "integer", "minimum": 0 }, "limit": { "type": "integer", "minimum": 1 } }, "required": ["path"], "additionalProperties": false }), }, ToolSpec { name: "write_file", description: "Write a text file in the workspace.", input_schema: json!({ "type": "object", "properties": { "path": { "type": "string" }, "content": { "type": "string" } }, "required": ["path", "content"], "additionalProperties": false }), }, ToolSpec { name: "edit_file", description: "Replace text in a workspace file.", input_schema: json!({ "type": "object", "properties": { "path": { "type": "string" }, "old_string": { "type": "string" }, "new_string": { "type": "string" }, "replace_all": { "type": "boolean" } }, "required": ["path", "old_string", "new_string"], "additionalProperties": false }), }, ToolSpec { name: "glob_search", description: "Find files by glob pattern.", input_schema: json!({ "type": "object", "properties": { "pattern": { "type": "string" }, "path": { "type": "string" } }, "required": ["pattern"], "additionalProperties": false }), }, ToolSpec { name: "grep_search", description: "Search file contents with a regex pattern.", input_schema: json!({ "type": "object", "properties": { "pattern": { "type": "string" }, "path": { "type": "string" }, "glob": { "type": "string" }, "output_mode": { "type": "string" }, "-B": { "type": "integer", "minimum": 0 }, "-A": { "type": "integer", "minimum": 0 }, "-C": { "type": "integer", "minimum": 0 }, "context": { "type": "integer", "minimum": 0 }, "-n": { "type": "boolean" }, "-i": { "type": "boolean" }, "type": { "type": "string" }, "head_limit": { "type": "integer", "minimum": 1 }, "offset": { "type": "integer", "minimum": 0 }, "multiline": { "type": "boolean" } }, "required": ["pattern"], "additionalProperties": false }), }, ] } pub fn execute_tool(name: &str, input: &Value) -> Result { match name { "bash" => from_value::(input).and_then(run_bash), "read_file" => from_value::(input).and_then(|input| run_read_file(&input)), "write_file" => { from_value::(input).and_then(|input| run_write_file(&input)) } "edit_file" => from_value::(input).and_then(|input| run_edit_file(&input)), "glob_search" => { from_value::(input).and_then(|input| run_glob_search(&input)) } "grep_search" => { from_value::(input).and_then(|input| run_grep_search(&input)) } _ => Err(format!("unsupported tool: {name}")), } } fn from_value Deserialize<'de>>(input: &Value) -> Result { serde_json::from_value(input.clone()).map_err(|error| error.to_string()) } fn run_bash(input: BashCommandInput) -> Result { serde_json::to_string_pretty(&execute_bash(input).map_err(|error| error.to_string())?) .map_err(|error| error.to_string()) } fn run_read_file(input: &ReadFileInput) -> Result { to_pretty_json( read_file(&input.path, input.offset, input.limit).map_err(|error| error.to_string())?, ) } fn run_write_file(input: &WriteFileInput) -> Result { to_pretty_json(write_file(&input.path, &input.content).map_err(|error| error.to_string())?) } fn run_edit_file(input: &EditFileInput) -> Result { to_pretty_json( edit_file( &input.path, &input.old_string, &input.new_string, input.replace_all.unwrap_or(false), ) .map_err(|error| error.to_string())?, ) } fn run_glob_search(input: &GlobSearchInputValue) -> Result { to_pretty_json( glob_search(&input.pattern, input.path.as_deref()).map_err(|error| error.to_string())?, ) } fn run_grep_search(input: &GrepSearchInput) -> Result { to_pretty_json(grep_search(input).map_err(|error| error.to_string())?) } fn to_pretty_json(value: T) -> Result { serde_json::to_string_pretty(&value).map_err(|error| error.to_string()) } #[derive(Debug, Deserialize)] struct ReadFileInput { path: String, offset: Option, limit: Option, } #[derive(Debug, Deserialize)] struct WriteFileInput { path: String, content: String, } #[derive(Debug, Deserialize)] struct EditFileInput { path: String, old_string: String, new_string: String, replace_all: Option, } #[derive(Debug, Deserialize)] struct GlobSearchInputValue { pattern: String, path: Option, } #[cfg(test)] mod tests { use super::{execute_tool, mvp_tool_specs}; use serde_json::json; #[test] fn exposes_mvp_tools() { let names = mvp_tool_specs() .into_iter() .map(|spec| spec.name) .collect::>(); assert!(names.contains(&"bash")); assert!(names.contains(&"read_file")); } #[test] fn rejects_unknown_tool_names() { let error = execute_tool("nope", &json!({})).expect_err("tool should be rejected"); assert!(error.contains("unsupported tool")); } }