use std::path::PathBuf;
use khive_runtime::{
config_from_env, runtime_config_from_khive_config, KhiveConfig, KhiveRuntime, RuntimeConfig,
};
use crate::args::{resolve_cli_namespace, Args};
use crate::server::KhiveMcpServer;
use crate::transport::{ServeOptions, TransportRegistry};
pub async fn run(args: Args, registry: &TransportRegistry) -> anyhow::Result<()> {
let server = build_server(&args)?;
#[cfg(unix)]
if args.daemon {
khive_runtime::daemon::run_daemon(server).await?;
return Ok(());
}
#[cfg(not(unix))]
if args.daemon {
anyhow::bail!(
"--daemon mode requires Unix (macOS/Linux). On Windows, use the stdio transport."
);
}
let transport_name = args.transport.as_deref().unwrap_or("stdio");
let transport = registry.get(transport_name).ok_or_else(|| {
anyhow::anyhow!(
"unknown transport {transport_name:?}; registered: {}",
registry.names().join(", ")
)
})?;
let opts = ServeOptions {
bind: args.bind.clone(),
};
transport.serve(server, &opts).await
}
pub fn build_server(args: &Args) -> anyhow::Result<KhiveMcpServer> {
let (cli_namespace_explicit, cli_namespace) =
resolve_cli_namespace(args).map_err(|e| anyhow::anyhow!("{e}"))?;
let config = resolve_runtime_config(RuntimeConfigInputs {
db: args.db.as_deref(),
config: args.config.as_deref(),
namespace: cli_namespace,
namespace_explicit: cli_namespace_explicit,
no_embed: args.no_embed,
packs: if args.pack.is_empty() {
None
} else {
Some(args.pack.clone())
},
brain_profile: args.brain_profile.clone(),
})?;
let runtime = KhiveRuntime::new(config)?;
KhiveMcpServer::new(runtime).map_err(|e| anyhow::anyhow!("{e}"))
}
pub struct RuntimeConfigInputs<'a> {
pub db: Option<&'a str>,
pub config: Option<&'a std::path::Path>,
pub namespace: khive_runtime::Namespace,
pub namespace_explicit: bool,
pub no_embed: bool,
pub packs: Option<Vec<String>>,
pub brain_profile: Option<String>,
}
pub fn resolve_runtime_config(inputs: RuntimeConfigInputs<'_>) -> anyhow::Result<RuntimeConfig> {
let db_path = match inputs.db {
Some(":memory:") => None,
Some(path) => Some(PathBuf::from(path)),
None => {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
Some(PathBuf::from(format!("{home}/.khive/khive.db")))
}
};
let packs = inputs
.packs
.unwrap_or_else(|| RuntimeConfig::default().packs);
let cli_brain_profile = inputs.brain_profile.filter(|s| !s.trim().is_empty());
let base_config = RuntimeConfig {
db_path,
default_namespace: inputs.namespace,
packs,
brain_profile: cli_brain_profile,
..RuntimeConfig::default()
};
let resolved = if inputs.no_embed {
let no_embed_base = RuntimeConfig {
embedding_model: None,
additional_embedding_models: vec![],
..base_config
};
resolve_actor_from_config(inputs.config, no_embed_base, inputs.namespace_explicit)?
} else {
resolve_config(inputs.config, base_config, inputs.namespace_explicit)?
};
Ok(apply_env_brain_profile(resolved))
}
fn apply_env_brain_profile(mut cfg: RuntimeConfig) -> RuntimeConfig {
if cfg.brain_profile.is_none() {
cfg.brain_profile = std::env::var("KHIVE_BRAIN_PROFILE")
.ok()
.filter(|s| !s.trim().is_empty());
}
cfg
}
fn resolve_config(
config_path: Option<&std::path::Path>,
base: RuntimeConfig,
cli_namespace_explicit: bool,
) -> anyhow::Result<RuntimeConfig> {
match KhiveConfig::load_with_home_fallback(config_path)
.map_err(|e| anyhow::anyhow!("config error: {e}"))?
{
Some(khive_cfg) => {
let env_primary = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
let env_additional = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS").ok();
if env_primary.is_some() || env_additional.is_some() {
tracing::warn!(
"khive config file is present; KHIVE_EMBEDDING_MODEL and \
KHIVE_ADDITIONAL_EMBEDDING_MODELS env vars are ignored"
);
}
let effective_cfg = if cli_namespace_explicit {
let mut c = khive_cfg;
c.actor.id = None;
c
} else {
khive_cfg
};
Ok(runtime_config_from_khive_config(&effective_cfg, base))
}
None => {
let env_cfg = config_from_env();
if env_cfg.engines.is_empty() {
Ok(base)
} else {
Ok(runtime_config_from_khive_config(&env_cfg, base))
}
}
}
}
fn resolve_actor_from_config(
config_path: Option<&std::path::Path>,
base: RuntimeConfig,
cli_namespace_explicit: bool,
) -> anyhow::Result<RuntimeConfig> {
if cli_namespace_explicit {
return Ok(base);
}
match KhiveConfig::load_with_home_fallback(config_path)
.map_err(|e| anyhow::anyhow!("config error: {e}"))?
{
Some(khive_cfg) => {
let resolved = runtime_config_from_khive_config(&khive_cfg, base);
Ok(RuntimeConfig {
embedding_model: None,
additional_embedding_models: vec![],
..resolved
})
}
None => Ok(base),
}
}
#[cfg(test)]
mod tests {
use super::*;
use khive_runtime::Namespace;
use serial_test::serial;
use std::io::Write;
fn write_config(dir: &std::path::Path, body: &str) -> PathBuf {
let path = dir.join("khive.toml");
let mut f = std::fs::File::create(&path).expect("create config file");
f.write_all(body.as_bytes()).expect("write config");
path
}
#[test]
#[serial]
fn resolver_uses_config_file_engines_over_defaults() {
std::env::remove_var("KHIVE_EMBEDDING_MODEL");
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
let default_cfg = RuntimeConfig::default();
let default_primary = format!("{:?}", default_cfg.embedding_model);
assert!(
!default_cfg.additional_embedding_models.is_empty(),
"precondition: default config has additional engines"
);
let dir = tempfile::tempdir().expect("temp dir");
let path = write_config(
dir.path(),
r#"
[[engines]]
name = "primary"
model = "bge-small-en-v1.5"
default = true
"#,
);
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
no_embed: false,
packs: None,
brain_profile: None,
})
.expect("resolve config");
let resolved_primary = format!("{:?}", resolved.embedding_model);
assert_ne!(
resolved_primary, default_primary,
"resolved primary engine must come from the config file, not the default"
);
assert!(
resolved.embedding_model.is_some(),
"config-file engine must resolve to a primary embedding model"
);
assert!(
resolved.additional_embedding_models.is_empty(),
"config file declares one engine; additional list must be empty (not the default's)"
);
assert_eq!(resolved.db_path, None, ":memory: must map to in-memory db");
}
#[test]
#[serial]
fn brain_profile_config_beats_env() {
std::env::set_var("KHIVE_BRAIN_PROFILE", "env-profile");
let dir = tempfile::tempdir().expect("temp dir");
let path = write_config(
dir.path(),
r#"
[runtime]
brain_profile = "project-profile"
"#,
);
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
no_embed: false,
packs: None,
brain_profile: None, })
.expect("resolve config");
std::env::remove_var("KHIVE_BRAIN_PROFILE");
assert_eq!(
resolved.brain_profile.as_deref(),
Some("project-profile"),
"project TOML brain_profile must win over KHIVE_BRAIN_PROFILE env var"
);
}
#[test]
#[serial]
fn brain_profile_env_fallback_when_no_toml() {
std::env::set_var("KHIVE_BRAIN_PROFILE", "env-profile");
let dir = tempfile::tempdir().expect("temp dir");
let path = write_config(
dir.path(),
r#"
[[engines]]
name = "primary"
model = "bge-small-en-v1.5"
default = true
"#,
);
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
no_embed: false,
packs: None,
brain_profile: None,
})
.expect("resolve config");
std::env::remove_var("KHIVE_BRAIN_PROFILE");
assert_eq!(
resolved.brain_profile.as_deref(),
Some("env-profile"),
"env var must be used when no CLI flag and no TOML brain_profile is set"
);
}
#[test]
#[serial]
fn brain_profile_cli_wins_over_all() {
std::env::set_var("KHIVE_BRAIN_PROFILE", "env-profile");
let dir = tempfile::tempdir().expect("temp dir");
let path = write_config(
dir.path(),
r#"
[runtime]
brain_profile = "project-profile"
"#,
);
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
no_embed: false,
packs: None,
brain_profile: Some("cli-profile".to_string()), })
.expect("resolve config");
std::env::remove_var("KHIVE_BRAIN_PROFILE");
assert_eq!(
resolved.brain_profile.as_deref(),
Some("cli-profile"),
"CLI --brain-profile must win over both TOML and KHIVE_BRAIN_PROFILE env var"
);
}
}