use agentic_tools_core::ToolRegistry;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use tracing::warn;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct AgenticToolsConfig {
#[serde(default)]
pub allowlist: Option<HashSet<String>>,
#[serde(default)]
pub extras: serde_json::Value,
}
pub struct AgenticTools;
const CODING_NAMES: &[&str] = &[
"cli_ls",
"ask_agent",
"cli_grep",
"cli_glob",
"cli_just_search",
"cli_just_execute",
];
const PR_COMMENTS_NAMES: &[&str] = &["gh_get_comments", "gh_add_comment_reply", "gh_get_prs"];
const LINEAR_NAMES: &[&str] = &[
"linear_search_issues",
"linear_read_issue",
"linear_create_issue",
"linear_add_comment",
"linear_archive_issue",
"linear_get_metadata",
];
const GPT5_NAMES: &[&str] = &["ask_reasoning_model"];
const THOUGHTS_NAMES: &[&str] = &[
"thoughts_write_document",
"thoughts_list_documents",
"thoughts_list_references",
"thoughts_add_reference",
"thoughts_get_template",
];
const WEB_NAMES: &[&str] = &["web_fetch", "web_search"];
impl AgenticTools {
#[allow(clippy::new_ret_no_self)]
pub fn new(config: AgenticToolsConfig) -> ToolRegistry {
let allow = normalize_allowlist(config.allowlist);
let domain_wanted = |names: &[&str]| match &allow {
None => true,
Some(set) => names.iter().any(|n| set.contains(&n.to_lowercase())),
};
let mut regs = Vec::new();
if domain_wanted(CODING_NAMES) {
let svc = Arc::new(coding_agent_tools::CodingAgentTools::new());
regs.push(coding_agent_tools::build_registry(svc));
}
if domain_wanted(PR_COMMENTS_NAMES) {
let tool = match pr_comments::PrComments::new() {
Ok(t) => t,
Err(e) => {
warn!(
"pr_comments: ambient repo detection failed ({}); tools will return a clear error until repo context is available",
e
);
pr_comments::PrComments::disabled(format!("{:#}", e))
}
};
regs.push(pr_comments::build_registry(Arc::new(tool)));
}
if domain_wanted(LINEAR_NAMES) {
let linear = Arc::new(linear_tools::LinearTools::new());
regs.push(linear_tools::build_registry(linear));
}
if domain_wanted(GPT5_NAMES) {
regs.push(gpt5_reasoner::build_registry());
}
if domain_wanted(THOUGHTS_NAMES) {
regs.push(thoughts_mcp_tools::build_registry());
}
if domain_wanted(WEB_NAMES) {
let web = Arc::new(web_retrieval::WebTools::new());
regs.push(web_retrieval::build_registry(web));
}
let merged = ToolRegistry::merge_all(regs);
if let Some(set) = allow {
let names: Vec<&str> = set.iter().map(|s| s.as_str()).collect();
for name in &names {
if !merged.contains(name) {
warn!("Unknown tool in allowlist: {}", name);
}
}
merged.subset(names)
} else {
merged
}
}
pub fn total_tool_count() -> usize {
CODING_NAMES.len()
+ PR_COMMENTS_NAMES.len()
+ LINEAR_NAMES.len()
+ GPT5_NAMES.len()
+ THOUGHTS_NAMES.len()
+ WEB_NAMES.len()
}
}
fn normalize_allowlist(allowlist: Option<HashSet<String>>) -> Option<HashSet<String>> {
allowlist.and_then(|s| {
let normalized: HashSet<String> = s
.into_iter()
.map(|n| n.trim().to_lowercase())
.filter(|n| !n.is_empty())
.collect();
if normalized.is_empty() {
None } else {
Some(normalized)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn total_tool_count_is_23() {
assert_eq!(AgenticTools::total_tool_count(), 23);
}
#[test]
fn normalize_allowlist_lowercases() {
let mut set = HashSet::new();
set.insert("CLI_LS".to_string());
set.insert("Ask_Reasoning_Model".to_string());
let normalized = normalize_allowlist(Some(set)).unwrap();
assert!(normalized.contains("cli_ls"));
assert!(normalized.contains("ask_reasoning_model"));
assert!(!normalized.contains("CLI_LS"));
}
#[test]
fn normalize_allowlist_filters_empty() {
let mut set = HashSet::new();
set.insert("".to_string());
set.insert(" ".to_string());
set.insert("cli_ls".to_string());
let normalized = normalize_allowlist(Some(set)).unwrap();
assert_eq!(normalized.len(), 1);
assert!(normalized.contains("cli_ls"));
}
#[test]
fn normalize_allowlist_none_returns_none() {
assert!(normalize_allowlist(None).is_none());
}
#[test]
fn allowlist_none_builds_all_tools() {
let reg = AgenticTools::new(AgenticToolsConfig::default());
let names = reg.list_names();
assert!(
names.len() >= 23,
"expected at least 23 tools, got {}",
names.len()
);
assert!(
reg.contains("cli_ls"),
"missing cli_ls from coding_agent_tools"
);
assert!(
reg.contains("gh_get_comments"),
"missing gh_get_comments from pr_comments"
);
assert!(
reg.contains("linear_search_issues"),
"missing linear_search_issues from linear_tools"
);
assert!(
reg.contains("ask_reasoning_model"),
"missing ask_reasoning_model from gpt5_reasoner"
);
assert!(
reg.contains("thoughts_add_reference"),
"missing thoughts_add_reference from thoughts_mcp_tools"
);
assert!(
reg.contains("web_fetch"),
"missing web_fetch from web_retrieval"
);
assert!(
reg.contains("web_search"),
"missing web_search from web_retrieval"
);
}
#[test]
fn allowlist_filters_to_specific_tools() {
let mut set = HashSet::new();
set.insert("cli_ls".to_string());
set.insert("ask_reasoning_model".to_string());
let config = AgenticToolsConfig {
allowlist: Some(set),
extras: serde_json::json!({}),
};
let reg = AgenticTools::new(config);
let names = reg.list_names();
assert_eq!(names.len(), 2);
assert!(reg.contains("cli_ls"));
assert!(reg.contains("ask_reasoning_model"));
assert!(!reg.contains("cli_grep"));
}
#[test]
fn allowlist_is_case_insensitive() {
let mut set = HashSet::new();
set.insert("CLI_LS".to_string());
set.insert("ASK_REASONING_MODEL".to_string());
let config = AgenticToolsConfig {
allowlist: Some(set),
extras: serde_json::json!({}),
};
let reg = AgenticTools::new(config);
assert!(reg.contains("cli_ls"));
assert!(reg.contains("ask_reasoning_model"));
}
#[test]
fn empty_allowlist_enables_all_tools() {
let config = AgenticToolsConfig {
allowlist: Some(HashSet::new()),
extras: serde_json::json!({}),
};
let reg = AgenticTools::new(config);
assert!(reg.len() >= 23);
}
#[test]
fn allowlist_web_search_only() {
let mut set = HashSet::new();
set.insert("web_search".to_string());
let config = AgenticToolsConfig {
allowlist: Some(set),
extras: serde_json::json!({}),
};
let reg = AgenticTools::new(config);
assert_eq!(reg.len(), 1);
assert!(reg.contains("web_search"));
assert!(!reg.contains("web_fetch"));
}
#[test]
fn unknown_allowlist_names_are_ignored() {
let mut set = HashSet::new();
set.insert("cli_ls".to_string());
set.insert("nonexistent_tool".to_string());
let config = AgenticToolsConfig {
allowlist: Some(set),
extras: serde_json::json!({}),
};
let reg = AgenticTools::new(config);
assert_eq!(reg.len(), 1);
assert!(reg.contains("cli_ls"));
}
}