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    "setup",
20    "package",
21    "tools",
22    "providers",
23    "openapi",
24    "help",
25];
26
27/// Validates a raw manifest JSON value against schema and Soma policy,
28/// returning the deserialized [`ProviderManifest`] on success.
29pub fn validate_provider_manifest_value(
30    value: &Value,
31) -> Result<ProviderManifest, ProviderValidationError> {
32    validate_manifest_schema(value)?;
33    let manifest: ProviderManifest = serde_json::from_value(value.clone()).map_err(|error| {
34        ProviderValidationError::new("manifest_deserialize_failed", error.to_string())
35    })?;
36    validate_provider_manifest(&manifest)?;
37    Ok(manifest)
38}
39
40/// Validates a manifest JSON value against the provider-core schema.
41pub fn validate_manifest_schema(value: &Value) -> Result<(), ProviderValidationError> {
42    soma_provider_core::validate_manifest_schema(value)
43}
44
45/// Validates a manifest through provider-core, then applies Soma-specific
46/// policy: reserved CLI verbs and rejection of `SOMA_`/`LAB_`-prefixed env.
47pub fn validate_provider_manifest(
48    manifest: &ProviderManifest,
49) -> Result<(), ProviderValidationError> {
50    soma_provider_core::validate_provider_manifest(manifest)?;
51    validate_soma_cli_policy(manifest)?;
52    for env in manifest
53        .env
54        .iter()
55        .chain(manifest.tools.iter().flat_map(|tool| tool.env.iter()))
56    {
57        if env.name.starts_with("SOMA_") || env.name.starts_with("LAB_") {
58            return Err(ProviderValidationError::new(
59                "invalid_env_declaration",
60                format!(
61                    "env declaration `{}` must be logical and unprefixed",
62                    env.name
63                ),
64            ));
65        }
66    }
67    Ok(())
68}
69
70fn validate_soma_cli_policy(manifest: &ProviderManifest) -> Result<(), ProviderValidationError> {
71    for tool in &manifest.tools {
72        let Some(cli) = &tool.cli else {
73            continue;
74        };
75        if !cli.enabled {
76            continue;
77        }
78        let command = cli.command.as_deref().unwrap_or(&tool.name);
79        for candidate in std::iter::once(command).chain(cli.aliases.iter().map(String::as_str)) {
80            if manifest.provider.kind == ProviderKind::StaticRust
81                && tool.name == "help"
82                && candidate == "help"
83            {
84                continue;
85            }
86            if RESERVED_CLI_COMMANDS.contains(&candidate) {
87                return Err(ProviderValidationError::new(
88                    "reserved_cli_command",
89                    format!("provider command `{candidate}` is reserved"),
90                ));
91            }
92        }
93    }
94    Ok(())
95}
96
97#[cfg(test)]
98#[path = "provider_validation_tests.rs"]
99mod tests;