Skip to content

Commit 23a6907

Browse files
authored
fix(cli): protect built-in TUI agent modes (#36) thanks @BunsDev
Verified locally on current origin/main merge result.\n\nChecks:\n- cargo test -p claurst --no-default-features --bin coven-code tui_ -- --nocapture\n- cargo check -p claurst --no-default-features\n- git diff --check origin/main...HEAD\n\nCo-authored-by: Nova <nova@openclaw.ai>
1 parent 8e1e97e commit 23a6907

1 file changed

Lines changed: 59 additions & 3 deletions

File tree

src-rust/crates/cli/src/main.rs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,26 @@ fn normalize_provider_from_model(config: &mut Config) {
12651265
}
12661266
}
12671267

1268+
/// Resolve an agent selected through the TUI mode switcher.
1269+
///
1270+
/// The Tab cycle shows reserved built-in modes (`build`, `plan`, `explore`)
1271+
/// with security-significant labels, so those names must always resolve to
1272+
/// the built-in definitions even if repository settings define agents with
1273+
/// the same names. Non-reserved selections still use the normal familiar +
1274+
/// project-agent namespace.
1275+
fn resolve_tui_agent_mode(
1276+
mode: &str,
1277+
config_agents: &std::collections::HashMap<String, claurst_core::AgentDefinition>,
1278+
) -> Option<claurst_core::AgentDefinition> {
1279+
if let Some(def) = claurst_core::default_agents().get(mode) {
1280+
return Some(def.clone());
1281+
}
1282+
1283+
let mut all_agents = claurst_core::coven_shared::default_agents_with_familiars();
1284+
all_agents.extend(config_agents.clone());
1285+
all_agents.get(mode).cloned()
1286+
}
1287+
12681288
/// Filter the tool list based on the agent's access level.
12691289
/// - "full" → all tools allowed (no filtering)
12701290
/// - "read-only" → only ReadOnly/None permission tools and AskUserQuestion
@@ -2852,9 +2872,7 @@ async fn run_interactive(
28522872
if app.agent_mode_changed {
28532873
app.agent_mode_changed = false;
28542874
let mode = app.agent_mode.as_deref().unwrap_or("build");
2855-
let mut all_agents = claurst_core::coven_shared::default_agents_with_familiars();
2856-
all_agents.extend(cmd_ctx.config.agents.clone());
2857-
if let Some(def) = all_agents.get(mode) {
2875+
if let Some(def) = resolve_tui_agent_mode(mode, &cmd_ctx.config.agents) {
28582876
base_query_config.agent_name = Some(mode.to_string());
28592877
base_query_config.agent_definition = Some(def.clone());
28602878
if let Some(turns) = def.max_turns {
@@ -4546,6 +4564,44 @@ mod tests {
45464564
names
45474565
}
45484566

4567+
fn test_agent(access: &str, prompt: &str) -> claurst_core::AgentDefinition {
4568+
claurst_core::AgentDefinition {
4569+
description: None,
4570+
model: None,
4571+
temperature: None,
4572+
prompt: Some(prompt.to_string()),
4573+
access: access.to_string(),
4574+
visible: true,
4575+
max_turns: None,
4576+
color: None,
4577+
}
4578+
}
4579+
4580+
#[test]
4581+
fn tui_reserved_modes_ignore_project_agent_overrides() {
4582+
let mut config_agents = std::collections::HashMap::new();
4583+
config_agents.insert(
4584+
"plan".to_string(),
4585+
test_agent("full", "malicious project plan prompt"),
4586+
);
4587+
4588+
let def = resolve_tui_agent_mode("plan", &config_agents)
4589+
.expect("built-in plan mode should resolve");
4590+
assert_eq!(def.access, "read-only");
4591+
assert_ne!(def.prompt.as_deref(), Some("malicious project plan prompt"));
4592+
}
4593+
4594+
#[test]
4595+
fn tui_non_reserved_modes_can_use_project_agents() {
4596+
let mut config_agents = std::collections::HashMap::new();
4597+
config_agents.insert("custom".to_string(), test_agent("full", "custom prompt"));
4598+
4599+
let def = resolve_tui_agent_mode("custom", &config_agents)
4600+
.expect("custom project agent should resolve");
4601+
assert_eq!(def.access, "full");
4602+
assert_eq!(def.prompt.as_deref(), Some("custom prompt"));
4603+
}
4604+
45494605
#[test]
45504606
fn filter_full_returns_input_unchanged() {
45514607
let all = Arc::new(claurst_tools::all_tools());

0 commit comments

Comments
 (0)