use sha2::{Digest, Sha256};
pub const PIN_CANON_VERSION: u32 = 1;
pub fn action_hash(
tool_name: &str,
input: &serde_json::Value,
channel_id: &str,
step_or_call_id: &str,
agent_id: &str,
) -> String {
let canon = serde_json::json!({
"v": PIN_CANON_VERSION,
"tool": tool_name,
"input": input,
"channel": channel_id,
"step": step_or_call_id,
"agent": agent_id,
});
let bytes = serde_json::to_vec(&canon).unwrap_or_default();
let mut h = Sha256::new();
h.update(&bytes);
format!("{:x}", h.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_stable_and_order_independent() {
let a = action_hash("bash", &serde_json::json!({"b":1,"a":2}), "c", "s0", "mur");
let b = action_hash("bash", &serde_json::json!({"a":2,"b":1}), "c", "s0", "mur");
assert_eq!(a, b);
assert_eq!(a.len(), 64);
}
#[test]
fn drift_changes_the_hash() {
let approved = action_hash("bash", &serde_json::json!({"cmd":"rm a"}), "c", "s0", "mur");
let executed = action_hash("bash", &serde_json::json!({"cmd":"rm b"}), "c", "s0", "mur");
assert_ne!(approved, executed, "different args MUST fail the re-verify");
}
}