Skip to main content

ralph/config/validation/
config_rules.rs

1//! Full-config validation orchestration.
2//!
3//! Responsibilities:
4//! - Validate top-level config version and cross-domain settings.
5//! - Delegate queue, agent, CI gate, and profile checks to focused validators.
6//!
7//! Not handled here:
8//! - Config loading/merging.
9//! - Queue file contents or lock state.
10//!
11//! Invariants/assumptions:
12//! - Parallel workspace roots must be normalized paths.
13//! - Profile agent patches reuse the same agent validator used elsewhere.
14
15use super::{
16    agent::{validate_agent_patch, validate_instruction_files_entries},
17    ci_gate::validate_ci_gate_config,
18    queue::{validate_queue_aging_thresholds, validate_queue_overrides},
19    validate_agent_binary_paths,
20};
21use crate::constants::runner::{MAX_PHASES, MIN_ITERATIONS, MIN_PARALLEL_WORKERS, MIN_PHASES};
22use crate::contracts::{
23    Config, builtin_profile_names, is_reserved_profile_name, validate_webhook_settings,
24};
25use anyhow::{Result, bail};
26use std::path::Component;
27
28pub fn validate_config(cfg: &Config) -> Result<()> {
29    if cfg.version != 2 {
30        bail!(
31            "Unsupported config version: {}. Ralph requires version 2. Upgrade your config file to the 0.3 contract and set `version` to 2.",
32            cfg.version
33        );
34    }
35
36    validate_queue_overrides(&cfg.queue)?;
37    validate_queue_aging_thresholds(&cfg.queue.aging_thresholds)?;
38
39    if let Some(phases) = cfg.agent.phases
40        && !(MIN_PHASES..=MAX_PHASES).contains(&phases)
41    {
42        bail!(
43            "Invalid agent.phases: {}. Supported values are {}, {}, or {}. Update .ralph/config.jsonc or CLI flags.",
44            phases,
45            MIN_PHASES,
46            MIN_PHASES + 1,
47            MAX_PHASES
48        );
49    }
50
51    if let Some(iterations) = cfg.agent.iterations
52        && iterations < MIN_ITERATIONS
53    {
54        bail!(
55            "Invalid agent.iterations: {}. Iterations must be at least {}. Update .ralph/config.jsonc.",
56            iterations,
57            MIN_ITERATIONS
58        );
59    }
60
61    if let Some(workers) = cfg.parallel.workers
62        && workers < MIN_PARALLEL_WORKERS
63    {
64        bail!(
65            "Invalid parallel.workers: {}. Parallel workers must be >= {}. Update .ralph/config.jsonc or CLI flags.",
66            workers,
67            MIN_PARALLEL_WORKERS
68        );
69    }
70
71    if let Some(root) = &cfg.parallel.workspace_root {
72        if root.as_os_str().is_empty() {
73            bail!(
74                "Empty parallel.workspace_root: path is required if specified. Set a valid path or remove the field."
75            );
76        }
77        if root
78            .components()
79            .any(|component| matches!(component, Component::ParentDir))
80        {
81            bail!(
82                "Invalid parallel.workspace_root: path must not contain '..' components (got {}). Use a normalized path.",
83                root.display()
84            );
85        }
86    }
87
88    if let Some(timeout) = cfg.agent.session_timeout_hours
89        && timeout == 0
90    {
91        bail!(
92            "Invalid agent.session_timeout_hours: {}. Session timeout must be greater than 0. Update .ralph/config.jsonc.",
93            timeout
94        );
95    }
96
97    validate_instruction_files_entries(cfg.agent.instruction_files.as_ref(), "agent")?;
98    validate_agent_binary_paths(&cfg.agent, "agent")?;
99    validate_ci_gate_config(cfg.agent.ci_gate.as_ref(), "agent")?;
100    validate_webhook_settings(&cfg.agent.webhook)?;
101
102    if let Some(profiles) = cfg.profiles.as_ref() {
103        for (name, patch) in profiles {
104            if is_reserved_profile_name(name) {
105                bail!(
106                    "Invalid profiles.{name}: `{name}` is a reserved built-in profile name. Rename your custom profile. Reserved names: {}.",
107                    builtin_profile_names().collect::<Vec<_>>().join(", ")
108                );
109            }
110            validate_agent_patch(patch, &format!("profiles.{name}"))?;
111        }
112    }
113
114    Ok(())
115}