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 = "wasm")]
364 pub fn find_wasm_package(&self, name: &str) -> Result<PathBuf, LoadError> {
365 for search_path in &self.search_paths {
366 if !search_path.is_dir() {
367 continue;
368 }
369 for entry in std::fs::read_dir(search_path)? {
370 let entry = entry?;
371 let path = entry.path();
372 if !path.is_dir() || !path.join("package.toml").exists() {
373 continue;
374 }
375 let Ok(manifest) = fidius_core::package::load_manifest_untyped(&path) else {
376 continue;
377 };
378 if matches!(
379 manifest.package.runtime(),
380 fidius_core::package::PackageRuntime::Wasm
381 ) && manifest.package.name == name
382 {
383 return Ok(path);
384 }
385 }
386 }
387 Err(LoadError::PluginNotFound {
388 name: name.to_string(),
389 })
390 }
391
392 #[cfg(feature = "wasm")]
407 pub fn load_wasm(
408 &self,
409 name: &str,
410 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
411 ) -> Result<crate::handle::PluginHandle, LoadError> {
412 self.load_wasm_impl(name, descriptor, self.egress.clone())
413 }
414
415 #[cfg(feature = "wasm")]
421 pub fn load_wasm_with_egress(
422 &self,
423 name: &str,
424 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
425 egress: impl crate::executor::wasm::EgressPolicy,
426 ) -> Result<crate::handle::PluginHandle, LoadError> {
427 self.load_wasm_impl(name, descriptor, Some(Arc::new(egress)))
428 }
429
430 #[cfg(feature = "wasm")]
431 fn load_wasm_impl(
432 &self,
433 name: &str,
434 descriptor: &'static fidius_core::wasm_descriptor::WasmInterfaceDescriptor,
435 egress: Option<Arc<dyn crate::executor::wasm::EgressPolicy>>,
436 ) -> Result<crate::handle::PluginHandle, LoadError> {
437 use crate::executor::wasm::{WasmComponentExecutor, WasmMethod};
438
439 let dir = self.find_wasm_package(name)?;
440 if self.require_signature {
442 signing::verify_package_signature(&dir, &self.trusted_keys)?;
443 }
444 let manifest = fidius_core::package::load_manifest_untyped(&dir)
445 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
446 let wasm_meta = manifest
447 .wasm
448 .as_ref()
449 .ok_or_else(|| LoadError::WasmLoad("manifest is missing the [wasm] section".into()))?;
450
451 let methods: Vec<WasmMethod> = descriptor
452 .methods
453 .iter()
454 .map(|m| WasmMethod {
455 name: m.name.to_string(),
456 wire_raw: m.wire_raw,
457 streaming: m.streaming,
458 })
459 .collect();
460 let info = crate::types::PluginInfo {
461 name: manifest.package.name.clone(),
462 interface_name: descriptor.interface_name.to_string(),
463 interface_hash: descriptor.interface_hash,
464 interface_version: manifest.package.interface_version,
465 capabilities: 0,
466 buffer_strategy: fidius_core::descriptor::BufferStrategyKind::PluginAllocated,
467 runtime: crate::types::PluginRuntimeKind::Wasm,
468 };
469 let interface = descriptor.interface_export.to_string();
470 let capabilities = wasm_meta.capabilities.clone();
471
472 let cwasm_path = wasm_meta
478 .precompiled
479 .as_ref()
480 .map(|p| dir.join(p))
481 .or_else(|| {
482 let sibling = dir.join(&wasm_meta.component).with_extension("cwasm");
483 sibling.exists().then_some(sibling)
484 });
485
486 let jit = |interface: String, methods, capabilities, info| -> Result<_, LoadError> {
487 let bytes = std::fs::read(dir.join(&wasm_meta.component))?;
488 WasmComponentExecutor::from_component_bytes_with_egress(
489 &bytes,
490 interface,
491 methods,
492 capabilities,
493 egress.clone(),
494 info,
495 )
496 .map_err(|e| LoadError::WasmLoad(e.to_string()))
497 };
498
499 let executor = match cwasm_path {
500 Some(cwasm) if cwasm.exists() => {
501 let bytes = std::fs::read(&cwasm)?;
502 let aot = unsafe {
506 WasmComponentExecutor::from_cwasm_with_egress(
507 &bytes,
508 interface.clone(),
509 methods.clone(),
510 capabilities.clone(),
511 egress.clone(),
512 info.clone(),
513 )
514 };
515 match aot {
516 Ok(e) => e,
517 Err(_err) => {
518 #[cfg(feature = "tracing")]
519 tracing::warn!(
520 cwasm = %cwasm.display(),
521 error = %_err,
522 "precompiled .cwasm rejected (likely engine/version mismatch); falling back to JIT"
523 );
524 jit(interface, methods, capabilities, info)?
525 }
526 }
527 }
528 _ => jit(interface, methods, capabilities, info)?,
529 };
530
531 let got = executor
533 .interface_hash()
534 .map_err(|e| LoadError::WasmLoad(e.to_string()))?;
535 if got != descriptor.interface_hash {
536 return Err(LoadError::InterfaceHashMismatch {
537 got,
538 expected: descriptor.interface_hash,
539 });
540 }
541
542 Ok(crate::handle::PluginHandle::from_wasm(executor))
543 }
544}
545
546fn is_dylib(path: &Path) -> bool {
548 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
549 if cfg!(target_os = "macos") {
550 ext == "dylib"
551 } else if cfg!(target_os = "windows") {
552 ext == "dll"
553 } else {
554 ext == "so"
555 }
556}