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", "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}