use anyhow::Result;
use wasmtime::component::{Component, Linker};
use wasmtime::Store;
use super::mother_child::PluginEngine;
use super::{wasm_engine, GrantedCapabilities, PluginManifest};
use crate::mother::Toy;
use super::command::QueryDispatchFn;
mod task_bindings {
pub struct TaskHostState {
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 TaskHostState {
fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> {
wasmtime_wasi::WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.wasi_table,
}
}
}
wasmtime::component::bindgen!({
path: "wit/task/",
world: "task",
});
impl patina::host::log::Host for TaskHostState {
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 TaskHostState {}
impl patina::host::layer::Host for TaskHostState {
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 TaskHostState {
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 TaskHostState {
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,
})
}
}
}
pub struct TaskEngine {
linker: Linker<task_bindings::TaskHostState>,
}
impl TaskEngine {
pub fn new() -> Result<Self> {
let mut linker = Linker::new(wasm_engine());
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
task_bindings::Task::add_to_linker::<
task_bindings::TaskHostState,
wasmtime::component::HasSelf<task_bindings::TaskHostState>,
>(&mut linker, |s| s)?;
Ok(Self { linker })
}
pub fn load_component(&self, wasm: &[u8]) -> Result<Component> {
Component::new(wasm_engine(), wasm)
}
pub fn run_task(
&self,
component: &Component,
manifest: &PluginManifest,
args: &[String],
query_fn: Option<QueryDispatchFn>,
) -> Result<(i32, Vec<Toy>)> {
PluginEngine::check_capabilities(manifest)?;
let http_client = super::host_support::build_http_client()?;
let wasi = wasmtime_wasi::WasiCtxBuilder::new()
.inherit_stdout()
.inherit_stderr()
.build();
let project_root = crate::session::SessionManager::find_project_root().ok();
let grants = manifest.granted_capabilities();
let host_state = task_bindings::TaskHostState {
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 = task_bindings::Task::instantiate(&mut store, component, &self.linker)?;
instance.call_init(&mut store)?;
let exit_code = instance.call_run(&mut store, args)?;
let wasm_toys = instance.call_toys(&mut store)?;
let filtered_toys = wasm_toys
.into_iter()
.filter_map(|t| {
let toy = Toy {
name: t.name,
command: t.command,
args: t.args,
};
if manifest.allowed_toy_commands.contains(&toy.command) {
Some(toy)
} else {
eprintln!(
"[plugin:{}] toy '{}' denied: command '{}' not in allowed list {:?}",
manifest.name, toy.name, toy.command, manifest.allowed_toy_commands
);
None
}
})
.collect();
Ok((exit_code, filtered_toys))
}
pub fn get_task_name(&self, component: &Component) -> Result<String> {
let host_state = Self::probe_host_state();
let mut store = Store::new(wasm_engine(), host_state);
let instance = task_bindings::Task::instantiate(&mut store, component, &self.linker)?;
instance.call_init(&mut store)?;
instance.call_name(&mut store)
}
pub fn get_task_description(&self, component: &Component) -> Result<String> {
let host_state = Self::probe_host_state();
let mut store = Store::new(wasm_engine(), host_state);
let instance = task_bindings::Task::instantiate(&mut store, component, &self.linker)?;
instance.call_init(&mut store)?;
instance.call_description(&mut store)
}
fn probe_host_state() -> task_bindings::TaskHostState {
let wasi = wasmtime_wasi::WasiCtxBuilder::new().build();
let project_root = crate::session::SessionManager::find_project_root().ok();
task_bindings::TaskHostState {
plugin_name: "probe".to_string(),
wasi,
wasi_table: wasmtime::component::ResourceTable::new(),
project_root,
grants: GrantedCapabilities::default(),
query_fn: None,
http_client: reqwest::blocking::Client::new(),
}
}
}