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}