Skip to main content

soma_domain/
provider_validation.rs

1//! Soma policy layered over provider-core's transport-neutral validation.
2//!
3//! `soma-provider-core` (shared layer) validates manifests generically and
4//! must stay product-agnostic. The extra policy here — reserved Soma CLI
5//! verbs, `SOMA_`/`LAB_`-prefixed env rejection — is Soma-specific, so it
6//! lives in `soma-domain` instead of being folded into the shared crate.
7
8use serde_json::Value;
9
10use soma_provider_core::{ProviderKind, ProviderManifest};
11
12pub use soma_provider_core::ProviderValidationError;
13
14const RESERVED_CLI_COMMANDS: &[&str] = &[
15    "serve",
16    "mcp",
17    "doctor",
18    "watch",
19    "self-update",
20    "setup",
21    "package",
22    "tools",
23    "providers",
24    "openapi",
25    "help",
26];
27
28/// Validates a raw manifest JSON value against schema and Soma policy,
29/// returning the deserialized [`ProviderManifest`] on success.
30pub fn validate_provider_manifest_value(
31    value: &Value,
32) -> Result<ProviderManifest, ProviderValidationError> {
33    validate_manifest_schema(value)?;
34    let manifest: ProviderManifest = serde_json::from_value(value.clone()).map_err(|error| {
35        ProviderValidationError::new("manifest_deserialize_failed", error.to_string())
36    })?;
37    validate_provider_manifest(&manifest)?;
38    Ok(manifest)
39}
40
41/// Validates a manifest JSON value against the provider-core schema.
42pub fn validate_manifest_schema(value: &Value) -> Result<(), ProviderValidationError> {
43    soma_provider_core::validate_manifest_schema(value)
44}
45
46/// Validates a manifest through provider-core, then applies Soma-specific
47/// policy: reserved CLI verbs and rejection of `SOMA_`/`LAB_`-prefixed env.
48pub fn validate_provider_manifest(
49    manifest: &ProviderManifest,
50) -> Result<(), ProviderValidationError> {
51    soma_provider_core::validate_provider_manifest(manifest)?;
52    validate_soma_cli_policy(manifest)?;
53    for env in manifest
54        .env
55        .iter()
56        .chain(manifest.tools.iter().flat_map(|tool| tool.env.iter()))
57    {
58        if env.name.starts_with("SOMA_") || env.name.starts_with("LAB_") {
59            return Err(ProviderValidationError::new(
60                "invalid_env_declaration",
61                format!(
62                    "env declaration `{}` must be logical and unprefixed",
63                    env.name
64                ),
65            ));
66        }
67    }
68    Ok(())
69}
70
71fn validate_soma_cli_policy(manifest: &ProviderManifest) -> Result<(), ProviderValidationError> {
72    for tool in &manifest.tools {
73        let Some(cli) = &tool.cli else {
74            continue;
75        };
76        if !cli.enabled {
77            continue;
78        }
79        let command = cli.command.as_deref().unwrap_or(&tool.name);
80        for candidate in std::iter::once(command).chain(cli.aliases.iter().map(String::as_str)) {
81            if manifest.provider.kind == ProviderKind::StaticRust
82                && tool.name == "help"
83                && candidate == "help"
84            {
85                continue;
86            }
87            if RESERVED_CLI_COMMANDS.contains(&candidate) {
88                return Err(ProviderValidationError::new(
89                    "reserved_cli_command",
90                    format!("provider command `{candidate}` is reserved"),
91                ));
92            }
93        }
94    }
95    Ok(())
96}
97
98#[cfg(test)]
99#[path = "provider_validation_tests.rs"]
100mod tests;