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)]
175pub struct TelegramConfig {
176 pub token: Option<String>,
177 #[serde(default)]
178 pub allowed_users: Vec<String>,
179 #[serde(default)]
180 pub skills: ChannelSkillsConfig,
181}
182
183impl std::fmt::Debug for TelegramConfig {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.debug_struct("TelegramConfig")
186 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
187 .field("allowed_users", &self.allowed_users)
188 .field("skills", &self.skills)
189 .finish()
190 }
191}
192
193#[derive(Clone, Deserialize, Serialize)]
194pub struct DiscordConfig {
195 pub token: Option<String>,
196 pub application_id: Option<String>,
197 #[serde(default)]
198 pub allowed_user_ids: Vec<String>,
199 #[serde(default)]
200 pub allowed_role_ids: Vec<String>,
201 #[serde(default)]
202 pub allowed_channel_ids: Vec<String>,
203 #[serde(default)]
204 pub skills: ChannelSkillsConfig,
205}
206
207impl std::fmt::Debug for DiscordConfig {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("DiscordConfig")
210 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
211 .field("application_id", &self.application_id)
212 .field("allowed_user_ids", &self.allowed_user_ids)
213 .field("allowed_role_ids", &self.allowed_role_ids)
214 .field("allowed_channel_ids", &self.allowed_channel_ids)
215 .field("skills", &self.skills)
216 .finish()
217 }
218}
219
220#[derive(Clone, Deserialize, Serialize)]
221pub struct SlackConfig {
222 pub bot_token: Option<String>,
223 pub signing_secret: Option<String>,
224 #[serde(default = "default_slack_webhook_host")]
225 pub webhook_host: String,
226 #[serde(default = "default_slack_port")]
227 pub port: u16,
228 #[serde(default)]
229 pub allowed_user_ids: Vec<String>,
230 #[serde(default)]
231 pub allowed_channel_ids: Vec<String>,
232 #[serde(default)]
233 pub skills: ChannelSkillsConfig,
234}
235
236impl std::fmt::Debug for SlackConfig {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.debug_struct("SlackConfig")
239 .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
240 .field(
241 "signing_secret",
242 &self.signing_secret.as_ref().map(|_| "[REDACTED]"), )
244 .field("webhook_host", &self.webhook_host)
245 .field("port", &self.port)
246 .field("allowed_user_ids", &self.allowed_user_ids)
247 .field("allowed_channel_ids", &self.allowed_channel_ids)
248 .field("skills", &self.skills)
249 .finish()
250 }
251}
252
253#[derive(Debug, Clone, Deserialize, Serialize)]
257pub struct IbctKeyConfig {
258 pub key_id: String,
260 pub key_hex: String,
262}
263
264fn default_ibct_ttl() -> u64 {
265 300
266}
267
268#[derive(Deserialize, Serialize)]
269#[allow(clippy::struct_excessive_bools)] pub struct A2aServerConfig {
271 #[serde(default)]
272 pub enabled: bool,
273 #[serde(default = "default_a2a_host")]
274 pub host: String,
275 #[serde(default = "default_a2a_port")]
276 pub port: u16,
277 #[serde(default)]
278 pub public_url: String,
279 #[serde(default)]
280 pub auth_token: Option<String>,
281 #[serde(default = "default_a2a_rate_limit")]
282 pub rate_limit: u32,
283 #[serde(default = "default_true")]
284 pub require_tls: bool,
285 #[serde(default = "default_true")]
286 pub ssrf_protection: bool,
287 #[serde(default = "default_a2a_max_body")]
288 pub max_body_size: usize,
289 #[serde(default = "default_drain_timeout_ms")]
290 pub drain_timeout_ms: u64,
291 #[serde(default)]
295 pub require_auth: bool,
296 #[serde(default)]
301 pub ibct_keys: Vec<IbctKeyConfig>,
302 #[serde(default)]
308 pub ibct_signing_key_vault_ref: Option<String>,
309 #[serde(default = "default_ibct_ttl")]
311 pub ibct_ttl_secs: u64,
312}
313
314impl std::fmt::Debug for A2aServerConfig {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 f.debug_struct("A2aServerConfig")
317 .field("enabled", &self.enabled)
318 .field("host", &self.host)
319 .field("port", &self.port)
320 .field("public_url", &self.public_url)
321 .field(
322 "auth_token",
323 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
324 )
325 .field("rate_limit", &self.rate_limit)
326 .field("require_tls", &self.require_tls)
327 .field("ssrf_protection", &self.ssrf_protection)
328 .field("max_body_size", &self.max_body_size)
329 .field("drain_timeout_ms", &self.drain_timeout_ms)
330 .field("require_auth", &self.require_auth)
331 .field("ibct_keys_count", &self.ibct_keys.len())
332 .field(
333 "ibct_signing_key_vault_ref",
334 &self.ibct_signing_key_vault_ref,
335 )
336 .field("ibct_ttl_secs", &self.ibct_ttl_secs)
337 .finish()
338 }
339}
340
341impl Default for A2aServerConfig {
342 fn default() -> Self {
343 Self {
344 enabled: false,
345 host: default_a2a_host(),
346 port: default_a2a_port(),
347 public_url: String::new(),
348 auth_token: None,
349 rate_limit: default_a2a_rate_limit(),
350 require_tls: true,
351 ssrf_protection: true,
352 max_body_size: default_a2a_max_body(),
353 drain_timeout_ms: default_drain_timeout_ms(),
354 require_auth: false,
355 ibct_keys: Vec::new(),
356 ibct_signing_key_vault_ref: None,
357 ibct_ttl_secs: default_ibct_ttl(),
358 }
359 }
360}
361
362#[derive(Debug, Clone, Deserialize, Serialize)]
368#[serde(default)]
369pub struct ToolPruningConfig {
370 pub enabled: bool,
372 pub max_tools: usize,
374 pub pruning_provider: ProviderName,
377 pub min_tools_to_prune: usize,
379 pub always_include: Vec<String>,
381}
382
383impl Default for ToolPruningConfig {
384 fn default() -> Self {
385 Self {
386 enabled: false,
387 max_tools: 15,
388 pruning_provider: ProviderName::default(),
389 min_tools_to_prune: 10,
390 always_include: Vec::new(),
391 }
392 }
393}
394
395#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
400#[serde(rename_all = "lowercase")]
401pub enum ToolDiscoveryStrategyConfig {
402 Embedding,
404 Llm,
406 #[default]
408 None,
409}
410
411#[derive(Debug, Clone, Deserialize, Serialize)]
417#[serde(default)]
418pub struct ToolDiscoveryConfig {
419 pub strategy: ToolDiscoveryStrategyConfig,
421 pub top_k: usize,
423 pub min_similarity: f32,
425 pub embedding_provider: ProviderName,
429 pub always_include: Vec<String>,
431 pub min_tools_to_filter: usize,
433 pub strict: bool,
436}
437
438impl Default for ToolDiscoveryConfig {
439 fn default() -> Self {
440 Self {
441 strategy: ToolDiscoveryStrategyConfig::None,
442 top_k: 10,
443 min_similarity: 0.2,
444 embedding_provider: ProviderName::default(),
445 always_include: Vec::new(),
446 min_tools_to_filter: 10,
447 strict: false,
448 }
449 }
450}
451
452#[derive(Debug, Clone, Deserialize, Serialize)]
454#[allow(clippy::struct_excessive_bools)]
455pub struct TrustCalibrationConfig {
456 #[serde(default)]
458 pub enabled: bool,
459 #[serde(default = "default_true")]
461 pub probe_on_connect: bool,
462 #[serde(default = "default_true")]
464 pub monitor_invocations: bool,
465 #[serde(default = "default_true")]
467 pub persist_scores: bool,
468 #[serde(default = "default_decay_rate")]
470 pub decay_rate_per_day: f64,
471 #[serde(default = "default_injection_penalty")]
473 pub injection_penalty: f64,
474 #[serde(default)]
476 pub verifier_provider: ProviderName,
477}
478
479fn default_decay_rate() -> f64 {
480 0.01
481}
482
483fn default_injection_penalty() -> f64 {
484 0.25
485}
486
487impl Default for TrustCalibrationConfig {
488 fn default() -> Self {
489 Self {
490 enabled: false,
491 probe_on_connect: true,
492 monitor_invocations: true,
493 persist_scores: true,
494 decay_rate_per_day: default_decay_rate(),
495 injection_penalty: default_injection_penalty(),
496 verifier_provider: ProviderName::default(),
497 }
498 }
499}
500
501fn default_max_description_bytes() -> usize {
502 2048
503}
504
505fn default_max_instructions_bytes() -> usize {
506 2048
507}
508
509fn default_elicitation_timeout() -> u64 {
510 120
511}
512
513fn default_elicitation_queue_capacity() -> usize {
514 16
515}
516
517#[allow(clippy::struct_excessive_bools)]
518#[derive(Debug, Clone, Deserialize, Serialize)]
519pub struct McpConfig {
520 #[serde(default)]
521 pub servers: Vec<McpServerConfig>,
522 #[serde(default)]
523 pub allowed_commands: Vec<String>,
524 #[serde(default = "default_max_dynamic_servers")]
525 pub max_dynamic_servers: usize,
526 #[serde(default)]
528 pub pruning: ToolPruningConfig,
529 #[serde(default)]
531 pub trust_calibration: TrustCalibrationConfig,
532 #[serde(default)]
534 pub tool_discovery: ToolDiscoveryConfig,
535 #[serde(default = "default_max_description_bytes")]
537 pub max_description_bytes: usize,
538 #[serde(default = "default_max_instructions_bytes")]
540 pub max_instructions_bytes: usize,
541 #[serde(default)]
545 pub elicitation_enabled: bool,
546 #[serde(default = "default_elicitation_timeout")]
548 pub elicitation_timeout: u64,
549 #[serde(default = "default_elicitation_queue_capacity")]
553 pub elicitation_queue_capacity: usize,
554 #[serde(default = "default_true")]
557 pub elicitation_warn_sensitive_fields: bool,
558 #[serde(default)]
564 pub lock_tool_list: bool,
565 #[serde(default)]
570 pub default_env_isolation: bool,
571}
572
573impl Default for McpConfig {
574 fn default() -> Self {
575 Self {
576 servers: Vec::new(),
577 allowed_commands: Vec::new(),
578 max_dynamic_servers: default_max_dynamic_servers(),
579 pruning: ToolPruningConfig::default(),
580 trust_calibration: TrustCalibrationConfig::default(),
581 tool_discovery: ToolDiscoveryConfig::default(),
582 max_description_bytes: default_max_description_bytes(),
583 max_instructions_bytes: default_max_instructions_bytes(),
584 elicitation_enabled: false,
585 elicitation_timeout: default_elicitation_timeout(),
586 elicitation_queue_capacity: default_elicitation_queue_capacity(),
587 elicitation_warn_sensitive_fields: true,
588 lock_tool_list: false,
589 default_env_isolation: false,
590 }
591 }
592}
593
594#[derive(Clone, Deserialize, Serialize)]
595pub struct McpServerConfig {
596 pub id: String,
597 pub command: Option<String>,
599 #[serde(default)]
600 pub args: Vec<String>,
601 #[serde(default)]
602 pub env: HashMap<String, String>,
603 pub url: Option<String>,
605 #[serde(default = "default_mcp_timeout")]
606 pub timeout: u64,
607 #[serde(default)]
609 pub policy: zeph_mcp::McpPolicy,
610 #[serde(default)]
613 pub headers: HashMap<String, String>,
614 #[serde(default)]
616 pub oauth: Option<McpOAuthConfig>,
617 #[serde(default)]
619 pub trust_level: McpTrustLevel,
620 #[serde(default)]
624 pub tool_allowlist: Option<Vec<String>>,
625 #[serde(default)]
631 pub expected_tools: Vec<String>,
632 #[serde(default)]
636 pub roots: Vec<McpRootEntry>,
637 #[serde(default)]
640 pub tool_metadata: HashMap<String, ToolSecurityMeta>,
641 #[serde(default)]
645 pub elicitation_enabled: Option<bool>,
646 #[serde(default)]
653 pub env_isolation: Option<bool>,
654}
655
656#[derive(Debug, Clone, Deserialize, Serialize)]
658pub struct McpRootEntry {
659 pub uri: String,
661 #[serde(default)]
663 pub name: Option<String>,
664}
665
666#[derive(Debug, Clone, Deserialize, Serialize)]
668pub struct McpOAuthConfig {
669 #[serde(default)]
671 pub enabled: bool,
672 #[serde(default)]
674 pub token_storage: OAuthTokenStorage,
675 #[serde(default)]
677 pub scopes: Vec<String>,
678 #[serde(default = "default_oauth_callback_port")]
680 pub callback_port: u16,
681 #[serde(default = "default_oauth_client_name")]
683 pub client_name: String,
684}
685
686impl Default for McpOAuthConfig {
687 fn default() -> Self {
688 Self {
689 enabled: false,
690 token_storage: OAuthTokenStorage::default(),
691 scopes: Vec::new(),
692 callback_port: default_oauth_callback_port(),
693 client_name: default_oauth_client_name(),
694 }
695 }
696}
697
698#[derive(Debug, Clone, Default, Deserialize, Serialize)]
700#[serde(rename_all = "lowercase")]
701pub enum OAuthTokenStorage {
702 #[default]
704 Vault,
705 Memory,
707}
708
709impl std::fmt::Debug for McpServerConfig {
710 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
711 let redacted_env: HashMap<&str, &str> = self
712 .env
713 .keys()
714 .map(|k| (k.as_str(), "[REDACTED]"))
715 .collect();
716 let redacted_headers: HashMap<&str, &str> = self
718 .headers
719 .keys()
720 .map(|k| (k.as_str(), "[REDACTED]"))
721 .collect();
722 f.debug_struct("McpServerConfig")
723 .field("id", &self.id)
724 .field("command", &self.command)
725 .field("args", &self.args)
726 .field("env", &redacted_env)
727 .field("url", &self.url)
728 .field("timeout", &self.timeout)
729 .field("policy", &self.policy)
730 .field("headers", &redacted_headers)
731 .field("oauth", &self.oauth)
732 .field("trust_level", &self.trust_level)
733 .field("tool_allowlist", &self.tool_allowlist)
734 .field("expected_tools", &self.expected_tools)
735 .field("roots", &self.roots)
736 .field(
737 "tool_metadata_keys",
738 &self.tool_metadata.keys().collect::<Vec<_>>(),
739 )
740 .field("elicitation_enabled", &self.elicitation_enabled)
741 .field("env_isolation", &self.env_isolation)
742 .finish()
743 }
744}