use serde_json::Value;
pub use mcpmesh_local_api::{AuditKind, AuditRecord};
pub fn args_hash(params: &Value) -> String {
let ours = |k: &str| {
mcpmesh_net::service::RESERVED_META_PREFIXES
.iter()
.any(|p| k.starts_with(p))
};
let bytes = match params.get("_meta").and_then(Value::as_object) {
Some(meta) if meta.keys().any(|k| ours(k)) => {
let mut owned = params.clone();
if let Some(m) = owned.get_mut("_meta").and_then(Value::as_object_mut) {
m.retain(|k, _| !ours(k));
if m.is_empty() {
owned.as_object_mut().map(|o| o.remove("_meta"));
}
}
serde_json::to_vec(&owned).unwrap_or_default()
}
_ => serde_json::to_vec(params).unwrap_or_default(),
};
format!("blake3:{}", blake3::hash(&bytes).to_hex())
}
#[cfg(test)]
mod args_hash_tests {
use super::args_hash;
use serde_json::json;
fn injected(peer: serde_json::Value) -> serde_json::Value {
let mut meta = serde_json::Map::new();
for key in mcpmesh_net::service::PEER_KEYS {
meta.insert(key.to_string(), peer.clone());
}
json!({"name": "read_file", "arguments": {"path": "/x"}, "_meta": meta})
}
#[test]
fn the_injected_caller_identity_does_not_change_the_args_hash() {
let bare = json!({"name": "read_file", "arguments": {"path": "/x"}});
let expected = args_hash(&bare);
assert_eq!(
args_hash(&injected(
json!({"eid": "eid:aa", "name": "alice", "groups": ["eng"]})
)),
expected,
"the same call from any peer must hash identically, under every spelling written"
);
assert_eq!(
args_hash(&injected(
json!({"eid": "eid:bb", "name": "bob", "groups": []})
)),
expected
);
}
#[test]
fn caller_supplied_meta_still_affects_the_args_hash() {
let a = json!({"arguments": {}, "_meta": {"app/trace-id": "one"}});
let b = json!({"arguments": {}, "_meta": {"app/trace-id": "two"}});
assert_ne!(
args_hash(&a),
args_hash(&b),
"a caller-supplied `_meta` value is caller input and must be covered by the digest"
);
let mixed = json!({"arguments": {},
"_meta": {"app/trace-id": "one",
"mcpmesh/peer": {"eid": "eid:aa"},
"tech.counterpunch.mcpmesh/peer": {"eid": "eid:aa"}}});
assert_eq!(
args_hash(&mixed),
args_hash(&a),
"stripping our annotations must leave the caller's other `_meta` keys hashed: {mixed}"
);
}
#[test]
fn odd_params_shapes_hash_without_panicking() {
for v in [
json!(null),
json!("a string"),
json!([1, 2, 3]),
json!({"_meta": "not an object"}),
json!({"_meta": null}),
json!({}),
] {
let h = args_hash(&v);
assert!(h.starts_with("blake3:"), "{v} -> {h}");
}
}
}
pub fn now_ts() -> String {
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
fmt_rfc3339_millis(millis)
}
pub fn fmt_rfc3339_millis(unix_millis: u128) -> String {
let secs = (unix_millis / 1000) as i64;
let millis = (unix_millis % 1000) as u64;
let days = secs.div_euclid(86_400);
let secs_of_day = secs.rem_euclid(86_400);
let (h, m, s) = (
secs_of_day / 3600,
(secs_of_day % 3600) / 60,
secs_of_day % 60,
);
let (year, month, day) = civil_from_days(days);
format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{millis:03}Z")
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn timestamp_formats_epoch_and_millis() {
assert_eq!(fmt_rfc3339_millis(0), "1970-01-01T00:00:00.000Z");
assert_eq!(fmt_rfc3339_millis(1500), "1970-01-01T00:00:01.500Z");
assert_eq!(
fmt_rfc3339_millis(1_700_000_000_000),
"2023-11-14T22:13:20.000Z"
);
assert_eq!(&fmt_rfc3339_millis(1_700_000_000_000)[..7], "2023-11");
}
#[test]
fn args_hash_is_blake3_prefixed_and_deterministic() {
let p = json!({"path": "/secret/passwords.txt"});
let h = args_hash(&p);
assert!(h.starts_with("blake3:"));
assert_eq!(h, args_hash(&p), "same params → same hash");
assert_ne!(h, args_hash(&json!({"path": "/other"})), "differs by input");
assert_eq!(h.len(), "blake3:".len() + 64);
}
#[test]
fn proxied_request_serialization_never_leaks_raw_arguments() {
let secret = "hunter2-super-secret-token";
let params = json!({"name": "read_file", "arguments": {"token": secret}});
let rec = AuditRecord::proxied_request(
"2026-07-03T14:02:11.480Z".into(),
Some("bob".into()),
"notes".into(),
"tools/call".into(),
Some("read_file".into()),
args_hash(¶ms),
6210,
"ok".into(),
41,
None,
);
let line = serde_json::to_string(&rec).unwrap();
assert!(
!line.contains(secret),
"raw argument leaked into the record: {line}"
);
assert!(line.contains("blake3:"), "args_hash digest must be present");
assert!(
line.contains("\"tool\":\"read_file\""),
"tool NAME is recorded"
);
assert!(line.contains("\"method\":\"tools/call\""));
let s = AuditRecord::session_open(
"2026-07-03T14:02:11.480Z".into(),
Some("bob".into()),
"notes".into(),
None,
);
let sline = serde_json::to_string(&s).unwrap();
assert!(sline.contains("\"kind\":\"session_open\""));
assert!(
!sline.contains("method"),
"session record elides request fields"
);
}
#[test]
fn record_round_trips_through_jsonl() {
let rec = AuditRecord::trust(
"2026-07-03T14:02:11.480Z".into(),
"pair".into(),
Some("bob".into()),
None,
);
let line = serde_json::to_string(&rec).unwrap();
let back: AuditRecord = serde_json::from_str(&line).unwrap();
assert_eq!(back, rec);
assert_eq!(back.kind, AuditKind::Trust);
}
}