use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use super::{ToolDispatchContext, ToolDispatchResult, ToolMiddleware, ToolPipeline};
use crate::message::ToolContent;
use crate::tool::shield::{SafetyAction, ShieldContext, ToolSafetyShield};
pub struct SafetyShieldMiddleware {
shield: Arc<dyn ToolSafetyShield>,
recent: std::sync::Mutex<Vec<(String, usize)>>,
}
impl SafetyShieldMiddleware {
#[must_use]
pub fn new(shield: Arc<dyn ToolSafetyShield>) -> Self {
Self {
shield,
recent: std::sync::Mutex::new(Vec::new()),
}
}
fn shield_ctx(&self, ctx: &ToolDispatchContext) -> ShieldContext {
ShieldContext {
tool_name: ctx.tool_name.clone(),
input: ctx.input.clone(),
turn: ctx.turn_number,
recent_calls: self
.recent
.lock()
.map(|log| log.clone())
.unwrap_or_default(),
}
}
}
impl ToolMiddleware for SafetyShieldMiddleware {
fn name(&self) -> &'static str {
"safety_shield"
}
fn dispatch<'a>(
&'a self,
ctx: &'a mut ToolDispatchContext,
next: &'a ToolPipeline,
) -> Pin<Box<dyn Future<Output = ToolDispatchResult> + Send + 'a>> {
let watched = self.shield.watched_tools();
let watched = (!watched.is_empty()).then(|| watched.contains(&ctx.tool_name));
let decision = match watched {
Some(true) => Some(self.shield.evaluate(&self.shield_ctx(ctx))),
_ => None,
};
if let Some(decision) = &decision
&& decision.action == SafetyAction::Warn
{
tracing::warn!(
tool = %ctx.tool_name,
reason = decision.reason.as_deref().unwrap_or(""),
category = decision.category.as_deref().unwrap_or(""),
"safety shield warning"
);
}
Box::pin(async move {
if let Some(decision) = decision
&& decision.action == SafetyAction::Block
{
let reason = decision.reason;
return ToolDispatchResult {
tool_call_id: ctx.call_id.clone(),
output: ToolContent::Text(format!(
"blocked by safety shield: {}",
reason.as_deref().unwrap_or("high risk")
)),
is_error: true,
resolved_tool_name: String::new(),
duration: std::time::Duration::ZERO,
display_hint: None,
};
}
let (tool_name, input) = (ctx.tool_name.clone(), ctx.input.clone());
let result = next.dispatch(ctx).await;
if watched == Some(true) {
if let Ok(mut log) = self.recent.lock() {
log.push((tool_name.clone(), ctx.turn_number));
if log.len() > 20 {
let drain_until = log.len().saturating_sub(20);
log.drain(..drain_until);
}
}
self.shield
.record_invocation(&tool_name, &input, !result.is_error);
}
result
})
}
}