safe-chains 0.125.0

Auto-allow safe, read-only bash commands in agentic coding tools
Documentation
use std::fmt;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum SafetyLevel {
    Inert = 0,
    SafeRead = 1,
    SafeWrite = 2,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Verdict {
    Denied,
    Allowed(SafetyLevel),
}

impl Verdict {
    pub fn combine(self, other: Verdict) -> Verdict {
        match (self, other) {
            (Verdict::Denied, _) | (_, Verdict::Denied) => Verdict::Denied,
            (Verdict::Allowed(a), Verdict::Allowed(b)) => Verdict::Allowed(a.max(b)),
        }
    }

    pub fn is_allowed(self) -> bool {
        matches!(self, Verdict::Allowed(_))
    }
}

impl fmt::Display for SafetyLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SafetyLevel::Inert => write!(f, "inert"),
            SafetyLevel::SafeRead => write!(f, "safe-read"),
            SafetyLevel::SafeWrite => write!(f, "safe-write"),
        }
    }
}

impl fmt::Display for Verdict {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Verdict::Denied => write!(f, "denied"),
            Verdict::Allowed(level) => write!(f, "allowed ({level})"),
        }
    }
}

impl clap::ValueEnum for SafetyLevel {
    fn value_variants<'a>() -> &'a [Self] {
        &[SafetyLevel::Inert, SafetyLevel::SafeRead, SafetyLevel::SafeWrite]
    }

    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
        match self {
            SafetyLevel::Inert => Some(clap::builder::PossibleValue::new("inert")),
            SafetyLevel::SafeRead => Some(clap::builder::PossibleValue::new("safe-read")),
            SafetyLevel::SafeWrite => Some(clap::builder::PossibleValue::new("safe-write")),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn level_ordering() {
        assert!(SafetyLevel::Inert < SafetyLevel::SafeRead);
        assert!(SafetyLevel::SafeRead < SafetyLevel::SafeWrite);
    }

    #[test]
    fn combine_both_allowed() {
        let a = Verdict::Allowed(SafetyLevel::Inert);
        let b = Verdict::Allowed(SafetyLevel::SafeRead);
        assert_eq!(a.combine(b), Verdict::Allowed(SafetyLevel::SafeRead));
    }

    #[test]
    fn combine_one_denied() {
        let a = Verdict::Allowed(SafetyLevel::Inert);
        assert_eq!(a.combine(Verdict::Denied), Verdict::Denied);
        assert_eq!(Verdict::Denied.combine(a), Verdict::Denied);
    }

    #[test]
    fn combine_identity() {
        let a = Verdict::Allowed(SafetyLevel::SafeWrite);
        let identity = Verdict::Allowed(SafetyLevel::Inert);
        assert_eq!(identity.combine(a), a);
    }
}