systemprompt_security/policy/
config.rs1use std::path::Path;
36
37use serde_yaml::Value as YamlValue;
38use thiserror::Error;
39
40#[derive(Debug, Error)]
41pub enum GovernanceConfigError {
42 #[error("governance config is not valid YAML: {0}")]
43 Yaml(#[from] serde_yaml::Error),
44 #[error("governance config has no `governance.policies` sequence")]
45 MissingPolicies,
46 #[error("governance config policy entry {index} has no string `id`")]
47 MissingPolicyId { index: usize },
48 #[error("governance config exists but could not be read: {0}")]
49 Unreadable(#[from] std::io::Error),
50 #[error(
51 "governance config has an unknown mode `{value}` at {location}; expected `enforce` or `warn`"
52 )]
53 InvalidMode { location: String, value: String },
54}
55
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
58pub enum PolicyMode {
59 #[default]
60 Enforce,
61 Warn,
62}
63
64impl PolicyMode {
65 #[must_use]
66 pub const fn as_str(self) -> &'static str {
67 match self {
68 Self::Enforce => "enforce",
69 Self::Warn => "warn",
70 }
71 }
72
73 #[must_use]
74 pub const fn is_warn(self) -> bool {
75 matches!(self, Self::Warn)
76 }
77
78 fn parse_str(value: &str) -> Option<Self> {
79 match value {
80 "enforce" => Some(Self::Enforce),
81 "warn" => Some(Self::Warn),
82 _ => None,
83 }
84 }
85}
86
87impl std::fmt::Display for PolicyMode {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str(self.as_str())
90 }
91}
92
93fn read_mode(
94 node: Option<&YamlValue>,
95 location: &str,
96) -> Result<Option<PolicyMode>, GovernanceConfigError> {
97 let Some(raw) = node.and_then(|n| n.get("mode")) else {
98 return Ok(None);
99 };
100 let text = raw
101 .as_str()
102 .ok_or_else(|| GovernanceConfigError::InvalidMode {
103 location: location.to_owned(),
104 value: format!("{raw:?}"),
105 })?;
106 PolicyMode::parse_str(text)
107 .map(Some)
108 .ok_or_else(|| GovernanceConfigError::InvalidMode {
109 location: location.to_owned(),
110 value: text.to_owned(),
111 })
112}
113
114#[derive(Debug, Clone)]
117pub struct PolicyConfig {
118 pub id: String,
119 pub enabled: bool,
120 pub mode: PolicyMode,
121 pub params: YamlValue,
122}
123
124#[derive(Debug, Clone)]
126pub struct GovernanceConfig {
127 pub enabled: bool,
128 pub mode: PolicyMode,
129 pub policies: Vec<PolicyConfig>,
130}
131
132impl GovernanceConfig {
133 #[must_use]
134 pub fn defaults() -> Self {
135 let policies = ["scope_check", "secret_scan", "tool_blocklist", "rate_limit"]
136 .into_iter()
137 .map(|id| PolicyConfig {
138 id: id.to_owned(),
139 enabled: true,
140 mode: PolicyMode::Enforce,
141 params: YamlValue::Null,
142 })
143 .collect();
144 Self {
145 enabled: true,
146 mode: PolicyMode::Enforce,
147 policies,
148 }
149 }
150
151 pub fn parse(yaml: &str) -> Result<Self, GovernanceConfigError> {
152 let root: YamlValue = serde_yaml::from_str(yaml)?;
153 let governance = root.get("governance");
154 let enabled = governance
155 .and_then(|g| g.get("enabled"))
156 .and_then(YamlValue::as_bool)
157 .unwrap_or(true);
158 let default_mode = read_mode(governance, "governance")?.unwrap_or_default();
159 let policies = governance
160 .and_then(|g| g.get("policies"))
161 .and_then(YamlValue::as_sequence)
162 .ok_or(GovernanceConfigError::MissingPolicies)?;
163
164 let mut out = Vec::with_capacity(policies.len());
165 for (index, entry) in policies.iter().enumerate() {
166 let id = entry
167 .get("id")
168 .and_then(YamlValue::as_str)
169 .ok_or(GovernanceConfigError::MissingPolicyId { index })?
170 .to_owned();
171 let enabled = entry
172 .get("enabled")
173 .and_then(YamlValue::as_bool)
174 .unwrap_or(true);
175 let mode = read_mode(Some(entry), &format!("governance.policies[{index}] ({id})"))?
176 .unwrap_or(default_mode);
177 out.push(PolicyConfig {
178 id,
179 enabled,
180 mode,
181 params: entry.clone(),
182 });
183 }
184 Ok(Self {
185 enabled,
186 mode: default_mode,
187 policies: out,
188 })
189 }
190
191 fn read(path: &Path) -> Result<Option<Self>, GovernanceConfigError> {
192 match std::fs::read_to_string(path) {
193 Ok(text) => Self::parse(&text).map(Some),
194 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
195 Err(e) => Err(GovernanceConfigError::Unreadable(e)),
196 }
197 }
198
199 pub fn validate(path: &Path) -> Result<(), GovernanceConfigError> {
200 Self::read(path).map(|_| ())
201 }
202
203 #[must_use]
204 pub fn load(path: &Path) -> Self {
205 match Self::read(path) {
206 Ok(Some(config)) => config,
207 Ok(None) => {
208 tracing::warn!(
209 path = %path.display(),
210 "governance config not found; falling back to the built-in defaults, \
211 which enable every policy"
212 );
213 Self::defaults()
214 },
215 Err(error) => {
216 tracing::error!(
217 path = %path.display(),
218 %error,
219 "governance config rejected; falling back to the built-in defaults, \
220 which enable every policy and may not be what this file asked for"
221 );
222 Self::defaults()
223 },
224 }
225 }
226}