klieo-ops 3.3.0

Operational layer above klieo-core: supervisor, governor, gates, escalation, worklog, handoff.
Documentation
//! `GatedTool` wraps any `klieo_core::Tool` and runs stacked gates on every
//! invocation, regardless of `Tool::is_effectful()`.
//!
//! Wrapping a `Tool` with `GatedTool` is an explicit caller declaration that
//! the tool requires gating. `Tool::is_effectful` is informational (used by
//! external tooling to flag side-effect risk) but does **not** control gate
//! evaluation here — gates run unconditionally on every invocation of a
//! `GatedTool`-wrapped tool.

use super::trait_::{Gate, GateDecision, GateRequest};
use async_trait::async_trait;
use klieo_core::{Tool, ToolCtx, ToolError};
use std::sync::Arc;

/// Wraps an inner `Tool` and consults a stack of `Gate`s pre-invocation.
pub struct GatedTool {
    inner: Arc<dyn Tool>,
    gates: Vec<Arc<dyn Gate>>,
}

impl GatedTool {
    /// Build a gated wrapper.
    #[must_use]
    pub fn new(inner: Arc<dyn Tool>, gates: Vec<Arc<dyn Gate>>) -> Self {
        Self { inner, gates }
    }

    /// Test hook: run gate evaluation in isolation.
    #[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
    }
}