safe-chains 0.125.0

Auto-allow safe, read-only bash commands in agentic coding tools
Documentation
use crate::parse::Token;
use crate::verdict::{SafetyLevel, Verdict};
use crate::parse::WordSet;
use crate::policy::{self, FlagPolicy, FlagStyle};

static MLR_POLICY: FlagPolicy = FlagPolicy {
    standalone: WordSet::flags(&[
        "--csv", "--csvlite", "--dkvp", "--from",
        "--headerless-csv-output", "--help",
        "--implicit-csv-header", "--json", "--jsonl",
        "--markdown", "--nidx", "--no-auto",
        "--pprint", "--ragged-or-trim", "--tsv",
        "--version", "--xtab",
        "-h",
    ]),
    valued: WordSet::flags(&[
        "--ifs", "--ips", "--irs", "--nr-progress-mod",
        "--ofs", "--ops", "--ors", "--prepipe", "--seed",
    ]),
    bare: false,
    max_positional: None,
    flag_style: FlagStyle::Positional,
    numeric_dash: false,
};

fn is_safe_mlr(tokens: &[Token]) -> Verdict {
    for t in &tokens[1..] {
        if t == "-I" || t == "--in-place" {
            return Verdict::Denied;
        }
    }
    if policy::check(tokens, &MLR_POLICY) { Verdict::Allowed(SafetyLevel::Inert) } else { Verdict::Denied }
}

pub(super) fn dispatch(cmd: &str, tokens: &[Token]) -> Option<Verdict> {
    match cmd {
        "mlr" => Some(is_safe_mlr(tokens)),
        _ => None,
    }
}

pub fn command_docs() -> Vec<crate::docs::CommandDoc> {
    vec![
        crate::docs::CommandDoc::handler("mlr",
            "https://miller.readthedocs.io/",
            "Data processing allowed. Verbs and file arguments accepted."),
    ]
}

#[cfg(test)]
pub(in crate::handlers::coreutils) const REGISTRY: &[crate::handlers::CommandEntry] = &[
    crate::handlers::CommandEntry::Positional { cmd: "mlr" },
];

#[cfg(test)]
mod tests {
    use crate::is_safe_command;
    fn check(cmd: &str) -> bool { is_safe_command(cmd) }

    safe! {
        mlr_csv_head: "mlr --csv head -n 10 data.csv",
        mlr_json_filter: "mlr --json filter '$age > 30' data.json",
        mlr_tsv_cut: "mlr --tsv cut -f name,age data.tsv",
    }

    denied! {
        mlr_bare_denied: "mlr",
        mlr_inplace_denied: "mlr -I --csv head -n 10 data.csv",
        mlr_inplace_long_denied: "mlr --in-place --csv head -n 10 data.csv",
    }
}