Files
claw/src/compat/scene_platform/scheduled_registry.rs

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)
}