Skip to main content

ferrin_policy/
lib.rs

1//! Policy-based tool approval for Ferrin.
2//!
3//! A [`PolicyClient`] evaluates a policy path with a JSON input and returns
4//! the raw decision document. [`policy_approval`] turns a client into an
5//! [`ApprovalPolicy`](ferrin_core::generate_text::ApprovalPolicy): the tool
6//! name, its input, the messages and the runtime context become the policy
7//! input, and the normalized [`PolicyDecision`] becomes the approval status.
8//! [`capability_middleware`] filters the tools offered to the model through
9//! the same client. [`shadow`] observes decisions without enforcing them and
10//! [`with_default`] gives every call a decision.
11//!
12//! Clients: [`HttpPolicyClient`] speaks the OPA REST Data API
13//! (`POST /v1/data/<path>`); [`RegoPolicyClient`] (feature `rego`) evaluates
14//! Rego policies in-process with `regorus`. [`policy_client`] adapts a
15//! closure, for tests and static rules.
16//!
17//! Design: `docs/01-architecture/18-policy-approval.md`, ADR 0020.
18//!
19//! # Attribution
20//!
21//! The decision document format, its normalization rules and the shadow and
22//! capability patterns are derived from the Vercel AI SDK (Apache-2.0,
23//! Copyright 2023 Vercel, Inc.) and reimplemented in Rust. See the `NOTICE`
24//! file in the crate root.
25//!
26//! # Examples
27//!
28//! ```
29//! use ferrin_core::generate_text::ApprovalStatus;
30//! use ferrin_policy::PolicyDecision;
31//! use ferrin_policy::policy_approval;
32//! use ferrin_policy::policy_client;
33//! use serde_json::json;
34//!
35//! // A decision document as returned by a policy server or a Rego rule.
36//! let decision = PolicyDecision::normalize(&json!({
37//!     "decision": "requires-approval",
38//!     "reason": "writes outside the workspace"
39//! }));
40//! assert_eq!(
41//!     decision.into_approval(),
42//!     Some(ApprovalStatus::user_approval().with_reason("writes outside the workspace"))
43//! );
44//!
45//! // A static in-process client; pass the policy to
46//! // `generate_text(..).tool_approval(policy)`.
47//! let client = policy_client(|_path, input| {
48//!     Ok(json!({ "decision": if input["tool"]["name"] == "delete_file" { "deny" } else { "allow" } }))
49//! });
50//! let _policy = policy_approval(client, "ferrin/tools/decision");
51//! ```
52
53mod approval;
54mod capability;
55mod client;
56mod decision;
57mod diagnostics;
58mod error;
59mod http;
60mod path;
61#[cfg(feature = "rego")]
62mod rego;
63mod shadow;
64
65pub use approval::FailureMode;
66pub use approval::PolicyApproval;
67pub use approval::ToInputFn;
68pub use approval::WithDefault;
69pub use approval::default_input;
70pub use approval::policy_approval;
71pub use approval::with_default;
72pub use capability::CapabilityInputFn;
73pub use capability::CapabilityMiddleware;
74pub use capability::capability_middleware;
75pub use capability::default_capability_input;
76pub use capability::parse_allowlist;
77pub use client::PolicyClient;
78pub use client::PolicyClientFn;
79pub use client::SharedPolicyClient;
80pub use client::policy_client;
81pub use decision::PolicyDecision;
82pub use decision::UNRECOGNIZED_DECISION;
83pub use error::PolicyError;
84pub use http::DEFAULT_MAX_RESPONSE_BYTES;
85pub use http::HttpPolicyClient;
86pub use http::HttpPolicyClientBuilder;
87#[cfg(feature = "rego")]
88pub use rego::RegoPolicyClient;
89#[cfg(feature = "rego")]
90pub use rego::RegoPolicyClientBuilder;
91pub use shadow::Enforcement;
92pub use shadow::OnDecisionFn;
93pub use shadow::OnDecisionSyncFn;
94pub use shadow::PolicyDecisionEvent;
95pub use shadow::PolicyDecisionToolCall;
96pub use shadow::Shadow;
97pub use shadow::shadow;