1use serde::Serialize;
11
12use crate::{Features, Harness, Info, Readiness};
13#[cfg(feature = "claude")]
14use crate::Claude;
15#[cfg(feature = "codex")]
16use crate::Codex;
17
18#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Listing {
27 pub manifest: Info,
28 pub capabilities: Features,
29}
30
31pub const DEFAULT_HARNESS_ID: &str = "claude";
35
36#[derive(Default)]
47pub struct Registry {
48 harnesses: Vec<Box<dyn Harness>>,
49}
50
51impl Registry {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn register(mut self, harness: impl Harness + 'static) -> Self {
60 self.harnesses.push(Box::new(harness));
61 self
62 }
63
64 pub fn register_boxed(mut self, harness: Box<dyn Harness>) -> Self {
68 self.harnesses.push(harness);
69 self
70 }
71
72 pub fn by_id(&self, id: &str) -> Option<&dyn Harness> {
74 self.harnesses
75 .iter()
76 .map(Box::as_ref)
77 .find(|h| h.info().id == id)
78 }
79
80 pub fn into_by_id(self, id: &str) -> Option<Box<dyn Harness>> {
84 self.harnesses.into_iter().find(|h| h.info().id == id)
85 }
86
87 pub fn discover(&self) -> Vec<Readiness> {
91 self.harnesses.iter().map(|h| h.readiness()).collect()
92 }
93
94 pub fn catalog(&self) -> Vec<Listing> {
97 self.harnesses
98 .iter()
99 .map(|h| Listing { manifest: h.info(), capabilities: h.features() })
100 .collect()
101 }
102
103 pub fn ids(&self) -> Vec<String> {
105 self.harnesses.iter().map(|h| h.info().id).collect()
106 }
107}
108
109pub fn default_registry() -> Registry {
112 #[allow(unused_mut)]
113 let mut reg = Registry::new();
114 #[cfg(feature = "claude")]
115 {
116 reg = reg.register(Claude::new());
117 }
118 #[cfg(feature = "codex")]
119 {
120 reg = reg.register(Codex::new());
121 }
122 reg
123}
124
125pub fn harness_by_id(id: &str) -> Option<Box<dyn Harness>> {
128 let _ = id;
129 #[cfg(feature = "claude")]
130 {
131 if id == crate::CLAUDE_HARNESS_ID {
132 return Some(Box::new(Claude::new()));
133 }
134 }
135 #[cfg(feature = "codex")]
136 {
137 if id == crate::CODEX_HARNESS_ID {
138 return Some(Box::new(Codex::new()));
139 }
140 }
141 None
142}
143
144pub fn harness_catalog() -> Vec<Listing> {
146 default_registry().catalog()
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::{
153 CredentialSpec, Features, Readiness, RunCallback,
154 RunHandle, RunRequest,
155 };
156
157 #[test]
158 fn default_registry_lists_claude_codex_in_order() {
159 assert_eq!(default_registry().ids(), vec!["claude", "codex"]);
160 assert_eq!(default_registry().catalog()[0].manifest.id, DEFAULT_HARNESS_ID);
161 }
162
163 #[test]
164 fn harness_by_id_resolves_builtins_and_rejects_unknown() {
165 assert!(harness_by_id("claude").is_some());
166 assert!(harness_by_id("codex").is_some());
167 assert!(harness_by_id("nope").is_none());
168 }
169
170 #[test]
171 fn capabilities_match_each_adapter_and_back_credential_required() {
172 let caps = |id: &str| harness_by_id(id).unwrap().features();
173
174 let claude = caps("claude");
175 assert!(!claude.credential_required && !claude.previews_edits);
176 assert!(!claude.models.is_empty() && !claude.custom_model);
177 assert!(claude.max_turns && !claude.effort);
178
179 let codex = caps("codex");
180 assert!(!codex.credential_required && !codex.previews_edits);
181 assert!(codex.custom_model && codex.effort && !codex.max_turns);
182
183 assert!(claude.login && codex.login);
184 }
185
186 struct Acme;
189 impl Harness for Acme {
190 fn info(&self) -> Info {
191 Info {
192 id: "acme".to_owned(),
193 display_name: "Acme".to_owned(),
194 description: "A custom third-party harness.".to_owned(),
195 install_hint: None,
196 }
197 }
198
199 fn features(&self) -> Features {
200 Features { custom_model: true, ..Default::default() }
201 }
202 fn readiness(&self) -> Readiness {
203 Readiness {
204 harness_id: "acme".to_owned(),
205 ready: true,
206 installed: true,
207 version: None,
208 auth_configured: true,
209 error: None,
210 details: serde_json::Value::Null,
211 }
212 }
213 fn start(
214 &self,
215 _req: RunRequest,
216 _on_event: RunCallback,
217 ) -> Result<RunHandle, crate::Error> {
218 Err(crate::Error::Other(
221 "acme: run not implemented in test".to_owned(),
222 ))
223 }
224 fn credential(&self) -> CredentialSpec {
225 CredentialSpec {
226 label: "Acme key".to_owned(),
227 keychain_service: "acme".to_owned(),
228 keychain_account: "ACME_API_KEY".to_owned(),
229 required: false,
230 }
231 }
232 }
233
234 #[test]
235 fn custom_harness_registers_and_resolves_alongside_builtins() {
236 let reg = Registry::new().register(Claude::new()).register(Acme);
237 assert!(reg.by_id("claude").is_some());
238 assert!(reg.by_id("acme").is_some(), "custom harness must resolve");
239 assert_eq!(reg.ids(), vec!["claude", "acme"]);
240 }
241
242 #[test]
243 fn register_boxed_then_into_by_id_returns_an_owned_box() {
244 let reg = Registry::new().register_boxed(Box::new(Acme));
245 assert_eq!(reg.ids(), vec!["acme"]);
246 let owned: Option<Box<dyn Harness>> = reg.into_by_id("acme");
247 assert!(owned.is_some(), "into_by_id must hand back the owned box");
248 }
249
250 #[test]
251 fn discover_probes_readiness_of_every_registered_harness() {
252 let readiness = Registry::new().register_boxed(Box::new(Acme)).discover();
253 assert_eq!(readiness.len(), 1);
254 assert_eq!(readiness[0].harness_id, "acme");
255 assert!(readiness[0].ready);
256 }
257}