use std::collections::{HashMap, HashSet};
fn tool_groups() -> HashMap<&'static str, HashSet<&'static str>> {
let mut groups = HashMap::new();
groups.insert(
"group:read",
HashSet::from([
"read_file",
"list_files",
"search",
"find_symbol",
"find_referencing_symbols",
"read_pdf",
"analyze_image",
]),
);
groups.insert(
"group:write",
HashSet::from([
"write_file",
"edit_file",
"insert_before_symbol",
"insert_after_symbol",
"replace_symbol_body",
"rename_symbol",
"notebook_edit",
"apply_patch",
]),
);
groups.insert("group:process", HashSet::from(["run_command"]));
groups.insert(
"group:web",
HashSet::from([
"fetch_url",
"web_search",
"capture_web_screenshot",
"capture_screenshot",
"browser",
"open_browser",
]),
);
groups.insert(
"group:session",
HashSet::from([
"list_sessions",
"get_session_history",
"spawn_subagent",
"get_subagent_output",
"list_subagents",
]),
);
groups.insert(
"group:memory",
HashSet::from(["memory_search", "memory_write"]),
);
groups.insert(
"group:meta",
HashSet::from([
"task_complete",
"ask_user",
"present_plan",
"write_todos",
"update_todo",
"complete_todo",
"list_todos",
"clear_todos",
"search_tools",
"invoke_skill",
]),
);
groups.insert("group:messaging", HashSet::from(["send_message"]));
groups.insert("group:automation", HashSet::from(["schedule"]));
groups.insert("group:thinking", HashSet::new());
groups.insert("group:mcp", HashSet::new());
groups
}
fn profiles() -> HashMap<&'static str, Vec<&'static str>> {
let mut p = HashMap::new();
p.insert("minimal", vec!["group:read", "group:meta"]);
p.insert(
"review",
vec!["group:read", "group:meta", "group:web", "group:session"],
);
p.insert(
"coding",
vec![
"group:read",
"group:write",
"group:process",
"group:web",
"group:meta",
"group:session",
"group:memory",
],
);
p.insert(
"full",
vec![
"group:read",
"group:write",
"group:process",
"group:web",
"group:session",
"group:memory",
"group:meta",
"group:messaging",
"group:automation",
"group:thinking",
"group:mcp",
],
);
p
}
const ALWAYS_ALLOWED: &[&str] = &["task_complete", "ask_user"];
pub struct ToolPolicy;
impl ToolPolicy {
pub fn resolve(
profile: &str,
additions: Option<&[&str]>,
exclusions: Option<&[&str]>,
) -> Result<HashSet<String>, String> {
let all_profiles = profiles();
let group_names = match all_profiles.get(profile) {
Some(g) => g,
None => {
let available: Vec<_> = all_profiles.keys().collect();
return Err(format!(
"Unknown tool profile: '{}'. Available: {:?}",
profile, available
));
}
};
let groups = tool_groups();
let mut allowed: HashSet<String> = HashSet::new();
for group_name in group_names {
if let Some(tools) = groups.get(group_name) {
for tool in tools {
allowed.insert((*tool).to_string());
}
}
}
for tool in ALWAYS_ALLOWED {
allowed.insert((*tool).to_string());
}
if let Some(adds) = additions {
for tool in adds {
allowed.insert((*tool).to_string());
}
}
if let Some(excls) = exclusions {
for tool in excls {
allowed.remove(*tool);
}
}
Ok(allowed)
}
pub fn get_profile_names() -> Vec<&'static str> {
let p = profiles();
let mut names: Vec<_> = p.keys().copied().collect();
names.sort();
names
}
pub fn get_group_names() -> Vec<&'static str> {
let g = tool_groups();
let mut names: Vec<_> = g.keys().copied().collect();
names.sort();
names
}
pub fn get_tools_in_group(group_name: &str) -> HashSet<String> {
let groups = tool_groups();
groups
.get(group_name)
.map(|tools| tools.iter().map(|t| (*t).to_string()).collect())
.unwrap_or_default()
}
pub fn get_profile_description(profile: &str) -> &'static str {
match profile {
"minimal" => "Read-only tools + meta tools (for planning/exploration)",
"review" => "Read + web + git + session tools (for code review)",
"coding" => "Full development toolset without messaging/automation",
"full" => "All available tools (default)",
_ => "Unknown profile",
}
}
}
#[cfg(test)]
#[path = "policy_tests.rs"]
mod tests;