mod chain;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::LazyLock;
use systemprompt_config::ProfileBootstrap;
use thiserror::Error;
use super::audit::ChainEntryOutcome;
use super::builtin::SECRET_SCAN_ID;
use super::config::{GovernanceConfig, PolicyConfig, PolicyMode};
use super::governed::GovernedInput;
use super::registry::{PolicyConfigurationError, PolicyFactory, PolicyRegistration};
use super::secrets::{SecretFinding, SecretScanner};
use super::types::{GovernancePolicy, PolicyContext};
use crate::authz::types::Decision;
#[derive(Debug)]
pub struct Evaluation {
pub decision: Decision,
pub chain: Vec<ChainEntryOutcome>,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum GovernanceEngineError {
#[error(
"governance config names policy `{id}`, but no implementation is linked into this binary"
)]
UnknownPolicyId { id: String },
#[error("governance policy `{id}` has invalid configuration: {source}")]
InvalidPolicyConfiguration {
id: String,
#[source]
source: PolicyConfigurationError,
},
#[error("governance config at {path} was rejected: {message}")]
ConfigRejected { path: String, message: String },
}
struct ChainEntry {
config: PolicyConfig,
instance: Box<dyn GovernancePolicy>,
}
pub struct GovernanceEngine {
enabled: bool,
entries: Vec<ChainEntry>,
}
impl std::fmt::Debug for GovernanceEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GovernanceEngine")
.field("enabled", &self.enabled)
.field(
"policies",
&self
.entries
.iter()
.map(|e| e.config.id.as_str())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl GovernanceEngine {
pub fn global() -> Result<&'static Self, GovernanceEngineError> {
static ENGINE: LazyLock<Result<GovernanceEngine, GovernanceEngineError>> =
LazyLock::new(|| {
let config = match governance_config_path() {
Some(path) => GovernanceConfig::load(&path).map_err(|error| {
GovernanceEngineError::ConfigRejected {
path: path.display().to_string(),
message: error.to_string(),
}
})?,
None => GovernanceConfig::defaults(),
};
GovernanceEngine::from_config(&config)
});
ENGINE.as_ref().map_err(Clone::clone)
}
pub fn from_config(config: &GovernanceConfig) -> Result<Self, GovernanceEngineError> {
if !config.enabled {
tracing::warn!(
"governance is DISABLED by config: no scope, secret, blocklist or rate-limit \
check will run on any request"
);
}
let factories: HashMap<&'static str, PolicyFactory> =
inventory::iter::<PolicyRegistration>()
.map(|r| (r.id, r.factory))
.collect();
let mut entries = Vec::with_capacity(config.policies.len());
for cfg in &config.policies {
let factory = factories
.get(cfg.id.as_str())
.ok_or_else(|| GovernanceEngineError::UnknownPolicyId { id: cfg.id.clone() })?;
let instance = factory(&cfg.params).map_err(|source| {
GovernanceEngineError::InvalidPolicyConfiguration {
id: cfg.id.clone(),
source,
}
})?;
entries.push(ChainEntry {
config: cfg.clone(),
instance,
});
}
let mentioned: HashSet<&str> = config.policies.iter().map(|p| p.id.as_str()).collect();
for r in inventory::iter::<PolicyRegistration>().filter(|r| !mentioned.contains(r.id)) {
let config = PolicyConfig {
id: r.id.to_owned(),
enabled: false,
mode: PolicyMode::Enforce,
params: serde_yaml::Value::Null,
};
let instance = (r.factory)(&config.params).map_err(|source| {
GovernanceEngineError::InvalidPolicyConfiguration {
id: r.id.to_owned(),
source,
}
})?;
entries.push(ChainEntry { config, instance });
}
Ok(Self {
enabled: config.enabled,
entries,
})
}
#[must_use]
pub fn enforces_prompt_secrets(&self) -> bool {
self.enabled
&& self.entries.iter().any(|entry| {
entry.config.enabled
&& !entry.config.mode.is_warn()
&& entry.config.id == SECRET_SCAN_ID
})
}
#[must_use]
pub fn secret_scanner(&self) -> Option<&SecretScanner> {
self.entries
.iter()
.find(|entry| entry.config.id == SECRET_SCAN_ID)
.and_then(|entry| entry.instance.secret_scanner())
}
pub fn policies(&self) -> impl Iterator<Item = (&PolicyConfig, &dyn GovernancePolicy)> {
self.entries
.iter()
.map(|e| (&e.config, e.instance.as_ref()))
}
pub fn evaluate(&self, ctx: &PolicyContext<'_>) -> Evaluation {
self.evaluate_chain(ctx, None)
}
pub fn evaluate_with_prompt_recovery(
&self,
ctx: &PolicyContext<'_>,
mut recover: impl FnMut(&[SecretFinding]) -> Option<GovernedInput>,
) -> Evaluation {
self.evaluate_chain(ctx, Some(&mut recover))
}
}
fn governance_config_path() -> Option<PathBuf> {
let profile = ProfileBootstrap::get()
.inspect_err(|e| {
tracing::error!(
error = %e,
"governance profile bootstrap failed; policies fall back to built-in defaults"
);
})
.ok()?;
Some(PathBuf::from(&profile.paths.services).join("governance/config.yaml"))
}