use super::trait_::{Gate, GateDecision, GateRequest};
use async_trait::async_trait;
use klieo_core::{Tool, ToolCtx, ToolError};
use std::sync::Arc;
pub struct GatedTool {
inner: Arc<dyn Tool>,
gates: Vec<Arc<dyn Gate>>,
}
impl GatedTool {
#[must_use]
pub fn new(inner: Arc<dyn Tool>, gates: Vec<Arc<dyn Gate>>) -> Self {
Self { inner, gates }
}
#[cfg(any(test, feature = "test-utils"))]
pub async fn evaluate_for_test(
&self,
tool_name: &str,
args: &serde_json::Value,
) -> GateDecision {
let req = GateRequest {
tool_name: tool_name.to_string(),
args: args.clone(),
};
for g in &self.gates {
match g.evaluate(req.clone()).await {
GateDecision::Allow => continue,
other => return other,
}
}
GateDecision::Allow
}
}
#[async_trait]
impl Tool for GatedTool {
fn name(&self) -> &str {
self.inner.name()
}
fn description(&self) -> &str {
self.inner.description()
}
fn json_schema(&self) -> &serde_json::Value {
self.inner.json_schema()
}
fn is_effectful(&self) -> bool {
self.inner.is_effectful()
}
fn redacts_audit(&self) -> bool {
self.inner.redacts_audit()
}
async fn invoke(
&self,
args: serde_json::Value,
ctx: ToolCtx,
) -> Result<serde_json::Value, ToolError> {
let req = GateRequest {
tool_name: self.inner.name().to_string(),
args: args.clone(),
};
for g in &self.gates {
match g.evaluate(req.clone()).await {
GateDecision::Allow => continue,
GateDecision::Deny { code, reason } => {
return Err(ToolError::Permanent(format!(
"gate `{}` denied: code={code} reason={reason}",
g.name()
)));
}
GateDecision::RequireApproval { .. } => {
return Err(ToolError::Permanent(format!(
"gate `{}` requires human approval (Phase A: rejected)",
g.name()
)));
}
}
}
self.inner.invoke(args, ctx).await
}
}