1use crate::{Harness, HarnessInfo};
11#[cfg(feature = "bob")]
12use crate::Bob;
13#[cfg(feature = "claude")]
14use crate::Claude;
15#[cfg(feature = "codex")]
16use crate::Codex;
17
18pub const DEFAULT_HARNESS_ID: &str = "bob";
22
23#[derive(Default)]
34pub struct Registry {
35 harnesses: Vec<Box<dyn Harness>>,
36}
37
38impl Registry {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn register(mut self, harness: impl Harness + 'static) -> Self {
47 self.harnesses.push(Box::new(harness));
48 self
49 }
50
51 pub fn by_id(&self, id: &str) -> Option<&dyn Harness> {
53 self.harnesses
54 .iter()
55 .map(Box::as_ref)
56 .find(|h| h.info().id == id)
57 }
58
59 pub fn catalog(&self) -> Vec<HarnessInfo> {
61 self.harnesses.iter().map(|h| h.info()).collect()
62 }
63
64 pub fn ids(&self) -> Vec<String> {
66 self.harnesses.iter().map(|h| h.info().id).collect()
67 }
68}
69
70pub fn default_registry() -> Registry {
73 #[allow(unused_mut)]
74 let mut reg = Registry::new();
75 #[cfg(feature = "bob")]
76 {
77 reg = reg.register(Bob::new());
78 }
79 #[cfg(feature = "claude")]
80 {
81 reg = reg.register(Claude::new());
82 }
83 #[cfg(feature = "codex")]
84 {
85 reg = reg.register(Codex::new());
86 }
87 reg
88}
89
90pub fn harness_by_id(id: &str) -> Option<Box<dyn Harness>> {
93 let _ = id;
94 #[cfg(feature = "bob")]
95 {
96 if id == crate::BOB_HARNESS_ID {
97 return Some(Box::new(Bob::new()));
98 }
99 }
100 #[cfg(feature = "claude")]
101 {
102 if id == crate::CLAUDE_HARNESS_ID {
103 return Some(Box::new(Claude::new()));
104 }
105 }
106 #[cfg(feature = "codex")]
107 {
108 if id == crate::CODEX_HARNESS_ID {
109 return Some(Box::new(Codex::new()));
110 }
111 }
112 None
113}
114
115pub fn harness_catalog() -> Vec<HarnessInfo> {
117 default_registry().catalog()
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::{
124 CredentialSpec, HarnessCapabilities, HarnessReadiness, InstallCallback, RunCallback,
125 RunHandle, RunRequest,
126 };
127
128 #[test]
129 fn default_registry_lists_bob_claude_codex_in_order() {
130 assert_eq!(default_registry().ids(), vec!["bob", "claude", "codex"]);
131 assert_eq!(default_registry().catalog()[0].id, DEFAULT_HARNESS_ID);
132 }
133
134 #[test]
135 fn harness_by_id_resolves_builtins_and_rejects_unknown() {
136 assert!(harness_by_id("bob").is_some());
137 assert!(harness_by_id("claude").is_some());
138 assert!(harness_by_id("codex").is_some());
139 assert!(harness_by_id("nope").is_none());
140 }
141
142 #[test]
143 fn capabilities_match_each_adapter_and_back_credential_required() {
144 let caps = |id: &str| harness_by_id(id).unwrap().info().capabilities;
145
146 let bob = caps("bob");
147 assert!(bob.credential_required && !bob.previews_edits);
150 assert!(bob.models.is_empty() && !bob.supports_effort && !bob.supports_max_turns);
151 assert_eq!(
152 bob.credential_required,
153 harness_by_id("bob").unwrap().credential().required
154 );
155
156 let claude = caps("claude");
157 assert!(!claude.credential_required && !claude.previews_edits);
158 assert!(!claude.models.is_empty() && !claude.allows_custom_model);
159 assert!(claude.supports_max_turns && !claude.supports_effort);
160
161 let codex = caps("codex");
162 assert!(!codex.credential_required && !codex.previews_edits);
163 assert!(codex.allows_custom_model && codex.supports_effort && !codex.supports_max_turns);
164
165 assert!(claude.supports_login && codex.supports_login && !bob.supports_login);
166 }
167
168 struct Acme;
171 impl Harness for Acme {
172 fn info(&self) -> HarnessInfo {
173 HarnessInfo {
174 id: "acme".to_owned(),
175 display_name: "Acme".to_owned(),
176 description: "A custom third-party harness.".to_owned(),
177 requires_install: false,
178 capabilities: HarnessCapabilities {
179 credential_required: false,
180 previews_edits: false,
181 models: Vec::new(),
182 allows_custom_model: true,
183 supports_effort: false,
184 supports_max_turns: false,
185 supports_login: false,
186 },
187 }
188 }
189 fn readiness(&self) -> HarnessReadiness {
190 HarnessReadiness {
191 harness_id: "acme".to_owned(),
192 ready: true,
193 installed: true,
194 version: None,
195 auth_configured: true,
196 error: None,
197 details: serde_json::Value::Null,
198 }
199 }
200 fn install(&self, _on_event: InstallCallback) -> Result<(), crate::HarnessError> {
201 Ok(())
202 }
203 fn run(
204 &self,
205 _req: RunRequest,
206 _on_event: RunCallback,
207 ) -> Result<RunHandle, crate::HarnessError> {
208 Err(crate::HarnessError::Other(
211 "acme: run not implemented in test".to_owned(),
212 ))
213 }
214 fn credential(&self) -> CredentialSpec {
215 CredentialSpec {
216 label: "Acme key".to_owned(),
217 keychain_service: "acme".to_owned(),
218 keychain_account: "ACME_API_KEY".to_owned(),
219 required: false,
220 }
221 }
222 }
223
224 #[test]
225 fn custom_harness_registers_and_resolves_alongside_builtins() {
226 let reg = Registry::new().register(Bob::new()).register(Acme);
227 assert!(reg.by_id("bob").is_some());
228 assert!(reg.by_id("acme").is_some(), "custom harness must resolve");
229 assert_eq!(reg.ids(), vec!["bob", "acme"]);
230 }
231}