1use std::path::{Path, PathBuf};
18#[cfg(feature = "wasm")]
19use std::sync::Arc;
20
21use ed25519_dalek::VerifyingKey;
22use fidius_core::descriptor::BufferStrategyKind;
23
24use crate::error::LoadError;
25use crate::loader::{self, LoadedPlugin};
26use crate::signing;
27use crate::types::{LoadPolicy, PluginInfo, PluginRuntimeKind};
28
29#[allow(dead_code)] pub struct PluginHost {
32 search_paths: Vec<PathBuf>,
33 load_policy: LoadPolicy,
34 require_signature: bool,
35 trusted_keys: Vec<VerifyingKey>,
36 expected_hash: Option<u64>,
37 expected_strategy: Option<BufferStrategyKind>,
38 #[cfg(feature = "wasm")]
42 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
43}
44
45pub struct PluginHostBuilder {
47 search_paths: Vec<PathBuf>,
48 load_policy: LoadPolicy,
49 require_signature: bool,
50 trusted_keys: Vec<VerifyingKey>,
51 expected_hash: Option<u64>,
52 expected_strategy: Option<BufferStrategyKind>,
53 #[cfg(feature = "wasm")]
54 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
55}
56
57impl PluginHostBuilder {
58 fn new() -> Self {
59 Self {
60 search_paths: Vec::new(),
61 load_policy: LoadPolicy::Strict,
62 require_signature: false,
63 trusted_keys: Vec::new(),
64 expected_hash: None,
65 expected_strategy: None,
66 #[cfg(feature = "wasm")]
67 egress: None,
68 }
69 }
70
71 #[cfg(feature = "wasm")]
77 pub fn egress(mut self, policy: impl crate::executor::wasm::EgressPolicy) -> Self {
78 self.egress = Some(Arc::new(policy));
79 self
80 }
81
82 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
84 self.search_paths.push(path.into());
85 self
86 }
87
88 pub fn load_policy(mut self, policy: LoadPolicy) -> Self {
90 self.load_policy = policy;
91 self
92 }
93
94 pub fn require_signature(mut self, require: bool) -> Self {
96 self.require_signature = require;
97 self
98 }
99
100 pub fn trusted_keys(mut self, keys: &[VerifyingKey]) -> Self {
102 self.trusted_keys = keys.to_vec();
103 self
104 }
105
106 pub fn interface_hash(mut self, hash: u64) -> Self {
108 self.expected_hash = Some(hash);
109 self
110 }
111
112 pub fn buffer_strategy(mut self, strategy: BufferStrategyKind) -> Self {
114 self.expected_strategy = Some(strategy);
115 self
116 }
117
118 pub fn build(self) -> Result<PluginHost, LoadError> {
120 Ok(PluginHost {
121 search_paths: self.search_paths,
122 load_policy: self.load_policy,
123 require_signature: self.require_signature,
124 trusted_keys: self.trusted_keys,
125 expected_hash: self.expected_hash,
126 expected_strategy: self.expected_strategy,
127 #[cfg(feature = "wasm")]
128 egress: self.egress,
129 })
130 }
131}
132
133impl PluginHost {
134 pub fn builder() -> PluginHostBuilder {
136 PluginHostBuilder::new()
137 }
138
139 pub fn discover(&self) -> Result<Vec<PluginInfo>, LoadError> {
149 #[cfg(feature = "tracing")]
150 tracing::info!(search_paths = ?self.search_paths, "discovering plugins");
151
152 let mut plugins = Vec::new();
153
154 for search_path in &self.search_paths {
155 if !search_path.is_dir() {
156 continue;
157 }
158
159 let entries = std::fs::read_dir(search_path)?;
160 for entry in entries {
161 let entry = entry?;
162 let path = entry.path();
163
164 if is_dylib(&path) {
165 self.discover_cdylib(&path, &mut plugins);
166 } else if path.is_dir() && path.join("package.toml").exists() {
167 self.discover_package(&path, &mut plugins);
168 }
169 }
170 }
171
172 Ok(plugins)
173 }
174
175 fn discover_cdylib(&self, path: &Path, plugins: &mut Vec<PluginInfo>) {
176 if self.require_signature && signing::verify_signature(path, &self.trusted_keys).is_err() {
178 return;
179 }
180
181 let Ok(loaded) = loader::load_library(path) else {
182 return; };
184 for plugin in &loaded.plugins {
185 if loader::validate_against_interface(
186 plugin,
187 self.expected_hash,
188 self.expected_strategy,
189 )
190 .is_ok()
191 {
192 plugins.push(plugin.info.clone());
193 }
194 }
195 }
196
197 fn discover_package(&self, dir: &Path, plugins: &mut Vec<PluginInfo>) {
201 let Ok(manifest) = fidius_core::package::load_manifest_untyped(dir) else {
202 return;
203 };
204 use fidius_core::package::PackageRuntime;
205 let runtime = match manifest.package.runtime() {
206 PackageRuntime::Python => PluginRuntimeKind::Python,
207 PackageRuntime::Wasm => PluginRuntimeKind::Wasm,
208 PackageRuntime::Rust => return,
211 };
212 plugins.push(PluginInfo {
213 name: manifest.package.name.clone(),
214 interface_name: manifest.package.interface.clone(),
215 interface_hash: 0,
219 interface_version: manifest.package.interface_version,
220 capabilities: 0,
221 buffer_strategy: BufferStrategyKind::PluginAllocated,
222 runtime,
223 });
224 }
225
226 pub fn load(&self, name: &str) -> Result<LoadedPlugin, LoadError> {
231 #[cfg(feature = "tracing")]
232 tracing::info!(plugin_name = name, "loading plugin");
233
234 for search_path in &self.search_paths {
235 if !search_path.is_dir() {
236 continue;
237 }
238
239 let entries = std::fs::read_dir(search_path)?;
240 for entry in entries {
241 let entry = entry?;
242 let path = entry.path();
243
244 if !is_dylib(&path) {
245 continue;
246 }
247
248 if self.require_signature {
250 signing::verify_signature(&path, &self.trusted_keys)?;
251 }
252
253 match loader::load_library(&path) {
254 Ok(loaded) => {
255 for plugin in loaded.plugins {
256 if plugin.info.name == name {
257 loader::validate_against_interface(
258 &plugin,
259 self.expected_hash,
260 self.expected_strategy,
261 )?;
262 return Ok(plugin);
263 }
264 }
265 }
266 Err(_) => continue,
267 }
268 }
269 }
270
271 Err(LoadError::PluginNotFound {
272 name: name.to_string(),
273 })
274 }
275
276 pub fn find_python_package(&self, name: &str) -> Result<PathBuf, LoadError> {
280 for search_path in &self.search_paths {
281 if !search_path.is_dir() {
282 continue;
283 }
284 let entries = std::fs::read_dir(search_path)?;
285 for entry in entries {
286 let entry = entry?;
287 let path = entry.path();
288 if !path.is_dir() {
289 continue;
290 }
291 if !path.join("package.toml").exists() {
292 continue;
293 }
294 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
295 continue;
296 };
297 if matches!(
298 manifest.package.runtime(),
299 fidius_core::package::PackageRuntime::Python
300 ) && manifest.package.name == name
301 {
302 return Ok(path);
303 }
304 }
305 }
306 Err(LoadError::PluginNotFound {
307 name: name.to_string(),
308 })
309 }
310
311 #[cfg(feature = "python")]
321 pub fn load_python(
322 &self,
323 name: &str,
324 descriptor: &'static fidius_core::python_descriptor::PythonInterfaceDescriptor,
325 ) -> Result<crate::handle::PluginHandle, LoadError> {
326 let dir = self.find_python_package(name)?;
327 if self.require_signature {
329 signing::verify_package_signature(&dir, &self.trusted_keys)?;
330 }
331 let manifest = fidius_core::package::load_manifest_untyped(&dir)
332 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
333 let py = fidius_python::load_python_plugin(&dir, descriptor)
334 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
335 let info = crate::types::PluginInfo {
339 name: manifest.package.name.clone(),
340 interface_name: descriptor.interface_name.to_string(),
341 interface_hash: descriptor.interface_hash,
342 interface_version: manifest.package.interface_version,
343 capabilities: 0,
344 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
345 runtime: crate::types::PluginRuntimeKind::Python,
346 };
347 Ok(crate::handle::PluginHandle::from_python(py, info))
348 }
349
350 #[cfg(feature = "wasm")]
353 pub fn find_wasm_package(&self, name: &str) -> Result<PathBuf, LoadError> {
354 for search_path in &self.search_paths {
355 if !search_path.is_dir() {
356 continue;
357 }
358 for entry in std::fs::read_dir(search_path)? {
359 let entry = entry?;
360 let path = entry.path();
361 if !path.is_dir() || !path.join("package.toml").exists() {
362 continue;
363 }
364 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
365 continue;
366 };
367 if matches!(
368 manifest.package.runtime(),
369 fidius_core::package::PackageRuntime::Wasm
370 ) && manifest.package.name == name
371 {
372 return Ok(path);
373 }
374 }
375 }
376 Err(LoadError::PluginNotFound {
377 name: name.to_string(),
378 })
379 }
380
381 #[cfg(feature = "wasm")]
396 pub fn load_wasm(
397 &self,
398 name: &str,
399 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
400 ) -> Result<crate::handle::PluginHandle, LoadError> {
401 self.load_wasm_impl(name, descriptor, self.egress.clone())
402 }
403
404 #[cfg(feature = "wasm")]
410 pub fn load_wasm_with_egress(
411 &self,
412 name: &str,
413 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
414 egress: impl crate::executor::wasm::EgressPolicy,
415 ) -> Result<crate::handle::PluginHandle, LoadError> {
416 self.load_wasm_impl(name, descriptor, Some(Arc::new(egress)))
417 }
418
419 #[cfg(feature = "wasm")]
420 fn load_wasm_impl(
421 &self,
422 name: &str,
423 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
424 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
425 ) -> Result<crate::handle::PluginHandle, LoadError> {
426 use crate::executor::wasm::{WasmComponentExecutor, WasmMethod};
427
428 let dir = self.find_wasm_package(name)?;
429 if self.require_signature {
431 signing::verify_package_signature(&dir, &self.trusted_keys)?;
432 }
433 let manifest = fidius_core::package::load_manifest_untyped(&dir)
434 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
435 let wasm_meta = manifest
436 .wasm
437 .as_ref()
438 .ok_or_else(|| LoadError::WasmLoad("manifest is missing the [wasm] section".into()))?;
439
440 let methods: Vec<WasmMethod> = descriptor
441 .methods
442 .iter()
443 .map(|m| WasmMethod {
444 name: m.name.to_string(),
445 wire_raw: m.wire_raw,
446 streaming: m.streaming,
447 })
448 .collect();
449 let info = crate::types::PluginInfo {
450 name: manifest.package.name.clone(),
451 interface_name: descriptor.interface_name.to_string(),
452 interface_hash: descriptor.interface_hash,
453 interface_version: manifest.package.interface_version,
454 capabilities: 0,
455 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
456 runtime: crate::types::PluginRuntimeKind::Wasm,
457 };
458 let interface = descriptor.interface_export.to_string();
459 let capabilities = wasm_meta.capabilities.clone();
460
461 let cwasm_path = wasm_meta
467 .precompiled
468 .as_ref()
469 .map(|p| dir.join(p))
470 .or_else(|| {
471 let sibling = dir.join(&wasm_meta.component).with_extension("cwasm");
472 sibling.exists().then_some(sibling)
473 });
474
475 let jit = |interface: String, methods, capabilities, info| -> Result<_, LoadError> {
476 let bytes = std::fs::read(dir.join(&wasm_meta.component))?;
477 WasmComponentExecutor::from_component_bytes_with_egress(
478 &bytes,
479 interface,
480 methods,
481 capabilities,
482 egress.clone(),
483 info,
484 )
485 .map_err(|e| LoadError::WasmLoad(e.to_string()))
486 };
487
488 let executor = match cwasm_path {
489 Some(cwasm) if cwasm.exists() => {
490 let bytes = std::fs::read(&cwasm)?;
491 let aot = unsafe {
495 WasmComponentExecutor::from_cwasm_with_egress(
496 &bytes,
497 interface.clone(),
498 methods.clone(),
499 capabilities.clone(),
500 egress.clone(),
501 info.clone(),
502 )
503 };
504 match aot {
505 Ok(e) => e,
506 Err(_err) => {
507 #[cfg(feature = "tracing")]
508 tracing::warn!(
509 cwasm = %cwasm.display(),
510 error = %_err,
511 "precompiled .cwasm rejected (likely engine/version mismatch); falling back to JIT"
512 );
513 jit(interface, methods, capabilities, info)?
514 }
515 }
516 }
517 _ => jit(interface, methods, capabilities, info)?,
518 };
519
520 let got = executor
522 .interface_hash()
523 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
524 if got != descriptor.interface_hash {
525 return Err(LoadError::InterfaceHashMismatch {
526 got,
527 expected: descriptor.interface_hash,
528 });
529 }
530
531 Ok(crate::handle::PluginHandle::from_wasm(executor))
532 }
533}
534
535fn is_dylib(path: &Path) -> bool {
537 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
538 if cfg!(target_os = "macos") {
539 ext == "dylib"
540 } else if cfg!(target_os = "windows") {
541 ext == "dll"
542 } else {
543 ext == "so"
544 }
545}