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(
97 node: Option<&YamlValue>,
98 location: &str,
99) -> Result<Option<PolicyMode>, GovernanceConfigError> {
100 let Some(raw) = node.and_then(|n| n.get("mode")) else {
101 return Ok(None);
102 };
103 let text = raw
104 .as_str()
105 .ok_or_else(|| GovernanceConfigError::InvalidMode {
106 location: location.to_owned(),
107 value: format!("{raw:?}"),
108 })?;
109 PolicyMode::parse_str(text)
110 .map(Some)
111 .ok_or_else(|| GovernanceConfigError::InvalidMode {
112 location: location.to_owned(),
113 value: text.to_owned(),
114 })
115}
116
117#[derive(Debug, Clone)]
120pub struct PolicyConfig {
121 pub id: String,
122 pub enabled: bool,
123 pub mode: PolicyMode,
124 pub params: YamlValue,
125}
126
127#[derive(Debug, Clone)]
129pub struct GovernanceConfig {
130 pub enabled: bool,
131 pub mode: PolicyMode,
132 pub policies: Vec<PolicyConfig>,
133}
134
135impl GovernanceConfig {
136 #[must_use]
137 pub fn defaults() -> Self {
138 let policies = ["secret_scan", "scope_check", "tool_blocklist", "rate_limit"]
139 .into_iter()
140 .map(|id| PolicyConfig {
141 id: id.to_owned(),
142 enabled: true,
143 mode: PolicyMode::Enforce,
144 params: YamlValue::Null,
145 })
146 .collect();
147 Self {
148 enabled: true,
149 mode: PolicyMode::Enforce,
150 policies,
151 }
152 }
153
154 pub fn parse(yaml: &str) -> Result<Self, GovernanceConfigError> {
155 let root: YamlValue = serde_yaml::from_str(yaml)?;
156 let governance = root.get("governance");
157 let enabled = governance
158 .and_then(|g| g.get("enabled"))
159 .and_then(YamlValue::as_bool)
160 .unwrap_or(true);
161 let default_mode = read_mode(governance, "governance")?.unwrap_or_default();
162 let policies = governance
163 .and_then(|g| g.get("policies"))
164 .and_then(YamlValue::as_sequence)
165 .ok_or(GovernanceConfigError::MissingPolicies)?;
166
167 let mut out = Vec::with_capacity(policies.len());
168 for (index, entry) in policies.iter().enumerate() {
169 let id = entry
170 .get("id")
171 .and_then(YamlValue::as_str)
172 .ok_or(GovernanceConfigError::MissingPolicyId { index })?
173 .to_owned();
174 let enabled = entry
175 .get("enabled")
176 .and_then(YamlValue::as_bool)
177 .unwrap_or(true);
178 let mode = read_mode(Some(entry), &format!("governance.policies[{index}] ({id})"))?
179 .unwrap_or(default_mode);
180 out.push(PolicyConfig {
181 id,
182 enabled,
183 mode,
184 params: entry.clone(),
185 });
186 }
187 Ok(Self {
188 enabled,
189 mode: default_mode,
190 policies: out,
191 })
192 }
193
194 fn read(path: &Path) -> Result<Option<Self>, GovernanceConfigError> {
195 match std::fs::read_to_string(path) {
196 Ok(text) => Self::parse(&text).map(Some),
197 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
198 Err(e) => Err(GovernanceConfigError::Unreadable(e)),
199 }
200 }
201
202 pub fn validate(path: &Path) -> Result<(), GovernanceConfigError> {
203 Self::read(path).map(|_| ())
204 }
205
206 #[must_use]
207 pub fn load(path: &Path) -> Self {
208 match Self::read(path) {
209 Ok(Some(config)) => config,
210 Ok(None) => {
211 tracing::warn!(
212 path = %path.display(),
213 "governance config not found; falling back to the built-in defaults, \
214 which enable every policy"
215 );
216 Self::defaults()
217 },
218 Err(error) => {
219 tracing::error!(
220 path = %path.display(),
221 %error,
222 "governance config rejected; falling back to the built-in defaults, \
223 which enable every policy and may not be what this file asked for"
224 );
225 Self::defaults()
226 },
227 }
228 }
229}