Skip to main content

edgecrab_plugins/
config.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::types::TrustLevel;
7
8#[derive(Debug, Clone, Deserialize, Serialize)]
9#[serde(default)]
10pub struct PluginsConfig {
11    pub enabled: bool,
12    pub auto_enable: bool,
13    pub call_timeout_secs: u64,
14    pub startup_timeout_secs: u64,
15    pub disabled: Vec<String>,
16    pub platform_disabled: HashMap<String, Vec<String>>,
17    pub install_dir: PathBuf,
18    pub quarantine_dir: PathBuf,
19    pub external_dirs: Vec<String>,
20    pub security: PluginSecurityConfig,
21    pub hub: PluginsHubConfig,
22    pub host_api: HostApiLimitsConfig,
23    pub overrides: HashMap<String, PluginOverrideConfig>,
24}
25
26impl Default for PluginsConfig {
27    fn default() -> Self {
28        let home = std::env::var("EDGECRAB_HOME")
29            .map(PathBuf::from)
30            .unwrap_or_else(|_| {
31                dirs::home_dir()
32                    .unwrap_or_else(|| PathBuf::from("."))
33                    .join(".edgecrab")
34            });
35        Self {
36            enabled: true,
37            auto_enable: true,
38            call_timeout_secs: 60,
39            startup_timeout_secs: 10,
40            disabled: Vec::new(),
41            platform_disabled: HashMap::new(),
42            install_dir: home.join("plugins"),
43            quarantine_dir: home.join("plugins").join(".quarantine"),
44            external_dirs: Vec::new(),
45            security: PluginSecurityConfig::default(),
46            hub: PluginsHubConfig::default(),
47            host_api: HostApiLimitsConfig::default(),
48            overrides: HashMap::new(),
49        }
50    }
51}
52
53impl PluginsConfig {
54    pub fn is_plugin_enabled(&self, name: &str, platform: Option<&str>) -> bool {
55        if !self.enabled {
56            return false;
57        }
58
59        if self.disabled.iter().any(|candidate| candidate == name) {
60            return self
61                .overrides
62                .get(name)
63                .and_then(|override_cfg| override_cfg.disabled)
64                == Some(false);
65        }
66
67        if let Some(platform) = platform {
68            let platform_key = platform.to_ascii_lowercase();
69            if self
70                .platform_disabled
71                .get(&platform_key)
72                .is_some_and(|names| names.iter().any(|candidate| candidate == name))
73            {
74                return self
75                    .overrides
76                    .get(name)
77                    .and_then(|override_cfg| override_cfg.disabled)
78                    == Some(false);
79            }
80        }
81
82        !matches!(
83            self.overrides
84                .get(name)
85                .and_then(|override_cfg| override_cfg.disabled),
86            Some(true)
87        )
88    }
89
90    pub fn expanded_external_dirs(&self) -> Vec<PathBuf> {
91        self.external_dirs
92            .iter()
93            .filter_map(|raw| expand_plugin_dir(raw))
94            .collect()
95    }
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize)]
99#[serde(default)]
100pub struct PluginSecurityConfig {
101    pub min_trust_level: TrustLevel,
102    pub allow_caution: bool,
103    pub max_tool_count: usize,
104    pub max_skill_size_kb: usize,
105    pub scan_on_load: bool,
106}
107
108impl Default for PluginSecurityConfig {
109    fn default() -> Self {
110        Self {
111            min_trust_level: TrustLevel::Unverified,
112            allow_caution: false,
113            max_tool_count: 100,
114            max_skill_size_kb: 512,
115            scan_on_load: true,
116        }
117    }
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize)]
121#[serde(default)]
122pub struct PluginsHubConfig {
123    pub enabled: bool,
124    pub cache_ttl_secs: u64,
125    pub sources: Vec<HubSource>,
126}
127
128impl Default for PluginsHubConfig {
129    fn default() -> Self {
130        Self {
131            enabled: true,
132            cache_ttl_secs: 900,
133            sources: Vec::new(),
134        }
135    }
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize)]
139pub struct HubSource {
140    pub url: String,
141    pub name: String,
142    pub trust_override: Option<TrustLevel>,
143}
144
145#[derive(Debug, Clone, Deserialize, Serialize)]
146#[serde(default)]
147pub struct HostApiLimitsConfig {
148    pub max_memory_write_per_min: u32,
149    pub max_secret_get_per_min: u32,
150    pub max_inject_per_min: u32,
151    pub max_tool_delegate_per_min: u32,
152    pub max_log_per_min: u32,
153}
154
155impl Default for HostApiLimitsConfig {
156    fn default() -> Self {
157        Self {
158            max_memory_write_per_min: 60,
159            max_secret_get_per_min: 20,
160            max_inject_per_min: 5,
161            max_tool_delegate_per_min: 30,
162            max_log_per_min: 200,
163        }
164    }
165}
166
167#[derive(Debug, Clone, Deserialize, Serialize, Default)]
168#[serde(default)]
169pub struct PluginOverrideConfig {
170    pub call_timeout_secs: Option<u64>,
171    pub disabled: Option<bool>,
172}
173
174fn expand_plugin_dir(raw: &str) -> Option<PathBuf> {
175    let trimmed = raw.trim();
176    if trimmed.is_empty() {
177        return None;
178    }
179
180    let mut expanded = trimmed.to_string();
181    if let Some(rest) = expanded.strip_prefix("~/") {
182        expanded = dirs::home_dir()?.join(rest).display().to_string();
183    }
184
185    for (name, value) in std::env::vars() {
186        let pattern = format!("${{{name}}}");
187        if expanded.contains(&pattern) {
188            expanded = expanded.replace(&pattern, &value);
189        }
190    }
191
192    Some(PathBuf::from(expanded))
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn plugin_enable_checks_global_and_platform_lists() {
201        let mut config = PluginsConfig::default();
202        assert!(config.is_plugin_enabled("demo", Some("cli")));
203
204        config.disabled.push("demo".into());
205        assert!(!config.is_plugin_enabled("demo", Some("cli")));
206
207        config.disabled.clear();
208        config
209            .platform_disabled
210            .insert("cli".into(), vec!["demo".into()]);
211        assert!(!config.is_plugin_enabled("demo", Some("cli")));
212        assert!(config.is_plugin_enabled("demo", Some("telegram")));
213    }
214
215    #[test]
216    fn override_can_reenable_plugin() {
217        let mut config = PluginsConfig::default();
218        config.disabled.push("demo".into());
219        config.overrides.insert(
220            "demo".into(),
221            PluginOverrideConfig {
222                disabled: Some(false),
223                call_timeout_secs: None,
224            },
225        );
226
227        assert!(config.is_plugin_enabled("demo", Some("cli")));
228    }
229
230    #[test]
231    fn expands_external_dirs_with_home_and_env() {
232        let home = dirs::home_dir().expect("home dir");
233        let env_home = std::env::var("HOME").expect("HOME env");
234        let config = PluginsConfig {
235            external_dirs: vec!["~/custom".into(), "${HOME}/plugins-extra".into()],
236            ..PluginsConfig::default()
237        };
238        let dirs = config.expanded_external_dirs();
239
240        assert_eq!(dirs[0], home.join("custom"));
241        assert_eq!(dirs[1], PathBuf::from(env_home).join("plugins-extra"));
242    }
243}