mars_agents/models/
harness.rs1use 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", "cursor", "opencode"];
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!(
107 candidates,
108 vec!["codex", "claude", "pi", "cursor", "opencode"]
109 );
110 }
111
112 #[test]
113 fn candidates_for_anthropic_native_first_then_default_order() {
114 let candidates = harness_candidates_for_provider("anthropic");
115 assert_eq!(
116 candidates,
117 vec!["claude", "codex", "pi", "cursor", "opencode"]
118 );
119 }
120
121 #[test]
122 fn candidates_for_unknown_provider() {
123 let candidates = harness_candidates_for_provider("unknown");
124 assert_eq!(
125 candidates,
126 vec!["claude", "codex", "pi", "cursor", "opencode"]
127 );
128 }
129
130 #[test]
131 fn valid_harness_validation_rejects_gemini() {
132 assert!(is_valid_harness("claude"));
133 assert!(is_valid_harness("OpenCode"));
134 assert!(!is_valid_harness("gemini"));
135 assert!(!is_valid_harness("unknown"));
136 }
137}