use crate::llm::ToolDefinition;
use crate::skills::{LoadedSkill, SkillTrust};
const READ_ONLY_TOOLS: &[&str] = &[
"memory_search",
"memory_read",
"memory_tree",
"time",
"echo",
"json",
"skill_list",
"skill_search",
];
#[derive(Debug, Clone)]
pub struct AttenuationResult {
pub tools: Vec<ToolDefinition>,
pub min_trust: SkillTrust,
pub explanation: String,
pub removed_tools: Vec<String>,
}
pub fn attenuate_tools(
tools: &[ToolDefinition],
active_skills: &[LoadedSkill],
) -> AttenuationResult {
if active_skills.is_empty() {
return AttenuationResult {
tools: tools.to_vec(),
min_trust: SkillTrust::Trusted,
explanation: "No skills active, all tools available".to_string(),
removed_tools: vec![],
};
}
let min_trust = active_skills
.iter()
.map(|s| s.trust)
.min()
.unwrap_or(SkillTrust::Trusted);
match min_trust {
SkillTrust::Trusted => {
AttenuationResult {
tools: tools.to_vec(),
min_trust,
explanation: "All active skills are trusted (full trust), all tools available"
.to_string(),
removed_tools: vec![],
}
}
SkillTrust::Installed => {
let mut kept = Vec::new();
let mut removed = Vec::new();
for tool in tools {
if READ_ONLY_TOOLS.contains(&tool.name.as_str()) {
kept.push(tool.clone());
} else {
removed.push(tool.name.clone());
}
}
let explanation = format!(
"Installed skill present: restricted to read-only tools, removed {} tool(s): {}",
removed.len(),
removed.join(", ")
);
AttenuationResult {
tools: kept,
min_trust,
explanation,
removed_tools: removed,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills::{ActivationCriteria, SkillManifest, SkillSource};
use std::path::PathBuf;
fn make_tool(name: &str) -> ToolDefinition {
ToolDefinition {
name: name.to_string(),
description: format!("{} tool", name),
parameters: serde_json::json!({}),
}
}
fn make_skill_with_trust(name: &str, trust: SkillTrust) -> LoadedSkill {
LoadedSkill {
manifest: SkillManifest {
name: name.to_string(),
version: "1.0.0".to_string(),
description: String::new(),
activation: ActivationCriteria::default(),
metadata: None,
},
prompt_content: "test".to_string(),
trust,
source: SkillSource::User(PathBuf::from("/tmp")),
content_hash: "sha256:000".to_string(),
compiled_patterns: vec![],
lowercased_keywords: vec![],
lowercased_exclude_keywords: vec![],
lowercased_tags: vec![],
}
}
fn all_tools() -> Vec<ToolDefinition> {
vec![
make_tool("shell"),
make_tool("http"),
make_tool("memory_write"),
make_tool("memory_search"),
make_tool("memory_read"),
make_tool("memory_tree"),
make_tool("time"),
make_tool("echo"),
make_tool("json"),
]
}
#[test]
fn test_no_skills_returns_all_tools() {
let tools = all_tools();
let result = attenuate_tools(&tools, &[]);
assert_eq!(result.tools.len(), tools.len());
assert!(result.removed_tools.is_empty());
}
#[test]
fn test_trusted_skills_no_filtering() {
let tools = all_tools();
let skills = vec![make_skill_with_trust("trusted_skill", SkillTrust::Trusted)];
let result = attenuate_tools(&tools, &skills);
assert_eq!(result.tools.len(), tools.len());
assert!(result.removed_tools.is_empty());
assert_eq!(result.min_trust, SkillTrust::Trusted);
}
#[test]
fn test_installed_only_read_only() {
let tools = all_tools();
let skills = vec![make_skill_with_trust(
"installed_skill",
SkillTrust::Installed,
)];
let result = attenuate_tools(&tools, &skills);
let kept_names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
assert!(!kept_names.contains(&"shell"));
assert!(!kept_names.contains(&"http"));
assert!(!kept_names.contains(&"memory_write"));
assert!(kept_names.contains(&"memory_search"));
assert!(kept_names.contains(&"memory_read"));
assert!(kept_names.contains(&"time"));
assert_eq!(result.min_trust, SkillTrust::Installed);
}
#[test]
fn test_mixed_trust_drops_to_lowest() {
let tools = all_tools();
let skills = vec![
make_skill_with_trust("trusted_skill", SkillTrust::Trusted),
make_skill_with_trust("installed_skill", SkillTrust::Installed),
];
let result = attenuate_tools(&tools, &skills);
assert_eq!(result.min_trust, SkillTrust::Installed);
let kept_names: Vec<&str> = result.tools.iter().map(|t| t.name.as_str()).collect();
assert!(!kept_names.contains(&"shell"));
}
#[test]
fn test_attenuation_result_has_explanation() {
let tools = vec![make_tool("shell"), make_tool("time")];
let skills = vec![make_skill_with_trust("installed", SkillTrust::Installed)];
let result = attenuate_tools(&tools, &skills);
assert!(!result.explanation.is_empty());
assert!(result.removed_tools.contains(&"shell".to_string()));
assert!(!result.removed_tools.contains(&"time".to_string()));
}
}