@@ -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