Skip to main content

heliosdb_proxy/plugins/
config.rs

1//! Plugin Configuration
2//!
3//! Configuration types for the WASM plugin system.
4
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8
9/// Configuration for the plugin runtime
10#[derive(Debug, Clone)]
11pub struct PluginRuntimeConfig {
12    /// Enable plugin system
13    pub enabled: bool,
14
15    /// Plugin directory
16    pub plugin_dir: PathBuf,
17
18    /// Hot reload on file change
19    pub hot_reload: bool,
20
21    /// Memory limit per plugin (bytes)
22    pub memory_limit: usize,
23
24    /// Execution timeout per call
25    pub timeout: Duration,
26
27    /// Maximum plugins loaded
28    pub max_plugins: usize,
29
30    /// Enable fuel metering (limits CPU cycles)
31    pub fuel_metering: bool,
32
33    /// Fuel limit per call (if fuel_metering enabled)
34    pub fuel_limit: u64,
35
36    /// Enable WASM SIMD
37    pub enable_simd: bool,
38
39    /// Enable multi-threading
40    pub enable_threads: bool,
41
42    /// Cache compiled modules
43    pub cache_modules: bool,
44
45    /// Module cache directory
46    pub cache_dir: Option<PathBuf>,
47
48    /// Per-plugin configurations
49    pub plugins: HashMap<String, PluginConfig>,
50
51    /// Optional Ed25519 trust root: directory of `*.pub` files. When
52    /// set, every loaded `.wasm` requires a sidecar `.sig` that
53    /// verifies against one of the keys. When `None`, signatures
54    /// aren't checked (preserves the dev-loop ergonomic of dropping
55    /// unsigned `.wasm` files in the plugin dir).
56    pub trust_root: Option<PathBuf>,
57
58    /// Max bytes for a single plugin-KV value (`0` = unlimited).
59    /// Applied to the shared `KvBackend` at runtime construction.
60    pub kv_max_value_bytes: usize,
61
62    /// Max distinct keys per plugin KV namespace (`0` = unlimited).
63    pub kv_max_keys_per_plugin: usize,
64
65    /// Max distinct plugin KV namespaces (`0` = unlimited). Bounds how
66    /// many `<plugin>` namespaces the `/admin/kv` endpoint can create.
67    pub kv_max_plugins: usize,
68
69    /// Max TOTAL retained bytes across ALL plugin KV namespaces (key +
70    /// value + namespace-name bytes; `0` = unlimited). The single cap
71    /// that bounds the whole KV footprint regardless of the per-axis
72    /// product, so a token-holding `/admin/kv` caller cannot drive the
73    /// proxy to an OOM.
74    pub kv_max_total_bytes: usize,
75}
76
77impl Default for PluginRuntimeConfig {
78    fn default() -> Self {
79        Self {
80            enabled: true,
81            plugin_dir: PathBuf::from("/etc/heliosproxy/plugins"),
82            hot_reload: false,
83            memory_limit: 64 * 1024 * 1024, // 64MB
84            timeout: Duration::from_millis(100),
85            max_plugins: 20,
86            fuel_metering: true,
87            fuel_limit: 1_000_000,
88            enable_simd: true,
89            enable_threads: false,
90            cache_modules: true,
91            cache_dir: None,
92            plugins: HashMap::new(),
93            trust_root: None,
94            kv_max_value_bytes: 65536,
95            kv_max_keys_per_plugin: 1024,
96            kv_max_plugins: 256,
97            kv_max_total_bytes: 64 * 1024 * 1024,
98        }
99    }
100}
101
102/// Convert the TOML-shaped `PluginToml` into a live `PluginRuntimeConfig`.
103///
104/// Values that aren't exposed in TOML (SIMD, threading, module cache)
105/// take their runtime defaults.
106impl From<&crate::config::PluginToml> for PluginRuntimeConfig {
107    fn from(t: &crate::config::PluginToml) -> Self {
108        Self {
109            enabled: t.enabled,
110            plugin_dir: PathBuf::from(&t.plugin_dir),
111            hot_reload: t.hot_reload,
112            memory_limit: t.memory_limit_mb.saturating_mul(1024 * 1024),
113            timeout: Duration::from_millis(t.timeout_ms),
114            max_plugins: t.max_plugins,
115            fuel_metering: t.fuel_metering,
116            fuel_limit: t.fuel_limit,
117            enable_simd: true,
118            enable_threads: false,
119            cache_modules: true,
120            cache_dir: None,
121            plugins: HashMap::new(),
122            trust_root: t.trust_root.as_ref().map(PathBuf::from),
123            kv_max_value_bytes: t.kv_max_value_bytes,
124            kv_max_keys_per_plugin: t.kv_max_keys_per_plugin,
125            kv_max_plugins: t.kv_max_plugins,
126            kv_max_total_bytes: t.kv_max_total_bytes,
127        }
128    }
129}
130
131/// Builder for PluginRuntimeConfig
132pub struct PluginRuntimeConfigBuilder {
133    config: PluginRuntimeConfig,
134}
135
136impl PluginRuntimeConfigBuilder {
137    /// Create a new builder
138    pub fn new() -> Self {
139        Self {
140            config: PluginRuntimeConfig::default(),
141        }
142    }
143
144    /// Set enabled
145    pub fn enabled(mut self, enabled: bool) -> Self {
146        self.config.enabled = enabled;
147        self
148    }
149
150    /// Set plugin directory
151    pub fn plugin_dir(mut self, dir: PathBuf) -> Self {
152        self.config.plugin_dir = dir;
153        self
154    }
155
156    /// Set hot reload
157    pub fn hot_reload(mut self, enabled: bool) -> Self {
158        self.config.hot_reload = enabled;
159        self
160    }
161
162    /// Set memory limit
163    pub fn memory_limit(mut self, limit: usize) -> Self {
164        self.config.memory_limit = limit;
165        self
166    }
167
168    /// Set timeout
169    pub fn timeout(mut self, timeout: Duration) -> Self {
170        self.config.timeout = timeout;
171        self
172    }
173
174    /// Set max plugins
175    pub fn max_plugins(mut self, max: usize) -> Self {
176        self.config.max_plugins = max;
177        self
178    }
179
180    /// Set fuel metering
181    pub fn fuel_metering(mut self, enabled: bool) -> Self {
182        self.config.fuel_metering = enabled;
183        self
184    }
185
186    /// Set fuel limit
187    pub fn fuel_limit(mut self, limit: u64) -> Self {
188        self.config.fuel_limit = limit;
189        self
190    }
191
192    /// Enable SIMD
193    pub fn enable_simd(mut self, enabled: bool) -> Self {
194        self.config.enable_simd = enabled;
195        self
196    }
197
198    /// Enable threads
199    pub fn enable_threads(mut self, enabled: bool) -> Self {
200        self.config.enable_threads = enabled;
201        self
202    }
203
204    /// Enable module caching
205    pub fn cache_modules(mut self, enabled: bool) -> Self {
206        self.config.cache_modules = enabled;
207        self
208    }
209
210    /// Set cache directory
211    pub fn cache_dir(mut self, dir: PathBuf) -> Self {
212        self.config.cache_dir = Some(dir);
213        self
214    }
215
216    /// Add plugin config
217    pub fn add_plugin(mut self, name: String, config: PluginConfig) -> Self {
218        self.config.plugins.insert(name, config);
219        self
220    }
221
222    /// Build the config
223    pub fn build(self) -> PluginRuntimeConfig {
224        self.config
225    }
226}
227
228impl Default for PluginRuntimeConfigBuilder {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234/// Per-plugin configuration
235#[derive(Debug, Clone)]
236pub struct PluginConfig {
237    /// Enable this plugin
238    pub enabled: bool,
239
240    /// Plugin priority (higher = earlier execution)
241    pub priority: i32,
242
243    /// Custom config passed to plugin
244    pub config: HashMap<String, serde_json::Value>,
245
246    /// Override memory limit
247    pub memory_limit: Option<usize>,
248
249    /// Override timeout
250    pub timeout: Option<Duration>,
251
252    /// Override fuel limit
253    pub fuel_limit: Option<u64>,
254
255    /// Allowed permissions
256    pub permissions: Vec<String>,
257}
258
259impl Default for PluginConfig {
260    fn default() -> Self {
261        Self {
262            enabled: true,
263            priority: 0,
264            config: HashMap::new(),
265            memory_limit: None,
266            timeout: None,
267            fuel_limit: None,
268            permissions: Vec::new(),
269        }
270    }
271}
272
273/// Builder for PluginConfig
274pub struct PluginConfigBuilder {
275    config: PluginConfig,
276}
277
278impl PluginConfigBuilder {
279    /// Create new builder
280    pub fn new() -> Self {
281        Self {
282            config: PluginConfig::default(),
283        }
284    }
285
286    /// Set enabled
287    pub fn enabled(mut self, enabled: bool) -> Self {
288        self.config.enabled = enabled;
289        self
290    }
291
292    /// Set priority
293    pub fn priority(mut self, priority: i32) -> Self {
294        self.config.priority = priority;
295        self
296    }
297
298    /// Add config value
299    pub fn config_value(mut self, key: &str, value: serde_json::Value) -> Self {
300        self.config.config.insert(key.to_string(), value);
301        self
302    }
303
304    /// Set memory limit
305    pub fn memory_limit(mut self, limit: usize) -> Self {
306        self.config.memory_limit = Some(limit);
307        self
308    }
309
310    /// Set timeout
311    pub fn timeout(mut self, timeout: Duration) -> Self {
312        self.config.timeout = Some(timeout);
313        self
314    }
315
316    /// Set fuel limit
317    pub fn fuel_limit(mut self, limit: u64) -> Self {
318        self.config.fuel_limit = Some(limit);
319        self
320    }
321
322    /// Add permission
323    pub fn permission(mut self, permission: &str) -> Self {
324        self.config.permissions.push(permission.to_string());
325        self
326    }
327
328    /// Build
329    pub fn build(self) -> PluginConfig {
330        self.config
331    }
332}
333
334impl Default for PluginConfigBuilder {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_runtime_config_default() {
346        let config = PluginRuntimeConfig::default();
347        assert!(config.enabled);
348        assert!(!config.hot_reload);
349        assert_eq!(config.memory_limit, 64 * 1024 * 1024);
350        assert_eq!(config.max_plugins, 20);
351    }
352
353    #[test]
354    fn test_runtime_config_builder() {
355        let config = PluginRuntimeConfigBuilder::new()
356            .enabled(true)
357            .hot_reload(true)
358            .memory_limit(128 * 1024 * 1024)
359            .timeout(Duration::from_millis(200))
360            .max_plugins(50)
361            .fuel_metering(true)
362            .fuel_limit(2_000_000)
363            .build();
364
365        assert!(config.enabled);
366        assert!(config.hot_reload);
367        assert_eq!(config.memory_limit, 128 * 1024 * 1024);
368        assert_eq!(config.timeout, Duration::from_millis(200));
369        assert_eq!(config.max_plugins, 50);
370    }
371
372    #[test]
373    fn test_plugin_config_default() {
374        let config = PluginConfig::default();
375        assert!(config.enabled);
376        assert_eq!(config.priority, 0);
377        assert!(config.permissions.is_empty());
378    }
379
380    #[test]
381    fn test_plugin_config_builder() {
382        let config = PluginConfigBuilder::new()
383            .enabled(true)
384            .priority(100)
385            .config_value("key", serde_json::json!("value"))
386            .memory_limit(32 * 1024 * 1024)
387            .permission("http_fetch")
388            .permission("cache_read")
389            .build();
390
391        assert!(config.enabled);
392        assert_eq!(config.priority, 100);
393        assert_eq!(config.config.get("key"), Some(&serde_json::json!("value")));
394        assert_eq!(config.memory_limit, Some(32 * 1024 * 1024));
395        assert_eq!(config.permissions.len(), 2);
396    }
397}