1use std::collections::HashMap;
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15use tracing::debug;
16
17use clawft_types::routing::UserPermissions;
18
19#[non_exhaustive]
24#[derive(Debug, thiserror::Error)]
25pub enum ToolError {
26 #[error("tool not found: {0}")]
28 NotFound(String),
29
30 #[error("invalid arguments: {0}")]
32 InvalidArgs(String),
33
34 #[error("execution failed: {0}")]
36 ExecutionFailed(String),
37
38 #[error("permission denied for tool '{tool}': {reason}")]
44 PermissionDenied { tool: String, reason: String },
45
46 #[error("not found: {0}")]
48 FileNotFound(String),
49
50 #[error("invalid path: {0}")]
52 InvalidPath(String),
53
54 #[error("timeout after {0}s")]
56 Timeout(u64),
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct ToolMetadata {
69 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub required_permission_level: Option<u8>,
73
74 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
78 pub required_custom_permissions: HashMap<String, serde_json::Value>,
79}
80
81fn glob_matches(pattern: &str, text: &str) -> bool {
98 let pattern: Vec<char> = pattern.chars().collect();
99 let text: Vec<char> = text.chars().collect();
100 let (plen, tlen) = (pattern.len(), text.len());
101
102 let mut pi = 0;
103 let mut ti = 0;
104 let mut star_pi = None;
105 let mut star_ti = 0;
106
107 while ti < tlen {
108 if pi < plen && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
109 pi += 1;
110 ti += 1;
111 } else if pi < plen && pattern[pi] == '*' {
112 star_pi = Some(pi);
113 star_ti = ti;
114 pi += 1;
115 } else if let Some(spi) = star_pi {
116 pi = spi + 1;
117 star_ti += 1;
118 ti = star_ti;
119 } else {
120 return false;
121 }
122 }
123
124 while pi < plen && pattern[pi] == '*' {
126 pi += 1;
127 }
128
129 pi == plen
130}
131
132fn matches_any_pattern(tool_name: &str, patterns: &[String]) -> bool {
135 patterns.iter().any(|pattern| {
136 if pattern.contains('*') || pattern.contains('?') {
137 glob_matches(pattern, tool_name)
138 } else {
139 pattern == tool_name
140 }
141 })
142}
143
144pub fn check_tool_permission(
161 tool_name: &str,
162 permissions: &UserPermissions,
163 tool_metadata: Option<&ToolMetadata>,
164) -> Result<(), ToolError> {
165 if matches_any_pattern(tool_name, &permissions.tool_denylist) {
167 return Err(ToolError::PermissionDenied {
168 tool: tool_name.to_string(),
169 reason: "tool is explicitly denied for this user".to_string(),
170 });
171 }
172
173 let allowed = if permissions.tool_access.is_empty() {
175 false
177 } else if permissions.tool_access.iter().any(|s| s == "*") {
178 true
180 } else {
181 matches_any_pattern(tool_name, &permissions.tool_access)
183 };
184
185 if !allowed {
186 return Err(ToolError::PermissionDenied {
187 tool: tool_name.to_string(),
188 reason: format!(
189 "tool is not in the allowed tools for permission level {}",
190 permissions.level,
191 ),
192 });
193 }
194
195 if let Some(meta) = tool_metadata {
197 if let Some(required_level) = meta.required_permission_level
198 && permissions.level < required_level
199 {
200 return Err(ToolError::PermissionDenied {
201 tool: tool_name.to_string(),
202 reason: format!(
203 "tool requires permission level {} but user has level {}",
204 required_level, permissions.level,
205 ),
206 });
207 }
208
209 for (key, required_value) in &meta.required_custom_permissions {
211 match permissions.custom_permissions.get(key) {
212 None => {
213 return Err(ToolError::PermissionDenied {
214 tool: tool_name.to_string(),
215 reason: format!(
216 "tool requires custom permission '{}' which is not set",
217 key,
218 ),
219 });
220 }
221 Some(actual) if actual != required_value => {
222 return Err(ToolError::PermissionDenied {
223 tool: tool_name.to_string(),
224 reason: format!(
225 "tool requires {}={} but user has {}={}",
226 key, required_value, key, actual,
227 ),
228 });
229 }
230 Some(_) => {} }
232 }
233 }
234
235 Ok(())
236}
237
238pub fn extract_mcp_metadata(tool_decl: &serde_json::Value) -> ToolMetadata {
244 let required_level = tool_decl
245 .get("required_permission_level")
246 .and_then(|v| v.as_u64())
247 .map(|v| v as u8);
248
249 let required_custom = tool_decl
250 .get("required_custom_permissions")
251 .and_then(|v| v.as_object())
252 .map(|obj| {
253 obj.iter()
254 .map(|(k, v)| (k.clone(), v.clone()))
255 .collect()
256 })
257 .unwrap_or_default();
258
259 ToolMetadata {
260 required_permission_level: required_level,
261 required_custom_permissions: required_custom,
262 }
263}
264
265#[cfg_attr(not(feature = "browser"), async_trait)]
305#[cfg_attr(feature = "browser", async_trait(?Send))]
306pub trait Tool: Send + Sync {
307 fn name(&self) -> &str;
309
310 fn description(&self) -> &str;
312
313 fn parameters(&self) -> serde_json::Value;
318
319 async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError>;
325
326 fn metadata(&self) -> Option<ToolMetadata> {
331 None
332 }
333}
334
335pub struct ToolRegistry {
340 tools: HashMap<String, Arc<dyn Tool>>,
341 metadata: HashMap<String, ToolMetadata>,
342}
343
344impl ToolRegistry {
345 pub fn new() -> Self {
347 Self {
348 tools: HashMap::new(),
349 metadata: HashMap::new(),
350 }
351 }
352
353 pub fn register(&mut self, tool: Arc<dyn Tool>) {
359 let name = tool.name().to_string();
360 debug!(tool = %name, "registering tool");
361 if let Some(meta) = tool.metadata() {
363 self.metadata.insert(name.clone(), meta);
364 }
365 self.tools.insert(name, tool);
366 }
367
368 pub fn register_with_metadata(&mut self, tool: Arc<dyn Tool>, metadata: ToolMetadata) {
371 let name = tool.name().to_string();
372 debug!(tool = %name, "registering tool with metadata");
373 self.metadata.insert(name.clone(), metadata);
374 self.tools.insert(name, tool);
375 }
376
377 pub fn has(&self, name: &str) -> bool {
379 self.tools.contains_key(name)
380 }
381
382 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
384 self.tools.get(name).cloned()
385 }
386
387 pub fn get_metadata(&self, name: &str) -> Option<&ToolMetadata> {
389 self.metadata.get(name)
390 }
391
392 pub fn list(&self) -> Vec<String> {
394 let mut names: Vec<String> = self.tools.keys().cloned().collect();
395 names.sort();
396 names
397 }
398
399 pub fn schemas(&self) -> Vec<serde_json::Value> {
415 let mut schemas: Vec<(String, serde_json::Value)> = self
416 .tools
417 .iter()
418 .map(|(name, tool)| {
419 let schema = serde_json::json!({
420 "type": "function",
421 "function": {
422 "name": name,
423 "description": tool.description(),
424 "parameters": tool.parameters(),
425 }
426 });
427 (name.clone(), schema)
428 })
429 .collect();
430
431 schemas.sort_by(|a, b| a.0.cmp(&b.0));
432 schemas.into_iter().map(|(_, v)| v).collect()
433 }
434
435 pub fn schemas_for_tools(&self, allowed: &[String]) -> Vec<serde_json::Value> {
443 let mut schemas: Vec<(String, serde_json::Value)> = self
444 .tools
445 .iter()
446 .filter(|(name, _)| matches_any_pattern(name, allowed))
447 .map(|(name, tool)| {
448 let schema = serde_json::json!({
449 "type": "function",
450 "function": {
451 "name": name,
452 "description": tool.description(),
453 "parameters": tool.parameters(),
454 }
455 });
456 (name.clone(), schema)
457 })
458 .collect();
459
460 schemas.sort_by(|a, b| a.0.cmp(&b.0));
461 schemas.into_iter().map(|(_, v)| v).collect()
462 }
463
464 pub async fn execute(
473 &self,
474 name: &str,
475 args: serde_json::Value,
476 permissions: Option<&UserPermissions>,
477 ) -> Result<serde_json::Value, ToolError> {
478 let tool = self
480 .tools
481 .get(name)
482 .ok_or_else(|| ToolError::NotFound(name.to_string()))?;
483
484 if let Some(perms) = permissions {
486 let meta = self.metadata.get(name);
487 check_tool_permission(name, perms, meta)?;
488 }
489
490 debug!(tool = %name, "executing tool");
491 tool.execute(args).await
492 }
493
494 pub fn len(&self) -> usize {
496 self.tools.len()
497 }
498
499 pub fn is_empty(&self) -> bool {
501 self.tools.is_empty()
502 }
503
504 pub fn filtered_tools(&self, allow: &[String], deny: &[String]) -> Self {
514 let mut filtered = Self::new();
515 for (name, tool) in &self.tools {
516 if deny.iter().any(|d| d == name) {
518 continue;
519 }
520 if !allow.is_empty() && !allow.iter().any(|a| a == name) {
522 continue;
523 }
524 filtered.tools.insert(name.clone(), tool.clone());
525 if let Some(meta) = self.metadata.get(name) {
526 filtered.metadata.insert(name.clone(), meta.clone());
527 }
528 }
529 filtered
530 }
531
532 pub fn snapshot(&self) -> Self {
540 Self {
541 tools: self.tools.clone(),
542 metadata: self.metadata.clone(),
543 }
544 }
545}
546
547impl Default for ToolRegistry {
548 fn default() -> Self {
549 Self::new()
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 struct EchoTool;
559
560 #[async_trait]
561 impl Tool for EchoTool {
562 fn name(&self) -> &str {
563 "echo"
564 }
565
566 fn description(&self) -> &str {
567 "Echo back the input text"
568 }
569
570 fn parameters(&self) -> serde_json::Value {
571 serde_json::json!({
572 "type": "object",
573 "properties": {
574 "text": {
575 "type": "string",
576 "description": "Text to echo"
577 }
578 },
579 "required": ["text"]
580 })
581 }
582
583 async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
584 let text = args
585 .get("text")
586 .and_then(|v| v.as_str())
587 .ok_or_else(|| ToolError::InvalidArgs("missing 'text' field".into()))?;
588 Ok(serde_json::json!({ "output": text }))
589 }
590 }
591
592 struct FailTool;
594
595 #[async_trait]
596 impl Tool for FailTool {
597 fn name(&self) -> &str {
598 "fail"
599 }
600
601 fn description(&self) -> &str {
602 "A tool that always fails"
603 }
604
605 fn parameters(&self) -> serde_json::Value {
606 serde_json::json!({
607 "type": "object",
608 "properties": {}
609 })
610 }
611
612 async fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
613 Err(ToolError::ExecutionFailed("intentional failure".into()))
614 }
615 }
616
617 struct AddTool;
619
620 #[async_trait]
621 impl Tool for AddTool {
622 fn name(&self) -> &str {
623 "add"
624 }
625
626 fn description(&self) -> &str {
627 "Add two numbers"
628 }
629
630 fn parameters(&self) -> serde_json::Value {
631 serde_json::json!({
632 "type": "object",
633 "properties": {
634 "a": { "type": "number" },
635 "b": { "type": "number" }
636 },
637 "required": ["a", "b"]
638 })
639 }
640
641 async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
642 let a = args
643 .get("a")
644 .and_then(|v| v.as_f64())
645 .ok_or_else(|| ToolError::InvalidArgs("missing 'a'".into()))?;
646 let b = args
647 .get("b")
648 .and_then(|v| v.as_f64())
649 .ok_or_else(|| ToolError::InvalidArgs("missing 'b'".into()))?;
650 Ok(serde_json::json!({ "result": a + b }))
651 }
652 }
653
654 fn admin_permissions() -> UserPermissions {
656 UserPermissions {
657 level: 2,
658 tool_access: vec!["*".into()],
659 ..UserPermissions::default()
660 }
661 }
662
663 fn user_permissions(tools: Vec<&str>) -> UserPermissions {
665 UserPermissions {
666 level: 1,
667 tool_access: tools.into_iter().map(String::from).collect(),
668 ..UserPermissions::default()
669 }
670 }
671
672 fn zero_trust_permissions() -> UserPermissions {
674 UserPermissions::default()
675 }
676
677 #[test]
678 fn new_registry_is_empty() {
679 let registry = ToolRegistry::new();
680 assert!(registry.is_empty());
681 assert_eq!(registry.len(), 0);
682 assert!(registry.list().is_empty());
683 }
684
685 #[test]
686 fn default_registry_is_empty() {
687 let registry = ToolRegistry::default();
688 assert!(registry.is_empty());
689 }
690
691 #[test]
692 fn register_and_get() {
693 let mut registry = ToolRegistry::new();
694 registry.register(Arc::new(EchoTool));
695
696 let tool = registry.get("echo").unwrap();
697 assert_eq!(tool.name(), "echo");
698 assert_eq!(tool.description(), "Echo back the input text");
699 }
700
701 #[test]
702 fn get_nonexistent_returns_none() {
703 let registry = ToolRegistry::new();
704 assert!(registry.get("nonexistent").is_none());
705 }
706
707 #[test]
708 fn list_returns_sorted_names() {
709 let mut registry = ToolRegistry::new();
710 registry.register(Arc::new(EchoTool));
711 registry.register(Arc::new(AddTool));
712 registry.register(Arc::new(FailTool));
713
714 let names = registry.list();
715 assert_eq!(names, vec!["add", "echo", "fail"]);
716 }
717
718 #[test]
719 fn len_reflects_registered_count() {
720 let mut registry = ToolRegistry::new();
721 assert_eq!(registry.len(), 0);
722
723 registry.register(Arc::new(EchoTool));
724 assert_eq!(registry.len(), 1);
725
726 registry.register(Arc::new(AddTool));
727 assert_eq!(registry.len(), 2);
728 }
729
730 #[test]
731 fn register_replaces_existing() {
732 let mut registry = ToolRegistry::new();
733 registry.register(Arc::new(EchoTool));
734 registry.register(Arc::new(EchoTool)); assert_eq!(registry.len(), 1);
736 }
737
738 #[tokio::test]
739 async fn execute_echo_tool() {
740 let mut registry = ToolRegistry::new();
741 registry.register(Arc::new(EchoTool));
742
743 let result = registry
744 .execute("echo", serde_json::json!({ "text": "hello" }), None)
745 .await
746 .unwrap();
747
748 assert_eq!(result["output"], "hello");
749 }
750
751 #[tokio::test]
752 async fn execute_add_tool() {
753 let mut registry = ToolRegistry::new();
754 registry.register(Arc::new(AddTool));
755
756 let result = registry
757 .execute("add", serde_json::json!({ "a": 3, "b": 4 }), None)
758 .await
759 .unwrap();
760
761 assert_eq!(result["result"], 7.0);
762 }
763
764 #[tokio::test]
765 async fn execute_not_found() {
766 let registry = ToolRegistry::new();
767 let result = registry
768 .execute("missing", serde_json::json!({}), None)
769 .await;
770
771 assert!(result.is_err());
772 match result.unwrap_err() {
773 ToolError::NotFound(name) => assert_eq!(name, "missing"),
774 other => panic!("expected NotFound, got: {other}"),
775 }
776 }
777
778 #[tokio::test]
779 async fn execute_tool_that_fails() {
780 let mut registry = ToolRegistry::new();
781 registry.register(Arc::new(FailTool));
782
783 let result = registry
784 .execute("fail", serde_json::json!({}), None)
785 .await;
786 assert!(result.is_err());
787 match result.unwrap_err() {
788 ToolError::ExecutionFailed(msg) => {
789 assert_eq!(msg, "intentional failure");
790 }
791 other => panic!("expected ExecutionFailed, got: {other}"),
792 }
793 }
794
795 #[tokio::test]
796 async fn execute_with_invalid_args() {
797 let mut registry = ToolRegistry::new();
798 registry.register(Arc::new(EchoTool));
799
800 let result = registry
801 .execute("echo", serde_json::json!({}), None) .await;
803
804 assert!(result.is_err());
805 match result.unwrap_err() {
806 ToolError::InvalidArgs(msg) => {
807 assert!(msg.contains("text"));
808 }
809 other => panic!("expected InvalidArgs, got: {other}"),
810 }
811 }
812
813 #[test]
814 fn schemas_openai_format() {
815 let mut registry = ToolRegistry::new();
816 registry.register(Arc::new(EchoTool));
817 registry.register(Arc::new(AddTool));
818
819 let schemas = registry.schemas();
820 assert_eq!(schemas.len(), 2);
821
822 let first = &schemas[0];
824 assert_eq!(first["type"], "function");
825 assert_eq!(first["function"]["name"], "add");
826 assert_eq!(first["function"]["description"], "Add two numbers");
827 assert!(
828 first["function"]["parameters"]["properties"]
829 .get("a")
830 .is_some()
831 );
832
833 let second = &schemas[1];
834 assert_eq!(second["type"], "function");
835 assert_eq!(second["function"]["name"], "echo");
836 assert_eq!(
837 second["function"]["description"],
838 "Echo back the input text"
839 );
840 assert!(
841 second["function"]["parameters"]["properties"]
842 .get("text")
843 .is_some()
844 );
845 }
846
847 #[test]
848 fn schemas_empty_registry() {
849 let registry = ToolRegistry::new();
850 assert!(registry.schemas().is_empty());
851 }
852
853 #[test]
854 fn schemas_for_tools_filters_by_pattern() {
855 let mut registry = ToolRegistry::new();
856 registry.register(Arc::new(EchoTool));
857 registry.register(Arc::new(AddTool));
858 registry.register(Arc::new(FailTool));
859
860 let schemas = registry.schemas_for_tools(&["echo".into()]);
862 assert_eq!(schemas.len(), 1);
863 assert_eq!(schemas[0]["function"]["name"], "echo");
864
865 let schemas = registry.schemas_for_tools(&["*".into()]);
867 assert_eq!(schemas.len(), 3);
868
869 let schemas = registry.schemas_for_tools(&["nonexistent".into()]);
871 assert!(schemas.is_empty());
872
873 let schemas = registry.schemas_for_tools(&["echo".into(), "add".into()]);
875 assert_eq!(schemas.len(), 2);
876 }
877
878 #[test]
879 fn tool_error_display() {
880 let err = ToolError::NotFound("web_search".into());
881 assert_eq!(err.to_string(), "tool not found: web_search");
882
883 let err = ToolError::InvalidArgs("missing required field".into());
884 assert_eq!(err.to_string(), "invalid arguments: missing required field");
885
886 let err = ToolError::ExecutionFailed("command failed".into());
887 assert_eq!(err.to_string(), "execution failed: command failed");
888
889 let err = ToolError::PermissionDenied {
890 tool: "test".into(),
891 reason: "no exec access".into(),
892 };
893 assert_eq!(
894 err.to_string(),
895 "permission denied for tool 'test': no exec access"
896 );
897
898 let err = ToolError::FileNotFound("/tmp/missing.txt".into());
899 assert_eq!(err.to_string(), "not found: /tmp/missing.txt");
900
901 let err = ToolError::InvalidPath("../../../etc/passwd".into());
902 assert_eq!(err.to_string(), "invalid path: ../../../etc/passwd");
903
904 let err = ToolError::Timeout(30);
905 assert_eq!(err.to_string(), "timeout after 30s");
906 }
907
908 #[test]
909 fn tool_trait_is_object_safe() {
910 fn accepts_tool(_t: &dyn Tool) {}
912 let tool = EchoTool;
913 accepts_tool(&tool);
914 }
915
916 #[test]
917 fn tool_parameters_returns_valid_json_schema() {
918 let tool = EchoTool;
919 let params = tool.parameters();
920 assert_eq!(params["type"], "object");
921 assert!(params["properties"].is_object());
922 assert!(params["required"].is_array());
923 }
924
925 #[test]
928 fn test_glob_star_matches() {
929 assert!(glob_matches("file_*", "file_read"));
930 assert!(glob_matches("file_*", "file_write"));
931 assert!(!glob_matches("file_*", "web_search"));
932 assert!(glob_matches("*", "anything"));
933 assert!(glob_matches("*", ""));
934 assert!(glob_matches("myserver__*", "myserver__search"));
935 assert!(!glob_matches("myserver__*", "otherserver__search"));
936 }
937
938 #[test]
939 fn test_glob_question_mark() {
940 assert!(glob_matches("read_?", "read_a"));
941 assert!(!glob_matches("read_?", "read_file"));
942 assert!(glob_matches("?", "a"));
943 assert!(!glob_matches("?", ""));
944 }
945
946 #[test]
947 fn test_glob_exact_match_fast_path() {
948 assert!(matches_any_pattern("read_file", &["read_file".into()]));
950 assert!(!matches_any_pattern("write_file", &["read_file".into()]));
951 }
952
953 #[test]
956 fn test_wildcard_allows_all_tools() {
957 let perms = admin_permissions();
958 assert!(check_tool_permission("exec_shell", &perms, None).is_ok());
959 assert!(check_tool_permission("read_file", &perms, None).is_ok());
960 assert!(check_tool_permission("myserver__tool", &perms, None).is_ok());
961 }
962
963 #[test]
964 fn test_empty_allowlist_denies_all_tools() {
965 let perms = zero_trust_permissions();
966 let result = check_tool_permission("read_file", &perms, None);
967 assert!(result.is_err());
968 match result.unwrap_err() {
969 ToolError::PermissionDenied { tool, reason } => {
970 assert_eq!(tool, "read_file");
971 assert!(reason.contains("not in the allowed tools"));
972 }
973 other => panic!("expected PermissionDenied, got: {other}"),
974 }
975 }
976
977 #[test]
978 fn test_explicit_allowlist_permits_listed_tool() {
979 let perms = user_permissions(vec!["read_file", "write_file"]);
980 assert!(check_tool_permission("read_file", &perms, None).is_ok());
981 assert!(check_tool_permission("write_file", &perms, None).is_ok());
982 }
983
984 #[test]
985 fn test_explicit_allowlist_denies_unlisted_tool() {
986 let perms = user_permissions(vec!["read_file", "write_file"]);
987 assert!(check_tool_permission("exec_shell", &perms, None).is_err());
988 }
989
990 #[test]
991 fn test_denylist_overrides_wildcard() {
992 let perms = UserPermissions {
993 level: 2,
994 tool_access: vec!["*".into()],
995 tool_denylist: vec!["exec_shell".into()],
996 ..UserPermissions::default()
997 };
998 assert!(check_tool_permission("exec_shell", &perms, None).is_err());
999 assert!(check_tool_permission("read_file", &perms, None).is_ok());
1000 }
1001
1002 #[test]
1003 fn test_denylist_overrides_explicit_allow() {
1004 let perms = UserPermissions {
1005 level: 1,
1006 tool_access: vec!["exec_shell".into()],
1007 tool_denylist: vec!["exec_shell".into()],
1008 ..UserPermissions::default()
1009 };
1010 assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1011 }
1012
1013 #[test]
1014 fn test_glob_allowlist_pattern() {
1015 let perms = UserPermissions {
1016 level: 1,
1017 tool_access: vec!["file_*".into()],
1018 ..UserPermissions::default()
1019 };
1020 assert!(check_tool_permission("file_read", &perms, None).is_ok());
1021 assert!(check_tool_permission("file_write", &perms, None).is_ok());
1022 assert!(check_tool_permission("web_search", &perms, None).is_err());
1023 }
1024
1025 #[test]
1026 fn test_glob_denylist_pattern() {
1027 let perms = UserPermissions {
1028 level: 2,
1029 tool_access: vec!["*".into()],
1030 tool_denylist: vec!["exec_*".into()],
1031 ..UserPermissions::default()
1032 };
1033 assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1034 assert!(check_tool_permission("exec_spawn", &perms, None).is_err());
1035 assert!(check_tool_permission("read_file", &perms, None).is_ok());
1036 }
1037
1038 #[test]
1039 fn test_metadata_required_level_blocks_low_user() {
1040 let perms = UserPermissions {
1041 level: 1,
1042 tool_access: vec!["*".into()],
1043 ..UserPermissions::default()
1044 };
1045 let meta = ToolMetadata {
1046 required_permission_level: Some(2),
1047 ..ToolMetadata::default()
1048 };
1049 let result = check_tool_permission("admin_tool", &perms, Some(&meta));
1050 assert!(result.is_err());
1051 match result.unwrap_err() {
1052 ToolError::PermissionDenied { reason, .. } => {
1053 assert!(reason.contains("requires permission level 2"));
1054 assert!(reason.contains("user has level 1"));
1055 }
1056 other => panic!("expected PermissionDenied, got: {other}"),
1057 }
1058 }
1059
1060 #[test]
1061 fn test_metadata_required_level_allows_sufficient_user() {
1062 let perms = UserPermissions {
1063 level: 2,
1064 tool_access: vec!["*".into()],
1065 ..UserPermissions::default()
1066 };
1067 let meta = ToolMetadata {
1068 required_permission_level: Some(2),
1069 ..ToolMetadata::default()
1070 };
1071 assert!(check_tool_permission("admin_tool", &perms, Some(&meta)).is_ok());
1072 }
1073
1074 #[test]
1075 fn test_metadata_custom_permission_missing() {
1076 let perms = admin_permissions();
1077 let meta = ToolMetadata {
1078 required_custom_permissions: HashMap::from([(
1079 "exec_enabled".into(),
1080 serde_json::json!(true),
1081 )]),
1082 ..ToolMetadata::default()
1083 };
1084 let result = check_tool_permission("custom_tool", &perms, Some(&meta));
1085 assert!(result.is_err());
1086 match result.unwrap_err() {
1087 ToolError::PermissionDenied { reason, .. } => {
1088 assert!(reason.contains("exec_enabled"));
1089 assert!(reason.contains("not set"));
1090 }
1091 other => panic!("expected PermissionDenied, got: {other}"),
1092 }
1093 }
1094
1095 #[test]
1096 fn test_metadata_custom_permission_wrong_value() {
1097 let perms = UserPermissions {
1098 level: 2,
1099 tool_access: vec!["*".into()],
1100 custom_permissions: HashMap::from([(
1101 "exec_enabled".into(),
1102 serde_json::json!(false),
1103 )]),
1104 ..UserPermissions::default()
1105 };
1106 let meta = ToolMetadata {
1107 required_custom_permissions: HashMap::from([(
1108 "exec_enabled".into(),
1109 serde_json::json!(true),
1110 )]),
1111 ..ToolMetadata::default()
1112 };
1113 let result = check_tool_permission("custom_tool", &perms, Some(&meta));
1114 assert!(result.is_err());
1115 }
1116
1117 #[test]
1118 fn test_mcp_namespaced_tool_exact_match() {
1119 let perms = UserPermissions {
1120 level: 1,
1121 tool_access: vec!["myserver__search".into()],
1122 ..UserPermissions::default()
1123 };
1124 assert!(check_tool_permission("myserver__search", &perms, None).is_ok());
1125 assert!(check_tool_permission("myserver__exec", &perms, None).is_err());
1126 }
1127
1128 #[test]
1129 fn test_mcp_namespaced_tool_glob_match() {
1130 let perms = UserPermissions {
1131 level: 1,
1132 tool_access: vec!["myserver__*".into()],
1133 ..UserPermissions::default()
1134 };
1135 assert!(check_tool_permission("myserver__search", &perms, None).is_ok());
1136 assert!(check_tool_permission("otherserver__search", &perms, None).is_err());
1137 }
1138
1139 #[tokio::test]
1142 async fn test_registry_execute_with_none_permissions() {
1143 let mut registry = ToolRegistry::new();
1144 registry.register(Arc::new(EchoTool));
1145
1146 let result = registry
1148 .execute("echo", serde_json::json!({ "text": "hello" }), None)
1149 .await;
1150 assert!(result.is_ok());
1151 }
1152
1153 #[tokio::test]
1154 async fn test_registry_execute_with_denied_permissions() {
1155 let mut registry = ToolRegistry::new();
1156 registry.register(Arc::new(EchoTool));
1157
1158 let perms = zero_trust_permissions();
1159 let result = registry
1160 .execute(
1161 "echo",
1162 serde_json::json!({ "text": "hello" }),
1163 Some(&perms),
1164 )
1165 .await;
1166 assert!(result.is_err());
1167 assert!(matches!(
1168 result.unwrap_err(),
1169 ToolError::PermissionDenied { .. }
1170 ));
1171 }
1172
1173 #[tokio::test]
1174 async fn test_registry_execute_not_found_before_permission_check() {
1175 let registry = ToolRegistry::new();
1176 let perms = zero_trust_permissions();
1177
1178 let result = registry
1180 .execute("nonexistent", serde_json::json!({}), Some(&perms))
1181 .await;
1182 assert!(matches!(result.unwrap_err(), ToolError::NotFound(_)));
1183 }
1184
1185 #[test]
1188 fn test_zero_trust_blocked_from_exec_and_spawn() {
1189 let perms = zero_trust_permissions();
1190 assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1191 assert!(check_tool_permission("spawn", &perms, None).is_err());
1192 }
1193
1194 #[test]
1195 fn test_user_level_blocked_from_exec_and_spawn() {
1196 let perms = user_permissions(vec![
1197 "read_file",
1198 "write_file",
1199 "edit_file",
1200 "list_dir",
1201 "web_search",
1202 "web_fetch",
1203 "message",
1204 ]);
1205 assert!(check_tool_permission("exec_shell", &perms, None).is_err());
1206 assert!(check_tool_permission("spawn", &perms, None).is_err());
1207 }
1208
1209 #[test]
1210 fn test_admin_can_call_all_tools() {
1211 let perms = admin_permissions();
1212 assert!(check_tool_permission("exec_shell", &perms, None).is_ok());
1213 assert!(check_tool_permission("spawn", &perms, None).is_ok());
1214 assert!(check_tool_permission("read_file", &perms, None).is_ok());
1215 assert!(check_tool_permission("myserver__tool", &perms, None).is_ok());
1216 }
1217
1218 #[test]
1219 fn test_permission_denied_error_includes_tool_name() {
1220 let perms = zero_trust_permissions();
1221 let result = check_tool_permission("exec_shell", &perms, None);
1222 match result.unwrap_err() {
1223 ToolError::PermissionDenied { tool, reason } => {
1224 assert_eq!(tool, "exec_shell");
1225 assert!(!reason.is_empty());
1226 }
1227 other => panic!("expected PermissionDenied, got: {other}"),
1228 }
1229 }
1230
1231 #[test]
1232 fn test_permission_denied_error_display_format() {
1233 let err = ToolError::PermissionDenied {
1234 tool: "exec_shell".into(),
1235 reason: "tool is explicitly denied for this user".into(),
1236 };
1237 assert_eq!(
1238 err.to_string(),
1239 "permission denied for tool 'exec_shell': tool is explicitly denied for this user"
1240 );
1241 }
1242
1243 #[test]
1246 fn test_extract_mcp_metadata() {
1247 let decl = serde_json::json!({
1248 "required_permission_level": 2,
1249 "required_custom_permissions": {
1250 "exec_enabled": true
1251 }
1252 });
1253 let meta = extract_mcp_metadata(&decl);
1254 assert_eq!(meta.required_permission_level, Some(2));
1255 assert_eq!(
1256 meta.required_custom_permissions.get("exec_enabled"),
1257 Some(&serde_json::json!(true))
1258 );
1259 }
1260
1261 #[test]
1262 fn test_extract_mcp_metadata_empty() {
1263 let decl = serde_json::json!({});
1264 let meta = extract_mcp_metadata(&decl);
1265 assert!(meta.required_permission_level.is_none());
1266 assert!(meta.required_custom_permissions.is_empty());
1267 }
1268
1269 #[test]
1272 fn test_register_with_metadata_stores_metadata() {
1273 let mut registry = ToolRegistry::new();
1274 let meta = ToolMetadata {
1275 required_permission_level: Some(2),
1276 ..ToolMetadata::default()
1277 };
1278 registry.register_with_metadata(Arc::new(EchoTool), meta);
1279
1280 let stored = registry.get_metadata("echo").unwrap();
1281 assert_eq!(stored.required_permission_level, Some(2));
1282 }
1283
1284 #[tokio::test]
1285 async fn test_registry_execute_with_admin_and_metadata() {
1286 let mut registry = ToolRegistry::new();
1287 let meta = ToolMetadata {
1288 required_permission_level: Some(2),
1289 ..ToolMetadata::default()
1290 };
1291 registry.register_with_metadata(Arc::new(EchoTool), meta);
1292
1293 let perms = admin_permissions();
1294 let result = registry
1295 .execute(
1296 "echo",
1297 serde_json::json!({ "text": "hello" }),
1298 Some(&perms),
1299 )
1300 .await;
1301 assert!(result.is_ok());
1302 }
1303
1304 #[test]
1307 fn filtered_tools_allow_empty_returns_all() {
1308 let mut registry = ToolRegistry::new();
1309 registry.register(Arc::new(EchoTool));
1310 registry.register(Arc::new(AddTool));
1311 registry.register(Arc::new(FailTool));
1312
1313 let filtered = registry.filtered_tools(&[], &[]);
1314 assert_eq!(filtered.len(), 3);
1315 }
1316
1317 #[test]
1318 fn filtered_tools_allow_restricts() {
1319 let mut registry = ToolRegistry::new();
1320 registry.register(Arc::new(EchoTool));
1321 registry.register(Arc::new(AddTool));
1322 registry.register(Arc::new(FailTool));
1323
1324 let filtered = registry.filtered_tools(&["echo".into(), "add".into()], &[]);
1325 assert_eq!(filtered.len(), 2);
1326 assert!(filtered.has("echo"));
1327 assert!(filtered.has("add"));
1328 assert!(!filtered.has("fail"));
1329 }
1330
1331 #[test]
1332 fn filtered_tools_deny_overrides_allow() {
1333 let mut registry = ToolRegistry::new();
1334 registry.register(Arc::new(EchoTool));
1335 registry.register(Arc::new(AddTool));
1336
1337 let filtered = registry.filtered_tools(
1338 &["echo".into(), "add".into()],
1339 &["echo".into()],
1340 );
1341 assert_eq!(filtered.len(), 1);
1342 assert!(filtered.has("add"));
1343 assert!(!filtered.has("echo"));
1344 }
1345
1346 #[test]
1347 fn filtered_tools_deny_only() {
1348 let mut registry = ToolRegistry::new();
1349 registry.register(Arc::new(EchoTool));
1350 registry.register(Arc::new(AddTool));
1351 registry.register(Arc::new(FailTool));
1352
1353 let filtered = registry.filtered_tools(&[], &["fail".into()]);
1354 assert_eq!(filtered.len(), 2);
1355 assert!(!filtered.has("fail"));
1356 }
1357
1358 #[test]
1359 fn filtered_tools_preserves_metadata() {
1360 let mut registry = ToolRegistry::new();
1361 let meta = ToolMetadata {
1362 required_permission_level: Some(2),
1363 ..ToolMetadata::default()
1364 };
1365 registry.register_with_metadata(Arc::new(EchoTool), meta);
1366 registry.register(Arc::new(AddTool));
1367
1368 let filtered = registry.filtered_tools(&["echo".into()], &[]);
1369 assert!(filtered.get_metadata("echo").is_some());
1370 assert_eq!(
1371 filtered.get_metadata("echo").unwrap().required_permission_level,
1372 Some(2)
1373 );
1374 }
1375
1376 #[tokio::test]
1377 async fn test_registry_execute_with_low_level_and_metadata() {
1378 let mut registry = ToolRegistry::new();
1379 let meta = ToolMetadata {
1380 required_permission_level: Some(2),
1381 ..ToolMetadata::default()
1382 };
1383 registry.register_with_metadata(Arc::new(EchoTool), meta);
1384
1385 let perms = UserPermissions {
1386 level: 1,
1387 tool_access: vec!["*".into()],
1388 ..UserPermissions::default()
1389 };
1390 let result = registry
1391 .execute(
1392 "echo",
1393 serde_json::json!({ "text": "hello" }),
1394 Some(&perms),
1395 )
1396 .await;
1397 assert!(matches!(
1398 result.unwrap_err(),
1399 ToolError::PermissionDenied { .. }
1400 ));
1401 }
1402}