lean_ctx/core/ocla/adapters/
registry.rs1use 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#[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#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct AdapterHealth {
39 pub key: AdapterKey,
40 pub healthy: bool,
41}
42
43pub 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 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 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 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 pub fn register_arc(&self, adapter: Arc<dyn CapabilityAdapter>) -> OclaResult<()> {
117 self.register_record(adapter.manifest().clone(), Some(adapter), None, None)
118 }
119
120 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 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 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 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 pub fn get(&self, capability_id: &str, version: &str) -> Option<Arc<dyn CapabilityAdapter>> {
246 self.lookup(capability_id, version)
247 }
248
249 pub fn health_check_all(&self) -> OclaResult<Vec<AdapterHealth>> {
251 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 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 pub fn list_available(&self) -> Vec<CapabilityManifestV1> {
275 self.list_available_adapters()
276 }
277
278 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 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 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 = ®istry.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 = ®istry.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 = ®istry.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 = ®istry.keys();
424 assert!(found.is_empty(), "{found:?}");
425 }
426 {
427 let found = ®istry.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}