Skip to main content

mars_agents/models/
harness.rs

1// qa-validated: harness-order-settings-audit
2
3use std::collections::HashSet;
4use std::path::PathBuf;
5
6use crate::harness::host::{
7    ExecutableResolver, ExecutableState, PathExecutableResolver,
8    native_harness_authenticated as host_native_authed, resolve_binary_path,
9};
10use crate::harness::registry::{self, HarnessId};
11
12pub const VALID_HARNESSES: &[&str] = &["claude", "codex", "pi", "opencode", "cursor"];
13
14pub fn detect_installed_harnesses() -> HashSet<String> {
15    let resolver = PathExecutableResolver;
16    registry::all()
17        .iter()
18        .copied()
19        .filter(|id| {
20            matches!(
21                resolver.resolve(registry::descriptor(*id).binary),
22                ExecutableState::Found { .. }
23            )
24        })
25        .map(|id| id.as_str().to_string())
26        .collect()
27}
28
29pub fn is_valid_harness(name: &str) -> bool {
30    registry::is_known(name)
31}
32
33pub fn normalize_harness_name(name: &str) -> Option<String> {
34    registry::normalize_name(name)
35}
36
37pub fn harness_candidates_for_provider(provider: &str) -> Vec<String> {
38    registry::provider_candidate_order(provider)
39        .into_iter()
40        .map(|id| id.as_str().to_string())
41        .collect()
42}
43
44pub fn native_harness_authenticated(harness: &str) -> bool {
45    host_native_authed(harness)
46}
47
48pub fn resolve_command(command: &str) -> PathBuf {
49    let resolver = PathExecutableResolver;
50    resolve_binary_path(command, &resolver).unwrap_or_else(|| PathBuf::from(command))
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum HarnessOrderFailure {
55    Empty,
56    NoneInstalled { valid_candidates: Vec<String> },
57}
58
59pub struct ParsedHarnessOrder {
60    pub valid_candidates: Vec<String>,
61    pub warnings: Vec<String>,
62    pub failure: Option<HarnessOrderFailure>,
63}
64
65pub fn parse_settings_harness_order(order: &[String]) -> ParsedHarnessOrder {
66    if order.is_empty() {
67        return ParsedHarnessOrder {
68            valid_candidates: Vec::new(),
69            warnings: Vec::new(),
70            failure: Some(HarnessOrderFailure::Empty),
71        };
72    }
73
74    let mut valid_candidates = Vec::new();
75    let mut warnings = Vec::new();
76    for candidate in order {
77        let Some(normalized) = normalize_harness_name(candidate) else {
78            warnings.push(format!(
79                "settings.harness_order contains unrecognized harness `{candidate}`; skipping (valid: {})",
80                VALID_HARNESSES.join(", ")
81            ));
82            continue;
83        };
84
85        valid_candidates.push(normalized);
86    }
87
88    ParsedHarnessOrder {
89        valid_candidates,
90        warnings,
91        failure: None,
92    }
93}
94
95pub fn parse_harness_id(name: &str) -> Option<HarnessId> {
96    registry::parse(name)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn candidates_for_known_provider() {
105        let candidates = harness_candidates_for_provider("openai");
106        assert_eq!(candidates, vec!["codex", "pi", "opencode", "cursor"]);
107    }
108
109    #[test]
110    fn candidates_for_anthropic_use_pi_first_fallback_chain() {
111        let candidates = harness_candidates_for_provider("anthropic");
112        assert_eq!(candidates, vec!["claude", "pi", "opencode", "cursor"]);
113    }
114
115    #[test]
116    fn candidates_for_unknown_provider() {
117        let candidates = harness_candidates_for_provider("unknown");
118        assert_eq!(candidates, vec!["pi", "opencode", "cursor"]);
119    }
120
121    #[test]
122    fn valid_harness_validation_rejects_gemini() {
123        assert!(is_valid_harness("claude"));
124        assert!(is_valid_harness("OpenCode"));
125        assert!(!is_valid_harness("gemini"));
126        assert!(!is_valid_harness("unknown"));
127    }
128}