ralph/config/validation/
config_rules.rs1use 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}