refactor(plugins): add validation profiles with strict runtime defaults

This commit is contained in:
xj 2026-03-01 17:04:17 -08:00
parent c2380f8457
commit 362a81a3e5

View File

@ -15,6 +15,17 @@ const SUPPORTED_WIT_MAJOR: u64 = 1;
const SUPPORTED_WIT_PACKAGES: [&str; 3] =
["zeroclaw:hooks", "zeroclaw:tools", "zeroclaw:providers"];
/// Validation profile for plugin manifests.
///
/// Runtime uses `RuntimeWasm` today (strict; requires module path).
/// `SchemaOnly` exists so future non-WASM plugin forms can validate metadata
/// without forcing a fake module path.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ManifestValidationProfile {
RuntimeWasm,
SchemaOnly,
}
/// Filename plugins must use for their manifest.
pub const PLUGIN_MANIFEST_FILENAME: &str = "zeroclaw.plugin.toml";
@ -49,7 +60,8 @@ pub struct PluginManifest {
/// Declared capability set for this plugin.
#[serde(default)]
pub capabilities: Vec<PluginCapability>,
/// Optional module path used by WASM-oriented plugin runtimes.
/// WASM module path used by runtime execution.
/// Required in runtime validation; optional in schema-only validation.
#[serde(default)]
pub module_path: String,
/// Declared WIT package contracts the plugin expects.
@ -138,7 +150,10 @@ fn required_wit_package_for_capability(capability: &PluginCapability) -> &'stati
}
}
pub fn validate_manifest(manifest: &PluginManifest) -> anyhow::Result<()> {
pub fn validate_manifest_with_profile(
manifest: &PluginManifest,
profile: ManifestValidationProfile,
) -> anyhow::Result<()> {
if manifest.id.trim().is_empty() {
anyhow::bail!("plugin id cannot be empty");
}
@ -147,7 +162,9 @@ pub fn validate_manifest(manifest: &PluginManifest) -> anyhow::Result<()> {
anyhow::bail!("plugin version cannot be empty");
}
}
if manifest.module_path.trim().is_empty() {
if matches!(profile, ManifestValidationProfile::RuntimeWasm)
&& manifest.module_path.trim().is_empty()
{
anyhow::bail!("plugin module_path cannot be empty");
}
let mut declared_wit_packages = HashSet::new();
@ -204,6 +221,10 @@ pub fn validate_manifest(manifest: &PluginManifest) -> anyhow::Result<()> {
Ok(())
}
pub fn validate_manifest(manifest: &PluginManifest) -> anyhow::Result<()> {
validate_manifest_with_profile(manifest, ManifestValidationProfile::RuntimeWasm)
}
impl PluginManifest {
pub fn is_valid(&self) -> bool {
validate_manifest(self).is_ok()
@ -343,6 +364,27 @@ id = " "
assert!(validate_manifest(&manifest).is_err());
}
#[test]
fn schema_only_validation_allows_empty_module_path() {
let manifest = PluginManifest {
id: "demo".into(),
name: None,
description: None,
version: Some("1.0.0".into()),
config_schema: None,
capabilities: vec![],
module_path: " ".into(),
wit_packages: vec![],
tools: vec![],
providers: vec![],
};
assert!(validate_manifest_with_profile(
&manifest,
ManifestValidationProfile::SchemaOnly
)
.is_ok());
}
#[test]
fn manifest_rejects_capability_without_matching_wit_package() {
let manifest = PluginManifest {
@ -400,4 +442,21 @@ id = " "
};
assert!(validate_manifest(&manifest).is_err());
}
#[test]
fn manifest_rejects_providers_without_providers_wit_package() {
let manifest = PluginManifest {
id: "demo".into(),
name: None,
description: None,
version: Some("1.0.0".into()),
config_schema: None,
capabilities: vec![],
module_path: "plugins/demo.wasm".into(),
wit_packages: vec!["zeroclaw:hooks@1.0.0".into()],
tools: vec![],
providers: vec!["demo_provider".into()],
};
assert!(validate_manifest(&manifest).is_err());
}
}