llm_kernel/provider/
policy.rs1use serde::{Deserialize, Serialize};
21
22#[non_exhaustive]
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum Sensitivity {
30 #[default]
32 Public,
33 Internal,
35 Confidential,
37 Restricted,
39}
40
41#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum PolicyAction {
46 Allow,
48 Redact,
50 Warn,
52 ReRoute {
54 provider: String,
56 },
57 Block,
59}
60
61#[non_exhaustive]
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum ImagePolicy {
66 #[default]
68 Allow,
69 Strip,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct PolicyThreshold {
76 pub min_sensitivity: Sensitivity,
78 pub action: PolicyAction,
80}
81
82#[non_exhaustive]
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
89pub struct DataPolicy {
90 #[serde(default)]
92 pub image: ImagePolicy,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub thresholds: Vec<PolicyThreshold>,
98}
99
100impl DataPolicy {
101 pub fn default_for(svc: &crate::provider::ServiceDescriptor) -> Self {
109 if svc.family == "local" {
110 return Self::default();
111 }
112 Self {
113 image: ImagePolicy::Allow,
114 thresholds: vec![
115 PolicyThreshold {
116 min_sensitivity: Sensitivity::Restricted,
117 action: PolicyAction::Block,
118 },
119 PolicyThreshold {
120 min_sensitivity: Sensitivity::Confidential,
121 action: PolicyAction::ReRoute {
122 provider: "ollama".to_string(),
123 },
124 },
125 PolicyThreshold {
126 min_sensitivity: Sensitivity::Internal,
127 action: PolicyAction::Warn,
128 },
129 ],
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn default_is_permissive() {
140 let policy = DataPolicy::default();
141 assert!(policy.thresholds.is_empty());
142 assert_eq!(policy.image, ImagePolicy::Allow);
143 }
144
145 #[test]
146 fn sensitivity_is_ordered() {
147 assert!(Sensitivity::Public < Sensitivity::Internal);
148 assert!(Sensitivity::Internal < Sensitivity::Confidential);
149 assert!(Sensitivity::Confidential < Sensitivity::Restricted);
150 }
151
152 #[test]
153 fn data_policy_serde_roundtrip_keeps_reroute_payload() {
154 let policy = DataPolicy {
155 image: ImagePolicy::Strip,
156 thresholds: vec![PolicyThreshold {
157 min_sensitivity: Sensitivity::Confidential,
158 action: PolicyAction::ReRoute {
159 provider: "ollama".to_string(),
160 },
161 }],
162 };
163 let json = serde_json::to_string(&policy).expect("serialize");
164 let back: DataPolicy = serde_json::from_str(&json).expect("deserialize");
165 assert_eq!(back, policy);
166 assert!(json.contains("\"re_route\""), "got: {json}");
167 }
168
169 #[test]
170 fn default_for_local_provider_is_permissive() {
171 let catalog = crate::provider::ProviderIndex::embedded();
172 let ollama = catalog.get("ollama").expect("ollama in catalog");
173 assert_eq!(ollama.family, "local");
174 assert!(DataPolicy::default_for(ollama).thresholds.is_empty());
175 }
176
177 #[test]
178 fn default_for_cloud_provider_has_thresholds() {
179 let catalog = crate::provider::ProviderIndex::embedded();
180 let openai = catalog.get("openai").expect("openai in catalog");
181 let policy = DataPolicy::default_for(openai);
182 assert_eq!(policy.image, ImagePolicy::Allow);
183 assert_eq!(policy.thresholds.len(), 3);
184 assert_eq!(policy.thresholds[0].action, PolicyAction::Block);
185 assert_eq!(
186 policy.thresholds[1].action,
187 PolicyAction::ReRoute {
188 provider: "ollama".to_string()
189 }
190 );
191 assert_eq!(policy.thresholds[2].action, PolicyAction::Warn);
192 }
193
194 #[test]
195 fn service_descriptor_data_policy_survives_serde_roundtrip() {
196 let catalog = crate::provider::ProviderIndex::embedded();
199 let mut svc = catalog.get("zai").expect("zai in catalog").clone();
200 svc.data_policy = Some(DataPolicy {
201 image: ImagePolicy::Strip,
202 thresholds: vec![PolicyThreshold {
203 min_sensitivity: Sensitivity::Internal,
204 action: PolicyAction::Warn,
205 }],
206 });
207 let json = serde_json::to_string_pretty(&svc).expect("serialize");
208 let back: crate::provider::ServiceDescriptor =
209 serde_json::from_str(&json).expect("deserialize");
210 assert_eq!(back.data_policy, svc.data_policy);
211 }
212}