systemprompt_api/services/gateway/audit/
payload.rs1use bytes::Bytes;
7use serde_json::Value;
8use sha2::{Digest, Sha256};
9
10const PAYLOAD_CAP_BYTES: usize = 1024 * 1024;
11const EXCERPT_BYTES: usize = 8 * 1024;
12
13#[derive(Debug, Clone)]
18pub struct PayloadCapture {
19 pub json: Option<Value>,
20 pub excerpt: Option<String>,
21 pub truncated: bool,
22 pub byte_len: i32,
23 pub sha256: String,
24}
25
26#[must_use]
27pub fn digest_hex(bytes: &[u8]) -> String {
28 hex::encode(Sha256::digest(bytes))
29}
30
31#[must_use]
32pub fn slice_payload(bytes: &Bytes) -> PayloadCapture {
33 let len = bytes.len();
34 let byte_len = len.min(i32::MAX as usize) as i32;
35 let sha256 = digest_hex(bytes);
36 if len <= PAYLOAD_CAP_BYTES {
37 serde_json::from_slice::<Value>(bytes).map_or_else(
38 |_| PayloadCapture {
39 json: None,
40 excerpt: Some(String::from_utf8_lossy(bytes).to_string()),
41 truncated: false,
42 byte_len,
43 sha256: sha256.clone(),
44 },
45 |v| PayloadCapture {
46 json: Some(v),
47 excerpt: None,
48 truncated: false,
49 byte_len,
50 sha256: sha256.clone(),
51 },
52 )
53 } else {
54 let head_len = EXCERPT_BYTES.min(len);
55 let head = String::from_utf8_lossy(&bytes[..head_len]).to_string();
56 let tail_start = len.saturating_sub(EXCERPT_BYTES);
57 let tail_len = len - tail_start;
58 let tail = String::from_utf8_lossy(&bytes[tail_start..]).to_string();
59 let dropped = len - head_len - tail_len;
60 let excerpt = format!("{head}\n...<truncated {dropped} bytes>...\n{tail}");
61 PayloadCapture {
62 json: None,
63 excerpt: Some(excerpt),
64 truncated: true,
65 byte_len,
66 sha256,
67 }
68 }
69}
70
71pub fn truncate_for_tool_input(input: &str) -> String {
72 const TOOL_INPUT_CAP: usize = 64 * 1024;
73 if input.len() <= TOOL_INPUT_CAP {
74 input.to_owned()
75 } else {
76 let cut = systemprompt_models::text::floor_char_boundary(input, TOOL_INPUT_CAP);
77 let head = &input[..cut];
78 format!("{head}...<truncated {} bytes>", input.len() - cut)
79 }
80}