1use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::defaults::default_true;
9use crate::providers::ProviderName;
10
11pub use zeph_mcp::{McpTrustLevel, tool::ToolSecurityMeta};
12
13fn default_skill_allowlist() -> Vec<String> {
14 vec!["*".into()]
15}
16
17#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct ChannelSkillsConfig {
24 #[serde(default = "default_skill_allowlist")]
27 pub allowed: Vec<String>,
28}
29
30impl Default for ChannelSkillsConfig {
31 fn default() -> Self {
32 Self {
33 allowed: default_skill_allowlist(),
34 }
35 }
36}
37
38#[must_use]
43pub fn is_skill_allowed(name: &str, config: &ChannelSkillsConfig) -> bool {
44 config.allowed.iter().any(|p| glob_match(p, name))
45}
46
47fn glob_match(pattern: &str, name: &str) -> bool {
48 if let Some(prefix) = pattern.strip_suffix('*') {
49 if prefix.is_empty() {
50 return true;
51 }
52 name.starts_with(prefix)
53 } else {
54 pattern == name
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 fn allow(patterns: &[&str]) -> ChannelSkillsConfig {
63 ChannelSkillsConfig {
64 allowed: patterns.iter().map(ToString::to_string).collect(),
65 }
66 }
67
68 #[test]
69 fn wildcard_star_allows_any_skill() {
70 let cfg = allow(&["*"]);
71 assert!(is_skill_allowed("anything", &cfg));
72 assert!(is_skill_allowed("web-search", &cfg));
73 }
74
75 #[test]
76 fn empty_allowlist_denies_all() {
77 let cfg = allow(&[]);
78 assert!(!is_skill_allowed("web-search", &cfg));
79 assert!(!is_skill_allowed("shell", &cfg));
80 }
81
82 #[test]
83 fn exact_match_allows_only_that_skill() {
84 let cfg = allow(&["web-search"]);
85 assert!(is_skill_allowed("web-search", &cfg));
86 assert!(!is_skill_allowed("shell", &cfg));
87 assert!(!is_skill_allowed("web-search-extra", &cfg));
88 }
89
90 #[test]
91 fn prefix_wildcard_allows_matching_skills() {
92 let cfg = allow(&["web-*"]);
93 assert!(is_skill_allowed("web-search", &cfg));
94 assert!(is_skill_allowed("web-fetch", &cfg));
95 assert!(!is_skill_allowed("shell", &cfg));
96 assert!(!is_skill_allowed("awesome-web-thing", &cfg));
97 }
98
99 #[test]
100 fn multiple_patterns_or_logic() {
101 let cfg = allow(&["shell", "web-*"]);
102 assert!(is_skill_allowed("shell", &cfg));
103 assert!(is_skill_allowed("web-search", &cfg));
104 assert!(!is_skill_allowed("memory", &cfg));
105 }
106
107 #[test]
108 fn default_config_allows_all() {
109 let cfg = ChannelSkillsConfig::default();
110 assert!(is_skill_allowed("any-skill", &cfg));
111 }
112
113 #[test]
114 fn prefix_wildcard_does_not_match_empty_suffix() {
115 let cfg = allow(&["web-*"]);
116 assert!(is_skill_allowed("web-", &cfg));
120 }
121
122 #[test]
123 fn matching_is_case_sensitive() {
124 let cfg = allow(&["Web-Search"]);
125 assert!(!is_skill_allowed("web-search", &cfg));
126 assert!(is_skill_allowed("Web-Search", &cfg));
127 }
128}
129
130fn default_slack_port() -> u16 {
131 3000
132}
133
134fn default_slack_webhook_host() -> String {
135 "127.0.0.1".into()
136}
137
138fn default_a2a_host() -> String {
139 "0.0.0.0".into()
140}
141
142fn default_a2a_port() -> u16 {
143 8080
144}
145
146fn default_a2a_rate_limit() -> u32 {
147 60
148}
149
150fn default_a2a_max_body() -> usize {
151 1_048_576
152}
153
154fn default_drain_timeout_ms() -> u64 {
155 30_000
156}
157
158fn default_max_dynamic_servers() -> usize {
159 10
160}
161
162fn default_mcp_timeout() -> u64 {
163 30
164}
165
166fn default_oauth_callback_port() -> u16 {
167 18766
168}
169
170fn default_oauth_client_name() -> String {
171 "Zeph".into()
172}
173
174#[derive(Clone, Deserialize, Serialize)]
186pub struct TelegramConfig {
187 pub token: Option<String>,
189 #[serde(default)]
191 pub allowed_users: Vec<String>,
192 #[serde(default)]
194 pub skills: ChannelSkillsConfig,
195}
196
197impl std::fmt::Debug for TelegramConfig {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("TelegramConfig")
200 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
201 .field("allowed_users", &self.allowed_users)
202 .field("skills", &self.skills)
203 .finish()
204 }
205}
206
207#[derive(Clone, Deserialize, Serialize)]
208pub struct DiscordConfig {
209 pub token: Option<String>,
210 pub application_id: Option<String>,
211 #[serde(default)]
212 pub allowed_user_ids: Vec<String>,
213 #[serde(default)]
214 pub allowed_role_ids: Vec<String>,
215 #[serde(default)]
216 pub allowed_channel_ids: Vec<String>,
217 #[serde(default)]
218 pub skills: ChannelSkillsConfig,
219}
220
221impl std::fmt::Debug for DiscordConfig {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct("DiscordConfig")
224 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
225 .field("application_id", &self.application_id)
226 .field("allowed_user_ids", &self.allowed_user_ids)
227 .field("allowed_role_ids", &self.allowed_role_ids)
228 .field("allowed_channel_ids", &self.allowed_channel_ids)
229 .field("skills", &self.skills)
230 .finish()
231 }
232}
233
234#[derive(Clone, Deserialize, Serialize)]
235pub struct SlackConfig {
236 pub bot_token: Option<String>,
237 pub signing_secret: Option<String>,
238 #[serde(default = "default_slack_webhook_host")]
239 pub webhook_host: String,
240 #[serde(default = "default_slack_port")]
241 pub port: u16,
242 #[serde(default)]
243 pub allowed_user_ids: Vec<String>,
244 #[serde(default)]
245 pub allowed_channel_ids: Vec<String>,
246 #[serde(default)]
247 pub skills: ChannelSkillsConfig,
248}
249
250impl std::fmt::Debug for SlackConfig {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("SlackConfig")
253 .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
254 .field(
255 "signing_secret",
256 &self.signing_secret.as_ref().map(|_| "[REDACTED]"), )
258 .field("webhook_host", &self.webhook_host)
259 .field("port", &self.port)
260 .field("allowed_user_ids", &self.allowed_user_ids)
261 .field("allowed_channel_ids", &self.allowed_channel_ids)
262 .field("skills", &self.skills)
263 .finish()
264 }
265}
266
267#[derive(Debug, Clone, Deserialize, Serialize)]
271pub struct IbctKeyConfig {
272 pub key_id: String,
274 pub key_hex: String,
276}
277
278fn default_ibct_ttl() -> u64 {
279 300
280}
281
282#[derive(Deserialize, Serialize)]
283#[allow(clippy::struct_excessive_bools)] pub struct A2aServerConfig {
285 #[serde(default)]
286 pub enabled: bool,
287 #[serde(default = "default_a2a_host")]
288 pub host: String,
289 #[serde(default = "default_a2a_port")]
290 pub port: u16,
291 #[serde(default)]
292 pub public_url: String,
293 #[serde(default)]
294 pub auth_token: Option<String>,
295 #[serde(default = "default_a2a_rate_limit")]
296 pub rate_limit: u32,
297 #[serde(default = "default_true")]
298 pub require_tls: bool,
299 #[serde(default = "default_true")]
300 pub ssrf_protection: bool,
301 #[serde(default = "default_a2a_max_body")]
302 pub max_body_size: usize,
303 #[serde(default = "default_drain_timeout_ms")]
304 pub drain_timeout_ms: u64,
305 #[serde(default)]
309 pub require_auth: bool,
310 #[serde(default)]
315 pub ibct_keys: Vec<IbctKeyConfig>,
316 #[serde(default)]
322 pub ibct_signing_key_vault_ref: Option<String>,
323 #[serde(default = "default_ibct_ttl")]
325 pub ibct_ttl_secs: u64,
326}
327
328impl std::fmt::Debug for A2aServerConfig {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 f.debug_struct("A2aServerConfig")
331 .field("enabled", &self.enabled)
332 .field("host", &self.host)
333 .field("port", &self.port)
334 .field("public_url", &self.public_url)
335 .field(
336 "auth_token",
337 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
338 )
339 .field("rate_limit", &self.rate_limit)
340 .field("require_tls", &self.require_tls)
341 .field("ssrf_protection", &self.ssrf_protection)
342 .field("max_body_size", &self.max_body_size)
343 .field("drain_timeout_ms", &self.drain_timeout_ms)
344 .field("require_auth", &self.require_auth)
345 .field("ibct_keys_count", &self.ibct_keys.len())
346 .field(
347 "ibct_signing_key_vault_ref",
348 &self.ibct_signing_key_vault_ref,
349 )
350 .field("ibct_ttl_secs", &self.ibct_ttl_secs)
351 .finish()
352 }
353}
354
355impl Default for A2aServerConfig {
356 fn default() -> Self {
357 Self {
358 enabled: false,
359 host: default_a2a_host(),
360 port: default_a2a_port(),
361 public_url: String::new(),
362 auth_token: None,
363 rate_limit: default_a2a_rate_limit(),
364 require_tls: true,
365 ssrf_protection: true,
366 max_body_size: default_a2a_max_body(),
367 drain_timeout_ms: default_drain_timeout_ms(),
368 require_auth: false,
369 ibct_keys: Vec::new(),
370 ibct_signing_key_vault_ref: None,
371 ibct_ttl_secs: default_ibct_ttl(),
372 }
373 }
374}
375
376#[derive(Debug, Clone, Deserialize, Serialize)]
382#[serde(default)]
383pub struct ToolPruningConfig {
384 pub enabled: bool,
386 pub max_tools: usize,
388 pub pruning_provider: ProviderName,
391 pub min_tools_to_prune: usize,
393 pub always_include: Vec<String>,
395}
396
397impl Default for ToolPruningConfig {
398 fn default() -> Self {
399 Self {
400 enabled: false,
401 max_tools: 15,
402 pruning_provider: ProviderName::default(),
403 min_tools_to_prune: 10,
404 always_include: Vec::new(),
405 }
406 }
407}
408
409#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
414#[serde(rename_all = "lowercase")]
415pub enum ToolDiscoveryStrategyConfig {
416 Embedding,
418 Llm,
420 #[default]
422 None,
423}
424
425#[derive(Debug, Clone, Deserialize, Serialize)]
431#[serde(default)]
432pub struct ToolDiscoveryConfig {
433 pub strategy: ToolDiscoveryStrategyConfig,
435 pub top_k: usize,
437 pub min_similarity: f32,
439 pub embedding_provider: ProviderName,
443 pub always_include: Vec<String>,
445 pub min_tools_to_filter: usize,
447 pub strict: bool,
450}
451
452impl Default for ToolDiscoveryConfig {
453 fn default() -> Self {
454 Self {
455 strategy: ToolDiscoveryStrategyConfig::None,
456 top_k: 10,
457 min_similarity: 0.2,
458 embedding_provider: ProviderName::default(),
459 always_include: Vec::new(),
460 min_tools_to_filter: 10,
461 strict: false,
462 }
463 }
464}
465
466#[derive(Debug, Clone, Deserialize, Serialize)]
468#[allow(clippy::struct_excessive_bools)]
469pub struct TrustCalibrationConfig {
470 #[serde(default)]
472 pub enabled: bool,
473 #[serde(default = "default_true")]
475 pub probe_on_connect: bool,
476 #[serde(default = "default_true")]
478 pub monitor_invocations: bool,
479 #[serde(default = "default_true")]
481 pub persist_scores: bool,
482 #[serde(default = "default_decay_rate")]
484 pub decay_rate_per_day: f64,
485 #[serde(default = "default_injection_penalty")]
487 pub injection_penalty: f64,
488 #[serde(default)]
490 pub verifier_provider: ProviderName,
491}
492
493fn default_decay_rate() -> f64 {
494 0.01
495}
496
497fn default_injection_penalty() -> f64 {
498 0.25
499}
500
501impl Default for TrustCalibrationConfig {
502 fn default() -> Self {
503 Self {
504 enabled: false,
505 probe_on_connect: true,
506 monitor_invocations: true,
507 persist_scores: true,
508 decay_rate_per_day: default_decay_rate(),
509 injection_penalty: default_injection_penalty(),
510 verifier_provider: ProviderName::default(),
511 }
512 }
513}
514
515fn default_max_description_bytes() -> usize {
516 2048
517}
518
519fn default_max_instructions_bytes() -> usize {
520 2048
521}
522
523fn default_elicitation_timeout() -> u64 {
524 120
525}
526
527fn default_elicitation_queue_capacity() -> usize {
528 16
529}
530
531#[allow(clippy::struct_excessive_bools)]
532#[derive(Debug, Clone, Deserialize, Serialize)]
533pub struct McpConfig {
534 #[serde(default)]
535 pub servers: Vec<McpServerConfig>,
536 #[serde(default)]
537 pub allowed_commands: Vec<String>,
538 #[serde(default = "default_max_dynamic_servers")]
539 pub max_dynamic_servers: usize,
540 #[serde(default)]
542 pub pruning: ToolPruningConfig,
543 #[serde(default)]
545 pub trust_calibration: TrustCalibrationConfig,
546 #[serde(default)]
548 pub tool_discovery: ToolDiscoveryConfig,
549 #[serde(default = "default_max_description_bytes")]
551 pub max_description_bytes: usize,
552 #[serde(default = "default_max_instructions_bytes")]
554 pub max_instructions_bytes: usize,
555 #[serde(default)]
559 pub elicitation_enabled: bool,
560 #[serde(default = "default_elicitation_timeout")]
562 pub elicitation_timeout: u64,
563 #[serde(default = "default_elicitation_queue_capacity")]
567 pub elicitation_queue_capacity: usize,
568 #[serde(default = "default_true")]
571 pub elicitation_warn_sensitive_fields: bool,
572 #[serde(default)]
578 pub lock_tool_list: bool,
579 #[serde(default)]
584 pub default_env_isolation: bool,
585}
586
587impl Default for McpConfig {
588 fn default() -> Self {
589 Self {
590 servers: Vec::new(),
591 allowed_commands: Vec::new(),
592 max_dynamic_servers: default_max_dynamic_servers(),
593 pruning: ToolPruningConfig::default(),
594 trust_calibration: TrustCalibrationConfig::default(),
595 tool_discovery: ToolDiscoveryConfig::default(),
596 max_description_bytes: default_max_description_bytes(),
597 max_instructions_bytes: default_max_instructions_bytes(),
598 elicitation_enabled: false,
599 elicitation_timeout: default_elicitation_timeout(),
600 elicitation_queue_capacity: default_elicitation_queue_capacity(),
601 elicitation_warn_sensitive_fields: true,
602 lock_tool_list: false,
603 default_env_isolation: false,
604 }
605 }
606}
607
608#[derive(Clone, Deserialize, Serialize)]
609pub struct McpServerConfig {
610 pub id: String,
611 pub command: Option<String>,
613 #[serde(default)]
614 pub args: Vec<String>,
615 #[serde(default)]
616 pub env: HashMap<String, String>,
617 pub url: Option<String>,
619 #[serde(default = "default_mcp_timeout")]
620 pub timeout: u64,
621 #[serde(default)]
623 pub policy: zeph_mcp::McpPolicy,
624 #[serde(default)]
627 pub headers: HashMap<String, String>,
628 #[serde(default)]
630 pub oauth: Option<McpOAuthConfig>,
631 #[serde(default)]
633 pub trust_level: McpTrustLevel,
634 #[serde(default)]
638 pub tool_allowlist: Option<Vec<String>>,
639 #[serde(default)]
645 pub expected_tools: Vec<String>,
646 #[serde(default)]
650 pub roots: Vec<McpRootEntry>,
651 #[serde(default)]
654 pub tool_metadata: HashMap<String, ToolSecurityMeta>,
655 #[serde(default)]
659 pub elicitation_enabled: Option<bool>,
660 #[serde(default)]
667 pub env_isolation: Option<bool>,
668}
669
670#[derive(Debug, Clone, Deserialize, Serialize)]
672pub struct McpRootEntry {
673 pub uri: String,
675 #[serde(default)]
677 pub name: Option<String>,
678}
679
680#[derive(Debug, Clone, Deserialize, Serialize)]
682pub struct McpOAuthConfig {
683 #[serde(default)]
685 pub enabled: bool,
686 #[serde(default)]
688 pub token_storage: OAuthTokenStorage,
689 #[serde(default)]
691 pub scopes: Vec<String>,
692 #[serde(default = "default_oauth_callback_port")]
694 pub callback_port: u16,
695 #[serde(default = "default_oauth_client_name")]
697 pub client_name: String,
698}
699
700impl Default for McpOAuthConfig {
701 fn default() -> Self {
702 Self {
703 enabled: false,
704 token_storage: OAuthTokenStorage::default(),
705 scopes: Vec::new(),
706 callback_port: default_oauth_callback_port(),
707 client_name: default_oauth_client_name(),
708 }
709 }
710}
711
712#[derive(Debug, Clone, Default, Deserialize, Serialize)]
714#[serde(rename_all = "lowercase")]
715pub enum OAuthTokenStorage {
716 #[default]
718 Vault,
719 Memory,
721}
722
723impl std::fmt::Debug for McpServerConfig {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 let redacted_env: HashMap<&str, &str> = self
726 .env
727 .keys()
728 .map(|k| (k.as_str(), "[REDACTED]"))
729 .collect();
730 let redacted_headers: HashMap<&str, &str> = self
732 .headers
733 .keys()
734 .map(|k| (k.as_str(), "[REDACTED]"))
735 .collect();
736 f.debug_struct("McpServerConfig")
737 .field("id", &self.id)
738 .field("command", &self.command)
739 .field("args", &self.args)
740 .field("env", &redacted_env)
741 .field("url", &self.url)
742 .field("timeout", &self.timeout)
743 .field("policy", &self.policy)
744 .field("headers", &redacted_headers)
745 .field("oauth", &self.oauth)
746 .field("trust_level", &self.trust_level)
747 .field("tool_allowlist", &self.tool_allowlist)
748 .field("expected_tools", &self.expected_tools)
749 .field("roots", &self.roots)
750 .field(
751 "tool_metadata_keys",
752 &self.tool_metadata.keys().collect::<Vec<_>>(),
753 )
754 .field("elicitation_enabled", &self.elicitation_enabled)
755 .field("env_isolation", &self.env_isolation)
756 .finish()
757 }
758}