Skip to main content

systemprompt_security/policy/
config.rs

1//! Governance-chain configuration.
2//!
3//! One YAML document (`governance.policies: [{id, enabled, ...params}]`)
4//! declares which policies run, in what order, and with what per-policy
5//! parameters. [`GovernanceConfig::load`] is deliberately lenient — a missing
6//! or malformed file degrades to [`GovernanceConfig::defaults`] with a warning,
7//! because a governance deployment that fails closed on a config typo would
8//! block every tool call in the installation. [`GovernanceConfig::parse`] is
9//! the strict form for callers that want the error.
10//!
11//! Path resolution is the caller's concern: core takes a path, extensions
12//! resolve it from their profile (`<services>/governance/config.yaml`).
13//!
14//! Copyright (c) systemprompt.io — Business Source License 1.1.
15//! See <https://systemprompt.io> for licensing details.
16
17use 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/// One entry of the configured chain: which policy, whether it runs, and the
33/// raw YAML mapping handed to the policy's factory as parameters.
34#[derive(Debug, Clone)]
35pub struct PolicyConfig {
36    pub id: String,
37    pub enabled: bool,
38    pub params: YamlValue,
39}
40
41/// The ordered policy chain declaration.
42#[derive(Debug, Clone)]
43pub struct GovernanceConfig {
44    pub policies: Vec<PolicyConfig>,
45}
46
47impl GovernanceConfig {
48    /// The four built-in policies, enabled, with default parameters, in
49    /// first-deny-wins order: cheap-and-fatal checks before stateful ones.
50    #[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    /// Strict parse of a YAML document.
64    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    /// Lenient load: any failure — absent file, unreadable file, invalid
93    /// YAML, missing `governance.policies` — logs and falls back to
94    /// [`Self::defaults`].
95    #[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}