a3s_code_core/subagent/
loader.rs1use super::*;
2
3pub fn parse_agent_yaml(content: &str) -> anyhow::Result<AgentDefinition> {
8 let value: serde_yaml::Value = serde_yaml::from_str(content)
9 .map_err(|e| anyhow::anyhow!("Failed to parse agent YAML: {}", e))?;
10
11 parse_agent_yaml_value(value, "agent YAML")
12}
13
14fn parse_agent_yaml_value(
15 value: serde_yaml::Value,
16 context: &str,
17) -> anyhow::Result<AgentDefinition> {
18 let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
19 .map(parse_tools_field)
20 .unwrap_or_default();
21 let disallowed_tools = yaml_get_any(
22 &value,
23 &["disallowedTools", "disallowed-tools", "disallowed_tools"],
24 )
25 .map(parse_tools_field)
26 .unwrap_or_default();
27
28 if yaml_value_has_key(&value, "kind") {
29 let mut spec: WorkerAgentSpec = serde_yaml::from_value(value)
30 .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
31 validate_agent_name(&spec.name)?;
32 apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
33 return Ok(spec.into_agent_definition());
34 }
35
36 let mut agent: AgentDefinition = serde_yaml::from_value(value)
37 .map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", context, e))?;
38 validate_agent_name(&agent.name)?;
39 apply_claude_style_tools_to_agent(&mut agent, &tools, &disallowed_tools);
40 Ok(agent)
41}
42
43fn apply_claude_style_tools_to_agent(
44 agent: &mut AgentDefinition,
45 tools: &[String],
46 disallowed_tools: &[String],
47) {
48 if !tools.is_empty() {
49 agent.permissions = allow_only_permission_policy(tools);
50 }
51 if !disallowed_tools.is_empty() {
52 let base = std::mem::take(&mut agent.permissions);
53 agent.permissions = add_denied_tools(base, disallowed_tools);
54 }
55 if (!tools.is_empty() || !disallowed_tools.is_empty())
56 && agent.confirmation_inheritance.is_none()
57 {
58 agent.confirmation_inheritance = Some(ConfirmationInheritance::AutoApprove);
59 }
60}
61
62fn apply_claude_style_tools_to_spec(
63 spec: &mut WorkerAgentSpec,
64 tools: &[String],
65 disallowed_tools: &[String],
66) {
67 if tools.is_empty() && disallowed_tools.is_empty() {
68 return;
69 }
70
71 let base = if tools.is_empty() {
72 spec.permissions
73 .clone()
74 .unwrap_or_else(|| spec.kind.default_permissions())
75 } else {
76 allow_only_permission_policy(tools)
77 };
78 spec.permissions = Some(add_denied_tools(base, disallowed_tools));
79 if spec.confirmation_inheritance.is_none() {
80 spec.confirmation_inheritance = Some(ConfirmationInheritance::AutoApprove);
81 }
82}
83
84fn parse_worker_yaml_value(
85 value: serde_yaml::Value,
86 context: &str,
87) -> anyhow::Result<WorkerAgentSpec> {
88 let spec: WorkerAgentSpec = serde_yaml::from_value(value)
89 .map_err(|e| anyhow::anyhow!("Failed to parse worker {}: {}", context, e))?;
90 validate_agent_name(&spec.name)?;
91 Ok(spec)
92}
93
94fn yaml_value_has_key(value: &serde_yaml::Value, key: &str) -> bool {
95 value
96 .as_mapping()
97 .map(|mapping| mapping.contains_key(serde_yaml::Value::String(key.to_string())))
98 .unwrap_or(false)
99}
100
101fn yaml_get<'a>(value: &'a serde_yaml::Value, key: &str) -> Option<&'a serde_yaml::Value> {
102 value
103 .as_mapping()
104 .and_then(|mapping| mapping.get(serde_yaml::Value::String(key.to_string())))
105}
106
107fn yaml_get_any<'a>(value: &'a serde_yaml::Value, keys: &[&str]) -> Option<&'a serde_yaml::Value> {
108 keys.iter().find_map(|key| yaml_get(value, key))
109}
110
111fn parse_tools_field(value: &serde_yaml::Value) -> Vec<String> {
112 match value {
113 serde_yaml::Value::String(raw) => raw
114 .split(',')
115 .map(str::trim)
116 .filter(|tool| !tool.is_empty())
117 .map(str::to_string)
118 .collect(),
119 serde_yaml::Value::Sequence(items) => items
120 .iter()
121 .filter_map(|item| item.as_str())
122 .map(str::trim)
123 .filter(|tool| !tool.is_empty())
124 .map(str::to_string)
125 .collect(),
126 _ => Vec::new(),
127 }
128}
129
130fn tool_name_to_permission(tool: &str) -> String {
131 let normalized = tool.trim();
132 match normalized.to_ascii_lowercase().as_str() {
133 "*" => "*".to_string(),
134 "read" => "read(*)".to_string(),
135 "write" => "write(*)".to_string(),
136 "edit" => "edit(*)".to_string(),
137 "search" => "search(*)".to_string(),
138 "ls" => "ls(*)".to_string(),
139 "bash" => "bash(*)".to_string(),
140 "task" => "task(*)".to_string(),
141 "parallel_task" | "parallel-task" => "parallel_task(*)".to_string(),
142 _ if normalized.contains('(') => normalized.to_string(),
143 _ => format!("{normalized}(*)"),
144 }
145}
146
147fn permission_policy_from_tools(tools: &[String]) -> PermissionPolicy {
148 tools.iter().fold(PermissionPolicy::new(), |policy, tool| {
149 policy.allow(&tool_name_to_permission(tool))
150 })
151}
152
153fn allow_only_permission_policy(tools: &[String]) -> PermissionPolicy {
154 let mut policy = permission_policy_from_tools(tools);
155 policy.default_decision = PermissionDecision::Deny;
156 policy
157}
158
159fn add_denied_tools(mut policy: PermissionPolicy, tools: &[String]) -> PermissionPolicy {
160 for tool in tools {
161 policy = policy.deny(&tool_name_to_permission(tool));
162 }
163 policy
164}
165
166fn validate_agent_name(name: &str) -> anyhow::Result<()> {
167 if name.trim().is_empty() {
168 return Err(anyhow::anyhow!("Agent name is required"));
169 }
170 Ok(())
171}
172
173pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
177 let parts: Vec<&str> = content.splitn(3, "---").collect();
179
180 if parts.len() < 3 {
181 return Err(anyhow::anyhow!(
182 "Invalid markdown format: missing YAML frontmatter"
183 ));
184 }
185
186 let frontmatter = parts[1].trim();
187 let body = parts[2].trim();
188
189 let value: serde_yaml::Value = serde_yaml::from_str(frontmatter)
191 .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
192
193 if yaml_value_has_key(&value, "kind") {
194 let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
195 .map(parse_tools_field)
196 .unwrap_or_default();
197 let disallowed_tools = yaml_get_any(
198 &value,
199 &["disallowedTools", "disallowed-tools", "disallowed_tools"],
200 )
201 .map(parse_tools_field)
202 .unwrap_or_default();
203 let mut spec = parse_worker_yaml_value(value, "frontmatter")?;
204 if spec.prompt.is_none() && !body.is_empty() {
205 spec.prompt = Some(body.to_string());
206 }
207 apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
208 return Ok(spec.into_agent_definition());
209 }
210
211 let mut agent = parse_agent_yaml_value(value, "agent frontmatter")?;
212
213 if agent.prompt.is_none() && !body.is_empty() {
215 agent.prompt = Some(body.to_string());
216 }
217
218 Ok(agent)
219}
220
221pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
226 let mut agents = Vec::new();
227 load_agents_from_dir_inner(dir, &mut agents);
228 agents
229}
230
231fn load_agents_from_dir_inner(dir: &Path, agents: &mut Vec<AgentDefinition>) {
232 let Ok(entries) = std::fs::read_dir(dir) else {
233 tracing::warn!("Failed to read agent directory: {}", dir.display());
234 return;
235 };
236
237 for entry in entries.flatten() {
238 let path = entry.path();
239
240 if path.is_dir() {
241 load_agents_from_dir_inner(&path, agents);
242 continue;
243 }
244 if !path.is_file() {
245 continue;
246 }
247
248 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
249 continue;
250 };
251
252 let Ok(content) = std::fs::read_to_string(&path) else {
254 tracing::warn!("Failed to read agent file: {}", path.display());
255 continue;
256 };
257
258 let result = match ext {
260 "yaml" | "yml" => parse_agent_yaml(&content),
261 "md" => parse_agent_md(&content),
262 _ => continue,
263 };
264
265 match result {
266 Ok(agent) => {
267 tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
268 agents.push(agent);
269 }
270 Err(e) => {
271 tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
272 }
273 }
274 }
275}