ralph/config/validation/
agent.rs1use super::ci_gate::validate_ci_gate_config;
16use crate::constants::runner::{MAX_PHASES, MIN_ITERATIONS, MIN_PHASES};
17use crate::contracts::{AgentConfig, PhaseOverrides, Runner, validate_webhook_settings};
18use anyhow::{Result, bail};
19use std::path::PathBuf;
20
21pub fn validate_agent_binary_paths(agent: &AgentConfig, label: &str) -> Result<()> {
22 macro_rules! check_bin {
23 ($field:ident) => {
24 if let Some(bin) = &agent.$field
25 && bin.trim().is_empty()
26 {
27 bail!(
28 "Empty {label}.{}: binary path is required if specified.",
29 stringify!($field)
30 );
31 }
32 };
33 }
34
35 check_bin!(codex_bin);
36 check_bin!(opencode_bin);
37 check_bin!(gemini_bin);
38 check_bin!(claude_bin);
39 check_bin!(cursor_bin);
40 check_bin!(kimi_bin);
41 check_bin!(pi_bin);
42
43 Ok(())
44}
45
46pub(crate) fn validate_instruction_files_entries(
51 paths: Option<&Vec<PathBuf>>,
52 label: &str,
53) -> Result<()> {
54 let Some(paths) = paths else {
55 return Ok(());
56 };
57 for raw in paths {
58 if raw.as_os_str().is_empty() || raw.to_string_lossy().trim().is_empty() {
59 bail!(
60 "Invalid {label}.instruction_files: each entry must be a non-empty path. Remove blank list items or fix the path string in .ralph/config.jsonc."
61 );
62 }
63 }
64 Ok(())
65}
66
67pub fn validate_agent_patch(agent: &AgentConfig, label: &str) -> Result<()> {
68 validate_instruction_files_entries(agent.instruction_files.as_ref(), label)?;
69 if let Some(phases) = agent.phases
70 && !(MIN_PHASES..=MAX_PHASES).contains(&phases)
71 {
72 bail!(
73 "Invalid {label}.phases: {phases}. Supported values are {MIN_PHASES}, {}, or {MAX_PHASES}.",
74 MIN_PHASES + 1
75 );
76 }
77
78 if let Some(iterations) = agent.iterations
79 && iterations < MIN_ITERATIONS
80 {
81 bail!(
82 "Invalid {label}.iterations: {iterations}. Iterations must be at least {MIN_ITERATIONS}."
83 );
84 }
85
86 if let Some(timeout) = agent.session_timeout_hours
87 && timeout == 0
88 {
89 bail!(
90 "Invalid {label}.session_timeout_hours: {timeout}. Session timeout must be greater than 0."
91 );
92 }
93
94 validate_agent_binary_paths(agent, label)?;
95 validate_ci_gate_config(agent.ci_gate.as_ref(), label)?;
96 if let Err(err) = validate_webhook_settings(&agent.webhook) {
97 bail!("Invalid {label}.webhook: {err:#}");
98 }
99 Ok(())
100}
101
102pub(crate) fn agent_has_execution_settings(agent: &AgentConfig) -> bool {
103 agent.ci_gate.is_some()
104 || agent.codex_bin.is_some()
105 || agent.opencode_bin.is_some()
106 || agent.gemini_bin.is_some()
107 || agent.claude_bin.is_some()
108 || agent.cursor_bin.is_some()
109 || agent.kimi_bin.is_some()
110 || agent.pi_bin.is_some()
111 || agent.runner.as_ref().is_some_and(Runner::is_plugin)
112 || agent
113 .phase_overrides
114 .as_ref()
115 .is_some_and(phase_overrides_have_plugin_runner)
116}
117
118fn phase_overrides_have_plugin_runner(overrides: &PhaseOverrides) -> bool {
119 [&overrides.phase1, &overrides.phase2, &overrides.phase3]
120 .into_iter()
121 .flatten()
122 .filter_map(|phase| phase.runner.as_ref())
123 .any(Runner::is_plugin)
124}