use std::path::Path;
use crate::judge::{self, JudgeContext, JudgeRuntime, JudgeVerdict};
use crate::policy::{self, Decision, Tier, VaultPolicyData};
const HIGH_HUMAN_ONLY: &str =
"high-sensitivity secret — a human must retrieve it via 'svault secret get'";
pub const GENERIC_DENY: &str = "request not authorized for this secret";
pub struct Verdict {
pub decision: Decision,
pub note: String,
}
impl Verdict {
pub fn allowed(&self) -> bool {
matches!(self.decision, Decision::Allow(_))
}
pub fn tier(&self) -> Tier {
self.decision.tier()
}
}
pub fn authorize(
policy: &VaultPolicyData,
req: &policy::Request,
judge: Option<&JudgeRuntime>,
) -> Verdict {
let base = policy::evaluate(policy, req);
let tier = base.tier();
if let Decision::Deny(_, why) = &base {
let note = why.clone();
return Verdict {
decision: base,
note,
};
}
let vault_enabled = policy.judge.enabled.unwrap_or(true);
let active = judge.is_some() && vault_enabled;
let rule = policy.classify(req.secret);
let require_reason = rule.map(|r| r.require_reason).unwrap_or(false);
let consult = active && (tier != Tier::Low || require_reason);
if !consult {
return match tier {
Tier::Low => allow(tier, "ok"),
Tier::Medium => allow(tier, "elevated (judge off)"),
Tier::High if !active => deny(tier, HIGH_HUMAN_ONLY),
Tier::High => allow(tier, "ok"),
};
}
let rt = judge.expect("active implies Some");
let model = rt.model.clone();
let recent = recent_summary(req.vault_dir, req.caller);
let ctx = JudgeContext {
caller: req.caller,
scope: req.scope,
reason: req.reason,
secret: req.secret,
tier,
vault: req.vault,
vault_description: req.vault_description,
secret_description: rule.map(|r| r.description.as_str()).unwrap_or(""),
recent: &recent,
};
let verdict = judge::evaluate(rt, &model, &ctx);
let threshold = if tier == Tier::High {
rt.high_threshold
} else {
rt.allow_threshold
};
let fail_open = tier != Tier::High;
match verdict {
JudgeVerdict::Allow { score, rationale } if score >= threshold => Verdict {
decision: Decision::Allow(tier),
note: format!("judge allow ({score}): {rationale}"),
},
JudgeVerdict::Allow { score, rationale } | JudgeVerdict::Deny { score, rationale } => {
Verdict {
decision: Decision::Deny(tier, format!("judge denied (score {score})")),
note: format!("judge deny ({score}): {rationale}"),
}
}
JudgeVerdict::Unavailable { err } => {
if fail_open {
Verdict {
decision: Decision::Allow(tier),
note: format!("judge-unavailable (fail-open): {err}"),
}
} else {
Verdict {
decision: Decision::Deny(
tier,
"AI judge unavailable — high-tier access fails closed".to_string(),
),
note: format!("judge-unavailable (fail-closed): {err}"),
}
}
}
}
}
fn allow(tier: Tier, note: &str) -> Verdict {
Verdict {
decision: Decision::Allow(tier),
note: note.to_string(),
}
}
fn deny(tier: Tier, why: &str) -> Verdict {
Verdict {
decision: Decision::Deny(tier, why.to_string()),
note: why.to_string(),
}
}
pub fn recent_summary(vault_dir: &Path, caller: &str) -> String {
let since = chrono::Utc::now() - chrono::Duration::hours(1);
let entries = crate::audit::recent(vault_dir, caller, since).unwrap_or_default();
if entries.is_empty() {
return "no prior requests in the last hour".to_string();
}
let allowed = entries.iter().filter(|e| e.decision == "allow").count();
let denied = entries.len() - allowed;
format!(
"{} request(s) in the last hour ({allowed} allowed, {denied} denied)",
entries.len()
)
}