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`](crate::Harness) in their own crate and calling
8//! [`Registry::register`] — no fork of this crate required.
9
10use 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
18/// The identifier used when the caller doesn't pick one — the bob adapter,
19/// the conventional default. (A literal so it's available even in builds
20/// that compile without the `bob` feature; hosts override as needed.)
21pub const DEFAULT_HARNESS_ID: &str = "bob";
22
23/// An open set of harnesses. Build it with the ones you want — the
24/// built-ins (`Bob`/`Claude`/`Codex`) and/or your own:
25///
26/// ```no_run
27/// use harness::Registry;
28/// let reg = Registry::new()
29///     .register(harness::Bob::new());
30///     // .register(MyCustomHarness::new())   // your own impl Harness
31/// assert!(reg.by_id("bob").is_some());
32/// ```
33#[derive(Default)]
34pub struct Registry {
35    harnesses: Vec<Box<dyn Harness>>,
36}
37
38impl Registry {
39    /// An empty registry.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Add a harness. Chainable. Registration order is preserved (it's the
45    /// UI display order; the first registered is the conventional default).
46    pub fn register(mut self, harness: impl Harness + 'static) -> Self {
47        self.harnesses.push(Box::new(harness));
48        self
49    }
50
51    /// Resolve a harness by its [`HarnessInfo::id`].
52    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    /// Metadata for every registered harness, in registration order.
60    pub fn catalog(&self) -> Vec<HarnessInfo> {
61        self.harnesses.iter().map(|h| h.info()).collect()
62    }
63
64    /// The ids of every registered harness, in registration order.
65    pub fn ids(&self) -> Vec<String> {
66        self.harnesses.iter().map(|h| h.info().id).collect()
67    }
68}
69
70/// A [`Registry`] of the built-in adapters compiled into this build
71/// (bob / claude / codex), in display order.
72pub 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
90/// Resolve a *built-in* harness by id, as an owned box — convenience for
91/// hosts that look one up per call. Returns `None` for an unknown id.
92pub 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
115/// Metadata for every built-in harness — the payload the UI picker renders.
116pub 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);
148        assert!(bob.models.is_empty() && !bob.supports_effort && !bob.supports_max_turns);
149        assert_eq!(
150            bob.credential_required,
151            harness_by_id("bob").unwrap().credential().required
152        );
153
154        let claude = caps("claude");
155        assert!(!claude.credential_required && !claude.previews_edits);
156        assert!(!claude.models.is_empty() && !claude.allows_custom_model);
157        assert!(claude.supports_max_turns && !claude.supports_effort);
158
159        let codex = caps("codex");
160        assert!(!codex.credential_required && !codex.previews_edits);
161        assert!(codex.allows_custom_model && codex.supports_effort && !codex.supports_max_turns);
162
163        assert!(claude.supports_login && codex.supports_login && !bob.supports_login);
164    }
165
166    // A third-party / custom provider — proves the registry is open: this
167    // type lives "outside" the built-ins yet registers + resolves the same.
168    struct Acme;
169    impl Harness for Acme {
170        fn info(&self) -> HarnessInfo {
171            HarnessInfo {
172                id: "acme".to_owned(),
173                display_name: "Acme".to_owned(),
174                description: "A custom third-party harness.".to_owned(),
175                requires_install: false,
176                capabilities: HarnessCapabilities {
177                    credential_required: false,
178                    previews_edits: false,
179                    models: Vec::new(),
180                    allows_custom_model: true,
181                    supports_effort: false,
182                    supports_max_turns: false,
183                    supports_login: false,
184                },
185            }
186        }
187        fn readiness(&self) -> HarnessReadiness {
188            HarnessReadiness {
189                harness_id: "acme".to_owned(),
190                ready: true,
191                installed: true,
192                version: None,
193                auth_configured: true,
194                error: None,
195                details: serde_json::Value::Null,
196            }
197        }
198        fn install(&self, _on_event: InstallCallback) -> Result<(), crate::HarnessError> {
199            Ok(())
200        }
201        fn run(
202            &self,
203            _req: RunRequest,
204            _on_event: RunCallback,
205        ) -> Result<RunHandle, crate::HarnessError> {
206            // A real API-backed harness would call its HTTP endpoint here and
207            // emit RunEvents through `on_event`; the dummy never runs.
208            Err(crate::HarnessError::Other(
209                "acme: run not implemented in test".to_owned(),
210            ))
211        }
212        fn credential(&self) -> CredentialSpec {
213            CredentialSpec {
214                label: "Acme key".to_owned(),
215                keychain_service: "acme".to_owned(),
216                keychain_account: "ACME_API_KEY".to_owned(),
217                required: false,
218            }
219        }
220    }
221
222    #[test]
223    fn custom_harness_registers_and_resolves_alongside_builtins() {
224        let reg = Registry::new().register(Bob::new()).register(Acme);
225        assert!(reg.by_id("bob").is_some());
226        assert!(reg.by_id("acme").is_some(), "custom harness must resolve");
227        assert_eq!(reg.ids(), vec!["bob", "acme"]);
228    }
229}