From 362a81a3e501c56acfb450be69169f48db5a3ebd Mon Sep 17 00:00:00 2001 From: xj Date: Sun, 1 Mar 2026 17:04:17 -0800 Subject: [PATCH 1/2] refactor(plugins): add validation profiles with strict runtime defaults --- src/plugins/manifest.rs | 65 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/src/plugins/manifest.rs b/src/plugins/manifest.rs index ea0f80c92..1784a804a 100644 --- a/src/plugins/manifest.rs +++ b/src/plugins/manifest.rs @@ -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, - /// 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()); + } } From 36b047179d483015d0d792a573d05144b15852b5 Mon Sep 17 00:00:00 2001 From: xj Date: Sun, 1 Mar 2026 17:09:58 -0800 Subject: [PATCH 2/2] fix(ci): format manifest profile regression tests --- src/plugins/manifest.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/plugins/manifest.rs b/src/plugins/manifest.rs index 1784a804a..8525f8cc7 100644 --- a/src/plugins/manifest.rs +++ b/src/plugins/manifest.rs @@ -378,11 +378,10 @@ id = " " tools: vec![], providers: vec![], }; - assert!(validate_manifest_with_profile( - &manifest, - ManifestValidationProfile::SchemaOnly - ) - .is_ok()); + assert!( + validate_manifest_with_profile(&manifest, ManifestValidationProfile::SchemaOnly) + .is_ok() + ); } #[test]