use std::sync::Arc;
use async_trait::async_trait;
use crate::tools::ToolResult;
#[derive(Debug)]
pub enum HookDecision {
Allow,
Block(String), }
#[async_trait]
pub trait ToolHook: Send + Sync {
async fn before_tool_use(&self, tool_name: &str, arguments: &str) -> HookDecision;
async fn after_tool_result(&self, _tool_name: &str, _arguments: &str, _result: &ToolResult) {}
}
pub struct PermissionHook {
deny: Vec<String>,
allow: Vec<String>,
}
impl PermissionHook {
pub fn new(deny: Vec<String>, allow: Vec<String>) -> Self {
Self { deny, allow }
}
fn matches(pattern: &str, tool_name: &str) -> bool {
if let Some(prefix) = pattern.strip_suffix('*') {
tool_name.starts_with(prefix)
} else {
pattern == tool_name
}
}
}
#[async_trait]
impl ToolHook for PermissionHook {
async fn before_tool_use(&self, tool_name: &str, _arguments: &str) -> HookDecision {
for pattern in &self.deny {
if Self::matches(pattern, tool_name) {
return HookDecision::Block(format!(
"Tool '{}' is blocked by permission rules (deny list).",
tool_name
));
}
}
if !self.allow.is_empty() {
let allowed = self.allow.iter().any(|p| Self::matches(p, tool_name));
if !allowed {
return HookDecision::Block(format!(
"Tool '{}' is not in the allow list.",
tool_name
));
}
}
HookDecision::Allow
}
}
pub struct LoggingHook;
#[async_trait]
impl ToolHook for LoggingHook {
async fn before_tool_use(&self, tool_name: &str, arguments: &str) -> HookDecision {
tracing::debug!("→ tool:{} args_len:{}", tool_name, arguments.len());
HookDecision::Allow
}
async fn after_tool_result(&self, tool_name: &str, _arguments: &str, result: &ToolResult) {
tracing::debug!(
"← tool:{} is_error:{} len:{}",
tool_name,
result.is_error,
result.output.len()
);
}
}
pub async fn run_pre_hooks(
hooks: &[Arc<dyn ToolHook>],
tool_name: &str,
arguments: &str,
) -> HookDecision {
for hook in hooks {
match hook.before_tool_use(tool_name, arguments).await {
HookDecision::Block(reason) => return HookDecision::Block(reason),
HookDecision::Allow => {}
}
}
HookDecision::Allow
}
pub async fn run_post_hooks(
hooks: &[Arc<dyn ToolHook>],
tool_name: &str,
arguments: &str,
result: &ToolResult,
) {
for hook in hooks {
hook.after_tool_result(tool_name, arguments, result).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn permission_hook_deny() {
let hook = PermissionHook::new(vec!["exec".to_string()], vec![]);
assert!(matches!(
hook.before_tool_use("exec", "{}").await,
HookDecision::Block(_)
));
assert!(matches!(
hook.before_tool_use("web_search", "{}").await,
HookDecision::Allow
));
}
#[tokio::test]
async fn permission_hook_allow_list() {
let hook = PermissionHook::new(
vec![],
vec!["web_search".to_string(), "file_ops".to_string()],
);
assert!(matches!(
hook.before_tool_use("web_search", "{}").await,
HookDecision::Allow
));
assert!(matches!(
hook.before_tool_use("exec", "{}").await,
HookDecision::Block(_)
));
}
#[tokio::test]
async fn permission_hook_wildcard() {
let hook = PermissionHook::new(vec!["web_*".to_string()], vec![]);
assert!(matches!(
hook.before_tool_use("web_search", "{}").await,
HookDecision::Block(_)
));
assert!(matches!(
hook.before_tool_use("web_fetch", "{}").await,
HookDecision::Block(_)
));
assert!(matches!(
hook.before_tool_use("exec", "{}").await,
HookDecision::Allow
));
}
#[tokio::test]
async fn run_pre_hooks_first_block_wins() {
let hooks: Vec<Arc<dyn ToolHook>> = vec![
Arc::new(PermissionHook::new(vec!["exec".to_string()], vec![])),
Arc::new(PermissionHook::new(vec!["web_search".to_string()], vec![])),
];
assert!(matches!(
run_pre_hooks(&hooks, "exec", "{}").await,
HookDecision::Block(_)
));
assert!(matches!(
run_pre_hooks(&hooks, "web_search", "{}").await,
HookDecision::Block(_)
));
assert!(matches!(
run_pre_hooks(&hooks, "file_ops", "{}").await,
HookDecision::Allow
));
}
}