generated-scene: add scheduled monitoring runtime and helper lifecycle hardening
This commit is contained in:
293
src/compat/scene_platform/scheduled_registry.rs
Normal file
293
src/compat/scene_platform/scheduled_registry.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user