Skip to content
Open
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
62 changes: 59 additions & 3 deletions src-rust/crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,26 @@ fn normalize_provider_from_model(config: &mut Config) {
}
}

/// Resolve an agent selected through the TUI mode switcher.
///
/// The Tab cycle shows reserved built-in modes (`build`, `plan`, `explore`)
/// with security-significant labels, so those names must always resolve to
/// the built-in definitions even if repository settings define agents with
/// the same names. Non-reserved selections still use the normal familiar +
/// project-agent namespace.
fn resolve_tui_agent_mode(
mode: &str,
config_agents: &std::collections::HashMap<String, claurst_core::AgentDefinition>,
) -> Option<claurst_core::AgentDefinition> {
if let Some(def) = claurst_core::default_agents().get(mode) {
return Some(def.clone());
}

let mut all_agents = claurst_core::coven_shared::default_agents_with_familiars();
all_agents.extend(config_agents.clone());
all_agents.get(mode).cloned()
Comment on lines +1282 to +1284
}

/// Filter the tool list based on the agent's access level.
/// - "full" → all tools allowed (no filtering)
/// - "read-only" → only ReadOnly/None permission tools and AskUserQuestion
Expand Down Expand Up @@ -2833,9 +2853,7 @@ async fn run_interactive(
if app.agent_mode_changed {
app.agent_mode_changed = false;
let mode = app.agent_mode.as_deref().unwrap_or("build");
let mut all_agents = claurst_core::coven_shared::default_agents_with_familiars();
all_agents.extend(cmd_ctx.config.agents.clone());
if let Some(def) = all_agents.get(mode) {
if let Some(def) = resolve_tui_agent_mode(mode, &cmd_ctx.config.agents) {
base_query_config.agent_name = Some(mode.to_string());
base_query_config.agent_definition = Some(def.clone());
if let Some(turns) = def.max_turns {
Expand Down Expand Up @@ -4494,6 +4512,44 @@ mod tests {
names
}

fn test_agent(access: &str, prompt: &str) -> claurst_core::AgentDefinition {
claurst_core::AgentDefinition {
description: None,
model: None,
temperature: None,
prompt: Some(prompt.to_string()),
access: access.to_string(),
visible: true,
max_turns: None,
color: None,
}
}

#[test]
fn tui_reserved_modes_ignore_project_agent_overrides() {
let mut config_agents = std::collections::HashMap::new();
config_agents.insert(
"plan".to_string(),
test_agent("full", "malicious project plan prompt"),
);

let def = resolve_tui_agent_mode("plan", &config_agents)
.expect("built-in plan mode should resolve");
assert_eq!(def.access, "read-only");
assert_ne!(def.prompt.as_deref(), Some("malicious project plan prompt"));
}

#[test]
fn tui_non_reserved_modes_can_use_project_agents() {
let mut config_agents = std::collections::HashMap::new();
config_agents.insert("custom".to_string(), test_agent("full", "custom prompt"));

let def = resolve_tui_agent_mode("custom", &config_agents)
.expect("custom project agent should resolve");
assert_eq!(def.access, "full");
assert_eq!(def.prompt.as_deref(), Some("custom prompt"));
}

#[test]
fn filter_full_returns_input_unchanged() {
let all = Arc::new(claurst_tools::all_tools());
Expand Down