use std::sync::OnceLock;
use regex::Regex;
#[allow(dead_code)]
pub const MAX_CONTENT_LENGTH: usize = 50_000;
pub const MAX_RESULT_LENGTH: usize = 10_000;
const SKIP_RESULT_TOOLS: &[&str] = &["WebSearch"];
fn secret_patterns() -> &'static [Regex] {
static PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();
PATTERNS.get_or_init(|| {
[
r#"(?i)['"]?(?:api[-_]?key|apikey|token|secret|password|passwd|pwd)['"]?\s*[:=]\s*['"]?[^\s'"]+['"]?"#,
r#"(?:AWS|aws)[-_]?(?:SECRET|secret)[-_]?(?:ACCESS|access)[-_]?(?:KEY|key)\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}['"]?"#,
r"\b[A-Za-z0-9]{32,}\b",
r"(?i)Bearer\s+[A-Za-z0-9\-._~+/]+=*",
r"(?i)Basic\s+[A-Za-z0-9+/]+=*",
r"sk-ant-[A-Za-z0-9\-_]{90,}",
r"sk-[A-Za-z0-9]{48,}",
]
.iter()
.map(|pattern| Regex::new(pattern).expect("secret pattern compiles"))
.collect()
})
}
const GENERIC_TOKEN_IDX: usize = 2;
fn generic_token_qualifies(s: &str) -> bool {
s.bytes().any(|b| b.is_ascii_digit()) && s.bytes().any(|b| b.is_ascii_alphabetic())
}
pub fn contains_secrets(content: &str) -> bool {
for (idx, pattern) in secret_patterns().iter().enumerate() {
if idx == GENERIC_TOKEN_IDX {
if pattern
.find_iter(content)
.any(|m| generic_token_qualifies(m.as_str()))
{
return true;
}
} else if pattern.is_match(content) {
return true;
}
}
false
}
pub fn mask_secrets(content: &str) -> String {
let mut masked = content.to_string();
for (idx, pattern) in secret_patterns().iter().enumerate() {
masked = pattern
.replace_all(&masked, |caps: ®ex::Captures<'_>| {
let matched = &caps[0];
if idx == GENERIC_TOKEN_IDX && !generic_token_qualifies(matched) {
return matched.to_string();
}
match matched.find([':', '=']) {
Some(sep) => format!("{}=[REDACTED]", &matched[..sep]),
None => "[REDACTED]".to_string(),
}
})
.into_owned();
}
masked
}
pub fn truncate_content(content: &str, max_length: usize) -> String {
let (prefix, total_units) = utf16_prefix(content, max_length);
if total_units <= max_length {
return content.to_string();
}
let remaining = total_units - max_length;
format!("{prefix}\n\n[Truncated {remaining} characters]")
}
pub fn filter_content(content: &str, max_length: usize) -> String {
let filtered = if contains_secrets(content) {
mask_secrets(content)
} else {
content.to_string()
};
truncate_content(&filtered, max_length)
}
pub fn should_capture_tool_result(tool_name: &str) -> bool {
!SKIP_RESULT_TOOLS.contains(&tool_name)
}
pub fn filter_tool_result(
tool_name: &str,
result: Option<&serde_json::Value>,
max_length: usize,
) -> Option<String> {
if !should_capture_tool_result(tool_name) {
return Some("[Result not captured]".to_string());
}
let result = match result {
Some(serde_json::Value::Null) | None => return None,
Some(v) => v,
};
let result_str = match result {
serde_json::Value::String(s) => s.clone(),
other => serde_json::to_string_pretty(other).unwrap_or_else(|_| other.to_string()),
};
Some(filter_content(&result_str, max_length))
}
fn utf16_prefix(content: &str, max_units: usize) -> (&str, usize) {
let mut units = 0;
for (byte_idx, ch) in content.char_indices() {
let ch_units = ch.len_utf16();
if units + ch_units > max_units {
let total = units
+ content[byte_idx..]
.chars()
.map(char::len_utf16)
.sum::<usize>();
return (&content[..byte_idx], total);
}
units += ch_units;
}
(content, units)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn short_content_unchanged() {
assert_eq!(filter_content("hello", 50_000), "hello");
}
#[test]
fn truncation_uses_adapter_notice() {
let content = "x".repeat(15);
assert_eq!(
truncate_content(&content, 10),
format!("{}\n\n[Truncated 5 characters]", "x".repeat(10))
);
}
#[test]
fn utf16_length_like_javascript() {
let content = "🦀🦀🦀";
assert_eq!(truncate_content(content, 6), content);
assert_eq!(
truncate_content(content, 4),
"🦀🦀\n\n[Truncated 2 characters]"
);
}
#[test]
fn bash_string_result_passes_through() {
assert_eq!(
filter_tool_result("Bash", Some(&json!("output line")), MAX_RESULT_LENGTH),
Some("output line".to_string())
);
}
#[test]
fn bash_object_result_is_pretty_json() {
let r = json!({ "exitCode": 0 });
assert_eq!(
filter_tool_result("Bash", Some(&r), MAX_RESULT_LENGTH),
Some("{\n \"exitCode\": 0\n}".to_string())
);
}
#[test]
fn websearch_result_not_captured() {
assert_eq!(
filter_tool_result("WebSearch", Some(&json!("anything")), MAX_RESULT_LENGTH),
Some("[Result not captured]".to_string())
);
}
#[test]
fn null_result_is_none() {
assert_eq!(
filter_tool_result("Bash", Some(&serde_json::Value::Null), MAX_RESULT_LENGTH),
None
);
assert_eq!(filter_tool_result("Bash", None, MAX_RESULT_LENGTH), None);
}
#[test]
fn secret_assignment_is_masked() {
let masked = filter_content("api_key=supersecretvalue123", 50_000);
assert!(masked.contains("[REDACTED]"), "got {masked}");
assert!(!masked.contains("supersecretvalue123"), "got {masked}");
}
}