Skip to main content

harness/
registry.rs

1//! The harness registry — an **open** builder so consumers compose their
2//! own set of harnesses (the built-ins *and/or* their own custom
3//! `impl Harness`), plus convenience constructors over the built-in
4//! adapters for hosts that just want "all of them".
5//!
6//! This is the extensibility seam: a third party adds a provider by
7//! implementing [`Harness`] in their own crate and calling
8//! [`Registry::register`] — no fork of this crate required.
9
10use 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/// One row of a picker: who a harness is, and what it can do.
19///
20/// These are separate questions to the [`Harness`] trait — identity is asked
21/// once, capability constantly — but a UI needs both at the same moment, and
22/// this is the shape that crosses to it. Serializable and `camelCase`, so a
23/// host hands it to a frontend unchanged.
24#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Listing {
27    pub manifest: Info,
28    pub capabilities: Features,
29}
30
31/// The identifier used when the caller doesn't pick one. (A literal so it's
32/// available even in builds compiled without the `claude` feature; hosts
33/// override as needed.)
34pub const DEFAULT_HARNESS_ID: &str = "claude";
35
36/// An open set of harnesses. Build it with the ones you want — the
37/// built-ins (`Claude`/`Codex`) and/or your own:
38///
39/// ```no_run
40/// use harness::Registry;
41/// let reg = Registry::new()
42///     .register(harness::Claude::new());
43///     // .register(MyCustomHarness::new())   // your own impl Harness
44/// assert!(reg.by_id("claude").is_some());
45/// ```
46#[derive(Default)]
47pub struct Registry {
48    harnesses: Vec<Box<dyn Harness>>,
49}
50
51impl Registry {
52    /// An empty registry.
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Add a harness. Chainable. Registration order is preserved (it's the
58    /// UI display order; the first registered is the conventional default).
59    pub fn register(mut self, harness: impl Harness + 'static) -> Self {
60        self.harnesses.push(Box::new(harness));
61        self
62    }
63
64    /// Add an already-boxed harness — like [`register`](Registry::register) but
65    /// for a `Box<dyn Harness>` a host built behind the trait object (e.g. its
66    /// configured providers). Chainable.
67    pub fn register_boxed(mut self, harness: Box<dyn Harness>) -> Self {
68        self.harnesses.push(harness);
69        self
70    }
71
72    /// Resolve a harness by its [`Info::id`].
73    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    /// Resolve a harness by id, taking ownership of its box out of the registry —
81    /// for a host that needs an owned `Box<dyn Harness>` to hold across a run,
82    /// rather than the borrow [`by_id`](Registry::by_id) returns.
83    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    /// Probe readiness of every registered harness, in registration order — the
88    /// "what's actually on this machine" discovery a picker renders. Each probe
89    /// may shell out; treat as blocking and run it off the UI thread.
90    pub fn discover(&self) -> Vec<Readiness> {
91        self.harnesses.iter().map(|h| h.readiness()).collect()
92    }
93
94    /// Every registered harness, in registration order, as a picker renders it:
95    /// who it is and what it supports.
96    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    /// The ids of every registered harness, in registration order.
104    pub fn ids(&self) -> Vec<String> {
105        self.harnesses.iter().map(|h| h.info().id).collect()
106    }
107}
108
109/// A [`Registry`] of the built-in adapters compiled into this build
110/// (claude / codex), in display order.
111pub 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
125/// Resolve a *built-in* harness by id, as an owned box — convenience for
126/// hosts that look one up per call. Returns `None` for an unknown id.
127pub 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
144/// Metadata for every built-in harness — the payload the UI picker renders.
145pub 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    // A third-party / custom provider — proves the registry is open: this
187    // type lives "outside" the built-ins yet registers + resolves the same.
188    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            // A real API-backed harness would call its HTTP endpoint here and
219            // emit RunEvents through `on_event`; the dummy never runs.
220            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}