1use std::{
4 collections::{BTreeMap, HashMap, VecDeque},
5 path::{Path, PathBuf},
6 sync::{
7 Arc, RwLock,
8 atomic::{AtomicBool, Ordering},
9 },
10};
11
12use async_trait::async_trait;
13use serde::{Deserialize, Serialize};
14use serde_json::{Value, json};
15use soma_domain::provider_validation::{ProviderValidationError, validate_provider_manifest};
16use soma_provider_adapters::python::materializer::PreparedPythonEnvironment;
17use soma_provider_core::{
18 ProviderCall as CoreProviderCall, ProviderCatalog, ProviderRegistry as CoreRegistry,
19 ProviderResource, RegistrySnapshot as CoreRegistrySnapshot,
20};
21
22use crate::{
23 capabilities::CapabilityBroker,
24 provider_errors::ProviderError,
25 providers::filesystem::{FileProviderSource, PythonProviderEnvironmentSelections},
26};
27
28mod dispatch_lease;
29mod enforcement;
30mod generations;
31#[path = "provider_registry_map.rs"]
32mod map;
33use map::provider_map;
34#[path = "provider_registry_openapi.rs"]
35mod openapi;
36mod python_operations;
37mod refresh;
38mod reports;
39mod resources;
40use dispatch_lease::DispatchLease;
41pub(super) use enforcement::provider_tool_surface_enabled;
42use enforcement::{enforce_capabilities, enforce_pre_input, enforce_response_limit};
43use generations::{
44 StoredGeneration, publish_generation, settle_transition, settle_transition_async,
45};
46use openapi::openapi_paths_from_core;
47use refresh::{ProviderRefreshEvent, fingerprint_file_source};
48use resources::ResourceIndex;
49pub use resources::{DynamicResourceTemplate, ResourceReadOutput};
50pub use soma_provider_core::{Provider as CoreProvider, ProviderOutput};
51pub type ProviderInvocation = CoreProviderCall;
54
55#[async_trait]
58pub trait Provider: Send + Sync {
59 fn catalog(&self) -> ProviderCatalog;
61
62 async fn call(&self, call: ProviderCall) -> Result<ProviderOutput, ProviderError>;
64
65 async fn retire(&self) {}
68
69 async fn suspend(&self) {}
71
72 fn runtime_status(&self) -> Option<Value> {
74 None
75 }
76
77 fn cancel_active(&self) -> bool {
79 false
80 }
81
82 async fn reset_quarantine(&self) {}
84
85 fn deactivate(&self) {}
87
88 fn activate(&self) {}
90
91 fn acquire_dispatch(&self) -> bool {
93 true
94 }
95
96 fn release_dispatch(&self) {}
98
99 fn dynamic_resource_templates(&self) -> Vec<DynamicResourceTemplate> {
103 Vec::new()
104 }
105
106 fn supports_resource_reads(&self) -> bool {
116 false
117 }
118
119 async fn read_resource(
124 &self,
125 uri: &str,
126 params: &BTreeMap<String, String>,
127 ) -> Result<ResourceReadOutput, ProviderError> {
128 let _ = params;
129 Err(ProviderError::validation(
130 &self.catalog().provider.name,
131 uri,
132 "resource_read_not_supported",
133 "this provider does not support resource reads",
134 ))
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "kebab-case")]
141pub enum ProviderSurface {
142 Mcp,
144 Rest,
146 Cli,
148 Palette,
150}
151
152impl ProviderSurface {
153 pub fn as_str(self) -> &'static str {
155 match self {
156 Self::Mcp => "mcp",
157 Self::Rest => "rest",
158 Self::Cli => "cli",
159 Self::Palette => "palette",
160 }
161 }
162
163 fn core(self) -> soma_provider_core::ProviderSurface {
164 match self {
165 Self::Mcp => soma_provider_core::ProviderSurface::Mcp,
166 Self::Rest => soma_provider_core::ProviderSurface::Rest,
167 Self::Cli => soma_provider_core::ProviderSurface::Cli,
168 Self::Palette => soma_provider_core::ProviderSurface::Palette,
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum ProviderAuthMode {
176 LoopbackDev,
178 TrustedGateway,
180 Mounted,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct ProviderPrincipal {
187 pub subject: String,
189 pub scopes: Vec<String>,
191}
192
193impl ProviderPrincipal {
194 pub fn loopback_dev() -> Self {
197 Self {
198 subject: "loopback-dev".to_owned(),
199 scopes: vec![soma_domain::actions::READ_SCOPE.to_owned()],
200 }
201 }
202
203 pub fn anonymous() -> Self {
205 Self {
206 subject: "anonymous".to_owned(),
207 scopes: Vec::new(),
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct ProviderRequestLimits {
215 pub max_input_bytes: usize,
217 pub max_response_bytes: usize,
219}
220
221impl Default for ProviderRequestLimits {
222 fn default() -> Self {
223 Self {
224 max_input_bytes: 64 * 1024,
225 max_response_bytes: soma_domain::token_limit::MAX_RESPONSE_BYTES,
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
233pub struct ProviderCall {
234 pub provider: String,
236 pub action: String,
238 pub params: Value,
240 pub principal: ProviderPrincipal,
242 pub auth_mode: ProviderAuthMode,
244 pub surface: ProviderSurface,
246 pub destructive_confirmed: bool,
248 pub limits: ProviderRequestLimits,
250 pub snapshot_id: String,
252 pub request_id: String,
254 pub traceparent: Option<String>,
256 pub tracestate: Option<String>,
258 pub progress: soma_provider_core::ProviderProgressReporter,
260}
261
262impl ProviderCall {
263 pub fn provider_invocation(&self) -> ProviderInvocation {
265 self.provider_invocation_with_progress(self.progress.clone())
266 }
267
268 pub fn provider_invocation_with_progress(
270 &self,
271 progress: soma_provider_core::ProviderProgressReporter,
272 ) -> ProviderInvocation {
273 ProviderInvocation {
274 provider: self.provider.clone(),
275 action: self.action.clone(),
276 params: self.params.clone(),
277 surface: self.surface.core(),
278 snapshot_id: self.snapshot_id.clone(),
279 context: soma_provider_core::ProviderInvocationContext {
280 request_id: self.request_id.clone(),
281 actor_id: (!self.principal.subject.is_empty())
282 .then(|| self.principal.subject.clone()),
283 actor_scopes: self.principal.scopes.clone(),
284 traceparent: self.traceparent.clone(),
285 tracestate: self.tracestate.clone(),
286 progress,
287 },
288 }
289 }
290
291 pub fn execution_envelope(&self) -> ProviderExecutionEnvelope {
293 ProviderExecutionEnvelope {
294 schema_version: 1,
295 provider: self.provider.clone(),
296 action: self.action.clone(),
297 params: self.params.clone(),
298 surface: self.surface,
299 snapshot_id: self.snapshot_id.clone(),
300 }
301 }
302
303 pub fn execution_payload(&self) -> Result<Vec<u8>, serde_json::Error> {
305 serde_json::to_vec(&self.execution_envelope())
306 }
307}
308
309#[derive(Debug, Clone, Serialize)]
312pub struct ProviderExecutionEnvelope {
313 pub schema_version: u32,
315 pub provider: String,
317 pub action: String,
319 pub params: Value,
321 pub surface: ProviderSurface,
323 pub snapshot_id: String,
325}
326
327#[derive(Clone)]
330pub struct RegistrySnapshot {
331 pub id: String,
333 pub fingerprint: String,
335 pub catalogs: Vec<ProviderCatalog>,
337 core: Arc<CoreRegistrySnapshot>,
338 exact_resources: HashMap<String, (String, ProviderResource)>,
339 dynamic_resources: Vec<(String, DynamicResourceTemplate)>,
340 pub compiled_validator_count: usize,
342 pub cached_openapi_bytes: Arc<Vec<u8>>,
344 pub cached_catalog_summary: Arc<Value>,
346}
347
348impl RegistrySnapshot {
349 pub fn action_names(&self) -> Vec<&str> {
351 self.core.action_names().collect()
352 }
353
354 pub fn route_action(&self, method: &str, path: &str) -> Option<&str> {
356 self.core.route_action(method, path)
357 }
358
359 pub fn cli_action(&self, command: &str) -> Option<&str> {
361 self.core.cli_action(command)
362 }
363
364 pub fn primitive_kind(&self, name: &str) -> Option<&str> {
366 self.core.primitive_kind(name)
367 }
368
369 pub fn action_requires_confirmation(&self, action: &str) -> bool {
371 self.core
372 .tool(action)
373 .map(|entry| entry.spec().destructive)
374 .unwrap_or(false)
375 }
376
377 pub fn provider_for_action(&self, action: &str) -> Option<&str> {
379 self.core
380 .tool(action)
381 .map(|entry| entry.provider_id().as_str())
382 }
383
384 pub fn core_snapshot(&self) -> &CoreRegistrySnapshot {
386 &self.core
387 }
388
389 pub(crate) fn rest_routes(&self) -> impl Iterator<Item = (&str, &str, &str)> {
390 self.core.rest_routes()
391 }
392}
393
394#[derive(Clone)]
397pub struct ProviderRegistry {
398 state: Arc<RwLock<RegistryState>>,
399 refresh_gate: Arc<tokio::sync::Mutex<()>>,
400 refresh_dirty: Arc<AtomicBool>,
401 capabilities: CapabilityBroker,
402 base_providers: Arc<Vec<Arc<dyn Provider>>>,
403 file_source: Option<FileProviderSource>,
404}
405
406struct RegistryState {
407 providers: BTreeMap<String, Arc<dyn Provider>>,
408 core_registry: CoreRegistry,
409 snapshot: Arc<RegistrySnapshot>,
410 file_fingerprint: Option<String>,
411 python_environments: PythonProviderEnvironmentSelections,
412 generation_id: u64,
413 history: VecDeque<StoredGeneration>,
414}
415
416const REFRESH_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(75);
417
418impl ProviderRegistry {
419 #[must_use]
421 pub fn file_provider_root(&self) -> Option<&std::path::Path> {
422 self.file_source.as_ref().map(FileProviderSource::root)
423 }
424
425 pub fn new(providers: Vec<Arc<dyn Provider>>) -> Result<Self, ProviderValidationError> {
428 Self::with_capabilities(providers, CapabilityBroker::default_deny())
429 }
430
431 pub fn with_capabilities(
433 providers: Vec<Arc<dyn Provider>>,
434 capabilities: CapabilityBroker,
435 ) -> Result<Self, ProviderValidationError> {
436 let (providers, core_registry, snapshot) = build_registry(providers)?;
437 Ok(Self {
438 state: Arc::new(RwLock::new(RegistryState {
439 providers,
440 core_registry,
441 snapshot,
442 file_fingerprint: None,
443 python_environments: PythonProviderEnvironmentSelections::new(),
444 generation_id: 1,
445 history: VecDeque::new(),
446 })),
447 refresh_gate: Arc::new(tokio::sync::Mutex::new(())),
448 refresh_dirty: Arc::new(AtomicBool::new(false)),
449 capabilities,
450 base_providers: Arc::new(Vec::new()),
451 file_source: None,
452 })
453 }
454
455 pub fn with_file_source(
458 providers: Vec<Arc<dyn Provider>>,
459 capabilities: CapabilityBroker,
460 file_source: FileProviderSource,
461 ) -> Result<Self, ProviderValidationError> {
462 let file_fingerprint = file_source.fingerprint().map_err(|error| {
463 ProviderValidationError::new("provider_file_load_failed", error.to_string())
464 })?;
465 let dynamic_providers = file_source.load().map_err(|error| {
466 ProviderValidationError::new("provider_file_load_failed", error.to_string())
467 })?;
468 let base_providers = Arc::new(providers);
469 let mut all_providers = base_providers.iter().cloned().collect::<Vec<_>>();
470 all_providers.extend(dynamic_providers);
471 let (providers, core_registry, snapshot) = build_registry(all_providers)?;
472 Ok(Self {
473 state: Arc::new(RwLock::new(RegistryState {
474 providers,
475 core_registry,
476 snapshot,
477 file_fingerprint: Some(file_fingerprint),
478 python_environments: PythonProviderEnvironmentSelections::new(),
479 generation_id: 1,
480 history: VecDeque::new(),
481 })),
482 refresh_gate: Arc::new(tokio::sync::Mutex::new(())),
483 refresh_dirty: Arc::new(AtomicBool::new(false)),
484 capabilities,
485 base_providers,
486 file_source: Some(file_source),
487 })
488 }
489
490 pub async fn with_file_source_async(
493 providers: Vec<Arc<dyn Provider>>,
494 capabilities: CapabilityBroker,
495 file_source: FileProviderSource,
496 ) -> Result<Self, ProviderValidationError> {
497 let selections = PythonProviderEnvironmentSelections::new();
498 let file_fingerprint = file_source
499 .fingerprint_with_python_environments(&selections)
500 .map_err(|error| {
501 ProviderValidationError::new("provider_file_load_failed", error.to_string())
502 })?;
503 let dynamic_providers = file_source
504 .load_with_python_environments_async(&selections)
505 .await
506 .map_err(|error| {
507 ProviderValidationError::new("provider_file_load_failed", error.to_string())
508 })?;
509 let base_providers = Arc::new(providers);
510 let mut all_providers = base_providers.iter().cloned().collect::<Vec<_>>();
511 all_providers.extend(dynamic_providers);
512 let (providers, core_registry, snapshot) = build_registry(all_providers)?;
513 Ok(Self {
514 state: Arc::new(RwLock::new(RegistryState {
515 providers,
516 core_registry,
517 snapshot,
518 file_fingerprint: Some(file_fingerprint),
519 python_environments: selections,
520 generation_id: 1,
521 history: VecDeque::new(),
522 })),
523 refresh_gate: Arc::new(tokio::sync::Mutex::new(())),
524 refresh_dirty: Arc::new(AtomicBool::new(false)),
525 capabilities,
526 base_providers,
527 file_source: Some(file_source),
528 })
529 }
530
531 pub fn snapshot(&self) -> Arc<RegistrySnapshot> {
533 self.state
534 .read()
535 .expect("provider registry lock should not be poisoned")
536 .snapshot
537 .clone()
538 }
539
540 pub(crate) fn authorize_candidate_capabilities(
543 &self,
544 catalog: &ProviderCatalog,
545 action: &str,
546 ) -> Result<(), ProviderError> {
547 self.capabilities
548 .authorize(&catalog.provider.name, action, &catalog.capabilities)
549 }
550
551 pub fn refresh_file_providers(&self) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
561 let Some(file_source) = &self.file_source else {
562 return Ok(self.snapshot());
563 };
564 let (baseline_fingerprint, active_environments) = {
565 let state = self
566 .state
567 .read()
568 .expect("provider registry lock should not be poisoned");
569 (
570 state.file_fingerprint.clone(),
571 state.python_environments.clone(),
572 )
573 };
574 let file_fingerprint =
575 match file_source.fingerprint_with_python_environments(&active_environments) {
576 Ok(fingerprint) => fingerprint,
577 Err(error) => {
578 return Ok(self.snapshot_after_refresh_failure(
579 file_source,
580 "provider_file_fingerprint_failed",
581 &error.to_string(),
582 ));
583 }
584 };
585 if baseline_fingerprint.as_deref() == Some(file_fingerprint.as_str()) {
586 return Ok(self.snapshot());
587 }
588
589 let rebuilt: Result<_, ProviderValidationError> = (|| {
590 let dynamic_providers = file_source
591 .load_with_python_environments(&active_environments)
592 .map_err(|error| {
593 ProviderValidationError::new("provider_file_load_failed", error.to_string())
594 })?;
595 let mut providers = self.base_providers.iter().cloned().collect::<Vec<_>>();
596 providers.extend(dynamic_providers);
597 build_registry(providers)
598 })();
599 let (providers, core_registry, snapshot) = match rebuilt {
600 Ok(parts) => parts,
601 Err(error) => {
602 return Ok(self.snapshot_after_refresh_failure(
603 file_source,
604 error.code(),
605 error.message(),
606 ));
607 }
608 };
609
610 let mut state = self
611 .state
612 .write()
613 .expect("provider registry lock should not be poisoned");
614 if state.file_fingerprint != baseline_fingerprint
615 || state.python_environments != active_environments
616 {
617 return Ok(state.snapshot.clone());
618 }
619 let previous = state.snapshot.clone();
620 let public_catalog_changed = previous.fingerprint != snapshot.fingerprint;
621 let transition = publish_generation(
622 &mut state,
623 providers,
624 core_registry,
625 snapshot.clone(),
626 Some(file_fingerprint),
627 active_environments,
628 );
629 if public_catalog_changed {
630 ProviderRefreshEvent::new(&previous, &snapshot).log(file_source.root());
631 }
632 let active = state.snapshot.clone();
633 drop(state);
634 settle_transition(transition);
635 Ok(active)
636 }
637
638 pub async fn refresh_file_providers_async(
641 &self,
642 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
643 let Some(file_source) = &self.file_source else {
644 return Ok(self.snapshot());
645 };
646 let Ok(_refresh_guard) = self.refresh_gate.try_lock() else {
650 self.refresh_dirty.store(true, Ordering::Release);
651 return Ok(self.snapshot());
652 };
653 let mut active = self.snapshot();
654 for pass in 0..2 {
655 self.refresh_dirty.store(false, Ordering::Release);
656 active = self
657 .refresh_file_providers_owned(file_source, false)
658 .await?;
659 if !self.refresh_dirty.swap(false, Ordering::AcqRel) {
660 break;
661 }
662 if pass == 1 {
663 self.refresh_dirty.store(true, Ordering::Release);
667 }
668 }
669 Ok(active)
670 }
671
672 pub(crate) async fn lock_refresh_lane(&self) -> tokio::sync::OwnedMutexGuard<()> {
675 self.refresh_gate.clone().lock_owned().await
676 }
677
678 pub(crate) async fn refresh_file_providers_strict_in_lane(
680 &self,
681 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
682 let Some(file_source) = &self.file_source else {
683 return Ok(self.snapshot());
684 };
685 self.refresh_file_providers_owned(file_source, true).await
686 }
687
688 async fn refresh_file_providers_owned(
689 &self,
690 file_source: &FileProviderSource,
691 strict: bool,
692 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
693 let (baseline_fingerprint, active_environments) = {
694 let state = self
695 .state
696 .read()
697 .expect("provider registry lock should not be poisoned");
698 (
699 state.file_fingerprint.clone(),
700 state.python_environments.clone(),
701 )
702 };
703 let observed_fingerprint =
704 match fingerprint_file_source(file_source.clone(), active_environments.clone()).await {
705 Ok(fingerprint) => fingerprint,
706 Err(error) => {
707 if strict {
708 return Err(ProviderValidationError::new(
709 "provider_file_fingerprint_failed",
710 error.to_string(),
711 ));
712 }
713 return Ok(self.snapshot_after_refresh_failure(
714 file_source,
715 "provider_file_fingerprint_failed",
716 &error.to_string(),
717 ));
718 }
719 };
720 if baseline_fingerprint.as_deref() == Some(observed_fingerprint.as_str()) {
721 return Ok(self.snapshot());
722 }
723 tokio::time::sleep(REFRESH_DEBOUNCE).await;
726 let file_fingerprint =
727 match fingerprint_file_source(file_source.clone(), active_environments.clone()).await {
728 Ok(fingerprint) => fingerprint,
729 Err(error) => {
730 if strict {
731 return Err(ProviderValidationError::new(
732 "provider_file_fingerprint_failed",
733 error.to_string(),
734 ));
735 }
736 return Ok(self.snapshot_after_refresh_failure(
737 file_source,
738 "provider_file_fingerprint_failed",
739 &error.to_string(),
740 ));
741 }
742 };
743 if baseline_fingerprint.as_deref() == Some(file_fingerprint.as_str()) {
744 return Ok(self.snapshot());
745 }
746 let dynamic_providers = match file_source
747 .load_with_python_environments_async(&active_environments)
748 .await
749 {
750 Ok(providers) => providers,
751 Err(error) => {
752 if strict {
753 return Err(ProviderValidationError::new(
754 "provider_file_load_failed",
755 error.to_string(),
756 ));
757 }
758 return Ok(self.snapshot_after_refresh_failure(
759 file_source,
760 "provider_file_load_failed",
761 &error.to_string(),
762 ));
763 }
764 };
765 if strict
766 && let Some((provider, status)) = dynamic_providers.iter().find_map(|provider| {
767 provider
768 .runtime_status()
769 .filter(|status| status.get("ready").and_then(Value::as_bool) == Some(false))
770 .map(|status| (provider.catalog().provider.name, status))
771 })
772 {
773 return Err(ProviderValidationError::new(
774 "provider_runtime_not_ready",
775 format!("provider `{provider}` failed runtime preparation: {status}"),
776 ));
777 }
778 let mut all_providers = self.base_providers.iter().cloned().collect::<Vec<_>>();
779 all_providers.extend(dynamic_providers);
780 let (providers, core_registry, snapshot) = match build_registry(all_providers) {
781 Ok(parts) => parts,
782 Err(error) => {
783 if strict {
784 return Err(error);
785 }
786 return Ok(self.snapshot_after_refresh_failure(
787 file_source,
788 error.code(),
789 error.message(),
790 ));
791 }
792 };
793 let (active, transition) = {
794 let mut state = self
795 .state
796 .write()
797 .expect("provider registry lock should not be poisoned");
798 if state.file_fingerprint != baseline_fingerprint
799 || state.python_environments != active_environments
800 {
801 if strict {
802 return Err(ProviderValidationError::new(
803 "provider_refresh_raced",
804 "provider registry changed during strict refresh",
805 ));
806 }
807 return Ok(state.snapshot.clone());
808 }
809 let previous = state.snapshot.clone();
810 let public_catalog_changed = previous.fingerprint != snapshot.fingerprint;
811 let transition = publish_generation(
812 &mut state,
813 providers,
814 core_registry,
815 snapshot.clone(),
816 Some(file_fingerprint),
817 active_environments,
818 );
819 if public_catalog_changed {
820 ProviderRefreshEvent::new(&previous, &snapshot).log(file_source.root());
821 }
822 (state.snapshot.clone(), transition)
823 };
824 settle_transition_async(transition).await;
825 Ok(active)
826 }
827
828 pub async fn activate_python_candidate(
830 &self,
831 provider_path: &Path,
832 candidate: PreparedPythonEnvironment,
833 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
834 let _refresh_guard = self.refresh_gate.lock().await;
837 let file_source = self.file_source.as_ref().ok_or_else(|| {
838 ProviderValidationError::new(
839 "provider_file_source_unavailable",
840 "Python candidates require a file-backed provider registry",
841 )
842 })?;
843 let provider_path = file_source
844 .resolve_python_provider_path(provider_path)
845 .map_err(|error| {
846 ProviderValidationError::new("provider_candidate_path_invalid", error.to_string())
847 })?;
848 let (baseline_fingerprint, baseline_environments) = {
849 let state = self
850 .state
851 .read()
852 .expect("provider registry lock should not be poisoned");
853 (
854 state.file_fingerprint.clone(),
855 state.python_environments.clone(),
856 )
857 };
858 let mut next_environments = baseline_environments.clone();
859 next_environments.insert(provider_path, candidate);
860
861 let dynamic_providers = file_source
862 .load_with_python_environments_async(&next_environments)
863 .await
864 .map_err(|error| {
865 ProviderValidationError::new(
866 "provider_candidate_validation_failed",
867 error.to_string(),
868 )
869 })?;
870 let file_fingerprint = file_source
871 .fingerprint_with_python_environments(&next_environments)
872 .map_err(|error| {
873 ProviderValidationError::new("provider_file_fingerprint_failed", error.to_string())
874 })?;
875 let mut providers = self.base_providers.iter().cloned().collect::<Vec<_>>();
876 providers.extend(dynamic_providers);
877 let (providers, core_registry, snapshot) = build_registry(providers)?;
878
879 let (active, transition) = {
880 let mut state = self
881 .state
882 .write()
883 .expect("provider registry lock should not be poisoned");
884 if state.file_fingerprint != baseline_fingerprint
885 || state.python_environments != baseline_environments
886 {
887 return Err(ProviderValidationError::new(
888 "provider_candidate_activation_raced",
889 "provider registry changed while the candidate was being validated",
890 ));
891 }
892 let previous = state.snapshot.clone();
893 let public_catalog_changed = previous.fingerprint != snapshot.fingerprint;
894 let transition = publish_generation(
895 &mut state,
896 providers,
897 core_registry,
898 snapshot.clone(),
899 Some(file_fingerprint),
900 next_environments,
901 );
902 if public_catalog_changed {
903 ProviderRefreshEvent::new(&previous, &snapshot).log(file_source.root());
904 }
905 (state.snapshot.clone(), transition)
906 };
907 settle_transition_async(transition).await;
908 Ok(active)
909 }
910
911 fn snapshot_after_refresh_failure(
912 &self,
913 file_source: &FileProviderSource,
914 code: &str,
915 message: &str,
916 ) -> Arc<RegistrySnapshot> {
917 tracing::warn!(
918 root = %file_source.root().display(),
919 code,
920 message,
921 "provider directory refresh failed; keeping the last valid snapshot active"
922 );
923 self.snapshot()
924 }
925
926 pub fn validate_reload(
929 &self,
930 providers: Vec<Arc<dyn Provider>>,
931 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
932 let (_, _, snapshot) = build_registry(providers)?;
933 Ok(snapshot)
934 }
935
936 pub fn reload(
939 &self,
940 providers: Vec<Arc<dyn Provider>>,
941 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
942 let (providers, core_registry, snapshot) = build_registry(providers)?;
943 let mut state = self
944 .state
945 .write()
946 .expect("provider registry lock should not be poisoned");
947 let transition = publish_generation(
948 &mut state,
949 providers,
950 core_registry,
951 snapshot,
952 None,
953 PythonProviderEnvironmentSelections::new(),
954 );
955 let active = state.snapshot.clone();
956 drop(state);
957 settle_transition(transition);
958 Ok(active)
959 }
960
961 pub async fn dispatch(&self, mut call: ProviderCall) -> Result<ProviderOutput, ProviderError> {
965 let (snapshot, core_registry, lease, tool, capabilities, provider_kind) = {
966 let state = self
967 .state
968 .read()
969 .expect("provider registry lock should not be poisoned");
970 let snapshot = state.snapshot.clone();
971 let entry = snapshot.core_snapshot().tool(&call.action).ok_or_else(|| {
972 ProviderError::validation(
973 "registry",
974 call.action.clone(),
975 "unknown_action",
976 format!("unknown provider action `{}`", call.action),
977 )
978 })?;
979 let provider_name = entry.provider_id().as_str();
980 let tool = entry.spec().clone();
981 let provider = state.providers.get(provider_name).cloned().ok_or_else(|| {
982 ProviderError::new(
983 "provider_not_loaded",
984 provider_name,
985 Some(entry.spec().name.clone()),
986 "provider is not loaded in the active registry",
987 "Reload providers and retry.",
988 )
989 })?;
990 let lease = DispatchLease::acquire(provider).ok_or_else(|| {
991 ProviderError::new(
992 "provider_generation_draining",
993 provider_name,
994 Some(entry.spec().name.clone()),
995 "provider generation is draining",
996 "Retry against the active provider generation.",
997 )
998 })?;
999 let (capabilities, provider_kind) = snapshot
1000 .catalogs
1001 .iter()
1002 .find(|catalog| catalog.provider.name == provider_name)
1003 .map(|catalog| (catalog.capabilities.clone(), catalog.provider.kind))
1004 .expect("core provider index must reference an active catalog");
1005 (
1006 snapshot,
1007 state.core_registry.clone(),
1008 lease,
1009 tool,
1010 capabilities,
1011 provider_kind,
1012 )
1013 };
1014
1015 call.provider = lease.provider.catalog().provider.name;
1016 call.snapshot_id = snapshot.id.clone();
1017 let pre_input_call = call.clone();
1018 let invocation_call = call.clone();
1019 let pre_input_tool = tool.clone();
1020 let capability_broker = self.capabilities.clone();
1021 let progress = soma_provider_core::ProviderProgressReporter::default();
1022 let mut output = core_registry
1023 .dispatch_with_pre_input(
1024 call.provider_invocation_with_progress(progress.clone()),
1025 move |invocation| {
1026 let mut call = pre_input_call;
1027 call.provider.clone_from(&invocation.provider);
1028 call.snapshot_id.clone_from(&invocation.snapshot_id);
1029 enforce_pre_input(&pre_input_tool, &call, provider_kind)
1030 },
1031 move |_, invocation| {
1032 let mut call = invocation_call;
1033 call.provider = invocation.provider;
1034 call.snapshot_id = invocation.snapshot_id;
1035 call.progress = invocation.context.progress;
1036 async move {
1037 enforce_capabilities(&capabilities, &call, &capability_broker)?;
1038 let output = lease.provider.call(call.clone()).await?;
1039 enforce_response_limit(&tool, &call, &output)?;
1040 Ok(output)
1041 }
1042 },
1043 )
1044 .await
1045 .inspect_err(|error| {
1046 let (provider, action, code) = error.log_code();
1047 tracing::warn!(provider, action, code, "provider call failed");
1048 })?;
1049 output.progress.extend(progress.events());
1050 Ok(output)
1051 }
1052}
1053
1054#[derive(Clone)]
1062pub struct SharedAdapter(Arc<dyn soma_provider_core::Provider>);
1063
1064impl SharedAdapter {
1065 pub fn wrap(inner: Arc<dyn soma_provider_core::Provider>) -> Arc<dyn Provider> {
1068 Arc::new(Self(inner))
1069 }
1070}
1071
1072#[async_trait]
1073impl Provider for SharedAdapter {
1074 fn catalog(&self) -> ProviderCatalog {
1075 self.0.catalog()
1076 }
1077
1078 async fn call(&self, call: ProviderCall) -> Result<ProviderOutput, ProviderError> {
1079 self.0.call(call.provider_invocation()).await
1080 }
1081
1082 async fn retire(&self) {
1083 self.0.retire().await;
1084 }
1085
1086 async fn suspend(&self) {
1087 self.0.suspend().await;
1088 }
1089
1090 fn runtime_status(&self) -> Option<Value> {
1091 self.0.runtime_status()
1092 }
1093
1094 fn cancel_active(&self) -> bool {
1095 self.0.cancel_active()
1096 }
1097
1098 async fn reset_quarantine(&self) {
1099 self.0.reset_quarantine().await;
1100 }
1101
1102 fn deactivate(&self) {
1103 self.0.deactivate();
1104 }
1105
1106 fn activate(&self) {
1107 self.0.activate();
1108 }
1109
1110 fn acquire_dispatch(&self) -> bool {
1111 self.0.acquire_dispatch()
1112 }
1113
1114 fn release_dispatch(&self) {
1115 self.0.release_dispatch();
1116 }
1117}
1118
1119#[derive(Clone)]
1120struct CoreProviderAdapter(Arc<dyn Provider>);
1121
1122type BuiltRegistry = (
1123 BTreeMap<String, Arc<dyn Provider>>,
1124 CoreRegistry,
1125 Arc<RegistrySnapshot>,
1126);
1127
1128#[async_trait]
1129impl CoreProvider for CoreProviderAdapter {
1130 fn catalog(&self) -> ProviderCatalog {
1131 self.0.catalog()
1132 }
1133
1134 async fn call(&self, call: CoreProviderCall) -> Result<ProviderOutput, ProviderError> {
1135 let surface = match call.surface {
1136 soma_provider_core::ProviderSurface::Mcp => ProviderSurface::Mcp,
1137 soma_provider_core::ProviderSurface::Rest => ProviderSurface::Rest,
1138 soma_provider_core::ProviderSurface::Cli => ProviderSurface::Cli,
1139 soma_provider_core::ProviderSurface::Palette => ProviderSurface::Palette,
1140 soma_provider_core::ProviderSurface::Internal
1141 | soma_provider_core::ProviderSurface::Ui => {
1142 return Err(ProviderError::validation(
1143 call.provider,
1144 call.action,
1145 "unsupported_product_surface",
1146 "Soma providers expose only MCP, REST, CLI, and Palette surfaces",
1147 ));
1148 }
1149 };
1150 self.0
1151 .call(ProviderCall {
1152 provider: call.provider,
1153 action: call.action,
1154 params: call.params,
1155 principal: ProviderPrincipal::anonymous(),
1156 auth_mode: ProviderAuthMode::TrustedGateway,
1157 surface,
1158 destructive_confirmed: false,
1159 limits: ProviderRequestLimits::default(),
1160 snapshot_id: call.snapshot_id,
1161 request_id: call.context.request_id,
1162 traceparent: call.context.traceparent,
1163 tracestate: call.context.tracestate,
1164 progress: call.context.progress,
1165 })
1166 .await
1167 }
1168
1169 fn acquire_dispatch(&self) -> bool {
1170 self.0.acquire_dispatch()
1171 }
1172
1173 fn release_dispatch(&self) {
1174 self.0.release_dispatch();
1175 }
1176}
1177
1178fn build_registry(
1179 providers: Vec<Arc<dyn Provider>>,
1180) -> Result<BuiltRegistry, ProviderValidationError> {
1181 let providers = provider_map(providers)?;
1182 let mut builder = CoreRegistry::builder();
1183 for provider in providers.values() {
1184 validate_provider_manifest(&provider.catalog())?;
1185 builder = builder.register_arc(Arc::new(CoreProviderAdapter(provider.clone())))?;
1186 }
1187 let core_registry = builder.build()?;
1188 let snapshot = Arc::new(build_snapshot(&providers, &core_registry)?);
1189 Ok((providers, core_registry, snapshot))
1190}
1191
1192fn build_snapshot(
1193 providers: &BTreeMap<String, Arc<dyn Provider>>,
1194 core_registry: &CoreRegistry,
1195) -> Result<RegistrySnapshot, ProviderValidationError> {
1196 let core = core_registry.snapshot();
1197 let mut resources = ResourceIndex::new();
1198 for provider in providers.values() {
1199 let catalog = provider.catalog();
1200 resources.register(&**provider, &catalog.provider.name, &catalog.resources)?;
1201 }
1202 let catalogs = core.catalogs().to_vec();
1203 let fingerprint = core.fingerprint().to_string();
1204 let id = fingerprint.clone();
1205 let action_names = core.action_names().map(str::to_owned).collect::<Vec<_>>();
1206 let openapi_paths = openapi_paths_from_core(&core);
1207 let cached_catalog_summary = Arc::new(json!({
1208 "schema_version": 1,
1209 "provider_fingerprint": fingerprint,
1210 "actions": action_names,
1211 }));
1212 let cached_openapi_bytes = Arc::new(
1213 serde_json::to_vec_pretty(&json!({
1214 "openapi": "3.1.0",
1215 "info": {"title": "soma provider API", "version": env!("CARGO_PKG_VERSION")},
1216 "x-soma": {
1217 "preferred_rest_style": "direct_routes",
1218 "provider_fingerprint": fingerprint
1219 },
1220 "paths": openapi_paths
1221 }))
1222 .expect("static OpenAPI summary serializes"),
1223 );
1224
1225 Ok(RegistrySnapshot {
1226 id,
1227 fingerprint,
1228 catalogs,
1229 core: core.clone(),
1230 exact_resources: resources.exact,
1231 dynamic_resources: resources.dynamic,
1232 compiled_validator_count: core.compiled_validator_count(),
1233 cached_openapi_bytes,
1234 cached_catalog_summary,
1235 })
1236}