use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Sensitivity {
#[default]
Public,
Internal,
Confidential,
Restricted,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyAction {
Allow,
Redact,
Warn,
ReRoute {
provider: String,
},
Block,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImagePolicy {
#[default]
Allow,
Strip,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PolicyThreshold {
pub min_sensitivity: Sensitivity,
pub action: PolicyAction,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct DataPolicy {
#[serde(default)]
pub image: ImagePolicy,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub thresholds: Vec<PolicyThreshold>,
}
impl DataPolicy {
pub fn default_for(svc: &crate::provider::ServiceDescriptor) -> Self {
if svc.family == "local" {
return Self::default();
}
Self {
image: ImagePolicy::Allow,
thresholds: vec![
PolicyThreshold {
min_sensitivity: Sensitivity::Restricted,
action: PolicyAction::Block,
},
PolicyThreshold {
min_sensitivity: Sensitivity::Confidential,
action: PolicyAction::ReRoute {
provider: "ollama".to_string(),
},
},
PolicyThreshold {
min_sensitivity: Sensitivity::Internal,
action: PolicyAction::Warn,
},
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_permissive() {
let policy = DataPolicy::default();
assert!(policy.thresholds.is_empty());
assert_eq!(policy.image, ImagePolicy::Allow);
}
#[test]
fn sensitivity_is_ordered() {
assert!(Sensitivity::Public < Sensitivity::Internal);
assert!(Sensitivity::Internal < Sensitivity::Confidential);
assert!(Sensitivity::Confidential < Sensitivity::Restricted);
}
#[test]
fn data_policy_serde_roundtrip_keeps_reroute_payload() {
let policy = DataPolicy {
image: ImagePolicy::Strip,
thresholds: vec![PolicyThreshold {
min_sensitivity: Sensitivity::Confidential,
action: PolicyAction::ReRoute {
provider: "ollama".to_string(),
},
}],
};
let json = serde_json::to_string(&policy).expect("serialize");
let back: DataPolicy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, policy);
assert!(json.contains("\"re_route\""), "got: {json}");
}
#[test]
fn default_for_local_provider_is_permissive() {
let catalog = crate::provider::ProviderIndex::embedded();
let ollama = catalog.get("ollama").expect("ollama in catalog");
assert_eq!(ollama.family, "local");
assert!(DataPolicy::default_for(ollama).thresholds.is_empty());
}
#[test]
fn default_for_cloud_provider_has_thresholds() {
let catalog = crate::provider::ProviderIndex::embedded();
let openai = catalog.get("openai").expect("openai in catalog");
let policy = DataPolicy::default_for(openai);
assert_eq!(policy.image, ImagePolicy::Allow);
assert_eq!(policy.thresholds.len(), 3);
assert_eq!(policy.thresholds[0].action, PolicyAction::Block);
assert_eq!(
policy.thresholds[1].action,
PolicyAction::ReRoute {
provider: "ollama".to_string()
}
);
assert_eq!(policy.thresholds[2].action, PolicyAction::Warn);
}
#[test]
fn service_descriptor_data_policy_survives_serde_roundtrip() {
let catalog = crate::provider::ProviderIndex::embedded();
let mut svc = catalog.get("zai").expect("zai in catalog").clone();
svc.data_policy = Some(DataPolicy {
image: ImagePolicy::Strip,
thresholds: vec![PolicyThreshold {
min_sensitivity: Sensitivity::Internal,
action: PolicyAction::Warn,
}],
});
let json = serde_json::to_string_pretty(&svc).expect("serialize");
let back: crate::provider::ServiceDescriptor =
serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.data_policy, svc.data_policy);
}
}