use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use khive_runtime::{
config_from_env, run_migrations, runtime_config_from_khive_config, BackendConfig, BackendId,
BackendKind, ConnectionPool, KhiveConfig, KhiveRuntime, OutputFormat, RuntimeConfig,
StorageBackend,
};
use crate::args::{resolve_cli_namespace, Args};
use crate::server::KhiveMcpServer;
use crate::transport::{ServeOptions, TransportRegistry};
pub struct MultiBackendRegistry {
pub registry: khive_runtime::VerbRegistry,
pub default_namespace: String,
pub config_id: String,
pub per_pack_runtimes: HashMap<String, Arc<KhiveRuntime>>,
pub main_backend: Arc<StorageBackend>,
}
pub async fn run(args: Args, registry: &TransportRegistry) -> anyhow::Result<()> {
if let Some(generation) = args.resumed_generation {
tracing::warn!(
generation,
"bridge self-heal: this process is a resumed generation of an \
in-place re-exec triggered by a stale daemon-protocol mismatch (#714)"
);
}
#[cfg(unix)]
let boot_guard = if args.daemon {
Some(khive_runtime::daemon::acquire_daemon_boot_guard()?)
} else {
khive_runtime::daemon::acquire_recovery_lock()
};
let (server, schedule_rt) = build_server(&args)?;
spawn_schedule_tick_loop_if_daemon(&args, &server, schedule_rt);
#[cfg(unix)]
if args.daemon {
khive_runtime::daemon::run_daemon_with_boot_guard(server, boot_guard).await?;
return Ok(());
}
#[cfg(unix)]
drop(boot_guard);
#[cfg(not(unix))]
if args.daemon {
anyhow::bail!(
"--daemon mode requires Unix (macOS/Linux). On Windows, use the stdio transport."
);
}
serve_with_session_sweep(server, &args, registry).await
}
struct SessionSweepHandle {
shutdown_tx: tokio::sync::watch::Sender<()>,
join: tokio::task::JoinHandle<()>,
}
impl SessionSweepHandle {
async fn shutdown(self) {
drop(self.shutdown_tx);
if tokio::time::timeout(std::time::Duration::from_secs(2), self.join)
.await
.is_err()
{
tracing::warn!(
"ADR-091 Amendment 2 Plank A: session sweep task did not exit within 2s of \
the shutdown signal; its walpin heartbeat removal may not have completed"
);
}
}
}
fn spawn_session_walpin_sweep(server: &KhiveMcpServer) -> Option<SessionSweepHandle> {
let mut backends = Vec::new();
if let Some(pool) = server.pool() {
backends.push(khive_db::SweepBackend {
pool,
is_main: true,
});
}
for pool in server.secondary_pools() {
backends.push(khive_db::SweepBackend {
pool,
is_main: false,
});
}
if backends.is_empty() {
return None;
}
let config = khive_db::SessionSweepConfig::from_env();
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(());
let join = tokio::spawn(khive_db::run_session_sweep_task(
backends,
config,
shutdown_rx,
));
tracing::info!("ADR-091 Amendment 2 Plank A: session WAL-registry sweep started");
Some(SessionSweepHandle { shutdown_tx, join })
}
async fn serve_with_session_sweep(
server: KhiveMcpServer,
args: &Args,
registry: &TransportRegistry,
) -> anyhow::Result<()> {
let session_sweep = spawn_session_walpin_sweep(&server);
serve_holding_sweep(session_sweep, server, args, registry).await
}
async fn serve_holding_sweep(
session_sweep: Option<SessionSweepHandle>,
server: KhiveMcpServer,
args: &Args,
registry: &TransportRegistry,
) -> anyhow::Result<()> {
let result = async {
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
}
.await;
if let Some(sweep) = session_sweep {
sweep.shutdown().await;
}
result
}
fn spawn_schedule_tick_loop_if_daemon(
args: &Args,
server: &KhiveMcpServer,
schedule_rt: Option<KhiveRuntime>,
) {
if !args.daemon {
tracing::info!("schedule tick loop: skipped (client role; daemon owns the tick)");
return;
}
let Some(rt) = schedule_rt else {
tracing::info!(
"schedule tick loop: skipped (\"schedule\" pack is not in this daemon's \
resolved pack set)"
);
return;
};
let interval = crate::pending_events::tick_interval_from_env();
tracing::info!(
interval_secs = interval.as_secs(),
"schedule tick loop: spawning (daemon role)"
);
tokio::spawn(crate::pending_events::schedule_tick_loop(
rt,
server.clone(),
interval,
));
}
pub async fn serve_server(
server: KhiveMcpServer,
args: &Args,
registry: &TransportRegistry,
boot_guard: Option<std::fs::File>,
schedule_rt: Option<KhiveRuntime>,
) -> anyhow::Result<()> {
if let Some(generation) = args.resumed_generation {
tracing::warn!(
generation,
"bridge self-heal: this process is a resumed generation of an \
in-place re-exec triggered by a stale daemon-protocol mismatch (#714)"
);
}
spawn_schedule_tick_loop_if_daemon(args, &server, schedule_rt);
#[cfg(unix)]
if args.daemon {
khive_runtime::daemon::run_daemon_with_boot_guard(server, boot_guard).await?;
return Ok(());
}
drop(boot_guard);
#[cfg(not(unix))]
if args.daemon {
anyhow::bail!(
"--daemon mode requires Unix (macOS/Linux). On Windows, use the stdio transport."
);
}
serve_with_session_sweep(server, args, registry).await
}
pub fn build_registry_for_multi_backend(
base_config: RuntimeConfig,
khive_cfg: &KhiveConfig,
cli_db_override: Option<&str>,
) -> anyhow::Result<MultiBackendRegistry> {
khive_runtime::assert_db_anchor_consistent(base_config.db_path.as_deref(), cli_db_override)?;
build_registry_for_multi_backend_inner(base_config, khive_cfg, cli_db_override)
}
pub fn build_registry_for_multi_backend_with_db_anchor(
base_config: RuntimeConfig,
khive_cfg: &KhiveConfig,
cli_db_override: Option<&str>,
db_anchor: Option<&std::path::Path>,
) -> anyhow::Result<MultiBackendRegistry> {
khive_runtime::assert_captured_db_anchor_consistent(base_config.db_path.as_deref(), db_anchor)?;
build_registry_for_multi_backend_inner(base_config, khive_cfg, cli_db_override)
}
fn build_registry_for_multi_backend_inner(
base_config: RuntimeConfig,
khive_cfg: &KhiveConfig,
cli_db_override: Option<&str>,
) -> anyhow::Result<MultiBackendRegistry> {
let backend_count = khive_cfg.backends.len();
let force_memory = match cli_db_override {
Some(":memory:") => {
tracing::warn!(
"--db :memory: (or KHIVE_DB=:memory:) is overriding {backend_count} \
configured [[backends]] entries to in-memory storage for this invocation; \
khive.toml's declared backend paths will not be used this run"
);
true
}
Some(other) => {
anyhow::bail!(
"--db {other:?} (or KHIVE_DB) cannot be combined with [[backends]]: \
{backend_count} backend(s) are already declared in khive.toml, so applying \
this override here is ambiguous (it could silently collapse distinct \
declared backends onto a single file). Edit khive.toml directly to change \
backend paths, or pass --db :memory: to force all backends in-memory for \
this invocation."
);
}
None => false,
};
let mut backends: HashMap<String, Arc<StorageBackend>> = HashMap::new();
let mut path_to_backend: HashMap<std::path::PathBuf, Arc<StorageBackend>> = HashMap::new();
for backend_cfg in &khive_cfg.backends {
let owned_cfg = if force_memory {
BackendConfig {
kind: BackendKind::Memory,
path: None,
..backend_cfg.clone()
}
} else {
backend_cfg.clone()
};
let backend_cfg = &owned_cfg;
let canonical = canonical_backend_path(backend_cfg)?;
if let Some(ref canon) = canonical {
if let Some(existing) = path_to_backend.get(canon) {
backends.insert(backend_cfg.name.clone(), existing.clone());
continue;
}
}
let backend = open_backend(backend_cfg)?;
{
let mut writer = backend.pool().try_writer().map_err(|e| {
anyhow::anyhow!("backend {}: migration writer: {e}", backend_cfg.name)
})?;
run_migrations(writer.conn_mut())
.map_err(|e| anyhow::anyhow!("backend {}: migration: {e}", backend_cfg.name))?;
}
let arc = Arc::new(backend);
if let Some(canon) = canonical {
path_to_backend.insert(canon, arc.clone());
}
backends.insert(backend_cfg.name.clone(), arc);
}
let main_backend = backends
.get(BackendId::MAIN)
.ok_or_else(|| {
anyhow::anyhow!(
"[[backends]] is declared but no backend named \"main\" was found; \
add a [[backends]] entry with name = \"main\""
)
})?
.clone();
let pack_names = &base_config.packs;
let mut per_pack_runtimes_local: HashMap<String, KhiveRuntime> = HashMap::new();
for pack_name in pack_names {
let (backend_name, backend) = match khive_cfg.packs.get(pack_name.as_str()) {
None => (BackendId::MAIN, main_backend.clone()),
Some(pack_cfg) => {
let backend_name = pack_cfg.backend.as_str();
let backend = backends.get(backend_name).cloned().ok_or_else(|| {
let defined = backends.keys().cloned().collect::<Vec<_>>().join(", ");
anyhow::anyhow!(
"[packs.{pack_name}].backend = {backend_name:?} references an unknown backend; defined backends: {defined}"
)
})?;
(backend_name, backend)
}
};
let mut rt_config = base_config.clone();
rt_config.backend_id = BackendId::new(backend_name);
per_pack_runtimes_local.insert(
pack_name.clone(),
build_pack_runtime(backend, backend_name, rt_config, &main_backend),
);
}
let default_runtime = KhiveRuntime::from_backend(main_backend.clone(), {
let mut cfg = base_config.clone();
cfg.backend_id = BackendId::main();
cfg
});
if let Some(store) =
install_resolved_blob_store(&default_runtime, khive_cfg, main_backend.as_ref())?
{
for rt in per_pack_runtimes_local.values() {
rt.install_blob_store(store.clone());
}
}
#[cfg(feature = "bench-embedder")]
{
for rt in per_pack_runtimes_local.values() {
for name in rt.registered_embedding_model_names() {
rt.register_embedder(crate::bench_embedder::FeatureHashProvider::new(name));
}
}
for name in default_runtime.registered_embedding_model_names() {
default_runtime
.register_embedder(crate::bench_embedder::FeatureHashProvider::new(name));
}
}
enforce_strict_actor_mode(
default_runtime.config().actor_id.as_deref(),
&default_runtime.config().packs,
)?;
if should_warn_unattributed(
default_runtime.config().actor_id.as_deref(),
&default_runtime.config().packs,
) {
tracing::warn!(
"actor identity resolved to \"local\": comm sends will be stamped from \
\"local\" (unattributed) and comm.inbox will be unscoped (party-line). \
Set KHIVE_ACTOR or --actor to this lambda's id."
);
}
let gate = default_runtime.config().gate.clone();
let default_namespace = default_runtime.config().default_namespace.clone();
let config_id = crate::server::compute_config_id(default_runtime.config(), Some(khive_cfg));
let visible_namespaces = default_runtime.config().visible_namespaces.clone();
let mut builder = khive_runtime::VerbRegistryBuilder::new();
builder.with_gate(gate);
builder.with_default_namespace(default_namespace.as_str());
builder.with_visible_namespaces(visible_namespaces);
builder.with_actor_id(default_runtime.config().actor_id.clone());
if let Ok(tok) = default_runtime.authorize(khive_runtime::Namespace::local()) {
if let Ok(event_store) = default_runtime.events(&tok) {
builder.with_event_store(event_store);
}
}
khive_runtime::PackRegistry::register_packs_with_runtimes(
pack_names,
&per_pack_runtimes_local,
&default_runtime,
&mut builder,
)
.map_err(|e| anyhow::anyhow!("pack registration: {e}"))?;
let registry = builder
.build()
.map_err(|e| anyhow::anyhow!("registry build: {e}"))?;
default_runtime.install_edge_rules(registry.all_edge_rules());
for rt in per_pack_runtimes_local.values() {
rt.install_edge_rules(registry.all_edge_rules());
}
registry.call_register_embedders(&default_runtime);
registry.call_register_entity_type_validators(&default_runtime);
registry.call_register_note_mutation_hooks(&default_runtime);
let backend_for_pack: HashMap<&str, &StorageBackend> = per_pack_runtimes_local
.iter()
.map(|(name, rt)| (name.as_str(), rt.backend()))
.collect();
let main_ref: &StorageBackend = main_backend.as_ref();
registry
.apply_schema_plans_with_map(&backend_for_pack, main_ref)
.map_err(|e| anyhow::anyhow!("pack schema boot failure: {e}"))?;
let per_pack_runtimes_arc: HashMap<String, Arc<KhiveRuntime>> = per_pack_runtimes_local
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect();
Ok(MultiBackendRegistry {
registry,
default_namespace: default_namespace.as_str().to_string(),
config_id,
per_pack_runtimes: per_pack_runtimes_arc,
main_backend,
})
}
pub(crate) fn should_warn_unattributed(actor_id: Option<&str>, loaded_packs: &[String]) -> bool {
khive_runtime::should_warn_unattributed_actor(actor_id, loaded_packs)
}
pub(crate) fn is_strict_actor_mode() -> bool {
std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR")
.map(|v| v.trim() == "1")
.unwrap_or(false)
}
pub fn enforce_strict_actor_mode(
actor_id: Option<&str>,
loaded_packs: &[String],
) -> anyhow::Result<()> {
if is_strict_actor_mode() && should_warn_unattributed(actor_id, loaded_packs) {
anyhow::bail!(
"KHIVE_REQUIRE_ATTRIBUTED_ACTOR=1 is set but no actor identity is \
configured. Set KHIVE_ACTOR or --actor to this lambda's id before \
starting in strict mode (comm pack requires an attributed actor to \
prevent party-line inbox exposure)."
);
}
Ok(())
}
pub fn build_server(args: &Args) -> anyhow::Result<(KhiveMcpServer, Option<KhiveRuntime>)> {
let (cli_namespace_explicit, cli_namespace) =
resolve_cli_namespace(args).map_err(|e| anyhow::anyhow!("{e}"))?;
build_server_with_explicit_namespace(
args,
cli_namespace,
cli_namespace_explicit,
cli_namespace_explicit,
)
}
pub fn build_server_with_explicit_namespace(
args: &Args,
namespace: khive_runtime::Namespace,
namespace_explicit: bool,
actor_explicit: bool,
) -> anyhow::Result<(KhiveMcpServer, Option<KhiveRuntime>)> {
let (config, db_anchor) = resolve_runtime_config_with_db_anchor(RuntimeConfigInputs {
db: args.db.as_deref(),
config: args.config.as_deref(),
namespace,
namespace_explicit,
actor_explicit,
no_embed: args.no_embed,
packs: if args.pack.is_empty() {
None
} else {
Some(args.pack.clone())
},
brain_profile: args.brain_profile.clone(),
})?;
khive_runtime::assert_captured_db_anchor_consistent(
config.db_path.as_deref(),
db_anchor.as_deref(),
)?;
let db_path_for_config = config_discovery_db_anchor(args.db.as_deref());
let khive_cfg =
KhiveConfig::load_with_home_fallback(args.config.as_deref(), db_path_for_config.as_deref())
.map_err(|e| anyhow::anyhow!("config error: {e}"))?
.unwrap_or_default();
if khive_cfg.backends.is_empty() {
let runtime = KhiveRuntime::new(config)?;
install_resolved_blob_store(&runtime, &khive_cfg, runtime.backend())?;
#[cfg(feature = "bench-embedder")]
{
for name in runtime.registered_embedding_model_names() {
runtime.register_embedder(crate::bench_embedder::FeatureHashProvider::new(name));
}
}
enforce_strict_actor_mode(
runtime.config().actor_id.as_deref(),
&runtime.config().packs,
)?;
if should_warn_unattributed(
runtime.config().actor_id.as_deref(),
&runtime.config().packs,
) {
tracing::warn!(
"actor identity resolved to \"local\": comm sends will be stamped from \
\"local\" (unattributed) and comm.inbox will be unscoped (party-line). \
Set KHIVE_ACTOR or --actor to this lambda's id."
);
}
let schedule_rt = runtime
.config()
.packs
.iter()
.any(|p| p == "schedule")
.then(|| runtime.clone());
let fmt = apply_env_output_format(khive_cfg.runtime.default_output_format);
let server = KhiveMcpServer::new(runtime)
.map(|s| s.with_default_output_format(fmt))
.map_err(|e| anyhow::anyhow!("{e}"))?;
return Ok((server, schedule_rt));
}
let multi = build_registry_for_multi_backend_with_db_anchor(
config,
&khive_cfg,
args.db.as_deref(),
db_anchor.as_deref(),
)?;
let schedule_rt = multi
.per_pack_runtimes
.get("schedule")
.map(|rt| (**rt).clone());
let server = build_server_from_multi_backend_registry(multi, &khive_cfg, None);
Ok((server, schedule_rt))
}
fn canonical_backend_path(cfg: &BackendConfig) -> anyhow::Result<Option<PathBuf>> {
if cfg.kind == BackendKind::Memory {
return Ok(None);
}
let path = match cfg.path.as_ref() {
Some(p) => expand_tilde(p),
None => return Ok(None),
};
let parent = path
.parent()
.ok_or_else(|| anyhow::anyhow!("backend {}: path has no parent directory", cfg.name))?;
let file_name = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("backend {}: path has no file name", cfg.name))?;
std::fs::create_dir_all(parent).map_err(|e| {
anyhow::anyhow!(
"backend {}: cannot create parent dir {}: {e}",
cfg.name,
parent.display()
)
})?;
let canon_parent = parent.canonicalize().map_err(|e| {
anyhow::anyhow!(
"backend {}: cannot canonicalize parent dir {}: {e}",
cfg.name,
parent.display()
)
})?;
Ok(Some(canon_parent.join(file_name)))
}
pub fn build_server_multi_backend(
base_config: RuntimeConfig,
khive_cfg: &KhiveConfig,
cli_db_override: Option<&str>,
) -> anyhow::Result<KhiveMcpServer> {
khive_runtime::assert_db_anchor_consistent(base_config.db_path.as_deref(), cli_db_override)?;
let multi = build_registry_for_multi_backend_inner(base_config, khive_cfg, cli_db_override)?;
Ok(build_server_from_multi_backend_registry(
multi, khive_cfg, None,
))
}
pub fn build_server_multi_backend_with_db_anchor(
base_config: RuntimeConfig,
khive_cfg: &KhiveConfig,
cli_db_override: Option<&str>,
db_anchor: Option<&std::path::Path>,
) -> anyhow::Result<KhiveMcpServer> {
let multi = build_registry_for_multi_backend_with_db_anchor(
base_config,
khive_cfg,
cli_db_override,
db_anchor,
)?;
Ok(build_server_from_multi_backend_registry(
multi, khive_cfg, None,
))
}
pub fn build_server_from_multi_backend_registry(
multi: MultiBackendRegistry,
khive_cfg: &KhiveConfig,
coordinator: Option<Arc<dyn crate::coordinator::CoordinatorService>>,
) -> KhiveMcpServer {
let pool = checkpoint_pool_for(multi.main_backend.as_ref());
let secondary_pools = secondary_file_backed_pools(&multi);
let fmt = apply_env_output_format(khive_cfg.runtime.default_output_format);
let server = KhiveMcpServer::from_registry_with_meta(
multi.registry,
&multi.default_namespace,
&multi.config_id,
)
.with_default_output_format(fmt)
.with_secondary_pools(secondary_pools);
let server = match coordinator {
Some(c) => server.with_coordinator(c),
None => server,
};
match pool {
Some(p) => server.with_pool(p),
None => server,
}
}
fn secondary_file_backed_pools(multi: &MultiBackendRegistry) -> Vec<Arc<ConnectionPool>> {
use khive_storage::tx_registry::TxOrigin;
let mut seen: HashSet<khive_storage::tx_registry::DbIdentity> = HashSet::new();
if let TxOrigin::Database(id) = multi.main_backend.pool_arc().origin() {
seen.insert(id);
}
let mut pools = Vec::new();
for rt in multi.per_pack_runtimes.values() {
let backend = rt.backend();
if !backend.is_file_backed() {
continue;
}
let pool = backend.pool_arc();
let TxOrigin::Database(id) = pool.origin() else {
continue;
};
if seen.insert(id) {
pools.push(pool);
}
}
pools
}
#[derive(Debug, PartialEq, Eq)]
pub struct WiringSurface {
pub has_checkpoint_pool: bool,
pub output_format: OutputFormat,
}
impl WiringSurface {
pub fn capture(server: &KhiveMcpServer) -> Self {
Self {
has_checkpoint_pool: server.pool().is_some(),
output_format: server.default_output_format(),
}
}
}
pub fn checkpoint_pool_for(main_backend: &StorageBackend) -> Option<Arc<ConnectionPool>> {
if main_backend.is_file_backed() {
Some(main_backend.pool_arc())
} else {
None
}
}
fn install_resolved_blob_store(
rt: &KhiveRuntime,
khive_cfg: &KhiveConfig,
backend: &StorageBackend,
) -> anyhow::Result<Option<Arc<dyn khive_storage::BlobStore>>> {
match khive_runtime::resolve_blob_store(khive_cfg, backend) {
Ok(store) => {
rt.install_blob_store(store.clone());
Ok(Some(store))
}
Err(e) if khive_cfg.storage.blob.is_none() => {
tracing::debug!(
error = %e,
"no usable BlobStore for this backend and no [storage.blob] configured; \
leaving KhiveRuntime::blob_store() unset"
);
Ok(None)
}
Err(e) => Err(anyhow::anyhow!("[storage.blob] configuration error: {e}")),
}
}
fn build_pack_runtime(
backend: Arc<StorageBackend>,
backend_name: &str,
rt_config: RuntimeConfig,
main_backend: &Arc<StorageBackend>,
) -> KhiveRuntime {
let rt = KhiveRuntime::from_backend(backend, rt_config);
if backend_name != BackendId::MAIN {
rt.with_core_backend(main_backend.clone())
} else {
rt
}
}
fn open_backend(cfg: &BackendConfig) -> anyhow::Result<StorageBackend> {
match cfg.kind {
BackendKind::Memory => StorageBackend::memory()
.map_err(|e| anyhow::anyhow!("backend {}: memory open: {e}", cfg.name)),
BackendKind::Sqlite => {
let path = cfg.path.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"backend {}: sqlite backend requires a `path` field",
cfg.name
)
})?;
let expanded = expand_tilde(path);
if let Some(parent) = expanded.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
anyhow::anyhow!(
"backend {}: cannot create parent dir {}: {e}",
cfg.name,
parent.display()
)
})?;
}
if cfg.read_only {
StorageBackend::sqlite_read_only(&expanded).map_err(|e| {
anyhow::anyhow!("backend {}: sqlite read-only open: {e}", cfg.name)
})
} else {
StorageBackend::sqlite(&expanded)
.map_err(|e| anyhow::anyhow!("backend {}: sqlite open: {e}", cfg.name))
}
}
}
}
fn expand_tilde(path: &std::path::Path) -> PathBuf {
let s = path.to_string_lossy();
if let Some(rest) = s.strip_prefix("~/") {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(format!("{home}/{rest}"))
} else if s == "~" {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home)
} else {
path.to_path_buf()
}
}
pub fn config_discovery_db_anchor(db: Option<&str>) -> Option<std::path::PathBuf> {
db.and_then(|d| khive_runtime::resolve_db_anchor(Some(d)))
}
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 actor_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 (config, _) = resolve_runtime_config_with_db_anchor(inputs)?;
Ok(config)
}
pub fn resolve_runtime_config_with_db_anchor(
inputs: RuntimeConfigInputs<'_>,
) -> anyhow::Result<(RuntimeConfig, Option<PathBuf>)> {
let db_anchor = khive_runtime::resolve_db_anchor(inputs.db);
let db_path = db_anchor.clone();
let packs = inputs
.packs
.unwrap_or_else(|| RuntimeConfig::default().packs);
let cli_brain_profile = inputs.brain_profile.filter(|s| !s.trim().is_empty());
let db_path_for_config = config_discovery_db_anchor(inputs.db);
let resolved = if inputs.no_embed {
let no_embed_base = RuntimeConfig {
db_path,
default_namespace: inputs.namespace,
packs,
brain_profile: cli_brain_profile,
..RuntimeConfig::no_embeddings()
};
resolve_actor_from_config(inputs.config, no_embed_base, db_path_for_config.as_deref())?
} else {
let base_config = RuntimeConfig {
db_path,
default_namespace: inputs.namespace,
packs,
brain_profile: cli_brain_profile,
..RuntimeConfig::default()
};
resolve_config(inputs.config, base_config, db_path_for_config.as_deref())?
};
let resolved = {
let mut resolved = resolved;
let ns = resolved.default_namespace.as_str().to_string();
if inputs.namespace_explicit && ns != "local" {
resolved.actor_id = Some(ns);
} else if inputs.actor_explicit {
resolved.actor_id = None;
} else {
let project_actor = khive_runtime::resolve_project_actor_id(inputs.config)
.map_err(|e| anyhow::anyhow!("config error: {e}"))?;
resolved.actor_id = project_actor.or(resolved.actor_id);
}
resolved
};
Ok((apply_env_brain_profile(resolved), db_anchor))
}
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
}
pub fn apply_env_output_format(toml_default: Option<OutputFormat>) -> OutputFormat {
if let Ok(val) = std::env::var("KHIVE_OUTPUT_FORMAT") {
match val.trim() {
"json" => return OutputFormat::Json,
"auto" => return OutputFormat::Auto,
"table" => return OutputFormat::Table,
_ => {
tracing::warn!(
value = %val,
"KHIVE_OUTPUT_FORMAT has unknown value; falling back to TOML / builtin default"
);
}
}
}
toml_default.unwrap_or(OutputFormat::Json)
}
fn resolve_config(
config_path: Option<&std::path::Path>,
base: RuntimeConfig,
db_path: Option<&std::path::Path>,
) -> anyhow::Result<RuntimeConfig> {
match KhiveConfig::load_with_home_fallback(config_path, db_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 !khive_cfg.engines.is_empty() && (env_primary.is_some() || env_additional.is_some())
{
tracing::warn!(
"khive config [[engines]] present; KHIVE_EMBEDDING_MODEL / \
KHIVE_ADDITIONAL_EMBEDDING_MODELS env vars are overridden"
);
}
Ok(runtime_config_from_khive_config(&khive_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,
db_path: Option<&std::path::Path>,
) -> anyhow::Result<RuntimeConfig> {
match KhiveConfig::load_with_home_fallback(config_path, db_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::{BlobConfig, Namespace, StorageSectionConfig};
use serial_test::serial;
use std::io::Write;
#[allow(unused_imports)]
use khive_pack_template::TemplatePack as _TemplatePack;
#[test]
fn config_discovery_db_anchor_unset_is_none() {
assert_eq!(
config_discovery_db_anchor(None),
None,
"unset --db must not anchor discovery on the materialized home default"
);
}
#[test]
fn config_discovery_db_anchor_explicit_matches_resolve_db_anchor() {
assert_eq!(
config_discovery_db_anchor(Some("/tmp/explicit.db")),
khive_runtime::resolve_db_anchor(Some("/tmp/explicit.db")),
"an explicit --db must anchor discovery identically to resolve_db_anchor"
);
}
#[test]
fn config_discovery_db_anchor_memory_sentinel_is_none() {
assert_eq!(config_discovery_db_anchor(Some(":memory:")), None);
}
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::set_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS", "paraphrase");
let default_cfg = RuntimeConfig::default();
let default_primary = format!("{:?}", default_cfg.embedding_model);
assert!(
!default_cfg.additional_embedding_models.is_empty(),
"precondition: default config must carry an additional engine for this test to discriminate"
);
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,
actor_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");
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
}
#[test]
#[serial]
fn resolver_falls_back_to_env_when_config_has_no_engines() {
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
std::env::set_var("KHIVE_EMBEDDING_MODEL", "bge-small-en-v1.5");
let dir = tempfile::tempdir().expect("temp dir");
let path = write_config(
dir.path(),
r#"
[runtime]
brain_profile = "unrelated"
"#,
);
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: false,
packs: None,
brain_profile: None,
})
.expect("resolve config");
std::env::remove_var("KHIVE_EMBEDDING_MODEL");
assert_eq!(
format!("{:?}", resolved.embedding_model),
"Some(BgeSmallEnV15)",
"KHIVE_EMBEDDING_MODEL must be applied as the fallback when the \
config file has no [[engines]] block, not treated as ignored"
);
}
#[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,
actor_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,
actor_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,
actor_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"
);
}
#[test]
#[serial]
fn cli_actor_flag_populates_actor_id() {
std::env::remove_var("KHIVE_ACTOR");
let missing_config =
std::path::PathBuf::from("/nonexistent/khive-cli-actor-test/config.toml");
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&missing_config),
namespace: Namespace::parse("lambda:agent-x").expect("ns"),
namespace_explicit: true,
actor_explicit: true,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config");
assert_eq!(
resolved.actor_id.as_deref(),
Some("lambda:agent-x"),
"--actor flag must populate actor_id (flag==env parity), not just default_namespace"
);
assert_eq!(
resolved.default_namespace.as_str(),
"lambda:agent-x",
"the flag still sets the write namespace"
);
}
#[test]
#[serial]
fn no_embed_explicit_actor_preserves_git_write_config() {
std::env::remove_var("KHIVE_ACTOR");
let repo = tempfile::tempdir().expect("repo tempdir");
std::fs::create_dir(repo.path().join(".git")).expect("create .git");
let dir = tempfile::tempdir().expect("config tempdir");
let path = write_config(
dir.path(),
&format!(
"[[git_write.allowed]]\nrepo = {:?}\nbranches = [\"feat/*\"]\n",
repo.path().display().to_string()
),
);
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("lambda:cli-actor").expect("ns"),
namespace_explicit: true,
actor_explicit: true,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve no-embed config");
assert_eq!(resolved.default_namespace.as_str(), "lambda:cli-actor");
assert_eq!(resolved.actor_id.as_deref(), Some("lambda:cli-actor"));
assert_eq!(resolved.git_write.allowed.len(), 1);
assert_eq!(
resolved.git_write.allowed[0].repo,
repo.path().display().to_string()
);
assert_eq!(resolved.git_write.allowed[0].branches, vec!["feat/*"]);
}
#[test]
#[serial]
fn cli_actor_flag_local_stays_anonymous() {
std::env::remove_var("KHIVE_ACTOR");
let missing_config =
std::path::PathBuf::from("/nonexistent/khive-cli-actor-local-test/config.toml");
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&missing_config),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: true,
actor_explicit: true,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config");
assert_eq!(
resolved.actor_id, None,
"explicit --actor local must remain anonymous (no actor_id) so the \
unattributed-comm warning still fires"
);
}
struct SeatEnv {
original_cwd: PathBuf,
original_home: Option<std::ffi::OsString>,
_isolated_home: tempfile::TempDir,
}
impl SeatEnv {
fn enter(project_root: &std::path::Path) -> Self {
let original_cwd = std::env::current_dir().expect("read cwd");
let original_home = std::env::var_os("HOME");
let isolated_home = tempfile::tempdir().expect("isolated HOME tempdir");
std::env::set_current_dir(project_root).expect("chdir into seat project root");
std::env::set_var("HOME", isolated_home.path());
Self {
original_cwd,
original_home,
_isolated_home: isolated_home,
}
}
}
impl Drop for SeatEnv {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original_cwd);
match &self.original_home {
Some(h) => std::env::set_var("HOME", h),
None => std::env::remove_var("HOME"),
}
}
}
#[test]
#[serial]
fn resolve_project_actor_id_reads_cwd_anchored_project_config() {
std::env::remove_var("KHIVE_ACTOR");
let seat_dir = tempfile::tempdir().expect("seat tempdir");
std::fs::create_dir_all(seat_dir.path().join(".khive")).expect("mkdir seat .khive");
std::fs::write(
seat_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:seat-actor\"\n",
)
.expect("write seat config");
let _seat_env = SeatEnv::enter(seat_dir.path());
assert_eq!(
khive_runtime::resolve_project_actor_id(None).expect("no config error"),
Some("lambda:seat-actor".to_string()),
"resolve_project_actor_id must read the cwd-anchored .khive/config.toml \
regardless of any database directory"
);
}
#[test]
#[serial]
fn seat_shaped_project_actor_resolves_through_full_tier_chain() {
std::env::remove_var("KHIVE_ACTOR");
let seat_dir = tempfile::tempdir().expect("seat tempdir");
std::fs::create_dir_all(seat_dir.path().join(".khive")).expect("mkdir seat .khive");
std::fs::write(
seat_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:seat-actor\"\n",
)
.expect("write seat config");
let db_dir = tempfile::tempdir().expect("db tempdir");
let khive_dir = db_dir.path().join(".khive");
std::fs::create_dir_all(&khive_dir).expect("mkdir db .khive");
let db_path = khive_dir.join("khive.db");
std::fs::write(&db_path, b"").expect("touch db file");
let db_str = db_path.to_str().expect("utf8 path").to_string();
let _seat_env = SeatEnv::enter(seat_dir.path());
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(&db_str),
config: None,
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve seat-shaped config");
assert_eq!(
resolved.actor_id.as_deref(),
Some("lambda:seat-actor"),
"a seat-shaped cwd with its own [actor] must resolve that actor through \
the full discovery path even when the shared db-anchored config \
location carries none — got {:?}",
resolved.actor_id
);
assert_ne!(
resolved.actor_id.as_deref(),
Some("local"),
"must not collapse to the literal namespace string"
);
}
#[test]
#[serial]
fn resolve_runtime_config_unset_db_discovers_cwd_config_over_home() {
std::env::remove_var("KHIVE_ACTOR");
let project_dir = tempfile::tempdir().expect("project tempdir");
std::fs::create_dir_all(project_dir.path().join(".khive")).expect("mkdir project .khive");
std::fs::write(
project_dir.path().join(".khive/config.toml"),
"[runtime]\nbrain_profile = \"cwd-profile\"\n",
)
.expect("write project config");
let seat_env = SeatEnv::enter(project_dir.path());
std::fs::create_dir_all(seat_env._isolated_home.path().join(".khive"))
.expect("mkdir home .khive");
std::fs::write(
seat_env._isolated_home.path().join(".khive/config.toml"),
"[runtime]\nbrain_profile = \"home-profile\"\n",
)
.expect("write home config");
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: None,
config: None,
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve unset-db config");
assert_eq!(
resolved.brain_profile.as_deref(),
Some("cwd-profile"),
"unset --db must resolve tier-3 discovery against the project cwd, \
not $HOME/.khive/khive.db's directory — got {:?}",
resolved.brain_profile
);
}
#[test]
#[serial]
fn cli_actor_flag_wins_over_project_config_actor() {
std::env::remove_var("KHIVE_ACTOR");
let seat_dir = tempfile::tempdir().expect("seat tempdir");
std::fs::create_dir_all(seat_dir.path().join(".khive")).expect("mkdir seat .khive");
std::fs::write(
seat_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:project-actor\"\n",
)
.expect("write seat config");
let _seat_env = SeatEnv::enter(seat_dir.path());
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: None,
namespace: Namespace::parse("lambda:cli-actor").expect("ns"),
namespace_explicit: true,
actor_explicit: true,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config");
assert_eq!(
resolved.actor_id.as_deref(),
Some("lambda:cli-actor"),
"an explicit --actor flag must win over a discovered project-config actor"
);
}
#[test]
#[serial]
fn project_actor_config_beats_khive_actor_env_which_falls_back_to_anonymous() {
std::env::remove_var("KHIVE_ACTOR");
let dir = tempfile::tempdir().expect("temp dir");
let path = write_config(
dir.path(),
r#"
[actor]
id = "lambda:project-actor"
"#,
);
std::env::set_var("KHIVE_ACTOR", "lambda:env-actor");
let with_project_config = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&path),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config with project actor");
let missing_config =
std::path::PathBuf::from("/nonexistent/khive-project-vs-env-test/config.toml");
let without_project_config = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: Some(&missing_config),
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config without project actor");
std::env::remove_var("KHIVE_ACTOR");
assert_eq!(
with_project_config.actor_id.as_deref(),
Some("lambda:project-actor"),
"a project-config [actor] id must win over KHIVE_ACTOR env"
);
assert_eq!(
without_project_config.actor_id.as_deref(),
Some("lambda:env-actor"),
"KHIVE_ACTOR env must still be used when no project config actor exists"
);
}
#[test]
#[serial]
fn real_clap_path_khive_actor_env_no_longer_wins_over_project_config() {
use clap::Parser;
std::env::remove_var("KHIVE_ACTOR");
let seat_dir = tempfile::tempdir().expect("seat tempdir");
std::fs::create_dir_all(seat_dir.path().join(".khive")).expect("mkdir seat .khive");
std::fs::write(
seat_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:project-actor\"\n",
)
.expect("write seat config");
let _seat_env = SeatEnv::enter(seat_dir.path());
std::env::set_var("KHIVE_ACTOR", "lambda:env-actor");
let args = Args::try_parse_from(["mcp"]).expect("parse real mcp args");
let (namespace_explicit, namespace) =
resolve_cli_namespace(&args).expect("resolve cli namespace");
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: None,
namespace,
namespace_explicit,
actor_explicit: namespace_explicit,
no_embed: true,
packs: None,
brain_profile: None,
});
std::env::remove_var("KHIVE_ACTOR");
let resolved = resolved.expect("resolve config");
assert!(
!namespace_explicit,
"KHIVE_ACTOR env alone must NOT make the CLI namespace tier explicit"
);
assert_eq!(
resolved.actor_id.as_deref(),
Some("lambda:project-actor"),
"project-config [actor] id must win over KHIVE_ACTOR env on the real clap path"
);
assert_eq!(
resolved.default_namespace.as_str(),
"local",
"KHIVE_ACTOR env must never set default_namespace, only actor_id"
);
}
#[test]
#[serial]
fn real_clap_path_khive_actor_env_falls_back_to_tier3_actor_id() {
use clap::Parser;
std::env::remove_var("KHIVE_ACTOR");
let seat_dir = tempfile::tempdir().expect("seat tempdir");
let _seat_env = SeatEnv::enter(seat_dir.path());
std::env::set_var("KHIVE_ACTOR", "lambda:env-only-actor");
let args = Args::try_parse_from(["mcp"]).expect("parse real mcp args");
let (namespace_explicit, namespace) =
resolve_cli_namespace(&args).expect("resolve cli namespace");
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(":memory:"),
config: None,
namespace,
namespace_explicit,
actor_explicit: namespace_explicit,
no_embed: true,
packs: None,
brain_profile: None,
});
std::env::remove_var("KHIVE_ACTOR");
let resolved = resolved.expect("resolve config");
assert!(
!namespace_explicit,
"KHIVE_ACTOR env alone must NOT make the CLI namespace tier explicit"
);
assert_eq!(
resolved.actor_id.as_deref(),
Some("lambda:env-only-actor"),
"KHIVE_ACTOR env must still land as the tier-3 actor_id fallback \
when no project config exists"
);
assert_eq!(
resolved.default_namespace.as_str(),
"local",
"KHIVE_ACTOR env must never set default_namespace, only actor_id"
);
}
#[test]
#[serial]
fn explicit_actor_local_suppresses_project_and_db_actor_tiers() {
std::env::remove_var("KHIVE_ACTOR");
let seat_dir = tempfile::tempdir().expect("seat tempdir");
std::fs::create_dir_all(seat_dir.path().join(".khive")).expect("mkdir seat .khive");
std::fs::write(
seat_dir.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:seat-actor\"\n",
)
.expect("write seat config");
let db_dir = tempfile::tempdir().expect("db tempdir");
let khive_dir = db_dir.path().join(".khive");
std::fs::create_dir_all(&khive_dir).expect("mkdir db .khive");
std::fs::write(
khive_dir.join("config.toml"),
"[actor]\nid = \"lambda:db-actor\"\n",
)
.expect("write db-anchored config");
let db_path = khive_dir.join("khive.db");
std::fs::write(&db_path, b"").expect("touch db file");
let db_str = db_path.to_str().expect("utf8 path").to_string();
let _seat_env = SeatEnv::enter(seat_dir.path());
let resolved = resolve_runtime_config(RuntimeConfigInputs {
db: Some(&db_str),
config: None,
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: true,
actor_explicit: true,
no_embed: false,
packs: None,
brain_profile: None,
})
.expect("resolve config");
assert_eq!(
resolved.actor_id, None,
"explicit --actor local must resolve to anonymous even when both a \
project-config and a db-anchored config declare an [actor] id — got {:?}",
resolved.actor_id
);
assert_eq!(
resolved.default_namespace.as_str(),
"local",
"explicit --actor local must keep default_namespace local"
);
}
#[test]
#[serial]
fn config_id_byte_identical_across_different_actor_ids() {
std::env::remove_var("KHIVE_ACTOR");
std::env::remove_var("KHIVE_EMBEDDING_MODEL");
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
let db_dir = tempfile::tempdir().expect("db tempdir");
let khive_dir = db_dir.path().join(".khive");
std::fs::create_dir_all(&khive_dir).expect("mkdir db .khive");
let db_path = khive_dir.join("khive.db");
std::fs::write(&db_path, b"").expect("touch db file");
let db_str = db_path.to_str().expect("utf8 path").to_string();
let seat_a = tempfile::tempdir().expect("seat a");
std::fs::create_dir_all(seat_a.path().join(".khive")).expect("mkdir seat a .khive");
std::fs::write(
seat_a.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:actor-a\"\n",
)
.expect("write seat a config");
let seat_b = tempfile::tempdir().expect("seat b");
std::fs::create_dir_all(seat_b.path().join(".khive")).expect("mkdir seat b .khive");
std::fs::write(
seat_b.path().join(".khive/config.toml"),
"[actor]\nid = \"lambda:actor-b\"\n",
)
.expect("write seat b config");
let cfg_a = {
let _seat_env = SeatEnv::enter(seat_a.path());
resolve_runtime_config(RuntimeConfigInputs {
db: Some(&db_str),
config: None,
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config a")
};
let cfg_b = {
let _seat_env = SeatEnv::enter(seat_b.path());
resolve_runtime_config(RuntimeConfigInputs {
db: Some(&db_str),
config: None,
namespace: Namespace::parse("local").expect("ns"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: None,
brain_profile: None,
})
.expect("resolve config b")
};
assert_eq!(cfg_a.actor_id.as_deref(), Some("lambda:actor-a"));
assert_eq!(cfg_b.actor_id.as_deref(), Some("lambda:actor-b"));
assert_ne!(
cfg_a.actor_id, cfg_b.actor_id,
"precondition: the two connections must actually declare different actors"
);
assert_eq!(
cfg_a.default_namespace.as_str(),
"local",
"default_namespace must stay local regardless of the configured actor"
);
assert_eq!(
cfg_b.default_namespace.as_str(),
"local",
"default_namespace must stay local regardless of the configured actor"
);
assert_eq!(
crate::server::compute_config_id(&cfg_a, None),
crate::server::compute_config_id(&cfg_b, None),
"config_id must be byte-identical across connections that differ ONLY \
in [actor] id and folded visibility — identity fields must never feed compute_config_id"
);
}
fn base_runtime_config_for_multi_backend() -> RuntimeConfig {
use khive_runtime::{AllowAllGate, BackendId, Namespace};
RuntimeConfig {
db_path: khive_runtime::resolve_db_anchor(None),
gate: std::sync::Arc::new(AllowAllGate),
default_namespace: Namespace::parse("local").expect("ns"),
embedding_model: None,
additional_embedding_models: vec![],
packs: vec!["kg".to_string(), "template".to_string()],
backend_id: BackendId::main(),
..RuntimeConfig::default()
}
}
#[tokio::test]
#[serial]
async fn multi_backend_boots_ok_with_two_memory_backends() {
use crate::tools::request::RequestParams;
use khive_runtime::PackConfig;
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"template".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let server = build_server_multi_backend(base_cfg, &khive_cfg, None)
.expect("multi-backend boot must succeed");
let kg_resp = server
.dispatch_request_local(RequestParams {
ops: r#"create(kind="concept", name="MultiBackendTestEntity")"#.to_string(),
presentation: None,
presentation_per_op: None,
save_to: None,
format: None,
format_per_op: None,
request_id: None,
})
.await
.expect("kg dispatch must not error");
let kg_json: serde_json::Value =
serde_json::from_str(&kg_resp).expect("kg response is valid JSON");
let first_ok = kg_json["results"][0]["ok"].as_bool();
assert_eq!(
first_ok,
Some(true),
"kg create must succeed; response: {kg_resp}"
);
let template_resp = server
.dispatch_request_local(RequestParams {
ops: r#"template.my_verb(name="multi-backend-test")"#.to_string(),
presentation: None,
presentation_per_op: None,
save_to: None,
format: None,
format_per_op: None,
request_id: None,
})
.await
.expect("template dispatch must not error");
let template_json: serde_json::Value =
serde_json::from_str(&template_resp).expect("template response is valid JSON");
let first_template_ok = template_json["results"][0]["ok"].as_bool();
assert_eq!(
first_template_ok,
Some(true),
"template.my_verb must succeed; response: {template_resp}"
);
}
#[test]
#[serial]
fn kkernel_multi_backend_path_wires_pool_for_file_backed_main() {
let dir = tempfile::tempdir().expect("temp dir");
let main_path = dir.path().join("main.db");
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Sqlite,
path: Some(main_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
}],
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let multi = build_registry_for_multi_backend(base_cfg, &khive_cfg, None)
.expect("multi-backend registry build must succeed");
let server = build_server_from_multi_backend_registry(multi, &khive_cfg, None);
assert!(
server.pool().is_some(),
"file-backed multi-backend main must wire a checkpoint pool onto the server"
);
}
#[test]
#[serial]
fn kkernel_multi_backend_path_leaves_pool_none_for_in_memory_main() {
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let multi = build_registry_for_multi_backend(base_cfg, &khive_cfg, None)
.expect("multi-backend registry build must succeed");
let server = build_server_from_multi_backend_registry(multi, &khive_cfg, None);
assert!(
server.pool().is_none(),
"in-memory multi-backend main must never carry a checkpoint pool"
);
}
#[test]
#[serial]
fn single_backend_boot_wires_configured_s3_blob_store() {
std::env::remove_var("KHIVE_DB");
std::env::remove_var("KHIVE_ACTOR");
std::env::remove_var("KHIVE_PACKS");
std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR");
let prev_access_key = std::env::var("AWS_ACCESS_KEY_ID").ok();
let prev_secret_key = std::env::var("AWS_SECRET_ACCESS_KEY").ok();
std::env::remove_var("AWS_ACCESS_KEY_ID");
std::env::remove_var("AWS_SECRET_ACCESS_KEY");
let dir = tempfile::tempdir().expect("temp dir");
let config_path = write_config(
dir.path(),
r#"
[storage.blob]
backend = "s3"
bucket = "khive-blobs"
region = "us-east-1"
"#,
);
use clap::Parser;
let args = Args::parse_from([
"mcp",
"--db",
":memory:",
"--pack",
"kg",
"--config",
config_path.to_str().expect("utf8 path"),
]);
let result = build_server(&args);
match prev_access_key {
Some(v) => std::env::set_var("AWS_ACCESS_KEY_ID", v),
None => std::env::remove_var("AWS_ACCESS_KEY_ID"),
}
match prev_secret_key {
Some(v) => std::env::set_var("AWS_SECRET_ACCESS_KEY", v),
None => std::env::remove_var("AWS_SECRET_ACCESS_KEY"),
}
let err = result.err().expect(
"an s3 blob backend with no AWS credentials must fail boot through the real \
single-backend path -- a silent fs fallback would return Ok here instead",
);
let msg = err.to_string();
assert!(
msg.contains("AWS_ACCESS_KEY_ID"),
"expected the credential-env error surfaced through build_server, got: {msg}"
);
}
#[test]
#[serial]
fn multi_backend_boot_wires_configured_s3_blob_store() {
let prev_access_key = std::env::var("AWS_ACCESS_KEY_ID").ok();
let prev_secret_key = std::env::var("AWS_SECRET_ACCESS_KEY").ok();
std::env::remove_var("AWS_ACCESS_KEY_ID");
std::env::remove_var("AWS_SECRET_ACCESS_KEY");
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
storage: StorageSectionConfig {
blob: Some(BlobConfig::S3 {
bucket: "khive-blobs".to_string(),
region: "us-east-1".to_string(),
endpoint: None,
prefix: None,
allow_http: None,
}),
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_registry_for_multi_backend(base_cfg, &khive_cfg, None);
match prev_access_key {
Some(v) => std::env::set_var("AWS_ACCESS_KEY_ID", v),
None => std::env::remove_var("AWS_ACCESS_KEY_ID"),
}
match prev_secret_key {
Some(v) => std::env::set_var("AWS_SECRET_ACCESS_KEY", v),
None => std::env::remove_var("AWS_SECRET_ACCESS_KEY"),
}
let err = result.err().expect(
"an s3 blob backend with no AWS credentials must fail boot through the real \
multi-backend path -- a silent fs fallback would return Ok here instead",
);
let msg = err.to_string();
assert!(
msg.contains("AWS_ACCESS_KEY_ID"),
"expected the credential-env error surfaced through \
build_registry_for_multi_backend, got: {msg}"
);
}
const DUMMY_AWS_ACCESS_KEY_ID: &str = "AKIADUMMYWITNESSKEY00";
const DUMMY_AWS_SECRET_ACCESS_KEY: &str = "dummy-witness-secret-access-key-never-real";
struct DummyAwsCredsGuard {
prev_access_key: Option<String>,
prev_secret_key: Option<String>,
}
impl DummyAwsCredsGuard {
fn set() -> Self {
let prev_access_key = std::env::var("AWS_ACCESS_KEY_ID").ok();
let prev_secret_key = std::env::var("AWS_SECRET_ACCESS_KEY").ok();
std::env::set_var("AWS_ACCESS_KEY_ID", DUMMY_AWS_ACCESS_KEY_ID);
std::env::set_var("AWS_SECRET_ACCESS_KEY", DUMMY_AWS_SECRET_ACCESS_KEY);
Self {
prev_access_key,
prev_secret_key,
}
}
}
impl Drop for DummyAwsCredsGuard {
fn drop(&mut self) {
match self.prev_access_key.take() {
Some(v) => std::env::set_var("AWS_ACCESS_KEY_ID", v),
None => std::env::remove_var("AWS_ACCESS_KEY_ID"),
}
match self.prev_secret_key.take() {
Some(v) => std::env::set_var("AWS_SECRET_ACCESS_KEY", v),
None => std::env::remove_var("AWS_SECRET_ACCESS_KEY"),
}
}
}
fn s3_blob_config() -> BlobConfig {
BlobConfig::S3 {
bucket: "khive-blobs".to_string(),
region: "us-east-1".to_string(),
endpoint: None,
prefix: None,
allow_http: None,
}
}
#[test]
#[serial]
fn multi_backend_boot_installs_s3_blob_store_on_successful_selection() {
let _creds = DummyAwsCredsGuard::set();
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
storage: StorageSectionConfig {
blob: Some(s3_blob_config()),
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let multi = build_registry_for_multi_backend(base_cfg, &khive_cfg, None)
.expect("valid dummy AWS credentials must resolve through the multi-backend path");
assert!(
!multi.per_pack_runtimes.is_empty(),
"precondition: the base config declares at least one pack"
);
for (pack_name, rt) in &multi.per_pack_runtimes {
let store = rt.blob_store().unwrap_or_else(|| {
panic!("pack {pack_name:?} must have the S3 selection installed on its runtime")
});
let debug = format!("{store:?}");
assert!(
debug.contains("S3BlobStore"),
"pack {pack_name:?}: expected the installed store to be an S3BlobStore, got: {debug}"
);
}
}
#[test]
#[serial]
fn secondary_pack_runtime_core_resolves_to_main_after_build_registry() {
use khive_runtime::PackConfig;
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"template".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_registry_for_multi_backend(base_cfg, &khive_cfg, None)
.expect("multi-backend registry must boot");
let template_rt = result
.per_pack_runtimes
.get("template")
.expect("template pack runtime must be present in per_pack_runtimes");
assert_eq!(
template_rt.backend_id().as_str(),
"secondary",
"template pack runtime's own backend_id must be \"secondary\""
);
assert_eq!(
template_rt.core().backend_id().as_str(),
BackendId::MAIN,
"secondary-backend pack must have core_backend wired to main (ADR-073); \
core().backend_id() returned {:?} — build_pack_runtime wiring missing",
template_rt.core().backend_id().as_str()
);
}
#[test]
#[serial]
#[cfg(unix)]
fn secondary_pools_dedup_by_canonical_identity_across_alias_spellings() {
use khive_runtime::PackConfig;
let dir = tempfile::tempdir().unwrap();
let real_path = dir.path().join("khive.db");
std::fs::write(&real_path, b"").unwrap();
let alias_path = dir.path().join("khive_alias.db");
std::os::unix::fs::symlink(&real_path, &alias_path).unwrap();
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "direct".to_string(),
kind: BackendKind::Sqlite,
path: Some(real_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "alias".to_string(),
kind: BackendKind::Sqlite,
path: Some(alias_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"kg".to_string(),
PackConfig {
backend: "direct".to_string(),
},
);
m.insert(
"comm".to_string(),
PackConfig {
backend: "alias".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let multi = build_registry_for_multi_backend(base_cfg, &khive_cfg, None)
.expect("multi-backend registry with alias-spelled backends must boot");
let secondary = secondary_file_backed_pools(&multi);
assert_eq!(
secondary.len(),
1,
"two backends aliasing the same database file must dedup to exactly one \
secondary pool by canonical identity, got {} pools",
secondary.len()
);
let server = build_server_from_multi_backend_registry(multi, &khive_cfg, None);
let mut backends = Vec::new();
if let Some(pool) = server.pool() {
backends.push(khive_db::SweepBackend {
pool,
is_main: true,
});
}
for pool in server.secondary_pools() {
backends.push(khive_db::SweepBackend {
pool,
is_main: false,
});
}
assert_eq!(
backends.len(),
1,
"exactly one SweepBackend must survive dedup for the alias pair — the \
in-memory main backend contributes no pool of its own"
);
}
#[test]
#[serial]
fn memory_override_forces_all_backends_in_memory_and_never_creates_sqlite_file() {
use khive_runtime::PackConfig;
let dir = tempfile::tempdir().unwrap();
let main_path = dir.path().join("main_should_never_be_created.db");
let secondary_path = dir.path().join("secondary_should_never_be_created.db");
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Sqlite,
path: Some(main_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Sqlite,
path: Some(secondary_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"comm".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_registry_for_multi_backend(base_cfg, &khive_cfg, Some(":memory:"));
if let Err(ref e) = result {
panic!(
"--db :memory: override must force both declared sqlite backends \
in-memory and boot successfully; got: {e}"
);
}
assert!(
!main_path.exists(),
"main backend's declared sqlite path must never be created on disk when \
--db :memory: overrides it; found file at {main_path:?}"
);
assert!(
!secondary_path.exists(),
"secondary backend's declared sqlite path must never be created on disk \
when --db :memory: overrides it; found file at {secondary_path:?}"
);
}
#[test]
#[serial]
fn concrete_db_override_with_backends_declared_is_rejected() {
use khive_runtime::PackConfig;
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"comm".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = RuntimeConfig {
db_path: khive_runtime::resolve_db_anchor(Some("/tmp/some-explicit-override.db")),
..base_runtime_config_for_multi_backend()
};
let result = build_registry_for_multi_backend(
base_cfg,
&khive_cfg,
Some("/tmp/some-explicit-override.db"),
);
assert!(
result.is_err(),
"a concrete --db path override combined with declared [[backends]] must \
be rejected as ambiguous"
);
if let Err(err) = result {
let msg = err.to_string();
assert!(
msg.contains("khive.toml"),
"error message must point at khive.toml as where to make the change \
instead; got: {msg}"
);
}
}
#[test]
#[serial]
fn multi_backend_missing_main_returns_error_mentioning_main() {
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "secondary".to_string(), kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
packs: std::collections::HashMap::new(),
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_server_multi_backend(base_cfg, &khive_cfg, None);
assert!(
result.is_err(),
"missing main backend must produce an error"
);
if let Err(err) = result {
assert!(
err.to_string().contains("main"),
"error message must mention \"main\"; got: {err}"
);
}
}
#[test]
#[serial]
fn multi_backend_registry_rejects_undefined_pack_backend() {
use khive_runtime::PackConfig;
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"template".to_string(),
PackConfig {
backend: "archive".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_registry_for_multi_backend(base_cfg, &khive_cfg, None);
assert!(
result.is_err(),
"an undeclared configured pack backend must be a startup error, not a silent \
fallback to main"
);
if let Err(err) = result {
let msg = err.to_string();
assert!(
msg.contains("packs.template"),
"error must name the pack; got: {msg}"
);
assert!(
msg.contains("archive"),
"error must name the undeclared backend; got: {msg}"
);
assert!(
msg.contains("main"),
"error must list the defined backends; got: {msg}"
);
}
}
#[test]
#[serial]
fn multi_backend_server_rejects_undefined_pack_backend() {
use khive_runtime::PackConfig;
let khive_cfg = KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"template".to_string(),
PackConfig {
backend: "archive".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_server_multi_backend(base_cfg, &khive_cfg, None);
assert!(
result.is_err(),
"an undeclared configured pack backend must be a startup error, not a silent \
fallback to main"
);
if let Err(err) = result {
let msg = err.to_string();
assert!(
msg.contains("packs.template"),
"error must name the pack; got: {msg}"
);
assert!(
msg.contains("archive"),
"error must name the undeclared backend; got: {msg}"
);
assert!(
msg.contains("main"),
"error must list the defined backends; got: {msg}"
);
}
}
#[test]
fn read_only_backend_rejects_writes() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("ro_test.db");
let rw = StorageBackend::sqlite(&db_path).expect("rw backend");
rw.apply_pack_ddl_statements(&[
"CREATE TABLE IF NOT EXISTS ro_check (id INTEGER PRIMARY KEY)",
])
.expect("DDL on rw backend");
drop(rw);
let ro = StorageBackend::sqlite_read_only(&db_path).expect("ro backend");
let result = ro.apply_pack_ddl_statements(&["INSERT INTO ro_check (id) VALUES (1)"]);
assert!(
result.is_err(),
"write to a read-only backend must fail; got Ok(())"
);
}
struct HomeGuard {
original: Option<std::ffi::OsString>,
}
impl HomeGuard {
fn redirect_to(dir: &std::path::Path) -> Self {
let original = std::env::var_os("HOME");
std::env::set_var("HOME", dir);
Self { original }
}
}
impl Drop for HomeGuard {
fn drop(&mut self) {
match &self.original {
Some(h) => std::env::set_var("HOME", h),
None => std::env::remove_var("HOME"),
}
}
}
fn duplicate_sqlite_path_config(db_path: &std::path::Path) -> KhiveConfig {
use khive_runtime::PackConfig;
KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Sqlite,
path: Some(db_path.to_path_buf()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "alias".to_string(),
kind: BackendKind::Sqlite,
path: Some(db_path.to_path_buf()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut packs = std::collections::HashMap::new();
packs.insert(
"comm".to_string(),
PackConfig {
backend: "alias".to_string(),
},
);
packs
},
..KhiveConfig::default()
}
}
fn memory_main_backend_config() -> KhiveConfig {
KhiveConfig {
backends: vec![BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
}],
..KhiveConfig::default()
}
}
fn assert_db_anchor_drift<T>(result: anyhow::Result<T>) {
match result {
Err(error) => assert!(
error.to_string().contains("db-path resolution drift"),
"legacy builder must reject raw db input that disagrees with the resolved config: {error}"
),
Ok(_) => panic!("legacy builder accepted raw db input that disagrees with the resolved config"),
}
}
#[test]
fn legacy_registry_rejects_mismatched_explicit_db_override() {
let base_cfg = RuntimeConfig {
db_path: Some(PathBuf::from("/tmp/khive-resolved.db")),
..base_runtime_config_for_multi_backend()
};
assert_db_anchor_drift(build_registry_for_multi_backend(
base_cfg,
&memory_main_backend_config(),
Some("/tmp/khive-raw.db"),
));
}
#[test]
fn legacy_server_rejects_mismatched_explicit_db_override() {
let base_cfg = RuntimeConfig {
db_path: Some(PathBuf::from("/tmp/khive-resolved.db")),
..base_runtime_config_for_multi_backend()
};
assert_db_anchor_drift(build_server_multi_backend(
base_cfg,
&memory_main_backend_config(),
Some("/tmp/khive-raw.db"),
));
}
#[test]
#[serial]
fn legacy_registry_rejects_unset_db_after_home_changes() {
let first_home = tempfile::tempdir().unwrap();
let _home_guard = HomeGuard::redirect_to(first_home.path());
let base_cfg = base_runtime_config_for_multi_backend();
let second_home = tempfile::tempdir().unwrap();
std::env::set_var("HOME", second_home.path());
assert_db_anchor_drift(build_registry_for_multi_backend(
base_cfg,
&memory_main_backend_config(),
None,
));
}
#[test]
#[serial]
fn legacy_server_rejects_unset_db_after_home_changes() {
let first_home = tempfile::tempdir().unwrap();
let _home_guard = HomeGuard::redirect_to(first_home.path());
let base_cfg = base_runtime_config_for_multi_backend();
let second_home = tempfile::tempdir().unwrap();
std::env::set_var("HOME", second_home.path());
assert_db_anchor_drift(build_server_multi_backend(
base_cfg,
&memory_main_backend_config(),
None,
));
}
#[test]
#[serial]
fn duplicate_sqlite_paths_deduplicated_to_single_backend() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("shared.db");
let khive_cfg = duplicate_sqlite_path_config(&db_path);
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_server_multi_backend(base_cfg, &khive_cfg, None);
if let Err(ref e) = result {
panic!(
"two backends with the same canonical path must share one Arc and boot ok; got: {e}"
);
}
}
#[test]
#[serial]
fn multi_backend_boot_uses_anchor_captured_by_runtime_config() {
let first_home = tempfile::tempdir().unwrap();
let _home_guard = HomeGuard::redirect_to(first_home.path());
let config_path = first_home.path().join("config.toml");
std::fs::write(&config_path, "").expect("write empty config");
let (base_cfg, db_anchor) = resolve_runtime_config_with_db_anchor(RuntimeConfigInputs {
db: None,
config: Some(&config_path),
namespace: Namespace::parse("local").expect("namespace"),
namespace_explicit: false,
actor_explicit: false,
no_embed: true,
packs: Some(vec!["kg".to_string()]),
brain_profile: None,
})
.expect("resolve runtime config before HOME changes");
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().join("shared.db");
let khive_cfg = duplicate_sqlite_path_config(&db_path);
let second_home = tempfile::tempdir().unwrap();
std::env::set_var("HOME", second_home.path());
let result = build_server_multi_backend_with_db_anchor(
base_cfg,
&khive_cfg,
None,
db_anchor.as_deref(),
);
if let Err(error) = result {
panic!(
"multi-backend construction must retain the anchor captured by \
resolve_runtime_config instead of re-reading HOME: {error}"
);
}
}
#[test]
#[serial]
fn memory_override_forces_all_backends_in_memory_and_never_creates_sqlite_file_via_build_server_multi_backend(
) {
use khive_runtime::PackConfig;
let dir = tempfile::tempdir().unwrap();
let main_path = dir.path().join("main_should_never_be_created.db");
let secondary_path = dir.path().join("secondary_should_never_be_created.db");
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Sqlite,
path: Some(main_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Sqlite,
path: Some(secondary_path.clone()),
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"comm".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = base_runtime_config_for_multi_backend();
let result = build_server_multi_backend(base_cfg, &khive_cfg, Some(":memory:"));
if let Err(ref e) = result {
panic!(
"--db :memory: override must force both declared sqlite backends \
in-memory and boot successfully; got: {e}"
);
}
assert!(
!main_path.exists(),
"main backend's declared sqlite path must never be created on disk when \
--db :memory: overrides it; found file at {main_path:?}"
);
assert!(
!secondary_path.exists(),
"secondary backend's declared sqlite path must never be created on disk \
when --db :memory: overrides it; found file at {secondary_path:?}"
);
}
#[test]
#[serial]
fn concrete_db_override_with_backends_declared_is_rejected_via_build_server_multi_backend() {
use khive_runtime::PackConfig;
let khive_cfg = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: {
let mut m = std::collections::HashMap::new();
m.insert(
"comm".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
m
},
..KhiveConfig::default()
};
let base_cfg = RuntimeConfig {
db_path: khive_runtime::resolve_db_anchor(Some("/tmp/some-explicit-override.db")),
..base_runtime_config_for_multi_backend()
};
let result = build_server_multi_backend(
base_cfg,
&khive_cfg,
Some("/tmp/some-explicit-override.db"),
);
assert!(
result.is_err(),
"a concrete --db path override combined with declared [[backends]] must \
be rejected as ambiguous"
);
if let Err(err) = result {
let msg = err.to_string();
assert!(
msg.contains("khive.toml"),
"error message must point at khive.toml as where to make the change \
instead; got: {msg}"
);
}
}
#[test]
fn config_id_folds_backend_topology_when_non_empty() {
use khive_runtime::{BackendId, KhiveConfig, Namespace, PackConfig, RuntimeConfig};
let base_rt = RuntimeConfig {
db_path: None,
default_namespace: Namespace::parse("local").unwrap(),
embedding_model: None,
packs: vec!["kg".to_string(), "comm".to_string()],
backend_id: BackendId::main(),
..RuntimeConfig::default()
};
let id_no_backends = crate::server::compute_config_id(&base_rt, None);
let id_empty_backends =
crate::server::compute_config_id(&base_rt, Some(&KhiveConfig::default()));
assert_eq!(
id_no_backends, id_empty_backends,
"empty-backends config_id must be byte-identical to None-config config_id"
);
let mut packs_a = std::collections::HashMap::new();
packs_a.insert(
"comm".to_string(),
PackConfig {
backend: "secondary".to_string(),
},
);
let cfg_a = KhiveConfig {
backends: vec![
BackendConfig {
name: "main".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
BackendConfig {
name: "secondary".to_string(),
kind: BackendKind::Memory,
path: None,
cache_mb: None,
journal_mode: None,
read_only: false,
},
],
packs: packs_a,
..KhiveConfig::default()
};
let cfg_b = KhiveConfig {
backends: cfg_a.backends.clone(),
packs: std::collections::HashMap::new(),
..KhiveConfig::default()
};
let id_a = crate::server::compute_config_id(&base_rt, Some(&cfg_a));
let id_b = crate::server::compute_config_id(&base_rt, Some(&cfg_b));
assert_ne!(
id_a, id_b,
"configs differing only in pack→backend routing must produce different config_ids; \
both produced: {id_a}"
);
}
fn packs(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn warn_when_actor_is_none_and_comm_loaded() {
assert!(should_warn_unattributed(None, &packs(&["kg", "comm"])));
}
#[test]
fn warn_when_actor_is_local_and_comm_loaded() {
assert!(should_warn_unattributed(
Some("local"),
&packs(&["kg", "comm"])
));
}
#[test]
fn no_warn_when_actor_is_configured() {
assert!(!should_warn_unattributed(
Some("lambda:khive"),
&packs(&["kg", "comm"])
));
}
#[test]
fn no_warn_when_comm_not_loaded() {
assert!(!should_warn_unattributed(Some("local"), &packs(&["kg"])));
}
#[test]
fn no_warn_when_actor_none_and_no_comm() {
assert!(!should_warn_unattributed(None, &packs(&["kg", "memory"])));
}
#[test]
#[serial]
fn strict_mode_off_by_default() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR");
assert!(
!is_strict_actor_mode(),
"strict mode must be OFF when KHIVE_REQUIRE_ATTRIBUTED_ACTOR is unset"
);
if let Some(v) = prev {
std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v);
}
}
#[test]
#[serial]
fn strict_mode_on_when_env_var_is_1() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", "1");
assert!(
is_strict_actor_mode(),
"strict mode must be ON when KHIVE_REQUIRE_ATTRIBUTED_ACTOR=1"
);
match prev {
Some(v) => std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v),
None => std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
}
}
#[test]
#[serial]
fn strict_mode_off_when_env_var_is_not_1() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", "0");
assert!(
!is_strict_actor_mode(),
"strict mode must be OFF when KHIVE_REQUIRE_ATTRIBUTED_ACTOR=0"
);
match prev {
Some(v) => std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v),
None => std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
}
}
#[test]
#[serial]
fn enforce_strict_actor_mode_returns_err_when_strict_and_no_actor() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", "1");
let result = enforce_strict_actor_mode(None, &packs(&["kg", "comm", "memory"]));
match prev {
Some(v) => std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v),
None => std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
}
assert!(
result.is_err(),
"enforce_strict_actor_mode must return Err when strict mode is ON \
and no actor is configured (comm pack loaded)"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
"error message must name the env var; got: {msg}"
);
assert!(
msg.contains("KHIVE_ACTOR"),
"error message must name the remedy; got: {msg}"
);
}
#[test]
#[serial]
fn enforce_strict_actor_mode_ok_when_strict_and_actor_configured() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", "1");
let result = enforce_strict_actor_mode(Some("lambda:tenant-x"), &packs(&["kg", "comm"]));
match prev {
Some(v) => std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v),
None => std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
}
assert!(
result.is_ok(),
"enforce_strict_actor_mode must return Ok when actor is properly configured"
);
}
#[test]
#[serial]
fn enforce_strict_actor_mode_ok_when_strict_off_and_no_actor() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR");
let result = enforce_strict_actor_mode(None, &packs(&["kg", "comm", "memory"]));
match prev {
Some(v) => std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v),
None => std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
}
assert!(
result.is_ok(),
"enforce_strict_actor_mode must return Ok when strict mode is OFF \
(default OSS path must be completely unchanged)"
);
}
#[test]
#[serial]
fn enforce_strict_actor_mode_ok_when_strict_on_but_no_comm_pack() {
let prev = std::env::var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR").ok();
std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", "1");
let result = enforce_strict_actor_mode(None, &packs(&["kg", "memory"]));
match prev {
Some(v) => std::env::set_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR", v),
None => std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR"),
}
assert!(
result.is_ok(),
"enforce_strict_actor_mode must return Ok when comm pack is not loaded \
(no party-line risk even without actor)"
);
}
#[test]
#[serial]
fn build_server_schedule_tick_is_none_when_schedule_pack_is_not_in_the_restricted_pack_set() {
let seat_dir = tempfile::tempdir().expect("seat tempdir");
let _seat_env = SeatEnv::enter(seat_dir.path());
std::env::remove_var("KHIVE_DB");
std::env::remove_var("KHIVE_ACTOR");
std::env::remove_var("KHIVE_PACKS");
std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR");
use clap::Parser;
let args = Args::parse_from(["mcp", "--db", ":memory:", "--pack", "kg"]);
let (_server, schedule_rt) = build_server(&args).expect("build_server must succeed");
assert!(
schedule_rt.is_none(),
"when the operator restricts --pack to exclude \"schedule\", the tick must have \
nothing to drain against — never silently falling back to a runtime that can \
dispatch through a pack the daemon was not configured to load"
);
}
#[tokio::test]
#[serial]
async fn serve_with_session_sweep_completes_shutdown_on_unknown_transport() {
use clap::Parser;
let dir = tempfile::tempdir().expect("temp dir");
let db_path = dir.path().join("khive.db");
let config_path = write_config(dir.path(), "");
let args = Args::parse_from([
"kkernel",
"--db",
db_path.to_str().expect("utf8 path"),
"--config",
config_path.to_str().expect("utf8 path"),
"--transport",
"no-such-transport",
"--no-embed",
"--pack",
"kg",
]);
let (server, _schedule_rt) = build_server(&args).expect("build server");
let registry = TransportRegistry::default();
let err = tokio::time::timeout(
std::time::Duration::from_secs(10),
serve_with_session_sweep(server, &args, ®istry),
)
.await
.expect("guard must complete promptly, including sweep shutdown")
.expect_err("unknown transport must fail resolution");
assert!(
err.to_string().contains("unknown transport"),
"unexpected error: {err}"
);
let heartbeat = dir
.path()
.join("khive.db.walpin")
.join(format!("{}.json", std::process::id()));
assert!(
!heartbeat.exists(),
"sweep heartbeat must not survive the serve guard"
);
}
#[tokio::test]
#[serial]
async fn serve_guard_awaits_sweep_shutdown_before_returning() {
use clap::Parser;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let dir = tempfile::tempdir().expect("temp dir");
let db_path = dir.path().join("khive.db");
let config_path = write_config(dir.path(), "");
let args = Args::parse_from([
"kkernel",
"--db",
db_path.to_str().expect("utf8 path"),
"--config",
config_path.to_str().expect("utf8 path"),
"--transport",
"no-such-transport",
"--no-embed",
"--pack",
"kg",
]);
let (server, _schedule_rt) = build_server(&args).expect("build server");
let completed = Arc::new(AtomicBool::new(false));
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
let join = tokio::spawn({
let completed = Arc::clone(&completed);
async move {
let _ = shutdown_rx.changed().await;
completed.store(true, Ordering::SeqCst);
}
});
let handle = SessionSweepHandle { shutdown_tx, join };
let registry = TransportRegistry::default();
let err = serve_holding_sweep(Some(handle), server, &args, ®istry)
.await
.expect_err("unknown transport must fail resolution");
assert!(
err.to_string().contains("unknown transport"),
"unexpected error: {err}"
);
assert!(
completed.load(Ordering::SeqCst),
"the guard must await sweep shutdown before returning on the \
transport-resolution error path — an unawaited (dropped) handle \
leaves this flag unset at the moment the guard returns"
);
}
}