1use std::collections::HashMap;
15use std::pin::Pin;
16use std::sync::Arc;
17
18use zeph_skills::loader::Skill;
19use zeph_skills::registry::SkillRegistry;
20use zeph_tools::ToolCall;
21use zeph_tools::executor::{ErasedToolExecutor, ToolError, ToolOutput, extract_fenced_blocks};
22use zeph_tools::registry::{InvocationHint, ToolDef};
23
24use super::def::{SkillFilter, ToolPolicy};
25use super::error::SubAgentError;
26
27fn collect_fenced_tags(executor: &dyn ErasedToolExecutor) -> Vec<&'static str> {
31 executor
32 .tool_definitions_erased()
33 .into_iter()
34 .filter_map(|def| match def.invocation {
35 InvocationHint::FencedBlock(tag) => Some(tag),
36 _ => None,
37 })
38 .collect()
39}
40
41pub(crate) fn normalize_tool_id(s: &str) -> String {
47 let base = s.split('(').next().unwrap_or(s);
48 base.trim().to_lowercase()
49}
50
51pub struct FilteredToolExecutor {
61 inner: Arc<dyn ErasedToolExecutor>,
62 policy: ToolPolicy,
63 disallowed: Vec<String>,
64 fenced_tags: Vec<&'static str>,
67}
68
69impl FilteredToolExecutor {
70 #[must_use]
75 pub fn new(inner: Arc<dyn ErasedToolExecutor>, policy: ToolPolicy) -> Self {
76 let fenced_tags = collect_fenced_tags(&*inner);
77 Self {
78 inner,
79 policy,
80 disallowed: Vec::new(),
81 fenced_tags,
82 }
83 }
84
85 #[must_use]
90 pub fn with_disallowed(
91 inner: Arc<dyn ErasedToolExecutor>,
92 policy: ToolPolicy,
93 disallowed: Vec<String>,
94 ) -> Self {
95 let fenced_tags = collect_fenced_tags(&*inner);
96 Self {
97 inner,
98 policy,
99 disallowed,
100 fenced_tags,
101 }
102 }
103
104 fn has_fenced_tool_invocation(&self, response: &str) -> bool {
106 self.fenced_tags
107 .iter()
108 .any(|tag| !extract_fenced_blocks(response, tag).is_empty())
109 }
110
111 fn is_allowed(&self, tool_id: &str) -> bool {
117 let normalized = normalize_tool_id(tool_id);
118 if self
119 .disallowed
120 .iter()
121 .any(|t| normalize_tool_id(t) == normalized)
122 {
123 return false;
124 }
125 match &self.policy {
126 ToolPolicy::AllowList(list) => list.iter().any(|t| normalize_tool_id(t) == normalized),
127 ToolPolicy::DenyList(list) => !list.iter().any(|t| normalize_tool_id(t) == normalized),
128 _ => true,
129 }
130 }
131}
132
133impl ErasedToolExecutor for FilteredToolExecutor {
134 fn execute_erased<'a>(
135 &'a self,
136 response: &'a str,
137 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
138 {
139 if self.has_fenced_tool_invocation(response) {
150 tracing::warn!("sub-agent attempted fenced-block tool invocation — blocked by policy");
151 return Box::pin(std::future::ready(Err(ToolError::Blocked {
152 command: "fenced-block".into(),
153 })));
154 }
155 Box::pin(std::future::ready(Ok(None)))
156 }
157
158 fn execute_confirmed_erased<'a>(
159 &'a self,
160 response: &'a str,
161 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
162 {
163 if self.has_fenced_tool_invocation(response) {
165 tracing::warn!(
166 "sub-agent attempted confirmed fenced-block tool invocation — blocked by policy"
167 );
168 return Box::pin(std::future::ready(Err(ToolError::Blocked {
169 command: "fenced-block".into(),
170 })));
171 }
172 Box::pin(std::future::ready(Ok(None)))
173 }
174
175 fn tool_definitions_erased(&self) -> Vec<ToolDef> {
176 self.inner
178 .tool_definitions_erased()
179 .into_iter()
180 .filter(|def| self.is_allowed(&def.id))
181 .collect()
182 }
183
184 fn execute_tool_call_erased<'a>(
185 &'a self,
186 call: &'a ToolCall,
187 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
188 {
189 if !self.is_allowed(call.tool_id.as_str()) {
190 tracing::warn!(
191 tool_id = %call.tool_id,
192 "sub-agent tool call rejected by policy"
193 );
194 return Box::pin(std::future::ready(Err(ToolError::Blocked {
195 command: call.tool_id.to_string(),
196 })));
197 }
198 Box::pin(self.inner.execute_tool_call_erased(call))
199 }
200
201 fn execute_tool_call_confirmed_erased<'a>(
206 &'a self,
207 call: &'a ToolCall,
208 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
209 {
210 self.execute_tool_call_erased(call)
211 }
212
213 fn set_skill_env(&self, env: Option<HashMap<String, String>>) {
214 self.inner.set_skill_env(env);
215 }
216
217 fn set_effective_trust(&self, level: zeph_tools::SkillTrustLevel) {
218 self.inner.set_effective_trust(level);
219 }
220
221 fn is_tool_retryable_erased(&self, tool_id: &str) -> bool {
222 self.inner.is_tool_retryable_erased(tool_id)
223 }
224
225 fn requires_confirmation_erased(&self, call: &ToolCall) -> bool {
226 self.inner.requires_confirmation_erased(call)
227 }
228
229 zeph_tools::erased_tool_executor_forward!(inner);
230}
231
232pub struct PlanModeExecutor {
241 inner: Arc<dyn ErasedToolExecutor>,
242}
243
244impl PlanModeExecutor {
245 #[must_use]
247 pub fn new(inner: Arc<dyn ErasedToolExecutor>) -> Self {
248 Self { inner }
249 }
250}
251
252impl ErasedToolExecutor for PlanModeExecutor {
253 fn execute_erased<'a>(
254 &'a self,
255 _response: &'a str,
256 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
257 {
258 Box::pin(std::future::ready(Err(ToolError::Blocked {
259 command: "plan_mode".into(),
260 })))
261 }
262
263 fn execute_confirmed_erased<'a>(
264 &'a self,
265 _response: &'a str,
266 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
267 {
268 Box::pin(std::future::ready(Err(ToolError::Blocked {
269 command: "plan_mode".into(),
270 })))
271 }
272
273 fn tool_definitions_erased(&self) -> Vec<ToolDef> {
274 self.inner.tool_definitions_erased()
275 }
276
277 fn execute_tool_call_erased<'a>(
278 &'a self,
279 call: &'a ToolCall,
280 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
281 {
282 tracing::debug!(
283 tool_id = %call.tool_id,
284 "tool execution blocked in plan mode"
285 );
286 Box::pin(std::future::ready(Err(ToolError::Blocked {
287 command: call.tool_id.to_string(),
288 })))
289 }
290
291 fn execute_tool_call_confirmed_erased<'a>(
294 &'a self,
295 call: &'a ToolCall,
296 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
297 {
298 self.execute_tool_call_erased(call)
299 }
300
301 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
302 self.inner.set_skill_env(env);
303 }
304
305 fn set_effective_trust(&self, level: zeph_tools::SkillTrustLevel) {
308 self.inner.set_effective_trust(level);
309 }
310
311 fn is_tool_retryable_erased(&self, _tool_id: &str) -> bool {
312 false
313 }
314
315 fn requires_confirmation_erased(&self, _call: &ToolCall) -> bool {
316 false
317 }
318
319 zeph_tools::erased_tool_executor_forward!(inner);
325}
326
327const NETWORK_ONLY_TOOL_IDS: &[&str] = &["web_scrape", "fetch"];
336
337pub struct NetworkDenyToolExecutor {
356 inner: Arc<dyn ErasedToolExecutor>,
357 blocklist: Vec<String>,
358}
359
360impl NetworkDenyToolExecutor {
361 #[must_use]
363 pub fn new(inner: Arc<dyn ErasedToolExecutor>) -> Self {
364 Self {
365 inner,
366 blocklist: zeph_tools::NETWORK_COMMANDS
367 .iter()
368 .map(|s| (*s).to_owned())
369 .collect(),
370 }
371 }
372
373 fn check_call(&self, call: &ToolCall) -> Result<(), ToolError> {
377 let tool_id = normalize_tool_id(call.tool_id.as_str());
378
379 if NETWORK_ONLY_TOOL_IDS.contains(&tool_id.as_str()) {
380 tracing::warn!(
381 tool_id = %tool_id,
382 "network egress denied for sub-agent task (NetworkScope::Deny)"
383 );
384 return Err(ToolError::Blocked { command: tool_id });
385 }
386
387 if tool_id != "bash" {
388 return Ok(());
389 }
390 let Some(command) = call.params.get("command").and_then(|v| v.as_str()) else {
391 return Ok(());
392 };
393 if let Some(matched) = zeph_tools::check_blocklist(command, &self.blocklist) {
394 tracing::warn!(
395 command = %matched,
396 "network egress denied for sub-agent task (NetworkScope::Deny)"
397 );
398 return Err(ToolError::Blocked { command: matched });
399 }
400 Ok(())
401 }
402}
403
404impl ErasedToolExecutor for NetworkDenyToolExecutor {
405 fn execute_erased<'a>(
406 &'a self,
407 response: &'a str,
408 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
409 {
410 self.inner.execute_erased(response)
411 }
412
413 fn execute_confirmed_erased<'a>(
414 &'a self,
415 response: &'a str,
416 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
417 {
418 self.inner.execute_confirmed_erased(response)
419 }
420
421 fn tool_definitions_erased(&self) -> Vec<ToolDef> {
422 self.inner.tool_definitions_erased()
423 }
424
425 fn execute_tool_call_erased<'a>(
426 &'a self,
427 call: &'a ToolCall,
428 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
429 {
430 if let Err(e) = self.check_call(call) {
431 return Box::pin(std::future::ready(Err(e)));
432 }
433 Box::pin(self.inner.execute_tool_call_erased(call))
434 }
435
436 fn execute_tool_call_confirmed_erased<'a>(
437 &'a self,
438 call: &'a ToolCall,
439 ) -> Pin<Box<dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
440 {
441 if let Err(e) = self.check_call(call) {
442 return Box::pin(std::future::ready(Err(e)));
443 }
444 Box::pin(self.inner.execute_tool_call_confirmed_erased(call))
445 }
446
447 fn set_skill_env(&self, env: Option<HashMap<String, String>>) {
448 self.inner.set_skill_env(env);
449 }
450
451 fn set_effective_trust(&self, level: zeph_tools::SkillTrustLevel) {
452 self.inner.set_effective_trust(level);
453 }
454
455 fn is_tool_retryable_erased(&self, tool_id: &str) -> bool {
456 self.inner.is_tool_retryable_erased(tool_id)
457 }
458
459 fn requires_confirmation_erased(&self, call: &ToolCall) -> bool {
460 self.inner.requires_confirmation_erased(call)
461 }
462
463 zeph_tools::erased_tool_executor_forward!(inner);
464}
465
466pub fn filter_skills(
495 registry: &SkillRegistry,
496 filter: &SkillFilter,
497) -> Result<Vec<Skill>, SubAgentError> {
498 let compiled_include = compile_globs(&filter.include)?;
499 let compiled_exclude = compile_globs(&filter.exclude)?;
500
501 let all: Vec<Skill> = registry
502 .all_meta()
503 .into_iter()
504 .filter(|meta| {
505 let name = &meta.name;
506 let included =
507 compiled_include.is_empty() || compiled_include.iter().any(|p| glob_match(p, name));
508 let excluded = compiled_exclude.iter().any(|p| glob_match(p, name));
509 included && !excluded
510 })
511 .filter_map(|meta| registry.skill(&meta.name).ok())
512 .collect();
513
514 Ok(all)
515}
516
517struct GlobPattern {
519 raw: String,
520 prefix: String,
521 suffix: Option<String>,
522 is_star: bool,
523}
524
525fn compile_globs(patterns: &[String]) -> Result<Vec<GlobPattern>, SubAgentError> {
526 patterns.iter().map(|p| compile_glob(p)).collect()
527}
528
529fn compile_glob(pattern: &str) -> Result<GlobPattern, SubAgentError> {
530 if pattern.contains("**") {
533 return Err(SubAgentError::Invalid(format!(
534 "glob pattern '{pattern}' uses '**' which is not supported"
535 )));
536 }
537
538 let is_star = pattern == "*";
539
540 let (prefix, suffix) = if let Some(pos) = pattern.find('*') {
541 let before = pattern[..pos].to_owned();
542 let after = pattern[pos + 1..].to_owned();
543 (before, Some(after))
544 } else {
545 (pattern.to_owned(), None)
546 };
547
548 Ok(GlobPattern {
549 raw: pattern.to_owned(),
550 prefix,
551 suffix,
552 is_star,
553 })
554}
555
556fn glob_match(pattern: &GlobPattern, name: &str) -> bool {
557 if pattern.is_star {
558 return true;
559 }
560
561 match &pattern.suffix {
562 None => name == pattern.raw,
563 Some(suf) => {
564 name.starts_with(&pattern.prefix) && name.ends_with(suf.as_str()) && {
565 name.len() >= pattern.prefix.len() + suf.len()
567 }
568 }
569 }
570}
571
572#[cfg(test)]
575mod tests {
576 #![allow(clippy::default_trait_access)]
577 use std::assert_matches;
578
579 use super::*;
580 use crate::def::ToolPolicy;
581
582 struct StubExecutor {
585 tools: Vec<&'static str>,
586 }
587
588 struct StubFencedExecutor {
590 tag: &'static str,
591 }
592
593 impl ErasedToolExecutor for StubFencedExecutor {
594 fn execute_erased<'a>(
595 &'a self,
596 _response: &'a str,
597 ) -> Pin<
598 Box<
599 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
600 >,
601 > {
602 Box::pin(std::future::ready(Ok(None)))
603 }
604
605 fn execute_confirmed_erased<'a>(
606 &'a self,
607 _response: &'a str,
608 ) -> Pin<
609 Box<
610 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
611 >,
612 > {
613 Box::pin(std::future::ready(Ok(None)))
614 }
615
616 fn tool_definitions_erased(&self) -> Vec<ToolDef> {
617 use zeph_tools::registry::InvocationHint;
618 vec![ToolDef {
619 id: self.tag.into(),
620 description: "fenced stub".into(),
621 schema: schemars::Schema::default(),
622 invocation: InvocationHint::FencedBlock(self.tag),
623 output_schema: None,
624 server_id: None,
625 }]
626 }
627
628 fn execute_tool_call_erased<'a>(
629 &'a self,
630 call: &'a ToolCall,
631 ) -> Pin<
632 Box<
633 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
634 >,
635 > {
636 let result = Ok(Some(ToolOutput {
637 tool_name: call.tool_id.clone(),
638 summary: "ok".into(),
639 blocks_executed: 1,
640 filter_stats: None,
641 diff: None,
642 streamed: false,
643 terminal_id: None,
644 locations: None,
645 raw_response: None,
646 claim_source: None,
647 ..Default::default()
648 }));
649 Box::pin(std::future::ready(result))
650 }
651
652 fn is_tool_retryable_erased(&self, _tool_id: &str) -> bool {
653 false
654 }
655
656 fn requires_confirmation_erased(&self, _call: &ToolCall) -> bool {
657 false
658 }
659
660 fn execute_tool_call_confirmed_erased<'a>(
661 &'a self,
662 call: &'a ToolCall,
663 ) -> Pin<
664 Box<
665 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
666 >,
667 > {
668 self.execute_tool_call_erased(call)
669 }
670
671 fn checkpoint_undo_erased(&self, _n: usize) -> zeph_tools::CheckpointActionResult {
672 zeph_tools::CheckpointActionResult::unsupported()
673 }
674
675 fn checkpoint_redo_erased(&self) -> zeph_tools::CheckpointActionResult {
676 zeph_tools::CheckpointActionResult::unsupported()
677 }
678
679 fn checkpoint_list_erased(&self) -> zeph_tools::CheckpointListResult {
680 zeph_tools::CheckpointListResult::default()
681 }
682
683 fn is_tool_speculatable_erased(&self, _tool_id: &str) -> bool {
684 false
685 }
686 }
687
688 fn fenced_stub_box(tag: &'static str) -> Arc<dyn ErasedToolExecutor> {
689 Arc::new(StubFencedExecutor { tag })
690 }
691
692 impl ErasedToolExecutor for StubExecutor {
693 fn execute_erased<'a>(
694 &'a self,
695 _response: &'a str,
696 ) -> Pin<
697 Box<
698 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
699 >,
700 > {
701 Box::pin(std::future::ready(Ok(None)))
702 }
703
704 fn execute_confirmed_erased<'a>(
705 &'a self,
706 _response: &'a str,
707 ) -> Pin<
708 Box<
709 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
710 >,
711 > {
712 Box::pin(std::future::ready(Ok(None)))
713 }
714
715 fn tool_definitions_erased(&self) -> Vec<ToolDef> {
716 use zeph_tools::registry::InvocationHint;
718 self.tools
719 .iter()
720 .map(|id| ToolDef {
721 id: (*id).into(),
722 description: "stub".into(),
723 schema: schemars::Schema::default(),
724 invocation: InvocationHint::ToolCall,
725 output_schema: None,
726 server_id: None,
727 })
728 .collect()
729 }
730
731 fn execute_tool_call_erased<'a>(
732 &'a self,
733 call: &'a ToolCall,
734 ) -> Pin<
735 Box<
736 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
737 >,
738 > {
739 let result = Ok(Some(ToolOutput {
740 tool_name: call.tool_id.clone(),
741 summary: "ok".into(),
742 blocks_executed: 1,
743 filter_stats: None,
744 diff: None,
745 streamed: false,
746 terminal_id: None,
747 locations: None,
748 raw_response: None,
749 claim_source: None,
750 ..Default::default()
751 }));
752 Box::pin(std::future::ready(result))
753 }
754
755 fn is_tool_retryable_erased(&self, _tool_id: &str) -> bool {
756 false
757 }
758
759 fn requires_confirmation_erased(&self, _call: &ToolCall) -> bool {
760 false
761 }
762
763 fn execute_tool_call_confirmed_erased<'a>(
764 &'a self,
765 call: &'a ToolCall,
766 ) -> Pin<
767 Box<
768 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
769 >,
770 > {
771 self.execute_tool_call_erased(call)
772 }
773
774 fn checkpoint_undo_erased(&self, _n: usize) -> zeph_tools::CheckpointActionResult {
775 zeph_tools::CheckpointActionResult::unsupported()
776 }
777
778 fn checkpoint_redo_erased(&self) -> zeph_tools::CheckpointActionResult {
779 zeph_tools::CheckpointActionResult::unsupported()
780 }
781
782 fn checkpoint_list_erased(&self) -> zeph_tools::CheckpointListResult {
783 zeph_tools::CheckpointListResult::default()
784 }
785
786 fn is_tool_speculatable_erased(&self, _tool_id: &str) -> bool {
787 false
788 }
789 }
790
791 fn stub_box(tools: &[&'static str]) -> Arc<dyn ErasedToolExecutor> {
792 Arc::new(StubExecutor {
793 tools: tools.to_vec(),
794 })
795 }
796
797 #[tokio::test]
798 async fn allow_list_permits_listed_tool() {
799 let exec = FilteredToolExecutor::new(
800 stub_box(&["shell", "web"]),
801 ToolPolicy::AllowList(vec!["shell".into()]),
802 );
803 let call = ToolCall {
804 tool_id: "shell".into(),
805 params: serde_json::Map::default(),
806 caller_id: None,
807 context: None,
808
809 tool_call_id: String::new(),
810 skill_name: None,
811 };
812 let res = exec.execute_tool_call_erased(&call).await.unwrap();
813 assert!(res.is_some());
814 }
815
816 #[tokio::test]
817 async fn allow_list_blocks_unlisted_tool() {
818 let exec = FilteredToolExecutor::new(
819 stub_box(&["shell", "web"]),
820 ToolPolicy::AllowList(vec!["shell".into()]),
821 );
822 let call = ToolCall {
823 tool_id: "web".into(),
824 params: serde_json::Map::default(),
825 caller_id: None,
826 context: None,
827
828 tool_call_id: String::new(),
829 skill_name: None,
830 };
831 let res = exec.execute_tool_call_erased(&call).await;
832 assert!(res.is_err());
833 }
834
835 #[tokio::test]
836 async fn deny_list_blocks_listed_tool() {
837 let exec = FilteredToolExecutor::new(
838 stub_box(&["shell", "web"]),
839 ToolPolicy::DenyList(vec!["shell".into()]),
840 );
841 let call = ToolCall {
842 tool_id: "shell".into(),
843 params: serde_json::Map::default(),
844 caller_id: None,
845 context: None,
846
847 tool_call_id: String::new(),
848 skill_name: None,
849 };
850 let res = exec.execute_tool_call_erased(&call).await;
851 assert!(res.is_err());
852 }
853
854 #[tokio::test]
855 async fn inherit_all_permits_any_tool() {
856 let exec = FilteredToolExecutor::new(stub_box(&["shell"]), ToolPolicy::InheritAll);
857 let call = ToolCall {
858 tool_id: "shell".into(),
859 params: serde_json::Map::default(),
860 caller_id: None,
861 context: None,
862
863 tool_call_id: String::new(),
864 skill_name: None,
865 };
866 let res = exec.execute_tool_call_erased(&call).await.unwrap();
867 assert!(res.is_some());
868 }
869
870 #[test]
871 fn tool_definitions_filtered_by_allow_list() {
872 let exec = FilteredToolExecutor::new(
873 stub_box(&["shell", "web"]),
874 ToolPolicy::AllowList(vec!["shell".into()]),
875 );
876 let defs = exec.tool_definitions_erased();
877 assert_eq!(defs.len(), 1);
878 assert_eq!(defs[0].id, "shell");
879 }
880
881 fn matches(pattern: &str, name: &str) -> bool {
884 let p = compile_glob(pattern).unwrap();
885 glob_match(&p, name)
886 }
887
888 #[test]
889 fn glob_star_matches_all() {
890 assert!(matches("*", "anything"));
891 assert!(matches("*", ""));
892 }
893
894 #[test]
895 fn glob_prefix_star() {
896 assert!(matches("git-*", "git-commit"));
897 assert!(matches("git-*", "git-status"));
898 assert!(!matches("git-*", "rust-fmt"));
899 }
900
901 #[test]
902 fn glob_literal_exact_match() {
903 assert!(matches("shell", "shell"));
904 assert!(!matches("shell", "shell-extra"));
905 }
906
907 #[test]
908 fn glob_star_suffix() {
909 assert!(matches("*-review", "code-review"));
910 assert!(!matches("*-review", "code-reviewer"));
911 }
912
913 #[test]
914 fn glob_double_star_is_error() {
915 assert!(compile_glob("**").is_err());
916 }
917
918 #[test]
919 fn glob_mid_string_wildcard() {
920 assert!(matches("a*b", "axb"));
922 assert!(matches("a*b", "aXYZb"));
923 assert!(!matches("a*b", "ab-extra"));
924 assert!(!matches("a*b", "xab"));
925 }
926
927 #[tokio::test]
930 async fn deny_list_permits_unlisted_tool() {
931 let exec = FilteredToolExecutor::new(
932 stub_box(&["shell", "web"]),
933 ToolPolicy::DenyList(vec!["shell".into()]),
934 );
935 let call = ToolCall {
936 tool_id: "web".into(), params: serde_json::Map::default(),
938 caller_id: None,
939 context: None,
940
941 tool_call_id: String::new(),
942 skill_name: None,
943 };
944 let res = exec.execute_tool_call_erased(&call).await.unwrap();
945 assert!(res.is_some());
946 }
947
948 #[test]
949 fn tool_definitions_filtered_by_deny_list() {
950 let exec = FilteredToolExecutor::new(
951 stub_box(&["shell", "web"]),
952 ToolPolicy::DenyList(vec!["shell".into()]),
953 );
954 let defs = exec.tool_definitions_erased();
955 assert_eq!(defs.len(), 1);
956 assert_eq!(defs[0].id, "web");
957 }
958
959 #[test]
960 fn tool_definitions_inherit_all_returns_all() {
961 let exec = FilteredToolExecutor::new(stub_box(&["shell", "web"]), ToolPolicy::InheritAll);
962 let defs = exec.tool_definitions_erased();
963 assert_eq!(defs.len(), 2);
964 }
965
966 #[tokio::test]
969 async fn fenced_block_matching_tag_is_blocked() {
970 let exec = FilteredToolExecutor::new(fenced_stub_box("bash"), ToolPolicy::InheritAll);
972 let res = exec.execute_erased("```bash\nls\n```").await;
973 assert!(
974 res.is_err(),
975 "actual fenced-block invocation must be blocked"
976 );
977 }
978
979 #[tokio::test]
980 async fn fenced_block_matching_tag_confirmed_is_blocked() {
981 let exec = FilteredToolExecutor::new(fenced_stub_box("bash"), ToolPolicy::InheritAll);
982 let res = exec.execute_confirmed_erased("```bash\nls\n```").await;
983 assert!(
984 res.is_err(),
985 "actual fenced-block invocation (confirmed) must be blocked"
986 );
987 }
988
989 #[tokio::test]
990 async fn no_fenced_tools_plain_text_returns_ok_none() {
991 let exec = FilteredToolExecutor::new(stub_box(&["shell"]), ToolPolicy::InheritAll);
993 let res = exec.execute_erased("This is a plain text response.").await;
994 assert!(
995 res.unwrap().is_none(),
996 "plain text must not be treated as a tool call"
997 );
998 }
999
1000 #[tokio::test]
1001 async fn markdown_non_tool_fence_returns_ok_none() {
1002 let exec = FilteredToolExecutor::new(fenced_stub_box("bash"), ToolPolicy::InheritAll);
1004 let res = exec
1005 .execute_erased("Here is some code:\n```rust\nfn main() {}\n```")
1006 .await;
1007 assert!(
1008 res.unwrap().is_none(),
1009 "non-tool code fence must not trigger blocking"
1010 );
1011 }
1012
1013 #[tokio::test]
1014 async fn no_fenced_tools_plain_text_confirmed_returns_ok_none() {
1015 let exec = FilteredToolExecutor::new(stub_box(&["shell"]), ToolPolicy::InheritAll);
1016 let res = exec
1017 .execute_confirmed_erased("Plain response without any fences.")
1018 .await;
1019 assert!(res.unwrap().is_none());
1020 }
1021
1022 #[tokio::test]
1026 async fn fenced_executor_plain_text_returns_ok_none() {
1027 let exec = FilteredToolExecutor::new(fenced_stub_box("bash"), ToolPolicy::InheritAll);
1028 let res = exec
1029 .execute_erased("Here is my analysis of the code. No shell commands needed.")
1030 .await;
1031 assert!(
1032 res.unwrap().is_none(),
1033 "plain text with fenced executor must not be treated as a tool call"
1034 );
1035 }
1036
1037 #[tokio::test]
1040 async fn unclosed_fenced_block_returns_ok_none() {
1041 let exec = FilteredToolExecutor::new(fenced_stub_box("bash"), ToolPolicy::InheritAll);
1042 let res = exec.execute_erased("```bash\nls -la\n").await;
1043 assert!(
1044 res.unwrap().is_none(),
1045 "unclosed fenced block must not be treated as a tool invocation"
1046 );
1047 }
1048
1049 #[tokio::test]
1051 async fn multiple_fences_one_matching_tag_is_blocked() {
1052 let exec = FilteredToolExecutor::new(fenced_stub_box("bash"), ToolPolicy::InheritAll);
1053 let response = "Here is an example:\n```python\nprint('hello')\n```\nAnd the fix:\n```bash\nrm -rf /tmp/old\n```";
1054 let res = exec.execute_erased(response).await;
1055 assert!(
1056 res.is_err(),
1057 "response containing a matching fenced block must be blocked"
1058 );
1059 }
1060
1061 #[tokio::test]
1064 async fn disallowed_blocks_tool_from_allow_list() {
1065 let exec = FilteredToolExecutor::with_disallowed(
1066 stub_box(&["shell", "web"]),
1067 ToolPolicy::AllowList(vec!["shell".into(), "web".into()]),
1068 vec!["shell".into()],
1069 );
1070 let call = ToolCall {
1071 tool_id: "shell".into(),
1072 params: serde_json::Map::default(),
1073 caller_id: None,
1074 context: None,
1075
1076 tool_call_id: String::new(),
1077 skill_name: None,
1078 };
1079 let res = exec.execute_tool_call_erased(&call).await;
1080 assert!(
1081 res.is_err(),
1082 "disallowed tool must be blocked even if in allow list"
1083 );
1084 }
1085
1086 #[tokio::test]
1087 async fn disallowed_allows_non_disallowed_tool() {
1088 let exec = FilteredToolExecutor::with_disallowed(
1089 stub_box(&["shell", "web"]),
1090 ToolPolicy::AllowList(vec!["shell".into(), "web".into()]),
1091 vec!["shell".into()],
1092 );
1093 let call = ToolCall {
1094 tool_id: "web".into(),
1095 params: serde_json::Map::default(),
1096 caller_id: None,
1097 context: None,
1098
1099 tool_call_id: String::new(),
1100 skill_name: None,
1101 };
1102 let res = exec.execute_tool_call_erased(&call).await;
1103 assert!(res.is_ok(), "non-disallowed tool must be allowed");
1104 }
1105
1106 #[test]
1107 fn disallowed_empty_list_no_change() {
1108 let exec = FilteredToolExecutor::with_disallowed(
1109 stub_box(&["shell", "web"]),
1110 ToolPolicy::InheritAll,
1111 vec![],
1112 );
1113 let defs = exec.tool_definitions_erased();
1114 assert_eq!(defs.len(), 2);
1115 }
1116
1117 #[test]
1118 fn tool_definitions_filters_disallowed_tools() {
1119 let exec = FilteredToolExecutor::with_disallowed(
1120 stub_box(&["shell", "web", "dangerous"]),
1121 ToolPolicy::InheritAll,
1122 vec!["dangerous".into()],
1123 );
1124 let defs = exec.tool_definitions_erased();
1125 assert_eq!(defs.len(), 2);
1126 assert!(!defs.iter().any(|d| d.id == "dangerous"));
1127 }
1128
1129 fn bash_call(command: &str) -> ToolCall {
1132 let mut params = serde_json::Map::new();
1133 params.insert("command".into(), serde_json::Value::from(command));
1134 ToolCall {
1135 tool_id: "bash".into(),
1136 params,
1137 caller_id: None,
1138 context: None,
1139 tool_call_id: String::new(),
1140 skill_name: None,
1141 }
1142 }
1143
1144 #[tokio::test]
1145 async fn network_deny_blocks_curl() {
1146 let exec = NetworkDenyToolExecutor::new(stub_box(&["bash"]));
1147 let res = exec
1148 .execute_tool_call_erased(&bash_call("curl https://evil.example"))
1149 .await;
1150 assert_matches!(res, Err(ToolError::Blocked { .. }));
1151 }
1152
1153 #[tokio::test]
1154 async fn network_deny_blocks_wget_and_nc() {
1155 let exec = NetworkDenyToolExecutor::new(stub_box(&["bash"]));
1156 assert!(
1157 exec.execute_tool_call_erased(&bash_call("wget https://evil.example"))
1158 .await
1159 .is_err()
1160 );
1161 assert!(
1162 exec.execute_tool_call_erased(&bash_call("nc -l 4444"))
1163 .await
1164 .is_err()
1165 );
1166 }
1167
1168 #[tokio::test]
1169 async fn network_deny_permits_non_network_bash() {
1170 let exec = NetworkDenyToolExecutor::new(stub_box(&["bash"]));
1171 let res = exec.execute_tool_call_erased(&bash_call("ls -la")).await;
1172 assert!(res.is_ok(), "non-network command must pass through");
1173 }
1174
1175 #[tokio::test]
1176 async fn network_deny_ignores_non_bash_tools() {
1177 let exec = NetworkDenyToolExecutor::new(stub_box(&["web"]));
1178 let call = ToolCall {
1179 tool_id: "web".into(),
1180 params: serde_json::Map::default(),
1181 caller_id: None,
1182 context: None,
1183 tool_call_id: String::new(),
1184 skill_name: None,
1185 };
1186 let res = exec.execute_tool_call_erased(&call).await;
1187 assert!(res.is_ok(), "non-bash tool calls must not be inspected");
1188 }
1189
1190 #[tokio::test]
1191 async fn network_deny_confirmed_path_also_enforces() {
1192 let exec = NetworkDenyToolExecutor::new(stub_box(&["bash"]));
1193 let res = exec
1194 .execute_tool_call_confirmed_erased(&bash_call("curl https://evil.example"))
1195 .await;
1196 assert_matches!(res, Err(ToolError::Blocked { .. }));
1197 }
1198
1199 fn tool_call(tool_id: &str) -> ToolCall {
1200 ToolCall {
1201 tool_id: tool_id.into(),
1202 params: serde_json::Map::default(),
1203 caller_id: None,
1204 context: None,
1205 tool_call_id: String::new(),
1206 skill_name: None,
1207 }
1208 }
1209
1210 #[tokio::test]
1211 async fn network_deny_blocks_web_scrape_unconditionally() {
1212 let exec = NetworkDenyToolExecutor::new(stub_box(&["web_scrape"]));
1213 let res = exec
1214 .execute_tool_call_erased(&tool_call("web_scrape"))
1215 .await;
1216 assert_matches!(res, Err(ToolError::Blocked { .. }));
1217 }
1218
1219 #[tokio::test]
1220 async fn network_deny_blocks_fetch_unconditionally() {
1221 let exec = NetworkDenyToolExecutor::new(stub_box(&["fetch"]));
1222 let res = exec.execute_tool_call_erased(&tool_call("fetch")).await;
1223 assert_matches!(res, Err(ToolError::Blocked { .. }));
1224 }
1225
1226 #[tokio::test]
1227 async fn network_deny_blocks_fetch_confirmed_path_too() {
1228 let exec = NetworkDenyToolExecutor::new(stub_box(&["fetch"]));
1229 let res = exec
1230 .execute_tool_call_confirmed_erased(&tool_call("fetch"))
1231 .await;
1232 assert_matches!(res, Err(ToolError::Blocked { .. }));
1233 }
1234
1235 #[test]
1238 fn plan_mode_with_disallowed_excludes_from_catalog() {
1239 let inner = Arc::new(PlanModeExecutor::new(stub_box(&["shell", "web"])));
1242 let exec = FilteredToolExecutor::with_disallowed(
1243 inner,
1244 ToolPolicy::InheritAll,
1245 vec!["shell".into()],
1246 );
1247 let defs = exec.tool_definitions_erased();
1248 assert!(
1249 !defs.iter().any(|d| d.id == "shell"),
1250 "shell must be excluded from catalog"
1251 );
1252 assert!(
1253 defs.iter().any(|d| d.id == "web"),
1254 "web must remain in catalog"
1255 );
1256 }
1257
1258 #[tokio::test]
1261 async fn plan_mode_blocks_execute_erased() {
1262 let exec = PlanModeExecutor::new(stub_box(&["shell"]));
1263 let res = exec.execute_erased("response").await;
1264 assert!(res.is_err());
1265 }
1266
1267 #[tokio::test]
1268 async fn plan_mode_blocks_execute_confirmed_erased() {
1269 let exec = PlanModeExecutor::new(stub_box(&["shell"]));
1270 let res = exec.execute_confirmed_erased("response").await;
1271 assert!(res.is_err());
1272 }
1273
1274 #[tokio::test]
1275 async fn plan_mode_blocks_tool_call() {
1276 let exec = PlanModeExecutor::new(stub_box(&["shell"]));
1277 let call = ToolCall {
1278 tool_id: "shell".into(),
1279 params: serde_json::Map::default(),
1280 caller_id: None,
1281 context: None,
1282
1283 tool_call_id: String::new(),
1284 skill_name: None,
1285 };
1286 let res = exec.execute_tool_call_erased(&call).await;
1287 assert!(res.is_err(), "plan mode must block all tool execution");
1288 }
1289
1290 #[test]
1291 fn plan_mode_exposes_real_tool_definitions() {
1292 let exec = PlanModeExecutor::new(stub_box(&["shell", "web"]));
1293 let defs = exec.tool_definitions_erased();
1294 assert_eq!(defs.len(), 2);
1296 assert!(defs.iter().any(|d| d.id == "shell"));
1297 assert!(defs.iter().any(|d| d.id == "web"));
1298 }
1299
1300 struct CheckpointingStub {
1307 trust_recorded: std::sync::Mutex<Option<zeph_tools::SkillTrustLevel>>,
1308 }
1309
1310 impl ErasedToolExecutor for CheckpointingStub {
1311 fn execute_erased<'a>(
1312 &'a self,
1313 _response: &'a str,
1314 ) -> Pin<
1315 Box<
1316 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
1317 >,
1318 > {
1319 Box::pin(std::future::ready(Ok(None)))
1320 }
1321
1322 fn execute_confirmed_erased<'a>(
1323 &'a self,
1324 _response: &'a str,
1325 ) -> Pin<
1326 Box<
1327 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
1328 >,
1329 > {
1330 Box::pin(std::future::ready(Ok(None)))
1331 }
1332
1333 fn tool_definitions_erased(&self) -> Vec<ToolDef> {
1334 vec![]
1335 }
1336
1337 fn execute_tool_call_erased<'a>(
1338 &'a self,
1339 _call: &'a ToolCall,
1340 ) -> Pin<
1341 Box<
1342 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
1343 >,
1344 > {
1345 Box::pin(std::future::ready(Ok(None)))
1346 }
1347
1348 fn execute_tool_call_confirmed_erased<'a>(
1349 &'a self,
1350 call: &'a ToolCall,
1351 ) -> Pin<
1352 Box<
1353 dyn std::future::Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a,
1354 >,
1355 > {
1356 self.execute_tool_call_erased(call)
1357 }
1358
1359 fn is_tool_retryable_erased(&self, _tool_id: &str) -> bool {
1360 false
1361 }
1362
1363 fn requires_confirmation_erased(&self, _call: &ToolCall) -> bool {
1364 false
1365 }
1366
1367 fn is_tool_speculatable_erased(&self, _tool_id: &str) -> bool {
1368 true
1369 }
1370
1371 fn set_effective_trust(&self, level: zeph_tools::SkillTrustLevel) {
1372 *self.trust_recorded.lock().unwrap() = Some(level);
1373 }
1374
1375 fn checkpoint_undo_erased(&self, n: usize) -> zeph_tools::CheckpointActionResult {
1376 zeph_tools::CheckpointActionResult {
1377 reverted_commands: n,
1378 restored: 0,
1379 deleted: 0,
1380 supported: true,
1381 message: "stub-undo".into(),
1382 }
1383 }
1384
1385 fn checkpoint_redo_erased(&self) -> zeph_tools::CheckpointActionResult {
1386 zeph_tools::CheckpointActionResult {
1387 reverted_commands: 0,
1388 restored: 0,
1389 deleted: 0,
1390 supported: true,
1391 message: "stub-redo".into(),
1392 }
1393 }
1394
1395 fn checkpoint_list_erased(&self) -> zeph_tools::CheckpointListResult {
1396 zeph_tools::CheckpointListResult {
1397 entries: vec![],
1398 redo_depth: 7,
1399 supported: true,
1400 }
1401 }
1402 }
1403
1404 fn checkpointing_stub() -> Arc<CheckpointingStub> {
1405 Arc::new(CheckpointingStub {
1406 trust_recorded: std::sync::Mutex::new(None),
1407 })
1408 }
1409
1410 #[test]
1411 fn filtered_executor_forwards_checkpoint_trio_and_speculatable() {
1412 let inner = checkpointing_stub();
1413 let exec = FilteredToolExecutor::new(Arc::clone(&inner) as _, ToolPolicy::InheritAll);
1414
1415 let undo = exec.checkpoint_undo_erased(7);
1416 assert!(
1417 undo.supported,
1418 "checkpoint_undo_erased must forward to inner"
1419 );
1420 assert_eq!(
1421 undo.reverted_commands, 7,
1422 "n must be forwarded, not hardcoded"
1423 );
1424 assert!(
1425 exec.checkpoint_redo_erased().supported,
1426 "checkpoint_redo_erased must forward to inner"
1427 );
1428 assert_eq!(
1429 exec.checkpoint_list_erased().redo_depth,
1430 7,
1431 "checkpoint_list_erased must forward to inner"
1432 );
1433 assert!(
1434 exec.is_tool_speculatable_erased("anything"),
1435 "is_tool_speculatable_erased must forward to inner, not default to false"
1436 );
1437 }
1438
1439 #[tokio::test]
1440 async fn filtered_executor_confirmed_erased_still_enforces_policy() {
1441 let inner = checkpointing_stub();
1444 let exec = FilteredToolExecutor::with_disallowed(
1445 Arc::clone(&inner) as _,
1446 ToolPolicy::InheritAll,
1447 vec!["blocked_tool".into()],
1448 );
1449 let call = ToolCall {
1450 tool_id: "blocked_tool".into(),
1451 params: serde_json::Map::default(),
1452 caller_id: None,
1453 context: None,
1454
1455 tool_call_id: String::new(),
1456 skill_name: None,
1457 };
1458 let res = exec.execute_tool_call_confirmed_erased(&call).await;
1460 assert_matches!(res, Err(ToolError::Blocked { .. }));
1461 }
1462
1463 #[test]
1464 fn plan_mode_forwards_checkpoint_trio_and_speculatable() {
1465 let inner = checkpointing_stub();
1466 let exec = PlanModeExecutor::new(Arc::clone(&inner) as _);
1467
1468 let undo = exec.checkpoint_undo_erased(3);
1469 assert!(
1470 undo.supported,
1471 "checkpoint_undo_erased must forward to inner"
1472 );
1473 assert_eq!(undo.reverted_commands, 3);
1474 assert!(exec.checkpoint_redo_erased().supported);
1475 assert_eq!(exec.checkpoint_list_erased().redo_depth, 7);
1476 assert!(
1477 exec.is_tool_speculatable_erased("anything"),
1478 "is_tool_speculatable_erased must forward to inner, not default to false"
1479 );
1480 }
1481
1482 #[test]
1483 fn plan_mode_forwards_set_effective_trust() {
1484 let inner = checkpointing_stub();
1485 let exec = PlanModeExecutor::new(Arc::clone(&inner) as _);
1486 exec.set_effective_trust(zeph_tools::SkillTrustLevel::Quarantined);
1487 assert_eq!(
1488 *inner.trust_recorded.lock().unwrap(),
1489 Some(zeph_tools::SkillTrustLevel::Quarantined),
1490 "set_effective_trust must forward to inner"
1491 );
1492 }
1493
1494 #[tokio::test]
1495 async fn plan_mode_confirmed_erased_still_blocks_execution() {
1496 let inner = checkpointing_stub();
1497 let exec = PlanModeExecutor::new(Arc::clone(&inner) as _);
1498 let call = ToolCall {
1499 tool_id: "shell".into(),
1500 params: serde_json::Map::default(),
1501 caller_id: None,
1502 context: None,
1503
1504 tool_call_id: String::new(),
1505 skill_name: None,
1506 };
1507 let res = exec.execute_tool_call_confirmed_erased(&call).await;
1508 assert!(
1509 res.is_err(),
1510 "plan mode must block confirmed execution too, not just unconfirmed"
1511 );
1512 }
1513
1514 #[test]
1517 fn normalize_tool_id_lowercases() {
1518 assert_eq!(normalize_tool_id("Read"), "read");
1519 assert_eq!(normalize_tool_id("Write"), "write");
1520 assert_eq!(normalize_tool_id("Edit"), "edit");
1521 }
1522
1523 #[test]
1524 fn normalize_tool_id_strips_args() {
1525 assert_eq!(normalize_tool_id("Bash(cargo *)"), "bash");
1526 assert_eq!(normalize_tool_id("Bash(git *)"), "bash");
1527 assert_eq!(normalize_tool_id("bash"), "bash");
1528 }
1529
1530 #[test]
1531 fn allow_list_pascal_case_permits_lowercase_runtime_id() {
1532 let exec = FilteredToolExecutor::new(
1533 stub_box(&["read", "write", "bash"]),
1534 ToolPolicy::AllowList(vec!["Read".into(), "Write".into(), "Bash(cargo *)".into()]),
1535 );
1536 assert!(exec.is_allowed("read"));
1538 assert!(exec.is_allowed("write"));
1539 assert!(exec.is_allowed("bash"));
1540 assert!(!exec.is_allowed("web"));
1541 let defs = exec.tool_definitions_erased();
1543 assert_eq!(
1544 defs.len(),
1545 3,
1546 "read, write, bash must all appear in catalog"
1547 );
1548 }
1549
1550 #[test]
1553 fn filter_skills_empty_registry_returns_empty() {
1554 let registry = zeph_skills::registry::SkillRegistry::load(&[] as &[&str]);
1555 let filter = SkillFilter::default();
1556 let result = filter_skills(®istry, &filter).unwrap();
1557 assert!(result.is_empty());
1558 }
1559
1560 #[test]
1561 fn filter_skills_empty_include_passes_all() {
1562 let registry = zeph_skills::registry::SkillRegistry::load(&[] as &[&str]);
1565 let filter = SkillFilter {
1566 include: vec![],
1567 exclude: vec![],
1568 };
1569 let result = filter_skills(®istry, &filter).unwrap();
1570 assert!(result.is_empty());
1571 }
1572
1573 #[test]
1574 fn filter_skills_double_star_pattern_is_error() {
1575 let registry = zeph_skills::registry::SkillRegistry::load(&[] as &[&str]);
1576 let filter = SkillFilter {
1577 include: vec!["**".into()],
1578 exclude: vec![],
1579 };
1580 let err = filter_skills(®istry, &filter).unwrap_err();
1581 assert_matches!(err, SubAgentError::Invalid(_));
1582 }
1583
1584 mod proptest_glob {
1585 use proptest::prelude::*;
1586
1587 use super::{compile_glob, glob_match};
1588
1589 proptest! {
1590 #![proptest_config(proptest::test_runner::Config::with_cases(500))]
1591
1592 #[test]
1594 fn glob_match_never_panics(
1595 pattern in "[a-z*-]{1,10}",
1596 name in "[a-z-]{0,15}",
1597 ) {
1598 if !pattern.contains("**")
1600 && let Ok(p) = compile_glob(&pattern)
1601 {
1602 let _ = glob_match(&p, &name);
1603 }
1604 }
1605
1606 #[test]
1608 fn glob_literal_matches_only_exact(
1609 name in "[a-z-]{1,10}",
1610 ) {
1611 let p = compile_glob(&name).unwrap();
1613 prop_assert!(glob_match(&p, &name));
1614
1615 let other = format!("{name}-x");
1617 prop_assert!(!glob_match(&p, &other));
1618 }
1619
1620 #[test]
1622 fn glob_star_matches_everything(name in ".*") {
1623 let p = compile_glob("*").unwrap();
1624 prop_assert!(glob_match(&p, &name));
1625 }
1626 }
1627 }
1628}