use serde_json::Value;
pub const SENSITIVE_KEY_PARTS: &[&str] = &[
"secret",
"password",
"passphrase",
"passwd",
"api_key",
"apikey",
"private_key",
"privatekey",
"credential",
"access_token",
"accesstoken",
"refresh_token",
"refreshtoken",
"id_token",
"idtoken",
"auth_token",
"authtoken",
"authorization",
"cookie",
"proxy_auth",
];
pub const DISPLAY_ONLY_KEY_PARTS: &[&str] = &["passcode", "community"];
pub const REDACTED: &str = "<redacted>";
pub fn is_sensitive_key(key: &str) -> bool {
let key = key.to_ascii_lowercase().replace('-', "_");
key == "token"
|| key.ends_with("_token")
|| SENSITIVE_KEY_PARTS.iter().any(|part| key.contains(part))
}
pub fn is_sensitive_entry(key: &str, value: &Value) -> bool {
if value.is_null() {
return false;
}
if is_sensitive_key(key) {
return true;
}
let key = key.to_ascii_lowercase();
key == "auth" && value.is_string()
}
pub fn is_shown_redacted(key: &str, value: &Value) -> bool {
if value.is_null() {
return false;
}
if is_sensitive_entry(key, value) {
return true;
}
let key = key.to_ascii_lowercase();
DISPLAY_ONLY_KEY_PARTS.iter().any(|part| key.contains(part))
}
pub fn actions_have_credentials(actions: &[crate::llm::actions::ActionDefinition]) -> bool {
actions
.iter()
.any(|action| action.parameters.iter().any(|p| is_sensitive_key(&p.name)))
}
pub fn contains_credentials(value: &Value) -> bool {
const MAX_NODES: usize = 4096;
const MAX_DEPTH: usize = 64;
const MAX_BYTES: usize = 256 * 1024;
let mut pending = vec![(value, 0usize)];
let mut nodes = 0usize;
let mut bytes = 0usize;
while let Some((value, depth)) = pending.pop() {
nodes += 1;
bytes = bytes.saturating_add(std::mem::size_of::<Value>());
if nodes > MAX_NODES || depth > MAX_DEPTH || bytes > MAX_BYTES {
return true;
}
match value {
Value::String(s) => bytes = bytes.saturating_add(s.len()),
Value::Array(values) => {
if values.len() > MAX_NODES.saturating_sub(nodes + pending.len()) {
return true;
}
pending.extend(values.iter().map(|value| (value, depth + 1)));
}
Value::Object(values) => {
if values.len() > MAX_NODES.saturating_sub(nodes + pending.len()) {
return true;
}
for (key, value) in values {
bytes = bytes.saturating_add(key.len());
if bytes > MAX_BYTES {
return true;
}
if is_sensitive_entry(key, value) {
return true;
}
pending.push((value, depth + 1));
}
}
_ => {}
}
if bytes > MAX_BYTES {
return true;
}
}
false
}
pub fn hide_error_details(error: anyhow::Error) -> anyhow::Error {
if let Some(category) = error.downcast_ref::<crate::llm::rate_limiter::RateLimitError>() {
anyhow::Error::new(*category)
} else {
anyhow::anyhow!("credential-bearing request failed; diagnostics hidden")
}
}
pub const MAX_REDACT_DEPTH: usize = 64;
pub fn redact_sensitive(value: &Value) -> Value {
redact_at(value, 0)
}
fn redact_at(value: &Value, depth: usize) -> Value {
if depth > MAX_REDACT_DEPTH {
return Value::String(REDACTED.to_string());
}
match value {
Value::Object(map) => Value::Object(
map.iter()
.map(|(k, v)| {
let shown = if is_shown_redacted(k, v) {
Value::String(REDACTED.to_string())
} else {
redact_at(v, depth + 1)
};
(k.clone(), shown)
})
.collect(),
),
Value::Array(items) => {
Value::Array(items.iter().map(|v| redact_at(v, depth + 1)).collect())
}
other => other.clone(),
}
}