safe-chains 0.130.0

Auto-allow safe, read-only bash commands in agentic coding tools
Documentation
pub mod bun;
mod bunx;
mod npx;

use crate::parse::{Token, WordSet};
use crate::verdict::Verdict;

pub(super) static BUNX_FLAGS_NO_ARG: WordSet =
    WordSet::new(&["--bun", "--no-install", "--silent", "--verbose"]);

pub(super) fn find_runner_package_index(
    tokens: &[Token],
    start: usize,
    flags: &WordSet,
) -> Option<usize> {
    let mut i = start;
    while i < tokens.len() {
        if tokens[i] == "--package" || tokens[i] == "-p" {
            i += 2;
            continue;
        }
        if flags.contains(&tokens[i]) {
            i += 1;
            continue;
        }
        if tokens[i] == "--" {
            return Some(i + 1);
        }
        if tokens[i].starts_with("-") {
            return None;
        }
        return Some(i);
    }
    None
}

pub(super) fn runner_dispatch(tokens: &[Token], flags: &WordSet) -> crate::verdict::Verdict {
    use crate::verdict::{SafetyLevel, Verdict};

    if tokens.len() < 2 {
        return Verdict::Denied;
    }
    if tokens.len() == 2 && tokens[1] == "--version" {
        return Verdict::Allowed(SafetyLevel::Inert);
    }
    match find_runner_package_index(tokens, 1, flags) {
        Some(idx) => runner_verdict(tokens, idx),
        None => Verdict::Denied,
    }
}

fn strip_version(pkg: &str) -> &str {
    if let Some(at) = pkg.rfind('@').filter(|&at| at > 0) {
        return &pkg[..at];
    }
    pkg
}

pub(super) fn runner_verdict(tokens: &[Token], pkg_idx: usize) -> crate::verdict::Verdict {
    use crate::verdict::Verdict;

    if pkg_idx >= tokens.len() {
        return Verdict::Denied;
    }
    let pkg = strip_version(tokens[pkg_idx].as_str());
    let args: Vec<&str> = std::iter::once(pkg)
        .chain(tokens[pkg_idx + 1..].iter().map(|t| t.as_str()))
        .collect();
    let inner = shell_words::join(args);
    crate::command_verdict(&inner)
}

pub(crate) fn dispatch(cmd: &str, tokens: &[Token]) -> Option<Verdict> {
    npx::dispatch(cmd, tokens)
        .or_else(|| bunx::dispatch(cmd, tokens))
}

pub fn command_docs() -> Vec<crate::docs::CommandDoc> {
    let mut docs = Vec::new();
    docs.extend(bunx::command_docs());
    docs.extend(npx::command_docs());
    docs
}

#[cfg(test)]
pub(super) fn full_registry() -> Vec<&'static super::CommandEntry> {
    let mut v = Vec::new();
    v.extend(npx::REGISTRY);
    v.extend(bunx::REGISTRY);
    v
}