1use std::sync::Arc;
2
3use serde_json::Value;
4use soma_domain::{
5 scopes::{READ_SCOPE, WRITE_SCOPE},
6 token_limit::MAX_RESPONSE_BYTES,
7 AuthorizationMode, Principal, Surface,
8};
9use soma_provider_core::{ProviderPrompt, ProviderResource};
10
11use crate::{
12 ApplicationError, ApplicationPorts, CatalogSnapshot, CodeModeExecuteRequest, DoctorReport,
13 ElicitedName, ElicitedNameOutcome, ExecuteActionRequest, ExecuteActionResponse,
14 ExecutionContext, GatewayExecuteRequest, GatewayPromptRoute, GatewayReloadRequest,
15 GatewayResourceRoute, GatewayRouteScope, GatewayToolRoute, OpenApiExecuteRequest,
16 OperationResponse, ProviderAuthMode, ProviderCall, ProviderPrincipal, ProviderRegistry,
17 ProviderRequestLimits, ProviderSurface, ReadResourceRequest, ResourceContent,
18 ResourceReadOutput, ResourceTemplateSpec, ScaffoldIntent, ScaffoldIntentRequest, SomaService,
19};
20
21#[cfg(test)]
22#[path = "app_tests.rs"]
23mod tests;
24
25pub struct SomaApplication {
31 legacy_service: Arc<SomaService>,
32 legacy_registry: Arc<ProviderRegistry>,
33 ports: ApplicationPorts,
34}
35
36impl SomaApplication {
37 pub fn new(
39 legacy_service: Arc<SomaService>,
40 legacy_registry: Arc<ProviderRegistry>,
41 ports: ApplicationPorts,
42 ) -> Self {
43 Self {
44 legacy_service,
45 legacy_registry,
46 ports,
47 }
48 }
49
50 pub async fn execute_action(
52 &self,
53 request: ExecuteActionRequest,
54 context: ExecutionContext,
55 ) -> Result<ExecuteActionResponse, ApplicationError> {
56 let limits = ProviderRequestLimits {
57 max_response_bytes: context
58 .response_limit
59 .unwrap_or(ProviderRequestLimits::default().max_response_bytes),
60 ..ProviderRequestLimits::default()
61 };
62 let call = ProviderCall {
63 provider: String::new(),
64 action: request.action,
65 params: request.params,
66 principal: provider_principal(context.principal.as_ref()),
67 auth_mode: provider_auth_mode(context.authorization_mode),
68 surface: provider_surface(context.surface),
69 destructive_confirmed: context.destructive_confirmation.is_confirmed(),
70 limits,
71 snapshot_id: String::new(),
72 };
73 let output = self.legacy_registry.dispatch(call).await?;
74 Ok(ExecuteActionResponse {
75 output: output.value,
76 request_id: context.request_id.as_str().to_owned(),
77 })
78 }
79
80 pub fn elicited_name_greeting(&self, outcome: ElicitedName) -> Value {
82 match outcome {
83 ElicitedName::Accepted(name) => self
84 .legacy_service
85 .elicited_name_greeting(ElicitedNameOutcome::Accepted(&name)),
86 ElicitedName::NoInput => self
87 .legacy_service
88 .elicited_name_greeting(ElicitedNameOutcome::NoInput),
89 ElicitedName::Declined => self
90 .legacy_service
91 .elicited_name_greeting(ElicitedNameOutcome::Declined),
92 ElicitedName::Cancelled => self
93 .legacy_service
94 .elicited_name_greeting(ElicitedNameOutcome::Cancelled),
95 ElicitedName::Unsupported => self
96 .legacy_service
97 .elicited_name_greeting(ElicitedNameOutcome::Unsupported),
98 }
99 }
100
101 pub fn scaffold_intent(
103 &self,
104 request: ScaffoldIntentRequest,
105 ) -> Result<Value, ApplicationError> {
106 self.legacy_service
107 .scaffold_intent(ScaffoldIntent {
108 display_name: request.display_name,
109 crate_name: request.crate_name,
110 binary_name: request.binary_name,
111 server_category: request.server_category,
112 env_prefix: request.env_prefix,
113 auth_kind: request.auth_kind,
114 host: request.host,
115 port: request.port,
116 mcp_transport: request.mcp_transport,
117 mcp_primitives: request.mcp_primitives,
118 deployment: request.deployment,
119 plugins: request.plugins,
120 publish_mcp: request.publish_mcp,
121 crawl_urls: request.crawl_urls,
122 crawl_repos: request.crawl_repos,
123 crawl_search_topics: request.crawl_search_topics,
124 })
125 .map_err(|error| ApplicationError::service(&error))
126 }
127
128 pub fn catalog_snapshot(&self) -> CatalogSnapshot {
130 catalog_snapshot(self.legacy_registry.snapshot().as_ref())
131 }
132
133 pub fn resolve_cli_action(&self, command: &str) -> Result<String, ApplicationError> {
135 self.legacy_registry
136 .snapshot()
137 .cli_action(command)
138 .map(ToOwned::to_owned)
139 .ok_or_else(|| ApplicationError::not_found("CLI command", command))
140 }
141
142 pub fn action_requires_confirmation(&self, action: &str) -> bool {
144 self.legacy_registry
145 .snapshot()
146 .action_requires_confirmation(action)
147 }
148
149 pub fn provider_for_action(&self, action: &str) -> Option<String> {
151 self.legacy_registry
152 .snapshot()
153 .provider_for_action(action)
154 .map(ToOwned::to_owned)
155 }
156
157 pub fn provider_validation_summary(&self) -> Value {
159 self.legacy_registry.snapshot().validation_summary()
160 }
161
162 pub fn provider_inspection_report(&self) -> Value {
164 self.legacy_registry.snapshot().inspection_report()
165 }
166
167 pub fn resolve_rest_route(&self, method: &str, path: &str) -> Option<String> {
169 self.legacy_registry
170 .snapshot()
171 .route_action(method, path)
172 .map(ToOwned::to_owned)
173 }
174
175 pub fn openapi_document(&self) -> Result<Value, ApplicationError> {
177 serde_json::from_slice(&self.legacy_registry.snapshot().cached_openapi_bytes).map_err(
178 |error| {
179 ApplicationError::new(
180 "openapi_unavailable",
181 format!("runtime OpenAPI document is unavailable: {error}"),
182 false,
183 "Refresh the provider catalog and retry.",
184 )
185 },
186 )
187 }
188
189 pub fn refresh_providers(&self) -> Result<CatalogSnapshot, ApplicationError> {
191 self.refresh_providers_in_place()?;
192 Ok(self.catalog_snapshot())
193 }
194
195 pub fn refresh_providers_in_place(&self) -> Result<(), ApplicationError> {
197 self.legacy_registry
198 .refresh_file_providers()
199 .map(|_| ())
200 .map_err(|error| {
201 let diagnostic = crate::provider_errors::redact_public(&error.to_string());
202 ApplicationError::new(
203 "provider_refresh_failed",
204 format!("provider refresh failed: {diagnostic}"),
205 false,
206 "Fix invalid provider files and retry.",
207 )
208 })
209 }
210
211 pub async fn read_resource(
213 &self,
214 request: ReadResourceRequest,
215 context: ExecutionContext,
216 ) -> Result<ResourceContent, ApplicationError> {
217 let output = self
218 .legacy_registry
219 .read_resource(
220 &request.uri,
221 &provider_principal(context.principal.as_ref()),
222 provider_auth_mode(context.authorization_mode),
223 )
224 .await?;
225 Ok(match output {
226 ResourceReadOutput::Text { text, mime_type } => {
227 ResourceContent::Text { text, mime_type }
228 }
229 ResourceReadOutput::Blob {
230 blob_base64,
231 mime_type,
232 } => ResourceContent::Blob {
233 blob_base64,
234 mime_type,
235 },
236 })
237 }
238
239 pub fn list_resources(&self) -> Vec<ProviderResource> {
241 self.legacy_registry
242 .snapshot()
243 .exact_resources()
244 .cloned()
245 .collect()
246 }
247
248 pub fn list_resource_templates(&self) -> Vec<ResourceTemplateSpec> {
250 self.legacy_registry
251 .snapshot()
252 .dynamic_resource_templates()
253 .iter()
254 .map(|(_, template)| template)
255 .map(|template| ResourceTemplateSpec {
256 uri_template: template.uri_template(),
257 name: template.name.clone(),
258 description: template.description.clone(),
259 mime_type: template.mime_type.clone(),
260 })
261 .collect()
262 }
263
264 pub fn list_prompts(&self) -> Vec<ProviderPrompt> {
266 self.legacy_registry
267 .snapshot()
268 .catalogs
269 .iter()
270 .flat_map(|catalog| catalog.prompts.iter())
271 .filter(|prompt| prompt_is_servable(prompt))
272 .cloned()
273 .collect()
274 }
275
276 pub fn get_prompt(
278 &self,
279 name: &str,
280 context: &ExecutionContext,
281 ) -> Result<ProviderPrompt, ApplicationError> {
282 let prompt = self
283 .legacy_registry
284 .snapshot()
285 .catalogs
286 .iter()
287 .flat_map(|catalog| catalog.prompts.iter())
288 .filter(|prompt| prompt_is_servable(prompt))
289 .find(|prompt| prompt.name == name)
290 .cloned()
291 .ok_or_else(|| ApplicationError::not_found("prompt", name))?;
292 if !scope_visible(prompt.scope.as_deref(), context) {
293 let required = prompt.scope.as_deref().unwrap_or_default();
294 return Err(ApplicationError::new(
295 "insufficient_scope",
296 format!("prompt `{name}` requires scope `{required}`"),
297 false,
298 "Authenticate with a token that includes the required scope.",
299 ));
300 }
301 Ok(prompt)
302 }
303
304 pub async fn gateway_status(
306 &self,
307 context: ExecutionContext,
308 ) -> Result<OperationResponse, ApplicationError> {
309 let output = self.ports.gateway.status(&context).await?;
310 operation_response(output, &context)
311 }
312
313 pub async fn gateway_reload(
315 &self,
316 request: GatewayReloadRequest,
317 context: ExecutionContext,
318 ) -> Result<OperationResponse, ApplicationError> {
319 let output = self.ports.gateway.reload(request, &context).await?;
320 operation_response(output, &context)
321 }
322
323 pub async fn gateway_execute(
325 &self,
326 request: GatewayExecuteRequest,
327 context: ExecutionContext,
328 ) -> Result<OperationResponse, ApplicationError> {
329 let output = self.ports.gateway.execute(request, &context).await?;
330 operation_response(output, &context)
331 }
332
333 pub async fn gateway_mcp_tools(
335 &self,
336 scope: Option<&GatewayRouteScope>,
337 context: &ExecutionContext,
338 ) -> Result<Vec<GatewayToolRoute>, ApplicationError> {
339 Ok(self.ports.gateway.list_mcp_tools(scope, context).await?)
340 }
341
342 pub async fn gateway_call_mcp_tool(
344 &self,
345 name: &str,
346 params: Value,
347 scope: Option<&GatewayRouteScope>,
348 context: &ExecutionContext,
349 ) -> Result<Option<Value>, ApplicationError> {
350 Ok(self
351 .ports
352 .gateway
353 .call_mcp_tool(name, params, scope, context)
354 .await?)
355 }
356
357 pub async fn gateway_mcp_resources(
359 &self,
360 scope: Option<&GatewayRouteScope>,
361 context: &ExecutionContext,
362 ) -> Result<Vec<GatewayResourceRoute>, ApplicationError> {
363 Ok(self
364 .ports
365 .gateway
366 .list_mcp_resources(scope, context)
367 .await?)
368 }
369
370 pub async fn gateway_read_mcp_resource(
372 &self,
373 uri: &str,
374 scope: Option<&GatewayRouteScope>,
375 context: &ExecutionContext,
376 ) -> Result<Option<Value>, ApplicationError> {
377 Ok(self
378 .ports
379 .gateway
380 .read_mcp_resource(uri, scope, context)
381 .await?)
382 }
383
384 pub async fn gateway_mcp_prompts(
386 &self,
387 scope: Option<&GatewayRouteScope>,
388 context: &ExecutionContext,
389 ) -> Result<Vec<GatewayPromptRoute>, ApplicationError> {
390 Ok(self.ports.gateway.list_mcp_prompts(scope, context).await?)
391 }
392
393 pub async fn gateway_get_mcp_prompt(
395 &self,
396 name: &str,
397 arguments: Option<serde_json::Map<String, Value>>,
398 scope: Option<&GatewayRouteScope>,
399 context: &ExecutionContext,
400 ) -> Result<Option<Value>, ApplicationError> {
401 Ok(self
402 .ports
403 .gateway
404 .get_mcp_prompt(name, arguments, scope, context)
405 .await?)
406 }
407
408 pub async fn codemode_execute(
410 &self,
411 request: CodeModeExecuteRequest,
412 context: ExecutionContext,
413 ) -> Result<OperationResponse, ApplicationError> {
414 let output = self.ports.codemode.execute(request, &context).await?;
415 operation_response(output, &context)
416 }
417
418 pub async fn openapi_execute(
420 &self,
421 request: OpenApiExecuteRequest,
422 context: ExecutionContext,
423 ) -> Result<OperationResponse, ApplicationError> {
424 let output = self.ports.openapi.execute(request, &context).await?;
425 operation_response(output, &context)
426 }
427
428 pub async fn status(&self) -> Result<Value, ApplicationError> {
430 self.legacy_service
431 .status()
432 .await
433 .map_err(|error| ApplicationError::legacy("status", error))
434 }
435
436 pub async fn readiness(&self) -> Result<(), ApplicationError> {
438 self.legacy_service
439 .ready()
440 .await
441 .map_err(|error| ApplicationError::legacy("readiness", error))
442 }
443
444 pub async fn doctor(&self) -> DoctorReport {
446 let mut problems = Vec::new();
447 let ready = match self.readiness().await {
448 Ok(()) => true,
449 Err(error) => {
450 problems.push(error.to_string());
451 false
452 }
453 };
454 let status = match self.status().await {
455 Ok(status) => Some(status),
456 Err(error) => {
457 problems.push(error.to_string());
458 None
459 }
460 };
461 DoctorReport {
462 ready,
463 status,
464 problems,
465 }
466 }
467}
468
469fn catalog_snapshot(snapshot: &crate::RegistrySnapshot) -> CatalogSnapshot {
470 CatalogSnapshot {
471 id: snapshot.id.clone(),
472 fingerprint: snapshot.fingerprint.clone(),
473 catalogs: snapshot.catalogs.clone(),
474 }
475}
476
477fn operation_response(
478 output: Value,
479 context: &ExecutionContext,
480) -> Result<OperationResponse, ApplicationError> {
481 let maximum = context.response_limit.unwrap_or(MAX_RESPONSE_BYTES);
482 let actual = serde_json::to_vec(&output)
483 .map_err(|error| ApplicationError::legacy("response serialization", error))?
484 .len();
485 if actual > maximum {
486 return Err(ApplicationError::new(
487 "response_too_large",
488 format!("response is {actual} bytes; maximum is {maximum}"),
489 false,
490 "Increase the response limit or request a smaller result.",
491 ));
492 }
493 Ok(OperationResponse {
494 output,
495 request_id: context.request_id.as_str().to_owned(),
496 })
497}
498
499fn provider_principal(principal: Option<&Principal>) -> ProviderPrincipal {
500 principal.map_or_else(ProviderPrincipal::anonymous, |principal| {
501 ProviderPrincipal {
502 subject: principal.subject.clone(),
503 scopes: principal.scopes.to_vec(),
504 }
505 })
506}
507
508fn provider_auth_mode(mode: AuthorizationMode) -> ProviderAuthMode {
509 match mode {
510 AuthorizationMode::LoopbackDev => ProviderAuthMode::LoopbackDev,
511 AuthorizationMode::TrustedGateway => ProviderAuthMode::TrustedGateway,
512 AuthorizationMode::Mounted => ProviderAuthMode::Mounted,
513 }
514}
515
516fn provider_surface(surface: Surface) -> ProviderSurface {
517 match surface {
518 Surface::Mcp => ProviderSurface::Mcp,
519 Surface::Rest => ProviderSurface::Rest,
520 Surface::Cli => ProviderSurface::Cli,
521 Surface::Palette => ProviderSurface::Palette,
522 }
523}
524
525fn scope_visible(required: Option<&str>, context: &ExecutionContext) -> bool {
526 if !matches!(context.authorization_mode, AuthorizationMode::Mounted) {
527 return true;
528 }
529 let Some(required) = required else {
530 return true;
531 };
532 context.principal.as_ref().is_some_and(|principal| {
533 principal.scopes.contains(required)
534 || (required == READ_SCOPE && principal.scopes.contains(WRITE_SCOPE))
535 })
536}
537
538fn prompt_is_servable(prompt: &ProviderPrompt) -> bool {
539 prompt.template.is_some()
540 && prompt
541 .mcp
542 .as_ref()
543 .map(|metadata| metadata.enabled)
544 .unwrap_or(true)
545}