Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions crates/cli/src/doctor_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,9 @@ fn check_config(config_dir: Option<&Path>) -> Section {
}
}

// Semantic warnings (security, etc.)
// Semantic warnings (security, deprecated fields, etc.)
for d in &result.diagnostics {
if d.category == "security" || d.category == "unknown-provider" {
let status = match d.severity {
Severity::Error => Status::Fail,
Severity::Warning => Status::Warn,
Severity::Info => Status::Info,
};
if let Some(status) = config_validation_status(d) {
let msg = if d.path.is_empty() {
d.message.clone()
} else {
Expand Down Expand Up @@ -259,6 +254,21 @@ fn check_config(config_dir: Option<&Path>) -> Section {
section
}

fn config_validation_status(diagnostic: &moltis_config::Diagnostic) -> Option<Status> {
if diagnostic.category != "security"
&& diagnostic.category != "unknown-provider"
&& diagnostic.category != "deprecated-field"
{
return None;
}

Some(match diagnostic.severity {
Severity::Error => Status::Fail,
Severity::Warning => Status::Warn,
Severity::Info => Status::Info,
})
}

// ── 2. Security audit ───────────────────────────────────────────────────────

fn check_security(config: &MoltisConfig, config_dir: Option<&Path>, data_dir: &Path) -> Section {
Expand Down Expand Up @@ -680,7 +690,10 @@ fn check_mcp_servers(config: &MoltisConfig) -> Section {
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use {super::*, moltis_config::MoltisConfig};
use {
super::*,
moltis_config::{MoltisConfig, validate::Diagnostic},
};

#[test]
fn status_labels() {
Expand Down Expand Up @@ -717,6 +730,18 @@ mod tests {
assert_eq!(warnings, 2);
}

#[test]
fn config_validation_status_warns_for_deprecated_field() {
let diagnostic = Diagnostic {
severity: Severity::Warning,
category: "deprecated-field",
path: "memory.embedding_provider".into(),
message: "deprecated field; use \"memory.provider\" instead".into(),
};

assert_eq!(config_validation_status(&diagnostic), Some(Status::Warn));
}

#[test]
fn check_providers_empty_config() {
let config = MoltisConfig::default();
Expand Down
39 changes: 39 additions & 0 deletions crates/config/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,17 +1074,21 @@ pub struct MemoryEmbeddingConfig {
/// Memory backend: "builtin" (default) or "qmd" for QMD sidecar.
pub backend: Option<String>,
/// Embedding provider: "local", "ollama", "openai", "custom", or None for auto-detect.
#[serde(alias = "embedding_provider")]
pub provider: Option<String>,
/// Disable RAG embeddings and force keyword-only memory search.
#[serde(default)]
pub disable_rag: bool,
/// Base URL for the embedding API (e.g. "http://localhost:11434/v1" for Ollama).
#[serde(alias = "embedding_base_url")]
pub base_url: Option<String>,
/// Model name (e.g. "nomic-embed-text" for Ollama, "text-embedding-3-small" for OpenAI).
#[serde(alias = "embedding_model")]
pub model: Option<String>,
/// API key (optional for local endpoints like Ollama).
#[serde(
default,
alias = "embedding_api_key",
serialize_with = "serialize_option_secret",
skip_serializing_if = "Option::is_none"
)]
Expand Down Expand Up @@ -2323,6 +2327,8 @@ impl ProvidersConfig {
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use secrecy::ExposeSecret;

use super::*;

#[test]
Expand Down Expand Up @@ -2719,6 +2725,39 @@ url = "http://192.168.0.9:11434"
assert_eq!(entry.base_url.as_deref(), Some("http://192.168.0.9:11434"));
}

#[test]
fn memory_embedding_legacy_aliases_map_to_current_fields() {
let config: MoltisConfig = toml::from_str(
r#"
[memory]
embedding_provider = "custom"
embedding_base_url = "http://moltis-embeddings:7997/v1"
embedding_model = "intfloat/multilingual-e5-small"
embedding_api_key = "secret-key"
"#,
)
.unwrap();

assert_eq!(config.memory.provider.as_deref(), Some("custom"));
assert_eq!(
config.memory.base_url.as_deref(),
Some("http://moltis-embeddings:7997/v1")
);
assert_eq!(
config.memory.model.as_deref(),
Some("intfloat/multilingual-e5-small")
);
assert_eq!(
config
.memory
.api_key
.as_ref()
.map(ExposeSecret::expose_secret)
.map(String::as_str),
Some("secret-key")
);
}

#[test]
fn full_config_with_tool_mode() {
let toml_str = r#"
Expand Down
Loading
Loading