Skip to main content

eval_magic/sandbox/
guard.rs

1//! Shared guard-hook plumbing.
2//!
3//! The harness-specific verdict shaping lives with each harness's guard module
4//! (`crate::adapters::<harness>::guard`): the CLI handler reads the hook
5//! payload from stdin and the marker path from argv, and the harness guard
6//! calls [`parse_tool_call`] + `decide` to evaluate it. Both layers fail open —
7//! a malformed payload or unreadable marker yields "allow", so the guard can
8//! never brick a session.
9
10use std::path::Path;
11
12use serde_json::{Map, Value};
13
14use super::decide::GuardMarker;
15
16/// Read and parse the guard marker. Missing or unparseable → `None` (the guard
17/// then allows everything — fail open).
18pub fn read_marker(path: &Path) -> Option<GuardMarker> {
19    let text = std::fs::read_to_string(path).ok()?;
20    serde_json::from_str(&text).ok()
21}
22
23/// Extract the tool name + input from a PreToolUse hook payload (the JSON the
24/// harness sends on stdin). `None` for an empty or malformed payload — treated
25/// as allow by every caller.
26pub(crate) fn parse_tool_call(payload: &str) -> Option<(String, Value)> {
27    let trimmed = payload.trim();
28    let parsed: Value =
29        serde_json::from_str(if trimmed.is_empty() { "{}" } else { trimmed }).ok()?;
30
31    let tool_name = parsed
32        .get("tool_name")
33        .and_then(Value::as_str)
34        .or_else(|| {
35            parsed
36                .get("tool")
37                .and_then(Value::as_object)
38                .and_then(|tool| tool.get("name"))
39                .and_then(Value::as_str)
40        })
41        .unwrap_or("")
42        .to_string();
43
44    let tool_input = merge_top_level_files(
45        parsed
46            .get("tool_input")
47            .or_else(|| parsed.get("input"))
48            .cloned()
49            .unwrap_or(Value::Null),
50        &parsed,
51    );
52
53    Some((tool_name, tool_input))
54}
55
56fn merge_top_level_files(input: Value, parsed: &Value) -> Value {
57    let Some(files) = parsed.get("files") else {
58        return input;
59    };
60
61    match input {
62        Value::Object(mut obj) => {
63            obj.entry("files").or_insert_with(|| files.clone());
64            Value::Object(obj)
65        }
66        Value::Null => {
67            let mut obj = Map::new();
68            obj.insert("files".to_string(), files.clone());
69            Value::Object(obj)
70        }
71        other => other,
72    }
73}