use async_trait::async_trait;
use serde_json::Value;
use thiserror::Error;
pub mod bash;
pub mod consult;
pub mod grep;
pub mod knowledge;
pub mod ls;
pub mod read;
pub mod write;
#[derive(Debug, Error)]
pub enum ToolError {
#[error("Execution failed: {0}")]
ExecutionError(String),
#[error("Invalid arguments: {0}")]
InvalidArguments(String),
}
pub type ToolResult<T> = Result<T, ToolError>;
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> Value;
async fn execute(&self, args: Value) -> ToolResult<Value>;
fn requires_approval(&self) -> bool {
true
}
fn approval_notice(&self) -> Option<String> {
None
}
}
#[allow(dead_code)]
pub struct MockTool {
name: String,
description: String,
}
impl MockTool {
#[allow(dead_code)]
pub fn new(name: &str, description: &str) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
}
}
}
#[async_trait]
impl Tool for MockTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {}
})
}
async fn execute(&self, _args: Value) -> ToolResult<Value> {
Ok(serde_json::json!({"status": "success"}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_tool_metadata() {
let tool = MockTool::new("test_tool", "A test tool");
assert_eq!(tool.name(), "test_tool");
assert_eq!(tool.description(), "A test tool");
}
#[tokio::test]
async fn test_tool_execution() {
let tool = MockTool::new("test_tool", "A test tool");
let result = tool.execute(serde_json::json!({"input": "test"})).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!({"status": "success"}));
}
#[tokio::test]
async fn test_error_variants() {
let err = ToolError::ExecutionError("failed".to_string());
assert_eq!(err.to_string(), "Execution failed: failed");
let err = ToolError::InvalidArguments("bad args".to_string());
assert_eq!(err.to_string(), "Invalid arguments: bad args");
}
#[tokio::test]
async fn test_approval_policy_default_requires_approval() {
let tool = MockTool::new("test_dangerous", "a tool that executes commands");
assert!(
tool.requires_approval(),
"default requires_approval must be true (safe-by-default)"
);
}
struct AutoApproveMock;
#[async_trait]
impl Tool for AutoApproveMock {
fn name(&self) -> &str {
"auto_safe"
}
fn description(&self) -> &str {
"auto-approved safe mock"
}
fn input_schema(&self) -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
async fn execute(&self, _args: Value) -> ToolResult<Value> {
Ok(serde_json::json!({"status": "auto"}))
}
fn requires_approval(&self) -> bool {
false
}
}
#[tokio::test]
async fn test_approval_policy_override_to_false() {
let tool = AutoApproveMock;
assert!(
!tool.requires_approval(),
"explicit override to false must be respected"
);
}
#[tokio::test]
async fn test_approval_notice_default_is_none() {
let tool = MockTool::new("test_tool", "A test tool");
assert!(
tool.approval_notice().is_none(),
"default approval_notice must be None (most tools are silent when auto-approved)"
);
}
#[tokio::test]
async fn test_approval_notice_override_to_some() {
struct NoticeTool;
#[async_trait]
impl Tool for NoticeTool {
fn name(&self) -> &str {
"notice_tool"
}
fn description(&self) -> &str {
"tool that announces itself"
}
fn input_schema(&self) -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
async fn execute(&self, _args: Value) -> ToolResult<Value> {
Ok(serde_json::json!({"done": true}))
}
fn requires_approval(&self) -> bool {
false
}
fn approval_notice(&self) -> Option<String> {
Some("notice: auto-launch in progress".into())
}
}
let tool = NoticeTool;
assert_eq!(
tool.approval_notice(),
Some("notice: auto-launch in progress".into()),
"approval_notice override must return Some with the expected message"
);
}
}