use async_trait::async_trait;
use mecha_core::tool::{Approver, Decision, Tool};
use serde_json::Value;
use std::collections::HashSet;
use std::sync::Mutex;
use tokio::sync::{mpsc, oneshot};
pub struct Request {
pub tool: String,
pub summary: String,
pub reply: oneshot::Sender<Answer>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Answer {
Allow,
Always,
Deny,
}
pub struct TuiApprover {
tx: mpsc::UnboundedSender<Request>,
always: Mutex<HashSet<String>>,
}
impl TuiApprover {
pub fn new() -> (Self, mpsc::UnboundedReceiver<Request>) {
let (tx, rx) = mpsc::unbounded_channel();
(
TuiApprover {
tx,
always: Mutex::new(HashSet::new()),
},
rx,
)
}
}
#[async_trait]
impl Approver for TuiApprover {
async fn approve(&self, tool: &dyn Tool, input: &Value) -> Decision {
if self.always.lock().is_ok_and(|a| a.contains(tool.name())) {
return Decision::Allow;
}
let (reply, answer) = oneshot::channel();
let request = Request {
tool: tool.name().to_string(),
summary: crate::approve::summarize(tool.name(), input),
reply,
};
if self.tx.send(request).is_err() {
return Decision::Deny("the interface closed before this was approved".into());
}
match answer.await {
Ok(Answer::Allow) => Decision::Allow,
Ok(Answer::Always) => {
if let Ok(mut always) = self.always.lock() {
always.insert(tool.name().to_string());
}
Decision::Allow
}
Ok(Answer::Deny) => Decision::Deny("the user declined this call".into()),
Err(_) => Decision::Deny("the request was dismissed without an answer".into()),
}
}
}