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 "grep" => "grep(*)".to_string(),
138 "glob" => "glob(*)".to_string(),
139 "ls" => "ls(*)".to_string(),
140 "bash" => "bash(*)".to_string(),
141 "task" => "task(*)".to_string(),
142 "parallel_task" | "parallel-task" => "parallel_task(*)".to_string(),
143 _ if normalized.contains('(') => normalized.to_string(),
144 _ => format!("{normalized}(*)"),
145 }
146}
147
148fn permission_policy_from_tools(tools: &[String]) -> PermissionPolicy {
149 tools.iter().fold(PermissionPolicy::new(), |policy, tool| {
150 policy.allow(&tool_name_to_permission(tool))
151 })
152}
153
154fn allow_only_permission_policy(tools: &[String]) -> PermissionPolicy {
155 let mut policy = permission_policy_from_tools(tools);
156 policy.default_decision = PermissionDecision::Deny;
157 policy
158}
159
160fn add_denied_tools(mut policy: PermissionPolicy, tools: &[String]) -> PermissionPolicy {
161 for tool in tools {
162 policy = policy.deny(&tool_name_to_permission(tool));
163 }
164 policy
165}
166
167fn validate_agent_name(name: &str) -> anyhow::Result<()> {
168 if name.trim().is_empty() {
169 return Err(anyhow::anyhow!("Agent name is required"));
170 }
171 Ok(())
172}
173
174pub fn parse_agent_md(content: &str) -> anyhow::Result<AgentDefinition> {
178 let parts: Vec<&str> = content.splitn(3, "---").collect();
180
181 if parts.len() < 3 {
182 return Err(anyhow::anyhow!(
183 "Invalid markdown format: missing YAML frontmatter"
184 ));
185 }
186
187 let frontmatter = parts[1].trim();
188 let body = parts[2].trim();
189
190 let value: serde_yaml::Value = serde_yaml::from_str(frontmatter)
192 .map_err(|e| anyhow::anyhow!("Failed to parse agent frontmatter: {}", e))?;
193
194 if yaml_value_has_key(&value, "kind") {
195 let tools = yaml_get_any(&value, &["tools", "allowedTools", "allowed_tools"])
196 .map(parse_tools_field)
197 .unwrap_or_default();
198 let disallowed_tools = yaml_get_any(
199 &value,
200 &["disallowedTools", "disallowed-tools", "disallowed_tools"],
201 )
202 .map(parse_tools_field)
203 .unwrap_or_default();
204 let mut spec = parse_worker_yaml_value(value, "frontmatter")?;
205 if spec.prompt.is_none() && !body.is_empty() {
206 spec.prompt = Some(body.to_string());
207 }
208 apply_claude_style_tools_to_spec(&mut spec, &tools, &disallowed_tools);
209 return Ok(spec.into_agent_definition());
210 }
211
212 let mut agent = parse_agent_yaml_value(value, "agent frontmatter")?;
213
214 if agent.prompt.is_none() && !body.is_empty() {
216 agent.prompt = Some(body.to_string());
217 }
218
219 Ok(agent)
220}
221
222pub fn load_agents_from_dir(dir: &Path) -> Vec<AgentDefinition> {
227 let mut agents = Vec::new();
228 load_agents_from_dir_inner(dir, &mut agents);
229 agents
230}
231
232fn load_agents_from_dir_inner(dir: &Path, agents: &mut Vec<AgentDefinition>) {
233 let Ok(entries) = std::fs::read_dir(dir) else {
234 tracing::warn!("Failed to read agent directory: {}", dir.display());
235 return;
236 };
237
238 for entry in entries.flatten() {
239 let path = entry.path();
240
241 if path.is_dir() {
242 load_agents_from_dir_inner(&path, agents);
243 continue;
244 }
245 if !path.is_file() {
246 continue;
247 }
248
249 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
250 continue;
251 };
252
253 let Ok(content) = std::fs::read_to_string(&path) else {
255 tracing::warn!("Failed to read agent file: {}", path.display());
256 continue;
257 };
258
259 let result = match ext {
261 "yaml" | "yml" => parse_agent_yaml(&content),
262 "md" => parse_agent_md(&content),
263 _ => continue,
264 };
265
266 match result {
267 Ok(agent) => {
268 tracing::debug!("Loaded agent '{}' from {}", agent.name, path.display());
269 agents.push(agent);
270 }
271 Err(e) => {
272 tracing::warn!("Failed to parse agent file {}: {}", path.display(), e);
273 }
274 }
275 }
276}