ferrin_policy/
approval.rs1use std::fmt;
4use std::sync::Arc;
5
6use ferrin_core::generate_text::ApprovalContext;
7use ferrin_core::generate_text::ApprovalPolicy;
8use ferrin_core::generate_text::ApprovalStatus;
9use ferrin_core::generate_text::ParsedToolCall;
10use ferrin_spec::BoxFuture;
11use ferrin_spec::JsonValue;
12use serde_json::json;
13
14use crate::client::PolicyClient;
15use crate::decision::PolicyDecision;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20#[non_exhaustive]
21pub enum FailureMode {
22 #[default]
24 Deny,
25 FallThrough,
28}
29
30pub type ToInputFn = Arc<dyn Fn(&ParsedToolCall, &ApprovalContext<'_>) -> JsonValue + Send + Sync>;
32
33#[must_use]
38pub fn default_input(call: &ParsedToolCall, ctx: &ApprovalContext<'_>) -> JsonValue {
39 json!({
40 "tool": { "name": call.tool_name },
41 "args": call.input,
42 "messages": serde_json::to_value(ctx.messages).unwrap_or(JsonValue::Null),
43 "runtimeContext": ctx.runtime_context.cloned().unwrap_or(JsonValue::Null),
44 })
45}
46
47pub struct PolicyApproval<C> {
49 client: C,
50 path: String,
51 to_input: Option<ToInputFn>,
52 on_error: FailureMode,
53}
54
55pub fn policy_approval<C: PolicyClient>(client: C, path: impl Into<String>) -> PolicyApproval<C> {
63 PolicyApproval {
64 client,
65 path: path.into(),
66 to_input: None,
67 on_error: FailureMode::Deny,
68 }
69}
70
71impl<C> PolicyApproval<C> {
72 #[must_use]
74 pub fn to_input(
75 mut self,
76 f: impl Fn(&ParsedToolCall, &ApprovalContext<'_>) -> JsonValue + Send + Sync + 'static,
77 ) -> Self {
78 self.to_input = Some(Arc::new(f));
79 self
80 }
81
82 #[must_use]
84 pub fn on_error(mut self, mode: FailureMode) -> Self {
85 self.on_error = mode;
86 self
87 }
88
89 #[must_use]
91 pub fn path(&self) -> &str {
92 &self.path
93 }
94
95 #[must_use]
97 pub fn client(&self) -> &C {
98 &self.client
99 }
100}
101
102impl<C> fmt::Debug for PolicyApproval<C> {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.debug_struct("PolicyApproval")
105 .field("path", &self.path)
106 .field("custom_input", &self.to_input.is_some())
107 .field("on_error", &self.on_error)
108 .finish_non_exhaustive()
109 }
110}
111
112impl<C: PolicyClient> ApprovalPolicy for PolicyApproval<C> {
113 fn resolve<'a>(
114 &'a self,
115 call: &'a ParsedToolCall,
116 ctx: ApprovalContext<'a>,
117 ) -> BoxFuture<'a, Option<ApprovalStatus>> {
118 Box::pin(async move {
119 let input = match &self.to_input {
120 Some(to_input) => to_input(call, &ctx),
121 None => default_input(call, &ctx),
122 };
123 match self.client.evaluate(&self.path, input).await {
124 Ok(raw) => {
125 let decision = PolicyDecision::normalize(&raw);
126 tracing::debug!(
127 tool = %call.tool_name,
128 path = %self.path,
129 decision = crate::diagnostics::decision_kind(&decision),
130 "policy decision"
131 );
132 decision.into_approval()
133 }
134 Err(_error) => {
135 tracing::warn!(
136 tool = %call.tool_name,
137 path = %self.path,
138 "policy evaluation failed"
139 );
140 match self.on_error {
141 FailureMode::Deny => Some(ApprovalStatus::Denied {
142 reason: Some("policy evaluation failed".to_owned()),
143 }),
144 FailureMode::FallThrough => None,
145 }
146 }
147 }
148 })
149 }
150}
151
152pub struct WithDefault<P> {
154 inner: P,
155 default: ApprovalStatus,
156}
157
158pub fn with_default<P: ApprovalPolicy>(policy: P, default: ApprovalStatus) -> WithDefault<P> {
165 WithDefault {
166 inner: policy,
167 default,
168 }
169}
170
171impl<P> fmt::Debug for WithDefault<P> {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("WithDefault")
174 .field(
175 "default",
176 &crate::diagnostics::status_kind(Some(&self.default)),
177 )
178 .finish_non_exhaustive()
179 }
180}
181
182impl<P: ApprovalPolicy> ApprovalPolicy for WithDefault<P> {
183 fn resolve<'a>(
184 &'a self,
185 call: &'a ParsedToolCall,
186 ctx: ApprovalContext<'a>,
187 ) -> BoxFuture<'a, Option<ApprovalStatus>> {
188 Box::pin(async move {
189 match self.inner.resolve(call, ctx).await {
190 Some(ApprovalStatus::NotApplicable) | None => Some(self.default.clone()),
191 Some(status) => Some(status),
192 }
193 })
194 }
195}