Skip to main content

ralph/config/validation/
agent.rs

1//! Agent validation rules.
2//!
3//! Responsibilities:
4//! - Validate agent-specific numeric limits and binary path overrides.
5//! - Expose helpers used by trust validation to identify execution-sensitive settings.
6//!
7//! Not handled here:
8//! - Queue thresholds or git ref validation.
9//! - Full config version or parallel workspace rules.
10//!
11//! Invariants/assumptions:
12//! - Empty binary-path strings are invalid when provided.
13//! - Agent phases stay within the configured global limits.
14
15use super::ci_gate::validate_ci_gate_config;
16use crate::constants::runner::{MAX_PHASES, MIN_ITERATIONS, MIN_PHASES};
17use crate::contracts::{AgentConfig, PhaseOverrides, Runner};
18use anyhow::{Result, bail};
19
20pub fn validate_agent_binary_paths(agent: &AgentConfig, label: &str) -> Result<()> {
21    macro_rules! check_bin {
22        ($field:ident) => {
23            if let Some(bin) = &agent.$field
24                && bin.trim().is_empty()
25            {
26                bail!(
27                    "Empty {label}.{}: binary path is required if specified.",
28                    stringify!($field)
29                );
30            }
31        };
32    }
33
34    check_bin!(codex_bin);
35    check_bin!(opencode_bin);
36    check_bin!(gemini_bin);
37    check_bin!(claude_bin);
38    check_bin!(cursor_bin);
39    check_bin!(kimi_bin);
40    check_bin!(pi_bin);
41
42    Ok(())
43}
44
45pub fn validate_agent_patch(agent: &AgentConfig, label: &str) -> Result<()> {
46    if let Some(phases) = agent.phases
47        && !(MIN_PHASES..=MAX_PHASES).contains(&phases)
48    {
49        bail!(
50            "Invalid {label}.phases: {phases}. Supported values are {MIN_PHASES}, {}, or {MAX_PHASES}.",
51            MIN_PHASES + 1
52        );
53    }
54
55    if let Some(iterations) = agent.iterations
56        && iterations < MIN_ITERATIONS
57    {
58        bail!(
59            "Invalid {label}.iterations: {iterations}. Iterations must be at least {MIN_ITERATIONS}."
60        );
61    }
62
63    if let Some(timeout) = agent.session_timeout_hours
64        && timeout == 0
65    {
66        bail!(
67            "Invalid {label}.session_timeout_hours: {timeout}. Session timeout must be greater than 0."
68        );
69    }
70
71    validate_agent_binary_paths(agent, label)?;
72    validate_ci_gate_config(agent.ci_gate.as_ref(), label)?;
73    Ok(())
74}
75
76pub(crate) fn agent_has_execution_settings(agent: &AgentConfig) -> bool {
77    agent.ci_gate.is_some()
78        || agent.codex_bin.is_some()
79        || agent.opencode_bin.is_some()
80        || agent.gemini_bin.is_some()
81        || agent.claude_bin.is_some()
82        || agent.cursor_bin.is_some()
83        || agent.kimi_bin.is_some()
84        || agent.pi_bin.is_some()
85        || agent.runner.as_ref().is_some_and(Runner::is_plugin)
86        || agent
87            .phase_overrides
88            .as_ref()
89            .is_some_and(phase_overrides_have_plugin_runner)
90}
91
92fn phase_overrides_have_plugin_runner(overrides: &PhaseOverrides) -> bool {
93    [&overrides.phase1, &overrides.phase2, &overrides.phase3]
94        .into_iter()
95        .flatten()
96        .filter_map(|phase| phase.runner.as_ref())
97        .any(Runner::is_plugin)
98}