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 test_default_output_schema_hint_bytes_is_1024() {
70 assert_eq!(default_output_schema_hint_bytes(), 1024);
71 }
72
73 #[test]
74 fn test_mcp_config_default_output_schema_hint_bytes_is_1024() {
75 let cfg = McpConfig::default();
76 assert_eq!(cfg.output_schema_hint_bytes, 1024);
77 }
78
79 #[test]
80 fn wildcard_star_allows_any_skill() {
81 let cfg = allow(&["*"]);
82 assert!(is_skill_allowed("anything", &cfg));
83 assert!(is_skill_allowed("web-search", &cfg));
84 }
85
86 #[test]
87 fn empty_allowlist_denies_all() {
88 let cfg = allow(&[]);
89 assert!(!is_skill_allowed("web-search", &cfg));
90 assert!(!is_skill_allowed("shell", &cfg));
91 }
92
93 #[test]
94 fn exact_match_allows_only_that_skill() {
95 let cfg = allow(&["web-search"]);
96 assert!(is_skill_allowed("web-search", &cfg));
97 assert!(!is_skill_allowed("shell", &cfg));
98 assert!(!is_skill_allowed("web-search-extra", &cfg));
99 }
100
101 #[test]
102 fn prefix_wildcard_allows_matching_skills() {
103 let cfg = allow(&["web-*"]);
104 assert!(is_skill_allowed("web-search", &cfg));
105 assert!(is_skill_allowed("web-fetch", &cfg));
106 assert!(!is_skill_allowed("shell", &cfg));
107 assert!(!is_skill_allowed("awesome-web-thing", &cfg));
108 }
109
110 #[test]
111 fn multiple_patterns_or_logic() {
112 let cfg = allow(&["shell", "web-*"]);
113 assert!(is_skill_allowed("shell", &cfg));
114 assert!(is_skill_allowed("web-search", &cfg));
115 assert!(!is_skill_allowed("memory", &cfg));
116 }
117
118 #[test]
119 fn default_config_allows_all() {
120 let cfg = ChannelSkillsConfig::default();
121 assert!(is_skill_allowed("any-skill", &cfg));
122 }
123
124 #[test]
125 fn prefix_wildcard_does_not_match_empty_suffix() {
126 let cfg = allow(&["web-*"]);
127 assert!(is_skill_allowed("web-", &cfg));
131 }
132
133 #[test]
134 fn matching_is_case_sensitive() {
135 let cfg = allow(&["Web-Search"]);
136 assert!(!is_skill_allowed("web-search", &cfg));
137 assert!(is_skill_allowed("Web-Search", &cfg));
138 }
139}
140
141fn default_slack_port() -> u16 {
142 3000
143}
144
145fn default_slack_webhook_host() -> String {
146 "127.0.0.1".into()
147}
148
149fn default_a2a_host() -> String {
150 "0.0.0.0".into()
151}
152
153fn default_a2a_port() -> u16 {
154 8080
155}
156
157fn default_a2a_rate_limit() -> u32 {
158 60
159}
160
161fn default_a2a_max_body() -> usize {
162 1_048_576
163}
164
165fn default_drain_timeout_ms() -> u64 {
166 30_000
167}
168
169fn default_max_dynamic_servers() -> usize {
170 10
171}
172
173fn default_mcp_timeout() -> u64 {
174 30
175}
176
177fn default_oauth_callback_port() -> u16 {
178 18766
179}
180
181fn default_oauth_client_name() -> String {
182 "Zeph".into()
183}
184
185#[derive(Clone, Deserialize, Serialize)]
197pub struct TelegramConfig {
198 pub token: Option<String>,
200 #[serde(default)]
202 pub allowed_users: Vec<String>,
203 #[serde(default)]
205 pub skills: ChannelSkillsConfig,
206}
207
208impl std::fmt::Debug for TelegramConfig {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 f.debug_struct("TelegramConfig")
211 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
212 .field("allowed_users", &self.allowed_users)
213 .field("skills", &self.skills)
214 .finish()
215 }
216}
217
218#[derive(Clone, Deserialize, Serialize)]
219pub struct DiscordConfig {
220 pub token: Option<String>,
221 pub application_id: Option<String>,
222 #[serde(default)]
223 pub allowed_user_ids: Vec<String>,
224 #[serde(default)]
225 pub allowed_role_ids: Vec<String>,
226 #[serde(default)]
227 pub allowed_channel_ids: Vec<String>,
228 #[serde(default)]
229 pub skills: ChannelSkillsConfig,
230}
231
232impl std::fmt::Debug for DiscordConfig {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.debug_struct("DiscordConfig")
235 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
236 .field("application_id", &self.application_id)
237 .field("allowed_user_ids", &self.allowed_user_ids)
238 .field("allowed_role_ids", &self.allowed_role_ids)
239 .field("allowed_channel_ids", &self.allowed_channel_ids)
240 .field("skills", &self.skills)
241 .finish()
242 }
243}
244
245#[derive(Clone, Deserialize, Serialize)]
246pub struct SlackConfig {
247 pub bot_token: Option<String>,
248 pub signing_secret: Option<String>,
249 #[serde(default = "default_slack_webhook_host")]
250 pub webhook_host: String,
251 #[serde(default = "default_slack_port")]
252 pub port: u16,
253 #[serde(default)]
254 pub allowed_user_ids: Vec<String>,
255 #[serde(default)]
256 pub allowed_channel_ids: Vec<String>,
257 #[serde(default)]
258 pub skills: ChannelSkillsConfig,
259}
260
261impl std::fmt::Debug for SlackConfig {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 f.debug_struct("SlackConfig")
264 .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
265 .field(
266 "signing_secret",
267 &self.signing_secret.as_ref().map(|_| "[REDACTED]"), )
269 .field("webhook_host", &self.webhook_host)
270 .field("port", &self.port)
271 .field("allowed_user_ids", &self.allowed_user_ids)
272 .field("allowed_channel_ids", &self.allowed_channel_ids)
273 .field("skills", &self.skills)
274 .finish()
275 }
276}
277
278#[derive(Debug, Clone, Deserialize, Serialize)]
282pub struct IbctKeyConfig {
283 pub key_id: String,
285 pub key_hex: String,
287}
288
289fn default_ibct_ttl() -> u64 {
290 300
291}
292
293#[derive(Deserialize, Serialize)]
294#[allow(clippy::struct_excessive_bools)] pub struct A2aServerConfig {
296 #[serde(default)]
297 pub enabled: bool,
298 #[serde(default = "default_a2a_host")]
299 pub host: String,
300 #[serde(default = "default_a2a_port")]
301 pub port: u16,
302 #[serde(default)]
303 pub public_url: String,
304 #[serde(default)]
305 pub auth_token: Option<String>,
306 #[serde(default = "default_a2a_rate_limit")]
307 pub rate_limit: u32,
308 #[serde(default = "default_true")]
309 pub require_tls: bool,
310 #[serde(default = "default_true")]
311 pub ssrf_protection: bool,
312 #[serde(default = "default_a2a_max_body")]
313 pub max_body_size: usize,
314 #[serde(default = "default_drain_timeout_ms")]
315 pub drain_timeout_ms: u64,
316 #[serde(default)]
320 pub require_auth: bool,
321 #[serde(default)]
326 pub ibct_keys: Vec<IbctKeyConfig>,
327 #[serde(default)]
333 pub ibct_signing_key_vault_ref: Option<String>,
334 #[serde(default = "default_ibct_ttl")]
336 pub ibct_ttl_secs: u64,
337}
338
339impl std::fmt::Debug for A2aServerConfig {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 f.debug_struct("A2aServerConfig")
342 .field("enabled", &self.enabled)
343 .field("host", &self.host)
344 .field("port", &self.port)
345 .field("public_url", &self.public_url)
346 .field(
347 "auth_token",
348 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
349 )
350 .field("rate_limit", &self.rate_limit)
351 .field("require_tls", &self.require_tls)
352 .field("ssrf_protection", &self.ssrf_protection)
353 .field("max_body_size", &self.max_body_size)
354 .field("drain_timeout_ms", &self.drain_timeout_ms)
355 .field("require_auth", &self.require_auth)
356 .field("ibct_keys_count", &self.ibct_keys.len())
357 .field(
358 "ibct_signing_key_vault_ref",
359 &self.ibct_signing_key_vault_ref,
360 )
361 .field("ibct_ttl_secs", &self.ibct_ttl_secs)
362 .finish()
363 }
364}
365
366impl Default for A2aServerConfig {
367 fn default() -> Self {
368 Self {
369 enabled: false,
370 host: default_a2a_host(),
371 port: default_a2a_port(),
372 public_url: String::new(),
373 auth_token: None,
374 rate_limit: default_a2a_rate_limit(),
375 require_tls: true,
376 ssrf_protection: true,
377 max_body_size: default_a2a_max_body(),
378 drain_timeout_ms: default_drain_timeout_ms(),
379 require_auth: false,
380 ibct_keys: Vec::new(),
381 ibct_signing_key_vault_ref: None,
382 ibct_ttl_secs: default_ibct_ttl(),
383 }
384 }
385}
386
387#[derive(Debug, Clone, Deserialize, Serialize)]
393#[serde(default)]
394pub struct ToolPruningConfig {
395 pub enabled: bool,
397 pub max_tools: usize,
399 pub pruning_provider: ProviderName,
402 pub min_tools_to_prune: usize,
404 pub always_include: Vec<String>,
406}
407
408impl Default for ToolPruningConfig {
409 fn default() -> Self {
410 Self {
411 enabled: false,
412 max_tools: 15,
413 pruning_provider: ProviderName::default(),
414 min_tools_to_prune: 10,
415 always_include: Vec::new(),
416 }
417 }
418}
419
420#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
425#[serde(rename_all = "lowercase")]
426pub enum ToolDiscoveryStrategyConfig {
427 Embedding,
429 Llm,
431 #[default]
433 None,
434}
435
436#[derive(Debug, Clone, Deserialize, Serialize)]
442#[serde(default)]
443pub struct ToolDiscoveryConfig {
444 pub strategy: ToolDiscoveryStrategyConfig,
446 pub top_k: usize,
448 pub min_similarity: f32,
450 pub embedding_provider: ProviderName,
454 pub always_include: Vec<String>,
456 pub min_tools_to_filter: usize,
458 pub strict: bool,
461}
462
463impl Default for ToolDiscoveryConfig {
464 fn default() -> Self {
465 Self {
466 strategy: ToolDiscoveryStrategyConfig::None,
467 top_k: 10,
468 min_similarity: 0.2,
469 embedding_provider: ProviderName::default(),
470 always_include: Vec::new(),
471 min_tools_to_filter: 10,
472 strict: false,
473 }
474 }
475}
476
477#[derive(Debug, Clone, Deserialize, Serialize)]
479#[allow(clippy::struct_excessive_bools)]
480pub struct TrustCalibrationConfig {
481 #[serde(default)]
483 pub enabled: bool,
484 #[serde(default = "default_true")]
486 pub probe_on_connect: bool,
487 #[serde(default = "default_true")]
489 pub monitor_invocations: bool,
490 #[serde(default = "default_true")]
492 pub persist_scores: bool,
493 #[serde(default = "default_decay_rate")]
495 pub decay_rate_per_day: f64,
496 #[serde(default = "default_injection_penalty")]
498 pub injection_penalty: f64,
499 #[serde(default)]
501 pub verifier_provider: ProviderName,
502}
503
504fn default_decay_rate() -> f64 {
505 0.01
506}
507
508fn default_injection_penalty() -> f64 {
509 0.25
510}
511
512impl Default for TrustCalibrationConfig {
513 fn default() -> Self {
514 Self {
515 enabled: false,
516 probe_on_connect: true,
517 monitor_invocations: true,
518 persist_scores: true,
519 decay_rate_per_day: default_decay_rate(),
520 injection_penalty: default_injection_penalty(),
521 verifier_provider: ProviderName::default(),
522 }
523 }
524}
525
526fn default_max_description_bytes() -> usize {
527 2048
528}
529
530fn default_max_instructions_bytes() -> usize {
531 2048
532}
533
534fn default_elicitation_timeout() -> u64 {
535 120
536}
537
538fn default_elicitation_queue_capacity() -> usize {
539 16
540}
541
542fn default_output_schema_hint_bytes() -> usize {
543 1024
544}
545
546#[allow(clippy::struct_excessive_bools)]
547#[derive(Debug, Clone, Deserialize, Serialize)]
548pub struct McpConfig {
549 #[serde(default)]
550 pub servers: Vec<McpServerConfig>,
551 #[serde(default)]
552 pub allowed_commands: Vec<String>,
553 #[serde(default = "default_max_dynamic_servers")]
554 pub max_dynamic_servers: usize,
555 #[serde(default)]
557 pub pruning: ToolPruningConfig,
558 #[serde(default)]
560 pub trust_calibration: TrustCalibrationConfig,
561 #[serde(default)]
563 pub tool_discovery: ToolDiscoveryConfig,
564 #[serde(default = "default_max_description_bytes")]
566 pub max_description_bytes: usize,
567 #[serde(default = "default_max_instructions_bytes")]
569 pub max_instructions_bytes: usize,
570 #[serde(default)]
574 pub elicitation_enabled: bool,
575 #[serde(default = "default_elicitation_timeout")]
577 pub elicitation_timeout: u64,
578 #[serde(default = "default_elicitation_queue_capacity")]
582 pub elicitation_queue_capacity: usize,
583 #[serde(default = "default_true")]
586 pub elicitation_warn_sensitive_fields: bool,
587 #[serde(default)]
593 pub lock_tool_list: bool,
594 #[serde(default)]
599 pub default_env_isolation: bool,
600 #[serde(default)]
608 pub forward_output_schema: bool,
609 #[serde(default = "default_output_schema_hint_bytes")]
615 pub output_schema_hint_bytes: usize,
616}
617
618impl Default for McpConfig {
619 fn default() -> Self {
620 Self {
621 servers: Vec::new(),
622 allowed_commands: Vec::new(),
623 max_dynamic_servers: default_max_dynamic_servers(),
624 pruning: ToolPruningConfig::default(),
625 trust_calibration: TrustCalibrationConfig::default(),
626 tool_discovery: ToolDiscoveryConfig::default(),
627 max_description_bytes: default_max_description_bytes(),
628 max_instructions_bytes: default_max_instructions_bytes(),
629 elicitation_enabled: false,
630 elicitation_timeout: default_elicitation_timeout(),
631 elicitation_queue_capacity: default_elicitation_queue_capacity(),
632 elicitation_warn_sensitive_fields: true,
633 lock_tool_list: false,
634 default_env_isolation: false,
635 forward_output_schema: false,
636 output_schema_hint_bytes: default_output_schema_hint_bytes(),
637 }
638 }
639}
640
641#[derive(Clone, Deserialize, Serialize)]
642pub struct McpServerConfig {
643 pub id: String,
644 pub command: Option<String>,
646 #[serde(default)]
647 pub args: Vec<String>,
648 #[serde(default)]
649 pub env: HashMap<String, String>,
650 pub url: Option<String>,
652 #[serde(default = "default_mcp_timeout")]
653 pub timeout: u64,
654 #[serde(default)]
656 pub policy: zeph_mcp::McpPolicy,
657 #[serde(default)]
660 pub headers: HashMap<String, String>,
661 #[serde(default)]
663 pub oauth: Option<McpOAuthConfig>,
664 #[serde(default)]
666 pub trust_level: McpTrustLevel,
667 #[serde(default)]
671 pub tool_allowlist: Option<Vec<String>>,
672 #[serde(default)]
678 pub expected_tools: Vec<String>,
679 #[serde(default)]
683 pub roots: Vec<McpRootEntry>,
684 #[serde(default)]
687 pub tool_metadata: HashMap<String, ToolSecurityMeta>,
688 #[serde(default)]
692 pub elicitation_enabled: Option<bool>,
693 #[serde(default)]
700 pub env_isolation: Option<bool>,
701}
702
703#[derive(Debug, Clone, Deserialize, Serialize)]
705pub struct McpRootEntry {
706 pub uri: String,
708 #[serde(default)]
710 pub name: Option<String>,
711}
712
713#[derive(Debug, Clone, Deserialize, Serialize)]
715pub struct McpOAuthConfig {
716 #[serde(default)]
718 pub enabled: bool,
719 #[serde(default)]
721 pub token_storage: OAuthTokenStorage,
722 #[serde(default)]
724 pub scopes: Vec<String>,
725 #[serde(default = "default_oauth_callback_port")]
727 pub callback_port: u16,
728 #[serde(default = "default_oauth_client_name")]
730 pub client_name: String,
731}
732
733impl Default for McpOAuthConfig {
734 fn default() -> Self {
735 Self {
736 enabled: false,
737 token_storage: OAuthTokenStorage::default(),
738 scopes: Vec::new(),
739 callback_port: default_oauth_callback_port(),
740 client_name: default_oauth_client_name(),
741 }
742 }
743}
744
745#[derive(Debug, Clone, Default, Deserialize, Serialize)]
747#[serde(rename_all = "lowercase")]
748pub enum OAuthTokenStorage {
749 #[default]
751 Vault,
752 Memory,
754}
755
756impl std::fmt::Debug for McpServerConfig {
757 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758 let redacted_env: HashMap<&str, &str> = self
759 .env
760 .keys()
761 .map(|k| (k.as_str(), "[REDACTED]"))
762 .collect();
763 let redacted_headers: HashMap<&str, &str> = self
765 .headers
766 .keys()
767 .map(|k| (k.as_str(), "[REDACTED]"))
768 .collect();
769 f.debug_struct("McpServerConfig")
770 .field("id", &self.id)
771 .field("command", &self.command)
772 .field("args", &self.args)
773 .field("env", &redacted_env)
774 .field("url", &self.url)
775 .field("timeout", &self.timeout)
776 .field("policy", &self.policy)
777 .field("headers", &redacted_headers)
778 .field("oauth", &self.oauth)
779 .field("trust_level", &self.trust_level)
780 .field("tool_allowlist", &self.tool_allowlist)
781 .field("expected_tools", &self.expected_tools)
782 .field("roots", &self.roots)
783 .field(
784 "tool_metadata_keys",
785 &self.tool_metadata.keys().collect::<Vec<_>>(),
786 )
787 .field("elicitation_enabled", &self.elicitation_enabled)
788 .field("env_isolation", &self.env_isolation)
789 .finish()
790 }
791}