systemprompt_security/policy/
config.rs1use std::path::Path;
34
35use serde_yaml::Value as YamlValue;
36use thiserror::Error;
37
38use super::builtin::SECRET_SCAN_ID;
39use super::secrets::{SecretPatternError, SecretScanner};
40
41#[derive(Debug, Error)]
42pub enum GovernanceConfigError {
43 #[error("governance config is not valid YAML: {0}")]
44 Yaml(#[from] serde_yaml::Error),
45 #[error("governance config has no `governance.policies` sequence")]
46 MissingPolicies,
47 #[error("governance config policy entry {index} has no string `id`")]
48 MissingPolicyId { index: usize },
49 #[error("governance config exists but could not be read: {0}")]
50 Unreadable(#[from] std::io::Error),
51 #[error(
52 "governance config has an unknown mode `{value}` at {location}; expected `enforce` or `warn`"
53 )]
54 InvalidMode { location: String, value: String },
55 #[error("governance secret pattern catalog is invalid: {0}")]
56 InvalidSecretPatterns(#[from] SecretPatternError),
57}
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
61pub enum PolicyMode {
62 #[default]
63 Enforce,
64 Warn,
65}
66
67impl PolicyMode {
68 #[must_use]
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::Enforce => "enforce",
72 Self::Warn => "warn",
73 }
74 }
75
76 #[must_use]
77 pub const fn is_warn(self) -> bool {
78 matches!(self, Self::Warn)
79 }
80
81 fn parse_str(value: &str) -> Option<Self> {
82 match value {
83 "enforce" => Some(Self::Enforce),
84 "warn" => Some(Self::Warn),
85 _ => None,
86 }
87 }
88}
89
90impl std::fmt::Display for PolicyMode {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.write_str(self.as_str())
93 }
94}
95
96fn 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 = ["scope_check", "secret_scan", "tool_blocklist", "rate_limit"]
139 .into_iter()
140 .map(|id| PolicyConfig {
141 id: id.to_owned(),
142 enabled: true,
143 mode: PolicyMode::Warn,
144 params: YamlValue::Null,
145 })
146 .collect();
147 Self {
148 enabled: true,
149 mode: PolicyMode::Warn,
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 if id == SECRET_SCAN_ID {
181 SecretScanner::from_policy_yaml(entry)?;
182 }
183 out.push(PolicyConfig {
184 id,
185 enabled,
186 mode,
187 params: entry.clone(),
188 });
189 }
190 Ok(Self {
191 enabled,
192 mode: default_mode,
193 policies: out,
194 })
195 }
196
197 fn read(path: &Path) -> Result<Option<Self>, GovernanceConfigError> {
198 match std::fs::read_to_string(path) {
199 Ok(text) => Self::parse(&text).map(Some),
200 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
201 Err(e) => Err(GovernanceConfigError::Unreadable(e)),
202 }
203 }
204
205 pub fn load(path: &Path) -> Result<Self, GovernanceConfigError> {
206 let config = Self::read(path)?;
207 Ok(config.unwrap_or_else(|| {
208 tracing::warn!(
209 path = %path.display(),
210 "governance config not found; falling back to the vendor-neutral warn-only chain"
211 );
212 Self::defaults()
213 }))
214 }
215}