use std::path::Path;
use std::sync::OnceLock;
use anyhow::Result;
use wasmtime::component::Component;
use wasmtime::{Config, Engine};
mod command;
mod host_support;
mod mother_child;
mod pipeline;
mod task;
#[cfg(test)]
mod tests;
pub use command::{CommandEngine, QueryDispatchFn};
pub use mother_child::PluginEngine;
pub use pipeline::PipelineEngine;
pub use task::TaskEngine;
#[derive(Debug, Clone, PartialEq)]
pub enum PluginWorld {
MotherChild,
Command,
Task,
Pipeline,
}
impl std::str::FromStr for PluginWorld {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"mother-child" => Ok(Self::MotherChild),
"command" => Ok(Self::Command),
"task" => Ok(Self::Task),
"pipeline" => Ok(Self::Pipeline),
other => anyhow::bail!("unknown plugin world: '{}'", other),
}
}
}
impl PluginWorld {
pub fn allowed_capabilities(&self) -> &[&str] {
match self {
Self::MotherChild => &["host_log", "host_layer", "host_query", "host_http"],
Self::Command => &["host_log", "host_layer", "host_query"],
Self::Task => &["host_log", "host_layer", "host_query", "host_http"],
Self::Pipeline => &["host_log"],
}
}
}
impl std::fmt::Display for PluginWorld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MotherChild => write!(f, "mother-child"),
Self::Command => write!(f, "command"),
Self::Task => write!(f, "task"),
Self::Pipeline => write!(f, "pipeline"),
}
}
}
pub(super) fn wasm_engine() -> &'static Engine {
static ENGINE: OnceLock<Engine> = OnceLock::new();
ENGINE.get_or_init(|| {
let mut config = Config::new();
config.wasm_component_model(true);
Engine::new(&config).expect("failed to create wasmtime engine")
})
}
#[derive(Debug, Clone)]
pub struct PluginManifest {
pub name: String,
pub version: String,
pub description: String,
pub world: PluginWorld,
pub patina_min: String,
pub capabilities: Vec<String>,
pub allowed_toy_commands: Vec<String>,
pub host_query_kinds: Vec<String>,
pub host_http_domains: Vec<String>,
pub provides: PluginProvides,
}
#[derive(Debug, Clone, Default)]
pub enum QueryScope {
#[default]
CurrentProject,
AllRepos,
}
#[derive(Debug, Clone, Default)]
pub struct GrantedCapabilities {
pub query_kinds: std::collections::HashSet<String>,
pub query_scope: QueryScope,
pub http_domains: std::collections::HashSet<String>,
}
#[derive(Debug, Default, Clone)]
pub struct PluginProvides {
pub child: Option<String>,
pub commands: Vec<String>,
pub pipeline_ops: Vec<String>,
pub languages: Vec<String>,
}
impl PluginManifest {
pub(super) fn from_path(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let table: toml::Table = content.parse()?;
let plugin = table
.get("plugin")
.and_then(|v| v.as_table())
.ok_or_else(|| anyhow::anyhow!("missing [plugin] section"))?;
let name = plugin
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing plugin.name"))?
.to_string();
let version = plugin
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0")
.to_string();
let description = plugin
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let world_str = plugin
.get("world")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing plugin.world"))?;
let world = world_str.parse::<PluginWorld>()?;
let patina_min = plugin
.get("patina_min")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0")
.to_string();
let cap_table = table.get("capabilities").and_then(|v| v.as_table());
let capabilities = cap_table
.map(|cap| {
cap.iter()
.filter(|(_, v)| v.as_bool() == Some(true))
.map(|(k, _)| k.clone())
.collect()
})
.unwrap_or_default();
let allowed_toy_commands = cap_table
.and_then(|cap| cap.get("toys"))
.and_then(|v| v.as_table())
.and_then(|toys| toys.get("commands"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let host_query_kinds = cap_table
.and_then(|cap| cap.get("host_query"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let host_http_domains = cap_table
.and_then(|cap| cap.get("host_http"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let provides_table = table.get("provides").and_then(|v| v.as_table());
let child = provides_table
.and_then(|p| p.get("child"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let commands = provides_table
.and_then(|p| p.get("commands"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let pipeline_ops = provides_table
.and_then(|p| p.get("pipeline_ops"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let languages = provides_table
.and_then(|p| p.get("languages"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
Ok(Self {
name,
version,
description,
world,
patina_min,
capabilities,
allowed_toy_commands,
host_query_kinds,
host_http_domains,
provides: PluginProvides {
child,
commands,
pipeline_ops,
languages,
},
})
}
pub(super) fn load_component(wasm: &[u8]) -> Result<Component> {
Component::new(wasm_engine(), wasm)
}
pub fn granted_capabilities(&self) -> GrantedCapabilities {
let query_kinds = self.host_query_kinds.iter().cloned().collect();
let http_domains = self.host_http_domains.iter().cloned().collect();
let query_scope = QueryScope::CurrentProject;
GrantedCapabilities {
query_kinds,
query_scope,
http_domains,
}
}
}