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