use std::fmt;
use std::sync::Arc;
use ferrin_core::generate_text::ApprovalContext;
use ferrin_core::generate_text::ApprovalPolicy;
use ferrin_core::generate_text::ApprovalStatus;
use ferrin_core::generate_text::ParsedToolCall;
use ferrin_spec::BoxFuture;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Enforcement {
#[default]
Observe,
Enforce,
}
pub type OnDecisionFn = Arc<dyn Fn(&ParsedToolCall, Option<&ApprovalStatus>) + Send + Sync>;
pub struct Shadow<P> {
inner: P,
enforcement: Enforcement,
on_decision: Option<OnDecisionFn>,
}
pub fn shadow<P: ApprovalPolicy>(policy: P) -> Shadow<P> {
Shadow {
inner: policy,
enforcement: Enforcement::Observe,
on_decision: None,
}
}
impl<P> Shadow<P> {
#[must_use]
pub fn enforcement(mut self, enforcement: Enforcement) -> Self {
self.enforcement = enforcement;
self
}
#[must_use]
pub fn on_decision(
mut self,
f: impl Fn(&ParsedToolCall, Option<&ApprovalStatus>) + Send + Sync + 'static,
) -> Self {
self.on_decision = Some(Arc::new(f));
self
}
}
impl<P> fmt::Debug for Shadow<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shadow")
.field("enforcement", &self.enforcement)
.field("on_decision", &self.on_decision.is_some())
.finish_non_exhaustive()
}
}
impl<P: ApprovalPolicy> ApprovalPolicy for Shadow<P> {
fn resolve<'a>(
&'a self,
call: &'a ParsedToolCall,
ctx: ApprovalContext<'a>,
) -> BoxFuture<'a, Option<ApprovalStatus>> {
Box::pin(async move {
let status = self.inner.resolve(call, ctx).await;
tracing::debug!(
tool = %call.tool_name,
status = crate::diagnostics::status_kind(status.as_ref()),
enforcement = ?self.enforcement,
"shadow policy decision"
);
if let Some(on_decision) = &self.on_decision {
on_decision(call, status.as_ref());
}
match self.enforcement {
Enforcement::Observe => None,
Enforcement::Enforce => status,
}
})
}
}