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,
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::{Config, builtin_profile_names, is_reserved_profile_name};
23use anyhow::{Result, bail};
24use std::path::Component;
25
26pub fn validate_config(cfg: &Config) -> Result<()> {
27    if cfg.version != 2 {
28        bail!(
29            "Unsupported config version: {}. Ralph requires version 2. Upgrade your config file to the 0.3 contract and set `version` to 2.",
30            cfg.version
31        );
32    }
33
34    validate_queue_overrides(&cfg.queue)?;
35    validate_queue_aging_thresholds(&cfg.queue.aging_thresholds)?;
36
37    if let Some(phases) = cfg.agent.phases
38        && !(MIN_PHASES..=MAX_PHASES).contains(&phases)
39    {
40        bail!(
41            "Invalid agent.phases: {}. Supported values are {}, {}, or {}. Update .ralph/config.jsonc or CLI flags.",
42            phases,
43            MIN_PHASES,
44            MIN_PHASES + 1,
45            MAX_PHASES
46        );
47    }
48
49    if let Some(iterations) = cfg.agent.iterations
50        && iterations < MIN_ITERATIONS
51    {
52        bail!(
53            "Invalid agent.iterations: {}. Iterations must be at least {}. Update .ralph/config.jsonc.",
54            iterations,
55            MIN_ITERATIONS
56        );
57    }
58
59    if let Some(workers) = cfg.parallel.workers
60        && workers < MIN_PARALLEL_WORKERS
61    {
62        bail!(
63            "Invalid parallel.workers: {}. Parallel workers must be >= {}. Update .ralph/config.jsonc or CLI flags.",
64            workers,
65            MIN_PARALLEL_WORKERS
66        );
67    }
68
69    if let Some(root) = &cfg.parallel.workspace_root {
70        if root.as_os_str().is_empty() {
71            bail!(
72                "Empty parallel.workspace_root: path is required if specified. Set a valid path or remove the field."
73            );
74        }
75        if root
76            .components()
77            .any(|component| matches!(component, Component::ParentDir))
78        {
79            bail!(
80                "Invalid parallel.workspace_root: path must not contain '..' components (got {}). Use a normalized path.",
81                root.display()
82            );
83        }
84    }
85
86    if let Some(timeout) = cfg.agent.session_timeout_hours
87        && timeout == 0
88    {
89        bail!(
90            "Invalid agent.session_timeout_hours: {}. Session timeout must be greater than 0. Update .ralph/config.jsonc.",
91            timeout
92        );
93    }
94
95    validate_agent_binary_paths(&cfg.agent, "agent")?;
96    validate_ci_gate_config(cfg.agent.ci_gate.as_ref(), "agent")?;
97
98    if let Some(profiles) = cfg.profiles.as_ref() {
99        for (name, patch) in profiles {
100            if is_reserved_profile_name(name) {
101                bail!(
102                    "Invalid profiles.{name}: `{name}` is a reserved built-in profile name. Rename your custom profile. Reserved names: {}.",
103                    builtin_profile_names().collect::<Vec<_>>().join(", ")
104                );
105            }
106            validate_agent_patch(patch, &format!("profiles.{name}"))?;
107        }
108    }
109
110    Ok(())
111}