Skip to main content

lean_ctx/core/ocla/adapters/
registry.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Versioned registry for common capability adapters.
3
4use std::collections::BTreeMap;
5use std::sync::{Arc, RwLock};
6
7use lean_ctx_protocol::{CapabilityKind, CapabilityManifestV1};
8
9use super::super::capability_fabric::normalize_manifest;
10use super::super::catalogue::{CatalogueEntry, ModelEntry, ProviderEntry, TechnicalCatalogue};
11use super::super::invocation::CapabilityAdapter;
12use crate::core::ocla::{OclaError, OclaResult};
13
14/// Stable lookup key for an adapter manifest.
15#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
16pub struct AdapterKey {
17    pub capability_id: String,
18    pub version: String,
19}
20
21impl AdapterKey {
22    #[must_use]
23    pub fn new(capability_id: impl Into<String>, version: impl Into<String>) -> Self {
24        Self {
25            capability_id: capability_id.into(),
26            version: version.into(),
27        }
28    }
29
30    #[must_use]
31    pub fn as_string(&self) -> String {
32        format!("{}@{}", self.capability_id, self.version)
33    }
34}
35
36/// Result of checking one adapter's health.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct AdapterHealth {
39    pub key: AdapterKey,
40    pub healthy: bool,
41}
42
43/// Thread-safe registry keyed by capability ID and manifest version.
44pub struct AdapterRegistry {
45    adapters: RwLock<BTreeMap<AdapterKey, Registration>>,
46    initialization_error: Option<String>,
47}
48
49#[derive(Clone)]
50struct Registration {
51    manifest: CapabilityManifestV1,
52    adapter: Option<Arc<dyn CapabilityAdapter>>,
53    model: Option<ModelEntry>,
54    provider: Option<ProviderEntry>,
55}
56
57impl Registration {
58    fn healthy(&self) -> bool {
59        self.adapter.as_ref().is_some_and(|adapter| {
60            // A mutable implementation cannot silently replace its registered contract.
61            normalize_manifest(adapter.manifest().clone()).ok().as_ref() == Some(&self.manifest)
62                && adapter.health_check().unwrap_or(false)
63                && self
64                    .manifest
65                    .support_matrix
66                    .values()
67                    .any(|surface| surface.supported)
68        })
69    }
70}
71
72impl AdapterRegistry {
73    #[must_use]
74    pub fn new() -> Self {
75        Self {
76            adapters: RwLock::new(BTreeMap::new()),
77            initialization_error: None,
78        }
79    }
80
81    /// Required adapters are installed atomically, before the registry is shared.
82    /// A failed set remains unavailable instead of exposing a partial catalogue.
83    pub(in crate::core::ocla) fn with_required_adapters<const N: usize>(
84        required: [Arc<dyn CapabilityAdapter>; N],
85    ) -> Self {
86        let registry = Self::new();
87        for adapter in required {
88            if let Err(error) = registry.register_arc(adapter) {
89                return Self {
90                    adapters: RwLock::new(BTreeMap::new()),
91                    initialization_error: Some(error.to_string()),
92                };
93            }
94        }
95        registry
96    }
97
98    fn ensure_initialized(&self) -> OclaResult<()> {
99        match &self.initialization_error {
100            Some(error) => Err(OclaError::InvalidRequest(format!(
101                "required adapter initialization failed: {error}"
102            ))),
103            None => Ok(()),
104        }
105    }
106
107    /// Register one adapter, rejecting invalid or duplicate manifests.
108    pub fn register<A>(&self, adapter: A) -> OclaResult<()>
109    where
110        A: CapabilityAdapter + 'static,
111    {
112        self.register_arc(Arc::new(adapter))
113    }
114
115    /// Register an already shared adapter object.
116    pub fn register_arc(&self, adapter: Arc<dyn CapabilityAdapter>) -> OclaResult<()> {
117        self.register_record(adapter.manifest().clone(), Some(adapter), None, None)
118    }
119
120    /// Associate a real runtime adapter with its public technical model card.
121    pub fn register_model(
122        &self,
123        adapter: Arc<dyn CapabilityAdapter>,
124        model: ModelEntry,
125    ) -> OclaResult<()> {
126        self.register_record(adapter.manifest().clone(), Some(adapter), Some(model), None)
127    }
128
129    /// Associate a real runtime adapter with public provider metadata.
130    pub fn register_provider(
131        &self,
132        adapter: Arc<dyn CapabilityAdapter>,
133        provider: ProviderEntry,
134    ) -> OclaResult<()> {
135        self.register_record(
136            adapter.manifest().clone(),
137            Some(adapter),
138            None,
139            Some(provider),
140        )
141    }
142
143    /// Record a technical descriptor without claiming an installed runtime.
144    /// Addons and other descriptors stay unavailable until an actual adapter is registered.
145    pub fn register_descriptor(&self, manifest: CapabilityManifestV1) -> OclaResult<()> {
146        self.register_record(manifest, None, None, None)
147    }
148
149    fn register_record(
150        &self,
151        manifest: CapabilityManifestV1,
152        adapter: Option<Arc<dyn CapabilityAdapter>>,
153        model: Option<ModelEntry>,
154        mut provider: Option<ProviderEntry>,
155    ) -> OclaResult<()> {
156        self.ensure_initialized()?;
157        let manifest = normalize_manifest(manifest).map_err(|error| {
158            OclaError::InvalidRequest(format!("invalid capability manifest: {error}"))
159        })?;
160        if model.as_ref().is_some_and(|model| {
161            manifest.kind != CapabilityKind::Model
162                || model.model_id.trim().is_empty()
163                || model.context_window == 0
164        }) {
165            return Err(OclaError::InvalidRequest(
166                "model card requires a model capability and nonempty identity/window".to_owned(),
167            ));
168        }
169        if let Some(provider) = &mut provider {
170            if !matches!(
171                manifest.kind,
172                CapabilityKind::Provider | CapabilityKind::ModelProvider
173            ) || provider.provider_id != manifest.provider
174                || provider
175                    .models_available
176                    .iter()
177                    .chain(&provider.regions)
178                    .any(|value| value.trim().is_empty())
179            {
180                return Err(OclaError::InvalidRequest(
181                    "provider metadata does not bind its capability".to_owned(),
182                ));
183            }
184            provider.models_available.sort();
185            provider.models_available.dedup();
186            provider.regions.sort();
187            provider.regions.dedup();
188        }
189        let key = AdapterKey::new(manifest.capability_id.as_str(), manifest.version.clone());
190        let mut adapters = self
191            .adapters
192            .write()
193            .map_err(|_| OclaError::InvalidRequest("adapter registry lock poisoned".into()))?;
194        if adapters.get(&key).is_some_and(|existing| {
195            existing.adapter.is_some() || adapter.is_none() || existing.manifest != manifest
196        }) {
197            return Err(OclaError::InvalidRequest(format!(
198                "adapter already registered: {}",
199                key.as_string()
200            )));
201        }
202        if model.as_ref().is_some_and(|model| {
203            adapters.iter().any(|(other_key, entry)| {
204                other_key != &key
205                    && entry
206                        .model
207                        .as_ref()
208                        .is_some_and(|other| other.model_id == model.model_id)
209            })
210        }) || provider.as_ref().is_some_and(|provider| {
211            adapters.iter().any(|(other_key, entry)| {
212                other_key != &key
213                    && entry
214                        .provider
215                        .as_ref()
216                        .is_some_and(|other| other.provider_id == provider.provider_id)
217            })
218        }) {
219            return Err(OclaError::InvalidRequest(
220                "ambiguous technical catalogue identity".to_owned(),
221            ));
222        }
223        adapters.insert(
224            key,
225            Registration {
226                manifest,
227                adapter,
228                model,
229                provider,
230            },
231        );
232        Ok(())
233    }
234
235    /// Look up an adapter by its capability ID and exact version.
236    pub fn lookup(&self, capability_id: &str, version: &str) -> Option<Arc<dyn CapabilityAdapter>> {
237        let key = AdapterKey::new(capability_id, version);
238        self.adapters
239            .read()
240            .ok()
241            .and_then(|adapters| adapters.get(&key).and_then(|entry| entry.adapter.clone()))
242    }
243
244    /// Alias matching the existing provider registry vocabulary.
245    pub fn get(&self, capability_id: &str, version: &str) -> Option<Arc<dyn CapabilityAdapter>> {
246        self.lookup(capability_id, version)
247    }
248
249    /// Health-check every registered adapter in deterministic key order.
250    pub fn health_check_all(&self) -> OclaResult<Vec<AdapterHealth>> {
251        // Invoke third-party callbacks after releasing the registry lock.
252        self.snapshot()?
253            .into_iter()
254            .map(|(key, registration)| {
255                Ok(AdapterHealth {
256                    key,
257                    healthy: registration.healthy(),
258                })
259            })
260            .collect()
261    }
262
263    /// Return healthy executable manifests, sorted by `(capability_id, version)`.
264    pub fn list_available_adapters(&self) -> Vec<CapabilityManifestV1> {
265        self.snapshot()
266            .unwrap_or_default()
267            .into_iter()
268            .filter(|(_, registration)| registration.healthy())
269            .map(|(_, registration)| registration.manifest)
270            .collect()
271    }
272
273    /// Short alias for discovery callers.
274    pub fn list_available(&self) -> Vec<CapabilityManifestV1> {
275        self.list_available_adapters()
276    }
277
278    /// List exact registry keys without exposing adapter implementations.
279    pub fn keys(&self) -> Vec<AdapterKey> {
280        self.adapters
281            .read()
282            .map(|adapters| adapters.keys().cloned().collect())
283            .unwrap_or_default()
284    }
285
286    #[must_use]
287    pub fn len(&self) -> usize {
288        self.adapters.read().map_or(0, |adapters| adapters.len())
289    }
290
291    #[must_use]
292    pub fn is_empty(&self) -> bool {
293        self.len() == 0
294    }
295
296    fn snapshot(&self) -> OclaResult<Vec<(AdapterKey, Registration)>> {
297        self.ensure_initialized()?;
298        self.adapters
299            .read()
300            .map(|entries| {
301                entries
302                    .iter()
303                    .map(|(key, entry)| (key.clone(), entry.clone()))
304                    .collect()
305            })
306            .map_err(|_| OclaError::InvalidRequest("adapter registry lock poisoned".to_owned()))
307    }
308
309    /// One deterministic catalogue derived from registered contracts and live adapter health.
310    pub fn technical_catalogue(&self) -> OclaResult<TechnicalCatalogue> {
311        let mut catalogue = TechnicalCatalogue::default();
312        for (_, entry) in self.snapshot()? {
313            let available = entry.healthy();
314            // Only callable runtime registrations enter the scheduler's model/provider lists.
315            if available {
316                catalogue.models.extend(entry.model);
317                catalogue.providers.extend(entry.provider);
318            }
319            catalogue.capabilities.push(CatalogueEntry {
320                capability_id: entry.manifest.capability_id.as_str().to_owned(),
321                version: entry.manifest.version.clone(),
322                manifest: entry.manifest,
323                available,
324            });
325        }
326        catalogue
327            .models
328            .sort_by(|left, right| left.model_id.cmp(&right.model_id));
329        catalogue
330            .providers
331            .sort_by(|left, right| left.provider_id.cmp(&right.provider_id));
332        Ok(catalogue)
333    }
334}
335
336impl Default for AdapterRegistry {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::core::ocla::adapters::PassthroughAdapter;
346    use std::collections::BTreeSet;
347
348    #[test]
349    fn failed_required_set_exposes_no_partial_registry_and_cannot_be_reopened() {
350        let manifest = PassthroughAdapter::new().manifest().clone();
351        let registry = AdapterRegistry::with_required_adapters([
352            Arc::new(super::super::NativeContextAdapter::new()),
353            Arc::new(PassthroughAdapter::new()),
354            Arc::new(PassthroughAdapter::new()),
355        ]);
356        assert!(registry.is_empty());
357        {
358            let found = &registry.keys();
359            assert!(found.is_empty(), "{found:?}");
360        }
361        assert!(
362            registry
363                .lookup(manifest.capability_id.as_str(), &manifest.version)
364                .is_none()
365        );
366        {
367            let found = &registry.list_available();
368            assert!(found.is_empty(), "{found:?}");
369        }
370        assert!(registry.health_check_all().is_err());
371        assert!(registry.technical_catalogue().is_err());
372        assert!(registry.register(PassthroughAdapter::new()).is_err());
373        assert!(registry.register_descriptor(manifest).is_err());
374        assert!(registry.is_empty());
375    }
376
377    #[test]
378    fn duplicate_versions_are_rejected() {
379        let registry = AdapterRegistry::new();
380        registry
381            .register(PassthroughAdapter::new())
382            .expect("first registration");
383        assert!(registry.register(PassthroughAdapter::new()).is_err());
384    }
385
386    #[test]
387    fn descriptor_is_not_executable_until_matching_adapter_is_registered() {
388        let registry = AdapterRegistry::new();
389        let adapter = PassthroughAdapter::new();
390        let manifest = adapter.manifest().clone();
391        registry.register_descriptor(manifest.clone()).unwrap();
392        assert!(
393            registry
394                .lookup(manifest.capability_id.as_str(), &manifest.version)
395                .is_none()
396        );
397        {
398            let found = &registry.list_available();
399            assert!(found.is_empty(), "{found:?}");
400        }
401        assert!(!registry.technical_catalogue().unwrap().capabilities[0].available);
402        assert!(!registry.health_check_all().unwrap()[0].healthy);
403        registry.register(adapter).unwrap();
404        assert_eq!(registry.list_available().len(), 1);
405        assert!(registry.technical_catalogue().unwrap().capabilities[0].available);
406        assert!(registry.health_check_all().unwrap()[0].healthy);
407    }
408
409    #[test]
410    fn invalid_descriptor_is_rejected_before_registry_mutation() {
411        let registry = AdapterRegistry::new();
412        let mut manifest = PassthroughAdapter::new().manifest().clone();
413        manifest.support_matrix.clear();
414        assert!(registry.register_descriptor(manifest).is_err());
415        assert!(registry.is_empty());
416    }
417
418    #[test]
419    fn empty_registry_is_safe_to_inspect() {
420        let registry = AdapterRegistry::new();
421        assert!(registry.is_empty());
422        {
423            let found = &registry.keys();
424            assert!(found.is_empty(), "{found:?}");
425        }
426        {
427            let found = &registry.health_check_all().expect("health checks");
428            assert!(found.is_empty(), "{found:?}");
429        }
430    }
431
432    #[test]
433    fn key_order_is_deterministic() {
434        let keys = BTreeSet::from([
435            AdapterKey::new("capability://b", "1.0.0"),
436            AdapterKey::new("capability://a", "1.0.0"),
437        ]);
438        assert_eq!(
439            keys.into_iter().next().expect("first key").capability_id,
440            "capability://a"
441        );
442    }
443}