safe-chains 0.232.4

Auto-allow safe bash commands in agentic coding tools
Documentation
use super::budget::parsed_bytes;
use super::check::command_verdict;
use super::classify_budget::{CLASSIFY_BYTE_BASE, CLASSIFY_BYTES_PER_INPUT_BYTE};
use crate::verdict::Verdict;
use proptest::prelude::*;

const BRACE_FANOUT_SEED: &[u8] = include_bytes!("../../fuzz/corpus/level_monotonic/seed-timeout-brace-glob");

fn allowance(input: &str) -> u64 {
    CLASSIFY_BYTE_BASE + CLASSIFY_BYTES_PER_INPUT_BYTE * input.len() as u64
}

fn parsed_while(f: impl FnOnce()) -> u64 {
    let before = parsed_bytes();
    f();
    parsed_bytes() - before
}

fn fanned_out_wrapper(wrapper: &str, filler: &str, fanout: usize, tail: &str) -> String {
    format!("{filler}{}/{wrapper} {tail}", "{a,b}".repeat(fanout))
}

#[test]
fn a_brace_fanned_runner_chain_parses_within_the_byte_allowance() {
    let input = String::from_utf8_lossy(BRACE_FANOUT_SEED).into_owned();
    let start = std::time::Instant::now();
    let mut verdict = None;
    let parsed = parsed_while(|| verdict = Some(command_verdict(&input)));
    let explained = parsed_while(|| {
        let _ = super::explain(&input).render();
    });
    assert_eq!(verdict, Some(Verdict::Denied));
    assert!(parsed <= allowance(&input), "classifying parsed {parsed} bytes, over the {} allowance", allowance(&input));
    assert!(explained <= allowance(&input), "explaining parsed {explained} bytes, over the {} allowance", allowance(&input));
    let elapsed = start.elapsed();
    assert!(elapsed < std::time::Duration::from_secs(5), "the fan-out seed took {elapsed:?}");
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(48))]

    #[test]
    fn delegation_never_parses_more_than_the_allowance(
        wrapper in prop::sample::select(&["bunx", "npx", "env", "nice", "nohup", "time", "sudo"][..]),
        filler in "[a-z]{0,40}",
        fanout in 1usize..=8,
        tail in prop::sample::select(&[":", "ls", "echo hi", "$x"][..]),
    ) {
        let input = fanned_out_wrapper(wrapper, &filler, fanout, tail);
        let parsed = parsed_while(|| {
            let _ = command_verdict(&input);
        });
        prop_assert!(parsed <= allowance(&input), "`{input}` parsed {parsed} bytes, over the {} allowance", allowance(&input));
        let explained = parsed_while(|| {
            let _ = super::explain(&input).render();
        });
        prop_assert!(explained <= allowance(&input), "explaining `{input}` parsed {explained} bytes, over the {} allowance", allowance(&input));
    }
}