Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions src/cli/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,12 +568,7 @@ async fn test_server(name: String, user_id: String) -> anyhow::Result<()> {
};
println!(" • {}{}", tool.name, approval);
if !tool.description.is_empty() {
// Truncate long descriptions
let desc = if tool.description.len() > 60 {
format!("{}...", &tool.description[..57])
} else {
tool.description.clone()
};
let desc = truncate_description(&tool.description);
println!(" {}", desc);
}
}
Expand Down Expand Up @@ -681,6 +676,16 @@ async fn get_secrets_store() -> anyhow::Result<Arc<dyn SecretsStore + Send + Syn
crate::cli::init_secrets_store().await
}

/// Truncate a description to at most 57 display chars, appending "..." if needed.
/// Uses char-safe boundary to avoid panicking on multi-byte UTF-8.
fn truncate_description(s: &str) -> String {
if s.len() <= 60 {
return s.to_string();
}
let end = crate::util::floor_char_boundary(s, 57);
format!("{}...", &s[..end])
}
Comment on lines +679 to +687

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation should use character-aware truncation to avoid panics. In Rust, s.len() returns byte length, and slicing at an arbitrary index can fail if it's not a character boundary. When truncating, walk backwards from the desired length until a valid character boundary is found using is_char_boundary. Also, ensure tool outputs are truncated for previews to prevent excessive resource usage. Magic numbers are kept inline to maintain readability as per repository guidelines.

Suggested change
/// Truncate a description to at most 57 display chars, appending "..." if needed.
/// Uses char-safe boundary to avoid panicking on multi-byte UTF-8.
fn truncate_description(s: &str) -> String {
if s.len() <= 60 {
return s.to_string();
}
let end = crate::util::floor_char_boundary(s, 57);
format!("{}...", &s[..end])
}
fn truncate_description(s: &str) -> String {
if s.len() <= 60 {
return s.to_string();
}
let mut end = 57;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &s[..end])
}
References
  1. When truncating a UTF-8 string at a byte boundary, walk backwards from the desired length until a valid character boundary is found using is_char_boundary to prevent panics.
  2. Always truncate tool output for previews or status updates to a reasonable maximum length.
  3. Avoid extracting magic numbers into constants if the values are self-documenting in context and creating many constants would reduce readability.


#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -741,4 +746,47 @@ mod tests {
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid env var format"));
}

#[test]
fn test_truncate_description_ascii() {
let short = "short description";
assert_eq!(truncate_description(short), short);

let exactly_60 = "a".repeat(60);
assert_eq!(truncate_description(&exactly_60), exactly_60);

let long = "a".repeat(80);
let truncated = truncate_description(&long);
assert!(truncated.ends_with("..."));
assert!(truncated.len() <= 60);
}

#[test]
fn test_truncate_description_cjk_no_panic() {
// CJK chars are 3 bytes each; 20 chars = 60 bytes
let cjk = "这是一个很长的工具描述用来测试多字节字符截断是否会导致恐慌问题的文本";
assert!(cjk.len() > 60);
let truncated = truncate_description(cjk);
assert!(truncated.ends_with("..."));
// Must be valid UTF-8 (no panic, no split char)
assert!(truncated.is_char_boundary(truncated.len()));
}

#[test]
fn test_truncate_description_emoji_no_panic() {
// Emoji are 4 bytes each; 16 emojis = 64 bytes
let emoji = "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥";
assert!(emoji.len() > 60);
let truncated = truncate_description(emoji);
assert!(truncated.ends_with("..."));
}

#[test]
fn test_truncate_description_mixed_boundary() {
// ASCII + CJK boundary at exactly byte 57
let mixed = format!("{}{}", "a".repeat(56), "描述很长的文本需要截断");
assert!(mixed.len() > 60);
let truncated = truncate_description(&mixed);
assert!(truncated.ends_with("..."));
}
}
5 changes: 4 additions & 1 deletion src/config/channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ impl ChannelsConfig {
if scope.len() > 128 {
return Err(ConfigError::InvalidValue {
key: "WORKSPACE_READ_SCOPES".to_string(),
message: format!("scope '{}...' exceeds 128 characters", &scope[..32]),
message: format!(
"scope '{}...' exceeds 128 characters",
&scope[..crate::util::floor_char_boundary(scope, 32)]
),
});
}
}
Expand Down
Loading