Skip to main content

soma_application/
app.rs

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
33/// Shared use-case facade every surface (MCP, REST, CLI) calls into.
34///
35/// Wraps the legacy [`SomaService`] and [`ProviderRegistry`] plus the outbound
36/// [`ApplicationPorts`] (gateway, code mode, OpenAPI), exposing one method per
37/// application operation.
38pub struct SomaApplication {
39    legacy_service: Arc<SomaService>,
40    legacy_registry: Arc<ProviderRegistry>,
41    ports: ApplicationPorts,
42}
43
44impl SomaApplication {
45    /// Assemble the facade from its service, provider registry, and outbound ports.
46    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    /// Dispatch an action through the provider registry and return its output.
59    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    /// Build the greeting for the elicited-name demo from the collected outcome.
122    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    /// Normalize elicited scaffold requirements into the JSON handoff contract.
143    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    /// Return a snapshot of the currently loaded provider catalog.
170    pub fn catalog_snapshot(&self) -> CatalogSnapshot {
171        catalog_snapshot(self.legacy_registry.snapshot().as_ref())
172    }
173
174    /// Resolve a CLI command name to its backing action, or a not-found error.
175    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    /// Report whether the action is destructive and requires explicit confirmation.
184    pub fn action_requires_confirmation(&self, action: &str) -> bool {
185        self.legacy_registry
186            .snapshot()
187            .action_requires_confirmation(action)
188    }
189
190    /// Return the name of the provider that owns the given action, if any.
191    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    /// Return the provider validation summary for the loaded catalog.
199    pub fn provider_validation_summary(&self) -> Value {
200        self.legacy_registry.snapshot().validation_summary()
201    }
202
203    /// Return the detailed provider inspection report for the loaded catalog.
204    pub fn provider_inspection_report(&self) -> Value {
205        self.legacy_registry.snapshot().inspection_report()
206    }
207
208    /// Resolve an HTTP method and path to its backing action, if a route matches.
209    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    /// Return the runtime OpenAPI document assembled from the provider catalog.
217    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    /// Reload file-backed providers and return the resulting catalog snapshot.
231    pub fn refresh_providers(&self) -> Result<CatalogSnapshot, ApplicationError> {
232        self.refresh_providers_in_place()?;
233        Ok(self.catalog_snapshot())
234    }
235
236    /// Reload file-backed providers without returning a snapshot.
237    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    /// Asynchronously reload file-backed providers, preflighting persistent
253    /// Python candidates before publishing a replacement snapshot.
254    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    /// Async in-place refresh used by production HTTP and MCP surfaces.
260    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    /// Read a provider resource by URI and return its text or blob content.
277    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    /// List the exact (non-templated) provider resources in the catalog.
305    pub fn list_resources(&self) -> Vec<ProviderResource> {
306        self.legacy_registry
307            .snapshot()
308            .exact_resources()
309            .cloned()
310            .collect()
311    }
312
313    /// List the dynamic (URI-templated) provider resource templates in the catalog.
314    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    /// List the servable provider prompts in the catalog.
330    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    /// Fetch a servable prompt by name, enforcing scope visibility.
342    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    /// Query the MCP gateway status via the gateway port.
370    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    /// Reload the MCP gateway configuration via the gateway port.
379    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    /// Execute a gateway operation via the gateway port.
389    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    /// List MCP tools exposed through the gateway, optionally filtered by scope.
399    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    /// Call an MCP tool through the gateway, returning its result if routed.
408    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    /// Call an MCP tool once, preserving MRTR and task outcomes.
423    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    /// Get the latest state of a routed MCP task.
439    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    /// Supply follow-up input to a routed MCP task.
448    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    /// Request cancellation of a routed MCP task.
462    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    /// List MCP resources exposed through the gateway, optionally filtered by scope.
471    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    /// Read an MCP resource through the gateway, returning its content if routed.
484    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    /// Read an MCP resource once, preserving a possible input-required outcome.
498    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    /// List MCP prompts exposed through the gateway, optionally filtered by scope.
513    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    /// Fetch an MCP prompt through the gateway, returning its content if routed.
522    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    /// Fetch an MCP prompt once, preserving a possible input-required outcome.
537    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    /// Execute a Code Mode request via the code mode port.
553    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    /// Execute an OpenAPI-backed request via the OpenAPI port.
563    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    /// Return the upstream service status.
573    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    /// Probe upstream readiness; `Ok(())` when the dependency is reachable.
581    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    /// Run readiness and status probes and collect them into a diagnostic report.
589    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}