use std::cell::RefCell;
use crate::confirm::{fingerprint, nonce_from_bytes, ConfirmGate, ConfirmOutcome, NONCE_LEN};
use crate::error::Result;
use crate::hooks::{OperationContext, PreToolCallDecideHook};
use crate::types::{HookResult, ToolCall};
thread_local! {
static GATE: RefCell<ConfirmGate> = RefCell::new(ConfirmGate::new());
static LAST_USER_MSG: RefCell<String> = const { RefCell::new(String::new()) };
static AWAITING_CONFIRMATION: RefCell<bool> = const { RefCell::new(false) };
}
const CONFIRM_GATED: &[&str] = &[
"release_subdomain",
"bulk_release_subdomains",
"send_lh",
"batch_send_lh",
"spend_treasury",
"publish_app_to",
];
pub(crate) fn note_user_message(text: &str) {
LAST_USER_MSG.with(|m| *m.borrow_mut() = text.to_string());
}
pub(crate) fn take_awaiting_confirmation() -> bool {
AWAITING_CONFIRMATION.with(|f| f.replace(false))
}
fn fresh_nonce() -> String {
let mut bytes = [0u8; NONCE_LEN];
rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut bytes);
nonce_from_bytes(&bytes)
}
pub(crate) struct TypedConfirmationGuard;
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl PreToolCallDecideHook for TypedConfirmationGuard {
fn name(&self) -> &str {
"app::typed_confirmation_guard"
}
async fn run(&self, _ctx: &OperationContext, call: &ToolCall) -> Result<HookResult> {
if !CONFIRM_GATED.contains(&call.name.as_str()) {
return Ok(HookResult::allow());
}
let confirmation = call
.args
.get("confirmation")
.and_then(|v| v.as_str())
.unwrap_or("");
let fp = fingerprint(&call.name, &call.args);
let last_user = LAST_USER_MSG.with(|m| m.borrow().clone());
let outcome =
GATE.with(|g| g.borrow_mut().check(&fp, confirmation, &last_user, fresh_nonce()));
AWAITING_CONFIRMATION.with(|f| {
*f.borrow_mut() = !matches!(outcome, ConfirmOutcome::Approved);
});
match outcome {
ConfirmOutcome::Approved => Ok(HookResult::allow()),
ConfirmOutcome::Challenge { nonce } => {
crate::app::dom::set_confirm_callout(&call.name, &nonce);
Ok(HookResult::deny(format!(
"`{}` NOT executed — requires the owner's typed confirmation. The \
single-use code is shown to the owner in a confirm box; do NOT \
repeat the code yourself. Briefly explain what this call will do, \
ask the owner to type that code in chat, then STOP and wait. Once \
their next message contains it, retry this SAME call with \
`confirmation` set to the code they typed. The code is bound to \
these exact arguments and is replaced if you call again without it.",
call.name
)))
}
ConfirmOutcome::NotTypedByUser => Ok(HookResult::deny(format!(
"`{}` NOT executed — the confirmation code is correct but the OWNER has \
not typed it: it does not appear in their latest chat message, and \
echoing it yourself does not count. STOP, ask the owner to type the \
code, and retry only after a user message containing it (the same code \
stays valid).",
call.name
))),
}
}
}