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;
use ferrin_spec::JsonValue;
use serde_json::json;
use crate::client::PolicyClient;
use crate::decision::PolicyDecision;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum FailureMode {
#[default]
Deny,
FallThrough,
}
pub type ToInputFn = Arc<dyn Fn(&ParsedToolCall, &ApprovalContext<'_>) -> JsonValue + Send + Sync>;
#[must_use]
pub fn default_input(call: &ParsedToolCall, ctx: &ApprovalContext<'_>) -> JsonValue {
json!({
"tool": {
"name": call.tool_name,
"tool_call_id": call.tool_call_id,
"dynamic": call.dynamic,
"provider_executed": call.provider_executed,
"invalid": call.invalid,
},
"input": call.input,
"messages": serde_json::to_value(ctx.messages).unwrap_or(JsonValue::Null),
"tools_context": ctx.tools_context.cloned().unwrap_or(JsonValue::Null),
})
}
pub struct PolicyApproval<C> {
client: C,
path: String,
to_input: Option<ToInputFn>,
on_error: FailureMode,
}
pub fn policy_approval<C: PolicyClient>(client: C, path: impl Into<String>) -> PolicyApproval<C> {
PolicyApproval {
client,
path: path.into(),
to_input: None,
on_error: FailureMode::Deny,
}
}
impl<C> PolicyApproval<C> {
#[must_use]
pub fn to_input(
mut self,
f: impl Fn(&ParsedToolCall, &ApprovalContext<'_>) -> JsonValue + Send + Sync + 'static,
) -> Self {
self.to_input = Some(Arc::new(f));
self
}
#[must_use]
pub fn on_error(mut self, mode: FailureMode) -> Self {
self.on_error = mode;
self
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn client(&self) -> &C {
&self.client
}
}
impl<C> fmt::Debug for PolicyApproval<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PolicyApproval")
.field("path", &self.path)
.field("custom_input", &self.to_input.is_some())
.field("on_error", &self.on_error)
.finish_non_exhaustive()
}
}
impl<C: PolicyClient> ApprovalPolicy for PolicyApproval<C> {
fn resolve<'a>(
&'a self,
call: &'a ParsedToolCall,
ctx: ApprovalContext<'a>,
) -> BoxFuture<'a, Option<ApprovalStatus>> {
Box::pin(async move {
let input = match &self.to_input {
Some(to_input) => to_input(call, &ctx),
None => default_input(call, &ctx),
};
match self.client.evaluate(&self.path, input).await {
Ok(raw) => {
let decision = PolicyDecision::normalize(&raw);
tracing::debug!(
tool = %call.tool_name,
path = %self.path,
decision = crate::diagnostics::decision_kind(&decision),
"policy decision"
);
decision.into_approval()
}
Err(_error) => {
tracing::warn!(
tool = %call.tool_name,
path = %self.path,
"policy evaluation failed"
);
match self.on_error {
FailureMode::Deny => Some(ApprovalStatus::Denied {
reason: Some("policy evaluation failed".to_owned()),
}),
FailureMode::FallThrough => None,
}
}
}
})
}
}
pub struct WithDefault<P> {
inner: P,
default: ApprovalStatus,
}
pub fn with_default<P: ApprovalPolicy>(policy: P, default: ApprovalStatus) -> WithDefault<P> {
WithDefault {
inner: policy,
default,
}
}
impl<P> fmt::Debug for WithDefault<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WithDefault")
.field(
"default",
&crate::diagnostics::status_kind(Some(&self.default)),
)
.finish_non_exhaustive()
}
}
impl<P: ApprovalPolicy> ApprovalPolicy for WithDefault<P> {
fn resolve<'a>(
&'a self,
call: &'a ParsedToolCall,
ctx: ApprovalContext<'a>,
) -> BoxFuture<'a, Option<ApprovalStatus>> {
Box::pin(async move {
match self.inner.resolve(call, ctx).await {
Some(status) => Some(status),
None => Some(self.default.clone()),
}
})
}
}