use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::LazyLock;
use systemprompt_config::ProfileBootstrap;
use systemprompt_identifiers::PolicyId;
use super::audit::{ChainEntryOutcome, ChainEntryResult};
use super::config::{GovernanceConfig, PolicyConfig, PolicyMode};
use super::registry::{PolicyFactory, PolicyRegistration};
use super::types::{GovernancePolicy, PolicyContext};
use crate::authz::types::{Decision, DenyReason, MatchedBy};
#[derive(Debug)]
pub struct Evaluation {
pub decision: Decision,
pub chain: Vec<ChainEntryOutcome>,
}
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() -> &'static Self {
static ENGINE: LazyLock<GovernanceEngine> = LazyLock::new(|| {
let config = governance_config_path()
.map_or_else(GovernanceConfig::defaults, |p| GovernanceConfig::load(&p));
GovernanceEngine::from_config(&config)
});
&ENGINE
}
#[must_use]
pub fn from_config(config: &GovernanceConfig) -> Self {
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 Some(factory) = factories.get(cfg.id.as_str()) else {
tracing::warn!(
policy = %cfg.id,
"governance policy in config has no registered impl — skipping"
);
continue;
};
entries.push(ChainEntry {
config: cfg.clone(),
instance: factory(&cfg.params),
});
}
let mentioned: HashSet<&str> = entries.iter().map(|e| e.config.id.as_str()).collect();
let unmentioned: Vec<&PolicyRegistration> = inventory::iter::<PolicyRegistration>()
.filter(|r| !mentioned.contains(r.id))
.collect();
for r in unmentioned {
let cfg = PolicyConfig {
id: r.id.to_owned(),
enabled: false,
mode: PolicyMode::Enforce,
params: serde_yaml::Value::Null,
};
let instance = (r.factory)(&cfg.params);
entries.push(ChainEntry {
config: cfg,
instance,
});
}
Self {
enabled: config.enabled,
entries,
}
}
pub fn policies(&self) -> impl Iterator<Item = (&PolicyConfig, &dyn GovernancePolicy)> {
self.entries
.iter()
.map(|e| (&e.config, e.instance.as_ref()))
}
#[must_use]
fn master_switch_off(&self) -> Evaluation {
Evaluation {
decision: Decision::Allow {
matched_by: MatchedBy::DefaultIncluded,
},
chain: self
.entries
.iter()
.map(|entry| {
chain_entry(
&entry.config,
ChainEntryResult::Disabled,
"Governance disabled by master switch",
)
})
.collect(),
}
}
pub fn evaluate(&self, ctx: &PolicyContext<'_>) -> Evaluation {
if !self.enabled {
return self.master_switch_off();
}
let mut chain: Vec<ChainEntryOutcome> = Vec::with_capacity(self.entries.len());
let mut halted: Option<Decision> = None;
let mut first_warn: Option<DenyReason> = None;
for entry in &self.entries {
if !entry.config.enabled {
chain.push(chain_entry(
&entry.config,
ChainEntryResult::Disabled,
"Policy disabled in governance config",
));
continue;
}
if halted.is_some() {
chain.push(chain_entry(
&entry.config,
ChainEntryResult::Skip,
"Skipped — already halted by an earlier policy",
));
continue;
}
let started = std::time::Instant::now();
let decision = entry.instance.evaluate(ctx);
let duration_ms = started.elapsed().as_secs_f64() * 1000.0;
let (outcome, warn, halt) = classify(entry, &decision, duration_ms);
chain.push(outcome);
if let Some(reason) = warn
&& first_warn.is_none()
{
first_warn = Some(reason);
}
if halt {
halted = Some(decision);
}
}
let decision = halted.unwrap_or_else(|| {
first_warn.map_or(
Decision::Allow {
matched_by: MatchedBy::DefaultIncluded,
},
|reason| Decision::Warn { reason },
)
});
Evaluation { decision, chain }
}
}
fn classify(
entry: &ChainEntry,
decision: &Decision,
duration_ms: f64,
) -> (ChainEntryOutcome, Option<DenyReason>, bool) {
let row = |result, detail| ChainEntryOutcome {
policy_id: entry.instance.id(),
result,
detail,
duration_ms,
};
match decision {
Decision::Allow { matched_by } => (
row(ChainEntryResult::Pass, allow_detail(matched_by)),
None,
false,
),
Decision::Deny { reason } if entry.config.mode.is_warn() => {
tracing::warn!(
policy = %entry.config.id,
reason = %reason,
"governance policy in warn mode would have denied this call; allowing it"
);
(
row(ChainEntryResult::Warn, reason.to_string()),
Some(reason.clone()),
false,
)
},
Decision::Deny { reason } => (row(ChainEntryResult::Fail, reason.to_string()), None, true),
Decision::Warn { reason } => (
row(ChainEntryResult::Warn, reason.to_string()),
Some(reason.clone()),
false,
),
Decision::Pending { reason } => {
(row(ChainEntryResult::Hold, reason.to_string()), None, true)
},
}
}
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"))
}
fn chain_entry(cfg: &PolicyConfig, result: ChainEntryResult, detail: &str) -> ChainEntryOutcome {
ChainEntryOutcome {
policy_id: PolicyId::new(cfg.id.clone()),
result,
detail: detail.to_owned(),
duration_ms: 0.0,
}
}
fn allow_detail(matched_by: &MatchedBy) -> String {
match matched_by {
MatchedBy::PolicyAllow { detail, .. } => detail.to_string(),
MatchedBy::UserAllow => "user allow".to_owned(),
MatchedBy::RoleAllow { role } => format!("role allow: {role}"),
MatchedBy::AttributeAllow { rule_type, value } => format!("{rule_type} allow: {value}"),
MatchedBy::DefaultIncluded => "default included".to_owned(),
}
}