ai_agent/bridge/
debug_utils.rs1use once_cell::sync::Lazy;
6use regex::Regex;
7use serde_json::Value;
8
9const DEBUG_MSG_LIMIT: usize = 2000;
14
15static SECRET_FIELD_NAMES: [&str; 5] = [
16 "session_ingress_token",
17 "environment_secret",
18 "access_token",
19 "secret",
20 "token",
21];
22
23static SECRET_PATTERN: Lazy<Regex> = Lazy::new(|| {
24 let pattern = format!(r#""({})"\s*:\s*"([^"]*)""#, SECRET_FIELD_NAMES.join("|"));
25 Regex::new(&pattern).unwrap()
26});
27
28const REDACT_MIN_LENGTH: usize = 16;
29
30pub fn redact_secrets(s: &str) -> String {
36 SECRET_PATTERN
37 .replace_all(s, |caps: ®ex::Captures| {
38 let field = caps.get(1).map(|m| m.as_str()).unwrap_or("");
39 let value = caps.get(2).map(|m| m.as_str()).unwrap_or("");
40 if value.len() < REDACT_MIN_LENGTH {
41 return format!(r#""{}":"[REDACTED]""#, field);
42 }
43 let redacted = format!("{}...{}", &value[..8], &value[value.len() - 4..]);
44 format!(r#""{}":"{}""#, field, redacted)
45 })
46 .to_string()
47}
48
49pub fn debug_truncate(s: &str) -> String {
51 let flat = s.replace('\n', "\\n");
52 if flat.len() <= DEBUG_MSG_LIMIT {
53 return flat;
54 }
55 format!("{}... ({} chars)", &flat[..DEBUG_MSG_LIMIT], flat.len())
56}
57
58pub fn debug_body(data: &str) -> String {
60 let raw = if let Ok(parsed) = serde_json::from_str::<Value>(data) {
61 serde_json::to_string(&parsed).unwrap_or_else(|_| data.to_string())
62 } else {
63 data.to_string()
64 };
65 let s = redact_secrets(&raw);
66 if s.len() <= DEBUG_MSG_LIMIT {
67 return s;
68 }
69 format!("{}... ({} chars)", &s[..DEBUG_MSG_LIMIT], s.len())
70}
71
72fn error_message(err: &dyn std::error::Error) -> String {
78 err.to_string()
79}
80
81pub fn describe_axios_error(err: &serde_json::Value) -> String {
84 let msg = if let Some(err_str) = err.get("message").and_then(|v| v.as_str()) {
85 err_str.to_string()
86 } else {
87 "Unknown error".to_string()
88 };
89
90 if let Some(response) = err.get("response").and_then(|v| v.as_object()) {
91 if let Some(data) = response.get("data").and_then(|v| v.as_object()) {
92 let detail = data.get("message").and_then(|v| v.as_str()).or_else(|| {
93 data.get("error")
94 .and_then(|v| v.get("message"))
95 .and_then(|v| v.as_str())
96 });
97
98 if let Some(detail) = detail {
99 return format!("{}: {}", msg, detail);
100 }
101 }
102 }
103 msg
104}
105
106pub fn extract_http_status(err: &serde_json::Value) -> Option<u16> {
109 let response = err.get("response")?;
110 let status = response.get("status")?;
111 status.as_u64().map(|v| v as u16)
112}
113
114pub fn extract_error_detail(data: &serde_json::Value) -> Option<String> {
117 if let Some(msg) = data.get("message").and_then(|v| v.as_str()) {
118 return Some(msg.to_string());
119 }
120 if let Some(error) = data.get("error").and_then(|v| v.as_object()) {
121 if let Some(msg) = error.get("message").and_then(|v| v.as_str()) {
122 return Some(msg.to_string());
123 }
124 }
125 None
126}
127
128pub fn log_bridge_skip(reason: &str, debug_msg: Option<&str>, v2: Option<bool>) {
135 if let Some(msg) = debug_msg {
136 eprintln!("[bridge:debug] {}", msg);
137 }
138 let mut event = serde_json::json!({ "reason": reason });
140 if let Some(v2_val) = v2 {
141 event["v2"] = serde_json::json!(v2_val);
142 }
143 eprintln!("[bridge:analytics] tengu_bridge_repl_skipped: {}", event);
144}