use std::collections::HashMap;
use std::sync::Mutex;
use async_trait::async_trait;
use tokio::sync::{mpsc, oneshot};
use crate::core::permission::{PermissionDecision, PermissionGate, approval_key};
pub(crate) const PERMISSION_OPTIONS: [(&str, PermissionDecision); 4] = [
("Allow Once", PermissionDecision::AllowOnce),
("Allow Always", PermissionDecision::AllowAlways),
("Reject Once", PermissionDecision::RejectOnce),
("Reject Always", PermissionDecision::RejectAlways),
];
pub(crate) struct PermissionRequest {
pub(crate) tool_name: String,
pub(crate) arguments: String,
pub(crate) respond_to: oneshot::Sender<PermissionDecision>,
}
pub(crate) struct TuiPermissionGate {
pub(crate) tx: mpsc::UnboundedSender<PermissionRequest>,
approved_tools: Mutex<HashMap<String, PermissionDecision>>,
}
impl TuiPermissionGate {
pub(crate) fn new(tx: mpsc::UnboundedSender<PermissionRequest>) -> Self {
Self {
tx,
approved_tools: Mutex::new(HashMap::new()),
}
}
}
#[async_trait]
impl PermissionGate for TuiPermissionGate {
async fn check(
&self,
_tool_call_id: &str,
tool_name: &str,
arguments: &str,
) -> PermissionDecision {
let key = approval_key(tool_name, arguments);
if let Some(remembered) = self.approved_tools.lock().unwrap().get(&key).copied() {
return remembered;
}
let (respond_to, rx) = oneshot::channel();
let request = PermissionRequest {
tool_name: tool_name.to_string(),
arguments: arguments.to_string(),
respond_to,
};
if self.tx.send(request).is_err() {
return PermissionDecision::RejectOnce;
}
let decision = rx.await.unwrap_or(PermissionDecision::RejectOnce);
if matches!(
decision,
PermissionDecision::AllowAlways | PermissionDecision::RejectAlways
) {
self.approved_tools.lock().unwrap().insert(key, decision);
}
decision
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[tokio::test]
async fn check_returns_the_ui_loops_answer() {
let (tx, mut rx) = mpsc::unbounded_channel();
let gate = TuiPermissionGate::new(tx);
let check = tokio::spawn(async move { gate.check("call_1", "read_file", "{}").await });
let request = rx.recv().await.unwrap();
assert_eq!(request.tool_name, "read_file");
let _ = request.respond_to.send(PermissionDecision::AllowAlways);
assert_eq!(check.await.unwrap(), PermissionDecision::AllowAlways);
}
#[tokio::test]
async fn check_fails_closed_when_ui_loop_is_gone() {
let (tx, rx) = mpsc::unbounded_channel();
drop(rx); let gate = TuiPermissionGate::new(tx);
let decision = gate.check("call_1", "execute_command", "{}").await;
assert_eq!(decision, PermissionDecision::RejectOnce);
}
#[tokio::test]
async fn check_fails_closed_when_request_is_dropped_unanswered() {
let (tx, mut rx) = mpsc::unbounded_channel();
let gate = TuiPermissionGate::new(tx);
let check = tokio::spawn(async move { gate.check("call_1", "write_file", "{}").await });
let request = rx.recv().await.unwrap();
drop(request);
assert_eq!(check.await.unwrap(), PermissionDecision::RejectOnce);
}
#[tokio::test]
async fn allow_always_is_remembered_for_later_calls_to_the_same_tool() {
let (tx, mut rx) = mpsc::unbounded_channel();
let gate = Arc::new(TuiPermissionGate::new(tx));
let first_gate = gate.clone();
let first =
tokio::spawn(async move { first_gate.check("call_1", "read_file", "{}").await });
let request = rx.recv().await.unwrap();
let _ = request.respond_to.send(PermissionDecision::AllowAlways);
assert_eq!(first.await.unwrap(), PermissionDecision::AllowAlways);
let second = gate.check("call_2", "read_file", "{}").await;
assert_eq!(second, PermissionDecision::AllowAlways);
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn allow_once_is_not_remembered() {
let (tx, mut rx) = mpsc::unbounded_channel();
let gate = Arc::new(TuiPermissionGate::new(tx));
let first_gate = gate.clone();
let first =
tokio::spawn(async move { first_gate.check("call_1", "read_file", "{}").await });
let request = rx.recv().await.unwrap();
let _ = request.respond_to.send(PermissionDecision::AllowOnce);
assert_eq!(first.await.unwrap(), PermissionDecision::AllowOnce);
let second_gate = gate.clone();
let second =
tokio::spawn(async move { second_gate.check("call_2", "read_file", "{}").await });
let request = rx.recv().await.unwrap();
let _ = request.respond_to.send(PermissionDecision::AllowOnce);
assert_eq!(second.await.unwrap(), PermissionDecision::AllowOnce);
}
}