use std::cell::RefCell;
use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};
use crate::error::Result;
use crate::hooks::{OperationContext, PreToolCallDecideHook};
use crate::types::{HookResult, ToolCall};
thread_local! {
static RUN_HASHES: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
}
const GUARDED: &[&str] = &[
"notify",
"record_lesson",
"set_lessons",
"send_lh",
"batch_send_lh",
"post_bounty",
"submit_feedback",
"create_subdomain",
"create_and_publish_app",
"claim_bounty",
"submit_result",
"accept_result",
"create_guild",
"invite_to_guild",
"fund_guild",
"spend_treasury",
"propose_measure",
"cast_vote",
"execute_proposal",
"release_subdomain",
];
pub(crate) fn reset_run() {
RUN_HASHES.with(|h| h.borrow_mut().clear());
}
fn call_hash(call: &ToolCall) -> u64 {
let mut hasher = DefaultHasher::new();
call.name.hash(&mut hasher);
call.args.to_string().hash(&mut hasher);
hasher.finish()
}
pub(crate) struct DuplicateActionGuard;
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl PreToolCallDecideHook for DuplicateActionGuard {
fn name(&self) -> &str {
"app::duplicate_action_guard"
}
async fn run(&self, _ctx: &OperationContext, call: &ToolCall) -> Result<HookResult> {
if !GUARDED.contains(&call.name.as_str()) {
return Ok(HookResult::allow());
}
let h = call_hash(call);
let already_ran = RUN_HASHES.with(|s| !s.borrow_mut().insert(h));
if already_ran {
return Ok(HookResult::deny(format!(
"duplicate suppressed: `{}` with these exact arguments already \
executed during this request — do NOT repeat side-effecting \
actions. If the user explicitly wants it again, vary the \
arguments; if the request is fulfilled, call `finish` now.",
call.name
)));
}
Ok(HookResult::allow())
}
}