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 #[cfg(feature = "wasm")]
88 pub fn egress_policy(mut self, policy: Arc<dyn crate::executor::wasm::EgressPolicy>) -> Self {
89 self.egress = Some(policy);
90 self
91 }
92
93 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
95 self.search_paths.push(path.into());
96 self
97 }
98
99 pub fn load_policy(mut self, policy: LoadPolicy) -> Self {
101 self.load_policy = policy;
102 self
103 }
104
105 pub fn require_signature(mut self, require: bool) -> Self {
107 self.require_signature = require;
108 self
109 }
110
111 pub fn trusted_keys(mut self, keys: &[VerifyingKey]) -> Self {
113 self.trusted_keys = keys.to_vec();
114 self
115 }
116
117 pub fn interface_hash(mut self, hash: u64) -> Self {
119 self.expected_hash = Some(hash);
120 self
121 }
122
123 pub fn buffer_strategy(mut self, strategy: BufferStrategyKind) -> Self {
125 self.expected_strategy = Some(strategy);
126 self
127 }
128
129 pub fn build(self) -> Result<PluginHost, LoadError> {
131 Ok(PluginHost {
132 search_paths: self.search_paths,
133 load_policy: self.load_policy,
134 require_signature: self.require_signature,
135 trusted_keys: self.trusted_keys,
136 expected_hash: self.expected_hash,
137 expected_strategy: self.expected_strategy,
138 #[cfg(feature = "wasm")]
139 egress: self.egress,
140 })
141 }
142}
143
144impl PluginHost {
145 pub fn builder() -> PluginHostBuilder {
147 PluginHostBuilder::new()
148 }
149
150 pub fn discover(&self) -> Result<Vec<PluginInfo>, LoadError> {
160 #[cfg(feature = "tracing")]
161 tracing::info!(search_paths = ?self.search_paths, "discovering plugins");
162
163 let mut plugins = Vec::new();
164
165 for search_path in &self.search_paths {
166 if !search_path.is_dir() {
167 continue;
168 }
169
170 let entries = std::fs::read_dir(search_path)?;
171 for entry in entries {
172 let entry = entry?;
173 let path = entry.path();
174
175 if is_dylib(&path) {
176 self.discover_cdylib(&path, &mut plugins);
177 } else if path.is_dir() && path.join("package.toml").exists() {
178 self.discover_package(&path, &mut plugins);
179 }
180 }
181 }
182
183 Ok(plugins)
184 }
185
186 fn discover_cdylib(&self, path: &Path, plugins: &mut Vec<PluginInfo>) {
187 if self.require_signature && signing::verify_signature(path, &self.trusted_keys).is_err() {
189 return;
190 }
191
192 let Ok(loaded) = loader::load_library(path) else {
193 return; };
195 for plugin in &loaded.plugins {
196 if loader::validate_against_interface(
197 plugin,
198 self.expected_hash,
199 self.expected_strategy,
200 )
201 .is_ok()
202 {
203 plugins.push(plugin.info.clone());
204 }
205 }
206 }
207
208 fn discover_package(&self, dir: &Path, plugins: &mut Vec<PluginInfo>) {
212 let Ok(manifest) = fidius_core::package::load_manifest_untyped(dir) else {
213 return;
214 };
215 use fidius_core::package::PackageRuntime;
216 let runtime = match manifest.package.runtime() {
217 PackageRuntime::Python => PluginRuntimeKind::Python,
218 PackageRuntime::Wasm => PluginRuntimeKind::Wasm,
219 PackageRuntime::Rust => return,
222 };
223 plugins.push(PluginInfo {
224 name: manifest.package.name.clone(),
225 interface_name: manifest.package.interface.clone(),
226 interface_hash: 0,
230 interface_version: manifest.package.interface_version,
231 capabilities: 0,
232 buffer_strategy: BufferStrategyKind::PluginAllocated,
233 runtime,
234 });
235 }
236
237 pub fn load(&self, name: &str) -> Result<LoadedPlugin, LoadError> {
242 #[cfg(feature = "tracing")]
243 tracing::info!(plugin_name = name, "loading plugin");
244
245 for search_path in &self.search_paths {
246 if !search_path.is_dir() {
247 continue;
248 }
249
250 let entries = std::fs::read_dir(search_path)?;
251 for entry in entries {
252 let entry = entry?;
253 let path = entry.path();
254
255 if !is_dylib(&path) {
256 continue;
257 }
258
259 if self.require_signature {
261 signing::verify_signature(&path, &self.trusted_keys)?;
262 }
263
264 match loader::load_library(&path) {
265 Ok(loaded) => {
266 for plugin in loaded.plugins {
267 if plugin.info.name == name {
268 loader::validate_against_interface(
269 &plugin,
270 self.expected_hash,
271 self.expected_strategy,
272 )?;
273 return Ok(plugin);
274 }
275 }
276 }
277 Err(_) => continue,
278 }
279 }
280 }
281
282 Err(LoadError::PluginNotFound {
283 name: name.to_string(),
284 })
285 }
286
287 pub fn find_python_package(&self, name: &str) -> Result<PathBuf, LoadError> {
291 for search_path in &self.search_paths {
292 if !search_path.is_dir() {
293 continue;
294 }
295 let entries = std::fs::read_dir(search_path)?;
296 for entry in entries {
297 let entry = entry?;
298 let path = entry.path();
299 if !path.is_dir() {
300 continue;
301 }
302 if !path.join("package.toml").exists() {
303 continue;
304 }
305 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
306 continue;
307 };
308 if matches!(
309 manifest.package.runtime(),
310 fidius_core::package::PackageRuntime::Python
311 ) && manifest.package.name == name
312 {
313 return Ok(path);
314 }
315 }
316 }
317 Err(LoadError::PluginNotFound {
318 name: name.to_string(),
319 })
320 }
321
322 #[cfg(feature = "python")]
332 pub fn load_python(
333 &self,
334 name: &str,
335 descriptor: &'static fidius_core::python_descriptor::PythonInterfaceDescriptor,
336 ) -> Result<crate::handle::PluginHandle, LoadError> {
337 let dir = self.find_python_package(name)?;
338 if self.require_signature {
340 signing::verify_package_signature(&dir, &self.trusted_keys)?;
341 }
342 let manifest = fidius_core::package::load_manifest_untyped(&dir)
343 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
344 let py = fidius_python::load_python_plugin(&dir, descriptor)
345 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
346 let info = crate::types::PluginInfo {
350 name: manifest.package.name.clone(),
351 interface_name: descriptor.interface_name.to_string(),
352 interface_hash: descriptor.interface_hash,
353 interface_version: manifest.package.interface_version,
354 capabilities: 0,
355 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
356 runtime: crate::types::PluginRuntimeKind::Python,
357 };
358 Ok(crate::handle::PluginHandle::from_python(py, info))
359 }
360
361 #[cfg(feature = "python")]
367 pub fn load_python_configured<C: serde::Serialize>(
368 &self,
369 name: &str,
370 descriptor: &'static fidius_core::python_descriptor::PythonInterfaceDescriptor,
371 config: &C,
372 ) -> Result<crate::handle::PluginHandle, LoadError> {
373 let dir = self.find_python_package(name)?;
374 if self.require_signature {
375 signing::verify_package_signature(&dir, &self.trusted_keys)?;
376 }
377 let manifest = fidius_core::package::load_manifest_untyped(&dir)
378 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
379 let cfg = serde_json::to_value(config)
380 .map_err(|e| LoadError::PythonLoad(format!("config serialize: {e}")))?;
381 let py = fidius_python::load_python_plugin_configured(&dir, descriptor, &cfg)
382 .map_err(|e| LoadError::PythonLoad(e.to_string()))?;
383 let info = crate::types::PluginInfo {
384 name: manifest.package.name.clone(),
385 interface_name: descriptor.interface_name.to_string(),
386 interface_hash: descriptor.interface_hash,
387 interface_version: manifest.package.interface_version,
388 capabilities: 0,
389 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
390 runtime: crate::types::PluginRuntimeKind::Python,
391 };
392 Ok(crate::handle::PluginHandle::from_python(py, info))
393 }
394
395 #[cfg(feature = "wasm")]
398 pub fn find_wasm_package(&self, name: &str) -> Result<PathBuf, LoadError> {
399 for search_path in &self.search_paths {
400 if !search_path.is_dir() {
401 continue;
402 }
403 for entry in std::fs::read_dir(search_path)? {
404 let entry = entry?;
405 let path = entry.path();
406 if !path.is_dir() || !path.join("package.toml").exists() {
407 continue;
408 }
409 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
410 continue;
411 };
412 if matches!(
413 manifest.package.runtime(),
414 fidius_core::package::PackageRuntime::Wasm
415 ) && manifest.package.name == name
416 {
417 return Ok(path);
418 }
419 }
420 }
421 Err(LoadError::PluginNotFound {
422 name: name.to_string(),
423 })
424 }
425
426 #[cfg(feature = "wasm")]
441 pub fn load_wasm(
442 &self,
443 name: &str,
444 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
445 ) -> Result<crate::handle::PluginHandle, LoadError> {
446 self.load_wasm_impl(name, descriptor, self.egress.clone(), None)
447 }
448
449 #[cfg(feature = "wasm")]
455 pub fn load_wasm_configured<C: serde::Serialize>(
456 &self,
457 name: &str,
458 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
459 config: &C,
460 ) -> Result<crate::handle::PluginHandle, LoadError> {
461 let cfg = fidius_core::wire::serialize(config)
462 .map_err(|e| LoadError::WasmLoad(format!("config serialize: {e}")))?;
463 self.load_wasm_impl(name, descriptor, self.egress.clone(), Some(&cfg))
464 }
465
466 #[cfg(feature = "wasm")]
472 pub fn load_wasm_with_egress(
473 &self,
474 name: &str,
475 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
476 egress: impl crate::executor::wasm::EgressPolicy,
477 ) -> Result<crate::handle::PluginHandle, LoadError> {
478 self.load_wasm_impl(name, descriptor, Some(Arc::new(egress)), None)
479 }
480
481 #[cfg(feature = "wasm")]
482 fn load_wasm_impl(
483 &self,
484 name: &str,
485 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
486 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
487 config: Option<&[u8]>,
488 ) -> Result<crate::handle::PluginHandle, LoadError> {
489 use crate::executor::wasm::{WasmComponentExecutor, WasmMethod};
490
491 let dir = self.find_wasm_package(name)?;
492 if self.require_signature {
494 signing::verify_package_signature(&dir, &self.trusted_keys)?;
495 }
496 let manifest = fidius_core::package::load_manifest_untyped(&dir)
497 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
498 let wasm_meta = manifest
499 .wasm
500 .as_ref()
501 .ok_or_else(|| LoadError::WasmLoad("manifest is missing the [wasm] section".into()))?;
502
503 let methods: Vec<WasmMethod> = descriptor
504 .methods
505 .iter()
506 .map(|m| WasmMethod {
507 name: m.name.to_string(),
508 wire_raw: m.wire_raw,
509 streaming: m.streaming,
510 })
511 .collect();
512 let info = crate::types::PluginInfo {
513 name: manifest.package.name.clone(),
514 interface_name: descriptor.interface_name.to_string(),
515 interface_hash: descriptor.interface_hash,
516 interface_version: manifest.package.interface_version,
517 capabilities: 0,
518 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
519 runtime: crate::types::PluginRuntimeKind::Wasm,
520 };
521 let interface = descriptor.interface_export.to_string();
522 let capabilities = wasm_meta.capabilities.clone();
523
524 let cwasm_path = wasm_meta
530 .precompiled
531 .as_ref()
532 .map(|p| dir.join(p))
533 .or_else(|| {
534 let sibling = dir.join(&wasm_meta.component).with_extension("cwasm");
535 sibling.exists().then_some(sibling)
536 });
537
538 let jit = |interface: String, methods, capabilities, info| -> Result<_, LoadError> {
539 let bytes = std::fs::read(dir.join(&wasm_meta.component))?;
540 WasmComponentExecutor::from_component_bytes_with_egress(
541 &bytes,
542 interface,
543 methods,
544 capabilities,
545 egress.clone(),
546 info,
547 )
548 .map_err(|e| LoadError::WasmLoad(e.to_string()))
549 };
550
551 let executor = match cwasm_path {
552 Some(cwasm) if cwasm.exists() => {
553 let bytes = std::fs::read(&cwasm)?;
554 let aot = unsafe {
558 WasmComponentExecutor::from_cwasm_with_egress(
559 &bytes,
560 interface.clone(),
561 methods.clone(),
562 capabilities.clone(),
563 egress.clone(),
564 info.clone(),
565 )
566 };
567 match aot {
568 Ok(e) => e,
569 Err(_err) => {
570 #[cfg(feature = "tracing")]
571 tracing::warn!(
572 cwasm = %cwasm.display(),
573 error = %_err,
574 "precompiled .cwasm rejected (likely engine/version mismatch); falling back to JIT"
575 );
576 jit(interface, methods, capabilities, info)?
577 }
578 }
579 }
580 _ => jit(interface, methods, capabilities, info)?,
581 };
582
583 let got = executor
585 .interface_hash()
586 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
587 if got != descriptor.interface_hash {
588 return Err(LoadError::InterfaceHashMismatch {
589 got,
590 expected: descriptor.interface_hash,
591 });
592 }
593
594 let mut executor = executor;
597 if let Some(cfg) = config {
598 executor
599 .configure(cfg)
600 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
601 }
602 Ok(crate::handle::PluginHandle::from_wasm(executor))
603 }
604}
605
606fn is_dylib(path: &Path) -> bool {
608 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
609 if cfg!(target_os = "macos") {
610 ext == "dylib"
611 } else if cfg!(target_os = "windows") {
612 ext == "dll"
613 } else {
614 ext == "so"
615 }
616}