Skip to content

Commit b879145

Browse files
committed
Merge PR #255: fix: Batch fixes for issues #2319-#2328
2 parents 30beef7 + 628f809 commit b879145

10 files changed

Lines changed: 431 additions & 99 deletions

File tree

cortex-app-server/src/error.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ pub struct ErrorDetail {
138138

139139
impl IntoResponse for AppError {
140140
fn into_response(self) -> Response {
141+
use axum::http::header;
142+
141143
let status = self.status_code();
144+
let is_rate_limited = matches!(self, AppError::RateLimitExceeded);
145+
142146
let body = ErrorResponse {
143147
error: ErrorDetail {
144148
code: self.error_code().to_string(),
@@ -148,7 +152,18 @@ impl IntoResponse for AppError {
148152
},
149153
};
150154

151-
(status, Json(body)).into_response()
155+
let mut response = (status, Json(body)).into_response();
156+
157+
// Add Retry-After header for rate limit responses (429)
158+
// This helps clients implement proper backoff strategies
159+
if is_rate_limited {
160+
response.headers_mut().insert(
161+
header::RETRY_AFTER,
162+
"60".parse().unwrap(), // Suggest retry after 60 seconds
163+
);
164+
}
165+
166+
response
152167
}
153168
}
154169

cortex-cli/src/agent_cmd.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,69 @@ impl AgentCli {
312312
}
313313
}
314314

315+
/// Validate a model name for agent creation.
316+
///
317+
/// Returns the validated model name, resolving aliases if needed.
318+
/// Checks that the model is in valid format (provider/model or known alias).
319+
fn validate_model_name(model: &str) -> Result<String> {
320+
use cortex_common::resolve_model_alias;
321+
322+
// First, resolve any alias (e.g., "sonnet" -> "anthropic/claude-sonnet-4-20250514")
323+
let resolved = resolve_model_alias(model);
324+
325+
// Check if model is in provider/model format or just model name
326+
// Valid formats:
327+
// - "provider/model" (e.g., "anthropic/claude-sonnet-4-20250514")
328+
// - Known model name (e.g., "gpt-4o", "claude-sonnet-4-20250514")
329+
// - Known alias (e.g., "sonnet", "opus")
330+
331+
// If the model contains a '/', it should be in provider/model format
332+
if resolved.contains('/') {
333+
let parts: Vec<&str> = resolved.splitn(2, '/').collect();
334+
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
335+
bail!(
336+
"Invalid model format: '{}'. Expected 'provider/model' format.\n\
337+
Examples: anthropic/claude-sonnet-4-20250514, openai/gpt-4o\n\
338+
Run 'cortex models list' to see available models.",
339+
model
340+
);
341+
}
342+
// Validate provider is a known provider
343+
let valid_providers = [
344+
"anthropic",
345+
"openai",
346+
"google",
347+
"mistral",
348+
"xai",
349+
"deepseek",
350+
"ollama",
351+
];
352+
let provider = parts[0].to_lowercase();
353+
if !valid_providers.contains(&provider.as_str()) {
354+
eprintln!(
355+
"Warning: Unknown provider '{}'. Known providers: {}",
356+
provider,
357+
valid_providers.join(", ")
358+
);
359+
}
360+
} else {
361+
// Model name without provider - check if it looks valid
362+
// Model names typically contain alphanumeric chars, hyphens, dots, and colons
363+
let valid_chars = resolved
364+
.chars()
365+
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == ':');
366+
if !valid_chars || resolved.is_empty() {
367+
bail!(
368+
"Invalid model name: '{}'. Model names should contain only alphanumeric characters, hyphens, underscores, dots, and colons.\n\
369+
Run 'cortex models list' to see available models.",
370+
model
371+
);
372+
}
373+
}
374+
375+
Ok(resolved.to_string())
376+
}
377+
315378
/// Get the agents directory path.
316379
fn get_agents_dir() -> Result<PathBuf> {
317380
let cortex_home = dirs::home_dir()
@@ -1539,19 +1602,23 @@ async fn run_generate(args: CreateArgs) -> Result<()> {
15391602
};
15401603

15411604
// Validate model argument
1542-
if args.model.trim().is_empty() {
1605+
let model_arg = args.model.trim();
1606+
if model_arg.is_empty() {
15431607
bail!("Error: Model name cannot be empty");
15441608
}
15451609

1610+
// Validate the model name format and existence
1611+
let valid_model = validate_model_name(model_arg)?;
1612+
15461613
if !fully_automated {
15471614
println!();
15481615
println!("Generating agent configuration...");
1549-
println!(" Using model: {}", args.model);
1616+
println!(" Using model: {}", valid_model);
15501617
println!();
15511618
}
15521619

15531620
// Create generator and generate
1554-
let generator = AgentGenerator::new().with_model(&args.model);
1621+
let generator = AgentGenerator::new().with_model(&valid_model);
15551622

15561623
let generated = generator
15571624
.generate(&description)

cortex-cli/src/debug_cmd.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,10 @@ pub struct PathsArgs {
14751475
/// Output as JSON.
14761476
#[arg(long)]
14771477
pub json: bool,
1478+
1479+
/// Check if write locations are accessible (useful for Docker read-only containers).
1480+
#[arg(long)]
1481+
pub check_writable: bool,
14781482
}
14791483

14801484
/// Paths debug output.
@@ -1541,6 +1545,49 @@ fn dir_size(path: &PathBuf) -> Result<u64> {
15411545
async fn run_paths(args: PathsArgs) -> Result<()> {
15421546
let cortex_home = get_cortex_home();
15431547

1548+
// Handle --check-writable flag for Docker read-only container validation
1549+
if args.check_writable {
1550+
if let Some(warning) = cortex_engine::file_utils::check_write_permissions(&cortex_home) {
1551+
if args.json {
1552+
let statuses = cortex_engine::file_utils::validate_write_locations(&cortex_home);
1553+
let output: Vec<_> = statuses
1554+
.iter()
1555+
.map(|s| {
1556+
serde_json::json!({
1557+
"path": s.path,
1558+
"description": s.description,
1559+
"is_writable": s.is_writable,
1560+
"error": s.error,
1561+
})
1562+
})
1563+
.collect();
1564+
println!("{}", serde_json::to_string_pretty(&output)?);
1565+
} else {
1566+
eprintln!("{warning}");
1567+
}
1568+
std::process::exit(1);
1569+
} else {
1570+
if args.json {
1571+
let statuses = cortex_engine::file_utils::validate_write_locations(&cortex_home);
1572+
let output: Vec<_> = statuses
1573+
.iter()
1574+
.map(|s| {
1575+
serde_json::json!({
1576+
"path": s.path,
1577+
"description": s.description,
1578+
"is_writable": s.is_writable,
1579+
"error": s.error,
1580+
})
1581+
})
1582+
.collect();
1583+
println!("{}", serde_json::to_string_pretty(&output)?);
1584+
} else {
1585+
println!("✓ All write locations are accessible.");
1586+
}
1587+
return Ok(());
1588+
}
1589+
}
1590+
15441591
let output = PathsDebugOutput {
15451592
cortex_home: PathInfo::new(cortex_home.clone()),
15461593
config_dir: PathInfo::new(cortex_home.clone()),

cortex-cli/src/mcp_cmd.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ pub struct DebugArgs {
442442
/// Timeout in seconds for connection test.
443443
#[arg(long, default_value = "30")]
444444
pub timeout: u64,
445+
446+
/// Force fresh health check, bypassing any cache.
447+
#[arg(long)]
448+
pub no_cache: bool,
445449
}
446450

447451
impl McpCli {
@@ -1293,8 +1297,13 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
12931297
json,
12941298
test_auth,
12951299
timeout,
1300+
no_cache,
12961301
} = args;
12971302

1303+
// Note: no_cache flag ensures fresh health check
1304+
// When implemented with a health cache, this would clear/bypass it
1305+
let _ = no_cache; // Currently all debug checks are fresh, flag reserved for future use
1306+
12981307
validate_server_name(&name)?;
12991308

13001309
let server = get_mcp_server(&name)?
@@ -1314,6 +1323,9 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
13141323
.and_then(|v| v.as_str())
13151324
.unwrap_or("unknown");
13161325

1326+
// Record check timestamp to indicate freshness of results
1327+
let check_timestamp = chrono::Utc::now().to_rfc3339();
1328+
13171329
let mut debug_result = serde_json::json!({
13181330
"name": name,
13191331
"enabled": enabled,
@@ -1327,13 +1339,16 @@ async fn run_debug(args: DebugArgs) -> Result<()> {
13271339
"resources": [],
13281340
"prompts": [],
13291341
"auth_status": null,
1342+
"errors": [],
1343+
"checked_at": check_timestamp,
13301344
});
13311345

13321346
let mut errors: Vec<String> = Vec::new();
13331347

13341348
if !json {
13351349
println!("Debugging MCP Server: {name}");
13361350
println!("{}", "=".repeat(50));
1351+
println!("Checked at: {} (fresh)", check_timestamp);
13371352
println!();
13381353
println!("Configuration:");
13391354
println!(" Enabled: {enabled}");

0 commit comments

Comments
 (0)