Skip to content
Open
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
52 changes: 52 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ simd-json = "0.14"
toml = "0.8"

# HTTP client
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"], default-features = false }
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls", "cookies"], default-features = false }

# Error handling
thiserror = "2.0"
Expand Down
4 changes: 4 additions & 0 deletions cortex-app-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ pub struct RateLimitConfig {
/// Exempt paths from rate limiting.
#[serde(default)]
pub exempt_paths: Vec<String>,
/// Trust proxy headers (X-Real-IP, X-Forwarded-For) for client IP detection.
#[serde(default)]
pub trust_proxy: bool,
}

fn default_rpm() -> u32 {
Expand All @@ -286,6 +289,7 @@ impl Default for RateLimitConfig {
by_api_key: false,
by_user: false,
exempt_paths: vec!["/health".to_string()],
trust_proxy: false,
}
}
}
Expand Down
129 changes: 80 additions & 49 deletions cortex-cli/src/debug_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use cortex_protocol::ConversationId;
#[derive(Debug, Parser)]
pub struct DebugCli {
#[command(subcommand)]
pub subcommand: DebugSubcommand,
pub subcommand: Option<DebugSubcommand>,
}

/// Debug subcommands.
Expand Down Expand Up @@ -1446,51 +1446,63 @@ async fn run_skill(args: SkillArgs) -> Result<()> {

if let Some(ref path) = skill_path {
if path.exists() {
match std::fs::read_to_string(path) {
Ok(content) => {
// Try YAML first, then TOML
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let parse_result: Result<SkillDefinition, String> = match ext {
"yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| e.to_string()),
"toml" => toml::from_str(&content).map_err(|e| e.to_string()),
_ => {
// Try YAML first, then TOML
serde_yaml::from_str(&content)
.map_err(|e| e.to_string())
.or_else(|_| toml::from_str(&content).map_err(|e| e.to_string()))
}
};

match parse_result {
Ok(def) => {
// Validate the definition
if def.name.is_empty() {
warnings.push("Skill name is empty".to_string());
}
if def.description.is_empty() {
warnings.push("Skill description is empty".to_string());
// Check if path is a directory - provide helpful error message
if path.is_dir() {
errors.push(format!(
"Path '{}' is a directory, not a file. Please provide a path to a skill file (e.g., skill.yaml or skill.toml).",
path.display()
));
} else {
match std::fs::read_to_string(path) {
Ok(content) => {
// Try YAML first, then TOML
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let parse_result: Result<SkillDefinition, String> = match ext {
"yaml" | "yml" => {
serde_yaml::from_str(&content).map_err(|e| e.to_string())
}
if def.commands.is_empty() {
warnings.push("Skill has no commands".to_string());
"toml" => toml::from_str(&content).map_err(|e| e.to_string()),
_ => {
// Try YAML first, then TOML
serde_yaml::from_str(&content)
.map_err(|e| e.to_string())
.or_else(|_| {
toml::from_str(&content).map_err(|e| e.to_string())
})
}
for cmd in &def.commands {
if cmd.command.is_none() {
warnings.push(format!(
"Command '{}' has no command defined",
cmd.name
));
};

match parse_result {
Ok(def) => {
// Validate the definition
if def.name.is_empty() {
warnings.push("Skill name is empty".to_string());
}
if def.description.is_empty() {
warnings.push("Skill description is empty".to_string());
}
if def.commands.is_empty() {
warnings.push("Skill has no commands".to_string());
}
for cmd in &def.commands {
if cmd.command.is_none() {
warnings.push(format!(
"Command '{}' has no command defined",
cmd.name
));
}
}
valid = errors.is_empty();
definition = Some(def);
}
Err(e) => {
errors.push(format!("Parse error: {}", e));
}
valid = errors.is_empty();
definition = Some(def);
}
Err(e) => {
errors.push(format!("Parse error: {}", e));
}
}
}
Err(e) => {
errors.push(format!("Read error: {}", e));
Err(e) => {
errors.push(format!("Read error: {}", e));
}
}
}
}
Expand Down Expand Up @@ -2522,15 +2534,34 @@ impl DebugCli {
/// Run the debug command.
pub async fn run(self) -> Result<()> {
match self.subcommand {
DebugSubcommand::Config(args) => run_config(args).await,
DebugSubcommand::File(args) => run_file(args).await,
DebugSubcommand::Lsp(args) => run_lsp(args).await,
DebugSubcommand::Ripgrep(args) => run_ripgrep(args).await,
DebugSubcommand::Skill(args) => run_skill(args).await,
DebugSubcommand::Snapshot(args) => run_snapshot(args).await,
DebugSubcommand::Paths(args) => run_paths(args).await,
DebugSubcommand::System(args) => run_system(args).await,
DebugSubcommand::Wait(args) => run_wait(args).await,
Some(DebugSubcommand::Config(args)) => run_config(args).await,
Some(DebugSubcommand::File(args)) => run_file(args).await,
Some(DebugSubcommand::Lsp(args)) => run_lsp(args).await,
Some(DebugSubcommand::Ripgrep(args)) => run_ripgrep(args).await,
Some(DebugSubcommand::Skill(args)) => run_skill(args).await,
Some(DebugSubcommand::Snapshot(args)) => run_snapshot(args).await,
Some(DebugSubcommand::Paths(args)) => run_paths(args).await,
Some(DebugSubcommand::System(args)) => run_system(args).await,
Some(DebugSubcommand::Wait(args)) => run_wait(args).await,
None => {
// Show help when no subcommand is provided
println!("Cortex Debug Commands");
println!("{}", "=".repeat(50));
println!();
println!("Available subcommands:");
println!(" config Show resolved configuration and config file locations");
println!(" file Show file metadata, MIME type, and encoding");
println!(" lsp List and test LSP servers");
println!(" ripgrep Check ripgrep availability and test search");
println!(" skill Parse and validate a skill file");
println!(" snapshot Show snapshot status and diffs");
println!(" paths Show all Cortex paths");
println!(" system Show system information for bug reports");
println!(" wait Wait for a condition (useful for scripts)");
println!();
println!("Run 'cortex debug <subcommand> --help' for more information.");
Ok(())
}
}
}
}
Expand Down
80 changes: 60 additions & 20 deletions cortex-cli/src/github_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::path::PathBuf;
#[derive(Debug, Parser)]
pub struct GitHubCli {
#[command(subcommand)]
pub subcommand: GitHubSubcommand,
pub subcommand: Option<GitHubSubcommand>,
}

/// GitHub subcommands.
Expand Down Expand Up @@ -146,11 +146,26 @@ impl GitHubCli {
/// Run the GitHub command.
pub async fn run(self) -> Result<()> {
match self.subcommand {
GitHubSubcommand::Install(args) => run_install(args).await,
GitHubSubcommand::Run(args) => run_github_agent(args).await,
GitHubSubcommand::Status(args) => run_status(args).await,
GitHubSubcommand::Uninstall(args) => run_uninstall(args).await,
GitHubSubcommand::Update(args) => run_update(args).await,
Some(GitHubSubcommand::Install(args)) => run_install(args).await,
Some(GitHubSubcommand::Run(args)) => run_github_agent(args).await,
Some(GitHubSubcommand::Status(args)) => run_status(args).await,
Some(GitHubSubcommand::Uninstall(args)) => run_uninstall(args).await,
Some(GitHubSubcommand::Update(args)) => run_update(args).await,
None => {
// Show help when no subcommand is provided
println!("Cortex GitHub Integration");
println!("{}", "=".repeat(50));
println!();
println!("Available subcommands:");
println!(" install Install GitHub Actions workflow for CI/CD automation");
println!(" run Run GitHub agent in Actions context");
println!(" status Check GitHub Actions installation status");
println!(" uninstall Remove the Cortex GitHub workflow");
println!(" update Update the workflow to the latest version");
println!();
println!("Run 'cortex github <subcommand> --help' for more information.");
Ok(())
}
}
}
}
Expand Down Expand Up @@ -241,22 +256,33 @@ async fn run_install(args: InstallArgs) -> Result<()> {
async fn run_github_agent(args: RunArgs) -> Result<()> {
use cortex_engine::github::{GitHubEvent, parse_event};

let token = args.token.ok_or_else(|| {
anyhow::anyhow!("GitHub token required. Set GITHUB_TOKEN env var or use --token")
})?;

let repository = args.repository.ok_or_else(|| {
anyhow::anyhow!(
"GitHub repository required. Set GITHUB_REPOSITORY env var or use --repository"
)
})?;
// Try to get token from argument, then environment variable
// For dry-run mode or read-only operations on public repos, token is optional
let token = args
.token
.clone()
.or_else(|| std::env::var("GITHUB_TOKEN").ok());

let repository = args
.repository
.clone()
.or_else(|| std::env::var("GITHUB_REPOSITORY").ok())
.ok_or_else(|| {
anyhow::anyhow!(
"GitHub repository required. Set GITHUB_REPOSITORY env var or use --repository"
)
})?;

// Parse the event payload
let event_path = args.event_path.ok_or_else(|| {
anyhow::anyhow!(
"Event payload path required. Set GITHUB_EVENT_PATH env var or use --event-path"
)
})?;
let event_path = args
.event_path
.clone()
.or_else(|| std::env::var("GITHUB_EVENT_PATH").ok().map(PathBuf::from))
.ok_or_else(|| {
anyhow::anyhow!(
"Event payload path required. Set GITHUB_EVENT_PATH env var or use --event-path"
)
})?;

let event_content = std::fs::read_to_string(&event_path)
.with_context(|| format!("Failed to read event file: {}", event_path.display()))?;
Expand All @@ -280,6 +306,20 @@ async fn run_github_agent(args: RunArgs) -> Result<()> {
return Ok(());
}

// For non-dry-run mode, token is required for write operations
let token = match token {
Some(t) => t,
None => {
bail!(
"GitHub token required for executing actions.\n\n\
You can provide a token via:\n \
• --token <TOKEN>\n \
• GITHUB_TOKEN environment variable\n\n\
For read-only operations on public repos, use --dry-run to preview without a token."
);
}
};

// Execute the appropriate agent based on event type
match event {
GitHubEvent::IssueComment(comment) => {
Expand Down
Loading