1use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::defaults::default_true;
9use crate::providers::ProviderName;
10
11pub use crate::mcp_security::ToolSecurityMeta;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum McpTrustLevel {
21 Trusted,
23 #[default]
25 Untrusted,
26 Sandboxed,
28}
29
30impl McpTrustLevel {
31 #[must_use]
35 pub fn restriction_level(self) -> u8 {
36 match self {
37 Self::Trusted => 0,
38 Self::Untrusted => 1,
39 Self::Sandboxed => 2,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Deserialize, Serialize)]
46pub struct RateLimit {
47 pub max_calls_per_minute: u32,
49}
50
51#[derive(Debug, Clone, Default, Deserialize, Serialize)]
55#[serde(default)]
56pub struct McpPolicy {
57 pub allowed_tools: Option<Vec<String>>,
59 pub denied_tools: Vec<String>,
61 pub rate_limit: Option<RateLimit>,
63}
64
65fn default_skill_allowlist() -> Vec<String> {
66 vec!["*".into()]
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct ChannelSkillsConfig {
76 #[serde(default = "default_skill_allowlist")]
79 pub allowed: Vec<String>,
80}
81
82impl Default for ChannelSkillsConfig {
83 fn default() -> Self {
84 Self {
85 allowed: default_skill_allowlist(),
86 }
87 }
88}
89
90#[must_use]
95pub fn is_skill_allowed(name: &str, config: &ChannelSkillsConfig) -> bool {
96 config.allowed.iter().any(|p| glob_match(p, name))
97}
98
99fn glob_match(pattern: &str, name: &str) -> bool {
100 if let Some(prefix) = pattern.strip_suffix('*') {
101 if prefix.is_empty() {
102 return true;
103 }
104 name.starts_with(prefix)
105 } else {
106 pattern == name
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn allow(patterns: &[&str]) -> ChannelSkillsConfig {
115 ChannelSkillsConfig {
116 allowed: patterns.iter().map(ToString::to_string).collect(),
117 }
118 }
119
120 #[test]
121 fn test_default_output_schema_hint_bytes_is_1024() {
122 assert_eq!(default_output_schema_hint_bytes(), 1024);
123 }
124
125 #[test]
126 fn test_mcp_config_default_output_schema_hint_bytes_is_1024() {
127 let cfg = McpConfig::default();
128 assert_eq!(cfg.output_schema_hint_bytes, 1024);
129 }
130
131 #[test]
132 fn wildcard_star_allows_any_skill() {
133 let cfg = allow(&["*"]);
134 assert!(is_skill_allowed("anything", &cfg));
135 assert!(is_skill_allowed("web-search", &cfg));
136 }
137
138 #[test]
139 fn empty_allowlist_denies_all() {
140 let cfg = allow(&[]);
141 assert!(!is_skill_allowed("web-search", &cfg));
142 assert!(!is_skill_allowed("shell", &cfg));
143 }
144
145 #[test]
146 fn exact_match_allows_only_that_skill() {
147 let cfg = allow(&["web-search"]);
148 assert!(is_skill_allowed("web-search", &cfg));
149 assert!(!is_skill_allowed("shell", &cfg));
150 assert!(!is_skill_allowed("web-search-extra", &cfg));
151 }
152
153 #[test]
154 fn prefix_wildcard_allows_matching_skills() {
155 let cfg = allow(&["web-*"]);
156 assert!(is_skill_allowed("web-search", &cfg));
157 assert!(is_skill_allowed("web-fetch", &cfg));
158 assert!(!is_skill_allowed("shell", &cfg));
159 assert!(!is_skill_allowed("awesome-web-thing", &cfg));
160 }
161
162 #[test]
163 fn multiple_patterns_or_logic() {
164 let cfg = allow(&["shell", "web-*"]);
165 assert!(is_skill_allowed("shell", &cfg));
166 assert!(is_skill_allowed("web-search", &cfg));
167 assert!(!is_skill_allowed("memory", &cfg));
168 }
169
170 #[test]
171 fn default_config_allows_all() {
172 let cfg = ChannelSkillsConfig::default();
173 assert!(is_skill_allowed("any-skill", &cfg));
174 }
175
176 #[test]
177 fn prefix_wildcard_does_not_match_empty_suffix() {
178 let cfg = allow(&["web-*"]);
179 assert!(is_skill_allowed("web-", &cfg));
183 }
184
185 #[test]
186 fn matching_is_case_sensitive() {
187 let cfg = allow(&["Web-Search"]);
188 assert!(!is_skill_allowed("web-search", &cfg));
189 assert!(is_skill_allowed("Web-Search", &cfg));
190 }
191}
192
193fn default_slack_port() -> u16 {
194 3000
195}
196
197fn default_slack_webhook_host() -> String {
198 "127.0.0.1".into()
199}
200
201fn default_a2a_host() -> String {
202 "0.0.0.0".into()
203}
204
205fn default_a2a_port() -> u16 {
206 8080
207}
208
209fn default_a2a_rate_limit() -> u32 {
210 60
211}
212
213fn default_a2a_max_body() -> usize {
214 1_048_576
215}
216
217fn default_drain_timeout_ms() -> u64 {
218 30_000
219}
220
221fn default_max_dynamic_servers() -> usize {
222 10
223}
224
225fn default_mcp_timeout() -> u64 {
226 30
227}
228
229fn default_oauth_callback_port() -> u16 {
230 18766
231}
232
233fn default_oauth_client_name() -> String {
234 "Zeph".into()
235}
236
237#[derive(Clone, Deserialize, Serialize)]
249pub struct TelegramConfig {
250 pub token: Option<String>,
252 #[serde(default)]
254 pub allowed_users: Vec<String>,
255 #[serde(default)]
257 pub skills: ChannelSkillsConfig,
258}
259
260impl std::fmt::Debug for TelegramConfig {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 f.debug_struct("TelegramConfig")
263 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
264 .field("allowed_users", &self.allowed_users)
265 .field("skills", &self.skills)
266 .finish()
267 }
268}
269
270#[derive(Clone, Deserialize, Serialize)]
271pub struct DiscordConfig {
272 pub token: Option<String>,
273 pub application_id: Option<String>,
274 #[serde(default)]
275 pub allowed_user_ids: Vec<String>,
276 #[serde(default)]
277 pub allowed_role_ids: Vec<String>,
278 #[serde(default)]
279 pub allowed_channel_ids: Vec<String>,
280 #[serde(default)]
281 pub skills: ChannelSkillsConfig,
282}
283
284impl std::fmt::Debug for DiscordConfig {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.debug_struct("DiscordConfig")
287 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
288 .field("application_id", &self.application_id)
289 .field("allowed_user_ids", &self.allowed_user_ids)
290 .field("allowed_role_ids", &self.allowed_role_ids)
291 .field("allowed_channel_ids", &self.allowed_channel_ids)
292 .field("skills", &self.skills)
293 .finish()
294 }
295}
296
297#[derive(Clone, Deserialize, Serialize)]
298pub struct SlackConfig {
299 pub bot_token: Option<String>,
300 pub signing_secret: Option<String>,
301 #[serde(default = "default_slack_webhook_host")]
302 pub webhook_host: String,
303 #[serde(default = "default_slack_port")]
304 pub port: u16,
305 #[serde(default)]
306 pub allowed_user_ids: Vec<String>,
307 #[serde(default)]
308 pub allowed_channel_ids: Vec<String>,
309 #[serde(default)]
310 pub skills: ChannelSkillsConfig,
311}
312
313impl std::fmt::Debug for SlackConfig {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 f.debug_struct("SlackConfig")
316 .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
317 .field(
318 "signing_secret",
319 &self.signing_secret.as_ref().map(|_| "[REDACTED]"), )
321 .field("webhook_host", &self.webhook_host)
322 .field("port", &self.port)
323 .field("allowed_user_ids", &self.allowed_user_ids)
324 .field("allowed_channel_ids", &self.allowed_channel_ids)
325 .field("skills", &self.skills)
326 .finish()
327 }
328}
329
330#[derive(Debug, Clone, Deserialize, Serialize)]
334pub struct IbctKeyConfig {
335 pub key_id: String,
337 pub key_hex: String,
339}
340
341fn default_ibct_ttl() -> u64 {
342 300
343}
344
345#[derive(Deserialize, Serialize)]
351#[allow(clippy::struct_excessive_bools)] pub struct A2aServerConfig {
353 #[serde(default)]
354 pub enabled: bool,
355 #[serde(default = "default_a2a_host")]
356 pub host: String,
357 #[serde(default = "default_a2a_port")]
358 pub port: u16,
359 #[serde(default)]
360 pub public_url: String,
361 #[serde(default)]
362 pub auth_token: Option<String>,
363 #[serde(default = "default_a2a_rate_limit")]
364 pub rate_limit: u32,
365 #[serde(default = "default_true")]
366 pub require_tls: bool,
367 #[serde(default = "default_true")]
368 pub ssrf_protection: bool,
369 #[serde(default = "default_a2a_max_body")]
370 pub max_body_size: usize,
371 #[serde(default = "default_drain_timeout_ms")]
372 pub drain_timeout_ms: u64,
373 #[serde(default)]
377 pub require_auth: bool,
378 #[serde(default)]
383 pub ibct_keys: Vec<IbctKeyConfig>,
384 #[serde(default)]
390 pub ibct_signing_key_vault_ref: Option<String>,
391 #[serde(default = "default_ibct_ttl")]
393 pub ibct_ttl_secs: u64,
394 #[serde(default)]
408 pub advertise_files: bool,
409}
410
411impl std::fmt::Debug for A2aServerConfig {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 f.debug_struct("A2aServerConfig")
414 .field("enabled", &self.enabled)
415 .field("host", &self.host)
416 .field("port", &self.port)
417 .field("public_url", &self.public_url)
418 .field(
419 "auth_token",
420 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
421 )
422 .field("rate_limit", &self.rate_limit)
423 .field("require_tls", &self.require_tls)
424 .field("ssrf_protection", &self.ssrf_protection)
425 .field("max_body_size", &self.max_body_size)
426 .field("drain_timeout_ms", &self.drain_timeout_ms)
427 .field("require_auth", &self.require_auth)
428 .field("ibct_keys_count", &self.ibct_keys.len())
429 .field(
430 "ibct_signing_key_vault_ref",
431 &self.ibct_signing_key_vault_ref,
432 )
433 .field("ibct_ttl_secs", &self.ibct_ttl_secs)
434 .field("advertise_files", &self.advertise_files)
435 .finish()
436 }
437}
438
439impl Default for A2aServerConfig {
440 fn default() -> Self {
441 Self {
442 enabled: false,
443 host: default_a2a_host(),
444 port: default_a2a_port(),
445 public_url: String::new(),
446 auth_token: None,
447 rate_limit: default_a2a_rate_limit(),
448 require_tls: true,
449 ssrf_protection: true,
450 max_body_size: default_a2a_max_body(),
451 drain_timeout_ms: default_drain_timeout_ms(),
452 require_auth: false,
453 ibct_keys: Vec::new(),
454 ibct_signing_key_vault_ref: None,
455 ibct_ttl_secs: default_ibct_ttl(),
456 advertise_files: false,
457 }
458 }
459}
460
461#[derive(Debug, Clone, Deserialize, Serialize)]
467#[serde(default)]
468pub struct ToolPruningConfig {
469 pub enabled: bool,
471 pub max_tools: usize,
473 pub pruning_provider: ProviderName,
476 pub min_tools_to_prune: usize,
478 pub always_include: Vec<String>,
480}
481
482impl Default for ToolPruningConfig {
483 fn default() -> Self {
484 Self {
485 enabled: false,
486 max_tools: 15,
487 pruning_provider: ProviderName::default(),
488 min_tools_to_prune: 10,
489 always_include: Vec::new(),
490 }
491 }
492}
493
494#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
499#[serde(rename_all = "lowercase")]
500pub enum ToolDiscoveryStrategyConfig {
501 Embedding,
503 Llm,
505 #[default]
507 None,
508}
509
510#[derive(Debug, Clone, Deserialize, Serialize)]
516#[serde(default)]
517pub struct ToolDiscoveryConfig {
518 pub strategy: ToolDiscoveryStrategyConfig,
520 pub top_k: usize,
522 pub min_similarity: f32,
524 pub embedding_provider: ProviderName,
528 pub always_include: Vec<String>,
530 pub min_tools_to_filter: usize,
532 pub strict: bool,
535}
536
537impl Default for ToolDiscoveryConfig {
538 fn default() -> Self {
539 Self {
540 strategy: ToolDiscoveryStrategyConfig::None,
541 top_k: 10,
542 min_similarity: 0.2,
543 embedding_provider: ProviderName::default(),
544 always_include: Vec::new(),
545 min_tools_to_filter: 10,
546 strict: false,
547 }
548 }
549}
550
551#[derive(Debug, Clone, Deserialize, Serialize)]
553#[allow(clippy::struct_excessive_bools)] pub struct TrustCalibrationConfig {
555 #[serde(default)]
557 pub enabled: bool,
558 #[serde(default = "default_true")]
560 pub probe_on_connect: bool,
561 #[serde(default = "default_true")]
563 pub monitor_invocations: bool,
564 #[serde(default = "default_true")]
566 pub persist_scores: bool,
567 #[serde(default = "default_decay_rate")]
569 pub decay_rate_per_day: f64,
570 #[serde(default = "default_injection_penalty")]
572 pub injection_penalty: f64,
573 #[serde(default)]
575 pub verifier_provider: ProviderName,
576}
577
578fn default_decay_rate() -> f64 {
579 0.01
580}
581
582fn default_injection_penalty() -> f64 {
583 0.25
584}
585
586impl Default for TrustCalibrationConfig {
587 fn default() -> Self {
588 Self {
589 enabled: false,
590 probe_on_connect: true,
591 monitor_invocations: true,
592 persist_scores: true,
593 decay_rate_per_day: default_decay_rate(),
594 injection_penalty: default_injection_penalty(),
595 verifier_provider: ProviderName::default(),
596 }
597 }
598}
599
600fn default_max_description_bytes() -> usize {
601 2048
602}
603
604fn default_max_instructions_bytes() -> usize {
605 2048
606}
607
608fn default_elicitation_timeout() -> u64 {
609 120
610}
611
612fn default_elicitation_queue_capacity() -> usize {
613 16
614}
615
616fn default_output_schema_hint_bytes() -> usize {
617 1024
618}
619
620#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Deserialize, Serialize)]
622pub struct McpConfig {
623 #[serde(default)]
624 pub servers: Vec<McpServerConfig>,
625 #[serde(default)]
626 pub allowed_commands: Vec<String>,
627 #[serde(default = "default_max_dynamic_servers")]
628 pub max_dynamic_servers: usize,
629 #[serde(default)]
631 pub pruning: ToolPruningConfig,
632 #[serde(default)]
634 pub trust_calibration: TrustCalibrationConfig,
635 #[serde(default)]
637 pub tool_discovery: ToolDiscoveryConfig,
638 #[serde(default = "default_max_description_bytes")]
640 pub max_description_bytes: usize,
641 #[serde(default = "default_max_instructions_bytes")]
643 pub max_instructions_bytes: usize,
644 #[serde(default)]
648 pub elicitation_enabled: bool,
649 #[serde(default = "default_elicitation_timeout")]
651 pub elicitation_timeout: u64,
652 #[serde(default = "default_elicitation_queue_capacity")]
656 pub elicitation_queue_capacity: usize,
657 #[serde(default = "default_true")]
660 pub elicitation_warn_sensitive_fields: bool,
661 #[serde(default)]
667 pub lock_tool_list: bool,
668 #[serde(default)]
673 pub default_env_isolation: bool,
674 #[serde(default)]
682 pub forward_output_schema: bool,
683 #[serde(default = "default_output_schema_hint_bytes")]
689 pub output_schema_hint_bytes: usize,
690}
691
692impl Default for McpConfig {
693 fn default() -> Self {
694 Self {
695 servers: Vec::new(),
696 allowed_commands: Vec::new(),
697 max_dynamic_servers: default_max_dynamic_servers(),
698 pruning: ToolPruningConfig::default(),
699 trust_calibration: TrustCalibrationConfig::default(),
700 tool_discovery: ToolDiscoveryConfig::default(),
701 max_description_bytes: default_max_description_bytes(),
702 max_instructions_bytes: default_max_instructions_bytes(),
703 elicitation_enabled: false,
704 elicitation_timeout: default_elicitation_timeout(),
705 elicitation_queue_capacity: default_elicitation_queue_capacity(),
706 elicitation_warn_sensitive_fields: true,
707 lock_tool_list: false,
708 default_env_isolation: false,
709 forward_output_schema: false,
710 output_schema_hint_bytes: default_output_schema_hint_bytes(),
711 }
712 }
713}
714
715#[derive(Clone, Deserialize, Serialize)]
716pub struct McpServerConfig {
717 pub id: String,
718 pub command: Option<String>,
720 #[serde(default)]
721 pub args: Vec<String>,
722 #[serde(default)]
723 pub env: HashMap<String, String>,
724 pub url: Option<String>,
726 #[serde(default = "default_mcp_timeout")]
727 pub timeout: u64,
728 #[serde(default)]
730 pub policy: McpPolicy,
731 #[serde(default)]
734 pub headers: HashMap<String, String>,
735 #[serde(default)]
737 pub oauth: Option<McpOAuthConfig>,
738 #[serde(default)]
740 pub trust_level: McpTrustLevel,
741 #[serde(default)]
745 pub tool_allowlist: Option<Vec<String>>,
746 #[serde(default)]
752 pub expected_tools: Vec<String>,
753 #[serde(default)]
757 pub roots: Vec<McpRootEntry>,
758 #[serde(default)]
761 pub tool_metadata: HashMap<String, ToolSecurityMeta>,
762 #[serde(default)]
766 pub elicitation_enabled: Option<bool>,
767 #[serde(default)]
774 pub env_isolation: Option<bool>,
775}
776
777#[derive(Debug, Clone, Deserialize, Serialize)]
779pub struct McpRootEntry {
780 pub uri: String,
782 #[serde(default)]
784 pub name: Option<String>,
785}
786
787#[derive(Debug, Clone, Deserialize, Serialize)]
789pub struct McpOAuthConfig {
790 #[serde(default)]
792 pub enabled: bool,
793 #[serde(default)]
795 pub token_storage: OAuthTokenStorage,
796 #[serde(default)]
798 pub scopes: Vec<String>,
799 #[serde(default = "default_oauth_callback_port")]
801 pub callback_port: u16,
802 #[serde(default = "default_oauth_client_name")]
804 pub client_name: String,
805}
806
807impl Default for McpOAuthConfig {
808 fn default() -> Self {
809 Self {
810 enabled: false,
811 token_storage: OAuthTokenStorage::default(),
812 scopes: Vec::new(),
813 callback_port: default_oauth_callback_port(),
814 client_name: default_oauth_client_name(),
815 }
816 }
817}
818
819#[derive(Debug, Clone, Default, Deserialize, Serialize)]
821#[serde(rename_all = "lowercase")]
822pub enum OAuthTokenStorage {
823 #[default]
825 Vault,
826 Memory,
828}
829
830impl std::fmt::Debug for McpServerConfig {
831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
832 let redacted_env: HashMap<&str, &str> = self
833 .env
834 .keys()
835 .map(|k| (k.as_str(), "[REDACTED]"))
836 .collect();
837 let redacted_headers: HashMap<&str, &str> = self
839 .headers
840 .keys()
841 .map(|k| (k.as_str(), "[REDACTED]"))
842 .collect();
843 f.debug_struct("McpServerConfig")
844 .field("id", &self.id)
845 .field("command", &self.command)
846 .field("args", &self.args)
847 .field("env", &redacted_env)
848 .field("url", &self.url)
849 .field("timeout", &self.timeout)
850 .field("policy", &self.policy)
851 .field("headers", &redacted_headers)
852 .field("oauth", &self.oauth)
853 .field("trust_level", &self.trust_level)
854 .field("tool_allowlist", &self.tool_allowlist)
855 .field("expected_tools", &self.expected_tools)
856 .field("roots", &self.roots)
857 .field(
858 "tool_metadata_keys",
859 &self.tool_metadata.keys().collect::<Vec<_>>(),
860 )
861 .field("elicitation_enabled", &self.elicitation_enabled)
862 .field("env_isolation", &self.env_isolation)
863 .finish()
864 }
865}