use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
fn build_tool_http_client() -> reqwest::Client {
reqwest::Client::builder()
.no_proxy()
.build()
.expect("failed to construct tool HTTP client")
}
pub type ToolInput = HashMap<String, Value>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolParameter {
pub name: String,
#[serde(rename = "type")]
pub param_type: String,
pub description: String,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolMetadata {
pub name: String,
pub description: String,
pub parameters: Vec<ToolParameter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compliance_level: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_approval: Option<bool>,
}
#[async_trait]
pub trait Tool: Send + Sync {
fn metadata(&self) -> &ToolMetadata;
async fn execute(&self, input: ToolInput) -> Result<String>;
fn validate(&self, input: &ToolInput) -> Result<()> {
for param in &self.metadata().parameters {
if param.required && !input.contains_key(¶m.name) {
anyhow::bail!("Missing required parameter: {}", param.name);
}
}
Ok(())
}
fn to_schema(&self) -> Value {
let metadata = self.metadata();
let properties: HashMap<String, Value> = metadata
.parameters
.iter()
.map(|p| {
let mut prop = serde_json::json!({
"type": p.param_type,
"description": p.description,
});
if let Some(default) = &p.default {
prop["default"] = default.clone();
}
(p.name.clone(), prop)
})
.collect();
let required: Vec<String> = metadata
.parameters
.iter()
.filter(|p| p.required)
.map(|p| p.name.clone())
.collect();
serde_json::json!({
"name": metadata.name,
"description": metadata.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
}
})
}
}
pub struct ToolRegistry {
tools: HashMap<String, Box<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn register(&mut self, tool: Box<dyn Tool>) {
let name = tool.metadata().name.clone();
self.tools.insert(name, tool);
}
pub fn get(&self, name: &str) -> Option<&dyn Tool> {
self.tools.get(name).map(|t| t.as_ref())
}
pub fn list(&self) -> Vec<&dyn Tool> {
self.tools.values().map(|t| t.as_ref()).collect()
}
pub fn get_schemas(&self) -> Vec<Value> {
self.list().iter().map(|tool| tool.to_schema()).collect()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct CalculatorTool {
metadata: ToolMetadata,
}
impl CalculatorTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "calculator".to_string(),
description: "Perform mathematical calculations".to_string(),
parameters: vec![ToolParameter {
name: "expression".to_string(),
param_type: "string".to_string(),
description: "Mathematical expression to evaluate (e.g., \"2 + 2 * 3\")"
.to_string(),
required: true,
default: None,
}],
category: Some("utility".to_string()),
compliance_level: Some("public".to_string()),
requires_approval: Some(false),
},
}
}
}
#[async_trait]
impl Tool for CalculatorTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let expression = input
.get("expression")
.and_then(|v| v.as_str())
.context("Expression must be a string")?;
if !expression
.chars()
.all(|c| c.is_ascii_digit() || "+-*/(). ".contains(c))
{
anyhow::bail!(
"Invalid expression. Only numbers and basic operators (+, -, *, /, .) are allowed"
);
}
let result = meval::eval_str(expression).context("Calculation failed")?;
Ok(format!("{} = {}", expression, result))
}
}
impl Default for CalculatorTool {
fn default() -> Self {
Self::new()
}
}
pub struct WebSearchTool {
metadata: ToolMetadata,
client: reqwest::Client,
}
impl WebSearchTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "web_search".to_string(),
description: "Search the web for information using DuckDuckGo".to_string(),
parameters: vec![
ToolParameter {
name: "query".to_string(),
param_type: "string".to_string(),
description: "The search query".to_string(),
required: true,
default: None,
},
ToolParameter {
name: "num_results".to_string(),
param_type: "number".to_string(),
description: "Number of results to return".to_string(),
required: false,
default: Some(Value::Number(5.into())),
},
],
category: Some("web".to_string()),
compliance_level: Some("public".to_string()),
requires_approval: Some(false),
},
client: build_tool_http_client(),
}
}
}
#[async_trait]
impl Tool for WebSearchTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let query = input
.get("query")
.and_then(|v| v.as_str())
.context("Query must be a string")?;
let num_results = input
.get("num_results")
.and_then(|v| v.as_u64())
.unwrap_or(5) as usize;
let response = self
.client
.get("https://api.duckduckgo.com/")
.query(&[("q", query), ("format", "json"), ("no_html", "1")])
.send()
.await
.context("Web search request failed")?;
let data: Value = response
.json()
.await
.context("Failed to parse search results")?;
let mut results = Vec::new();
if let Some(abstract_text) = data.get("AbstractText").and_then(|v| v.as_str()) {
if !abstract_text.is_empty() {
results.push(format!("Summary: {}", abstract_text));
}
}
if let Some(topics) = data.get("RelatedTopics").and_then(|v| v.as_array()) {
for topic in topics.iter().take(num_results) {
if let (Some(text), Some(url)) = (
topic.get("Text").and_then(|v| v.as_str()),
topic.get("FirstURL").and_then(|v| v.as_str()),
) {
results.push(format!("- {}\n URL: {}", text, url));
}
}
}
if results.is_empty() {
return Ok(format!("No results found for query: {}", query));
}
Ok(format!(
"Search results for \"{}\":\n\n{}",
query,
results.join("\n\n")
))
}
}
impl Default for WebSearchTool {
fn default() -> Self {
Self::new()
}
}
pub struct HttpRequestTool {
metadata: ToolMetadata,
client: reqwest::Client,
}
impl HttpRequestTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "http_request".to_string(),
description: "Make HTTP requests to REST APIs".to_string(),
parameters: vec![
ToolParameter {
name: "url".to_string(),
param_type: "string".to_string(),
description: "The URL to request".to_string(),
required: true,
default: None,
},
ToolParameter {
name: "method".to_string(),
param_type: "string".to_string(),
description: "HTTP method (GET, POST, PUT, DELETE)".to_string(),
required: false,
default: Some(Value::String("GET".to_string())),
},
ToolParameter {
name: "headers".to_string(),
param_type: "object".to_string(),
description: "Request headers".to_string(),
required: false,
default: None,
},
ToolParameter {
name: "body".to_string(),
param_type: "object".to_string(),
description: "Request body (for POST/PUT)".to_string(),
required: false,
default: None,
},
],
category: Some("web".to_string()),
compliance_level: Some("internal".to_string()),
requires_approval: Some(true),
},
client: build_tool_http_client(),
}
}
}
#[async_trait]
impl Tool for HttpRequestTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let url = input
.get("url")
.and_then(|v| v.as_str())
.context("URL must be a string")?;
let method = input
.get("method")
.and_then(|v| v.as_str())
.unwrap_or("GET")
.to_uppercase();
let mut request = match method.as_str() {
"GET" => self.client.get(url),
"POST" => self.client.post(url),
"PUT" => self.client.put(url),
"DELETE" => self.client.delete(url),
_ => anyhow::bail!("Unsupported HTTP method: {}", method),
};
if let Some(headers) = input.get("headers").and_then(|v| v.as_object()) {
for (key, value) in headers {
if let Some(value_str) = value.as_str() {
request = request.header(key, value_str);
}
}
}
if let Some(body) = input.get("body") {
request = request.json(body);
}
let response = request.send().await.context("HTTP request failed")?;
let status = response.status();
let body = response
.text()
.await
.context("Failed to read response body")?;
Ok(format!("Status: {}\n\nBody:\n{}", status, body))
}
}
impl Default for HttpRequestTool {
fn default() -> Self {
Self::new()
}
}
pub struct FileReadTool {
metadata: ToolMetadata,
}
impl FileReadTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "file_read".to_string(),
description: "Read contents of a file".to_string(),
parameters: vec![ToolParameter {
name: "path".to_string(),
param_type: "string".to_string(),
description: "Path to the file to read".to_string(),
required: true,
default: None,
}],
category: Some("file".to_string()),
compliance_level: Some("restricted".to_string()),
requires_approval: Some(true),
},
}
}
}
#[async_trait]
impl Tool for FileReadTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let path = input
.get("path")
.and_then(|v| v.as_str())
.context("Path must be a string")?;
let content = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read file: {}", path))?;
Ok(content)
}
}
impl Default for FileReadTool {
fn default() -> Self {
Self::new()
}
}
pub struct VerificationStatusTool {
metadata: ToolMetadata,
registry_url: String,
client: reqwest::Client,
}
impl VerificationStatusTool {
pub fn new() -> Self {
let registry_url = std::env::var("SEKUIRE_REGISTRY_URL")
.unwrap_or_else(|_| "http://localhost:9300".to_string());
Self {
metadata: ToolMetadata {
name: "get_verification_status".to_string(),
description: "Check the verification and compliance status of this agent in the Sekuire registry. This tool is required for trust protocol compliance and must not be removed.".to_string(),
parameters: vec![
ToolParameter {
name: "sekuire_id".to_string(),
param_type: "string".to_string(),
description: "The Sekuire ID of the agent to check (defaults to current agent)".to_string(),
required: false,
default: None,
},
ToolParameter {
name: "include_details".to_string(),
param_type: "boolean".to_string(),
description: "Include detailed verification information".to_string(),
required: false,
default: Some(Value::Bool(false)),
}
],
category: Some("compliance".to_string()),
compliance_level: Some("system".to_string()), requires_approval: Some(false),
},
registry_url,
client: build_tool_http_client(),
}
}
fn get_agent_id(&self) -> Option<String> {
if let Ok(env_id) = std::env::var("SEKUIRE_AGENT_ID") {
return Some(env_id);
}
use std::fs;
let sekuire_path = std::path::Path::new(".sekuire/agent.json");
if sekuire_path.exists() {
if let Ok(content) = fs::read_to_string(sekuire_path) {
if let Ok(config) = serde_json::from_str::<Value>(&content) {
if let Some(id) = config.get("sekuire_id").and_then(|v| v.as_str()) {
return Some(id.to_string());
}
}
}
}
None
}
fn extract_compliance_frameworks(&self, manifest: &Value) -> Vec<String> {
let mut frameworks = Vec::new();
if let Some(manifest_obj) = manifest.as_object() {
if let Some(agent) = manifest_obj.get("agent") {
if let Some(compliance) = agent.get("compliance") {
if let Some(framework) = compliance.get("framework").and_then(|v| v.as_str()) {
frameworks.push(framework.to_string());
}
}
}
}
frameworks
}
fn is_compliant(&self, agent_data: &Value) -> bool {
let verification_status = agent_data
.get("verification_status")
.and_then(|v| v.as_str())
.unwrap_or("Unverified");
let has_verification =
verification_status == "Verified" || verification_status == "Pending";
let has_good_reputation = agent_data
.get("reputation_score")
.and_then(|v| v.as_i64())
.unwrap_or(0)
>= 0;
let has_security_score = agent_data
.get("security_score")
.and_then(|v| v.as_i64())
.unwrap_or(0)
>= 60;
has_verification && has_good_reputation && has_security_score
}
fn calculate_compliance_score(&self, agent_data: &Value) -> i64 {
let mut score = 0;
let verification_status = agent_data
.get("verification_status")
.and_then(|v| v.as_str())
.unwrap_or("Unverified");
if verification_status == "Verified" {
score += 40;
} else if verification_status == "Pending" {
score += 20;
}
let rep_score = agent_data
.get("reputation_score")
.and_then(|v| v.as_i64())
.unwrap_or(0)
/ 10;
score += rep_score.min(30);
let sec_score = agent_data
.get("security_score")
.and_then(|v| v.as_i64())
.unwrap_or(0);
score += ((sec_score as f64 / 100.0) * 20.0) as i64;
if agent_data
.get("code_review_status")
.and_then(|v| v.as_str())
== Some("approved")
{
score += 10;
}
score
}
fn format_response(&self, result: &Value) -> String {
let verification_status = result
.get("verification_status")
.and_then(|v| v.as_str())
.unwrap_or("Unverified");
let status_emoji = match verification_status {
"Verified" => "✅",
"Pending" => "⏳",
"Revoked" => "❌",
_ => "⚠️",
};
let mut lines = vec![
format!("{} Agent Verification Status\n", status_emoji),
format!(
"Sekuire ID: {}",
result
.get("sekuire_id")
.and_then(|v| v.as_str())
.unwrap_or("N/A")
),
format!("Status: {}", verification_status),
format!(
"Reputation: {}",
result
.get("reputation_score")
.and_then(|v| v.as_i64())
.unwrap_or(0)
),
];
if let Some(security_score) = result.get("security_score").and_then(|v| v.as_i64()) {
lines.push(format!("Security Score: {}/100", security_score));
}
if let Some(frameworks) = result
.get("compliance_frameworks")
.and_then(|v| v.as_array())
{
if !frameworks.is_empty() {
let framework_list: Vec<String> = frameworks
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
lines.push(format!("Compliance: {}", framework_list.join(", ")));
}
}
let is_compliant = result
.get("is_compliant")
.and_then(|v| v.as_bool())
.unwrap_or(false);
lines.push(format!(
"\nCompliant: {}",
if is_compliant { "Yes ✓" } else { "No ✗" }
));
if let Some(details) = result.get("verification_details") {
lines.push("\nDetailed Verification:".to_string());
if let Some(has_sig) = details.get("has_valid_signature").and_then(|v| v.as_bool()) {
lines.push(format!(
" • Valid Signature: {}",
if has_sig { "✓" } else { "✗" }
));
}
if let Some(repo_ver) = details.get("repository_verified").and_then(|v| v.as_bool()) {
lines.push(format!(
" • Repository Verified: {}",
if repo_ver { "✓" } else { "✗" }
));
}
if let Some(sec_checks) = details
.get("passes_security_checks")
.and_then(|v| v.as_bool())
{
lines.push(format!(
" • Security Checks: {}",
if sec_checks { "✓" } else { "✗" }
));
}
if let Some(comp_score) = details.get("compliance_score").and_then(|v| v.as_i64()) {
lines.push(format!(" • Compliance Score: {}/100", comp_score));
}
}
if !is_compliant {
lines.push("\n⚠️ Warning: This agent is not fully compliant.".to_string());
lines.push(" Run 'sekuire verify' to initiate verification process.".to_string());
}
lines.join("\n")
}
}
#[async_trait]
impl Tool for VerificationStatusTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let sekuire_id = input
.get("sekuire_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| self.get_agent_id());
let include_details = input
.get("include_details")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let Some(sekuire_id) = sekuire_id else {
return Ok(serde_json::to_string(&serde_json::json!({
"status": "error",
"message": "No Sekuire ID provided and unable to determine current agent ID",
"is_verified": false,
"is_compliant": false,
}))?);
};
let response = self
.client
.get(format!(
"{}/api/v1/agents/{}",
self.registry_url, sekuire_id
))
.header("Content-Type", "application/json")
.header("User-Agent", "Sekuire-SDK/1.0")
.send()
.await;
match response {
Ok(resp) => {
if resp.status() == 404 {
return Ok(serde_json::to_string(&serde_json::json!({
"status": "error",
"message": format!("Agent {} not found in registry. This agent may not be registered or verified.", sekuire_id),
"is_verified": false,
"is_compliant": false,
}))?);
}
let agent_data = resp
.json::<Value>()
.await
.context("Failed to parse agent data")?;
let manifest = agent_data.get("manifest").unwrap_or(&Value::Null);
let mut result = serde_json::json!({
"sekuire_id": agent_data.get("sekuire_id"),
"verification_status": agent_data.get("verification_status").and_then(|v| v.as_str()).unwrap_or("Unverified"),
"reputation_score": agent_data.get("reputation_score").and_then(|v| v.as_i64()).unwrap_or(0),
"security_score": agent_data.get("security_score"),
"code_review_status": agent_data.get("code_review_status"),
"compliance_frameworks": self.extract_compliance_frameworks(manifest),
"is_compliant": self.is_compliant(&agent_data),
"last_verified": agent_data.get("reviewed_at"),
"verified_by": agent_data.get("reviewed_by"),
});
if include_details {
result["verification_details"] = serde_json::json!({
"has_valid_signature": agent_data.get("public_key").is_some(),
"repository_verified": agent_data.get("repository_verified").and_then(|v| v.as_bool()).unwrap_or(false),
"passes_security_checks": agent_data.get("security_score").and_then(|v| v.as_i64()).unwrap_or(0) >= 70,
"compliance_score": self.calculate_compliance_score(&agent_data),
});
}
Ok(self.format_response(&result))
}
Err(e) => Ok(serde_json::to_string(&serde_json::json!({
"status": "error",
"message": format!("Failed to verify agent status: {}", e),
"is_verified": false,
"is_compliant": false,
"recommendation": "Unable to verify agent. Please ensure the agent is properly registered with 'sekuire publish'.",
}))?),
}
}
}
impl Default for VerificationStatusTool {
fn default() -> Self {
Self::new()
}
}
pub struct CreateDirectoryTool {
metadata: ToolMetadata,
}
impl CreateDirectoryTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "create_directory".to_string(),
description: "Create a new directory".to_string(),
parameters: vec![ToolParameter {
name: "path".to_string(),
param_type: "string".to_string(),
description: "Path to the directory to create".to_string(),
required: true,
default: None,
}],
category: Some("filesystem".to_string()),
compliance_level: Some("restricted".to_string()),
requires_approval: Some(true),
},
}
}
}
#[async_trait]
impl Tool for CreateDirectoryTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let path = input
.get("path")
.and_then(|v| v.as_str())
.context("Path must be a string")?;
std::fs::create_dir_all(path).context("Failed to create directory")?;
Ok(format!("Directory created: {}", path))
}
}
impl Default for CreateDirectoryTool {
fn default() -> Self {
Self::new()
}
}
pub struct ListDirectoryTool {
metadata: ToolMetadata,
}
impl ListDirectoryTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "list_directory".to_string(),
description: "List contents of a directory".to_string(),
parameters: vec![ToolParameter {
name: "path".to_string(),
param_type: "string".to_string(),
description: "Path to the directory to list".to_string(),
required: true,
default: None,
}],
category: Some("filesystem".to_string()),
compliance_level: Some("restricted".to_string()),
requires_approval: Some(false),
},
}
}
}
#[async_trait]
impl Tool for ListDirectoryTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let path = input
.get("path")
.and_then(|v| v.as_str())
.context("Path must be a string")?;
let entries = std::fs::read_dir(path)
.context("Failed to read directory")?
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect::<Vec<_>>();
Ok(serde_json::to_string_pretty(&entries)?)
}
}
impl Default for ListDirectoryTool {
fn default() -> Self {
Self::new()
}
}
pub struct WriteFileTool {
metadata: ToolMetadata,
}
impl WriteFileTool {
pub fn new() -> Self {
Self {
metadata: ToolMetadata {
name: "write_file".to_string(),
description: "Write content to a file".to_string(),
parameters: vec![
ToolParameter {
name: "path".to_string(),
param_type: "string".to_string(),
description: "Path to the file".to_string(),
required: true,
default: None,
},
ToolParameter {
name: "content".to_string(),
param_type: "string".to_string(),
description: "Content to write".to_string(),
required: true,
default: None,
},
],
category: Some("filesystem".to_string()),
compliance_level: Some("restricted".to_string()),
requires_approval: Some(true),
},
}
}
}
#[async_trait]
impl Tool for WriteFileTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute(&self, input: ToolInput) -> Result<String> {
self.validate(&input)?;
let path = input
.get("path")
.and_then(|v| v.as_str())
.context("Path must be a string")?;
let content = input
.get("content")
.and_then(|v| v.as_str())
.context("Content must be a string")?;
std::fs::write(path, content).context("Failed to write file")?;
Ok(format!("File written: {}", path))
}
}
impl Default for WriteFileTool {
fn default() -> Self {
Self::new()
}
}
pub fn create_default_tool_registry() -> ToolRegistry {
let mut registry = ToolRegistry::new();
registry.register(Box::new(CalculatorTool::new()));
registry.register(Box::new(WebSearchTool::new()));
registry.register(Box::new(HttpRequestTool::new()));
registry.register(Box::new(FileReadTool::new()));
registry.register(Box::new(WriteFileTool::new()));
registry.register(Box::new(CreateDirectoryTool::new()));
registry.register(Box::new(ListDirectoryTool::new()));
registry.register(Box::new(VerificationStatusTool::new()));
registry
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_calculator_tool() {
let tool = CalculatorTool::new();
let mut input = HashMap::new();
input.insert("expression".to_string(), Value::String("2 + 2".to_string()));
let result = tool.execute(input).await.unwrap();
assert!(result.contains("4"));
}
}