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",
];
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_status(
&format!("confirm {}: type {nonce} to proceed", call.name),
false,
);
Ok(HookResult::deny(format!(
"`{}` NOT executed — this action requires a typed confirmation. \
A single-use code has been issued and shown to the owner: {nonce}. \
Explain exactly what this call will do, ask the owner to TYPE the \
code {nonce} in chat, then STOP and wait. Only after a user message \
containing the code, retry this call with the SAME arguments plus \
`confirmation: \"{nonce}\"`. 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
))),
}
}
}