destructive_command_guard 0.5.6

An AI coding agent hook that blocks destructive commands before they execute
Documentation
//! Benchmarks for protocol-specific deny path formatting.
//!
//! Run with: `cargo bench --bench codex_deny`

use std::hint::black_box;

use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use destructive_command_guard::hook::{
    AllowOnceInfo, HookInput, HookProtocol, extract_command_with_protocol, write_denial_to,
};
use destructive_command_guard::packs::{REGISTRY, Severity};
use destructive_command_guard::{
    Config, EvaluationDecision, LayeredAllowlist, evaluate_command_with_pack_order,
};

const COMMAND: &str = "git reset --hard HEAD~1";

struct HookBenchInputs {
    enabled_keywords: Vec<&'static str>,
    ordered_packs: Vec<String>,
    keyword_index: Option<destructive_command_guard::packs::EnabledKeywordIndex>,
    compiled_overrides: destructive_command_guard::config::CompiledOverrides,
    heredoc_settings: destructive_command_guard::config::HeredocSettings,
}

struct BenchState {
    inputs: HookBenchInputs,
    allowlists: LayeredAllowlist,
    allow_once: AllowOnceInfo,
    codex_payload: String,
    claude_payload: String,
}

#[derive(Default)]
struct OutputBuffers {
    stdout: Vec<u8>,
    stderr: Vec<u8>,
}

impl OutputBuffers {
    fn clear(&mut self) {
        self.stdout.clear();
        self.stderr.clear();
    }
}

impl BenchState {
    fn new() -> Self {
        let mut config = Config::default();
        config.heredoc.enabled = Some(false);

        let enabled_packs = config.enabled_pack_ids();
        let ordered_packs = REGISTRY.expand_enabled_ordered(&enabled_packs);
        let inputs = HookBenchInputs {
            enabled_keywords: REGISTRY.collect_enabled_keywords(&enabled_packs),
            keyword_index: REGISTRY.build_enabled_keyword_index(&ordered_packs),
            ordered_packs,
            compiled_overrides: config.overrides.compile(),
            heredoc_settings: config.heredoc_settings(),
        };

        Self {
            inputs,
            allowlists: LayeredAllowlist::default(),
            allow_once: AllowOnceInfo {
                code: "abc123".to_string(),
                full_hash: "sha256:abc123def456".to_string(),
            },
            codex_payload: serde_json::json!({
                "tool_name": "Bash",
                "turn_id": "turn-bench",
                "tool_input": { "command": COMMAND }
            })
            .to_string(),
            claude_payload: serde_json::json!({
                "tool_name": "Bash",
                "tool_input": { "command": COMMAND }
            })
            .to_string(),
        }
    }
}

fn run_deny_path(payload: &str, state: &BenchState, buffers: &mut OutputBuffers) -> (usize, usize) {
    buffers.clear();

    let input: HookInput = serde_json::from_str(black_box(payload)).expect("valid hook payload");
    let (command, protocol) =
        extract_command_with_protocol(&input).expect("payload contains a shell command");
    debug_assert!(matches!(
        protocol,
        HookProtocol::Codex | HookProtocol::ClaudeCompatible
    ));

    let result = evaluate_command_with_pack_order(
        black_box(command.as_str()),
        black_box(state.inputs.enabled_keywords.as_slice()),
        black_box(state.inputs.ordered_packs.as_slice()),
        black_box(state.inputs.keyword_index.as_ref()),
        black_box(&state.inputs.compiled_overrides),
        black_box(&state.allowlists),
        black_box(&state.inputs.heredoc_settings),
    );
    debug_assert_eq!(result.decision, EvaluationDecision::Deny);

    let info = result
        .pattern_info
        .as_ref()
        .expect("deny includes pattern info");
    debug_assert_eq!(info.severity, Some(Severity::Critical));

    write_denial_to(
        &mut buffers.stdout,
        &mut buffers.stderr,
        protocol,
        command.as_str(),
        info.reason.as_str(),
        info.pack_id.as_deref(),
        info.pattern_name.as_deref(),
        info.explanation.as_deref(),
        Some(&state.allow_once),
        info.matched_span.as_ref(),
        info.severity,
        None,
        info.suggestions,
        None,
    );

    (buffers.stdout.len(), buffers.stderr.len())
}

fn bench_protocol_deny_path(c: &mut Criterion) {
    let state = BenchState::new();
    let mut group = c.benchmark_group("hook_deny_path");

    for (name, payload) in [
        ("codex_deny", state.codex_payload.as_str()),
        ("claude_deny", state.claude_payload.as_str()),
    ] {
        group.bench_with_input(
            BenchmarkId::from_parameter(name),
            payload,
            |b: &mut criterion::Bencher<'_>, payload: &str| {
                let mut buffers = OutputBuffers::default();
                b.iter(|| black_box(run_deny_path(black_box(payload), &state, &mut buffers)));
            },
        );
    }

    group.finish();
}

criterion_group!(benches, bench_protocol_deny_path);
criterion_main!(benches);