use polyc_llm::ToolSpec;
use crate::ToolRegistry;
pub const APPROVAL_MODE_ENV: &str = "POLYCHROME_APPROVAL_MODE";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ApprovalMode {
#[default]
Human,
Reviewer,
ApproveAllDangerous,
}
impl ApprovalMode {
#[must_use]
pub fn from_env() -> Self {
std::env::var(APPROVAL_MODE_ENV)
.ok()
.as_deref()
.and_then(Self::from_mode_str)
.unwrap_or_default()
}
#[must_use]
pub fn from_mode_str(raw: &str) -> Option<Self> {
match raw.trim() {
"human" => Some(Self::Human),
"reviewer" => Some(Self::Reviewer),
"approve-all-dangerous" => Some(Self::ApproveAllDangerous),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Human => "human",
Self::Reviewer => "reviewer",
Self::ApproveAllDangerous => "approve-all-dangerous",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RiskTier {
Low,
Medium,
High,
}
impl RiskTier {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
}
#[must_use]
pub const fn classify_tier(spec: &ToolSpec) -> RiskTier {
if spec.destructive {
RiskTier::High
} else if spec.read_only && spec.cacheable_approval {
RiskTier::Low
} else {
RiskTier::Medium
}
}
#[must_use]
pub const fn auto_review_eligible(mode: ApprovalMode, tier: RiskTier) -> bool {
matches!(mode, ApprovalMode::Reviewer) && matches!(tier, RiskTier::Low)
}
#[must_use]
pub fn classify_tool(name: &str) -> RiskTier {
ToolRegistry::all_specs()
.iter()
.find(|s| s.name == name)
.map_or(RiskTier::High, classify_tier)
}
#[must_use]
pub fn live_tool_spec(name: &str) -> Option<ToolSpec> {
ToolRegistry::all_specs()
.into_iter()
.find(|s| s.name == name)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use serde_json::json;
fn spec(name: &str) -> ToolSpec {
ToolSpec::new(name, "d", json!({}))
}
#[test]
fn approval_mode_parses_and_defaults_to_human() {
assert_eq!(
ApprovalMode::from_mode_str("human"),
Some(ApprovalMode::Human)
);
assert_eq!(
ApprovalMode::from_mode_str("reviewer"),
Some(ApprovalMode::Reviewer)
);
assert_eq!(
ApprovalMode::from_mode_str("approve-all-dangerous"),
Some(ApprovalMode::ApproveAllDangerous)
);
assert_eq!(ApprovalMode::from_mode_str(""), None);
assert_eq!(ApprovalMode::from_mode_str("approve_all"), None);
assert_eq!(ApprovalMode::from_mode_str("approve-all"), None);
assert_eq!(ApprovalMode::default(), ApprovalMode::Human);
for m in [
ApprovalMode::Human,
ApprovalMode::Reviewer,
ApprovalMode::ApproveAllDangerous,
] {
assert_eq!(ApprovalMode::from_mode_str(m.as_str()), Some(m));
}
}
#[test]
fn classify_tier_keys_off_annotations_only() {
let low = spec("file_read").read_only().cacheable_approval();
assert_eq!(classify_tier(&low), RiskTier::Low);
let high = spec("shell_exec").destructive();
assert_eq!(classify_tier(&high), RiskTier::High);
let paid = spec("paid_fetch").destructive().approval_required();
assert_eq!(classify_tier(&paid), RiskTier::High);
let medium = spec("rename");
assert_eq!(classify_tier(&medium), RiskTier::Medium);
let ro_only = spec("list").read_only();
assert_eq!(classify_tier(&ro_only), RiskTier::Medium);
}
#[test]
fn auto_review_eligible_requires_reviewer_mode_and_low_tier() {
assert!(auto_review_eligible(ApprovalMode::Reviewer, RiskTier::Low));
assert!(!auto_review_eligible(
ApprovalMode::Reviewer,
RiskTier::Medium
));
assert!(!auto_review_eligible(
ApprovalMode::Reviewer,
RiskTier::High
));
assert!(!auto_review_eligible(ApprovalMode::Human, RiskTier::Low));
assert!(!auto_review_eligible(
ApprovalMode::ApproveAllDangerous,
RiskTier::Low
));
}
#[test]
fn classify_tool_by_name_is_fail_closed() {
for low in ["file_read", "grep", "glob"] {
assert_eq!(classify_tool(low), RiskTier::Low, "{low} must be Low");
}
for high in ["file_write", "file_edit", "shell_exec"] {
assert_eq!(classify_tool(high), RiskTier::High, "{high} must be High");
}
assert_eq!(classify_tool("some_connector_tool"), RiskTier::High);
assert_eq!(classify_tool(""), RiskTier::High);
assert!(!auto_review_eligible(
ApprovalMode::Reviewer,
classify_tool("some_connector_tool")
));
}
#[test]
fn risk_tier_as_str_labels_each_tier() {
assert_eq!(RiskTier::Low.as_str(), "low");
assert_eq!(RiskTier::Medium.as_str(), "medium");
assert_eq!(RiskTier::High.as_str(), "high");
}
}