use anyhow::{anyhow, Result};
use super::Config;
impl Config {
pub fn validate(&self) -> Result<()> {
self.validate_thresholds()?;
self.validate_mcp_config()?;
if let Some(layers) = &self.layers {
self.validate_layers(layers)?;
}
self.validate_workflows()?;
self.validate_pipelines()?;
self.validate_hooks()?;
self.validate_required_fields()?;
Ok(())
}
fn validate_hooks(&self) -> Result<()> {
let mut seen_names = std::collections::HashSet::new();
let mut seen_binds = std::collections::HashSet::new();
for hook in &self.hooks {
if hook.name.is_empty() {
return Err(anyhow!("Hook has empty name"));
}
if !seen_names.insert(&hook.name) {
return Err(anyhow!("Duplicate hook name: '{}'", hook.name));
}
if hook.bind.is_empty() {
return Err(anyhow!("Hook '{}' has empty bind address", hook.name));
}
if !seen_binds.insert(&hook.bind) {
return Err(anyhow!(
"Hook '{}' has duplicate bind address '{}' (already used by another hook)",
hook.name,
hook.bind
));
}
if hook.bind.parse::<std::net::SocketAddr>().is_err() {
return Err(anyhow!(
"Hook '{}' has invalid bind address: '{}'",
hook.name,
hook.bind
));
}
if hook.script.is_empty() {
return Err(anyhow!("Hook '{}' has empty script path", hook.name));
}
if hook.timeout == 0 {
return Err(anyhow!("Hook '{}' timeout must be > 0", hook.name));
}
if hook.timeout > 3600 {
return Err(anyhow!(
"Hook '{}' timeout too high: {}s (max 3600)",
hook.name,
hook.timeout
));
}
}
Ok(())
}
fn validate_required_fields(&self) -> Result<()> {
if self.model.is_empty() {
return Err(anyhow!("Model field cannot be empty"));
}
if self.markdown_theme.is_empty() {
return Err(anyhow!("Markdown theme field cannot be empty"));
}
for role in &self.roles {
if role.config.temperature < 0.0 || role.config.temperature > 2.0 {
return Err(anyhow!(
"Role '{}' temperature must be between 0.0 and 2.0, got: {}",
role.name,
role.config.temperature
));
}
if role.config.top_p < 0.0 || role.config.top_p > 1.0 {
return Err(anyhow!(
"Role '{}' top_p must be between 0.0 and 1.0, got: {}",
role.name,
role.config.top_p
));
}
if role.config.top_k < 1 || role.config.top_k > 1000 {
return Err(anyhow!(
"Role '{}' top_k must be between 1 and 1000, got: {}",
role.name,
role.config.top_k
));
}
}
if let Some(layers) = &self.layers {
for layer in layers {
if layer.temperature < 0.0 || layer.temperature > 2.0 {
return Err(anyhow!(
"Layer '{}' temperature must be between 0.0 and 2.0, got: {}",
layer.name,
layer.temperature
));
}
if layer.top_p < 0.0 || layer.top_p > 1.0 {
return Err(anyhow!(
"Layer '{}' top_p must be between 0.0 and 1.0, got: {}",
layer.name,
layer.top_p
));
}
if layer.top_k < 1 || layer.top_k > 1000 {
return Err(anyhow!(
"Layer '{}' top_k must be between 1 and 1000, got: {}",
layer.name,
layer.top_k
));
}
}
}
Ok(())
}
pub fn validate_thresholds(&self) -> Result<()> {
if self.cache_tokens_threshold > 1_000_000 {
return Err(anyhow!(
"Cache tokens threshold too high: {}. Maximum allowed: 1,000,000",
self.cache_tokens_threshold
));
}
if self.mcp_response_warning_threshold > 1_000_000 {
return Err(anyhow!(
"MCP response warning threshold too high: {}. Maximum allowed: 1,000,000",
self.mcp_response_warning_threshold
));
}
if self.max_session_tokens_threshold > 2_000_000 {
return Err(anyhow!(
"Max session tokens threshold too high: {}. Maximum allowed: 2,000,000",
self.max_session_tokens_threshold
));
}
if self.cache_timeout_seconds > 86400 {
return Err(anyhow!(
"Cache timeout too high: {} seconds. Maximum allowed: 86400 (24 hours)",
self.cache_timeout_seconds
));
}
Ok(())
}
fn validate_mcp_config(&self) -> Result<()> {
for server_config in &self.mcp.servers {
let server_name = &server_config.name();
if server_config.timeout_seconds() == 0 {
return Err(anyhow!(
"Server '{}' has invalid timeout: 0. Must be greater than 0",
server_name
));
}
if server_config.timeout_seconds() > 3600 {
return Err(anyhow!(
"Server '{}' timeout too high: {} seconds. Maximum allowed: 3600 (1 hour)",
server_name,
server_config.timeout_seconds()
));
}
if matches!(
server_config.connection_type(),
crate::config::McpConnectionType::Http
) {
if server_config.url().is_none() && server_config.command().is_none() {
return Err(anyhow!(
"External server '{}' must have either 'url' or 'command' specified",
server_name
));
}
if server_config.url().is_some() && server_config.command().is_some() {
return Err(anyhow!(
"External server '{}' cannot have both 'url' and 'command' specified",
server_name
));
}
}
}
Ok(())
}
fn validate_layers(&self, layers: &[crate::session::layers::LayerConfig]) -> Result<()> {
for (index, layer) in layers.iter().enumerate() {
if layer.name.is_empty() {
return Err(anyhow!("Layer at index {} has empty name", index));
}
if layer.description.is_empty() {
return Err(anyhow!(
"Layer '{}' at index {} has empty description",
layer.name,
index
));
}
}
Ok(())
}
fn validate_pipelines(&self) -> Result<()> {
for pipeline in &self.pipelines {
pipeline
.validate()
.map_err(|e| anyhow!("Pipeline validation failed: {}", e))?;
}
for role in &self.roles {
if let Some(pipeline_name) = &role.pipeline {
if !self.pipelines.iter().any(|p| &p.name == pipeline_name) {
return Err(anyhow!(
"Role '{}' references undefined pipeline '{}'",
role.name,
pipeline_name
));
}
}
}
Ok(())
}
fn validate_workflows(&self) -> Result<()> {
for workflow in &self.workflows {
workflow
.validate()
.map_err(|e| anyhow!("Workflow validation failed: {}", e))?;
}
for role in &self.roles {
if let Some(workflow_name) = &role.workflow {
if !self.workflows.iter().any(|w| &w.name == workflow_name) {
return Err(anyhow!(
"Role '{}' references undefined workflow '{}'",
role.name,
workflow_name
));
}
}
}
if let Some(layers) = &self.layers {
use std::collections::HashSet;
let layer_names: HashSet<&str> = layers.iter().map(|l| l.name.as_str()).collect();
for workflow in &self.workflows {
fn validate_step_layers(
step: &crate::config::WorkflowStep,
layer_names: &HashSet<&str>,
workflow_name: &str,
) -> Result<(), anyhow::Error> {
if let Some(layer) = &step.layer {
if !layer_names.contains(layer.as_str()) {
return Err(anyhow!(
"Workflow '{}' step '{}' references undefined layer '{}'",
workflow_name,
step.name,
layer
));
}
}
for layer in &step.on_match {
if !layer_names.contains(layer.as_str()) {
return Err(anyhow!(
"Workflow '{}' step '{}' on_match references undefined layer '{}'",
workflow_name,
step.name,
layer
));
}
}
for layer in &step.on_no_match {
if !layer_names.contains(layer.as_str()) {
return Err(anyhow!(
"Workflow '{}' step '{}' on_no_match references undefined layer '{}'",
workflow_name,
step.name,
layer
));
}
}
for layer in &step.parallel_layers {
if !layer_names.contains(layer.as_str()) {
return Err(anyhow!(
"Workflow '{}' step '{}' parallel_layers references undefined layer '{}'",
workflow_name,
step.name,
layer
));
}
}
if let Some(aggregator) = &step.aggregator {
if !layer_names.contains(aggregator.as_str()) {
return Err(anyhow!(
"Workflow '{}' step '{}' aggregator references undefined layer '{}'",
workflow_name,
step.name,
aggregator
));
}
}
for substep in &step.substeps {
validate_step_layers(substep, layer_names, workflow_name)?;
}
Ok(())
}
for step in &workflow.steps {
validate_step_layers(step, &layer_names, &workflow.name)?;
}
}
}
Ok(())
}
}