1use std::{
4 collections::{BTreeMap, HashMap},
5 sync::{Arc, RwLock},
6};
7
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Map, Value};
11use soma_domain::provider_validation::{validate_provider_manifest, ProviderValidationError};
12use soma_provider_core::{
13 ProviderCall as CoreProviderCall, ProviderCatalog, ProviderRegistry as CoreRegistry,
14 ProviderResource, RegistrySnapshot as CoreRegistrySnapshot,
15};
16
17use crate::{
18 capabilities::CapabilityBroker, provider_errors::ProviderError,
19 providers::filesystem::FileProviderSource,
20};
21
22mod enforcement;
23mod refresh;
24mod reports;
25mod resources;
26pub(super) use enforcement::provider_tool_surface_enabled;
27use enforcement::{enforce_capabilities, enforce_pre_input, enforce_response_limit};
28use refresh::ProviderRefreshEvent;
29use resources::ResourceIndex;
30pub use resources::{DynamicResourceTemplate, ResourceReadOutput};
31pub use soma_provider_core::{Provider as CoreProvider, ProviderOutput};
32pub type ProviderInvocation = CoreProviderCall;
35
36#[async_trait]
39pub trait Provider: Send + Sync {
40 fn catalog(&self) -> ProviderCatalog;
42
43 async fn call(&self, call: ProviderCall) -> Result<ProviderOutput, ProviderError>;
45
46 fn dynamic_resource_templates(&self) -> Vec<DynamicResourceTemplate> {
50 Vec::new()
51 }
52
53 fn supports_resource_reads(&self) -> bool {
63 false
64 }
65
66 async fn read_resource(
71 &self,
72 uri: &str,
73 params: &BTreeMap<String, String>,
74 ) -> Result<ResourceReadOutput, ProviderError> {
75 let _ = params;
76 Err(ProviderError::validation(
77 &self.catalog().provider.name,
78 uri,
79 "resource_read_not_supported",
80 "this provider does not support resource reads",
81 ))
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "kebab-case")]
88pub enum ProviderSurface {
89 Mcp,
91 Rest,
93 Cli,
95 Palette,
97}
98
99impl ProviderSurface {
100 pub fn as_str(self) -> &'static str {
102 match self {
103 Self::Mcp => "mcp",
104 Self::Rest => "rest",
105 Self::Cli => "cli",
106 Self::Palette => "palette",
107 }
108 }
109
110 fn core(self) -> soma_provider_core::ProviderSurface {
111 match self {
112 Self::Mcp => soma_provider_core::ProviderSurface::Mcp,
113 Self::Rest => soma_provider_core::ProviderSurface::Rest,
114 Self::Cli => soma_provider_core::ProviderSurface::Cli,
115 Self::Palette => soma_provider_core::ProviderSurface::Palette,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum ProviderAuthMode {
123 LoopbackDev,
125 TrustedGateway,
127 Mounted,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ProviderPrincipal {
134 pub subject: String,
136 pub scopes: Vec<String>,
138}
139
140impl ProviderPrincipal {
141 pub fn loopback_dev() -> Self {
144 Self {
145 subject: "loopback-dev".to_owned(),
146 scopes: vec![soma_domain::actions::READ_SCOPE.to_owned()],
147 }
148 }
149
150 pub fn anonymous() -> Self {
152 Self {
153 subject: "anonymous".to_owned(),
154 scopes: Vec::new(),
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub struct ProviderRequestLimits {
162 pub max_input_bytes: usize,
164 pub max_response_bytes: usize,
166}
167
168impl Default for ProviderRequestLimits {
169 fn default() -> Self {
170 Self {
171 max_input_bytes: 64 * 1024,
172 max_response_bytes: soma_domain::token_limit::MAX_RESPONSE_BYTES,
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
180pub struct ProviderCall {
181 pub provider: String,
183 pub action: String,
185 pub params: Value,
187 pub principal: ProviderPrincipal,
189 pub auth_mode: ProviderAuthMode,
191 pub surface: ProviderSurface,
193 pub destructive_confirmed: bool,
195 pub limits: ProviderRequestLimits,
197 pub snapshot_id: String,
199}
200
201impl ProviderCall {
202 pub fn provider_invocation(&self) -> ProviderInvocation {
204 ProviderInvocation {
205 provider: self.provider.clone(),
206 action: self.action.clone(),
207 params: self.params.clone(),
208 surface: self.surface.core(),
209 snapshot_id: self.snapshot_id.clone(),
210 }
211 }
212
213 pub fn execution_envelope(&self) -> ProviderExecutionEnvelope {
215 ProviderExecutionEnvelope {
216 schema_version: 1,
217 provider: self.provider.clone(),
218 action: self.action.clone(),
219 params: self.params.clone(),
220 surface: self.surface,
221 snapshot_id: self.snapshot_id.clone(),
222 }
223 }
224
225 pub fn execution_payload(&self) -> Result<Vec<u8>, serde_json::Error> {
227 serde_json::to_vec(&self.execution_envelope())
228 }
229}
230
231#[derive(Debug, Clone, Serialize)]
234pub struct ProviderExecutionEnvelope {
235 pub schema_version: u32,
237 pub provider: String,
239 pub action: String,
241 pub params: Value,
243 pub surface: ProviderSurface,
245 pub snapshot_id: String,
247}
248
249#[derive(Clone)]
252pub struct RegistrySnapshot {
253 pub id: String,
255 pub fingerprint: String,
257 pub catalogs: Vec<ProviderCatalog>,
259 core: Arc<CoreRegistrySnapshot>,
260 exact_resources: HashMap<String, (String, ProviderResource)>,
261 dynamic_resources: Vec<(String, DynamicResourceTemplate)>,
262 pub compiled_validator_count: usize,
264 pub cached_openapi_bytes: Arc<Vec<u8>>,
266 pub cached_catalog_summary: Arc<Value>,
268}
269
270impl RegistrySnapshot {
271 pub fn action_names(&self) -> Vec<&str> {
273 self.core.action_names().collect()
274 }
275
276 pub fn route_action(&self, method: &str, path: &str) -> Option<&str> {
278 self.core.route_action(method, path)
279 }
280
281 pub fn cli_action(&self, command: &str) -> Option<&str> {
283 self.core.cli_action(command)
284 }
285
286 pub fn primitive_kind(&self, name: &str) -> Option<&str> {
288 self.core.primitive_kind(name)
289 }
290
291 pub fn action_requires_confirmation(&self, action: &str) -> bool {
293 self.core
294 .tool(action)
295 .map(|entry| entry.spec().destructive)
296 .unwrap_or(false)
297 }
298
299 pub fn provider_for_action(&self, action: &str) -> Option<&str> {
301 self.core
302 .tool(action)
303 .map(|entry| entry.provider_id().as_str())
304 }
305
306 pub fn core_snapshot(&self) -> &CoreRegistrySnapshot {
308 &self.core
309 }
310
311 pub(crate) fn rest_routes(&self) -> impl Iterator<Item = (&str, &str, &str)> {
312 self.core.rest_routes()
313 }
314}
315
316#[derive(Clone)]
319pub struct ProviderRegistry {
320 state: Arc<RwLock<RegistryState>>,
321 capabilities: CapabilityBroker,
322 base_providers: Arc<Vec<Arc<dyn Provider>>>,
323 file_source: Option<FileProviderSource>,
324}
325
326struct RegistryState {
327 providers: BTreeMap<String, Arc<dyn Provider>>,
328 core_registry: CoreRegistry,
329 snapshot: Arc<RegistrySnapshot>,
330 file_fingerprint: Option<String>,
331}
332
333impl ProviderRegistry {
334 pub fn new(providers: Vec<Arc<dyn Provider>>) -> Result<Self, ProviderValidationError> {
337 Self::with_capabilities(providers, CapabilityBroker::default_deny())
338 }
339
340 pub fn with_capabilities(
342 providers: Vec<Arc<dyn Provider>>,
343 capabilities: CapabilityBroker,
344 ) -> Result<Self, ProviderValidationError> {
345 let (providers, core_registry, snapshot) = build_registry(providers)?;
346 Ok(Self {
347 state: Arc::new(RwLock::new(RegistryState {
348 providers,
349 core_registry,
350 snapshot,
351 file_fingerprint: None,
352 })),
353 capabilities,
354 base_providers: Arc::new(Vec::new()),
355 file_source: None,
356 })
357 }
358
359 pub fn with_file_source(
362 providers: Vec<Arc<dyn Provider>>,
363 capabilities: CapabilityBroker,
364 file_source: FileProviderSource,
365 ) -> Result<Self, ProviderValidationError> {
366 let file_fingerprint = file_source.fingerprint().map_err(|error| {
367 ProviderValidationError::new("provider_file_load_failed", error.to_string())
368 })?;
369 let dynamic_providers = file_source.load().map_err(|error| {
370 ProviderValidationError::new("provider_file_load_failed", error.to_string())
371 })?;
372 let base_providers = Arc::new(providers);
373 let mut all_providers = base_providers.iter().cloned().collect::<Vec<_>>();
374 all_providers.extend(dynamic_providers);
375 let (providers, core_registry, snapshot) = build_registry(all_providers)?;
376 Ok(Self {
377 state: Arc::new(RwLock::new(RegistryState {
378 providers,
379 core_registry,
380 snapshot,
381 file_fingerprint: Some(file_fingerprint),
382 })),
383 capabilities,
384 base_providers,
385 file_source: Some(file_source),
386 })
387 }
388
389 pub fn snapshot(&self) -> Arc<RegistrySnapshot> {
391 self.state
392 .read()
393 .expect("provider registry lock should not be poisoned")
394 .snapshot
395 .clone()
396 }
397
398 pub fn refresh_file_providers(&self) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
408 let Some(file_source) = &self.file_source else {
409 return Ok(self.snapshot());
410 };
411 let file_fingerprint = match file_source.fingerprint() {
412 Ok(fingerprint) => fingerprint,
413 Err(error) => {
414 return Ok(self.snapshot_after_refresh_failure(
415 file_source,
416 "provider_file_fingerprint_failed",
417 &error.to_string(),
418 ));
419 }
420 };
421 {
422 let state = self
423 .state
424 .read()
425 .expect("provider registry lock should not be poisoned");
426 if state.file_fingerprint.as_deref() == Some(file_fingerprint.as_str()) {
427 return Ok(state.snapshot.clone());
428 }
429 }
430
431 let rebuilt: Result<_, ProviderValidationError> = (|| {
432 let dynamic_providers = file_source.load().map_err(|error| {
433 ProviderValidationError::new("provider_file_load_failed", error.to_string())
434 })?;
435 let mut providers = self.base_providers.iter().cloned().collect::<Vec<_>>();
436 providers.extend(dynamic_providers);
437 let (providers, core_registry, snapshot) = build_registry(providers)?;
438 Ok((providers, core_registry, snapshot))
439 })();
440 let (providers, core_registry, snapshot) = match rebuilt {
441 Ok(parts) => parts,
442 Err(error) => {
443 return Ok(self.snapshot_after_refresh_failure(
444 file_source,
445 error.code(),
446 error.message(),
447 ));
448 }
449 };
450
451 let mut state = self
452 .state
453 .write()
454 .expect("provider registry lock should not be poisoned");
455 if state.snapshot.fingerprint == snapshot.fingerprint {
456 return Ok(state.snapshot.clone());
457 }
458 let previous = state.snapshot.clone();
459 let event = ProviderRefreshEvent::new(&previous, &snapshot);
460 state.providers = providers;
461 state.core_registry = core_registry;
462 state.snapshot = snapshot.clone();
463 state.file_fingerprint = Some(file_fingerprint);
464 event.log(file_source.root());
465 Ok(snapshot)
466 }
467
468 fn snapshot_after_refresh_failure(
469 &self,
470 file_source: &FileProviderSource,
471 code: &str,
472 message: &str,
473 ) -> Arc<RegistrySnapshot> {
474 tracing::warn!(
475 root = %file_source.root().display(),
476 code,
477 message,
478 "provider directory refresh failed; keeping the last valid snapshot active"
479 );
480 self.snapshot()
481 }
482
483 pub fn validate_reload(
486 &self,
487 providers: Vec<Arc<dyn Provider>>,
488 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
489 let (_, _, snapshot) = build_registry(providers)?;
490 Ok(snapshot)
491 }
492
493 pub fn reload(
496 &self,
497 providers: Vec<Arc<dyn Provider>>,
498 ) -> Result<Arc<RegistrySnapshot>, ProviderValidationError> {
499 let (providers, core_registry, snapshot) = build_registry(providers)?;
500 let mut state = self
501 .state
502 .write()
503 .expect("provider registry lock should not be poisoned");
504 state.providers = providers;
505 state.core_registry = core_registry;
506 state.snapshot = snapshot.clone();
507 state.file_fingerprint = None;
508 Ok(snapshot)
509 }
510
511 pub async fn dispatch(&self, mut call: ProviderCall) -> Result<ProviderOutput, ProviderError> {
515 let (snapshot, core_registry, provider, tool, capabilities, provider_kind) = {
516 let state = self
517 .state
518 .read()
519 .expect("provider registry lock should not be poisoned");
520 let snapshot = state.snapshot.clone();
521 let entry = snapshot.core_snapshot().tool(&call.action).ok_or_else(|| {
522 ProviderError::validation(
523 "registry",
524 call.action.clone(),
525 "unknown_action",
526 format!("unknown provider action `{}`", call.action),
527 )
528 })?;
529 let provider_name = entry.provider_id().as_str();
530 let tool = entry.spec().clone();
531 let provider = state.providers.get(provider_name).cloned().ok_or_else(|| {
532 ProviderError::new(
533 "provider_not_loaded",
534 provider_name,
535 Some(entry.spec().name.clone()),
536 "provider is not loaded in the active registry",
537 "Reload providers and retry.",
538 )
539 })?;
540 let (capabilities, provider_kind) = snapshot
541 .catalogs
542 .iter()
543 .find(|catalog| catalog.provider.name == provider_name)
544 .map(|catalog| (catalog.capabilities.clone(), catalog.provider.kind))
545 .expect("core provider index must reference an active catalog");
546 (
547 snapshot,
548 state.core_registry.clone(),
549 provider,
550 tool,
551 capabilities,
552 provider_kind,
553 )
554 };
555
556 call.provider = provider.catalog().provider.name;
557 call.snapshot_id = snapshot.id.clone();
558 let pre_input_call = call.clone();
559 let invocation_call = call.clone();
560 let pre_input_tool = tool.clone();
561 let capability_broker = self.capabilities.clone();
562 core_registry
563 .dispatch_with_pre_input(
564 call.provider_invocation(),
565 move |invocation| {
566 let mut call = pre_input_call;
567 call.provider.clone_from(&invocation.provider);
568 call.snapshot_id.clone_from(&invocation.snapshot_id);
569 enforce_pre_input(&pre_input_tool, &call, provider_kind)
570 },
571 move |_, invocation| {
572 let mut call = invocation_call;
573 call.provider = invocation.provider;
574 call.snapshot_id = invocation.snapshot_id;
575 async move {
576 enforce_capabilities(&capabilities, &call, &capability_broker)?;
577 let output = provider.call(call.clone()).await?;
578 enforce_response_limit(&tool, &call, &output)?;
579 Ok(output)
580 }
581 },
582 )
583 .await
584 .inspect_err(|error| {
585 let (provider, action, code) = error.log_code();
586 tracing::warn!(provider, action, code, "provider call failed");
587 })
588 }
589}
590
591fn provider_map(
592 providers: Vec<Arc<dyn Provider>>,
593) -> Result<BTreeMap<String, Arc<dyn Provider>>, ProviderValidationError> {
594 let mut map = BTreeMap::new();
595 for provider in providers {
596 let name = provider.catalog().provider.name;
597 if map.insert(name.clone(), provider).is_some() {
598 return Err(ProviderValidationError::new(
599 "duplicate_provider_name",
600 format!("duplicate provider `{name}`"),
601 ));
602 }
603 }
604 Ok(map)
605}
606
607#[derive(Clone)]
615pub struct SharedAdapter(Arc<dyn soma_provider_core::Provider>);
616
617impl SharedAdapter {
618 pub fn wrap(inner: Arc<dyn soma_provider_core::Provider>) -> Arc<dyn Provider> {
621 Arc::new(Self(inner))
622 }
623}
624
625#[async_trait]
626impl Provider for SharedAdapter {
627 fn catalog(&self) -> ProviderCatalog {
628 self.0.catalog()
629 }
630
631 async fn call(&self, call: ProviderCall) -> Result<ProviderOutput, ProviderError> {
632 self.0.call(call.provider_invocation()).await
633 }
634}
635
636#[derive(Clone)]
637struct CoreProviderAdapter(Arc<dyn Provider>);
638
639type BuiltRegistry = (
640 BTreeMap<String, Arc<dyn Provider>>,
641 CoreRegistry,
642 Arc<RegistrySnapshot>,
643);
644
645#[async_trait]
646impl CoreProvider for CoreProviderAdapter {
647 fn catalog(&self) -> ProviderCatalog {
648 self.0.catalog()
649 }
650
651 async fn call(&self, call: CoreProviderCall) -> Result<ProviderOutput, ProviderError> {
652 let surface = match call.surface {
653 soma_provider_core::ProviderSurface::Mcp => ProviderSurface::Mcp,
654 soma_provider_core::ProviderSurface::Rest => ProviderSurface::Rest,
655 soma_provider_core::ProviderSurface::Cli => ProviderSurface::Cli,
656 soma_provider_core::ProviderSurface::Palette => ProviderSurface::Palette,
657 soma_provider_core::ProviderSurface::Internal
658 | soma_provider_core::ProviderSurface::Ui => {
659 return Err(ProviderError::validation(
660 call.provider,
661 call.action,
662 "unsupported_product_surface",
663 "Soma providers expose only MCP, REST, CLI, and Palette surfaces",
664 ));
665 }
666 };
667 self.0
668 .call(ProviderCall {
669 provider: call.provider,
670 action: call.action,
671 params: call.params,
672 principal: ProviderPrincipal::anonymous(),
673 auth_mode: ProviderAuthMode::TrustedGateway,
674 surface,
675 destructive_confirmed: false,
676 limits: ProviderRequestLimits::default(),
677 snapshot_id: call.snapshot_id,
678 })
679 .await
680 }
681}
682
683fn build_registry(
684 providers: Vec<Arc<dyn Provider>>,
685) -> Result<BuiltRegistry, ProviderValidationError> {
686 let providers = provider_map(providers)?;
687 let mut builder = CoreRegistry::builder();
688 for provider in providers.values() {
689 validate_provider_manifest(&provider.catalog())?;
690 builder = builder.register_arc(Arc::new(CoreProviderAdapter(provider.clone())))?;
691 }
692 let core_registry = builder.build()?;
693 let snapshot = Arc::new(build_snapshot(&providers, &core_registry)?);
694 Ok((providers, core_registry, snapshot))
695}
696
697fn build_snapshot(
698 providers: &BTreeMap<String, Arc<dyn Provider>>,
699 core_registry: &CoreRegistry,
700) -> Result<RegistrySnapshot, ProviderValidationError> {
701 let core = core_registry.snapshot();
702 let mut resources = ResourceIndex::new();
703 for provider in providers.values() {
704 let catalog = provider.catalog();
705 resources.register(&**provider, &catalog.provider.name, &catalog.resources)?;
706 }
707 let catalogs = core.catalogs().to_vec();
708 let fingerprint = core.fingerprint().to_string();
709 let id = fingerprint.clone();
710 let action_names = core.action_names().map(str::to_owned).collect::<Vec<_>>();
711 let openapi_paths = openapi_paths_from_core(&core);
712 let cached_catalog_summary = Arc::new(json!({
713 "schema_version": 1,
714 "provider_fingerprint": fingerprint,
715 "actions": action_names,
716 }));
717 let cached_openapi_bytes = Arc::new(
718 serde_json::to_vec_pretty(&json!({
719 "openapi": "3.1.0",
720 "info": {"title": "soma provider API", "version": env!("CARGO_PKG_VERSION")},
721 "x-soma": {
722 "preferred_rest_style": "direct_routes",
723 "provider_fingerprint": fingerprint
724 },
725 "paths": openapi_paths
726 }))
727 .expect("static OpenAPI summary serializes"),
728 );
729
730 Ok(RegistrySnapshot {
731 id,
732 fingerprint,
733 catalogs,
734 core: core.clone(),
735 exact_resources: resources.exact,
736 dynamic_resources: resources.dynamic,
737 compiled_validator_count: core.compiled_validator_count(),
738 cached_openapi_bytes,
739 cached_catalog_summary,
740 })
741}
742
743fn openapi_paths_from_core(core: &CoreRegistrySnapshot) -> Value {
744 let mut paths = Map::new();
745 paths.insert(
746 "/v1/capabilities".to_owned(),
747 json!({
748 "get": {
749 "summary": "List REST capabilities",
750 "operationId": "v1Capabilities",
751 "responses": {
752 "200": {"description": "Route inventory and server metadata"}
753 }
754 }
755 }),
756 );
757 paths.insert(
758 "/v1/providers".to_owned(),
759 json!({
760 "get": {
761 "summary": "Inspect live providers",
762 "operationId": "v1Providers",
763 "responses": {
764 "200": {"description": "Live provider catalog and runtime inventory"}
765 }
766 }
767 }),
768 );
769 paths.insert(
770 "/v1/tools/{action}".to_owned(),
771 json!({
772 "post": {
773 "summary": "Run a provider tool",
774 "operationId": "runProviderTool",
775 "parameters": [{
776 "name": "action",
777 "in": "path",
778 "required": true,
779 "schema": {"type": "string"},
780 "description": "Provider tool action name"
781 }],
782 "requestBody": {
783 "required": false,
784 "content": {
785 "application/json": {
786 "schema": {"type": "object", "additionalProperties": true}
787 }
788 }
789 },
790 "responses": {
791 "200": {"description": "Provider action response"},
792 "400": {"description": "Provider validation error"},
793 "403": {"description": "Provider authorization error"},
794 "404": {"description": "Unknown action or surface not exposed"}
795 }
796 }
797 }),
798 );
799
800 let mut routes = core
801 .rest_routes()
802 .map(|(method, path, action)| (method.to_owned(), path.to_owned(), action.to_owned()))
803 .collect::<Vec<_>>();
804 routes.sort_by(|left, right| left.1.cmp(&right.1).then(left.0.cmp(&right.0)));
805
806 for (method, path, action) in routes {
807 let entry = paths
808 .entry(path)
809 .or_insert_with(|| Value::Object(Map::new()));
810 if let Value::Object(methods) = entry {
811 methods.insert(
812 method.to_ascii_lowercase(),
813 json!({
814 "summary": format!("Provider action `{action}`"),
815 "operationId": action,
816 "responses": {
817 "200": {"description": "Provider action response"},
818 "400": {"description": "Provider validation error"}
819 }
820 }),
821 );
822 }
823 }
824 Value::Object(paths)
825}