1use crate::tools::ToolRegistry;
29
30pub fn is_coordinator_mode() -> bool {
37 cfg!(feature = "coordinator-mode")
38 && std::env::var("RECURSIVE_COORDINATOR_MODE").as_deref() == Ok("1")
39}
40
41pub fn coordinator_tool_set() -> &'static [&'static str] {
51 &[
52 "Read",
54 "Grep",
55 "Glob",
56 "shared_memory_read",
57 "list_workers",
58 "team_create",
60 "team_delete",
61 "send_message",
62 "task_create",
63 "task_get",
64 "task_list",
65 "task_output",
66 "task_stop",
67 "task_update",
68 "TodoWrite",
70 "WebFetch",
71 "WebSearch",
72 "agent",
74 "enter_plan_mode",
76 "exit_plan_mode",
77 "request_plan_mode",
78 ]
79}
80
81pub fn is_coordinator_tool(tool_name: &str) -> bool {
86 coordinator_tool_set().contains(&tool_name)
87}
88
89pub fn coordinator_deny_list() -> &'static [&'static str] {
97 &[
98 "Edit", "Write", "Bash",
100 ]
101}
102
103pub fn is_allowed_in_coordinator_mode(tool_name: &str) -> bool {
108 is_coordinator_tool(tool_name) && !coordinator_deny_list().contains(&tool_name)
109}
110
111pub fn filter_registry(registry: &mut ToolRegistry) {
116 if is_coordinator_mode() {
117 let allow: Vec<String> = coordinator_tool_set()
118 .iter()
119 .copied()
120 .filter(|name| is_allowed_in_coordinator_mode(name))
121 .map(str::to_string)
122 .collect();
123 registry.retain_tools(&allow);
124 }
125}
126
127#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::tools::build_standard_tools;
135
136 #[test]
137 fn allow_list_excludes_mutating_tools() {
138 for forbidden in ["Edit", "Write", "Bash"] {
141 assert!(
142 !is_allowed_in_coordinator_mode(forbidden),
143 "coordinator mode must forbid `{forbidden}`"
144 );
145 }
146 }
147
148 #[test]
149 fn allow_list_includes_team_and_task_tools() {
150 for required in [
151 "team_create",
152 "team_delete",
153 "send_message",
154 "task_create",
155 "task_get",
156 "task_list",
157 "task_output",
158 "task_stop",
159 "task_update",
160 ] {
161 assert!(
162 is_allowed_in_coordinator_mode(required),
163 "coordinator mode must allow `{required}`"
164 );
165 }
166 }
167
168 #[test]
169 fn allow_list_includes_agent_and_read_only_introspection() {
170 for required in ["agent", "Read", "Grep", "Glob", "shared_memory_read"] {
171 assert!(
172 is_allowed_in_coordinator_mode(required),
173 "coordinator mode must allow `{required}`"
174 );
175 }
176 }
177
178 #[test]
179 fn deny_list_blocks_even_if_allowlist_grows() {
180 assert!(coordinator_deny_list().contains(&"Edit"));
184 assert!(coordinator_deny_list().contains(&"Write"));
185 assert!(coordinator_deny_list().contains(&"Bash"));
186 }
187
188 #[test]
189 fn is_coordinator_tool_false_for_unknown_tool() {
190 assert!(
192 !is_coordinator_tool("NonExistentTool"),
193 "unknown tool must not be in the coordinator tool set"
194 );
195 }
196
197 #[test]
198 fn is_allowed_in_coordinator_mode_false_for_unknown_tool() {
199 assert!(
201 !is_allowed_in_coordinator_mode("NotATool"),
202 "unknown tool must not be allowed in coordinator mode"
203 );
204 }
205
206 #[test]
207 fn is_allowed_requires_both_allowlist_and_not_denylist() {
208 assert!(
214 !is_allowed_in_coordinator_mode("Edit"),
215 "Edit must be blocked regardless of allow list membership"
216 );
217 }
218
219 #[test]
220 fn coordinator_tool_set_is_non_empty() {
221 assert!(
223 !coordinator_tool_set().is_empty(),
224 "coordinator tool set must not be empty"
225 );
226 }
227
228 #[test]
229 fn is_coordinator_tool_true_for_agent_tool() {
230 assert!(
232 is_coordinator_tool("agent"),
233 "'agent' must be in coordinator tool set"
234 );
235 }
236
237 #[test]
238 fn is_coordinator_mode_false_without_env_var() {
239 let _env_lock = crate::test_util::env_lock();
240 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
242 std::env::remove_var("RECURSIVE_COORDINATOR_MODE");
243 let mode = is_coordinator_mode();
245 if let Some(v) = prev {
246 std::env::set_var("RECURSIVE_COORDINATOR_MODE", v);
247 }
248 if !cfg!(feature = "coordinator-mode") {
250 assert!(!mode, "coordinator mode must be false without feature");
251 } else {
252 assert!(
253 !mode,
254 "coordinator mode must be false without the env var even with feature"
255 );
256 }
257 }
258
259 #[test]
260 fn cargo_feature_gate_respected() {
261 let _env_lock = crate::test_util::env_lock();
262 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
266 std::env::set_var("RECURSIVE_COORDINATOR_MODE", "1");
267 let on_with_feature = cfg!(feature = "coordinator-mode");
268 let _ = is_coordinator_mode(); match prev {
271 Some(v) => std::env::set_var("RECURSIVE_COORDINATOR_MODE", v),
272 None => std::env::remove_var("RECURSIVE_COORDINATOR_MODE"),
273 }
274 let _ = on_with_feature;
276 }
277
278 #[test]
279 fn is_coordinator_mode_rejects_non_one_env_value() {
280 let _env_lock = crate::test_util::env_lock();
281 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
285 std::env::set_var("RECURSIVE_COORDINATOR_MODE", "0");
286 let mode = is_coordinator_mode();
287 match prev {
288 Some(v) => std::env::set_var("RECURSIVE_COORDINATOR_MODE", v),
289 None => std::env::remove_var("RECURSIVE_COORDINATOR_MODE"),
290 }
291 assert!(
292 !mode,
293 "RECURSIVE_COORDINATOR_MODE=0 must not enable coordinator mode"
294 );
295 }
296
297 #[test]
298 fn is_coordinator_mode_false_when_env_one_without_cargo_feature() {
299 let _env_lock = crate::test_util::env_lock();
300 if cfg!(feature = "coordinator-mode") {
303 return;
304 }
305 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
306 std::env::set_var("RECURSIVE_COORDINATOR_MODE", "1");
307 assert!(
308 !is_coordinator_mode(),
309 "env=1 must not enable coordinator mode without the cargo feature"
310 );
311 match prev {
312 Some(v) => std::env::set_var("RECURSIVE_COORDINATOR_MODE", v),
313 None => std::env::remove_var("RECURSIVE_COORDINATOR_MODE"),
314 }
315 }
316
317 #[cfg(feature = "coordinator-mode")]
318 #[test]
319 fn is_coordinator_mode_true_when_feature_and_env_one() {
320 let _env_lock = crate::test_util::env_lock();
321 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
323 std::env::set_var("RECURSIVE_COORDINATOR_MODE", "1");
324 assert!(
325 is_coordinator_mode(),
326 "coordinator mode must be on when feature and env=1"
327 );
328 match prev {
329 Some(v) => std::env::set_var("RECURSIVE_COORDINATOR_MODE", v),
330 None => std::env::remove_var("RECURSIVE_COORDINATOR_MODE"),
331 }
332 }
333
334 #[test]
335 fn filter_registry_noop_outside_coordinator_mode() {
336 let _env_lock = crate::test_util::env_lock();
337 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
340 std::env::remove_var("RECURSIVE_COORDINATOR_MODE");
341 let tmp = tempfile::tempdir().expect("tempdir");
342 let mut reg = build_standard_tools(tmp.path(), &[], 30);
343 assert!(
344 reg.find_by_name("Edit").is_some(),
345 "fixture must include Edit before filter"
346 );
347 filter_registry(&mut reg);
348 assert!(
349 reg.find_by_name("Edit").is_some(),
350 "filter_registry must be a no-op outside coordinator mode"
351 );
352 match prev {
353 Some(v) => std::env::set_var("RECURSIVE_COORDINATOR_MODE", v),
354 None => std::env::remove_var("RECURSIVE_COORDINATOR_MODE"),
355 }
356 }
357
358 #[cfg(feature = "coordinator-mode")]
359 #[test]
360 fn filter_registry_prunes_mutating_tools_in_coordinator_mode() {
361 let _env_lock = crate::test_util::env_lock();
362 let prev = std::env::var("RECURSIVE_COORDINATOR_MODE").ok();
364 std::env::set_var("RECURSIVE_COORDINATOR_MODE", "1");
365 let tmp = tempfile::tempdir().expect("tempdir");
366 let mut reg = build_standard_tools(tmp.path(), &[], 30);
367 filter_registry(&mut reg);
368 assert!(
369 reg.find_by_name("Edit").is_none(),
370 "Edit must be pruned in coordinator mode"
371 );
372 assert!(
373 reg.find_by_name("Read").is_some(),
374 "Read must remain in coordinator mode"
375 );
376 match prev {
377 Some(v) => std::env::set_var("RECURSIVE_COORDINATOR_MODE", v),
378 None => std::env::remove_var("RECURSIVE_COORDINATOR_MODE"),
379 }
380 }
381}