use serde_json::{json, Value};
use crate::{GateId, ToolError, ToolHandler};
#[derive(Debug)]
pub struct CortexConfigTool;
impl CortexConfigTool {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl Default for CortexConfigTool {
fn default() -> Self {
Self::new()
}
}
impl ToolHandler for CortexConfigTool {
fn name(&self) -> &'static str {
"cortex_config"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::HealthRead]
}
fn call(&self, _params: Value) -> Result<Value, ToolError> {
let env_backend = std::env::var("CORTEX_LLM_BACKEND")
.ok()
.filter(|s| !s.is_empty());
let env_model = std::env::var("CORTEX_LLM_MODEL")
.ok()
.filter(|s| !s.is_empty());
let env_endpoint = std::env::var("CORTEX_LLM_ENDPOINT")
.ok()
.filter(|s| !s.is_empty());
let config_file = read_config_file_raw();
let backend_str = env_backend
.as_deref()
.or_else(|| config_file.as_deref().and_then(extract_llm_backend))
.unwrap_or("offline");
let (llm_backend, llm_model, llm_endpoint) = match backend_str {
"ollama" => {
let model =
env_model.or_else(|| config_file.as_deref().and_then(extract_ollama_model));
let endpoint = env_endpoint
.or_else(|| config_file.as_deref().and_then(extract_ollama_endpoint))
.unwrap_or_else(|| "http://localhost:11434".to_string());
("ollama", model, Some(endpoint))
}
"claude" => {
let model =
env_model.or_else(|| config_file.as_deref().and_then(extract_claude_model));
("claude", model, None)
}
"openai-compat" => ("openai-compat", env_model, env_endpoint),
_ => ("offline", None, None),
};
let emb_env_backend = std::env::var("CORTEX_EMBEDDING_BACKEND")
.ok()
.filter(|s| !s.is_empty());
let emb_env_model = std::env::var("CORTEX_EMBEDDING_MODEL")
.ok()
.filter(|s| !s.is_empty());
let emb_env_endpoint = std::env::var("CORTEX_EMBEDDING_ENDPOINT")
.ok()
.filter(|s| !s.is_empty());
let emb_backend_str = emb_env_backend.as_deref().unwrap_or("stub");
let (embedding_backend, embedding_model, embedding_endpoint) = match emb_backend_str {
"ollama" => {
let endpoint = emb_env_endpoint
.or_else(|| config_file.as_deref().and_then(extract_ollama_endpoint))
.unwrap_or_else(|| "http://localhost:11434".to_string());
("ollama", emb_env_model, Some(endpoint))
}
_ => ("stub", None, None),
};
let mcp_auto_commit = if std::env::var("CORTEX_MCP_AUTO_COMMIT").as_deref() == Ok("1") {
true
} else {
config_file
.as_deref()
.and_then(extract_mcp_auto_commit)
.unwrap_or(false)
};
Ok(json!({
"llm_backend": llm_backend,
"llm_model": llm_model,
"llm_endpoint": llm_endpoint,
"embedding_backend": embedding_backend,
"embedding_model": embedding_model,
"embedding_endpoint": embedding_endpoint,
"mcp_auto_commit": mcp_auto_commit,
}))
}
}
fn read_config_file_raw() -> Option<String> {
let path: std::path::PathBuf =
if let Some(explicit) = std::env::var_os("CORTEX_CONFIG").filter(|v| !v.is_empty()) {
std::path::PathBuf::from(explicit)
} else if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty()) {
std::path::PathBuf::from(xdg)
.join("cortex")
.join("config.toml")
} else {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.filter(|v| !v.is_empty())?;
std::path::PathBuf::from(home)
.join(".config")
.join("cortex")
.join("config.toml")
};
std::fs::read_to_string(path).ok()
}
fn extract_llm_backend(raw: &str) -> Option<&str> {
extract_toml_string_value(raw, "backend", "[llm]", "[")
}
fn extract_ollama_model(raw: &str) -> Option<String> {
extract_toml_string_value(raw, "model", "[llm.ollama]", "[").map(ToOwned::to_owned)
}
fn extract_ollama_endpoint(raw: &str) -> Option<String> {
extract_toml_string_value(raw, "endpoint", "[llm.ollama]", "[").map(ToOwned::to_owned)
}
fn extract_claude_model(raw: &str) -> Option<String> {
extract_toml_string_value(raw, "model", "[llm.claude]", "[").map(ToOwned::to_owned)
}
fn extract_mcp_auto_commit(raw: &str) -> Option<bool> {
let section_start = raw.find("[mcp]")?;
let after_section = &raw[section_start + "[mcp]".len()..];
for line in after_section.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
break;
}
if let Some(rest) = trimmed.strip_prefix("auto_commit") {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
let val = rest.trim();
if val == "true" {
return Some(true);
} else if val == "false" {
return Some(false);
}
}
}
}
None
}
fn extract_toml_string_value<'a>(
raw: &'a str,
key: &str,
section_header: &str,
stop_at: &str,
) -> Option<&'a str> {
let section_start = raw.find(section_header)?;
let after_section = &raw[section_start + section_header.len()..];
for line in after_section.lines() {
let trimmed = line.trim();
if trimmed.starts_with(stop_at) && trimmed != section_header.trim_start_matches('[') {
break;
}
if let Some(rest) = trimmed.strip_prefix(key) {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
let rest = rest.trim();
if let Some(inner) = rest.strip_prefix('"') {
if let Some(end) = inner.find('"') {
return Some(&inner[..end]);
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tool() -> CortexConfigTool {
CortexConfigTool::new()
}
#[test]
fn gate_set_declares_health_read() {
let tool = make_tool();
assert!(
tool.gate_set().contains(&GateId::HealthRead),
"gate_set must include HealthRead"
);
}
#[test]
fn tool_name_matches_schema_contract() {
let tool = make_tool();
assert_eq!(tool.name(), "cortex_config");
}
#[test]
fn call_returns_all_required_keys() {
let tool = make_tool();
let result = tool
.call(serde_json::Value::Null)
.expect("call must succeed");
for key in &[
"llm_backend",
"llm_model",
"llm_endpoint",
"embedding_backend",
"embedding_model",
"embedding_endpoint",
"mcp_auto_commit",
] {
assert!(result.get(key).is_some(), "missing key: {key}");
}
assert!(
result["mcp_auto_commit"].is_boolean(),
"mcp_auto_commit must be a bool"
);
}
#[test]
fn toml_extractor_parses_simple_value() {
let toml = "[llm]\nbackend = \"ollama\"\n\n[llm.ollama]\nendpoint = \"http://localhost:11434\"\nmodel = \"llama3\"\n";
assert_eq!(
extract_toml_string_value(toml, "backend", "[llm]", "["),
Some("ollama")
);
assert_eq!(
extract_toml_string_value(toml, "model", "[llm.ollama]", "["),
Some("llama3")
);
assert_eq!(
extract_toml_string_value(toml, "endpoint", "[llm.ollama]", "["),
Some("http://localhost:11434")
);
}
#[test]
fn toml_extractor_returns_none_for_missing_section() {
let toml = "[other]\nkey = \"val\"\n";
assert!(extract_toml_string_value(toml, "backend", "[llm]", "[").is_none());
}
#[test]
fn mcp_auto_commit_extractor_detects_true() {
let toml = "[mcp]\nauto_commit = true\n";
assert_eq!(extract_mcp_auto_commit(toml), Some(true));
}
#[test]
fn mcp_auto_commit_extractor_detects_false() {
let toml = "[mcp]\nauto_commit = false\n";
assert_eq!(extract_mcp_auto_commit(toml), Some(false));
}
#[test]
fn mcp_auto_commit_extractor_returns_none_when_absent() {
let toml = "[llm]\nbackend = \"ollama\"\n";
assert_eq!(extract_mcp_auto_commit(toml), None);
}
#[test]
fn mcp_auto_commit_extractor_stops_at_next_section() {
let toml = "[mcp]\n[other]\nauto_commit = true\n";
assert_eq!(extract_mcp_auto_commit(toml), None);
}
}