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, 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), None)
464 }
465
466 #[cfg(feature = "wasm")]
487 pub fn load_wasm_configured_with_grants<C: serde::Serialize>(
488 &self,
489 name: &str,
490 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
491 config: &C,
492 capabilities: Vec<String>,
493 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
494 ) -> Result<crate::handle::PluginHandle, LoadError> {
495 let cfg = fidius_core::wire::serialize(config)
496 .map_err(|e| LoadError::WasmLoad(format!("config serialize: {e}")))?;
497 self.load_wasm_impl(name, descriptor, egress, Some(&cfg), Some(capabilities))
498 }
499
500 #[cfg(feature = "wasm")]
506 pub fn load_wasm_with_egress(
507 &self,
508 name: &str,
509 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
510 egress: impl crate::executor::wasm::EgressPolicy,
511 ) -> Result<crate::handle::PluginHandle, LoadError> {
512 self.load_wasm_impl(name, descriptor, Some(Arc::new(egress)), None, None)
513 }
514
515 #[cfg(feature = "wasm")]
516 fn load_wasm_impl(
517 &self,
518 name: &str,
519 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
520 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
521 config: Option<&[u8]>,
522 caps_override: Option<Vec<String>>,
523 ) -> Result<crate::handle::PluginHandle, LoadError> {
524 use crate::executor::wasm::{WasmComponentExecutor, WasmMethod};
525
526 let dir = self.find_wasm_package(name)?;
527 if self.require_signature {
529 signing::verify_package_signature(&dir, &self.trusted_keys)?;
530 }
531 let manifest = fidius_core::package::load_manifest_untyped(&dir)
532 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
533 let wasm_meta = manifest
534 .wasm
535 .as_ref()
536 .ok_or_else(|| LoadError::WasmLoad("manifest is missing the [wasm] section".into()))?;
537
538 let methods: Vec<WasmMethod> = descriptor
539 .methods
540 .iter()
541 .map(|m| WasmMethod {
542 name: m.name.to_string(),
543 wire_raw: m.wire_raw,
544 streaming: m.streaming,
545 })
546 .collect();
547 let info = crate::types::PluginInfo {
548 name: manifest.package.name.clone(),
549 interface_name: descriptor.interface_name.to_string(),
550 interface_hash: descriptor.interface_hash,
551 interface_version: manifest.package.interface_version,
552 capabilities: 0,
553 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
554 runtime: crate::types::PluginRuntimeKind::Wasm,
555 };
556 let interface = descriptor.interface_export.to_string();
557 let capabilities = caps_override.unwrap_or_else(|| wasm_meta.capabilities.clone());
561
562 let cwasm_path = wasm_meta
568 .precompiled
569 .as_ref()
570 .map(|p| dir.join(p))
571 .or_else(|| {
572 let sibling = dir.join(&wasm_meta.component).with_extension("cwasm");
573 sibling.exists().then_some(sibling)
574 });
575
576 let jit = |interface: String, methods, capabilities, info| -> Result<_, LoadError> {
577 let bytes = std::fs::read(dir.join(&wasm_meta.component))?;
578 WasmComponentExecutor::from_component_bytes_with_egress(
579 &bytes,
580 interface,
581 methods,
582 capabilities,
583 egress.clone(),
584 info,
585 )
586 .map_err(|e| LoadError::WasmLoad(e.to_string()))
587 };
588
589 let executor = match cwasm_path {
590 Some(cwasm) if cwasm.exists() => {
591 let bytes = std::fs::read(&cwasm)?;
592 let aot = unsafe {
596 WasmComponentExecutor::from_cwasm_with_egress(
597 &bytes,
598 interface.clone(),
599 methods.clone(),
600 capabilities.clone(),
601 egress.clone(),
602 info.clone(),
603 )
604 };
605 match aot {
606 Ok(e) => e,
607 Err(_err) => {
608 #[cfg(feature = "tracing")]
609 tracing::warn!(
610 cwasm = %cwasm.display(),
611 error = %_err,
612 "precompiled .cwasm rejected (likely engine/version mismatch); falling back to JIT"
613 );
614 jit(interface, methods, capabilities, info)?
615 }
616 }
617 }
618 _ => jit(interface, methods, capabilities, info)?,
619 };
620
621 let got = executor
623 .interface_hash()
624 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
625 if got != descriptor.interface_hash {
626 return Err(LoadError::InterfaceHashMismatch {
627 got,
628 expected: descriptor.interface_hash,
629 });
630 }
631
632 let mut executor = executor;
635 if let Some(cfg) = config {
636 executor
637 .configure(cfg)
638 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
639 }
640 Ok(crate::handle::PluginHandle::from_wasm(executor))
641 }
642}
643
644fn is_dylib(path: &Path) -> bool {
646 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
647 if cfg!(target_os = "macos") {
648 ext == "dylib"
649 } else if cfg!(target_os = "windows") {
650 ext == "dll"
651 } else {
652 ext == "so"
653 }
654}