Skip to main content

astrid_plugins/wasm/
loader.rs

1//! WASM plugin loader with builder-pattern configuration.
2//!
3//! [`WasmPluginLoader`] is the factory for creating [`WasmPlugin`] instances
4//! with shared configuration (security gate, memory limits, timeouts).
5
6use std::sync::Arc;
7use std::time::Duration;
8
9use crate::manifest::PluginManifest;
10use crate::security::PluginSecurityGate;
11use crate::wasm::plugin::{WasmPlugin, WasmPluginConfig};
12
13/// Default maximum WASM linear memory: 64 MB.
14const DEFAULT_MAX_MEMORY_BYTES: u64 = 64 * 1024 * 1024;
15
16/// Default maximum execution time per call: 30 seconds.
17const DEFAULT_MAX_EXECUTION_TIME: Duration = Duration::from_secs(30);
18
19/// Factory for creating [`WasmPlugin`] instances with shared configuration.
20///
21/// # Example
22///
23/// ```rust,no_run
24/// use astrid_plugins::wasm::WasmPluginLoader;
25/// use std::time::Duration;
26///
27/// let loader = WasmPluginLoader::new()
28///     .with_memory_limit(32 * 1024 * 1024) // 32 MB
29///     .with_timeout(Duration::from_secs(10));
30/// ```
31pub struct WasmPluginLoader {
32    security: Option<Arc<dyn PluginSecurityGate>>,
33    max_memory_bytes: u64,
34    max_execution_time: Duration,
35    require_hash: bool,
36}
37
38impl std::fmt::Debug for WasmPluginLoader {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("WasmPluginLoader")
41            .field("has_security", &self.security.is_some())
42            .field("max_memory_bytes", &self.max_memory_bytes)
43            .field("max_execution_time", &self.max_execution_time)
44            .field("require_hash", &self.require_hash)
45            .finish()
46    }
47}
48
49impl Default for WasmPluginLoader {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl WasmPluginLoader {
56    /// Create a new loader with default settings (64 MB memory, 30s timeout).
57    #[must_use]
58    pub fn new() -> Self {
59        Self {
60            security: None,
61            max_memory_bytes: DEFAULT_MAX_MEMORY_BYTES,
62            max_execution_time: DEFAULT_MAX_EXECUTION_TIME,
63            require_hash: false,
64        }
65    }
66
67    /// Set the security gate for authorizing host function calls.
68    #[must_use]
69    pub fn with_security(mut self, gate: Arc<dyn PluginSecurityGate>) -> Self {
70        self.security = Some(gate);
71        self
72    }
73
74    /// Set the maximum WASM linear memory in bytes.
75    #[must_use]
76    pub fn with_memory_limit(mut self, bytes: u64) -> Self {
77        self.max_memory_bytes = bytes;
78        self
79    }
80
81    /// Set the maximum execution time per WASM call.
82    #[must_use]
83    pub fn with_timeout(mut self, duration: Duration) -> Self {
84        self.max_execution_time = duration;
85        self
86    }
87
88    /// Require WASM modules to have a hash in their manifest.
89    ///
90    /// When enabled, plugins without a `hash` field in their manifest
91    /// entry point will fail to load. Recommended for production.
92    #[must_use]
93    pub fn with_require_hash(mut self, require: bool) -> Self {
94        self.require_hash = require;
95        self
96    }
97
98    /// Create an unloaded [`WasmPlugin`] from a manifest.
99    ///
100    /// The plugin must be loaded via [`Plugin::load()`](crate::Plugin::load)
101    /// before it can serve tools.
102    #[must_use]
103    pub fn create_plugin(&self, manifest: PluginManifest) -> WasmPlugin {
104        let config = WasmPluginConfig {
105            security: self.security.clone(),
106            max_memory_bytes: self.max_memory_bytes,
107            max_execution_time: self.max_execution_time,
108            require_hash: self.require_hash,
109        };
110        WasmPlugin::new(manifest, config)
111    }
112
113    /// Get the configured memory limit.
114    #[must_use]
115    pub fn max_memory_bytes(&self) -> u64 {
116        self.max_memory_bytes
117    }
118
119    /// Get the configured execution timeout.
120    #[must_use]
121    pub fn max_execution_time(&self) -> Duration {
122        self.max_execution_time
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn default_config() {
132        let loader = WasmPluginLoader::new();
133        assert_eq!(loader.max_memory_bytes(), 64 * 1024 * 1024);
134        assert_eq!(loader.max_execution_time(), Duration::from_secs(30));
135    }
136
137    #[test]
138    fn custom_config() {
139        let loader = WasmPluginLoader::new()
140            .with_memory_limit(32 * 1024 * 1024)
141            .with_timeout(Duration::from_secs(10));
142        assert_eq!(loader.max_memory_bytes(), 32 * 1024 * 1024);
143        assert_eq!(loader.max_execution_time(), Duration::from_secs(10));
144    }
145
146    #[test]
147    fn create_plugin_returns_unloaded() {
148        use crate::Plugin;
149        use crate::manifest::{PluginEntryPoint, PluginManifest};
150        use crate::plugin::PluginState;
151        use std::collections::HashMap;
152        use std::path::PathBuf;
153
154        let loader = WasmPluginLoader::new();
155        let manifest = PluginManifest {
156            id: crate::PluginId::from_static("test"),
157            name: "Test".into(),
158            version: "0.1.0".into(),
159            description: None,
160            author: None,
161            entry_point: PluginEntryPoint::Wasm {
162                path: PathBuf::from("test.wasm"),
163                hash: None,
164            },
165            capabilities: vec![],
166            config: HashMap::new(),
167        };
168        let plugin = loader.create_plugin(manifest);
169        assert_eq!(plugin.state(), PluginState::Unloaded);
170    }
171}