1use crate::config::CodeConfig;
58use crate::permissions::{PermissionChecker, PermissionDecision, PermissionPolicy};
59use serde::{Deserialize, Serialize};
60use std::collections::HashMap;
61use std::path::Path;
62use std::sync::RwLock;
63
64use crate::error::{read_or_recover, write_or_recover};
65
66#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
68#[serde(rename_all = "snake_case")]
69pub enum ConfirmationInheritance {
70 #[default]
73 AutoApprove,
74 DenyOnAsk,
76 InheritParent,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ModelConfig {
83 pub model: String,
85 pub provider: Option<String>,
87}
88
89impl ModelConfig {
90 pub fn new(model: impl Into<String>) -> Self {
92 Self {
93 model: model.into(),
94 provider: None,
95 }
96 }
97
98 pub fn with_provider(provider: impl Into<String>, model: impl Into<String>) -> Self {
100 Self {
101 model: model.into(),
102 provider: Some(provider.into()),
103 }
104 }
105
106 pub fn from_model_ref(model_ref: impl AsRef<str>) -> Self {
111 let model_ref = model_ref.as_ref();
112 if let Some((provider, model)) = model_ref.split_once('/') {
113 Self::with_provider(provider, model)
114 } else {
115 Self::new(model_ref)
116 }
117 }
118
119 pub fn model_ref(&self) -> String {
121 match &self.provider {
122 Some(provider) => format!("{}/{}", provider, self.model),
123 None => self.model.clone(),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum WorkerAgentKind {
136 #[serde(alias = "readonly", alias = "read-only", alias = "explore")]
138 ReadOnly,
139 #[serde(alias = "plan")]
141 Planner,
142 #[serde(alias = "implementation", alias = "general")]
144 Implementer,
145 #[serde(alias = "verification", alias = "verify")]
147 Verifier,
148 #[serde(alias = "review", alias = "code-review")]
150 Reviewer,
151 Custom,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct WorkerAgentSpec {
162 pub name: String,
164 pub description: String,
166 pub kind: WorkerAgentKind,
168 #[serde(default)]
170 pub hidden: bool,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub permissions: Option<PermissionPolicy>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub model: Option<ModelConfig>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub prompt: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub max_steps: Option<usize>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub confirmation_inheritance: Option<ConfirmationInheritance>,
186}
187
188impl WorkerAgentKind {
189 pub fn as_str(self) -> &'static str {
191 match self {
192 Self::ReadOnly => "read_only",
193 Self::Planner => "planner",
194 Self::Implementer => "implementer",
195 Self::Verifier => "verifier",
196 Self::Reviewer => "reviewer",
197 Self::Custom => "custom",
198 }
199 }
200
201 fn default_permissions(self) -> PermissionPolicy {
202 match self {
203 Self::ReadOnly => explore_permissions(),
204 Self::Planner => plan_permissions(),
205 Self::Implementer => general_permissions(),
206 Self::Verifier => verification_permissions(),
207 Self::Reviewer => review_permissions(),
208 Self::Custom => PermissionPolicy::strict(),
209 }
210 }
211
212 fn default_prompt(self) -> Option<&'static str> {
213 match self {
214 Self::ReadOnly => Some(EXPLORE_PROMPT),
215 Self::Planner => Some(PLAN_PROMPT),
216 Self::Verifier => Some(VERIFICATION_PROMPT),
217 Self::Reviewer => Some(REVIEW_PROMPT),
218 Self::Implementer | Self::Custom => None,
219 }
220 }
221
222 fn default_max_steps(self) -> usize {
223 match self {
224 Self::ReadOnly => 20,
225 Self::Planner => 30,
226 Self::Implementer => 50,
227 Self::Verifier => 30,
228 Self::Reviewer => 25,
229 Self::Custom => 30,
230 }
231 }
232}
233
234impl std::fmt::Display for WorkerAgentKind {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 f.write_str(self.as_str())
237 }
238}
239
240impl std::str::FromStr for WorkerAgentKind {
241 type Err = anyhow::Error;
242
243 fn from_str(value: &str) -> anyhow::Result<Self> {
244 match value.trim().to_ascii_lowercase().as_str() {
245 "read_only" | "readonly" | "read-only" | "explore" | "scanner" => Ok(Self::ReadOnly),
246 "planner" | "plan" => Ok(Self::Planner),
247 "implementer" | "implementation" | "general" | "executor" => Ok(Self::Implementer),
248 "verifier" | "verification" | "verify" | "tester" => Ok(Self::Verifier),
249 "reviewer" | "review" | "code-review" | "code_reviewer" => Ok(Self::Reviewer),
250 "custom" => Ok(Self::Custom),
251 other => Err(anyhow::anyhow!("unknown worker agent kind '{}'", other)),
252 }
253 }
254}
255
256pub type CattleAgentKind = WorkerAgentKind;
258pub type CattleAgentSpec = WorkerAgentSpec;
260
261impl WorkerAgentSpec {
262 pub fn new(
264 kind: WorkerAgentKind,
265 name: impl Into<String>,
266 description: impl Into<String>,
267 ) -> Self {
268 Self {
269 name: name.into(),
270 description: description.into(),
271 kind,
272 hidden: false,
273 permissions: None,
274 model: None,
275 prompt: None,
276 max_steps: None,
277 confirmation_inheritance: None,
278 }
279 }
280
281 pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
283 Self::new(WorkerAgentKind::ReadOnly, name, description)
284 }
285
286 pub fn planner(name: impl Into<String>, description: impl Into<String>) -> Self {
288 Self::new(WorkerAgentKind::Planner, name, description)
289 }
290
291 pub fn implementer(name: impl Into<String>, description: impl Into<String>) -> Self {
293 Self::new(WorkerAgentKind::Implementer, name, description)
294 }
295
296 pub fn verifier(name: impl Into<String>, description: impl Into<String>) -> Self {
298 Self::new(WorkerAgentKind::Verifier, name, description)
299 }
300
301 pub fn reviewer(name: impl Into<String>, description: impl Into<String>) -> Self {
303 Self::new(WorkerAgentKind::Reviewer, name, description)
304 }
305
306 pub fn custom(name: impl Into<String>, description: impl Into<String>) -> Self {
308 Self::new(WorkerAgentKind::Custom, name, description)
309 }
310
311 pub fn hidden(mut self, hidden: bool) -> Self {
313 self.hidden = hidden;
314 self
315 }
316
317 pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
319 self.permissions = Some(permissions);
320 self
321 }
322
323 pub fn with_model(mut self, model: ModelConfig) -> Self {
325 self.model = Some(model);
326 self
327 }
328
329 pub fn with_model_ref(mut self, model_ref: impl AsRef<str>) -> Self {
331 self.model = Some(ModelConfig::from_model_ref(model_ref));
332 self
333 }
334
335 pub fn with_provider_model(
337 mut self,
338 provider: impl Into<String>,
339 model: impl Into<String>,
340 ) -> Self {
341 self.model = Some(ModelConfig::with_provider(provider, model));
342 self
343 }
344
345 pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
347 self.prompt = Some(prompt.into());
348 self
349 }
350
351 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
353 self.max_steps = Some(max_steps);
354 self
355 }
356
357 pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
359 self.confirmation_inheritance = Some(inheritance);
360 self
361 }
362
363 pub fn into_agent_definition(self) -> AgentDefinition {
365 let mut agent = AgentDefinition::new(&self.name, &self.description)
366 .with_permissions(
367 self.permissions
368 .unwrap_or_else(|| self.kind.default_permissions()),
369 )
370 .with_max_steps(
371 self.max_steps
372 .unwrap_or_else(|| self.kind.default_max_steps()),
373 );
374
375 if self.hidden {
376 agent = agent.hidden();
377 }
378 if let Some(model) = self.model {
379 agent = agent.with_model(model);
380 }
381 if let Some(prompt) = self
382 .prompt
383 .or_else(|| self.kind.default_prompt().map(str::to_string))
384 {
385 agent = agent.with_prompt(&prompt);
386 }
387 if let Some(ci) = self.confirmation_inheritance {
388 agent = agent.with_confirmation(ci);
389 }
390 agent
391 }
392}
393
394impl From<WorkerAgentSpec> for AgentDefinition {
395 fn from(spec: WorkerAgentSpec) -> Self {
396 spec.into_agent_definition()
397 }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct AgentDefinition {
405 pub name: String,
407 pub description: String,
409 #[serde(default)]
411 pub native: bool,
412 #[serde(default)]
414 pub hidden: bool,
415 #[serde(default)]
417 pub permissions: PermissionPolicy,
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub model: Option<ModelConfig>,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub prompt: Option<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub max_steps: Option<usize>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub confirmation_inheritance: Option<ConfirmationInheritance>,
431}
432
433impl AgentDefinition {
434 pub fn new(name: &str, description: &str) -> Self {
436 Self {
437 name: name.to_string(),
438 description: description.to_string(),
439 native: false,
440 hidden: false,
441 permissions: PermissionPolicy::default(),
442 model: None,
443 prompt: None,
444 max_steps: None,
445 confirmation_inheritance: None,
446 }
447 }
448
449 pub fn worker(spec: WorkerAgentSpec) -> Self {
451 spec.into_agent_definition()
452 }
453
454 pub fn native(mut self) -> Self {
456 self.native = true;
457 self
458 }
459
460 pub fn hidden(mut self) -> Self {
462 self.hidden = true;
463 self
464 }
465
466 pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
468 self.permissions = permissions;
469 self
470 }
471
472 pub fn with_model(mut self, model: ModelConfig) -> Self {
474 self.model = Some(model);
475 self
476 }
477
478 pub fn with_prompt(mut self, prompt: &str) -> Self {
480 self.prompt = Some(prompt.to_string());
481 self
482 }
483
484 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
486 self.max_steps = Some(max_steps);
487 self
488 }
489
490 pub fn has_defined_permissions(&self) -> bool {
492 !self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
493 }
494
495 pub(crate) fn apply_to(&self, config: &mut crate::agent::AgentConfig) {
501 use std::sync::Arc;
502
503 if config.permission_checker.is_none() && self.has_defined_permissions() {
504 config.permission_checker =
505 Some(Arc::new(self.permissions.clone()) as Arc<dyn PermissionChecker>);
506 config.permission_policy = Some(self.permissions.clone());
507 }
508
509 if let Some(ref prompt) = self.prompt {
510 if config.prompt_slots.extra.is_none() {
511 config.prompt_slots.extra = Some(prompt.clone());
512 }
513 }
514
515 if let Some(max_steps) = self.max_steps {
516 if config.max_tool_rounds == crate::agent::MAX_TOOL_ROUNDS {
517 config.max_tool_rounds = max_steps;
518 }
519 }
520
521 if config.confirmation_manager.is_none() {
523 let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
524 if self.has_defined_permissions() {
525 ConfirmationInheritance::AutoApprove
526 } else {
527 ConfirmationInheritance::DenyOnAsk
528 }
529 });
530 match inheritance {
531 ConfirmationInheritance::AutoApprove => {
532 config.confirmation_manager =
533 Some(Arc::new(crate::hitl::AutoApproveConfirmation));
534 }
535 ConfirmationInheritance::DenyOnAsk => { }
536 ConfirmationInheritance::InheritParent => { }
537 }
538 }
539 }
540
541 pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
543 self.confirmation_inheritance = Some(inheritance);
544 self
545 }
546}
547
548pub struct AgentRegistry {
553 agents: RwLock<HashMap<String, AgentDefinition>>,
554}
555
556fn canonical_agent_name(name: &str) -> &str {
557 match name.trim() {
558 "general-purpose" | "general_purpose" | "generalpurpose" => "general",
559 "verify" | "verifier" => "verification",
560 "code-review" | "code_reviewer" | "reviewer" => "review",
561 other => other,
562 }
563}
564
565impl Default for AgentRegistry {
566 fn default() -> Self {
567 Self::new()
568 }
569}
570
571impl AgentRegistry {
572 pub fn new() -> Self {
574 let registry = Self {
575 agents: RwLock::new(HashMap::new()),
576 };
577
578 for agent in builtin_agents() {
580 registry.register(agent);
581 }
582
583 registry
584 }
585
586 pub fn with_config(config: &CodeConfig) -> Self {
590 let registry = Self::new();
591
592 for dir in &config.agent_dirs {
594 let agents = load_agents_from_dir(dir);
595 for agent in agents {
596 tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
597 registry.register(agent);
598 }
599 }
600
601 registry
602 }
603
604 pub fn register(&self, agent: AgentDefinition) {
606 let mut agents = write_or_recover(&self.agents);
607 tracing::debug!("Registering agent: {}", agent.name);
608 agents.insert(agent.name.clone(), agent);
609 }
610
611 pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
616 let agent = spec.into_agent_definition();
617 self.register(agent.clone());
618 agent
619 }
620
621 pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
623 where
624 I: IntoIterator<Item = WorkerAgentSpec>,
625 {
626 specs
627 .into_iter()
628 .map(|spec| self.register_worker(spec))
629 .collect()
630 }
631
632 pub fn unregister(&self, name: &str) -> bool {
636 let mut agents = write_or_recover(&self.agents);
637 agents.remove(name).is_some()
638 }
639
640 pub fn get(&self, name: &str) -> Option<AgentDefinition> {
642 let agents = read_or_recover(&self.agents);
643 agents
644 .get(name)
645 .or_else(|| agents.get(canonical_agent_name(name)))
646 .cloned()
647 }
648
649 pub fn list(&self) -> Vec<AgentDefinition> {
651 let agents = read_or_recover(&self.agents);
652 agents.values().cloned().collect()
653 }
654
655 pub fn list_visible(&self) -> Vec<AgentDefinition> {
657 let agents = read_or_recover(&self.agents);
658 agents.values().filter(|a| !a.hidden).cloned().collect()
659 }
660
661 pub fn exists(&self, name: &str) -> bool {
663 let agents = read_or_recover(&self.agents);
664 agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
665 }
666
667 pub fn len(&self) -> usize {
669 let agents = read_or_recover(&self.agents);
670 agents.len()
671 }
672
673 pub fn is_empty(&self) -> bool {
675 self.len() == 0
676 }
677}
678
679pub fn parse_agent_yaml(content: &str) -> anyhow::Result<AgentDefinition> {
688 let value: serde_yaml::Value = serde_yaml::from_str(content)
689 .map_err(|e| anyhow::anyhow!("Failed to parse agent YAML: {}", e))?;
690
691 parse_agent_yaml_value(value, "agent YAML")
692}
693
694fn parse_agent_yaml_value(
695 value: serde_yaml::Value,
696 context: &str,
697) -> anyhow::Result<AgentDefinition> {
698 let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
699 .map(parse_tools_field)
700 .unwrap_or_default();
701 let disallowed_tools = yaml_get_any(
702 &value,
703 &["disallowedTools", "disallowed-tools", "disallowed_tools"],
704 )
705 .map(parse_tools_field)
706 .unwrap_or_default();
707
708 if yaml_value_has_key(&value, "kind") {
709 let mut spec: WorkerAgentSpec = serde_yaml::from_value(value)
710 .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
711 validate_agent_name(&spec.name)?;
712 apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
713 return Ok(spec.into_agent_definition());
714 }
715
716 let mut agent: AgentDefinition = serde_yaml::from_value(value)
717 .map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", context, e))?;
718 validate_agent_name(&agent.name)?;
719 apply_claude_style_tools_to_agent(&mut agent, &tools, &disallowed_tools);
720 Ok(agent)
721}
722
723fn apply_claude_style_tools_to_agent(
724 agent: &mut AgentDefinition,
725 tools: &[String],
726 disallowed_tools: &[String],
727) {
728 if !tools.is_empty() {
729 agent.permissions = allow_only_permission_policy(tools);
730 }
731 if !disallowed_tools.is_empty() {
732 let base = std::mem::take(&mut agent.permissions);
733 agent.permissions = add_denied_tools(base, disallowed_tools);
734 }
735 if (!tools.is_empty() || !disallowed_tools.is_empty())
736 && agent.confirmation_inheritance.is_none()
737 {
738 agent.confirmation_inheritance = Some(ConfirmationInheritance::AutoApprove);
739 }
740}
741
742fn apply_claude_style_tools_to_spec(
743 spec: &mut WorkerAgentSpec,
744 tools: &[String],
745 disallowed_tools: &[String],
746) {
747 if tools.is_empty() && disallowed_tools.is_empty() {
748 return;
749 }
750
751 let base = if tools.is_empty() {
752 spec.permissions
753 .clone()
754 .unwrap_or_else(|| spec.kind.default_permissions())
755 } else {
756 allow_only_permission_policy(tools)
757 };
758 spec.permissions = Some(add_denied_tools(base, disallowed_tools));
759 if spec.confirmation_inheritance.is_none() {
760 spec.confirmation_inheritance = Some(ConfirmationInheritance::AutoApprove);
761 }
762}
763
764fn parse_worker_yaml_value(
765 value: serde_yaml::Value,
766 context: &str,
767) -> anyhow::Result<WorkerAgentSpec> {
768 let spec: WorkerAgentSpec = serde_yaml::from_value(value)
769 .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
770 validate_agent_name(&spec.name)?;
771 Ok(spec)
772}
773
774fn yaml_value_has_key(value: &serde_yaml::Value, key: &str) -> bool {
775 value
776 .as_mapping()
777 .map(|mapping| mapping.contains_key(serde_yaml::Value::String(key.to_string())))
778 .unwrap_or(false)
779}
780
781fn yaml_get<'a>(value: &'a serde_yaml::Value, key: &str) -> Option<&'a serde_yaml::Value> {
782 value
783 .as_mapping()
784 .and_then(|mapping| mapping.get(serde_yaml::Value::String(key.to_string())))
785}
786
787fn yaml_get_any<'a>(value: &'a serde_yaml::Value, keys: &[&str]) -> Option<&'a serde_yaml::Value> {
788 keys.iter().find_map(|key| yaml_get(value, key))
789}
790
791fn parse_tools_field(value: &serde_yaml::Value) -> Vec<String> {
792 match value {
793 serde_yaml::Value::String(raw) => raw
794 .split(',')
795 .map(str::trim)
796 .filter(|tool| !tool.is_empty())
797 .map(str::to_string)
798 .collect(),
799 serde_yaml::Value::Sequence(items) => items
800 .iter()
801 .filter_map(|item| item.as_str())
802 .map(str::trim)
803 .filter(|tool| !tool.is_empty())
804 .map(str::to_string)
805 .collect(),
806 _ => Vec::new(),
807 }
808}
809
810fn tool_name_to_permission(tool: &str) -> String {
811 let normalized = tool.trim();
812 match normalized.to_ascii_lowercase().as_str() {
813 "*" => "*".to_string(),
814 "read" => "read(*)".to_string(),
815 "write" => "write(*)".to_string(),
816 "edit" => "edit(*)".to_string(),
817 "grep" => "grep(*)".to_string(),
818 "glob" => "glob(*)".to_string(),
819 "ls" => "ls(*)".to_string(),
820 "bash" => "bash(*)".to_string(),
821 "task" => "task(*)".to_string(),
822 "parallel_task" | "parallel-task" => "parallel_task(*)".to_string(),
823 _ if normalized.contains('(') => normalized.to_string(),
824 _ => format!("{normalized}(*)"),
825 }
826}
827
828fn permission_policy_from_tools(tools: &[String]) -> PermissionPolicy {
829 tools.iter().fold(PermissionPolicy::new(), |policy, tool| {
830 policy.allow(&tool_name_to_permission(tool))
831 })
832}
833
834fn allow_only_permission_policy(tools: &[String]) -> PermissionPolicy {
835 let mut policy = permission_policy_from_tools(tools);
836 policy.default_decision = PermissionDecision::Deny;
837 policy
838}
839
840fn add_denied_tools(mut policy: PermissionPolicy, tools: &[String]) -> PermissionPolicy {
841 for tool in tools {
842 policy = policy.deny(&tool_name_to_permission(tool));
843 }
844 policy
845}
846
847fn validate_agent_name(name: &str) -> anyhow::Result<()> {
848 if name.trim().is_empty() {
849 return Err(anyhow::anyhow!("Agent name is required"));
850 }
851 Ok(())
852}
853
854pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
858 let parts: Vec<&str> = content.splitn(3, "---").collect();
860
861 if parts.len() < 3 {
862 return Err(anyhow::anyhow!(
863 "Invalid markdown format: missing YAML frontmatter"
864 ));
865 }
866
867 let frontmatter = parts[1].trim();
868 let body = parts[2].trim();
869
870 let value: serde_yaml::Value = serde_yaml::from_str(frontmatter)
872 .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
873
874 if yaml_value_has_key(&value, "kind") {
875 let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
876 .map(parse_tools_field)
877 .unwrap_or_default();
878 let disallowed_tools = yaml_get_any(
879 &value,
880 &["disallowedTools", "disallowed-tools", "disallowed_tools"],
881 )
882 .map(parse_tools_field)
883 .unwrap_or_default();
884 let mut spec = parse_worker_yaml_value(value, "frontmatter")?;
885 if spec.prompt.is_none() && !body.is_empty() {
886 spec.prompt = Some(body.to_string());
887 }
888 apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
889 return Ok(spec.into_agent_definition());
890 }
891
892 let mut agent = parse_agent_yaml_value(value, "agent frontmatter")?;
893
894 if agent.prompt.is_none() && !body.is_empty() {
896 agent.prompt = Some(body.to_string());
897 }
898
899 Ok(agent)
900}
901
902pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
907 let mut agents = Vec::new();
908 load_agents_from_dir_inner(dir, &mut agents);
909 agents
910}
911
912fn load_agents_from_dir_inner(dir: &Path, agents: &mut Vec<AgentDefinition>) {
913 let Ok(entries) = std::fs::read_dir(dir) else {
914 tracing::warn!("Failed to read agent directory: {}", dir.display());
915 return;
916 };
917
918 for entry in entries.flatten() {
919 let path = entry.path();
920
921 if path.is_dir() {
922 load_agents_from_dir_inner(&path, agents);
923 continue;
924 }
925 if !path.is_file() {
926 continue;
927 }
928
929 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
930 continue;
931 };
932
933 let Ok(content) = std::fs::read_to_string(&path) else {
935 tracing::warn!("Failed to read agent file: {}", path.display());
936 continue;
937 };
938
939 let result = match ext {
941 "yaml" | "yml" => parse_agent_yaml(&content),
942 "md" => parse_agent_md(&content),
943 _ => continue,
944 };
945
946 match result {
947 Ok(agent) => {
948 tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
949 agents.push(agent);
950 }
951 Err(e) => {
952 tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
953 }
954 }
955 }
956}
957
958pub fn builtin_agents() -> Vec<AgentDefinition> {
960 vec![
961 AgentDefinition::new(
963 "explore",
964 "Fast codebase exploration agent. Use for searching files, reading code, \
965 and understanding codebase structure. Read-only operations only.",
966 )
967 .native()
968 .with_permissions(explore_permissions())
969 .with_max_steps(20)
970 .with_prompt(EXPLORE_PROMPT),
971 AgentDefinition::new(
973 "general",
974 "General-purpose agent for multi-step task execution. Can read, write, \
975 and execute commands.",
976 )
977 .native()
978 .with_permissions(general_permissions())
979 .with_max_steps(50),
980 AgentDefinition::new(
982 "plan",
983 "Planning agent for designing implementation approaches. Read-only access \
984 to explore codebase and create plans.",
985 )
986 .native()
987 .with_permissions(plan_permissions())
988 .with_max_steps(30)
989 .with_prompt(PLAN_PROMPT),
990 AgentDefinition::new(
992 "verification",
993 "Verification agent for adversarial validation. Prefer real checks, \
994 reproductions, and regression testing over code reading alone.",
995 )
996 .native()
997 .with_permissions(verification_permissions())
998 .with_max_steps(30)
999 .with_prompt(VERIFICATION_PROMPT),
1000 AgentDefinition::new(
1002 "review",
1003 "Code review agent focused on correctness, regressions, security, \
1004 maintainability, and clear findings.",
1005 )
1006 .native()
1007 .with_permissions(review_permissions())
1008 .with_max_steps(25)
1009 .with_prompt(REVIEW_PROMPT),
1010 ]
1011}
1012
1013fn explore_permissions() -> PermissionPolicy {
1019 let mut policy = PermissionPolicy::new()
1020 .allow_all(&["read", "grep", "glob", "ls"])
1021 .deny_all(&["write", "edit", "task", "parallel_task"])
1022 .allow("Bash(ls:*)")
1023 .allow("Bash(cat:*)")
1024 .allow("Bash(head:*)")
1025 .allow("Bash(tail:*)")
1026 .allow("Bash(find:*)")
1027 .allow("Bash(wc:*)")
1028 .deny("Bash(rm:*)")
1029 .deny("Bash(mv:*)")
1030 .deny("Bash(cp:*)");
1031 policy.default_decision = PermissionDecision::Deny;
1032 policy
1033}
1034
1035fn general_permissions() -> PermissionPolicy {
1037 PermissionPolicy::new()
1038 .allow_all(&[
1039 "read",
1040 "write",
1041 "edit",
1042 "grep",
1043 "glob",
1044 "ls",
1045 "bash",
1046 "web_fetch",
1047 "web_search",
1048 "git",
1049 "patch",
1050 "batch",
1051 "generate_object",
1052 ])
1053 .deny("task")
1054 .deny("parallel_task")
1055}
1056
1057fn plan_permissions() -> PermissionPolicy {
1059 let mut policy = PermissionPolicy::new()
1060 .allow_all(&["read", "grep", "glob", "ls"])
1061 .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
1062 policy.default_decision = PermissionDecision::Deny;
1063 policy
1064}
1065
1066fn verification_permissions() -> PermissionPolicy {
1068 let mut policy = PermissionPolicy::new()
1069 .allow_all(&["read", "grep", "glob", "ls", "bash"])
1070 .deny_all(&["write", "edit", "task", "parallel_task"]);
1071 policy.default_decision = PermissionDecision::Deny;
1072 policy
1073}
1074
1075fn review_permissions() -> PermissionPolicy {
1077 let mut policy = PermissionPolicy::new()
1078 .allow_all(&["read", "grep", "glob", "ls", "bash"])
1079 .deny_all(&["write", "edit", "task", "parallel_task"]);
1080 policy.default_decision = PermissionDecision::Deny;
1081 policy
1082}
1083
1084const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
1089
1090const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
1091
1092const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
1093
1094const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
1095
1096#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 #[test]
1105 fn test_agent_definition_builder() {
1106 let agent = AgentDefinition::new("test", "Test agent")
1107 .native()
1108 .hidden()
1109 .with_max_steps(10);
1110
1111 assert_eq!(agent.name, "test");
1112 assert_eq!(agent.description, "Test agent");
1113 assert!(agent.native);
1114 assert!(agent.hidden);
1115 assert_eq!(agent.max_steps, Some(10));
1116 }
1117
1118 #[test]
1119 fn test_agent_registry_new() {
1120 let registry = AgentRegistry::new();
1121
1122 assert!(registry.exists("explore"));
1124 assert!(registry.exists("general"));
1125 assert!(registry.exists("plan"));
1126 assert!(registry.exists("verification"));
1127 assert!(registry.exists("review"));
1128 assert!(registry.exists("general-purpose"));
1129 assert_eq!(registry.len(), 5);
1130 }
1131
1132 #[test]
1133 fn test_agent_registry_get() {
1134 let registry = AgentRegistry::new();
1135
1136 let explore = registry.get("explore").unwrap();
1137 assert_eq!(explore.name, "explore");
1138 assert!(explore.native);
1139 assert!(!explore.hidden);
1140
1141 let general = registry.get("general-purpose").unwrap();
1142 assert_eq!(general.name, "general");
1143
1144 assert!(registry.get("nonexistent").is_none());
1145 }
1146
1147 #[test]
1148 fn test_agent_registry_register_unregister() {
1149 let registry = AgentRegistry::new();
1150 let initial_count = registry.len();
1151
1152 let custom = AgentDefinition::new("custom", "Custom agent");
1154 registry.register(custom);
1155 assert_eq!(registry.len(), initial_count + 1);
1156 assert!(registry.exists("custom"));
1157
1158 assert!(registry.unregister("custom"));
1160 assert_eq!(registry.len(), initial_count);
1161 assert!(!registry.exists("custom"));
1162
1163 assert!(!registry.unregister("nonexistent"));
1165 }
1166
1167 #[test]
1168 fn test_agent_registry_list_visible() {
1169 let registry = AgentRegistry::new();
1170
1171 let visible = registry.list_visible();
1172 let all = registry.list();
1173
1174 assert_eq!(visible.len(), all.len());
1175 assert!(visible.iter().all(|a| !a.hidden));
1176 }
1177
1178 #[test]
1179 fn test_builtin_agents() {
1180 let agents = builtin_agents();
1181
1182 let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
1184 assert!(names.contains(&"explore"));
1185 assert!(names.contains(&"general"));
1186 assert!(names.contains(&"plan"));
1187 assert!(names.contains(&"verification"));
1188 assert!(names.contains(&"review"));
1189
1190 let explore = agents.iter().find(|a| a.name == "explore").unwrap();
1192 assert!(!explore.permissions.deny.is_empty());
1193 }
1194
1195 #[test]
1200 fn test_parse_agent_yaml() {
1201 let yaml = r#"
1202name: test-agent
1203description: A test agent
1204hidden: false
1205max_steps: 20
1206"#;
1207 let agent = parse_agent_yaml(yaml).unwrap();
1208 assert_eq!(agent.name, "test-agent");
1209 assert_eq!(agent.description, "A test agent");
1210 assert!(!agent.hidden);
1211 assert_eq!(agent.max_steps, Some(20));
1212 }
1213
1214 #[test]
1215 fn test_parse_agent_yaml_with_permissions() {
1216 let yaml = r#"
1217name: restricted-agent
1218description: Agent with permissions
1219permissions:
1220 allow:
1221 - rule: read
1222 - rule: grep
1223 deny:
1224 - rule: write
1225"#;
1226 let agent = parse_agent_yaml(yaml).unwrap();
1227 assert_eq!(agent.name, "restricted-agent");
1228 assert_eq!(agent.permissions.allow.len(), 2);
1229 assert_eq!(agent.permissions.deny.len(), 1);
1230 assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
1232 assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
1233 assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
1234 }
1235
1236 #[test]
1237 fn test_parse_agent_yaml_with_plain_string_permissions() {
1238 let yaml = r#"
1240name: plain-agent
1241description: Agent with plain string permissions
1242permissions:
1243 allow:
1244 - read
1245 - grep
1246 - "Bash(cargo:*)"
1247 deny:
1248 - write
1249"#;
1250 let agent = parse_agent_yaml(yaml).unwrap();
1251 assert_eq!(agent.name, "plain-agent");
1252 assert_eq!(agent.permissions.allow.len(), 3);
1253 assert_eq!(agent.permissions.deny.len(), 1);
1254 assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
1256 assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
1257 assert!(agent.permissions.allow[2]
1258 .matches("Bash", &serde_json::json!({"command": "cargo build"})));
1259 assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
1260 }
1261
1262 #[test]
1263 fn test_parse_claude_style_agent_md_tools_field() {
1264 let md = r#"---
1265name: code-reviewer
1266description: Use proactively after code changes to review quality
1267tools: Read, Grep, Glob, Bash
1268---
1269Review the changed code and return prioritized findings.
1270"#;
1271 let agent = parse_agent_md(md).unwrap();
1272
1273 assert_eq!(agent.name, "code-reviewer");
1274 assert_eq!(
1275 agent.confirmation_inheritance,
1276 Some(ConfirmationInheritance::AutoApprove)
1277 );
1278 assert!(agent
1279 .permissions
1280 .allow
1281 .iter()
1282 .any(|r| r.matches("read", &serde_json::json!({}))));
1283 assert!(agent
1284 .permissions
1285 .allow
1286 .iter()
1287 .any(|r| r.matches("grep", &serde_json::json!({}))));
1288 assert!(agent
1289 .permissions
1290 .allow
1291 .iter()
1292 .any(|r| r.matches("bash", &serde_json::json!({}))));
1293 assert_eq!(
1294 agent
1295 .permissions
1296 .check("write", &serde_json::json!({"file_path": "src/lib.rs"})),
1297 PermissionDecision::Deny
1298 );
1299 assert!(agent
1300 .prompt
1301 .as_deref()
1302 .unwrap_or_default()
1303 .contains("prioritized findings"));
1304 }
1305
1306 #[test]
1307 fn test_parse_claude_style_agent_md_disallowed_tools_field() {
1308 let md = r#"---
1309name: shell-checker
1310description: Use proactively to run safe shell checks
1311tools:
1312 - Read
1313 - Bash
1314disallowedTools:
1315 - Bash(rm:*)
1316 - Write
1317---
1318Run safe checks only.
1319"#;
1320 let agent = parse_agent_md(md).unwrap();
1321
1322 assert_eq!(agent.name, "shell-checker");
1323 assert_eq!(
1324 agent
1325 .permissions
1326 .check("bash", &serde_json::json!({"command": "rm -rf target"})),
1327 PermissionDecision::Deny
1328 );
1329 assert_eq!(
1330 agent
1331 .permissions
1332 .check("bash", &serde_json::json!({"command": "cargo test"})),
1333 PermissionDecision::Allow
1334 );
1335 assert_eq!(
1336 agent
1337 .permissions
1338 .check("write", &serde_json::json!({"file_path": "x"})),
1339 PermissionDecision::Deny
1340 );
1341 }
1342
1343 #[test]
1344 fn test_parse_worker_agent_md_supports_claude_tools_fields() {
1345 let md = r#"---
1346name: planner-worker
1347description: Plan work
1348kind: planner
1349tools: Read, Grep
1350disallowedTools: Grep(secret:*)
1351---
1352Plan without editing.
1353"#;
1354 let agent = parse_agent_md(md).unwrap();
1355
1356 assert_eq!(agent.name, "planner-worker");
1357 assert_eq!(
1358 agent
1359 .permissions
1360 .check("read", &serde_json::json!({"file_path": "src/lib.rs"})),
1361 PermissionDecision::Allow
1362 );
1363 assert_eq!(
1364 agent.permissions.check(
1365 "grep",
1366 &serde_json::json!({"pattern": "secret", "path": "src"})
1367 ),
1368 PermissionDecision::Deny
1369 );
1370 assert_eq!(
1371 agent
1372 .permissions
1373 .check("bash", &serde_json::json!({"command": "echo no"})),
1374 PermissionDecision::Deny
1375 );
1376 }
1377
1378 #[test]
1379 fn test_builtin_agent_permissions_are_bounded() {
1380 let registry = AgentRegistry::new();
1381 let explore = registry.get("explore").unwrap();
1382 let general = registry.get("general-purpose").unwrap();
1383
1384 assert_eq!(
1385 explore
1386 .permissions
1387 .check("bash", &serde_json::json!({"command": "cargo test"})),
1388 PermissionDecision::Deny
1389 );
1390 assert_eq!(
1391 explore
1392 .permissions
1393 .check("bash", &serde_json::json!({"command": "ls src"})),
1394 PermissionDecision::Allow
1395 );
1396 assert_eq!(
1397 general
1398 .permissions
1399 .check("parallel_task", &serde_json::json!({})),
1400 PermissionDecision::Deny
1401 );
1402 }
1403
1404 #[test]
1405 fn test_parse_worker_agent_yaml_uses_cattle_defaults() {
1406 let yaml = r#"
1407name: frontend-fixer
1408description: Disposable frontend implementer
1409kind: implementer
1410max_steps: 7
1411"#;
1412 let agent = parse_agent_yaml(yaml).unwrap();
1413
1414 assert_eq!(agent.name, "frontend-fixer");
1415 assert_eq!(agent.max_steps, Some(7));
1416 assert!(agent
1417 .permissions
1418 .allow
1419 .iter()
1420 .any(|r| r.matches("write", &serde_json::json!({}))));
1421 assert!(agent
1422 .permissions
1423 .deny
1424 .iter()
1425 .any(|r| r.matches("task", &serde_json::json!({}))));
1426 }
1427
1428 #[test]
1429 fn test_parse_agent_yaml_missing_name() {
1430 let yaml = r#"
1431description: Agent without name
1432"#;
1433 let result = parse_agent_yaml(yaml);
1434 assert!(result.is_err());
1435 }
1436
1437 #[test]
1438 fn test_parse_agent_md() {
1439 let md = r#"---
1440name: md-agent
1441description: Agent from markdown
1442max_steps: 15
1443---
1444# System Prompt
1445
1446You are a helpful agent.
1447Do your best work.
1448"#;
1449 let agent = parse_agent_md(md).unwrap();
1450 assert_eq!(agent.name, "md-agent");
1451 assert_eq!(agent.description, "Agent from markdown");
1452 assert_eq!(agent.max_steps, Some(15));
1453 assert!(agent.prompt.is_some());
1454 assert!(agent.prompt.unwrap().contains("helpful agent"));
1455 }
1456
1457 #[test]
1458 fn test_parse_agent_md_with_prompt_in_frontmatter() {
1459 let md = r#"---
1460name: prompt-agent
1461description: Agent with prompt in frontmatter
1462prompt: "Frontmatter prompt"
1463---
1464Body content that should be ignored
1465"#;
1466 let agent = parse_agent_md(md).unwrap();
1467 assert_eq!(agent.prompt.unwrap(), "Frontmatter prompt");
1468 }
1469
1470 #[test]
1471 fn test_parse_worker_agent_md_uses_body_prompt() {
1472 let md = r#"---
1473name: review-cow
1474description: Disposable review worker
1475kind: reviewer
1476---
1477Review only the staged diff and return prioritized findings.
1478"#;
1479 let agent = parse_agent_md(md).unwrap();
1480
1481 assert_eq!(agent.name, "review-cow");
1482 assert_eq!(
1483 agent.prompt.as_deref(),
1484 Some("Review only the staged diff and return prioritized findings.")
1485 );
1486 assert!(agent
1487 .permissions
1488 .deny
1489 .iter()
1490 .any(|r| r.matches("write", &serde_json::json!({}))));
1491 }
1492
1493 #[test]
1494 fn test_parse_agent_md_missing_frontmatter() {
1495 let md = "Just markdown without frontmatter";
1496 let result = parse_agent_md(md);
1497 assert!(result.is_err());
1498 }
1499
1500 #[test]
1501 fn test_load_agents_from_dir() {
1502 let temp_dir = tempfile::tempdir().unwrap();
1503
1504 std::fs::write(
1506 temp_dir.path().join("agent1.yaml"),
1507 r#"
1508name: yaml-agent
1509description: Agent from YAML file
1510"#,
1511 )
1512 .unwrap();
1513
1514 std::fs::write(
1516 temp_dir.path().join("agent2.md"),
1517 r#"---
1518name: md-agent
1519description: Agent from Markdown file
1520---
1521System prompt here
1522"#,
1523 )
1524 .unwrap();
1525
1526 std::fs::write(temp_dir.path().join("invalid.yaml"), "not: valid: yaml: [").unwrap();
1528
1529 std::fs::create_dir_all(temp_dir.path().join("nested")).unwrap();
1531 std::fs::write(
1532 temp_dir.path().join("nested").join("agent3.md"),
1533 r#"---
1534name: nested-agent
1535description: Agent from nested Markdown file
1536---
1537Nested prompt
1538"#,
1539 )
1540 .unwrap();
1541
1542 std::fs::write(temp_dir.path().join("readme.txt"), "Just a text file").unwrap();
1544
1545 let agents = load_agents_from_dir(temp_dir.path());
1546 assert_eq!(agents.len(), 3);
1547
1548 let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
1549 assert!(names.contains(&"yaml-agent"));
1550 assert!(names.contains(&"md-agent"));
1551 assert!(names.contains(&"nested-agent"));
1552 }
1553
1554 #[test]
1555 fn test_load_agents_from_nonexistent_dir() {
1556 let agents = load_agents_from_dir(std::path::Path::new("/nonexistent/dir"));
1557 assert!(agents.is_empty());
1558 }
1559
1560 #[test]
1561 fn test_registry_with_config() {
1562 let temp_dir = tempfile::tempdir().unwrap();
1563
1564 std::fs::write(
1566 temp_dir.path().join("custom.yaml"),
1567 r#"
1568name: custom-agent
1569description: Custom agent from config
1570"#,
1571 )
1572 .unwrap();
1573
1574 let config = CodeConfig::new().add_agent_dir(temp_dir.path());
1575 let registry = AgentRegistry::with_config(&config);
1576
1577 assert!(registry.exists("explore"));
1579 assert!(registry.exists("custom-agent"));
1580 assert_eq!(registry.len(), 6); }
1582
1583 #[test]
1584 fn test_agent_definition_with_model() {
1585 let model = ModelConfig {
1586 model: "claude-3-5-sonnet".to_string(),
1587 provider: Some("anthropic".to_string()),
1588 };
1589 let agent = AgentDefinition::new("test", "Test").with_model(model);
1590 assert!(agent.model.is_some());
1591 assert_eq!(agent.model.unwrap().provider, Some("anthropic".to_string()));
1592 }
1593
1594 #[test]
1595 fn test_model_config_from_model_ref() {
1596 let model = ModelConfig::from_model_ref("openai/gpt-4o");
1597 assert_eq!(model.provider.as_deref(), Some("openai"));
1598 assert_eq!(model.model, "gpt-4o");
1599 assert_eq!(model.model_ref(), "openai/gpt-4o");
1600
1601 let inherited = ModelConfig::from_model_ref("claude-sonnet");
1602 assert_eq!(inherited.provider, None);
1603 assert_eq!(inherited.model_ref(), "claude-sonnet");
1604 }
1605
1606 #[test]
1607 fn test_worker_agent_kind_from_str_accepts_aliases() {
1608 assert_eq!(
1609 "explore".parse::<WorkerAgentKind>().unwrap(),
1610 WorkerAgentKind::ReadOnly
1611 );
1612 assert_eq!(
1613 "general".parse::<WorkerAgentKind>().unwrap(),
1614 WorkerAgentKind::Implementer
1615 );
1616 assert!("unknown".parse::<WorkerAgentKind>().is_err());
1617 }
1618
1619 #[test]
1620 fn worker_spec_implementer_creates_cattle_agent_definition() {
1621 let agent = WorkerAgentSpec::implementer("frontend-fixer", "Fix frontend issues")
1622 .with_prompt("Focus on small, verified patches.")
1623 .with_provider_model("anthropic", "claude-sonnet")
1624 .with_max_steps(12)
1625 .into_agent_definition();
1626
1627 assert_eq!(agent.name, "frontend-fixer");
1628 assert_eq!(agent.max_steps, Some(12));
1629 assert_eq!(
1630 agent.prompt.as_deref(),
1631 Some("Focus on small, verified patches.")
1632 );
1633 assert_eq!(agent.model.unwrap().provider.as_deref(), Some("anthropic"));
1634 assert!(agent
1635 .permissions
1636 .allow
1637 .iter()
1638 .any(|r| r.matches("write", &serde_json::json!({}))));
1639 assert!(agent
1640 .permissions
1641 .deny
1642 .iter()
1643 .any(|r| r.matches("task", &serde_json::json!({}))));
1644 }
1645
1646 #[test]
1647 fn worker_spec_read_only_uses_safe_defaults() {
1648 let agent = WorkerAgentSpec::read_only("scanner", "Scan repository")
1649 .hidden(true)
1650 .into_agent_definition();
1651
1652 assert!(agent.hidden);
1653 assert_eq!(agent.max_steps, Some(20));
1654 assert!(agent.prompt.is_some());
1655 assert!(agent
1656 .permissions
1657 .allow
1658 .iter()
1659 .any(|r| r.matches("read", &serde_json::json!({}))));
1660 assert!(agent
1661 .permissions
1662 .deny
1663 .iter()
1664 .any(|r| r.matches("write", &serde_json::json!({}))));
1665 }
1666
1667 #[test]
1668 fn registry_register_worker_returns_and_stores_definition() {
1669 let registry = AgentRegistry::new();
1670 let agent =
1671 registry.register_worker(WorkerAgentSpec::custom("strict-worker", "Strict worker"));
1672
1673 assert_eq!(agent.name, "strict-worker");
1674 assert!(registry.exists("strict-worker"));
1675 assert_eq!(
1676 agent
1677 .permissions
1678 .check("bash", &serde_json::json!({"command":"echo hi"})),
1679 crate::permissions::PermissionDecision::Ask
1680 );
1681 }
1682
1683 #[test]
1684 fn registry_register_workers_batches_cattle_specs() {
1685 let registry = AgentRegistry::new();
1686 let agents = registry.register_workers([
1687 WorkerAgentSpec::planner("planner-cow", "Plan work"),
1688 WorkerAgentSpec::verifier("verify-cow", "Verify work"),
1689 ]);
1690
1691 assert_eq!(agents.len(), 2);
1692 assert!(registry.exists("planner-cow"));
1693 assert!(registry.exists("verify-cow"));
1694 }
1695
1696 #[test]
1697 fn test_agent_registry_default() {
1698 let registry = AgentRegistry::default();
1699 assert!(!registry.is_empty());
1700 assert_eq!(registry.len(), 5);
1701 }
1702
1703 #[test]
1704 fn test_agent_registry_is_empty() {
1705 let registry = AgentRegistry {
1706 agents: RwLock::new(HashMap::new()),
1707 };
1708 assert!(registry.is_empty());
1709 assert_eq!(registry.len(), 0);
1710 }
1711
1712 #[test]
1713 fn test_apply_to_sets_permissions() {
1714 use crate::agent::AgentConfig;
1715 use crate::permissions::PermissionDecision;
1716
1717 let def = AgentDefinition::new("writer", "Write files")
1718 .with_permissions(PermissionPolicy::new().allow("write(*)"));
1719
1720 let mut config = AgentConfig::default();
1721 assert!(config.permission_checker.is_none());
1722
1723 def.apply_to(&mut config);
1724
1725 assert!(config.permission_checker.is_some());
1726 assert!(config.permission_policy.is_some());
1727 let checker = config.permission_checker.unwrap();
1728 assert_eq!(
1729 checker.check(
1730 "write",
1731 &serde_json::json!({"file_path": "x.txt", "content": "hi"})
1732 ),
1733 PermissionDecision::Allow
1734 );
1735 }
1736
1737 #[test]
1738 fn test_apply_to_sets_prompt() {
1739 use crate::agent::AgentConfig;
1740
1741 let def = AgentDefinition::new("helper", "Help").with_prompt("Be helpful.");
1742 let mut config = AgentConfig::default();
1743
1744 def.apply_to(&mut config);
1745
1746 assert_eq!(config.prompt_slots.extra.as_deref(), Some("Be helpful."));
1747 }
1748
1749 #[test]
1750 fn test_apply_to_sets_max_steps() {
1751 use crate::agent::AgentConfig;
1752
1753 let def = AgentDefinition::new("fast", "Fast agent").with_max_steps(7);
1754 let mut config = AgentConfig::default();
1755
1756 def.apply_to(&mut config);
1757
1758 assert_eq!(config.max_tool_rounds, 7);
1759 }
1760
1761 #[test]
1762 fn test_apply_to_respects_host_overrides() {
1763 use crate::agent::AgentConfig;
1764
1765 let def = AgentDefinition::new("agent", "Agent")
1766 .with_permissions(PermissionPolicy::new().allow("bash(*)"))
1767 .with_prompt("Agent prompt.")
1768 .with_max_steps(10);
1769
1770 let mut config = AgentConfig::default();
1771 config.prompt_slots.extra = Some("Host prompt.".to_string());
1772 config.max_tool_rounds = 25;
1773 config.permission_checker = Some(std::sync::Arc::new(PermissionPolicy::new().allow("*")));
1774
1775 def.apply_to(&mut config);
1776
1777 assert_eq!(config.prompt_slots.extra.as_deref(), Some("Host prompt."));
1779 assert_eq!(config.max_tool_rounds, 25);
1780 }
1781
1782 #[test]
1783 fn test_apply_to_skips_empty_permissions() {
1784 use crate::agent::AgentConfig;
1785
1786 let def = AgentDefinition::new("empty", "No permissions");
1787 let mut config = AgentConfig::default();
1788
1789 def.apply_to(&mut config);
1790
1791 assert!(config.permission_checker.is_none());
1792 assert!(config.permission_policy.is_none());
1793 }
1794}