Skip to content

Commit 42cfe26

Browse files
echobtBounty Bot
andauthored
fix: batch fixes for issues #1979, 1980, 1981, 1983, 1985, 1987, 1988, 1989, 1991, 1993 [skip ci] (#370)
Fixes: - #1979: Add --pretty flag to format JSON responses in scrape command - #1980: Add XML formatting support with --pretty flag in scrape command - #1981: Add description length validation (max 1000 chars) in agent create - #1983: Improve panic hook to detect and show helpful messages for resource limit errors - #1985: Improve timeout error messages to clearly state the timeout duration - #1987: Validate header values don't exceed 8KB HTTP spec limit - #1988: Warn about duplicate headers in scrape command - #1989: Use safe_println! in mcp debug to handle broken pipe gracefully - #1991: Add --full flag to show full model IDs without truncation - #1993: Add --sort flag (id, name, provider) to models list command Co-authored-by: Bounty Bot <bounty-bot@factory.ai>
1 parent 46a1a12 commit 42cfe26

5 files changed

Lines changed: 388 additions & 68 deletions

File tree

cortex-cli/src/agent_cmd.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,18 @@ async fn run_create(args: CreateArgs) -> Result<()> {
12831283
)?
12841284
};
12851285

1286+
// Validate description length (Issue #1981)
1287+
// Maximum description length: 1000 characters (reasonable for display and storage)
1288+
const MAX_DESCRIPTION_LENGTH: usize = 1000;
1289+
if description.len() > MAX_DESCRIPTION_LENGTH {
1290+
bail!(
1291+
"Description exceeds maximum length of {} characters ({} provided). \
1292+
Please use a shorter description.",
1293+
MAX_DESCRIPTION_LENGTH,
1294+
description.len()
1295+
);
1296+
}
1297+
12861298
// Get mode
12871299
let mode = if let Some(ref m) = args.mode {
12881300
m.parse::<AgentMode>().map_err(|e| anyhow::anyhow!(e))?

cortex-cli/src/lib.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,41 @@ fn install_panic_hook() {
155155
// Restore terminal state before printing panic message
156156
restore_terminal();
157157

158+
// Extract panic message to check for resource limit issues (Issue #1983)
159+
let panic_message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
160+
s.to_string()
161+
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
162+
s.clone()
163+
} else {
164+
String::new()
165+
};
166+
167+
// Check for common resource limit error patterns
168+
let is_resource_limit = panic_message.contains("Resource temporarily unavailable")
169+
|| panic_message.contains("EAGAIN")
170+
|| panic_message.contains("Cannot allocate memory")
171+
|| panic_message.contains("ENOMEM")
172+
|| panic_message.contains("Too many open files")
173+
|| panic_message.contains("EMFILE")
174+
|| panic_message.contains("No space left on device")
175+
|| panic_message.contains("ENOSPC")
176+
|| panic_message.contains("cannot spawn")
177+
|| panic_message.contains("thread 'main' panicked")
178+
&& panic_message.contains("Os { code: 11");
179+
180+
if is_resource_limit {
181+
eprintln!("\nError: System resource limit reached.");
182+
eprintln!("This can happen when:");
183+
eprintln!(" - Process limit (ulimit -u) is too low");
184+
eprintln!(" - File descriptor limit (ulimit -n) is too low");
185+
eprintln!(" - System memory is exhausted");
186+
eprintln!("\nTry increasing limits:");
187+
eprintln!(" ulimit -u 4096 # Increase process limit");
188+
eprintln!(" ulimit -n 4096 # Increase file descriptor limit");
189+
eprintln!("\nOr run with fewer concurrent operations.");
190+
return;
191+
}
192+
158193
// Log the panic location for debugging
159194
if let Some(location) = panic_info.location() {
160195
eprintln!(

cortex-cli/src/mcp_cmd.rs

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ use cortex_common::CliConfigOverrides;
66
use cortex_engine::{config::find_cortex_home, create_default_client};
77
use std::io::{self, BufRead, Write};
88

9+
// ============================================================================
10+
// Safe Print Utilities (Issue #1989 - Handle broken pipe gracefully)
11+
// ============================================================================
12+
13+
/// Safely prints to stdout, ignoring broken pipe errors.
14+
/// This prevents crashes when output is piped to commands like `head` that close early.
15+
macro_rules! safe_println {
16+
() => {
17+
let _ = writeln!(std::io::stdout());
18+
};
19+
($($arg:tt)*) => {
20+
let _ = writeln!(std::io::stdout(), $($arg)*);
21+
};
22+
}
23+
24+
/// Safely prints to stdout without newline, ignoring broken pipe errors.
25+
macro_rules! safe_print {
26+
($($arg:tt)*) => {
27+
let _ = write!(std::io::stdout(), $($arg)*);
28+
};
29+
}
30+
931
// ============================================================================
1032
// Input Validation Constants and Utilities
1133
// ============================================================================
@@ -1397,14 +1419,15 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
13971419

13981420
let mut errors: Vec<String> = Vec::new();
13991421

1422+
// Use safe_println! to avoid SIGPIPE crashes when output is piped (Issue #1989)
14001423
if !json {
1401-
println!("Debugging MCP Server: {name}");
1402-
println!("{}", "=".repeat(50));
1403-
println!("Checked at: {} (fresh)", check_timestamp);
1404-
println!();
1405-
println!("Configuration:");
1406-
println!(" Enabled: {enabled}");
1407-
println!(" Transport: {transport_type}");
1424+
safe_println!("Debugging MCP Server: {name}");
1425+
safe_println!("{}", "=".repeat(50));
1426+
safe_println!("Checked at: {} (fresh)", check_timestamp);
1427+
safe_println!();
1428+
safe_println!("Configuration:");
1429+
safe_println!(" Enabled: {enabled}");
1430+
safe_println!(" Transport: {transport_type}");
14081431
}
14091432

14101433
// Transport-specific info
@@ -1429,9 +1452,9 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
14291452
debug_result["args"] = serde_json::json!(args);
14301453

14311454
if !json {
1432-
println!(" Command: {cmd}");
1455+
safe_println!(" Command: {cmd}");
14331456
if !args.is_empty() {
1434-
println!(" Args: {args}");
1457+
safe_println!(" Args: {args}");
14351458
}
14361459
}
14371460

@@ -1445,20 +1468,20 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
14451468
debug_result["command_exists"] = serde_json::json!(cmd_exists);
14461469

14471470
if !json {
1448-
println!();
1449-
println!("Command Check:");
1471+
safe_println!();
1472+
safe_println!("Command Check:");
14501473
if cmd_exists {
1451-
println!(" ✓ Command '{cmd}' found in PATH");
1474+
safe_println!(" ✓ Command '{cmd}' found in PATH");
14521475
} else {
1453-
println!(" ✗ Command '{cmd}' not found in PATH");
1476+
safe_println!(" ✗ Command '{cmd}' not found in PATH");
14541477
errors.push(format!("Command '{}' not found in PATH", cmd));
14551478
}
14561479
}
14571480

14581481
// Try to connect and get capabilities
14591482
if !json {
1460-
println!();
1461-
println!("Connection Test:");
1483+
safe_println!();
1484+
safe_println!("Connection Test:");
14621485
}
14631486

14641487
match test_stdio_connection(&name, cmd, &args, timeout).await {
@@ -1472,18 +1495,18 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
14721495
debug_result["prompts"] = info.prompts.clone();
14731496

14741497
if !json {
1475-
println!(" ✓ Connected successfully");
1476-
println!();
1477-
println!("Server Capabilities:");
1498+
safe_println!(" ✓ Connected successfully");
1499+
safe_println!();
1500+
safe_println!("Server Capabilities:");
14781501
if let Some(caps) = info.capabilities.as_object() {
14791502
for (key, value) in caps {
1480-
println!(" • {key}: {value}");
1503+
safe_println!(" • {key}: {value}");
14811504
}
14821505
} else {
1483-
println!(" (none reported)");
1506+
safe_println!(" (none reported)");
14841507
}
1485-
println!();
1486-
println!(
1508+
safe_println!();
1509+
safe_println!(
14871510
"Available Tools: {}",
14881511
info.tools.as_array().map(|a| a.len()).unwrap_or(0)
14891512
);
@@ -1499,18 +1522,18 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
14991522
} else {
15001523
desc.to_string()
15011524
};
1502-
println!(" • {name}: {desc_short}");
1525+
safe_println!(" • {name}: {desc_short}");
15031526
}
15041527
}
15051528
if tools.len() > 10 {
1506-
println!(" ... and {} more", tools.len() - 10);
1529+
safe_println!(" ... and {} more", tools.len() - 10);
15071530
}
15081531
}
1509-
println!();
1532+
safe_println!();
15101533
let resources_count =
15111534
info.resources.as_array().map(|a| a.len()).unwrap_or(0);
15121535
let prompts_count = info.prompts.as_array().map(|a| a.len()).unwrap_or(0);
1513-
println!(
1536+
safe_println!(
15141537
"Available Resources: {}{}",
15151538
resources_count,
15161539
if resources_count == 0 {
@@ -1519,7 +1542,7 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
15191542
""
15201543
}
15211544
);
1522-
println!(
1545+
safe_println!(
15231546
"Available Prompts: {}{}",
15241547
prompts_count,
15251548
if prompts_count == 0 {
@@ -1539,7 +1562,7 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
15391562
errors.push(error_msg);
15401563

15411564
if !json {
1542-
println!(" ✗ Connection failed: {e}");
1565+
safe_println!(" ✗ Connection failed: {e}");
15431566
}
15441567
}
15451568
}
@@ -1550,13 +1573,13 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
15501573
debug_result["url"] = serde_json::json!(url);
15511574

15521575
if !json {
1553-
println!(" URL: {url}");
1576+
safe_println!(" URL: {url}");
15541577
}
15551578

15561579
// Test HTTP connection
15571580
if !json {
1558-
println!();
1559-
println!("Connection Test:");
1581+
safe_println!();
1582+
safe_println!("Connection Test:");
15601583
}
15611584

15621585
match test_http_connection(&name, url, timeout).await {
@@ -1570,18 +1593,18 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
15701593
debug_result["prompts"] = info.prompts.clone();
15711594

15721595
if !json {
1573-
println!(" ✓ Connected successfully");
1574-
println!();
1575-
println!("Server Capabilities:");
1596+
safe_println!(" ✓ Connected successfully");
1597+
safe_println!();
1598+
safe_println!("Server Capabilities:");
15761599
if let Some(caps) = info.capabilities.as_object() {
15771600
for (key, value) in caps {
1578-
println!(" • {key}: {value}");
1601+
safe_println!(" • {key}: {value}");
15791602
}
15801603
} else {
1581-
println!(" (none reported)");
1604+
safe_println!(" (none reported)");
15821605
}
1583-
println!();
1584-
println!(
1606+
safe_println!();
1607+
safe_println!(
15851608
"Available Tools: {}",
15861609
info.tools.as_array().map(|a| a.len()).unwrap_or(0)
15871610
);
@@ -1597,18 +1620,18 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
15971620
} else {
15981621
desc.to_string()
15991622
};
1600-
println!(" • {name}: {desc_short}");
1623+
safe_println!(" • {name}: {desc_short}");
16011624
}
16021625
}
16031626
if tools.len() > 10 {
1604-
println!(" ... and {} more", tools.len() - 10);
1627+
safe_println!(" ... and {} more", tools.len() - 10);
16051628
}
16061629
}
1607-
println!();
1630+
safe_println!();
16081631
let resources_count =
16091632
info.resources.as_array().map(|a| a.len()).unwrap_or(0);
16101633
let prompts_count = info.prompts.as_array().map(|a| a.len()).unwrap_or(0);
1611-
println!(
1634+
safe_println!(
16121635
"Available Resources: {}{}",
16131636
resources_count,
16141637
if resources_count == 0 {
@@ -1617,7 +1640,7 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
16171640
""
16181641
}
16191642
);
1620-
println!(
1643+
safe_println!(
16211644
"Available Prompts: {}{}",
16221645
prompts_count,
16231646
if prompts_count == 0 {
@@ -1637,16 +1660,16 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
16371660
errors.push(error_msg);
16381661

16391662
if !json {
1640-
println!(" ✗ Connection failed: {e}");
1663+
safe_println!(" ✗ Connection failed: {e}");
16411664
}
16421665
}
16431666
}
16441667

16451668
// Check OAuth status
16461669
if test_auth {
16471670
if !json {
1648-
println!();
1649-
println!("OAuth Status:");
1671+
safe_println!();
1672+
safe_println!("OAuth Status:");
16501673
}
16511674

16521675
let auth_status = get_auth_status_for_display(&name, url)
@@ -1655,18 +1678,18 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
16551678
debug_result["auth_status"] = serde_json::json!(auth_status);
16561679

16571680
if !json {
1658-
println!(" {auth_status}");
1681+
safe_println!(" {auth_status}");
16591682

16601683
if auth_status == "Not Authenticated" {
1661-
println!("\n Use 'cortex mcp auth {name}' to authenticate.");
1684+
safe_println!("\n Use 'cortex mcp auth {name}' to authenticate.");
16621685
}
16631686
}
16641687
}
16651688
}
16661689
_ => {
16671690
errors.push(format!("Unknown transport type: {}", transport_type));
16681691
if !json {
1669-
println!(" ✗ Unknown transport type: {transport_type}");
1692+
safe_println!(" ✗ Unknown transport type: {transport_type}");
16701693
}
16711694
}
16721695
}
@@ -1678,12 +1701,12 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
16781701

16791702
if json {
16801703
let output = serde_json::to_string_pretty(&debug_result)?;
1681-
println!("{output}");
1704+
safe_println!("{output}");
16821705
} else if !errors.is_empty() {
1683-
println!();
1684-
println!("Errors:");
1706+
safe_println!();
1707+
safe_println!("Errors:");
16851708
for err in &errors {
1686-
println!(" ✗ {err}");
1709+
safe_println!(" ✗ {err}");
16871710
}
16881711
}
16891712

0 commit comments

Comments
 (0)