use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use scv_core::{AgentError, ApprovalGate, ApprovalRequest, ToolRisk};
use scv_protocol::ServerEvent;
use tokio::sync::{Mutex, oneshot};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::{
config::ApprovalPolicy,
outbound::{OutboundSender, send_turn_event},
session::{TurnMeta, next_seq},
};
#[derive(Default)]
pub(crate) struct ApprovalBroker {
pub(crate) pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
}
impl ApprovalBroker {
pub(crate) async fn insert(&self, id: String, sender: oneshot::Sender<bool>) {
self.pending.lock().await.insert(id, sender);
}
pub(crate) async fn remove(&self, id: &str) {
self.pending.lock().await.remove(id);
}
pub(crate) async fn resolve(&self, id: &str, approved: bool) -> bool {
let sender = self.pending.lock().await.remove(id);
sender.is_some_and(|sender| sender.send(approved).is_ok())
}
}
pub(crate) fn policy_decision(policy: ApprovalPolicy, risk: ToolRisk) -> Option<bool> {
match policy {
ApprovalPolicy::OnRisk if risk == ToolRisk::ReadOnly => Some(true),
ApprovalPolicy::Never => Some(risk == ToolRisk::ReadOnly),
ApprovalPolicy::Always | ApprovalPolicy::OnRisk => None,
}
}
pub(crate) struct ProtocolApprovalGate {
pub(crate) policy: ApprovalPolicy,
pub(crate) broker: Arc<ApprovalBroker>,
pub(crate) meta: TurnMeta,
pub(crate) output: OutboundSender,
}
pub(crate) struct UnattendedGate {
pub(crate) policy: ApprovalPolicy,
pub(crate) client_approves_all: bool,
}
#[async_trait]
impl ApprovalGate for UnattendedGate {
async fn approve(
&self,
request: ApprovalRequest,
_cancellation: CancellationToken,
) -> Result<bool, AgentError> {
Ok(policy_decision(self.policy, request.risk).unwrap_or(self.client_approves_all))
}
}
#[async_trait]
impl ApprovalGate for ProtocolApprovalGate {
async fn approve(
&self,
request: ApprovalRequest,
cancellation: CancellationToken,
) -> Result<bool, AgentError> {
if let Some(decision) = policy_decision(self.policy, request.risk) {
return Ok(decision);
}
let approval_id = Uuid::new_v4().to_string();
let (sender, receiver) = oneshot::channel();
self.broker.insert(approval_id.clone(), sender).await;
let event = ServerEvent::ApprovalRequested {
request_id: self.meta.request_id.clone(),
session_id: self.meta.session_id.clone(),
turn_id: self.meta.turn_id.clone(),
seq: next_seq(&self.meta.seq),
approval_id: approval_id.clone(),
call_id: request.call_id,
name: request.name,
risk: request.risk.as_str().into(),
cwd: request.cwd.display().to_string(),
summary: request.summary,
};
if let Err(error) = send_turn_event(
&self.output,
event,
self.meta.max_server_frame,
&cancellation,
)
.await
{
self.broker.remove(&approval_id).await;
return Err(error);
}
tokio::select! {
result = receiver => result.map_err(|_| AgentError::Cancelled),
() = cancellation.cancelled() => {
self.broker.remove(&approval_id).await;
Err(AgentError::Cancelled)
}
}
}
}
#[cfg(test)]
mod tests;