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
145 changes: 141 additions & 4 deletions cortex-cli/src/agent_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,23 @@ pub struct AgentFrontmatter {
/// Display name (for UI).
#[serde(default, alias = "display-name")]
pub display_name: Option<String>,
/// Color for UI (hex).
/// Color for UI (hex format: #RGB or #RRGGBB).
#[serde(default)]
pub color: Option<String>,
/// Icon for visual identification (emoji or icon reference).
/// Example: "rocket", "code", "bug", or an emoji like "🚀"
#[serde(default)]
pub icon: Option<String>,
/// Whether agent is hidden from UI.
#[serde(default)]
pub hidden: bool,
/// Additional tools configuration (tool_name -> enabled).
#[serde(default)]
pub tools: HashMap<String, bool>,
/// Base agent to inherit from (for configuration inheritance).
/// The agent will inherit all settings from the base and can override specific ones.
#[serde(default, alias = "extends")]
pub extends: Option<String>,
}

fn default_can_delegate() -> bool {
Expand Down Expand Up @@ -341,8 +349,11 @@ pub struct AgentInfo {
pub temperature: Option<f32>,
/// Top-P for generation.
pub top_p: Option<f32>,
/// Color for UI (hex).
/// Color for UI (hex format: #RGB or #RRGGBB).
pub color: Option<String>,
/// Icon for visual identification (emoji or icon reference).
#[serde(default)]
pub icon: Option<String>,
/// Model override.
pub model: Option<String>,
/// Tools configuration (tool_name -> enabled).
Expand All @@ -365,6 +376,9 @@ pub struct AgentInfo {
pub source: AgentSource,
/// Path to agent definition file.
pub path: Option<PathBuf>,
/// Base agent this extends (for inheritance).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
}

impl AgentCli {
Expand All @@ -383,6 +397,79 @@ impl AgentCli {
}
}

/// Validate a hex color format (#RGB or #RRGGBB) (#3022)
///
/// Returns the validated color string.
/// Accepts:
/// - 3-digit hex: #RGB (e.g., #F00 for red)
/// - 6-digit hex: #RRGGBB (e.g., #FF0000 for red)
/// - Common color names: red, green, blue, yellow, orange, purple, cyan, white, black
fn validate_color(color: &str) -> Result<String> {
let color = color.trim();

// Map common color names to hex values
let color_names: std::collections::HashMap<&str, &str> = [
("red", "#FF0000"),
("green", "#00FF00"),
("blue", "#0000FF"),
("yellow", "#FFFF00"),
("orange", "#FFA500"),
("purple", "#800080"),
("cyan", "#00FFFF"),
("magenta", "#FF00FF"),
("white", "#FFFFFF"),
("black", "#000000"),
("gray", "#808080"),
("grey", "#808080"),
("pink", "#FFC0CB"),
]
.into_iter()
.collect();

// Check if it's a named color
let lower = color.to_lowercase();
if let Some(&hex) = color_names.get(lower.as_str()) {
return Ok(hex.to_string());
}

// Must start with #
if !color.starts_with('#') {
bail!(
"Invalid color format: '{}'. Colors must be in hex format (#RGB or #RRGGBB) or a named color.\n\
Examples: #FF5733, #F00, red, blue",
color
);
}

let hex = &color[1..];

// Validate length: 3 or 6 characters
if hex.len() != 3 && hex.len() != 6 {
bail!(
"Invalid color format: '{}'. Expected #RGB (3 digits) or #RRGGBB (6 digits).\n\
Examples: #FF5733, #F00",
color
);
}

// Validate all characters are hex digits
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
bail!(
"Invalid color format: '{}'. Contains non-hex characters.\n\
Hex colors can only contain 0-9 and A-F.",
color
);
}

// Normalize 3-digit to 6-digit
if hex.len() == 3 {
let expanded: String = hex.chars().flat_map(|c| [c, c]).collect();
Ok(format!("#{}", expanded.to_uppercase()))
} else {
Ok(format!("#{}", hex.to_uppercase()))
}
}

/// Validate a model name for agent creation.
///
/// Returns the validated model name, resolving aliases if needed.
Expand Down Expand Up @@ -529,6 +616,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: None,
top_p: None,
color: Some("#22c55e".to_string()),
icon: Some("hammer".to_string()),
model: None,
tools: HashMap::new(),
allowed_tools: None,
Expand All @@ -538,6 +626,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["development".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
AgentInfo {
name: "plan".to_string(),
Expand All @@ -550,6 +639,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: None,
top_p: None,
color: Some("#3b82f6".to_string()),
icon: Some("clipboard".to_string()),
model: None,
tools: HashMap::new(),
allowed_tools: Some(vec![
Expand All @@ -564,6 +654,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["analysis".to_string(), "read-only".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
AgentInfo {
name: "explore".to_string(),
Expand All @@ -576,6 +667,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: Some(0.3),
top_p: None,
color: Some("#f59e0b".to_string()),
icon: Some("compass".to_string()),
model: None,
tools: [
("edit".to_string(), false),
Expand All @@ -595,6 +687,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["code".to_string(), "analysis".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
AgentInfo {
name: "general".to_string(),
Expand All @@ -607,6 +700,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: None,
top_p: None,
color: Some("#8b5cf6".to_string()),
icon: Some("star".to_string()),
model: None,
tools: [
("todoread".to_string(), false),
Expand All @@ -619,6 +713,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["general".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
AgentInfo {
name: "code-explorer".to_string(),
Expand All @@ -631,6 +726,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: Some(0.3),
top_p: None,
color: Some("#06b6d4".to_string()),
icon: Some("search".to_string()),
model: None,
tools: HashMap::new(),
allowed_tools: Some(vec![
Expand All @@ -645,6 +741,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["code".to_string(), "analysis".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
AgentInfo {
name: "code-reviewer".to_string(),
Expand All @@ -657,6 +754,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: Some(0.2),
top_p: None,
color: Some("#ef4444".to_string()),
icon: Some("check-circle".to_string()),
model: None,
tools: HashMap::new(),
allowed_tools: Some(vec![
Expand All @@ -670,6 +768,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["review".to_string(), "quality".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
AgentInfo {
name: "architect".to_string(),
Expand All @@ -682,6 +781,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
temperature: Some(0.5),
top_p: None,
color: Some("#a855f7".to_string()),
icon: Some("layout".to_string()),
model: None,
tools: HashMap::new(),
allowed_tools: Some(vec![
Expand All @@ -696,6 +796,7 @@ fn load_builtin_agents() -> Vec<AgentInfo> {
tags: vec!["architecture".to_string(), "design".to_string()],
source: AgentSource::Builtin,
path: None,
extends: None,
},
]
}
Expand Down Expand Up @@ -816,6 +917,22 @@ fn load_agent_from_md(path: &Path, source: AgentSource) -> Result<AgentInfo> {

let (frontmatter, body) = parse_frontmatter(&content)?;

// Validate color if present (#3022)
let validated_color = if let Some(ref color) = frontmatter.color {
match validate_color(color) {
Ok(c) => Some(c),
Err(e) => {
eprintln!(
"Warning: Invalid color in agent '{}': {}",
frontmatter.name, e
);
None
}
}
} else {
None
};

Ok(AgentInfo {
name: frontmatter.name,
display_name: frontmatter.display_name,
Expand All @@ -826,7 +943,8 @@ fn load_agent_from_md(path: &Path, source: AgentSource) -> Result<AgentInfo> {
prompt: if body.is_empty() { None } else { Some(body) },
temperature: frontmatter.temperature,
top_p: frontmatter.top_p,
color: frontmatter.color,
color: validated_color,
icon: frontmatter.icon,
model: frontmatter.model,
tools: frontmatter.tools,
allowed_tools: frontmatter.allowed_tools,
Expand All @@ -836,6 +954,7 @@ fn load_agent_from_md(path: &Path, source: AgentSource) -> Result<AgentInfo> {
tags: frontmatter.tags,
source,
path: Some(path.to_path_buf()),
extends: frontmatter.extends,
})
}

Expand All @@ -858,6 +977,22 @@ fn load_agent_from_json(path: &Path, source: AgentSource) -> Result<AgentInfo> {
None
};

// Validate color if present (#3022)
let validated_color = if let Some(ref color) = frontmatter.color {
match validate_color(color) {
Ok(c) => Some(c),
Err(e) => {
eprintln!(
"Warning: Invalid color in agent '{}': {}",
frontmatter.name, e
);
None
}
}
} else {
None
};

Ok(AgentInfo {
name: frontmatter.name,
display_name: frontmatter.display_name,
Expand All @@ -868,7 +1003,8 @@ fn load_agent_from_json(path: &Path, source: AgentSource) -> Result<AgentInfo> {
prompt,
temperature: frontmatter.temperature,
top_p: frontmatter.top_p,
color: frontmatter.color,
color: validated_color,
icon: frontmatter.icon,
model: frontmatter.model,
tools: frontmatter.tools,
allowed_tools: frontmatter.allowed_tools,
Expand All @@ -878,6 +1014,7 @@ fn load_agent_from_json(path: &Path, source: AgentSource) -> Result<AgentInfo> {
tags: frontmatter.tags,
source,
path: Some(path.to_path_buf()),
extends: frontmatter.extends,
})
}

Expand Down
Loading