use serde_json::{Map, Value};
use super::EventObject;
use crate::wire;
#[cfg(test)]
mod tests;
const SIZE_GATE: usize = 10_240;
const BINARY_DATA_NOTE: &str = "[binary data redacted - not supported by AgentCat]";
const BINARY_RESOURCE_NOTE: &str = "[binary resource content redacted - not supported by AgentCat]";
pub(crate) fn sanitize_event(event: &EventObject) -> EventObject {
wire::own_entries(event)
.map(|(key, value)| {
let value = match key.as_str() {
"response" if !value.is_null() => sanitize_response(value),
"parameters" if !value.is_null() => sanitize_tool_values(value),
_ => value.clone(),
};
(key.clone(), value)
})
.collect()
}
fn sanitize_response(response: &Value) -> Value {
let Value::Object(map) = response else {
return response.clone();
};
Value::Object(
wire::own_entries(map)
.map(|(key, value)| {
let value = match (key.as_str(), value) {
("content", Value::Array(blocks)) => {
Value::Array(blocks.iter().map(sanitize_block).collect())
}
("structuredContent", Value::Object(_) | Value::Array(_)) => {
sanitize_tool_values(value)
}
_ => value.clone(),
};
(key.clone(), value)
})
.collect(),
)
}
fn text_block(text: &str) -> Value {
let mut block = Map::new();
block.insert("type".to_owned(), Value::String("text".to_owned()));
block.insert("text".to_owned(), Value::String(text.to_owned()));
Value::Object(block)
}
fn sanitize_block(block: &Value) -> Value {
let kind = match block {
Value::Object(map) => map.get("type"),
Value::Array(_) => None,
other => return other.clone(),
};
match kind.and_then(Value::as_str) {
Some("text" | "resource_link") => block.clone(),
Some("image") => text_block("[image content redacted - not supported by AgentCat]"),
Some("audio") => text_block("[audio content redacted - not supported by AgentCat]"),
Some("resource") => sanitize_resource(block),
_ => text_block(&format!(
"[unsupported content type \"{}\" redacted - not supported by AgentCat]",
wire::coerce_to_string(kind)
)),
}
}
fn sanitize_resource(block: &Value) -> Value {
match block.get("resource") {
Some(Value::Object(resource)) if resource.contains_key("blob") => {
text_block(BINARY_RESOURCE_NOTE)
}
_ => block.clone(),
}
}
fn sanitize_tool_values(value: &Value) -> Value {
match value {
Value::String(text) if is_binary(text) => Value::String(BINARY_DATA_NOTE.to_owned()),
Value::Array(items) => Value::Array(items.iter().map(sanitize_tool_values).collect()),
Value::Object(map) => Value::Object(
wire::own_entries(map)
.map(|(key, item)| (key.clone(), sanitize_tool_values(item)))
.collect(),
),
other => other.clone(),
}
}
fn is_binary(text: &str) -> bool {
if wire::utf16_len(text) < SIZE_GATE {
return false;
}
let body = text.trim_end_matches('=');
!body.is_empty()
&& body
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'\n' | b'\r'))
}