use crate::config::{McpPolicyConfig, OperatingMode};
use super::migration::v1_to_v2_rules;
use super::templates::get_template;
use super::types::{
tool_category, PolicyAction, PolicyRule, RuleConditions, ScheduleWindow, ToolCategory,
};
pub struct EvalContext<'a> {
pub tool_name: &'a str,
pub category: ToolCategory,
pub mode: &'a OperatingMode,
}
pub fn conditions_match(conditions: &RuleConditions, ctx: &EvalContext) -> bool {
if !conditions.tools.is_empty() && !conditions.tools.iter().any(|t| t == ctx.tool_name) {
return false;
}
if !conditions.categories.is_empty() && !conditions.categories.contains(&ctx.category) {
return false;
}
if !conditions.modes.is_empty() && !conditions.modes.iter().any(|m| m == ctx.mode) {
return false;
}
if let Some(window) = &conditions.schedule_window {
if !check_schedule_window(window) {
return false;
}
}
true
}
fn check_schedule_window(window: &ScheduleWindow) -> bool {
let now = chrono::Utc::now();
let tz: chrono_tz::Tz = window.timezone.parse().unwrap_or(chrono_tz::UTC);
let local = now.with_timezone(&tz);
let hour = local.format("%H").to_string().parse::<u8>().unwrap_or(0);
if !window.days.is_empty() {
let day = local.format("%a").to_string().to_lowercase();
let day_short = &day[..3.min(day.len())];
if !window.days.iter().any(|d| d.to_lowercase() == day_short) {
return false;
}
}
if window.start_hour <= window.end_hour {
hour >= window.start_hour && hour < window.end_hour
} else {
hour >= window.start_hour || hour < window.end_hour
}
}
pub fn build_effective_rules(config: &McpPolicyConfig, mode: &OperatingMode) -> Vec<PolicyRule> {
let mut rules: Vec<PolicyRule> = Vec::new();
rules.push(PolicyRule {
id: "hard:delete_approval".into(),
priority: 0,
label: "Delete always requires approval".into(),
enabled: true,
conditions: RuleConditions {
categories: vec![ToolCategory::Delete],
..Default::default()
},
action: PolicyAction::RequireApproval {
reason: "delete actions always require approval".into(),
},
});
if *mode == OperatingMode::Composer {
rules.push(PolicyRule {
id: "hard:composer_approval".into(),
priority: 10,
label: "Composer mode requires approval".into(),
enabled: true,
conditions: RuleConditions::default(),
action: PolicyAction::RequireApproval {
reason: "composer mode requires approval for all mutations".into(),
},
});
}
if let Some(template_name) = &config.template {
let template = get_template(template_name);
rules.extend(template.rules);
}
rules.extend(config.rules.iter().filter(|r| r.enabled).cloned());
if config.template.is_none() && config.rules.is_empty() {
rules.extend(v1_to_v2_rules(config));
}
rules.sort_by_key(|r| r.priority);
rules
}
pub fn find_matching_rule<'a>(
rules: &'a [PolicyRule],
ctx: &EvalContext,
) -> Option<&'a PolicyRule> {
rules
.iter()
.filter(|r| r.enabled)
.find(|r| conditions_match(&r.conditions, ctx))
}
pub fn make_eval_context<'a>(tool_name: &'a str, mode: &'a OperatingMode) -> EvalContext<'a> {
EvalContext {
tool_name,
category: tool_category(tool_name),
mode,
}
}