Skip to content

Commit 41f2411

Browse files
committed
Merge PR #268: fix: batch fixes for issues #2469-#2486
2 parents 2e4874c + 4c6a0ea commit 41f2411

9 files changed

Lines changed: 421 additions & 59 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cortex-app-server/src/config.rs

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,16 @@ pub struct ServerConfig {
4848
#[serde(default = "default_max_body_size")]
4949
pub max_body_size: usize,
5050

51-
/// Request timeout in seconds.
51+
/// Request timeout in seconds (applies to full request lifecycle).
5252
#[serde(default = "default_request_timeout")]
5353
pub request_timeout: u64,
5454

55+
/// Read timeout for individual chunks in seconds.
56+
/// Applies to chunked transfer encoding to prevent indefinite hangs
57+
/// when clients disconnect without sending the terminal chunk.
58+
#[serde(default = "default_read_timeout")]
59+
pub read_timeout: u64,
60+
5561
/// Enable metrics endpoint.
5662
#[serde(default = "default_true")]
5763
pub metrics_enabled: bool,
@@ -63,16 +69,6 @@ pub struct ServerConfig {
6369
/// CORS origins (empty = allow all).
6470
#[serde(default)]
6571
pub cors_origins: Vec<String>,
66-
67-
/// Graceful shutdown timeout in seconds.
68-
/// When the server receives SIGTERM, it will wait this long for in-flight
69-
/// requests to complete before forcefully terminating.
70-
#[serde(default = "default_shutdown_timeout")]
71-
pub shutdown_timeout: u64,
72-
}
73-
74-
fn default_shutdown_timeout() -> u64 {
75-
30 // 30 seconds
7672
}
7773

7874
fn default_listen_addr() -> String {
@@ -87,6 +83,10 @@ fn default_request_timeout() -> u64 {
8783
300 // 5 minutes
8884
}
8985

86+
fn default_read_timeout() -> u64 {
87+
30 // 30 seconds for individual chunk reads
88+
}
89+
9090
fn default_true() -> bool {
9191
true
9292
}
@@ -105,10 +105,10 @@ impl Default for ServerConfig {
105105
mdns: MdnsConfig::default(),
106106
max_body_size: default_max_body_size(),
107107
request_timeout: default_request_timeout(),
108+
read_timeout: default_read_timeout(),
108109
metrics_enabled: true,
109110
health_enabled: true,
110111
cors_origins: vec![],
111-
shutdown_timeout: default_shutdown_timeout(),
112112
}
113113
}
114114
}
@@ -162,9 +162,9 @@ impl ServerConfig {
162162
Duration::from_secs(self.request_timeout)
163163
}
164164

165-
/// Get shutdown timeout as Duration.
166-
pub fn shutdown_timeout_duration(&self) -> Duration {
167-
Duration::from_secs(self.shutdown_timeout)
165+
/// Get read timeout as Duration (for chunked transfers).
166+
pub fn read_timeout_duration(&self) -> Duration {
167+
Duration::from_secs(self.read_timeout)
168168
}
169169
}
170170

@@ -266,12 +266,6 @@ pub struct RateLimitConfig {
266266
/// Exempt paths from rate limiting.
267267
#[serde(default)]
268268
pub exempt_paths: Vec<String>,
269-
/// Trust proxy headers (X-Forwarded-For, X-Real-IP) for client IP detection.
270-
/// Enable this when running behind a reverse proxy (nginx, etc.).
271-
/// When enabled, the server will use X-Forwarded-For or X-Real-IP headers
272-
/// to determine the real client IP for logging and rate limiting.
273-
#[serde(default)]
274-
pub trust_proxy: bool,
275269
}
276270

277271
fn default_rpm() -> u32 {
@@ -292,7 +286,6 @@ impl Default for RateLimitConfig {
292286
by_api_key: false,
293287
by_user: false,
294288
exempt_paths: vec!["/health".to_string()],
295-
trust_proxy: false,
296289
}
297290
}
298291
}

cortex-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ cortex-protocol = { workspace = true }
2929
cortex-tui = { workspace = true, optional = true }
3030
cortex-exec = { workspace = true }
3131
cortex-common = { workspace = true, features = ["cli"] }
32+
cortex-commands = { workspace = true }
3233
cortex-login = { workspace = true }
3334
cortex-process-hardening = { workspace = true }
3435
cortex-app-server = { workspace = true }

cortex-cli/src/main.rs

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,10 @@ const CAT_ADVANCED: &str = "Advanced";
335335

