systemprompt_security/policy/
config.rs1use std::path::Path;
18
19use serde_yaml::Value as YamlValue;
20use thiserror::Error;
21
22#[derive(Debug, Error)]
23pub enum GovernanceConfigError {
24 #[error("governance config is not valid YAML: {0}")]
25 Yaml(#[from] serde_yaml::Error),
26 #[error("governance config has no `governance.policies` sequence")]
27 MissingPolicies,
28 #[error("governance config policy entry {index} has no string `id`")]
29 MissingPolicyId { index: usize },
30}
31
32#[derive(Debug, Clone)]
35pub struct PolicyConfig {
36 pub id: String,
37 pub enabled: bool,
38 pub params: YamlValue,
39}
40
41#[derive(Debug, Clone)]
43pub struct GovernanceConfig {
44 pub policies: Vec<PolicyConfig>,
45}
46
47impl GovernanceConfig {
48 #[must_use]
51 pub fn defaults() -> Self {
52 let policies = ["secret_scan", "scope_check", "tool_blocklist", "rate_limit"]
53 .into_iter()
54 .map(|id| PolicyConfig {
55 id: id.to_owned(),
56 enabled: true,
57 params: YamlValue::Null,
58 })
59 .collect();
60 Self { policies }
61 }
62
63 pub fn parse(yaml: &str) -> Result<Self, GovernanceConfigError> {
65 let root: YamlValue = serde_yaml::from_str(yaml)?;
66 let policies = root
67 .get("governance")
68 .and_then(|g| g.get("policies"))
69 .and_then(YamlValue::as_sequence)
70 .ok_or(GovernanceConfigError::MissingPolicies)?;
71
72 let mut out = Vec::with_capacity(policies.len());
73 for (index, entry) in policies.iter().enumerate() {
74 let id = entry
75 .get("id")
76 .and_then(YamlValue::as_str)
77 .ok_or(GovernanceConfigError::MissingPolicyId { index })?
78 .to_owned();
79 let enabled = entry
80 .get("enabled")
81 .and_then(YamlValue::as_bool)
82 .unwrap_or(true);
83 out.push(PolicyConfig {
84 id,
85 enabled,
86 params: entry.clone(),
87 });
88 }
89 Ok(Self { policies: out })
90 }
91
92 #[must_use]
96 pub fn load(path: &Path) -> Self {
97 let Ok(text) = std::fs::read_to_string(path) else {
98 tracing::info!(
99 path = %path.display(),
100 "governance config not found; using built-in defaults"
101 );
102 return Self::defaults();
103 };
104 Self::parse(&text).unwrap_or_else(|error| {
105 tracing::warn!(
106 path = %path.display(),
107 %error,
108 "governance config rejected; using built-in defaults"
109 );
110 Self::defaults()
111 })
112 }
113}