use sysknife_brain::planner::{AuthorizedPlan, PlanRiskLevel};
const HARDCODED_MAX_AUTO_APPROVE: MaxRisk = MaxRisk::Medium;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MaxRisk {
Low,
Medium,
High,
}
impl MaxRisk {
pub fn as_str(&self) -> &'static str {
match self {
MaxRisk::Low => "low",
MaxRisk::Medium => "medium",
MaxRisk::High => "high",
}
}
fn includes(&self, risk: &PlanRiskLevel) -> bool {
match self {
MaxRisk::Low => matches!(risk, PlanRiskLevel::Low),
MaxRisk::Medium => matches!(risk, PlanRiskLevel::Low | PlanRiskLevel::Medium),
MaxRisk::High => true,
}
}
}
pub struct ApprovalPolicy {
yes: bool,
max_risk: Option<MaxRisk>,
non_interactive: bool,
dry_run: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ApprovalDecision {
AutoApproved,
RequiresPrompt,
RequiresInteraction,
ExceedsCeiling(MaxRisk),
}
impl ApprovalPolicy {
pub fn new(yes: bool, max_risk: Option<MaxRisk>, non_interactive: bool, dry_run: bool) -> Self {
Self {
yes,
max_risk,
non_interactive,
dry_run,
}
}
pub fn effective_auto_ceiling(&self) -> Option<MaxRisk> {
if !self.yes {
return None;
}
let requested = self.max_risk.unwrap_or(MaxRisk::Low);
Some(requested.min(HARDCODED_MAX_AUTO_APPROVE))
}
pub fn decide_step(&self, risk: &PlanRiskLevel) -> ApprovalDecision {
if self.dry_run {
return ApprovalDecision::AutoApproved;
}
if let Some(ceiling) = self.max_risk {
if !ceiling.includes(risk) {
return ApprovalDecision::ExceedsCeiling(ceiling);
}
}
if let Some(auto_ceiling) = self.effective_auto_ceiling() {
if auto_ceiling.includes(risk) {
return ApprovalDecision::AutoApproved;
}
}
if self.non_interactive {
return ApprovalDecision::RequiresInteraction;
}
ApprovalDecision::RequiresPrompt
}
pub fn decide_plan(&self, plan: &AuthorizedPlan) -> ApprovalDecision {
let mut worst = ApprovalDecision::AutoApproved;
for step in plan.steps() {
let d = self.decide_step(step.risk_level());
match d {
ApprovalDecision::ExceedsCeiling(_) => return d,
ApprovalDecision::RequiresInteraction => return d,
ApprovalDecision::RequiresPrompt => worst = ApprovalDecision::RequiresPrompt,
ApprovalDecision::AutoApproved => {}
}
}
worst
}
}
#[cfg(test)]
mod tests {
use super::*;
use sysknife_brain::action_name::ActionName;
use sysknife_brain::planner::{Plan, PlanStep};
fn step(risk: PlanRiskLevel) -> PlanStep {
PlanStep::new(
ActionName::parse("GetDiskUsage").unwrap(),
"test step".into(),
risk,
serde_json::json!({}),
)
.unwrap()
}
fn plan(risks: &[PlanRiskLevel]) -> AuthorizedPlan {
let steps: Vec<PlanStep> = risks.iter().map(|r| step(r.clone())).collect();
Plan::new(
"test".into(),
"test plan".into(),
"test explanation".into(),
steps,
)
.unwrap()
.assume_authorized()
}
fn policy(
yes: bool,
max_risk: Option<MaxRisk>,
non_interactive: bool,
dry_run: bool,
) -> ApprovalPolicy {
ApprovalPolicy::new(yes, max_risk, non_interactive, dry_run)
}
#[test]
fn yes_alone_requires_prompt_for_medium() {
let p = policy(true, None, false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::RequiresPrompt
);
}
#[test]
fn yes_alone_requires_prompt_for_high() {
let p = policy(true, None, false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::RequiresPrompt
);
}
#[test]
fn yes_alone_auto_approves_low() {
let p = policy(true, None, false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Low),
ApprovalDecision::AutoApproved
);
}
#[test]
fn yes_max_risk_medium_auto_approves_medium() {
let p = policy(true, Some(MaxRisk::Medium), false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::AutoApproved
);
}
#[test]
fn yes_max_risk_high_does_not_auto_approve_high() {
let p = policy(true, Some(MaxRisk::High), false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::RequiresPrompt
);
}
#[test]
fn yes_max_risk_high_auto_approves_medium() {
let p = policy(true, Some(MaxRisk::High), false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::AutoApproved
);
}
#[test]
fn max_risk_ceiling_exceeds_for_high_step() {
let p = policy(false, Some(MaxRisk::Medium), false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::ExceedsCeiling(MaxRisk::Medium)
);
}
#[test]
fn max_risk_low_ceiling_exceeds_for_medium_step() {
let p = policy(false, Some(MaxRisk::Low), false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::ExceedsCeiling(MaxRisk::Low)
);
}
#[test]
fn max_risk_high_no_yes_high_step_requires_prompt() {
let p = policy(false, Some(MaxRisk::High), false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::RequiresPrompt
);
}
#[test]
fn non_interactive_no_yes_requires_interaction_for_medium() {
let p = policy(false, None, true, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::RequiresInteraction
);
}
#[test]
fn non_interactive_yes_auto_approves_low() {
let p = policy(true, None, true, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Low),
ApprovalDecision::AutoApproved
);
}
#[test]
fn non_interactive_yes_requires_interaction_for_medium() {
let p = policy(true, None, true, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::RequiresInteraction
);
}
#[test]
fn non_interactive_yes_max_risk_high_high_step_requires_interaction() {
let p = policy(true, Some(MaxRisk::High), true, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::RequiresInteraction
);
}
#[test]
fn dry_run_auto_approves_low() {
let p = policy(false, None, false, true);
assert_eq!(
p.decide_step(&PlanRiskLevel::Low),
ApprovalDecision::AutoApproved
);
}
#[test]
fn dry_run_auto_approves_medium() {
let p = policy(false, None, false, true);
assert_eq!(
p.decide_step(&PlanRiskLevel::Medium),
ApprovalDecision::AutoApproved
);
}
#[test]
fn dry_run_auto_approves_high() {
let p = policy(false, None, false, true);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::AutoApproved
);
}
#[test]
fn dry_run_overrides_yes_max_risk_high_with_high_step() {
let p = policy(true, Some(MaxRisk::High), false, true);
assert_eq!(
p.decide_step(&PlanRiskLevel::High),
ApprovalDecision::AutoApproved
);
}
#[test]
fn no_flags_low_step_requires_prompt() {
let p = policy(false, None, false, false);
assert_eq!(
p.decide_step(&PlanRiskLevel::Low),
ApprovalDecision::RequiresPrompt
);
}
#[test]
fn plan_single_low_step_with_yes_auto_approved() {
let p = policy(true, None, false, false);
let pl = plan(&[PlanRiskLevel::Low]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::AutoApproved);
}
#[test]
fn plan_all_low_with_yes_auto_approved() {
let p = policy(true, None, false, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::Low]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::AutoApproved);
}
#[test]
fn plan_all_low_no_yes_requires_prompt() {
let p = policy(false, None, false, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::Low]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::RequiresPrompt);
}
#[test]
fn plan_mixed_low_medium_with_yes_requires_prompt() {
let p = policy(true, None, false, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::Medium]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::RequiresPrompt);
}
#[test]
fn plan_high_first_exceeds_ceiling() {
let p = policy(false, Some(MaxRisk::Medium), false, false);
let pl = plan(&[PlanRiskLevel::High, PlanRiskLevel::Low]);
assert_eq!(
p.decide_plan(&pl),
ApprovalDecision::ExceedsCeiling(MaxRisk::Medium)
);
}
#[test]
fn plan_high_middle_exceeds_ceiling() {
let p = policy(false, Some(MaxRisk::Medium), false, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::High, PlanRiskLevel::Low]);
assert_eq!(
p.decide_plan(&pl),
ApprovalDecision::ExceedsCeiling(MaxRisk::Medium)
);
}
#[test]
fn plan_high_step_with_max_risk_medium_exceeds_ceiling() {
let p = policy(true, Some(MaxRisk::Medium), false, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::High]);
assert_eq!(
p.decide_plan(&pl),
ApprovalDecision::ExceedsCeiling(MaxRisk::Medium)
);
}
#[test]
fn plan_single_high_step_yes_max_risk_high_requires_prompt() {
let p = policy(true, Some(MaxRisk::High), false, false);
let pl = plan(&[PlanRiskLevel::High]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::RequiresPrompt);
}
#[test]
fn plan_all_high_yes_max_risk_high_requires_prompt() {
let p = policy(true, Some(MaxRisk::High), false, false);
let pl = plan(&[PlanRiskLevel::High, PlanRiskLevel::High]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::RequiresPrompt);
}
#[test]
fn plan_medium_step_non_interactive_yes_requires_interaction() {
let p = policy(true, None, true, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::Medium]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::RequiresInteraction);
}
#[test]
fn plan_non_interactive_yes_max_risk_high_high_step_requires_interaction() {
let p = policy(true, Some(MaxRisk::High), true, false);
let pl = plan(&[PlanRiskLevel::Low, PlanRiskLevel::High]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::RequiresInteraction);
}
#[test]
fn plan_dry_run_multi_step_auto_approved() {
let p = policy(false, None, false, true);
let pl = plan(&[
PlanRiskLevel::Low,
PlanRiskLevel::Medium,
PlanRiskLevel::High,
]);
assert_eq!(p.decide_plan(&pl), ApprovalDecision::AutoApproved);
}
#[test]
fn effective_ceiling_none_without_yes() {
let p = policy(false, None, false, false);
assert!(p.effective_auto_ceiling().is_none());
}
#[test]
fn effective_ceiling_low_with_bare_yes() {
let p = policy(true, None, false, false);
assert_eq!(p.effective_auto_ceiling(), Some(MaxRisk::Low));
}
#[test]
fn effective_ceiling_medium_with_yes_max_risk_medium() {
let p = policy(true, Some(MaxRisk::Medium), false, false);
assert_eq!(p.effective_auto_ceiling(), Some(MaxRisk::Medium));
}
#[test]
fn effective_ceiling_clamped_to_medium_with_yes_max_risk_high() {
let p = policy(true, Some(MaxRisk::High), false, false);
assert_eq!(p.effective_auto_ceiling(), Some(MaxRisk::Medium));
}
}