336336
#[derive(Subcommand)]
337337
enum Commands {
338+
/// Initialize AGENTS.md in the current directory.
339+
/// Creates a project-specific configuration file for AI agents.
340+
Init(InitCommand),
341+
338342
// ═══════════════════════════════════════════════════════════════════════════
339343
// Execution Modes
340344
// ═══════════════════════════════════════════════════════════════════════════
@@ -575,6 +579,25 @@ struct CompletionCommand {
575579
/// Falls back to bash if detection fails.
576580
#[arg(value_enum)]
577581
shell: Option<Shell>,
582+
583+
/// Install completions to your shell configuration file.
584+
/// This will append an eval command to your shell's rc file
585+
/// (e.g., ~/.bashrc, ~/.zshrc) to enable tab completion.
586+
#[arg(long = "install")]
587+
install: bool,
588+
}
589+
590+
/// Init command - initialize AGENTS.md.
591+
#[derive(Args)]
592+
struct InitCommand {
593+
/// Force overwrite if AGENTS.md already exists.
594+
#[arg(short = 'f', long = "force")]
595+
force: bool,
596+
597+
/// Accept defaults without prompting (non-interactive mode).
598+
/// Useful for scripting and automation.
599+
#[arg(short = 'y', long = "yes")]
600+
yes: bool,
578601
}
579602

580603
/// Resume command.
@@ -903,6 +926,7 @@ async fn main() -> Result<()> {
903926
};
904927
run_tui(initial_prompt, &cli.interactive).await
905928
}
929+
Some(Commands::Init(init_cli)) => run_init(init_cli).await,
906930
Some(Commands::Run(run_cli)) => run_cli.run().await,
907931
Some(Commands::Exec(exec_cli)) => exec_cli.run().await,
908932
Some(Commands::Login(login_cli)) => {
@@ -966,8 +990,12 @@ async fn main() -> Result<()> {
966990
}
967991
Some(Commands::Completion(completion_cli)) => {
968992
let shell = completion_cli.shell.unwrap_or_else(detect_shell_from_env);
969-
generate_completions(shell);
970-
Ok(())
993+
if completion_cli.install {
994+
install_completions(shell)
995+
} else {
996+
generate_completions(shell);
997+
Ok(())
998+
}
971999
}
9721000
Some(Commands::Sandbox(sandbox_args)) => match sandbox_args.cmd {
9731001
SandboxCommand::Macos(cmd) => {
@@ -1204,6 +1232,133 @@ _cortex_quote_path() {
12041232
let _ = writer.write_all(output.as_bytes());
12051233
}
12061234

1235+
/// Install shell completions to the user's shell configuration file.
1236+
fn install_completions(shell: Shell) -> Result<()> {
1237+
use std::fs::OpenOptions;
1238+
use std::io::Write;
1239+
1240+
let home =
1241+
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
1242+
1243+
let (rc_file, eval_cmd) = match shell {
1244+
Shell::Bash => (home.join(".bashrc"), r#"eval "$(cortex completion bash)""#),
1245+
Shell::Zsh => (home.join(".zshrc"), r#"eval "$(cortex completion zsh)""#),
1246+
Shell::Fish => {
1247+
// Fish uses a different approach - write completion file directly
1248+
let fish_dir = home.join(".config/fish/completions");
1249+
std::fs::create_dir_all(&fish_dir)?;
1250+
let fish_file = fish_dir.join("cortex.fish");
1251+
1252+
let mut cmd = Cli::command();
1253+
let mut output = Vec::new();
1254+
generate(shell, &mut cmd, "cortex", &mut output);
1255+
std::fs::write(&fish_file, output)?;
1256+
1257+
println!("Completions installed to: {}", fish_file.display());
1258+
println!(
1259+
"Restart your shell or run 'source {}' to activate.",
1260+
fish_file.display()
1261+
);
1262+
return Ok(());
1263+
}
1264+
Shell::PowerShell => {
1265+
// PowerShell uses profile
1266+
let profile_dir = if cfg!(windows) {
1267+
home.join("Documents/PowerShell")
1268+
} else {
1269+
home.join(".config/powershell")
1270+
};
1271+
std::fs::create_dir_all(&profile_dir)?;
1272+
1273+
(
1274+
profile_dir.join("Microsoft.PowerShell_profile.ps1"),
1275+
"cortex completion powershell | Out-String | Invoke-Expression",
1276+
)
1277+
}
1278+
Shell::Elvish => {
1279+
// Elvish uses lib directory
1280+
let elvish_dir = home.join(".elvish/lib");
1281+
std::fs::create_dir_all(&elvish_dir)?;
1282+
let elvish_file = elvish_dir.join("cortex.elv");
1283+
1284+
let mut cmd = Cli::command();
1285+
let mut output = Vec::new();
1286+
generate(shell, &mut cmd, "cortex", &mut output);
1287+
std::fs::write(&elvish_file, output)?;
1288+
1289+
println!("Completions installed to: {}", elvish_file.display());
1290+
println!("Add 'use cortex' to your rc.elv to activate.");
1291+
return Ok(());
1292+
}
1293+
_ => {
1294+
bail!(
1295+
"Shell {:?} is not supported for automatic installation. Use 'cortex completion {:?}' to generate the script manually.",
1296+
shell,
1297+
shell
1298+
);
1299+
}
1300+
};
1301+
1302+
// Check if already installed
1303+
if rc_file.exists() {
1304+
let content = std::fs::read_to_string(&rc_file)?;
1305+
if content.contains("cortex completion") {
1306+
println!("Completions already installed in {}", rc_file.display());
1307+
println!("If completions aren't working, restart your shell.");
1308+
return Ok(());
1309+
}
1310+
}
1311+
1312+
// Append the eval command to the rc file
1313+
let mut file = OpenOptions::new()
1314+
.create(true)
1315+
.append(true)
1316+
.open(&rc_file)?;
1317+
1318+
writeln!(file)?;
1319+
writeln!(file, "# Cortex CLI completions")?;
1320+
writeln!(file, "{}", eval_cmd)?;
1321+
1322+
println!("Completions installed to: {}", rc_file.display());
1323+
println!(
1324+
"Restart your shell or run 'source {}' to activate.",
1325+
rc_file.display()
1326+
);
1327+
1328+
Ok(())
1329+
}
1330+
1331+
/// Run the init command to create AGENTS.md.
1332+
async fn run_init(init_cli: InitCommand) -> Result<()> {
1333+
use cortex_commands::builtin::InitCommand as InitCmd;
1334+
use std::io::IsTerminal;
1335+
1336+
let cwd = std::env::current_dir()?;
1337+
1338+
// Check if we're in a TTY and --yes wasn't provided
1339+
let is_tty = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
1340+
1341+
// If not TTY and --yes not provided, fail gracefully instead of crashing
1342+
if !is_tty && !init_cli.yes {
1343+
bail!(
1344+
"Non-interactive mode detected but --yes flag not provided.\n\
1345+
Use 'cortex init --yes' to run non-interactively with default settings."
1346+
);
1347+
}
1348+
1349+
let cmd = InitCmd::new().force(init_cli.force);
1350+
1351+
match cmd.execute(&cwd) {
1352+
Ok(result) => {
1353+
println!("{}", result.message());
1354+
Ok(())
1355+
}
1356+
Err(e) => {
1357+
bail!("Failed to initialize: {}", e);
1358+
}
1359+
}
1360+
}
1361+
12071362
async fn run_resume(resume_cli: ResumeCommand) -> Result<()> {
12081363
use cortex_protocol::ConversationId;
12091364

cortex-cli/src/pr_cmd.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,15 @@ pub struct PrCli {
6363
#[arg(short, long)]
6464
pub path: Option<PathBuf>,
6565

66+
/// Custom local branch name for the PR checkout.
67+
/// If not specified, defaults to "pr-{number}".
68+
#[arg(short, long)]
69+
pub branch: Option<String>,
70+
6671
/// Force checkout even if there are uncommitted changes.
6772
/// WARNING: This may result in data loss! Uncommitted changes in your working
6873
/// directory may be overwritten. Consider using 'git stash' first to save your work.
69-
#[arg(short, long)]
74+
#[arg(short = 'F', long)]
7075
pub force: bool,
7176

7277
/// Show PR details without checking out.
@@ -305,12 +310,18 @@ async fn run_pr_checkout(args: PrCli) -> Result<()> {
305310
}
306311

307312
// Fetch the PR
308-
// SECURITY: Branch name and refspec are constructed from validated numeric PR number
309-
let branch_name = format!("pr-{}", pr_number);
310-
let refspec = format!("pull/{}/head:{}", pr_number, branch_name);
313+
// Use custom branch name if provided, otherwise default to "pr-{number}"
314+
let branch_name = args
315+
.branch
316+
.clone()
317+
.unwrap_or_else(|| format!("pr-{}", pr_number));
311318

312-
// Validate constructed names to ensure no injection is possible
319+
// SECURITY: Validate the branch name to prevent injection
313320
validate_branch_name(&branch_name)?;
321+
322+
let refspec = format!("pull/{}/head:{}", pr_number, branch_name);
323+
324+
// Validate refspec to ensure no injection is possible
314325
validate_refspec(&refspec)?;
315326

316327
println!("Fetching PR #{}...", pr_number);

cortex-cli/src/run_cmd.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
3232

3333
use crate::styled_output::{print_success, print_warning};
3434
use cortex_common::resolve_model_alias;
35+
use cortex_common::{resolve_model_with_info, warn_if_ambiguous_model};
3536
use cortex_engine::rollout::get_rollout_path;
3637
use cortex_engine::{Session, list_sessions};
3738
use cortex_protocol::{ConversationId, EventMsg, Op, Submission, UserInput};
@@ -630,8 +631,11 @@ impl RunCli {
630631
}
631632

632633
// Resolve model alias if provided (e.g., "sonnet" -> "anthropic/claude-sonnet-4-20250514")
634+
// Also warn if the model name was ambiguous and multiple models matched
633635
if let Some(ref model) = self.model {
634-
config.model = resolve_model_alias(model).to_string();
636+
let resolution = resolve_model_with_info(model);
637+
warn_if_ambiguous_model(&resolution, model);
638+
config.model = resolution.model;
635639
}
636640

637641
// Apply temperature override if provided

0 commit comments

Comments
 (0)