safe-chains 0.110.0

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

static YARN_LIST_POLICY: FlagPolicy = FlagPolicy {
    standalone: WordSet::flags(&["--json", "--long", "--production"]),
    valued: WordSet::flags(&["--depth", "--pattern"]),
    bare: true,
    max_positional: None,
    flag_style: FlagStyle::Strict,
};

static YARN_BARE_POLICY: FlagPolicy = FlagPolicy {
    standalone: WordSet::flags(&["--json"]),
    valued: WordSet::flags(&[]),
    bare: true,
    max_positional: None,
    flag_style: FlagStyle::Strict,
};

pub fn is_safe_yarn(tokens: &[Token]) -> Verdict {
    if tokens.len() < 2 {
        return Verdict::Denied;
    }
    if tokens.len() == 2 && matches!(tokens[1].as_str(), "--help" | "-h" | "--version" | "-V") {
        return Verdict::Allowed(SafetyLevel::Inert);
    }
    match tokens[1].as_str() {
        "list" | "ls" => if policy::check(&tokens[1..], &YARN_LIST_POLICY) { Verdict::Allowed(SafetyLevel::Inert) } else { Verdict::Denied },
        "info" | "why" => if policy::check(&tokens[1..], &YARN_BARE_POLICY) { Verdict::Allowed(SafetyLevel::Inert) } else { Verdict::Denied },
        "test" => Verdict::Allowed(SafetyLevel::SafeRead),
        _ if tokens[1].starts_with("test:") => Verdict::Allowed(SafetyLevel::SafeRead),
        _ => Verdict::Denied,
    }
}

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

pub fn command_docs() -> Vec<crate::docs::CommandDoc> {
    vec![
        crate::docs::CommandDoc::handler("yarn",
            "https://yarnpkg.com/cli",
            "Subcommands: info, list, ls, test, test:*, why."),
    ]
}

#[cfg(test)]
pub(crate) const REGISTRY: &[crate::handlers::CommandEntry] = &[
    crate::handlers::CommandEntry::Subcommand { cmd: "yarn", bare_ok: false, subs: &[
        crate::handlers::SubEntry::Policy { name: "list" },
        crate::handlers::SubEntry::Policy { name: "ls" },
        crate::handlers::SubEntry::Policy { name: "info" },
        crate::handlers::SubEntry::Policy { name: "why" },
        crate::handlers::SubEntry::Positional,
    ]},
];

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

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

    safe! {
        yarn_list: "yarn list --depth=0",
        yarn_list_json: "yarn list --json",
        yarn_ls: "yarn ls bootstrap",
        yarn_info: "yarn info react",
        yarn_info_json: "yarn info react --json",
        yarn_why: "yarn why lodash",
        yarn_version: "yarn --version",
        yarn_test: "yarn test",
        yarn_test_watch: "yarn test:watch",
        yarn_test_with_args: "yarn test --testPathPattern=Foo",
    }
}