use std::path::Path;
use std::sync::Mutex;
use anyhow::Result;
use wasmtime::component::{Component, Linker};
use wasmtime::Store;
use super::{wasm_engine, GrantedCapabilities, PluginManifest};
use crate::mother::{ChildHealth, ChildRequest, ChildResponse, MotherHost, Toy};
use super::command::QueryDispatchFn;
mod bindings {
pub struct HostState {
pub plugin_name: String,
pub wasi: wasmtime_wasi::WasiCtx,
pub wasi_table: wasmtime::component::ResourceTable,
pub project_root: Option<std::path::PathBuf>,
pub grants: super::GrantedCapabilities,
pub query_fn: Option<super::QueryDispatchFn>,
pub http_client: reqwest::blocking::Client,
}
impl wasmtime_wasi::WasiView for HostState {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.wasi_table,
}
}
}
wasmtime::component::bindgen!({
path: "wit/mother-child/",
world: "mother-child",
});
impl patina::host::log::Host for HostState {
fn log(&mut self, level: patina::host::log::LogLevel, message: String) {
let level_str = match level {
patina::host::log::LogLevel::Debug => "DEBUG",
patina::host::log::LogLevel::Info => "INFO",
patina::host::log::LogLevel::Warn => "WARN",
patina::host::log::LogLevel::Error => "ERROR",
};
super::super::host_support::log(&self.plugin_name, level_str, &message);
}
}
impl patina::host::types::Host for HostState {}
impl patina::host::layer::Host for HostState {
fn find_project_root(&mut self) -> Option<String> {
super::super::host_support::find_project_root(&self.project_root)
}
fn read_config(&mut self) -> Result<String, String> {
super::super::host_support::read_config(&self.project_root)
}
fn detect_environment(&mut self) -> Result<String, String> {
super::super::host_support::detect_environment()
}
fn get_stored_tools(&mut self) -> Vec<String> {
super::super::host_support::get_stored_tools(&self.project_root)
}
fn count_layer_files(&mut self, subdir: String) -> u32 {
super::super::host_support::count_layer_files(&self.project_root, &subdir)
}
fn get_project_uid(&mut self) -> Option<String> {
super::super::host_support::get_project_uid(&self.project_root)
}
fn check_adapter_version(
&mut self,
adapter_name: String,
) -> Result<Option<String>, String> {
super::super::host_support::check_adapter_version(&self.project_root, &adapter_name)
}
}
impl patina::host::query::Host for HostState {
fn query(&mut self, kind: String, params: String) -> Result<String, String> {
super::super::host_support::query(
&self.plugin_name,
&self.grants,
&mut self.query_fn,
&kind,
¶ms,
)
}
}
impl patina::host::http::Host for HostState {
fn http_post(
&mut self,
url: String,
body: String,
content_type: String,
) -> Result<patina::host::http::HttpResponse, String> {
let r = super::super::host_support::http_post(
&self.http_client,
&self.grants,
&self.plugin_name,
&url,
&body,
&content_type,
)?;
Ok(patina::host::http::HttpResponse {
status: r.status,
body: r.body,
})
}
fn http_get(&mut self, url: String) -> Result<patina::host::http::HttpResponse, String> {
let r = super::super::host_support::http_get(
&self.http_client,
&self.grants,
&self.plugin_name,
&url,
)?;
Ok(patina::host::http::HttpResponse {
status: r.status,
body: r.body,
})
}
}
}
use bindings::HostState;
pub struct PluginEngine {
linker: Linker<HostState>,
}
impl PluginEngine {
pub fn new() -> Result<Self> {
let mut linker = Linker::new(wasm_engine());
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
bindings::MotherChild::add_to_linker::<HostState, wasmtime::component::HasSelf<HostState>>(
&mut linker,
|s| s,
)?;
Ok(Self { linker })
}
pub fn load_manifest(path: &Path) -> Result<PluginManifest> {
PluginManifest::from_path(path)
}
pub fn load_component(&self, wasm: &[u8]) -> Result<Component> {
PluginManifest::load_component(wasm)
}
pub fn check_capabilities(manifest: &PluginManifest) -> Result<()> {
let allowed = manifest.world.allowed_capabilities();
let world_denied: Vec<&str> = manifest
.capabilities
.iter()
.filter(|cap| !allowed.contains(&cap.as_str()))
.map(|s| s.as_str())
.collect();
if !world_denied.is_empty() {
anyhow::bail!(
"plugin '{}' (world '{}') requests capabilities not allowed for this world: {}",
manifest.name,
manifest.world,
world_denied.join(", ")
);
}
let auto_granted = ["host_log", "host_layer"];
let denied: Vec<&str> = manifest
.capabilities
.iter()
.filter(|cap| !auto_granted.contains(&cap.as_str()))
.map(|s| s.as_str())
.collect();
if !denied.is_empty() {
anyhow::bail!(
"plugin '{}' requests capabilities not granted: {}",
manifest.name,
denied.join(", ")
);
}
const KNOWN_QUERY_KINDS: &[&str] = &["scry", "context", "assay"];
let unknown: Vec<&str> = manifest
.host_query_kinds
.iter()
.filter(|k| !KNOWN_QUERY_KINDS.contains(&k.as_str()))
.map(|s| s.as_str())
.collect();
if !unknown.is_empty() {
anyhow::bail!(
"plugin '{}' requests unknown query kinds: {}",
manifest.name,
unknown.join(", ")
);
}
for domain in &manifest.host_http_domains {
if domain.is_empty() {
anyhow::bail!(
"plugin '{}' has empty HTTP domain in host_http",
manifest.name
);
}
if !domain.is_ascii() {
anyhow::bail!(
"plugin '{}' has non-ASCII HTTP domain '{}' in host_http",
manifest.name,
domain
);
}
if domain.contains('/') {
anyhow::bail!(
"plugin '{}' has path component in HTTP domain '{}' in host_http",
manifest.name,
domain
);
}
}
Ok(())
}
pub fn instantiate_child(
&self,
component: &Component,
manifest: &PluginManifest,
query_fn: Option<QueryDispatchFn>,
) -> Result<Box<dyn crate::mother::MotherChild>> {
Self::check_capabilities(manifest)?;
let grants = manifest.granted_capabilities();
let http_client = super::host_support::build_http_client()?;
let wasi = wasmtime_wasi::WasiCtxBuilder::new().build();
let project_root = crate::session::SessionManager::find_project_root().ok();
let host_state = HostState {
plugin_name: manifest.name.clone(),
wasi,
wasi_table: wasmtime::component::ResourceTable::new(),
project_root,
grants,
query_fn,
http_client,
};
let mut store = Store::new(wasm_engine(), host_state);
let instance = bindings::MotherChild::instantiate(&mut store, component, &self.linker)?;
instance.call_init(&mut store)?;
let name = instance.call_name(&mut store)?;
Ok(Box::new(WasmChild {
name,
allowed_toy_commands: manifest.allowed_toy_commands.clone(),
inner: Mutex::new(WasmChildInner { store, instance }),
}))
}
}
struct WasmChild {
name: String,
allowed_toy_commands: Vec<String>,
inner: Mutex<WasmChildInner>,
}
struct WasmChildInner {
store: Store<HostState>,
instance: bindings::MotherChild,
}
impl crate::mother::MotherChild for WasmChild {
fn name(&self) -> &str {
&self.name
}
fn on_load(&mut self, _host: &dyn MotherHost) -> Result<()> {
let mut inner = self.inner.lock().unwrap_or_else(|e| {
eprintln!(
"[plugin:{}] WARN: mutex was poisoned, recovering. Previous call may have panicked.",
self.name
);
e.into_inner()
});
let WasmChildInner { store, instance } = &mut *inner;
match instance.call_on_load(store)? {
Ok(()) => Ok(()),
Err(e) => Err(anyhow::anyhow!("WASM on_load failed: {}", e)),
}
}
fn on_unload(&mut self) {
let mut inner = self.inner.lock().unwrap_or_else(|e| {
eprintln!(
"[plugin:{}] WARN: mutex was poisoned, recovering. Previous call may have panicked.",
self.name
);
e.into_inner()
});
let WasmChildInner { store, instance } = &mut *inner;
let _ = instance.call_on_unload(store);
}
fn health(&self) -> ChildHealth {
let mut inner = self.inner.lock().unwrap_or_else(|e| {
eprintln!(
"[plugin:{}] WARN: mutex was poisoned, recovering. Previous call may have panicked.",
self.name
);
e.into_inner()
});
let WasmChildInner { store, instance } = &mut *inner;
match instance.call_health(store) {
Ok(h) => {
let reason = h.reason.unwrap_or_default();
match h.status {
bindings::patina::host::types::HealthStatus::Healthy => ChildHealth::Healthy,
bindings::patina::host::types::HealthStatus::Degraded => {
ChildHealth::Degraded(if reason.is_empty() {
"degraded".into()
} else {
reason
})
}
bindings::patina::host::types::HealthStatus::Unhealthy => {
ChildHealth::Unhealthy(if reason.is_empty() {
"unhealthy".into()
} else {
reason
})
}
}
}
Err(e) => ChildHealth::Unhealthy(format!("WASM call failed: {}", e)),
}
}
fn handle(&self, request: &ChildRequest) -> Result<ChildResponse> {
let mut inner = self.inner.lock().unwrap_or_else(|e| {
eprintln!(
"[plugin:{}] WARN: mutex was poisoned, recovering. Previous call may have panicked.",
self.name
);
e.into_inner()
});
let WasmChildInner { store, instance } = &mut *inner;
let payload_json = serde_json::to_string(&request.payload)?;
let result = instance.call_handle(store, &request.action, &payload_json)?;
match result {
Ok(json) => Ok(ChildResponse {
payload: serde_json::from_str(&json)?,
}),
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}
fn tick(&mut self) -> Vec<Toy> {
let mut inner = self.inner.lock().unwrap_or_else(|e| {
eprintln!(
"[plugin:{}] WARN: mutex was poisoned, recovering. Previous call may have panicked.",
self.name
);
e.into_inner()
});
let WasmChildInner { store, instance } = &mut *inner;
match instance.call_tick(store) {
Ok(wasm_toys) => wasm_toys
.into_iter()
.filter_map(|t| {
let toy = Toy {
name: t.name,
command: t.command,
args: t.args,
};
if self.allowed_toy_commands.contains(&toy.command) {
Some(toy)
} else {
eprintln!(
"[plugin:{}] toy '{}' denied: command '{}' not in allowed list {:?}",
self.name, toy.name, toy.command, self.allowed_toy_commands
);
None
}
})
.collect(),
Err(e) => {
eprintln!("[plugin:{}] tick failed: {}", self.name, e);
vec![]
}
}
}
}