Merge branch 'main' into fix/release-v0.1.8-build-errors
This commit is contained in:
commit
9abe9119c5
@ -5740,18 +5740,10 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||
// Preserve startup perplexity filter config to ensure policy is not weakened
|
||||
// when runtime store lookup misses.
|
||||
startup_perplexity_filter: config.security.perplexity_filter.clone(),
|
||||
// WASM skill tools are sandboxed by the WASM engine and cannot access the
|
||||
// host filesystem, network, or shell. Pre-approve them so they are not
|
||||
// denied on non-CLI channels (which have no interactive stdin to prompt).
|
||||
approval_manager: {
|
||||
let mut autonomy = config.autonomy.clone();
|
||||
let skills_dir = workspace.join("skills");
|
||||
for name in tools::wasm_tool::wasm_tool_names_from_skills(&skills_dir) {
|
||||
if !autonomy.auto_approve.contains(&name) {
|
||||
autonomy.auto_approve.push(name);
|
||||
}
|
||||
}
|
||||
Arc::new(ApprovalManager::from_config(&autonomy))
|
||||
// Keep approval policy provenance-bound to static config. Do not
|
||||
// auto-approve tool names from untrusted manifest files.
|
||||
Arc::new(ApprovalManager::from_config(&config.autonomy))
|
||||
},
|
||||
safety_heartbeat: if config.agent.safety_heartbeat_interval > 0 {
|
||||
Some(SafetyHeartbeatConfig {
|
||||
|
||||
@ -124,12 +124,15 @@ pub fn discover_plugins(workspace_dir: Option<&Path>, extra_paths: &[PathBuf]) -
|
||||
seen.insert(plugin.manifest.id.clone(), i);
|
||||
}
|
||||
let mut deduped: Vec<DiscoveredPlugin> = Vec::with_capacity(seen.len());
|
||||
// Collect in insertion order of the winning index
|
||||
// Collect in insertion order of the winning index.
|
||||
// Sort descending for safe `swap_remove` on a shrinking vec, then restore
|
||||
// ascending order to preserve deterministic winner ordering.
|
||||
let mut indices: Vec<usize> = seen.values().copied().collect();
|
||||
indices.sort_unstable();
|
||||
indices.sort_unstable_by(|a, b| b.cmp(a));
|
||||
for i in indices {
|
||||
deduped.push(all_plugins.swap_remove(i));
|
||||
}
|
||||
deduped.reverse();
|
||||
|
||||
DiscoveryResult {
|
||||
plugins: deduped,
|
||||
@ -185,6 +188,24 @@ version = "0.1.0"
|
||||
assert!(result.plugins.iter().any(|p| p.manifest.id == "custom-one"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_handles_multiple_plugins_without_panicking() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ext_dir = tmp.path().join("custom-plugins");
|
||||
fs::create_dir_all(&ext_dir).unwrap();
|
||||
make_plugin_dir(&ext_dir, "custom-one");
|
||||
make_plugin_dir(&ext_dir, "custom-two");
|
||||
|
||||
let result = discover_plugins(None, &[ext_dir]);
|
||||
let ids: std::collections::HashSet<String> = result
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|p| p.manifest.id.clone())
|
||||
.collect();
|
||||
assert!(ids.contains("custom-one"));
|
||||
assert!(ids.contains("custom-two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_skips_hidden_dirs() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@ -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,26 @@ 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 +441,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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user