systemprompt_security/policy/
config.rs1use std::path::Path;
27
28use serde_yaml::Value as YamlValue;
29use thiserror::Error;
30
31#[derive(Debug, Error)]
32pub enum GovernanceConfigError {
33 #[error("governance config is not valid YAML: {0}")]
34 Yaml(#[from] serde_yaml::Error),
35 #[error("governance config has no `governance.policies` sequence")]
36 MissingPolicies,
37 #[error("governance config policy entry {index} has no string `id`")]
38 MissingPolicyId { index: usize },
39 #[error("governance config exists but could not be read: {0}")]
40 Unreadable(#[from] std::io::Error),
41}
42
43#[derive(Debug, Clone)]
46pub struct PolicyConfig {
47 pub id: String,
48 pub enabled: bool,
49 pub params: YamlValue,
50}
51
52#[derive(Debug, Clone)]
54pub struct GovernanceConfig {
55 pub enabled: bool,
56 pub policies: Vec<PolicyConfig>,
57}
58
59impl GovernanceConfig {
60 #[must_use]
61 pub fn defaults() -> Self {
62 let policies = ["secret_scan", "scope_check", "tool_blocklist", "rate_limit"]
63 .into_iter()
64 .map(|id| PolicyConfig {
65 id: id.to_owned(),
66 enabled: true,
67 params: YamlValue::Null,
68 })
69 .collect();
70 Self {
71 enabled: true,
72 policies,
73 }
74 }
75
76 pub fn parse(yaml: &str) -> Result<Self, GovernanceConfigError> {
77 let root: YamlValue = serde_yaml::from_str(yaml)?;
78 let governance = root.get("governance");
79 let enabled = governance
80 .and_then(|g| g.get("enabled"))
81 .and_then(YamlValue::as_bool)
82 .unwrap_or(true);
83 let policies = governance
84 .and_then(|g| g.get("policies"))
85 .and_then(YamlValue::as_sequence)
86 .ok_or(GovernanceConfigError::MissingPolicies)?;
87
88 let mut out = Vec::with_capacity(policies.len());
89 for (index, entry) in policies.iter().enumerate() {
90 let id = entry
91 .get("id")
92 .and_then(YamlValue::as_str)
93 .ok_or(GovernanceConfigError::MissingPolicyId { index })?
94 .to_owned();
95 let enabled = entry
96 .get("enabled")
97 .and_then(YamlValue::as_bool)
98 .unwrap_or(true);
99 out.push(PolicyConfig {
100 id,
101 enabled,
102 params: entry.clone(),
103 });
104 }
105 Ok(Self {
106 enabled,
107 policies: out,
108 })
109 }
110
111 fn read(path: &Path) -> Result<Option<Self>, GovernanceConfigError> {
112 match std::fs::read_to_string(path) {
113 Ok(text) => Self::parse(&text).map(Some),
114 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
115 Err(e) => Err(GovernanceConfigError::Unreadable(e)),
116 }
117 }
118
119 pub fn validate(path: &Path) -> Result<(), GovernanceConfigError> {
120 Self::read(path).map(|_| ())
121 }
122
123 #[must_use]
124 pub fn load(path: &Path) -> Self {
125 match Self::read(path) {
126 Ok(Some(config)) => config,
127 Ok(None) => {
128 tracing::warn!(
129 path = %path.display(),
130 "governance config not found; falling back to the built-in defaults, \
131 which enable every policy"
132 );
133 Self::defaults()
134 },
135 Err(error) => {
136 tracing::error!(
137 path = %path.display(),
138 %error,
139 "governance config rejected; falling back to the built-in defaults, \
140 which enable every policy and may not be what this file asked for"
141 );
142 Self::defaults()
143 },
144 }
145 }
146}