294 lines
10 KiB
Rust
294 lines
10 KiB
Rust
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use thiserror::Error;
|
|
use zeroclaw::skills::{load_skills_from_directory, Skill};
|
|
|
|
use crate::scene_contract::manifest::{
|
|
SceneManifest, SCENE_MANIFEST_FILE_NAME, SUPPORTED_SCHEMA_VERSION_V1,
|
|
SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1, SUPPORTED_SCHEDULED_MONITORING_KIND_V1,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ScheduledMonitoringRegistryEntry {
|
|
pub manifest: SceneManifest,
|
|
pub skill_root: PathBuf,
|
|
pub workflow_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ScheduledMonitoringRegistryError {
|
|
#[error("failed to read skills directory {path}: {source}")]
|
|
ReadSkillsDir {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
#[error("failed to read scene manifest {path}: {source}")]
|
|
ReadManifest {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
#[error("failed to parse scene manifest {path}: {source}")]
|
|
ParseManifest {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: toml::de::Error,
|
|
},
|
|
#[error("scheduled scene manifest {path} declares unsupported schema_version {version}; only {supported} is supported in v1")]
|
|
UnsupportedSchemaVersion {
|
|
path: PathBuf,
|
|
version: String,
|
|
supported: &'static str,
|
|
},
|
|
#[error("scheduled scene manifest {path} declares unsupported kind {kind}; only {supported} is supported")]
|
|
UnsupportedSceneKind {
|
|
path: PathBuf,
|
|
kind: String,
|
|
supported: &'static str,
|
|
},
|
|
#[error("scheduled scene manifest {path} declares unsupported category {category}; only {supported} is supported")]
|
|
UnsupportedSceneCategory {
|
|
path: PathBuf,
|
|
category: String,
|
|
supported: &'static str,
|
|
},
|
|
#[error("scheduled scene manifest {path} points to missing skill package {skill}")]
|
|
MissingSkill { path: PathBuf, skill: String },
|
|
#[error("scheduled scene manifest {path} declares skill {manifest_skill}, but containing skill package is {package_skill}")]
|
|
SkillPackageMismatch {
|
|
path: PathBuf,
|
|
manifest_skill: String,
|
|
package_skill: String,
|
|
},
|
|
#[error("scheduled scene manifest {path} is missing trigger section")]
|
|
MissingTriggerSection { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} is missing modes section")]
|
|
MissingModesSection { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} is missing runtime_context section")]
|
|
MissingRuntimeContextSection { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} is missing safety section")]
|
|
MissingSafetySection { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} is missing tools section")]
|
|
MissingToolsSection { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} is missing references section")]
|
|
MissingReferencesSection { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} is missing workflow_id")]
|
|
MissingWorkflowId { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} has unsafe active or queue_process enablement")]
|
|
UnsafeModesEnabled { path: PathBuf },
|
|
#[error("scheduled scene manifest {path} incorrectly exposes natural-language primary trigger")]
|
|
NaturalLanguagePrimaryEnabled { path: PathBuf },
|
|
#[error("scheduled workflow id {workflow_id} is declared twice: {first_path} and {second_path}")]
|
|
DuplicateWorkflowId {
|
|
workflow_id: String,
|
|
first_path: PathBuf,
|
|
second_path: PathBuf,
|
|
},
|
|
}
|
|
|
|
pub fn load_scheduled_monitoring_registry(
|
|
skills_dir: &Path,
|
|
) -> Result<Vec<ScheduledMonitoringRegistryEntry>, ScheduledMonitoringRegistryError> {
|
|
if !skills_dir.exists() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut skill_roots = Vec::new();
|
|
for entry in fs::read_dir(skills_dir).map_err(|source| {
|
|
ScheduledMonitoringRegistryError::ReadSkillsDir {
|
|
path: skills_dir.to_path_buf(),
|
|
source,
|
|
}
|
|
})? {
|
|
let entry = entry.map_err(|source| ScheduledMonitoringRegistryError::ReadSkillsDir {
|
|
path: skills_dir.to_path_buf(),
|
|
source,
|
|
})?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
skill_roots.push(path);
|
|
}
|
|
}
|
|
skill_roots.sort();
|
|
|
|
let skills_by_root = index_skills_by_root(skills_dir);
|
|
let mut workflow_ids = HashMap::new();
|
|
let mut registry = Vec::new();
|
|
|
|
for skill_root in skill_roots {
|
|
let manifest_path = skill_root.join(SCENE_MANIFEST_FILE_NAME);
|
|
if !manifest_path.exists() {
|
|
continue;
|
|
}
|
|
|
|
let manifest = load_manifest(&manifest_path)?;
|
|
if manifest.scene.kind != SUPPORTED_SCHEDULED_MONITORING_KIND_V1 {
|
|
continue;
|
|
}
|
|
|
|
let workflow_id = validate_manifest(&manifest, &manifest_path, &skill_root, &skills_by_root)?;
|
|
if let Some(first_path) = workflow_ids.insert(workflow_id.clone(), manifest_path.clone()) {
|
|
return Err(ScheduledMonitoringRegistryError::DuplicateWorkflowId {
|
|
workflow_id,
|
|
first_path,
|
|
second_path: manifest_path,
|
|
});
|
|
}
|
|
|
|
registry.push(ScheduledMonitoringRegistryEntry {
|
|
manifest,
|
|
skill_root,
|
|
workflow_id,
|
|
});
|
|
}
|
|
|
|
Ok(registry)
|
|
}
|
|
|
|
fn index_skills_by_root(skills_dir: &Path) -> HashMap<PathBuf, Skill> {
|
|
load_skills_from_directory(skills_dir, true)
|
|
.into_iter()
|
|
.filter_map(|skill| {
|
|
let skill_root = skill
|
|
.location
|
|
.as_deref()
|
|
.and_then(Path::parent)
|
|
.map(Path::to_path_buf)?;
|
|
Some((skill_root, skill))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn load_manifest(path: &Path) -> Result<SceneManifest, ScheduledMonitoringRegistryError> {
|
|
let content =
|
|
fs::read_to_string(path).map_err(|source| ScheduledMonitoringRegistryError::ReadManifest {
|
|
path: path.to_path_buf(),
|
|
source,
|
|
})?;
|
|
toml::from_str(&content).map_err(|source| ScheduledMonitoringRegistryError::ParseManifest {
|
|
path: path.to_path_buf(),
|
|
source,
|
|
})
|
|
}
|
|
|
|
fn validate_manifest(
|
|
manifest: &SceneManifest,
|
|
manifest_path: &Path,
|
|
skill_root: &Path,
|
|
skills_by_root: &HashMap<PathBuf, Skill>,
|
|
) -> Result<String, ScheduledMonitoringRegistryError> {
|
|
if manifest.manifest.schema_version != SUPPORTED_SCHEMA_VERSION_V1 {
|
|
return Err(ScheduledMonitoringRegistryError::UnsupportedSchemaVersion {
|
|
path: manifest_path.to_path_buf(),
|
|
version: manifest.manifest.schema_version.clone(),
|
|
supported: SUPPORTED_SCHEMA_VERSION_V1,
|
|
});
|
|
}
|
|
|
|
if manifest.scene.kind != SUPPORTED_SCHEDULED_MONITORING_KIND_V1 {
|
|
return Err(ScheduledMonitoringRegistryError::UnsupportedSceneKind {
|
|
path: manifest_path.to_path_buf(),
|
|
kind: manifest.scene.kind.clone(),
|
|
supported: SUPPORTED_SCHEDULED_MONITORING_KIND_V1,
|
|
});
|
|
}
|
|
|
|
if manifest.scene.category != SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1 {
|
|
return Err(ScheduledMonitoringRegistryError::UnsupportedSceneCategory {
|
|
path: manifest_path.to_path_buf(),
|
|
category: manifest.scene.category.clone(),
|
|
supported: SUPPORTED_SCHEDULED_MONITORING_CATEGORY_V1,
|
|
});
|
|
}
|
|
|
|
let trigger = manifest
|
|
.trigger
|
|
.as_ref()
|
|
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingTriggerSection {
|
|
path: manifest_path.to_path_buf(),
|
|
})?;
|
|
let modes = manifest
|
|
.modes
|
|
.as_ref()
|
|
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingModesSection {
|
|
path: manifest_path.to_path_buf(),
|
|
})?;
|
|
let _runtime_context = manifest.runtime_context.as_ref().ok_or_else(|| {
|
|
ScheduledMonitoringRegistryError::MissingRuntimeContextSection {
|
|
path: manifest_path.to_path_buf(),
|
|
}
|
|
})?;
|
|
let safety = manifest
|
|
.safety
|
|
.as_ref()
|
|
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingSafetySection {
|
|
path: manifest_path.to_path_buf(),
|
|
})?;
|
|
let tools = manifest
|
|
.tools
|
|
.as_ref()
|
|
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingToolsSection {
|
|
path: manifest_path.to_path_buf(),
|
|
})?;
|
|
let _references = manifest.references.as_ref().ok_or_else(|| {
|
|
ScheduledMonitoringRegistryError::MissingReferencesSection {
|
|
path: manifest_path.to_path_buf(),
|
|
}
|
|
})?;
|
|
|
|
let workflow_id = manifest
|
|
.scene
|
|
.workflow_id
|
|
.clone()
|
|
.filter(|value| !value.trim().is_empty())
|
|
.ok_or_else(|| ScheduledMonitoringRegistryError::MissingWorkflowId {
|
|
path: manifest_path.to_path_buf(),
|
|
})?;
|
|
|
|
if safety.active_enabled || safety.queue_process_enabled {
|
|
return Err(ScheduledMonitoringRegistryError::UnsafeModesEnabled {
|
|
path: manifest_path.to_path_buf(),
|
|
});
|
|
}
|
|
|
|
if trigger.natural_language_primary {
|
|
return Err(ScheduledMonitoringRegistryError::NaturalLanguagePrimaryEnabled {
|
|
path: manifest_path.to_path_buf(),
|
|
});
|
|
}
|
|
|
|
if !modes.enabled.iter().any(|mode| mode == "dry_run" || mode == "monitor_only") {
|
|
return Err(ScheduledMonitoringRegistryError::UnsafeModesEnabled {
|
|
path: manifest_path.to_path_buf(),
|
|
});
|
|
}
|
|
|
|
let Some(skill) = skills_by_root.get(skill_root) else {
|
|
return Err(ScheduledMonitoringRegistryError::MissingSkill {
|
|
path: manifest_path.to_path_buf(),
|
|
skill: manifest.scene.skill.clone(),
|
|
});
|
|
};
|
|
|
|
if skill.name != manifest.scene.skill {
|
|
return Err(ScheduledMonitoringRegistryError::SkillPackageMismatch {
|
|
path: manifest_path.to_path_buf(),
|
|
manifest_skill: manifest.scene.skill.clone(),
|
|
package_skill: skill.name.clone(),
|
|
});
|
|
}
|
|
|
|
for expected_tool in [&tools.detect, &tools.decide, &tools.action_plan] {
|
|
if expected_tool.trim().is_empty() {
|
|
return Err(ScheduledMonitoringRegistryError::MissingToolsSection {
|
|
path: manifest_path.to_path_buf(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(workflow_id)
|
|
}
|