1use std::sync::Arc;
2
3use serde_json::Value;
4use soma_domain::{
5 AuthorizationMode, Principal, Surface,
6 actions::SomaAction,
7 scopes::{READ_SCOPE, WRITE_SCOPE},
8 token_limit::MAX_RESPONSE_BYTES,
9};
10use soma_provider_core::{ProviderPrompt, ProviderResource};
11
12use crate::{
13 ApplicationError, ApplicationPorts, CatalogSnapshot, CodeModeExecuteRequest, DoctorReport,
14 ElicitedName, ElicitedNameOutcome, ExecuteActionRequest, ExecuteActionResponse,
15 ExecutionContext, GatewayExecuteRequest, GatewayMcpOutcome, GatewayMcpRoundTrip,
16 GatewayPromptRoute, GatewayReloadRequest, GatewayResourceRoute, GatewayRouteScope,
17 GatewayToolRoute, OpenApiExecuteRequest, OperationResponse, ProviderAuthMode, ProviderCall,
18 ProviderPrincipal, ProviderRegistry, ProviderRequestLimits, ProviderSurface,
19 ReadResourceRequest, ResourceContent, ResourceReadOutput, ResourceTemplateSpec, ScaffoldIntent,
20 ScaffoldIntentRequest, SomaService,
21};
22
23#[path = "app_python_componentize.rs"]
24mod python_componentize;
25#[path = "app_python_graduation.rs"]
26mod python_graduation;
27#[path = "app_python_operations.rs"]
28mod python_operations;
29#[cfg(test)]
30#[path = "app_tests.rs"]
31mod tests;
32
33pub struct SomaApplication {
39 legacy_service: Arc<SomaService>,
40 legacy_registry: Arc<ProviderRegistry>,
41 ports: ApplicationPorts,
42}
43
44impl SomaApplication {
45 pub fn new(
47 legacy_service: Arc<SomaService>,
48 legacy_registry: Arc<ProviderRegistry>,
49 ports: ApplicationPorts,
50 ) -> Self {
51 Self {
52 legacy_service,
53 legacy_registry,
54 ports,
55 }
56 }
57
58 pub async fn execute_action(
60 &self,
61 request: ExecuteActionRequest,
62 context: ExecutionContext,
63 ) -> Result<ExecuteActionResponse, ApplicationError> {
64 if matches!(
65 request.action.as_str(),
66 "python_environment_status"
67 | "python_environment_prune_plan"
68 | "python_environment_prune"
69 | "python_environment_repair"
70 | "python_environment_update"
71 | "python_worker_status"
72 | "python_worker_cancel"
73 | "python_worker_reset"
74 | "python_generation_status"
75 | "python_generation_rollback"
76 | "python_graduation_status"
77 | "python_graduation_apply"
78 ) {
79 return self
80 .execute_python_environment_action(request, context)
81 .await;
82 }
83 let limits = ProviderRequestLimits {
84 max_response_bytes: context
85 .response_limit
86 .unwrap_or(ProviderRequestLimits::default().max_response_bytes),
87 ..ProviderRequestLimits::default()
88 };
89 let call = ProviderCall {
90 provider: String::new(),
91 action: request.action,
92 params: request.params,
93 principal: provider_invocation_principal(&context),
94 auth_mode: provider_auth_mode(context.authorization_mode),
95 surface: provider_surface(context.surface),
96 destructive_confirmed: context.destructive_confirmation.is_confirmed(),
97 limits,
98 snapshot_id: String::new(),
99 request_id: context.request_id.as_str().to_owned(),
100 traceparent: context
101 .trace
102 .as_ref()
103 .and_then(|trace| trace.traceparent.clone()),
104 tracestate: context
105 .trace
106 .as_ref()
107 .and_then(|trace| trace.tracestate.clone()),
108 progress: Default::default(),
109 };
110 let max_response_bytes = call.limits.max_response_bytes;
111 let output = self.legacy_registry.dispatch(call).await?;
112 let response = ExecuteActionResponse {
113 output: output.value,
114 request_id: context.request_id.as_str().to_owned(),
115 progress: output.progress,
116 };
117 response.enforce_serialized_limit(max_response_bytes)?;
118 Ok(response)
119 }
120
121 pub fn elicited_name_greeting(&self, outcome: ElicitedName) -> Value {
123 match outcome {
124 ElicitedName::Accepted(name) => self
125 .legacy_service
126 .elicited_name_greeting(ElicitedNameOutcome::Accepted(&name)),
127 ElicitedName::NoInput => self
128 .legacy_service
129 .elicited_name_greeting(ElicitedNameOutcome::NoInput),
130 ElicitedName::Declined => self
131 .legacy_service
132 .elicited_name_greeting(ElicitedNameOutcome::Declined),
133 ElicitedName::Cancelled => self
134 .legacy_service
135 .elicited_name_greeting(ElicitedNameOutcome::Cancelled),
136 ElicitedName::Unsupported => self
137 .legacy_service
138 .elicited_name_greeting(ElicitedNameOutcome::Unsupported),
139 }
140 }
141
142 pub fn scaffold_intent(
144 &self,
145 request: ScaffoldIntentRequest,
146 ) -> Result<Value, ApplicationError> {
147 self.legacy_service
148 .scaffold_intent(ScaffoldIntent {
149 display_name: request.display_name,
150 crate_name: request.crate_name,
151 binary_name: request.binary_name,
152 server_category: request.server_category,
153 env_prefix: request.env_prefix,
154 auth_kind: request.auth_kind,
155 host: request.host,
156 port: request.port,
157 mcp_transport: request.mcp_transport,
158 mcp_primitives: request.mcp_primitives,
159 deployment: request.deployment,
160 plugins: request.plugins,
161 publish_mcp: request.publish_mcp,
162 crawl_urls: request.crawl_urls,
163 crawl_repos: request.crawl_repos,
164 crawl_search_topics: request.crawl_search_topics,
165 })
166 .map_err(|error| ApplicationError::service(&error))
167 }
168
169 pub fn catalog_snapshot(&self) -> CatalogSnapshot {
171 catalog_snapshot(self.legacy_registry.snapshot().as_ref())
172 }
173
174 pub fn resolve_cli_action(&self, command: &str) -> Result<String, ApplicationError> {
176 self.legacy_registry
177 .snapshot()
178 .cli_action(command)
179 .map(ToOwned::to_owned)
180 .ok_or_else(|| ApplicationError::not_found("CLI command", command))
181 }
182
183 pub fn action_requires_confirmation(&self, action: &str) -> bool {
185 self.legacy_registry
186 .snapshot()
187 .action_requires_confirmation(action)
188 }
189
190 pub fn provider_for_action(&self, action: &str) -> Option<String> {
192 self.legacy_registry
193 .snapshot()
194 .provider_for_action(action)
195 .map(ToOwned::to_owned)
196 }
197
198 pub fn provider_validation_summary(&self) -> Value {
200 self.legacy_registry.snapshot().validation_summary()
201 }
202
203 pub fn provider_inspection_report(&self) -> Value {
205 self.legacy_registry.snapshot().inspection_report()
206 }
207
208 pub fn resolve_rest_route(&self, method: &str, path: &str) -> Option<String> {
210 self.legacy_registry
211 .snapshot()
212 .route_action(method, path)
213 .map(ToOwned::to_owned)
214 }
215
216 pub fn openapi_document(&self) -> Result<Value, ApplicationError> {
218 serde_json::from_slice(&self.legacy_registry.snapshot().cached_openapi_bytes).map_err(
219 |error| {
220 ApplicationError::new(
221 "openapi_unavailable",
222 format!("runtime OpenAPI document is unavailable: {error}"),
223 false,
224 "Refresh the provider catalog and retry.",
225 )
226 },
227 )
228 }
229
230 pub fn refresh_providers(&self) -> Result<CatalogSnapshot, ApplicationError> {
232 self.refresh_providers_in_place()?;
233 Ok(self.catalog_snapshot())
234 }
235
236 pub fn refresh_providers_in_place(&self) -> Result<(), ApplicationError> {
238 self.legacy_registry
239 .refresh_file_providers()
240 .map(|_| ())
241 .map_err(|error| {
242 let diagnostic = crate::provider_errors::redact_public(&error.to_string());
243 ApplicationError::new(
244 "provider_refresh_failed",
245 format!("provider refresh failed: {diagnostic}"),
246 false,
247 "Fix invalid provider files and retry.",
248 )
249 })
250 }
251
252 pub async fn refresh_providers_async(&self) -> Result<CatalogSnapshot, ApplicationError> {
255 self.refresh_providers_in_place_async().await?;
256 Ok(self.catalog_snapshot())
257 }
258
259 pub async fn refresh_providers_in_place_async(&self) -> Result<(), ApplicationError> {
261 self.legacy_registry
262 .refresh_file_providers_async()
263 .await
264 .map(|_| ())
265 .map_err(|error| {
266 let diagnostic = crate::provider_errors::redact_public(&error.to_string());
267 ApplicationError::new(
268 "provider_refresh_failed",
269 format!("provider refresh failed: {diagnostic}"),
270 false,
271 "Fix invalid provider files and retry.",
272 )
273 })
274 }
275
276 pub async fn read_resource(
278 &self,
279 request: ReadResourceRequest,
280 context: ExecutionContext,
281 ) -> Result<ResourceContent, ApplicationError> {
282 let output = self
283 .legacy_registry
284 .read_resource(
285 &request.uri,
286 &provider_principal(context.principal.as_ref()),
287 provider_auth_mode(context.authorization_mode),
288 )
289 .await?;
290 Ok(match output {
291 ResourceReadOutput::Text { text, mime_type } => {
292 ResourceContent::Text { text, mime_type }
293 }
294 ResourceReadOutput::Blob {
295 blob_base64,
296 mime_type,
297 } => ResourceContent::Blob {
298 blob_base64,
299 mime_type,
300 },
301 })
302 }
303
304 pub fn list_resources(&self) -> Vec<ProviderResource> {
306 self.legacy_registry
307 .snapshot()
308 .exact_resources()
309 .cloned()
310 .collect()
311 }
312
313 pub fn list_resource_templates(&self) -> Vec<ResourceTemplateSpec> {
315 self.legacy_registry
316 .snapshot()
317 .dynamic_resource_templates()
318 .iter()
319 .map(|(_, template)| template)
320 .map(|template| ResourceTemplateSpec {
321 uri_template: template.uri_template(),
322 name: template.name.clone(),
323 description: template.description.clone(),
324 mime_type: template.mime_type.clone(),
325 })
326 .collect()
327 }
328
329 pub fn list_prompts(&self) -> Vec<ProviderPrompt> {
331 self.legacy_registry
332 .snapshot()
333 .catalogs
334 .iter()
335 .flat_map(|catalog| catalog.prompts.iter())
336 .filter(|prompt| prompt_is_servable(prompt))
337 .cloned()
338 .collect()
339 }
340
341 pub fn get_prompt(
343 &self,
344 name: &str,
345 context: &ExecutionContext,
346 ) -> Result<ProviderPrompt, ApplicationError> {
347 let prompt = self
348 .legacy_registry
349 .snapshot()
350 .catalogs
351 .iter()
352 .flat_map(|catalog| catalog.prompts.iter())
353 .filter(|prompt| prompt_is_servable(prompt))
354 .find(|prompt| prompt.name == name)
355 .cloned()
356 .ok_or_else(|| ApplicationError::not_found("prompt", name))?;
357 if !scope_visible(prompt.scope.as_deref(), context) {
358 let required = prompt.scope.as_deref().unwrap_or_default();
359 return Err(ApplicationError::new(
360 "insufficient_scope",
361 format!("prompt `{name}` requires scope `{required}`"),
362 false,
363 "Authenticate with a token that includes the required scope.",
364 ));
365 }
366 Ok(prompt)
367 }
368
369 pub async fn gateway_status(
371 &self,
372 context: ExecutionContext,
373 ) -> Result<OperationResponse, ApplicationError> {
374 let output = self.ports.gateway.status(&context).await?;
375 operation_response(output, &context)
376 }
377
378 pub async fn gateway_reload(
380 &self,
381 request: GatewayReloadRequest,
382 context: ExecutionContext,
383 ) -> Result<OperationResponse, ApplicationError> {
384 let output = self.ports.gateway.reload(request, &context).await?;
385 operation_response(output, &context)
386 }
387
388 pub async fn gateway_execute(
390 &self,
391 request: GatewayExecuteRequest,
392 context: ExecutionContext,
393 ) -> Result<OperationResponse, ApplicationError> {
394 let output = self.ports.gateway.execute(request, &context).await?;
395 operation_response(output, &context)
396 }
397
398 pub async fn gateway_mcp_tools(
400 &self,
401 scope: Option<&GatewayRouteScope>,
402 context: &ExecutionContext,
403 ) -> Result<Vec<GatewayToolRoute>, ApplicationError> {
404 Ok(self.ports.gateway.list_mcp_tools(scope, context).await?)
405 }
406
407 pub async fn gateway_call_mcp_tool(
409 &self,
410 name: &str,
411 params: Value,
412 scope: Option<&GatewayRouteScope>,
413 context: &ExecutionContext,
414 ) -> Result<Option<Value>, ApplicationError> {
415 Ok(self
416 .ports
417 .gateway
418 .call_mcp_tool(name, params, scope, context)
419 .await?)
420 }
421
422 pub async fn gateway_call_mcp_tool_once(
424 &self,
425 name: &str,
426 params: Value,
427 round_trip: GatewayMcpRoundTrip,
428 scope: Option<&GatewayRouteScope>,
429 context: &ExecutionContext,
430 ) -> Result<Option<GatewayMcpOutcome>, ApplicationError> {
431 Ok(self
432 .ports
433 .gateway
434 .call_mcp_tool_once(name, params, round_trip, scope, context)
435 .await?)
436 }
437
438 pub async fn gateway_get_mcp_task(
440 &self,
441 task_id: &str,
442 context: &ExecutionContext,
443 ) -> Result<Value, ApplicationError> {
444 Ok(self.ports.gateway.get_mcp_task(task_id, context).await?)
445 }
446
447 pub async fn gateway_update_mcp_task(
449 &self,
450 task_id: &str,
451 input_responses: std::collections::BTreeMap<String, Value>,
452 context: &ExecutionContext,
453 ) -> Result<(), ApplicationError> {
454 Ok(self
455 .ports
456 .gateway
457 .update_mcp_task(task_id, input_responses, context)
458 .await?)
459 }
460
461 pub async fn gateway_cancel_mcp_task(
463 &self,
464 task_id: &str,
465 context: &ExecutionContext,
466 ) -> Result<(), ApplicationError> {
467 Ok(self.ports.gateway.cancel_mcp_task(task_id, context).await?)
468 }
469
470 pub async fn gateway_mcp_resources(
472 &self,
473 scope: Option<&GatewayRouteScope>,
474 context: &ExecutionContext,
475 ) -> Result<Vec<GatewayResourceRoute>, ApplicationError> {
476 Ok(self
477 .ports
478 .gateway
479 .list_mcp_resources(scope, context)
480 .await?)
481 }
482
483 pub async fn gateway_read_mcp_resource(
485 &self,
486 uri: &str,
487 scope: Option<&GatewayRouteScope>,
488 context: &ExecutionContext,
489 ) -> Result<Option<Value>, ApplicationError> {
490 Ok(self
491 .ports
492 .gateway
493 .read_mcp_resource(uri, scope, context)
494 .await?)
495 }
496
497 pub async fn gateway_read_mcp_resource_once(
499 &self,
500 uri: &str,
501 round_trip: GatewayMcpRoundTrip,
502 scope: Option<&GatewayRouteScope>,
503 context: &ExecutionContext,
504 ) -> Result<Option<GatewayMcpOutcome>, ApplicationError> {
505 Ok(self
506 .ports
507 .gateway
508 .read_mcp_resource_once(uri, round_trip, scope, context)
509 .await?)
510 }
511
512 pub async fn gateway_mcp_prompts(
514 &self,
515 scope: Option<&GatewayRouteScope>,
516 context: &ExecutionContext,
517 ) -> Result<Vec<GatewayPromptRoute>, ApplicationError> {
518 Ok(self.ports.gateway.list_mcp_prompts(scope, context).await?)
519 }
520
521 pub async fn gateway_get_mcp_prompt(
523 &self,
524 name: &str,
525 arguments: Option<serde_json::Map<String, Value>>,
526 scope: Option<&GatewayRouteScope>,
527 context: &ExecutionContext,
528 ) -> Result<Option<Value>, ApplicationError> {
529 Ok(self
530 .ports
531 .gateway
532 .get_mcp_prompt(name, arguments, scope, context)
533 .await?)
534 }
535
536 pub async fn gateway_get_mcp_prompt_once(
538 &self,
539 name: &str,
540 arguments: Option<serde_json::Map<String, Value>>,
541 round_trip: GatewayMcpRoundTrip,
542 scope: Option<&GatewayRouteScope>,
543 context: &ExecutionContext,
544 ) -> Result<Option<GatewayMcpOutcome>, ApplicationError> {
545 Ok(self
546 .ports
547 .gateway
548 .get_mcp_prompt_once(name, arguments, round_trip, scope, context)
549 .await?)
550 }
551
552 pub async fn codemode_execute(
554 &self,
555 request: CodeModeExecuteRequest,
556 context: ExecutionContext,
557 ) -> Result<OperationResponse, ApplicationError> {
558 let output = self.ports.codemode.execute(request, &context).await?;
559 operation_response(output, &context)
560 }
561
562 pub async fn openapi_execute(
564 &self,
565 request: OpenApiExecuteRequest,
566 context: ExecutionContext,
567 ) -> Result<OperationResponse, ApplicationError> {
568 let output = self.ports.openapi.execute(request, &context).await?;
569 operation_response(output, &context)
570 }
571
572 pub async fn status(&self) -> Result<Value, ApplicationError> {
574 self.legacy_service
575 .status()
576 .await
577 .map_err(|error| ApplicationError::legacy("status", error))
578 }
579
580 pub async fn readiness(&self) -> Result<(), ApplicationError> {
582 self.legacy_service
583 .ready()
584 .await
585 .map_err(|error| ApplicationError::legacy("readiness", error))
586 }
587
588 pub async fn doctor(&self) -> DoctorReport {
590 let mut problems = Vec::new();
591 let ready = match self.readiness().await {
592 Ok(()) => true,
593 Err(error) => {
594 problems.push(error.to_string());
595 false
596 }
597 };
598 let status = match self.status().await {
599 Ok(status) => Some(status),
600 Err(error) => {
601 problems.push(error.to_string());
602 None
603 }
604 };
605 DoctorReport {
606 ready,
607 status,
608 problems,
609 }
610 }
611}
612
613fn catalog_snapshot(snapshot: &crate::RegistrySnapshot) -> CatalogSnapshot {
614 CatalogSnapshot {
615 id: snapshot.id.clone(),
616 fingerprint: snapshot.fingerprint.clone(),
617 catalogs: snapshot.catalogs.clone(),
618 }
619}
620
621fn operation_response(
622 output: Value,
623 context: &ExecutionContext,
624) -> Result<OperationResponse, ApplicationError> {
625 let maximum = context.response_limit.unwrap_or(MAX_RESPONSE_BYTES);
626 let actual = serde_json::to_vec(&output)
627 .map_err(|error| ApplicationError::legacy("response serialization", error))?
628 .len();
629 if actual > maximum {
630 return Err(ApplicationError::new(
631 "response_too_large",
632 format!("response is {actual} bytes; maximum is {maximum}"),
633 false,
634 "Increase the response limit or request a smaller result.",
635 ));
636 }
637 Ok(OperationResponse {
638 output,
639 request_id: context.request_id.as_str().to_owned(),
640 })
641}
642
643fn provider_principal(principal: Option<&Principal>) -> ProviderPrincipal {
644 principal.map_or_else(ProviderPrincipal::anonymous, |principal| {
645 ProviderPrincipal {
646 subject: principal.subject.clone(),
647 scopes: principal.scopes.to_vec(),
648 }
649 })
650}
651
652fn provider_invocation_principal(context: &ExecutionContext) -> ProviderPrincipal {
653 if let Some(principal) = context.principal.as_ref() {
654 return provider_principal(Some(principal));
655 }
656 match context.authorization_mode {
657 AuthorizationMode::LoopbackDev => ProviderPrincipal::loopback_dev(),
658 AuthorizationMode::TrustedGateway => ProviderPrincipal {
659 subject: "trusted-gateway".to_owned(),
660 scopes: vec![WRITE_SCOPE.to_owned()],
661 },
662 AuthorizationMode::Mounted => ProviderPrincipal::anonymous(),
663 }
664}
665
666fn provider_auth_mode(mode: AuthorizationMode) -> ProviderAuthMode {
667 match mode {
668 AuthorizationMode::LoopbackDev => ProviderAuthMode::LoopbackDev,
669 AuthorizationMode::TrustedGateway => ProviderAuthMode::TrustedGateway,
670 AuthorizationMode::Mounted => ProviderAuthMode::Mounted,
671 }
672}
673
674fn provider_surface(surface: Surface) -> ProviderSurface {
675 match surface {
676 Surface::Mcp => ProviderSurface::Mcp,
677 Surface::Rest => ProviderSurface::Rest,
678 Surface::Cli => ProviderSurface::Cli,
679 Surface::Palette => ProviderSurface::Palette,
680 }
681}
682
683fn scope_visible(required: Option<&str>, context: &ExecutionContext) -> bool {
684 if !matches!(context.authorization_mode, AuthorizationMode::Mounted) {
685 return true;
686 }
687 let Some(required) = required else {
688 return true;
689 };
690 context.principal.as_ref().is_some_and(|principal| {
691 principal.scopes.contains(required)
692 || (required == READ_SCOPE && principal.scopes.contains(WRITE_SCOPE))
693 })
694}
695
696fn prompt_is_servable(prompt: &ProviderPrompt) -> bool {
697 prompt.template.is_some()
698 && prompt
699 .mcp
700 .as_ref()
701 .map(|metadata| metadata.enabled)
702 .unwrap_or(true)
703}