use std::sync::Arc;
use crate::hooks::Hook;
use crate::hooks::HookAction;
use crate::hooks::context::PreToolUseContext;
pub trait ConfirmationHandler: Send + Sync {
fn confirm(&self, message: &str) -> bool;
}
pub struct ConfirmationHook {
tools: Vec<String>,
handler: Arc<dyn ConfirmationHandler>,
}
impl ConfirmationHook {
pub fn new(tools: Vec<String>, handler: Arc<dyn ConfirmationHandler>) -> Self {
Self { tools, handler }
}
}
impl Hook for ConfirmationHook {
fn name(&self) -> &'static str {
"confirmation"
}
fn on_pre_tool_use(&self, ctx: &PreToolUseContext) -> Option<HookAction> {
if self.tools.iter().any(|t| t == &ctx.tool_name) {
let msg = format!("Allow tool '{}' to execute?", ctx.tool_name);
if self.handler.confirm(&msg) {
None } else {
Some(HookAction::block("User denied permission"))
}
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct AlwaysAllow;
impl ConfirmationHandler for AlwaysAllow {
fn confirm(&self, _message: &str) -> bool {
true
}
}
struct AlwaysDeny;
impl ConfirmationHandler for AlwaysDeny {
fn confirm(&self, _message: &str) -> bool {
false
}
}
fn make_ctx(tool_name: &str) -> PreToolUseContext {
PreToolUseContext {
tool_name: tool_name.to_string(),
input: serde_json::json!({}),
session_id: uuid::Uuid::nil(),
turn_number: 0,
}
}
#[test]
fn confirmation_allows_when_confirmed() {
let hook = ConfirmationHook::new(vec!["deploy".to_string()], Arc::new(AlwaysAllow));
assert!(hook.on_pre_tool_use(&make_ctx("deploy")).is_none());
}
#[test]
fn confirmation_blocks_when_denied() {
let hook = ConfirmationHook::new(vec!["deploy".to_string()], Arc::new(AlwaysDeny));
let action = hook.on_pre_tool_use(&make_ctx("deploy")).unwrap();
assert!(action.is_block());
}
#[test]
fn confirmation_skips_unlisted_tools() {
let hook = ConfirmationHook::new(vec!["deploy".to_string()], Arc::new(AlwaysDeny));
assert!(hook.on_pre_tool_use(&make_ctx("read_file")).is_none());
}
}