safe-chains 0.130.0

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

fn is_list_mode(arg: &str) -> bool {
    if arg == "--list" {
        return true;
    }
    if !arg.starts_with('-') || arg.starts_with("--") {
        return false;
    }
    let flags = &arg[1..];
    flags.contains('t') && !flags.contains('c') && !flags.contains('x') && !flags.contains('u')
}

fn no_flags_after(tokens: &[Token], start: usize) -> bool {
    tokens[start..].iter().all(|t| !t.starts_with('-'))
}

pub fn is_safe_jar(tokens: &[Token]) -> bool {
    if tokens.len() < 2 {
        return false;
    }
    let first = tokens[1].as_str();
    match first {
        "--version" | "--help" => tokens.len() == 2,
        "tf" | "tvf" => no_flags_after(tokens, 2),
        _ => is_list_mode(first) && no_flags_after(tokens, 2),
    }
}

pub(crate) fn dispatch(cmd: &str, tokens: &[Token]) -> Option<Verdict> {
    if cmd == "jar" {
        Some(if is_safe_jar(tokens) { Verdict::Allowed(SafetyLevel::Inert) } else { Verdict::Denied })
    } else {
        None
    }
}

pub fn command_docs() -> Vec<crate::docs::CommandDoc> {
    use crate::docs::CommandDoc;
    vec![
        CommandDoc::handler("jar",
            "https://docs.oracle.com/en/java/javase/21/docs/specs/man/jar.html",
            "List mode only: tf, tvf, --list, -t. Also --version, --help."),
    ]
}

#[cfg(test)]
pub(in crate::handlers::jvm) const REGISTRY: &[crate::handlers::CommandEntry] = &[
    crate::handlers::CommandEntry::Custom { cmd: "jar", valid_prefix: Some("jar tf") },
];

#[cfg(test)]
mod tests {
    use crate::is_safe_command;

    fn check(cmd: &str) -> bool {
        is_safe_command(cmd)
    }

    safe! {
        jar_tf: "jar tf app.jar",
        jar_tvf: "jar tvf app.jar",
        jar_list: "jar --list",
        jar_t: "jar -tf app.jar",
        jar_tv: "jar -tvf app.jar",
        jar_version: "jar --version",
        jar_help: "jar --help",
    }

    denied! {
        jar_bare_denied: "jar",
        jar_create_denied: "jar cf app.jar .",
        jar_extract_denied: "jar xf app.jar",
        jar_update_denied: "jar uf app.jar file.class",
        jar_ct_denied: "jar -ctf app.jar",
        jar_unknown_denied: "jar --unknown",
    }
}