Skip to main content

zeph_config/
channels.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::defaults::default_true;
9
10pub use zeph_mcp::McpTrustLevel;
11
12fn default_slack_port() -> u16 {
13    3000
14}
15
16fn default_slack_webhook_host() -> String {
17    "127.0.0.1".into()
18}
19
20fn default_a2a_host() -> String {
21    "0.0.0.0".into()
22}
23
24fn default_a2a_port() -> u16 {
25    8080
26}
27
28fn default_a2a_rate_limit() -> u32 {
29    60
30}
31
32fn default_a2a_max_body() -> usize {
33    1_048_576
34}
35
36fn default_max_dynamic_servers() -> usize {
37    10
38}
39
40fn default_mcp_timeout() -> u64 {
41    30
42}
43
44fn default_oauth_callback_port() -> u16 {
45    18766
46}
47
48fn default_oauth_client_name() -> String {
49    "Zeph".into()
50}
51
52#[derive(Clone, Deserialize, Serialize)]
53pub struct TelegramConfig {
54    pub token: Option<String>,
55    #[serde(default)]
56    pub allowed_users: Vec<String>,
57}
58
59impl std::fmt::Debug for TelegramConfig {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("TelegramConfig")
62            .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
63            .field("allowed_users", &self.allowed_users)
64            .finish()
65    }
66}
67
68#[derive(Clone, Deserialize, Serialize)]
69pub struct DiscordConfig {
70    pub token: Option<String>,
71    pub application_id: Option<String>,
72    #[serde(default)]
73    pub allowed_user_ids: Vec<String>,
74    #[serde(default)]
75    pub allowed_role_ids: Vec<String>,
76    #[serde(default)]
77    pub allowed_channel_ids: Vec<String>,
78}
79
80impl std::fmt::Debug for DiscordConfig {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("DiscordConfig")
83            .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
84            .field("application_id", &self.application_id)
85            .field("allowed_user_ids", &self.allowed_user_ids)
86            .field("allowed_role_ids", &self.allowed_role_ids)
87            .field("allowed_channel_ids", &self.allowed_channel_ids)
88            .finish()
89    }
90}
91
92#[derive(Clone, Deserialize, Serialize)]
93pub struct SlackConfig {
94    pub bot_token: Option<String>,
95    pub signing_secret: Option<String>,
96    #[serde(default = "default_slack_webhook_host")]
97    pub webhook_host: String,
98    #[serde(default = "default_slack_port")]
99    pub port: u16,
100    #[serde(default)]
101    pub allowed_user_ids: Vec<String>,
102    #[serde(default)]
103    pub allowed_channel_ids: Vec<String>,
104}
105
106impl std::fmt::Debug for SlackConfig {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("SlackConfig")
109            .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
110            .field(
111                "signing_secret",
112                &self.signing_secret.as_ref().map(|_| "[REDACTED]"), // lgtm[rust/cleartext-logging]
113            )
114            .field("webhook_host", &self.webhook_host)
115            .field("port", &self.port)
116            .field("allowed_user_ids", &self.allowed_user_ids)
117            .field("allowed_channel_ids", &self.allowed_channel_ids)
118            .finish()
119    }
120}
121
122#[derive(Deserialize, Serialize)]
123pub struct A2aServerConfig {
124    #[serde(default)]
125    pub enabled: bool,
126    #[serde(default = "default_a2a_host")]
127    pub host: String,
128    #[serde(default = "default_a2a_port")]
129    pub port: u16,
130    #[serde(default)]
131    pub public_url: String,
132    #[serde(default)]
133    pub auth_token: Option<String>,
134    #[serde(default = "default_a2a_rate_limit")]
135    pub rate_limit: u32,
136    #[serde(default = "default_true")]
137    pub require_tls: bool,
138    #[serde(default = "default_true")]
139    pub ssrf_protection: bool,
140    #[serde(default = "default_a2a_max_body")]
141    pub max_body_size: usize,
142}
143
144impl std::fmt::Debug for A2aServerConfig {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        f.debug_struct("A2aServerConfig")
147            .field("enabled", &self.enabled)
148            .field("host", &self.host)
149            .field("port", &self.port)
150            .field("public_url", &self.public_url)
151            .field(
152                "auth_token",
153                &self.auth_token.as_ref().map(|_| "[REDACTED]"),
154            )
155            .field("rate_limit", &self.rate_limit)
156            .field("require_tls", &self.require_tls)
157            .field("ssrf_protection", &self.ssrf_protection)
158            .field("max_body_size", &self.max_body_size)
159            .finish()
160    }
161}
162
163impl Default for A2aServerConfig {
164    fn default() -> Self {
165        Self {
166            enabled: false,
167            host: default_a2a_host(),
168            port: default_a2a_port(),
169            public_url: String::new(),
170            auth_token: None,
171            rate_limit: default_a2a_rate_limit(),
172            require_tls: true,
173            ssrf_protection: true,
174            max_body_size: default_a2a_max_body(),
175        }
176    }
177}
178
179#[derive(Debug, Clone, Default, Deserialize, Serialize)]
180pub struct McpConfig {
181    #[serde(default)]
182    pub servers: Vec<McpServerConfig>,
183    #[serde(default)]
184    pub allowed_commands: Vec<String>,
185    #[serde(default = "default_max_dynamic_servers")]
186    pub max_dynamic_servers: usize,
187}
188
189#[derive(Clone, Deserialize, Serialize)]
190pub struct McpServerConfig {
191    pub id: String,
192    /// Stdio transport: command to spawn.
193    pub command: Option<String>,
194    #[serde(default)]
195    pub args: Vec<String>,
196    #[serde(default)]
197    pub env: HashMap<String, String>,
198    /// HTTP transport: remote MCP server URL.
199    pub url: Option<String>,
200    #[serde(default = "default_mcp_timeout")]
201    pub timeout: u64,
202    /// Optional declarative policy for this server (allowlist, denylist, rate limit).
203    #[serde(default)]
204    pub policy: zeph_mcp::McpPolicy,
205    /// Static HTTP headers for the transport (e.g. `Authorization: Bearer <token>`).
206    /// Values support vault references: `${VAULT_KEY}`.
207    #[serde(default)]
208    pub headers: HashMap<String, String>,
209    /// OAuth 2.1 configuration for this server.
210    #[serde(default)]
211    pub oauth: Option<McpOAuthConfig>,
212    /// Trust level for this server. Default: Untrusted.
213    #[serde(default)]
214    pub trust_level: McpTrustLevel,
215    /// Tool allowlist. Behavior depends on `trust_level`:
216    /// - Trusted: ignored (all tools exposed)
217    /// - Untrusted: empty = all tools (with warning), non-empty = only listed tools
218    /// - Sandboxed: empty = NO tools (fail-closed), non-empty = only listed tools
219    #[serde(default)]
220    pub tool_allowlist: Vec<String>,
221}
222
223/// OAuth 2.1 configuration for an MCP server.
224#[derive(Debug, Clone, Deserialize, Serialize)]
225pub struct McpOAuthConfig {
226    /// Enable OAuth 2.1 for this server.
227    #[serde(default)]
228    pub enabled: bool,
229    /// Token storage backend.
230    #[serde(default)]
231    pub token_storage: OAuthTokenStorage,
232    /// OAuth scopes to request. Empty = server default.
233    #[serde(default)]
234    pub scopes: Vec<String>,
235    /// Port for the local callback server. `0` = auto-assign, `18766` = default fixed port.
236    #[serde(default = "default_oauth_callback_port")]
237    pub callback_port: u16,
238    /// Client name sent during dynamic registration.
239    #[serde(default = "default_oauth_client_name")]
240    pub client_name: String,
241}
242
243impl Default for McpOAuthConfig {
244    fn default() -> Self {
245        Self {
246            enabled: false,
247            token_storage: OAuthTokenStorage::default(),
248            scopes: Vec::new(),
249            callback_port: default_oauth_callback_port(),
250            client_name: default_oauth_client_name(),
251        }
252    }
253}
254
255/// Where OAuth tokens are stored.
256#[derive(Debug, Clone, Default, Deserialize, Serialize)]
257#[serde(rename_all = "lowercase")]
258pub enum OAuthTokenStorage {
259    /// Persisted in the age vault (default).
260    #[default]
261    Vault,
262    /// In-memory only — tokens lost on restart.
263    Memory,
264}
265
266impl std::fmt::Debug for McpServerConfig {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        let redacted_env: HashMap<&str, &str> = self
269            .env
270            .keys()
271            .map(|k| (k.as_str(), "[REDACTED]"))
272            .collect();
273        // Redact header values to avoid leaking tokens in logs.
274        let redacted_headers: HashMap<&str, &str> = self
275            .headers
276            .keys()
277            .map(|k| (k.as_str(), "[REDACTED]"))
278            .collect();
279        f.debug_struct("McpServerConfig")
280            .field("id", &self.id)
281            .field("command", &self.command)
282            .field("args", &self.args)
283            .field("env", &redacted_env)
284            .field("url", &self.url)
285            .field("timeout", &self.timeout)
286            .field("policy", &self.policy)
287            .field("headers", &redacted_headers)
288            .field("oauth", &self.oauth)
289            .field("trust_level", &self.trust_level)
290            .field("tool_allowlist", &self.tool_allowlist)
291            .finish()
292    }
293}