Skip to main content

a3s_code_core/
subagent.rs

1//! Delegated Agent System
2//!
3//! Provides a system for delegating specialized tasks to focused child agents.
4//! Each delegated child run uses an isolated child session with restricted permissions.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! Parent Session
10//!   └── Task Tool
11//!         ├── AgentRegistry (lookup agent definitions)
12//!         └── Child Session (isolated execution)
13//!               ├── Restricted permissions
14//!               ├── Optional model override
15//!               └── Event forwarding to parent
16//! ```
17//!
18//! ## Built-in Agents
19//!
20//! - `explore`: Fast codebase exploration (read-only)
21//! - `general`: Multi-step task execution
22//! - `deep-research`: Parent-policy evidence collection (hidden)
23//! - `plan`: Read-only planning mode
24//! - `verification`: Adversarial verification specialist
25//! - `review`: Code review specialist
26//!
27//! ## Loading Agents from Files
28//!
29//! Agents can be loaded from YAML or Markdown files:
30//!
31//! ### YAML Format
32//! ```yaml
33//! name: my-agent
34//! description: Custom agent for specific tasks
35//! hidden: false
36//! max_steps: 30
37//! permissions:
38//!   allow:
39//!     - read
40//!     - grep
41//!   deny:
42//!     - write
43//! prompt: |
44//!   You are a specialized agent...
45//! ```
46//!
47//! ### Markdown Format
48//! ```markdown
49//! ---
50//! name: my-agent
51//! description: Custom agent
52//! max_steps: 30
53//! ---
54//! # System Prompt
55//! You are a specialized agent...
56//! ```
57
58use crate::config::CodeConfig;
59use crate::permissions::{PermissionChecker, PermissionDecision, PermissionPolicy};
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::path::Path;
63use std::sync::RwLock;
64
65use crate::error::{read_or_recover, write_or_recover};
66
67/// How a child run resolves tools that require confirmation (PermissionDecision::Ask).
68#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum ConfirmationInheritance {
71    /// Auto-approve all Ask decisions. Safe when the agent has explicit allow-list
72    /// permissions that already define the access boundary.
73    #[default]
74    AutoApprove,
75    /// Deny all Ask decisions (strict mode — only explicitly allowed tools run).
76    DenyOnAsk,
77    /// Inherit the parent session's confirmation manager.
78    InheritParent,
79}
80
81/// Model configuration for agent.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ModelConfig {
84    /// Model identifier (e.g., "claude-3-5-sonnet-20241022")
85    pub model: String,
86    /// Optional provider override
87    pub provider: Option<String>,
88}
89
90impl ModelConfig {
91    /// Create a model override that inherits the default provider.
92    pub fn new(model: impl Into<String>) -> Self {
93        Self {
94            model: model.into(),
95            provider: None,
96        }
97    }
98
99    /// Create a model override from provider and model parts.
100    pub fn with_provider(provider: impl Into<String>, model: impl Into<String>) -> Self {
101        Self {
102            model: model.into(),
103            provider: Some(provider.into()),
104        }
105    }
106
107    /// Parse a conventional `provider/model` reference.
108    ///
109    /// If no slash is present, the provider stays unset and the session binding
110    /// falls back to the host agent's default provider behavior.
111    pub fn from_model_ref(model_ref: impl AsRef<str>) -> Self {
112        let model_ref = model_ref.as_ref();
113        if let Some((provider, model)) = model_ref.split_once('/') {
114            Self::with_provider(provider, model)
115        } else {
116            Self::new(model_ref)
117        }
118    }
119
120    /// Return the model as `provider/model` when a provider is set.
121    pub fn model_ref(&self) -> String {
122        match &self.provider {
123            Some(provider) => format!("{}/{}", provider, self.model),
124            None => self.model.clone(),
125        }
126    }
127}
128
129/// Cattle-style worker agent role.
130///
131/// A worker role is a reproducible preset for disposable, task-scoped agents.
132/// Use [`WorkerAgentSpec`] to create many consistent workers instead of hand-tuning
133/// unique "pet" agents one by one.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum WorkerAgentKind {
137    /// Read-only exploration: fast code search and inspection.
138    #[serde(alias = "readonly", alias = "read-only", alias = "explore")]
139    ReadOnly,
140    /// Read-only planning: design work without modifying the workspace.
141    #[serde(alias = "plan")]
142    Planner,
143    /// Implementation work: read, edit, write, and run commands; no recursive task spawning.
144    #[serde(alias = "implementation", alias = "general")]
145    Implementer,
146    /// Verification work: run checks and inspect failures without editing files.
147    #[serde(alias = "verification", alias = "verify")]
148    Verifier,
149    /// Review work: inspect changes and report findings.
150    #[serde(alias = "review", alias = "code-review")]
151    Reviewer,
152    /// Strict custom worker: asks for any unspecified tool until permissions are supplied.
153    Custom,
154}
155
156/// Reproducible recipe for a disposable worker/subagent.
157///
158/// This is the public "cattle mode" API: callers define a small, serializable
159/// worker spec and register/spawn it repeatedly. The spec compiles to an
160/// [`AgentDefinition`] consumed by the existing delegation/runtime pipeline.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct WorkerAgentSpec {
163    /// Stable worker name used in task delegation (e.g., "frontend-fixer").
164    pub name: String,
165    /// Human-readable purpose shown to users and model selectors.
166    pub description: String,
167    /// Preset permission/prompt/step profile.
168    pub kind: WorkerAgentKind,
169    /// Hide from UI lists while still allowing explicit delegation.
170    #[serde(default)]
171    pub hidden: bool,
172    /// Optional permission policy override.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub permissions: Option<PermissionPolicy>,
175    /// Optional model override.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub model: Option<ModelConfig>,
178    /// Optional worker-specific prompt appended to the core agentic prompt.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub prompt: Option<String>,
181    /// Maximum execution steps/tool rounds.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub max_steps: Option<usize>,
184    /// How child runs resolve Ask decisions.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub confirmation_inheritance: Option<ConfirmationInheritance>,
187}
188
189impl WorkerAgentKind {
190    /// Stable snake_case identifier for this role.
191    pub fn as_str(self) -> &'static str {
192        match self {
193            Self::ReadOnly => "read_only",
194            Self::Planner => "planner",
195            Self::Implementer => "implementer",
196            Self::Verifier => "verifier",
197            Self::Reviewer => "reviewer",
198            Self::Custom => "custom",
199        }
200    }
201
202    fn default_permissions(self) -> PermissionPolicy {
203        match self {
204            Self::ReadOnly => explore_permissions(),
205            Self::Planner => plan_permissions(),
206            Self::Implementer => general_permissions(),
207            Self::Verifier => verification_permissions(),
208            Self::Reviewer => review_permissions(),
209            Self::Custom => PermissionPolicy::strict(),
210        }
211    }
212
213    fn default_prompt(self) -> Option<&'static str> {
214        match self {
215            Self::ReadOnly => Some(EXPLORE_PROMPT),
216            Self::Planner => Some(PLAN_PROMPT),
217            Self::Verifier => Some(VERIFICATION_PROMPT),
218            Self::Reviewer => Some(REVIEW_PROMPT),
219            Self::Implementer | Self::Custom => None,
220        }
221    }
222
223    fn default_max_steps(self) -> usize {
224        match self {
225            Self::ReadOnly => 20,
226            Self::Planner => 30,
227            Self::Implementer => 50,
228            Self::Verifier => 30,
229            Self::Reviewer => 25,
230            Self::Custom => 30,
231        }
232    }
233}
234
235impl std::fmt::Display for WorkerAgentKind {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        f.write_str(self.as_str())
238    }
239}
240
241impl std::str::FromStr for WorkerAgentKind {
242    type Err = anyhow::Error;
243
244    fn from_str(value: &str) -> anyhow::Result<Self> {
245        match value.trim().to_ascii_lowercase().as_str() {
246            "read_only" | "readonly" | "read-only" | "explore" | "scanner" => Ok(Self::ReadOnly),
247            "planner" | "plan" => Ok(Self::Planner),
248            "implementer" | "implementation" | "general" | "executor" => Ok(Self::Implementer),
249            "verifier" | "verification" | "verify" | "tester" => Ok(Self::Verifier),
250            "reviewer" | "review" | "code-review" | "code_reviewer" => Ok(Self::Reviewer),
251            "custom" => Ok(Self::Custom),
252            other => Err(anyhow::anyhow!("unknown worker agent kind '{}'", other)),
253        }
254    }
255}
256
257/// Backward-friendly alias for callers that name this pattern cattle mode.
258pub type CattleAgentKind = WorkerAgentKind;
259/// Backward-friendly alias for callers that name this pattern cattle mode.
260pub type CattleAgentSpec = WorkerAgentSpec;
261
262impl WorkerAgentSpec {
263    /// Create a worker spec from an explicit preset.
264    pub fn new(
265        kind: WorkerAgentKind,
266        name: impl Into<String>,
267        description: impl Into<String>,
268    ) -> Self {
269        Self {
270            name: name.into(),
271            description: description.into(),
272            kind,
273            hidden: false,
274            permissions: None,
275            model: None,
276            prompt: None,
277            max_steps: None,
278            confirmation_inheritance: None,
279        }
280    }
281
282    /// Read-only exploration worker.
283    pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
284        Self::new(WorkerAgentKind::ReadOnly, name, description)
285    }
286
287    /// Read-only planning worker.
288    pub fn planner(name: impl Into<String>, description: impl Into<String>) -> Self {
289        Self::new(WorkerAgentKind::Planner, name, description)
290    }
291
292    /// Implementation worker with read/write/bash capability and no recursive task spawning.
293    pub fn implementer(name: impl Into<String>, description: impl Into<String>) -> Self {
294        Self::new(WorkerAgentKind::Implementer, name, description)
295    }
296
297    /// Verification worker for tests/checks/reproductions without edits.
298    pub fn verifier(name: impl Into<String>, description: impl Into<String>) -> Self {
299        Self::new(WorkerAgentKind::Verifier, name, description)
300    }
301
302    /// Review worker for correctness/regression/security findings.
303    pub fn reviewer(name: impl Into<String>, description: impl Into<String>) -> Self {
304        Self::new(WorkerAgentKind::Reviewer, name, description)
305    }
306
307    /// Strict custom worker. Provide permissions explicitly for non-HITL execution.
308    pub fn custom(name: impl Into<String>, description: impl Into<String>) -> Self {
309        Self::new(WorkerAgentKind::Custom, name, description)
310    }
311
312    /// Hide or show this worker in UI lists.
313    pub fn hidden(mut self, hidden: bool) -> Self {
314        self.hidden = hidden;
315        self
316    }
317
318    /// Override the preset permission policy.
319    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
320        self.permissions = Some(permissions);
321        self
322    }
323
324    /// Override the preset model.
325    pub fn with_model(mut self, model: ModelConfig) -> Self {
326        self.model = Some(model);
327        self
328    }
329
330    /// Override the preset model using `provider/model` or a model id.
331    pub fn with_model_ref(mut self, model_ref: impl AsRef<str>) -> Self {
332        self.model = Some(ModelConfig::from_model_ref(model_ref));
333        self
334    }
335
336    /// Override the preset model using provider and model separately.
337    pub fn with_provider_model(
338        mut self,
339        provider: impl Into<String>,
340        model: impl Into<String>,
341    ) -> Self {
342        self.model = Some(ModelConfig::with_provider(provider, model));
343        self
344    }
345
346    /// Override the preset prompt.
347    pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
348        self.prompt = Some(prompt.into());
349        self
350    }
351
352    /// Override the preset step budget.
353    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
354        self.max_steps = Some(max_steps);
355        self
356    }
357
358    /// Set confirmation inheritance policy for child runs.
359    pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
360        self.confirmation_inheritance = Some(inheritance);
361        self
362    }
363
364    /// Compile this worker recipe into a runtime agent definition.
365    pub fn into_agent_definition(self) -> AgentDefinition {
366        let mut agent = AgentDefinition::new(&self.name, &self.description)
367            .with_permissions(
368                self.permissions
369                    .unwrap_or_else(|| self.kind.default_permissions()),
370            )
371            .with_max_steps(
372                self.max_steps
373                    .unwrap_or_else(|| self.kind.default_max_steps()),
374            );
375
376        if self.hidden {
377            agent = agent.hidden();
378        }
379        if let Some(model) = self.model {
380            agent = agent.with_model(model);
381        }
382        if let Some(prompt) = self
383            .prompt
384            .or_else(|| self.kind.default_prompt().map(str::to_string))
385        {
386            agent = agent.with_prompt(&prompt);
387        }
388        if let Some(ci) = self.confirmation_inheritance {
389            agent = agent.with_confirmation(ci);
390        }
391        agent
392    }
393}
394
395impl From<WorkerAgentSpec> for AgentDefinition {
396    fn from(spec: WorkerAgentSpec) -> Self {
397        spec.into_agent_definition()
398    }
399}
400
401/// Agent definition
402///
403/// Defines the configuration and capabilities of an agent type.
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct AgentDefinition {
406    /// Agent identifier (e.g., "explore", "plan", "general")
407    pub name: String,
408    /// Description of what the agent does
409    pub description: String,
410    /// Whether this is a built-in agent
411    #[serde(default)]
412    pub native: bool,
413    /// Whether to hide from UI
414    #[serde(default)]
415    pub hidden: bool,
416    /// Permission rules for this agent
417    #[serde(default)]
418    pub permissions: PermissionPolicy,
419    /// Optional model override
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub model: Option<ModelConfig>,
422    /// System prompt for this agent
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub prompt: Option<String>,
425    /// Maximum execution steps (tool rounds)
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub max_steps: Option<usize>,
428    /// How child runs resolve Ask decisions. Default: AutoApprove when
429    /// the agent has explicit allow rules, DenyOnAsk otherwise.
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub confirmation_inheritance: Option<ConfirmationInheritance>,
432}
433
434impl AgentDefinition {
435    /// Create a new agent definition
436    pub fn new(name: &str, description: &str) -> Self {
437        Self {
438            name: name.to_string(),
439            description: description.to_string(),
440            native: false,
441            hidden: false,
442            permissions: PermissionPolicy::default(),
443            model: None,
444            prompt: None,
445            max_steps: None,
446            confirmation_inheritance: None,
447        }
448    }
449
450    /// Create an agent definition from a disposable worker recipe.
451    pub fn worker(spec: WorkerAgentSpec) -> Self {
452        spec.into_agent_definition()
453    }
454
455    /// Mark as native (built-in)
456    pub fn native(mut self) -> Self {
457        self.native = true;
458        self
459    }
460
461    /// Mark as hidden from UI
462    pub fn hidden(mut self) -> Self {
463        self.hidden = true;
464        self
465    }
466
467    /// Set permission policy
468    pub fn with_permissions(mut self, permissions: PermissionPolicy) -> Self {
469        self.permissions = permissions;
470        self
471    }
472
473    /// Set model override
474    pub fn with_model(mut self, model: ModelConfig) -> Self {
475        self.model = Some(model);
476        self
477    }
478
479    /// Set system prompt
480    pub fn with_prompt(mut self, prompt: &str) -> Self {
481        self.prompt = Some(prompt.to_string());
482        self
483    }
484
485    /// Set maximum execution steps
486    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
487        self.max_steps = Some(max_steps);
488        self
489    }
490
491    /// Whether this definition has non-empty permission rules.
492    pub fn has_defined_permissions(&self) -> bool {
493        !self.permissions.allow.is_empty() || !self.permissions.deny.is_empty()
494    }
495
496    /// Apply this definition's declared configuration to a mutable AgentConfig.
497    ///
498    /// Follows the "host overrides win" principle: only fills fields that are
499    /// currently at their default/None state. Callers who want to force values
500    /// should set them *after* calling `apply_to`.
501    pub(crate) fn apply_to(&self, config: &mut crate::agent::AgentConfig) {
502        use std::sync::Arc;
503
504        if config.permission_checker.is_none() && self.has_defined_permissions() {
505            config.permission_checker =
506                Some(Arc::new(self.permissions.clone()) as Arc<dyn PermissionChecker>);
507            config.permission_policy = Some(self.permissions.clone());
508        }
509
510        if let Some(ref prompt) = self.prompt {
511            if config.prompt_slots.extra.is_none() {
512                config.prompt_slots.extra = Some(prompt.clone());
513            }
514        }
515
516        if let Some(max_steps) = self.max_steps {
517            if config.max_tool_rounds == crate::agent::MAX_TOOL_ROUNDS {
518                config.max_tool_rounds = max_steps;
519            }
520        }
521
522        // Confirmation inheritance: resolve Ask decisions in child runs.
523        if config.confirmation_manager.is_none() {
524            let inheritance = self.confirmation_inheritance.clone().unwrap_or_else(|| {
525                if self.has_defined_permissions() {
526                    ConfirmationInheritance::AutoApprove
527                } else {
528                    ConfirmationInheritance::DenyOnAsk
529                }
530            });
531            match inheritance {
532                ConfirmationInheritance::AutoApprove => {
533                    config.confirmation_manager =
534                        Some(Arc::new(crate::hitl::AutoApproveConfirmation));
535                }
536                ConfirmationInheritance::DenyOnAsk => { /* leave None — safety_gate denies */ }
537                ConfirmationInheritance::InheritParent => { /* caller passes parent's manager */ }
538            }
539        }
540    }
541
542    /// Set confirmation inheritance policy for child runs.
543    pub fn with_confirmation(mut self, inheritance: ConfirmationInheritance) -> Self {
544        self.confirmation_inheritance = Some(inheritance);
545        self
546    }
547}
548
549/// Agent registry for managing agent definitions
550///
551/// Thread-safe registry that stores agent definitions and provides
552/// lookup functionality.
553pub struct AgentRegistry {
554    agents: RwLock<HashMap<String, AgentDefinition>>,
555}
556
557fn canonical_agent_name(name: &str) -> &str {
558    match name.trim() {
559        "general-purpose" | "general_purpose" | "generalpurpose" => "general",
560        "deep_research" | "deepresearch" => "deep-research",
561        "verify" | "verifier" => "verification",
562        "code-review" | "code_reviewer" | "reviewer" => "review",
563        other => other,
564    }
565}
566
567impl Default for AgentRegistry {
568    fn default() -> Self {
569        Self::new()
570    }
571}
572
573impl AgentRegistry {
574    /// Create a new agent registry with built-in agents
575    pub fn new() -> Self {
576        let registry = Self {
577            agents: RwLock::new(HashMap::new()),
578        };
579
580        // Register built-in agents
581        for agent in builtin_agents() {
582            registry.register(agent);
583        }
584
585        registry
586    }
587
588    /// Create a new agent registry with configuration
589    ///
590    /// Loads built-in agents first, then loads agents from configured directories.
591    pub fn with_config(config: &CodeConfig) -> Self {
592        let registry = Self::new();
593
594        // Load agents from configured directories
595        for dir in &config.agent_dirs {
596            let agents = load_agents_from_dir(dir);
597            for agent in agents {
598                tracing::info!("Loaded agent '{}' from {}", agent.name, dir.display());
599                registry.register(agent);
600            }
601        }
602
603        registry
604    }
605
606    /// Register an agent definition
607    pub fn register(&self, agent: AgentDefinition) {
608        let mut agents = write_or_recover(&self.agents);
609        tracing::debug!("Registering agent: {}", agent.name);
610        agents.insert(agent.name.clone(), agent);
611    }
612
613    /// Register a disposable worker agent from a reproducible spec.
614    ///
615    /// Returns the compiled [`AgentDefinition`] so callers can inspect or pass it
616    /// directly to `session_for_agent`.
617    pub fn register_worker(&self, spec: WorkerAgentSpec) -> AgentDefinition {
618        let agent = spec.into_agent_definition();
619        self.register(agent.clone());
620        agent
621    }
622
623    /// Register multiple disposable worker agents and return their definitions.
624    pub fn register_workers<I>(&self, specs: I) -> Vec<AgentDefinition>
625    where
626        I: IntoIterator<Item = WorkerAgentSpec>,
627    {
628        specs
629            .into_iter()
630            .map(|spec| self.register_worker(spec))
631            .collect()
632    }
633
634    /// Unregister an agent by name
635    ///
636    /// Returns true if the agent was removed, false if not found.
637    pub fn unregister(&self, name: &str) -> bool {
638        let mut agents = write_or_recover(&self.agents);
639        agents.remove(name).is_some()
640    }
641
642    /// Get an agent definition by name
643    pub fn get(&self, name: &str) -> Option<AgentDefinition> {
644        let agents = read_or_recover(&self.agents);
645        agents
646            .get(name)
647            .or_else(|| agents.get(canonical_agent_name(name)))
648            .cloned()
649    }
650
651    /// List all registered agents
652    pub fn list(&self) -> Vec<AgentDefinition> {
653        let agents = read_or_recover(&self.agents);
654        agents.values().cloned().collect()
655    }
656
657    /// List visible agents (not hidden)
658    pub fn list_visible(&self) -> Vec<AgentDefinition> {
659        let agents = read_or_recover(&self.agents);
660        agents.values().filter(|a| !a.hidden).cloned().collect()
661    }
662
663    /// Check if an agent exists
664    pub fn exists(&self, name: &str) -> bool {
665        let agents = read_or_recover(&self.agents);
666        agents.contains_key(name) || agents.contains_key(canonical_agent_name(name))
667    }
668
669    /// Get the number of registered agents
670    pub fn len(&self) -> usize {
671        let agents = read_or_recover(&self.agents);
672        agents.len()
673    }
674
675    /// Check if the registry is empty
676    pub fn is_empty(&self) -> bool {
677        self.len() == 0
678    }
679}
680
681// ============================================================================
682// Agent File Loading
683// ============================================================================
684
685/// Parse an agent definition from YAML content.
686///
687/// The YAML can describe either a full [`AgentDefinition`] or a cattle-style
688/// [`WorkerAgentSpec`] by including a `kind` field.
689pub fn parse_agent_yaml(content: &str) -> anyhow::Result<AgentDefinition> {
690    let value: serde_yaml::Value = serde_yaml::from_str(content)
691        .map_err(|e| anyhow::anyhow!("Failed to parse agent YAML: {}", e))?;
692
693    parse_agent_yaml_value(value, "agent YAML")
694}
695
696fn parse_agent_yaml_value(
697    value: serde_yaml::Value,
698    context: &str,
699) -> anyhow::Result<AgentDefinition> {
700    let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
701        .map(parse_tools_field)
702        .unwrap_or_default();
703    let disallowed_tools = yaml_get_any(
704        &value,
705        &["disallowedTools", "disallowed-tools", "disallowed_tools"],
706    )
707    .map(parse_tools_field)
708    .unwrap_or_default();
709
710    if yaml_value_has_key(&value, "kind") {
711        let mut spec: WorkerAgentSpec = serde_yaml::from_value(value)
712            .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
713        validate_agent_name(&spec.name)?;
714        apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
715        return Ok(spec.into_agent_definition());
716    }
717
718    let mut agent: AgentDefinition = serde_yaml::from_value(value)
719        .map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", context, e))?;
720    validate_agent_name(&agent.name)?;
721    apply_claude_style_tools_to_agent(&mut agent, &tools, &disallowed_tools);
722    Ok(agent)
723}
724
725fn apply_claude_style_tools_to_agent(
726    agent: &mut AgentDefinition,
727    tools: &[String],
728    disallowed_tools: &[String],
729) {
730    if !tools.is_empty() {
731        agent.permissions = allow_only_permission_policy(tools);
732    }
733    if !disallowed_tools.is_empty() {
734        let base = std::mem::take(&mut agent.permissions);
735        agent.permissions = add_denied_tools(base, disallowed_tools);
736    }
737    if (!tools.is_empty() || !disallowed_tools.is_empty())
738        && agent.confirmation_inheritance.is_none()
739    {
740        agent.confirmation_inheritance = Some(ConfirmationInheritance::AutoApprove);
741    }
742}
743
744fn apply_claude_style_tools_to_spec(
745    spec: &mut WorkerAgentSpec,
746    tools: &[String],
747    disallowed_tools: &[String],
748) {
749    if tools.is_empty() && disallowed_tools.is_empty() {
750        return;
751    }
752
753    let base = if tools.is_empty() {
754        spec.permissions
755            .clone()
756            .unwrap_or_else(|| spec.kind.default_permissions())
757    } else {
758        allow_only_permission_policy(tools)
759    };
760    spec.permissions = Some(add_denied_tools(base, disallowed_tools));
761    if spec.confirmation_inheritance.is_none() {
762        spec.confirmation_inheritance = Some(ConfirmationInheritance::AutoApprove);
763    }
764}
765
766fn parse_worker_yaml_value(
767    value: serde_yaml::Value,
768    context: &str,
769) -> anyhow::Result<WorkerAgentSpec> {
770    let spec: WorkerAgentSpec = serde_yaml::from_value(value)
771        .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
772    validate_agent_name(&spec.name)?;
773    Ok(spec)
774}
775
776fn yaml_value_has_key(value: &serde_yaml::Value, key: &str) -> bool {
777    value
778        .as_mapping()
779        .map(|mapping| mapping.contains_key(serde_yaml::Value::String(key.to_string())))
780        .unwrap_or(false)
781}
782
783fn yaml_get<'a>(value: &'a serde_yaml::Value, key: &str) -> Option<&'a serde_yaml::Value> {
784    value
785        .as_mapping()
786        .and_then(|mapping| mapping.get(serde_yaml::Value::String(key.to_string())))
787}
788
789fn yaml_get_any<'a>(value: &'a serde_yaml::Value, keys: &[&str]) -> Option<&'a serde_yaml::Value> {
790    keys.iter().find_map(|key| yaml_get(value, key))
791}
792
793fn parse_tools_field(value: &serde_yaml::Value) -> Vec<String> {
794    match value {
795        serde_yaml::Value::String(raw) => raw
796            .split(',')
797            .map(str::trim)
798            .filter(|tool| !tool.is_empty())
799            .map(str::to_string)
800            .collect(),
801        serde_yaml::Value::Sequence(items) => items
802            .iter()
803            .filter_map(|item| item.as_str())
804            .map(str::trim)
805            .filter(|tool| !tool.is_empty())
806            .map(str::to_string)
807            .collect(),
808        _ => Vec::new(),
809    }
810}
811
812fn tool_name_to_permission(tool: &str) -> String {
813    let normalized = tool.trim();
814    match normalized.to_ascii_lowercase().as_str() {
815        "*" => "*".to_string(),
816        "read" => "read(*)".to_string(),
817        "write" => "write(*)".to_string(),
818        "edit" => "edit(*)".to_string(),
819        "grep" => "grep(*)".to_string(),
820        "glob" => "glob(*)".to_string(),
821        "ls" => "ls(*)".to_string(),
822        "bash" => "bash(*)".to_string(),
823        "task" => "task(*)".to_string(),
824        "parallel_task" | "parallel-task" => "parallel_task(*)".to_string(),
825        _ if normalized.contains('(') => normalized.to_string(),
826        _ => format!("{normalized}(*)"),
827    }
828}
829
830fn permission_policy_from_tools(tools: &[String]) -> PermissionPolicy {
831    tools.iter().fold(PermissionPolicy::new(), |policy, tool| {
832        policy.allow(&tool_name_to_permission(tool))
833    })
834}
835
836fn allow_only_permission_policy(tools: &[String]) -> PermissionPolicy {
837    let mut policy = permission_policy_from_tools(tools);
838    policy.default_decision = PermissionDecision::Deny;
839    policy
840}
841
842fn add_denied_tools(mut policy: PermissionPolicy, tools: &[String]) -> PermissionPolicy {
843    for tool in tools {
844        policy = policy.deny(&tool_name_to_permission(tool));
845    }
846    policy
847}
848
849fn validate_agent_name(name: &str) -> anyhow::Result<()> {
850    if name.trim().is_empty() {
851        return Err(anyhow::anyhow!("Agent name is required"));
852    }
853    Ok(())
854}
855
856/// Parse an agent definition from Markdown with YAML frontmatter
857///
858/// The frontmatter contains agent metadata, and the body becomes the prompt.
859pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
860    // Parse frontmatter (YAML between --- markers)
861    let parts: Vec<&str> = content.splitn(3, "---").collect();
862
863    if parts.len() < 3 {
864        return Err(anyhow::anyhow!(
865            "Invalid markdown format: missing YAML frontmatter"
866        ));
867    }
868
869    let frontmatter = parts[1].trim();
870    let body = parts[2].trim();
871
872    // Parse the frontmatter as YAML. A `kind` field selects WorkerAgentSpec.
873    let value: serde_yaml::Value = serde_yaml::from_str(frontmatter)
874        .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
875
876    if yaml_value_has_key(&value, "kind") {
877        let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
878            .map(parse_tools_field)
879            .unwrap_or_default();
880        let disallowed_tools = yaml_get_any(
881            &value,
882            &["disallowedTools", "disallowed-tools", "disallowed_tools"],
883        )
884        .map(parse_tools_field)
885        .unwrap_or_default();
886        let mut spec = parse_worker_yaml_value(value, "frontmatter")?;
887        if spec.prompt.is_none() && !body.is_empty() {
888            spec.prompt = Some(body.to_string());
889        }
890        apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
891        return Ok(spec.into_agent_definition());
892    }
893
894    let mut agent = parse_agent_yaml_value(value, "agent frontmatter")?;
895
896    // Use body as prompt if not already set in frontmatter.
897    if agent.prompt.is_none() && !body.is_empty() {
898        agent.prompt = Some(body.to_string());
899    }
900
901    Ok(agent)
902}
903
904/// Load all agent definitions from a directory
905///
906/// Scans for *.yaml and *.md files and parses them as agent definitions.
907/// Invalid files are logged and skipped.
908pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
909    let mut agents = Vec::new();
910    load_agents_from_dir_inner(dir, &mut agents);
911    agents
912}
913
914fn load_agents_from_dir_inner(dir: &Path, agents: &mut Vec<AgentDefinition>) {
915    let Ok(entries) = std::fs::read_dir(dir) else {
916        tracing::warn!("Failed to read agent directory: {}", dir.display());
917        return;
918    };
919
920    for entry in entries.flatten() {
921        let path = entry.path();
922
923        if path.is_dir() {
924            load_agents_from_dir_inner(&path, agents);
925            continue;
926        }
927        if !path.is_file() {
928            continue;
929        }
930
931        let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
932            continue;
933        };
934
935        // Read file content
936        let Ok(content) = std::fs::read_to_string(&path) else {
937            tracing::warn!("Failed to read agent file: {}", path.display());
938            continue;
939        };
940
941        // Parse based on extension
942        let result = match ext {
943            "yaml" | "yml" => parse_agent_yaml(&content),
944            "md" => parse_agent_md(&content),
945            _ => continue,
946        };
947
948        match result {
949            Ok(agent) => {
950                tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
951                agents.push(agent);
952            }
953            Err(e) => {
954                tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
955            }
956        }
957    }
958}
959
960/// Create built-in agent definitions
961pub fn builtin_agents() -> Vec<AgentDefinition> {
962    vec![
963        // Explore agent: Fast codebase exploration (read-only)
964        AgentDefinition::new(
965            "explore",
966            "Fast read-only exploration agent. Use for searching files, reading code, \
967             understanding codebase structure, and gathering external web evidence.",
968        )
969        .native()
970        .with_permissions(explore_permissions())
971        .with_max_steps(20)
972        .with_prompt(EXPLORE_PROMPT),
973        // General agent: Multi-step task execution
974        AgentDefinition::new(
975            "general",
976            "General-purpose agent for multi-step task execution. Can read, write, \
977             and execute commands.",
978        )
979        .native()
980        .with_permissions(general_permissions())
981        .with_max_steps(50),
982        // DeepResearch evidence agent: inherit the parent session's tool/skill
983        // policy instead of applying explore's read-only default-deny policy.
984        AgentDefinition::new(
985            "deep-research",
986            "DeepResearch evidence agent. Inherits the parent session's tools, \
987             skills, permissions, and HITL policy for bounded evidence collection.",
988        )
989        .native()
990        .hidden()
991        .with_confirmation(ConfirmationInheritance::InheritParent)
992        .with_max_steps(50),
993        // Plan agent: Read-only planning mode
994        AgentDefinition::new(
995            "plan",
996            "Planning agent for designing implementation approaches. Read-only access \
997             to explore codebase and create plans.",
998        )
999        .native()
1000        .with_permissions(plan_permissions())
1001        .with_max_steps(30)
1002        .with_prompt(PLAN_PROMPT),
1003        // Verification agent: adversarial validation and repro
1004        AgentDefinition::new(
1005            "verification",
1006            "Verification agent for adversarial validation. Prefer real checks, \
1007             reproductions, and regression testing over code reading alone.",
1008        )
1009        .native()
1010        .with_permissions(verification_permissions())
1011        .with_max_steps(30)
1012        .with_prompt(VERIFICATION_PROMPT),
1013        // Review agent: review-focused analysis
1014        AgentDefinition::new(
1015            "review",
1016            "Code review agent focused on correctness, regressions, security, \
1017             maintainability, and clear findings.",
1018        )
1019        .native()
1020        .with_permissions(review_permissions())
1021        .with_max_steps(25)
1022        .with_prompt(REVIEW_PROMPT),
1023    ]
1024}
1025
1026// ============================================================================
1027// Permission Policies for Built-in Agents
1028// ============================================================================
1029
1030/// Permission policy for explore agent (read-only)
1031fn explore_permissions() -> PermissionPolicy {
1032    let mut policy = PermissionPolicy::new()
1033        .allow_all(&["read", "grep", "glob", "ls", "web_fetch", "web_search"])
1034        .deny_all(&["write", "edit", "task", "parallel_task"])
1035        .allow("Bash(ls:*)")
1036        .allow("Bash(cat:*)")
1037        .allow("Bash(head:*)")
1038        .allow("Bash(tail:*)")
1039        .allow("Bash(find:*)")
1040        .allow("Bash(wc:*)")
1041        .deny("Bash(rm:*)")
1042        .deny("Bash(mv:*)")
1043        .deny("Bash(cp:*)");
1044    policy.default_decision = PermissionDecision::Deny;
1045    policy
1046}
1047
1048/// Permission policy for general agent (full access except task)
1049fn general_permissions() -> PermissionPolicy {
1050    PermissionPolicy::new()
1051        .allow_all(&[
1052            "read",
1053            "write",
1054            "edit",
1055            "grep",
1056            "glob",
1057            "ls",
1058            "bash",
1059            "web_fetch",
1060            "web_search",
1061            "git",
1062            "patch",
1063            "batch",
1064            "generate_object",
1065        ])
1066        .deny("task")
1067        .deny("parallel_task")
1068}
1069
1070/// Permission policy for plan agent (read-only)
1071fn plan_permissions() -> PermissionPolicy {
1072    let mut policy = PermissionPolicy::new()
1073        .allow_all(&["read", "grep", "glob", "ls"])
1074        .deny_all(&["write", "edit", "bash", "task", "parallel_task"]);
1075    policy.default_decision = PermissionDecision::Deny;
1076    policy
1077}
1078
1079/// Permission policy for verification agent (read-heavy with runtime checks)
1080fn verification_permissions() -> PermissionPolicy {
1081    let mut policy = PermissionPolicy::new()
1082        .allow_all(&[
1083            "read",
1084            "grep",
1085            "glob",
1086            "ls",
1087            "bash",
1088            "web_fetch",
1089            "web_search",
1090        ])
1091        .deny_all(&["write", "edit", "task", "parallel_task"]);
1092    policy.default_decision = PermissionDecision::Deny;
1093    policy
1094}
1095
1096/// Permission policy for review agent (read-heavy with optional lightweight checks)
1097fn review_permissions() -> PermissionPolicy {
1098    let mut policy = PermissionPolicy::new()
1099        .allow_all(&[
1100            "read",
1101            "grep",
1102            "glob",
1103            "ls",
1104            "bash",
1105            "web_fetch",
1106            "web_search",
1107        ])
1108        .deny_all(&["write", "edit", "task", "parallel_task"]);
1109    policy.default_decision = PermissionDecision::Deny;
1110    policy
1111}
1112
1113// ============================================================================
1114// System Prompts for Built-in Agents
1115// ============================================================================
1116
1117const EXPLORE_PROMPT: &str = crate::prompts::AGENT_EXPLORE;
1118
1119const PLAN_PROMPT: &str = crate::prompts::AGENT_PLAN;
1120
1121const VERIFICATION_PROMPT: &str = crate::prompts::AGENT_VERIFICATION;
1122
1123const REVIEW_PROMPT: &str = crate::prompts::AGENT_CODE_REVIEW;
1124
1125// ============================================================================
1126// Tests
1127// ============================================================================
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132
1133    #[test]
1134    fn test_agent_definition_builder() {
1135        let agent = AgentDefinition::new("test", "Test agent")
1136            .native()
1137            .hidden()
1138            .with_max_steps(10);
1139
1140        assert_eq!(agent.name, "test");
1141        assert_eq!(agent.description, "Test agent");
1142        assert!(agent.native);
1143        assert!(agent.hidden);
1144        assert_eq!(agent.max_steps, Some(10));
1145    }
1146
1147    #[test]
1148    fn test_agent_registry_new() {
1149        let registry = AgentRegistry::new();
1150
1151        // Should have built-in agents
1152        assert!(registry.exists("explore"));
1153        assert!(registry.exists("general"));
1154        assert!(registry.exists("plan"));
1155        assert!(registry.exists("verification"));
1156        assert!(registry.exists("review"));
1157        assert!(registry.exists("general-purpose"));
1158        assert!(registry.exists("deepresearch"));
1159        assert_eq!(registry.len(), 6);
1160    }
1161
1162    #[test]
1163    fn test_agent_registry_get() {
1164        let registry = AgentRegistry::new();
1165
1166        let explore = registry.get("explore").unwrap();
1167        assert_eq!(explore.name, "explore");
1168        assert!(explore.native);
1169        assert!(!explore.hidden);
1170
1171        let general = registry.get("general-purpose").unwrap();
1172        assert_eq!(general.name, "general");
1173
1174        assert!(registry.get("nonexistent").is_none());
1175    }
1176
1177    #[test]
1178    fn test_agent_registry_register_unregister() {
1179        let registry = AgentRegistry::new();
1180        let initial_count = registry.len();
1181
1182        // Register custom agent
1183        let custom = AgentDefinition::new("custom", "Custom agent");
1184        registry.register(custom);
1185        assert_eq!(registry.len(), initial_count + 1);
1186        assert!(registry.exists("custom"));
1187
1188        // Unregister
1189        assert!(registry.unregister("custom"));
1190        assert_eq!(registry.len(), initial_count);
1191        assert!(!registry.exists("custom"));
1192
1193        // Unregister non-existent
1194        assert!(!registry.unregister("nonexistent"));
1195    }
1196
1197    #[test]
1198    fn test_agent_registry_list_visible() {
1199        let registry = AgentRegistry::new();
1200
1201        let visible = registry.list_visible();
1202        let all = registry.list();
1203
1204        assert!(visible.len() < all.len());
1205        assert!(visible.iter().all(|a| !a.hidden));
1206        assert!(!visible.iter().any(|a| a.name == "deep-research"));
1207    }
1208
1209    #[test]
1210    fn test_builtin_agents() {
1211        let agents = builtin_agents();
1212
1213        // Check we have expected agents
1214        let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
1215        assert!(names.contains(&"explore"));
1216        assert!(names.contains(&"general"));
1217        assert!(names.contains(&"deep-research"));
1218        assert!(names.contains(&"plan"));
1219        assert!(names.contains(&"verification"));
1220        assert!(names.contains(&"review"));
1221
1222        // Check explore is read-only (has deny rules for write)
1223        let explore = agents.iter().find(|a| a.name == "explore").unwrap();
1224        assert!(!explore.permissions.deny.is_empty());
1225        let deep_research = agents.iter().find(|a| a.name == "deep-research").unwrap();
1226        assert!(deep_research.hidden);
1227        assert!(deep_research.permissions.allow.is_empty());
1228        assert!(deep_research.permissions.deny.is_empty());
1229        assert_eq!(
1230            deep_research.confirmation_inheritance,
1231            Some(ConfirmationInheritance::InheritParent)
1232        );
1233    }
1234
1235    // ========================================================================
1236    // Agent File Loading Tests
1237    // ========================================================================
1238
1239    #[test]
1240    fn test_parse_agent_yaml() {
1241        let yaml = r#"
1242name: test-agent
1243description: A test agent
1244hidden: false
1245max_steps: 20
1246"#;
1247        let agent = parse_agent_yaml(yaml).unwrap();
1248        assert_eq!(agent.name, "test-agent");
1249        assert_eq!(agent.description, "A test agent");
1250        assert!(!agent.hidden);
1251        assert_eq!(agent.max_steps, Some(20));
1252    }
1253
1254    #[test]
1255    fn test_parse_agent_yaml_with_permissions() {
1256        let yaml = r#"
1257name: restricted-agent
1258description: Agent with permissions
1259permissions:
1260  allow:
1261    - rule: read
1262    - rule: grep
1263  deny:
1264    - rule: write
1265"#;
1266        let agent = parse_agent_yaml(yaml).unwrap();
1267        assert_eq!(agent.name, "restricted-agent");
1268        assert_eq!(agent.permissions.allow.len(), 2);
1269        assert_eq!(agent.permissions.deny.len(), 1);
1270        // Verify that deserialized rules actually match (tool_name populated)
1271        assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
1272        assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
1273        assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
1274    }
1275
1276    #[test]
1277    fn test_parse_agent_yaml_with_plain_string_permissions() {
1278        // Users naturally write plain strings in allow/deny lists
1279        let yaml = r#"
1280name: plain-agent
1281description: Agent with plain string permissions
1282permissions:
1283  allow:
1284    - read
1285    - grep
1286    - "Bash(cargo:*)"
1287  deny:
1288    - write
1289"#;
1290        let agent = parse_agent_yaml(yaml).unwrap();
1291        assert_eq!(agent.name, "plain-agent");
1292        assert_eq!(agent.permissions.allow.len(), 3);
1293        assert_eq!(agent.permissions.deny.len(), 1);
1294        // Verify rules are functional
1295        assert!(agent.permissions.allow[0].matches("read", &serde_json::json!({})));
1296        assert!(agent.permissions.allow[1].matches("grep", &serde_json::json!({})));
1297        assert!(agent.permissions.allow[2]
1298            .matches("Bash", &serde_json::json!({"command": "cargo build"})));
1299        assert!(agent.permissions.deny[0].matches("write", &serde_json::json!({})));
1300    }
1301
1302    #[test]
1303    fn test_parse_claude_style_agent_md_tools_field() {
1304        let md = r#"---
1305name: code-reviewer
1306description: Use proactively after code changes to review quality
1307tools: Read, Grep, Glob, Bash
1308---
1309Review the changed code and return prioritized findings.
1310"#;
1311        let agent = parse_agent_md(md).unwrap();
1312
1313        assert_eq!(agent.name, "code-reviewer");
1314        assert_eq!(
1315            agent.confirmation_inheritance,
1316            Some(ConfirmationInheritance::AutoApprove)
1317        );
1318        assert!(agent
1319            .permissions
1320            .allow
1321            .iter()
1322            .any(|r| r.matches("read", &serde_json::json!({}))));
1323        assert!(agent
1324            .permissions
1325            .allow
1326            .iter()
1327            .any(|r| r.matches("grep", &serde_json::json!({}))));
1328        assert!(agent
1329            .permissions
1330            .allow
1331            .iter()
1332            .any(|r| r.matches("bash", &serde_json::json!({}))));
1333        assert_eq!(
1334            agent
1335                .permissions
1336                .check("write", &serde_json::json!({"file_path": "src/lib.rs"})),
1337            PermissionDecision::Deny
1338        );
1339        assert!(agent
1340            .prompt
1341            .as_deref()
1342            .unwrap_or_default()
1343            .contains("prioritized findings"));
1344    }
1345
1346    #[test]
1347    fn test_parse_claude_style_agent_md_disallowed_tools_field() {
1348        let md = r#"---
1349name: shell-checker
1350description: Use proactively to run safe shell checks
1351tools:
1352  - Read
1353  - Bash
1354disallowedTools:
1355  - Bash(rm:*)
1356  - Write
1357---
1358Run safe checks only.
1359"#;
1360        let agent = parse_agent_md(md).unwrap();
1361
1362        assert_eq!(agent.name, "shell-checker");
1363        assert_eq!(
1364            agent
1365                .permissions
1366                .check("bash", &serde_json::json!({"command": "rm -rf target"})),
1367            PermissionDecision::Deny
1368        );
1369        assert_eq!(
1370            agent
1371                .permissions
1372                .check("bash", &serde_json::json!({"command": "cargo test"})),
1373            PermissionDecision::Allow
1374        );
1375        assert_eq!(
1376            agent
1377                .permissions
1378                .check("write", &serde_json::json!({"file_path": "x"})),
1379            PermissionDecision::Deny
1380        );
1381    }
1382
1383    #[test]
1384    fn test_parse_worker_agent_md_supports_claude_tools_fields() {
1385        let md = r#"---
1386name: planner-worker
1387description: Plan work
1388kind: planner
1389tools: Read, Grep
1390disallowedTools: Grep(secret:*)
1391---
1392Plan without editing.
1393"#;
1394        let agent = parse_agent_md(md).unwrap();
1395
1396        assert_eq!(agent.name, "planner-worker");
1397        assert_eq!(
1398            agent
1399                .permissions
1400                .check("read", &serde_json::json!({"file_path": "src/lib.rs"})),
1401            PermissionDecision::Allow
1402        );
1403        assert_eq!(
1404            agent.permissions.check(
1405                "grep",
1406                &serde_json::json!({"pattern": "secret", "path": "src"})
1407            ),
1408            PermissionDecision::Deny
1409        );
1410        assert_eq!(
1411            agent
1412                .permissions
1413                .check("bash", &serde_json::json!({"command": "echo no"})),
1414            PermissionDecision::Deny
1415        );
1416    }
1417
1418    #[test]
1419    fn test_builtin_agent_permissions_are_bounded() {
1420        let registry = AgentRegistry::new();
1421        let explore = registry.get("explore").unwrap();
1422        let general = registry.get("general-purpose").unwrap();
1423        let deep_research = registry.get("deepresearch").unwrap();
1424        let verification = registry.get("verification").unwrap();
1425        let review = registry.get("review").unwrap();
1426
1427        assert_eq!(
1428            explore
1429                .permissions
1430                .check("bash", &serde_json::json!({"command": "cargo test"})),
1431            PermissionDecision::Deny
1432        );
1433        assert_eq!(
1434            explore
1435                .permissions
1436                .check("bash", &serde_json::json!({"command": "ls src"})),
1437            PermissionDecision::Allow
1438        );
1439        assert!(
1440            !deep_research.has_defined_permissions(),
1441            "DeepResearch child agent should inherit parent session permissions"
1442        );
1443        assert_eq!(
1444            explore
1445                .permissions
1446                .check("web_search", &serde_json::json!({"query": "a3s"})),
1447            PermissionDecision::Allow
1448        );
1449        assert_eq!(
1450            explore.permissions.check(
1451                "web_fetch",
1452                &serde_json::json!({"url": "https://example.com"})
1453            ),
1454            PermissionDecision::Allow
1455        );
1456        assert_eq!(
1457            explore
1458                .permissions
1459                .check("write", &serde_json::json!({"file_path": "x"})),
1460            PermissionDecision::Deny
1461        );
1462        assert_eq!(
1463            general
1464                .permissions
1465                .check("parallel_task", &serde_json::json!({})),
1466            PermissionDecision::Deny
1467        );
1468        for agent in [verification, review] {
1469            assert_eq!(
1470                agent
1471                    .permissions
1472                    .check("bash", &serde_json::json!({"command": "cargo test"})),
1473                PermissionDecision::Allow,
1474                "{} should allow runtime checks",
1475                agent.name
1476            );
1477            assert_eq!(
1478                agent
1479                    .permissions
1480                    .check("web_search", &serde_json::json!({"query": "a3s"})),
1481                PermissionDecision::Allow,
1482                "{} should allow evidence search",
1483                agent.name
1484            );
1485            assert_eq!(
1486                agent.permissions.check(
1487                    "web_fetch",
1488                    &serde_json::json!({"url": "https://example.com"})
1489                ),
1490                PermissionDecision::Allow,
1491                "{} should allow source fetches",
1492                agent.name
1493            );
1494            assert_eq!(
1495                agent
1496                    .permissions
1497                    .check("write", &serde_json::json!({"file_path": "x"})),
1498                PermissionDecision::Deny,
1499                "{} should stay read-only for workspace writes",
1500                agent.name
1501            );
1502            assert_eq!(
1503                agent
1504                    .permissions
1505                    .check("parallel_task", &serde_json::json!({})),
1506                PermissionDecision::Deny,
1507                "{} should not recurse into more subagents",
1508                agent.name
1509            );
1510        }
1511    }
1512
1513    #[test]
1514    fn test_parse_worker_agent_yaml_uses_cattle_defaults() {
1515        let yaml = r#"
1516name: frontend-fixer
1517description: Disposable frontend implementer
1518kind: implementer
1519max_steps: 7
1520"#;
1521        let agent = parse_agent_yaml(yaml).unwrap();
1522
1523        assert_eq!(agent.name, "frontend-fixer");
1524        assert_eq!(agent.max_steps, Some(7));
1525        assert!(agent
1526            .permissions
1527            .allow
1528            .iter()
1529            .any(|r| r.matches("write", &serde_json::json!({}))));
1530        assert!(agent
1531            .permissions
1532            .deny
1533            .iter()
1534            .any(|r| r.matches("task", &serde_json::json!({}))));
1535    }
1536
1537    #[test]
1538    fn test_parse_agent_yaml_missing_name() {
1539        let yaml = r#"
1540description: Agent without name
1541"#;
1542        let result = parse_agent_yaml(yaml);
1543        assert!(result.is_err());
1544    }
1545
1546    #[test]
1547    fn test_parse_agent_md() {
1548        let md = r#"---
1549name: md-agent
1550description: Agent from markdown
1551max_steps: 15
1552---
1553# System Prompt
1554
1555You are a helpful agent.
1556Do your best work.
1557"#;
1558        let agent = parse_agent_md(md).unwrap();
1559        assert_eq!(agent.name, "md-agent");
1560        assert_eq!(agent.description, "Agent from markdown");
1561        assert_eq!(agent.max_steps, Some(15));
1562        assert!(agent.prompt.is_some());
1563        assert!(agent.prompt.unwrap().contains("helpful agent"));
1564    }
1565
1566    #[test]
1567    fn test_parse_agent_md_with_prompt_in_frontmatter() {
1568        let md = r#"---
1569name: prompt-agent
1570description: Agent with prompt in frontmatter
1571prompt: "Frontmatter prompt"
1572---
1573Body content that should be ignored
1574"#;
1575        let agent = parse_agent_md(md).unwrap();
1576        assert_eq!(agent.prompt.unwrap(), "Frontmatter prompt");
1577    }
1578
1579    #[test]
1580    fn test_parse_worker_agent_md_uses_body_prompt() {
1581        let md = r#"---
1582name: review-cow
1583description: Disposable review worker
1584kind: reviewer
1585---
1586Review only the staged diff and return prioritized findings.
1587"#;
1588        let agent = parse_agent_md(md).unwrap();
1589
1590        assert_eq!(agent.name, "review-cow");
1591        assert_eq!(
1592            agent.prompt.as_deref(),
1593            Some("Review only the staged diff and return prioritized findings.")
1594        );
1595        assert!(agent
1596            .permissions
1597            .deny
1598            .iter()
1599            .any(|r| r.matches("write", &serde_json::json!({}))));
1600    }
1601
1602    #[test]
1603    fn test_parse_agent_md_missing_frontmatter() {
1604        let md = "Just markdown without frontmatter";
1605        let result = parse_agent_md(md);
1606        assert!(result.is_err());
1607    }
1608
1609    #[test]
1610    fn test_load_agents_from_dir() {
1611        let temp_dir = tempfile::tempdir().unwrap();
1612
1613        // Create a YAML agent file
1614        std::fs::write(
1615            temp_dir.path().join("agent1.yaml"),
1616            r#"
1617name: yaml-agent
1618description: Agent from YAML file
1619"#,
1620        )
1621        .unwrap();
1622
1623        // Create a Markdown agent file
1624        std::fs::write(
1625            temp_dir.path().join("agent2.md"),
1626            r#"---
1627name: md-agent
1628description: Agent from Markdown file
1629---
1630System prompt here
1631"#,
1632        )
1633        .unwrap();
1634
1635        // Create an invalid file (should be skipped)
1636        std::fs::write(temp_dir.path().join("invalid.yaml"), "not: valid: yaml: [").unwrap();
1637
1638        // Create a nested agent file (Claude-style directories are recursive)
1639        std::fs::create_dir_all(temp_dir.path().join("nested")).unwrap();
1640        std::fs::write(
1641            temp_dir.path().join("nested").join("agent3.md"),
1642            r#"---
1643name: nested-agent
1644description: Agent from nested Markdown file
1645---
1646Nested prompt
1647"#,
1648        )
1649        .unwrap();
1650
1651        // Create a non-agent file (should be skipped)
1652        std::fs::write(temp_dir.path().join("readme.txt"), "Just a text file").unwrap();
1653
1654        let agents = load_agents_from_dir(temp_dir.path());
1655        assert_eq!(agents.len(), 3);
1656
1657        let names: Vec<&str> = agents.iter().map(|a| a.name.as_str()).collect();
1658        assert!(names.contains(&"yaml-agent"));
1659        assert!(names.contains(&"md-agent"));
1660        assert!(names.contains(&"nested-agent"));
1661    }
1662
1663    #[test]
1664    fn test_load_agents_from_nonexistent_dir() {
1665        let agents = load_agents_from_dir(std::path::Path::new("/nonexistent/dir"));
1666        assert!(agents.is_empty());
1667    }
1668
1669    #[test]
1670    fn test_registry_with_config() {
1671        let temp_dir = tempfile::tempdir().unwrap();
1672
1673        // Create an agent file
1674        std::fs::write(
1675            temp_dir.path().join("custom.yaml"),
1676            r#"
1677name: custom-agent
1678description: Custom agent from config
1679"#,
1680        )
1681        .unwrap();
1682
1683        let config = CodeConfig::new().add_agent_dir(temp_dir.path());
1684        let registry = AgentRegistry::with_config(&config);
1685
1686        // Should have built-in agents plus custom agent
1687        assert!(registry.exists("explore"));
1688        assert!(registry.exists("custom-agent"));
1689        assert_eq!(registry.len(), 7); // 6 built-in + 1 custom
1690    }
1691
1692    #[test]
1693    fn test_agent_definition_with_model() {
1694        let model = ModelConfig {
1695            model: "claude-3-5-sonnet".to_string(),
1696            provider: Some("anthropic".to_string()),
1697        };
1698        let agent = AgentDefinition::new("test", "Test").with_model(model);
1699        assert!(agent.model.is_some());
1700        assert_eq!(agent.model.unwrap().provider, Some("anthropic".to_string()));
1701    }
1702
1703    #[test]
1704    fn test_model_config_from_model_ref() {
1705        let model = ModelConfig::from_model_ref("openai/gpt-4o");
1706        assert_eq!(model.provider.as_deref(), Some("openai"));
1707        assert_eq!(model.model, "gpt-4o");
1708        assert_eq!(model.model_ref(), "openai/gpt-4o");
1709
1710        let inherited = ModelConfig::from_model_ref("claude-sonnet");
1711        assert_eq!(inherited.provider, None);
1712        assert_eq!(inherited.model_ref(), "claude-sonnet");
1713    }
1714
1715    #[test]
1716    fn test_worker_agent_kind_from_str_accepts_aliases() {
1717        assert_eq!(
1718            "explore".parse::<WorkerAgentKind>().unwrap(),
1719            WorkerAgentKind::ReadOnly
1720        );
1721        assert_eq!(
1722            "general".parse::<WorkerAgentKind>().unwrap(),
1723            WorkerAgentKind::Implementer
1724        );
1725        assert!("unknown".parse::<WorkerAgentKind>().is_err());
1726    }
1727
1728    #[test]
1729    fn worker_spec_implementer_creates_cattle_agent_definition() {
1730        let agent = WorkerAgentSpec::implementer("frontend-fixer", "Fix frontend issues")
1731            .with_prompt("Focus on small, verified patches.")
1732            .with_provider_model("anthropic", "claude-sonnet")
1733            .with_max_steps(12)
1734            .into_agent_definition();
1735
1736        assert_eq!(agent.name, "frontend-fixer");
1737        assert_eq!(agent.max_steps, Some(12));
1738        assert_eq!(
1739            agent.prompt.as_deref(),
1740            Some("Focus on small, verified patches.")
1741        );
1742        assert_eq!(agent.model.unwrap().provider.as_deref(), Some("anthropic"));
1743        assert!(agent
1744            .permissions
1745            .allow
1746            .iter()
1747            .any(|r| r.matches("write", &serde_json::json!({}))));
1748        assert!(agent
1749            .permissions
1750            .deny
1751            .iter()
1752            .any(|r| r.matches("task", &serde_json::json!({}))));
1753    }
1754
1755    #[test]
1756    fn worker_spec_read_only_uses_safe_defaults() {
1757        let agent = WorkerAgentSpec::read_only("scanner", "Scan repository")
1758            .hidden(true)
1759            .into_agent_definition();
1760
1761        assert!(agent.hidden);
1762        assert_eq!(agent.max_steps, Some(20));
1763        assert!(agent.prompt.is_some());
1764        assert!(agent
1765            .permissions
1766            .allow
1767            .iter()
1768            .any(|r| r.matches("read", &serde_json::json!({}))));
1769        assert!(agent
1770            .permissions
1771            .deny
1772            .iter()
1773            .any(|r| r.matches("write", &serde_json::json!({}))));
1774    }
1775
1776    #[test]
1777    fn registry_register_worker_returns_and_stores_definition() {
1778        let registry = AgentRegistry::new();
1779        let agent =
1780            registry.register_worker(WorkerAgentSpec::custom("strict-worker", "Strict worker"));
1781
1782        assert_eq!(agent.name, "strict-worker");
1783        assert!(registry.exists("strict-worker"));
1784        assert_eq!(
1785            agent
1786                .permissions
1787                .check("bash", &serde_json::json!({"command":"echo hi"})),
1788            crate::permissions::PermissionDecision::Ask
1789        );
1790    }
1791
1792    #[test]
1793    fn registry_register_workers_batches_cattle_specs() {
1794        let registry = AgentRegistry::new();
1795        let agents = registry.register_workers([
1796            WorkerAgentSpec::planner("planner-cow", "Plan work"),
1797            WorkerAgentSpec::verifier("verify-cow", "Verify work"),
1798        ]);
1799
1800        assert_eq!(agents.len(), 2);
1801        assert!(registry.exists("planner-cow"));
1802        assert!(registry.exists("verify-cow"));
1803    }
1804
1805    #[test]
1806    fn test_agent_registry_default() {
1807        let registry = AgentRegistry::default();
1808        assert!(!registry.is_empty());
1809        assert_eq!(registry.len(), 6);
1810    }
1811
1812    #[test]
1813    fn test_agent_registry_is_empty() {
1814        let registry = AgentRegistry {
1815            agents: RwLock::new(HashMap::new()),
1816        };
1817        assert!(registry.is_empty());
1818        assert_eq!(registry.len(), 0);
1819    }
1820
1821    #[test]
1822    fn test_apply_to_sets_permissions() {
1823        use crate::agent::AgentConfig;
1824        use crate::permissions::PermissionDecision;
1825
1826        let def = AgentDefinition::new("writer", "Write files")
1827            .with_permissions(PermissionPolicy::new().allow("write(*)"));
1828
1829        let mut config = AgentConfig::default();
1830        assert!(config.permission_checker.is_none());
1831
1832        def.apply_to(&mut config);
1833
1834        assert!(config.permission_checker.is_some());
1835        assert!(config.permission_policy.is_some());
1836        let checker = config.permission_checker.unwrap();
1837        assert_eq!(
1838            checker.check(
1839                "write",
1840                &serde_json::json!({"file_path": "x.txt", "content": "hi"})
1841            ),
1842            PermissionDecision::Allow
1843        );
1844    }
1845
1846    #[test]
1847    fn test_apply_to_sets_prompt() {
1848        use crate::agent::AgentConfig;
1849
1850        let def = AgentDefinition::new("helper", "Help").with_prompt("Be helpful.");
1851        let mut config = AgentConfig::default();
1852
1853        def.apply_to(&mut config);
1854
1855        assert_eq!(config.prompt_slots.extra.as_deref(), Some("Be helpful."));
1856    }
1857
1858    #[test]
1859    fn test_apply_to_sets_max_steps() {
1860        use crate::agent::AgentConfig;
1861
1862        let def = AgentDefinition::new("fast", "Fast agent").with_max_steps(7);
1863        let mut config = AgentConfig::default();
1864
1865        def.apply_to(&mut config);
1866
1867        assert_eq!(config.max_tool_rounds, 7);
1868    }
1869
1870    #[test]
1871    fn test_apply_to_respects_host_overrides() {
1872        use crate::agent::AgentConfig;
1873
1874        let def = AgentDefinition::new("agent", "Agent")
1875            .with_permissions(PermissionPolicy::new().allow("bash(*)"))
1876            .with_prompt("Agent prompt.")
1877            .with_max_steps(10);
1878
1879        let mut config = AgentConfig::default();
1880        config.prompt_slots.extra = Some("Host prompt.".to_string());
1881        config.max_tool_rounds = 25;
1882        config.permission_checker = Some(std::sync::Arc::new(PermissionPolicy::new().allow("*")));
1883
1884        def.apply_to(&mut config);
1885
1886        // Host overrides should be preserved
1887        assert_eq!(config.prompt_slots.extra.as_deref(), Some("Host prompt."));
1888        assert_eq!(config.max_tool_rounds, 25);
1889    }
1890
1891    #[test]
1892    fn test_apply_to_skips_empty_permissions() {
1893        use crate::agent::AgentConfig;
1894
1895        let def = AgentDefinition::new("empty", "No permissions");
1896        let mut config = AgentConfig::default();
1897
1898        def.apply_to(&mut config);
1899
1900        assert!(config.permission_checker.is_none());
1901        assert!(config.permission_policy.is_none());
1902    }
1903}