use std::sync::{Arc, RwLock};
use khive_db::StorageBackend;
use khive_gate::{ActorRef, AllowAllGate, GateRef, GateRequest};
use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
use khive_types::{EdgeEndpointRule, Namespace};
use lattice_embed::{EmbeddingModel, EmbeddingService};
use crate::error::RuntimeResult;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BackendId(pub String);
impl BackendId {
pub const MAIN: &'static str = "main";
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn main() -> Self {
Self(Self::MAIN.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for BackendId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
mod private {
#[derive(Clone, Debug)]
pub(crate) struct Sealed;
}
#[derive(Clone, Debug)]
pub struct NamespaceToken {
namespace: Namespace,
actor: ActorRef,
_sealed: private::Sealed,
}
impl NamespaceToken {
pub(crate) fn mint_authorized(namespace: Namespace, actor: ActorRef) -> Self {
Self {
namespace,
actor,
_sealed: private::Sealed,
}
}
#[allow(dead_code)]
pub(crate) fn local() -> Self {
Self::mint_authorized(Namespace::local(), ActorRef::anonymous())
}
#[allow(dead_code)]
pub(crate) fn for_namespace(ns: Namespace) -> Self {
Self::mint_authorized(ns, ActorRef::anonymous())
}
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
pub fn actor(&self) -> &ActorRef {
&self.actor
}
pub fn with_namespace(&self, ns: Namespace) -> Self {
Self::mint_authorized(ns, self.actor.clone())
}
}
#[derive(Clone, Debug)]
pub struct RuntimeConfig {
pub db_path: Option<std::path::PathBuf>,
pub default_namespace: Namespace,
pub embedding_model: Option<EmbeddingModel>,
pub additional_embedding_models: Vec<EmbeddingModel>,
pub gate: GateRef,
pub packs: Vec<String>,
pub backend_id: BackendId,
}
pub fn parse_pack_list(s: &str) -> Vec<String> {
s.split(|c: char| c == ',' || c.is_whitespace())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
impl Default for RuntimeConfig {
fn default() -> Self {
let db_path = std::env::var("HOME")
.ok()
.map(|h| std::path::PathBuf::from(h).join(".khive/khive-graph.db"));
let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
.ok()
.and_then(|s| s.parse().ok())
.or(Some(EmbeddingModel::AllMiniLmL6V2));
let additional_embedding_models = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS")
.ok()
.map(|s| parse_embedding_model_list(&s))
.unwrap_or_else(|| vec![EmbeddingModel::ParaphraseMultilingualMiniLmL12V2]);
let packs = std::env::var("KHIVE_PACKS")
.ok()
.map(|s| parse_pack_list(&s))
.filter(|v| !v.is_empty())
.unwrap_or_else(|| {
vec![
"kg",
"gtd",
"memory",
"brain",
"comm",
"schedule",
"knowledge",
]
.into_iter()
.map(String::from)
.collect()
});
Self {
db_path,
default_namespace: Namespace::local(),
embedding_model,
additional_embedding_models,
gate: Arc::new(AllowAllGate),
packs,
backend_id: BackendId::main(),
}
}
}
#[derive(Clone)]
pub struct KhiveRuntime {
backend: Arc<StorageBackend>,
config: RuntimeConfig,
embedder_registry: Arc<std::sync::RwLock<crate::embedder_registry::EmbedderRegistry>>,
default_embedder_name: Arc<str>,
edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
valid_entity_kinds: Arc<RwLock<Vec<String>>>,
valid_note_kinds: Arc<RwLock<Vec<String>>>,
}
impl KhiveRuntime {
pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
let backend = match &config.db_path {
Some(path) => {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
StorageBackend::sqlite(path)?
}
None => StorageBackend::memory()?,
};
{
let mut writer = backend.pool().try_writer()?;
khive_db::run_migrations(writer.conn_mut())?;
}
register_configured_embedding_models(&backend, &config)?;
let (registry, default_embedder_name) = build_embedder_registry(&config);
Ok(Self {
backend: Arc::new(backend),
config,
embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
default_embedder_name,
edge_rules: Arc::new(RwLock::new(Vec::new())),
valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
})
}
pub fn new_readonly(config: RuntimeConfig) -> RuntimeResult<Self> {
let backend = match &config.db_path {
Some(path) => StorageBackend::sqlite(path)?,
None => StorageBackend::memory()?,
};
{
let mut writer = backend.pool().try_writer()?;
khive_db::run_migrations(writer.conn_mut())?;
}
let (registry, default_embedder_name) = build_embedder_registry(&config);
Ok(Self {
backend: Arc::new(backend),
config,
embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
default_embedder_name,
edge_rules: Arc::new(RwLock::new(Vec::new())),
valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
})
}
pub fn from_backend(backend: Arc<StorageBackend>, config: RuntimeConfig) -> Self {
if let Err(err) = register_configured_embedding_models(&backend, &config) {
tracing::warn!(error = %err, "failed to register configured embedding models");
}
let (registry, default_embedder_name) = build_embedder_registry(&config);
Self {
backend,
config,
embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
default_embedder_name,
edge_rules: Arc::new(RwLock::new(Vec::new())),
valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn memory() -> RuntimeResult<Self> {
Self::new(RuntimeConfig {
db_path: None,
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::main(),
})
}
pub fn backend_id(&self) -> &BackendId {
&self.config.backend_id
}
pub fn config(&self) -> &RuntimeConfig {
&self.config
}
pub fn backend(&self) -> &StorageBackend {
&self.backend
}
pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
Ok(self
.backend
.entities_for_namespace(token.namespace().as_str())?)
}
pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
Ok(self
.backend
.graph_for_namespace(token.namespace().as_str())?)
}
pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
Ok(self
.backend
.notes_for_namespace(token.namespace().as_str())?)
}
pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
Ok(self
.backend
.events_for_namespace(token.namespace().as_str())?)
}
pub fn sql(&self) -> Arc<dyn SqlAccess> {
self.backend.sql()
}
pub fn vectors(
&self,
token: &NamespaceToken,
) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
let model = self.resolve_embedding_model(None)?;
self.vectors_for_embedding_model(token, model)
}
pub fn vectors_for_model(
&self,
token: &NamespaceToken,
model_name: &str,
) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
if let Some(model) = parse_embedding_model_alias(model_name) {
let key = model.to_string();
let in_registry = self
.embedder_registry
.read()
.map(|reg| reg.contains(&key))
.unwrap_or(false);
if in_registry {
return self.vectors_for_embedding_model(token, model);
}
}
let dims = {
let registry = self.embedder_registry.read().map_err(|_| {
crate::RuntimeError::Internal("embedder registry lock poisoned".into())
})?;
registry
.get_provider(model_name)
.map(|p| p.dimensions())
.ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
};
let model_key = sanitize_key(model_name);
Ok(self.backend.vectors_for_namespace(
&model_key,
model_name,
dims,
token.namespace().as_str(),
)?)
}
fn vectors_for_embedding_model(
&self,
token: &NamespaceToken,
model: EmbeddingModel,
) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
Ok(self.backend.vectors_for_namespace(
&vec_model_key(model),
&model.to_string(),
model.dimensions(),
token.namespace().as_str(),
)?)
}
pub fn text(
&self,
token: &NamespaceToken,
) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
let key = format!("entities_{}", sanitize_key(token.namespace().as_str()));
Ok(self.backend.text(&key)?)
}
pub fn text_for_notes(
&self,
token: &NamespaceToken,
) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
let key = format!("notes_{}", sanitize_key(token.namespace().as_str()));
Ok(self.backend.text(&key)?)
}
pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
let actor = ActorRef::anonymous();
let req = GateRequest::new(
actor.clone(),
ns.clone(),
"authorize",
serde_json::Value::Null,
);
match self.config.gate.check(&req) {
Ok(ref decision) if decision.is_allow() => {
if let khive_gate::GateDecision::Allow { ref obligations } = decision {
if !obligations.is_empty() {
tracing::debug!(
namespace = %ns.as_str(),
"authorize: obligations={:?}",
obligations
);
}
}
Ok(NamespaceToken::mint_authorized(ns, actor))
}
Ok(khive_gate::GateDecision::Deny { reason }) => {
Err(crate::RuntimeError::PermissionDenied {
verb: "authorize".to_string(),
reason,
})
}
Ok(_) => Err(crate::RuntimeError::PermissionDenied {
verb: "authorize".to_string(),
reason: "gate denied".to_string(),
}),
Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
}
}
pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
if let Ok(mut guard) = self.edge_rules.write() {
*guard = rules;
}
}
pub fn install_kind_registry(&self, entity_kinds: Vec<String>, note_kinds: Vec<String>) {
if let Ok(mut guard) = self.valid_entity_kinds.write() {
*guard = entity_kinds;
}
if let Ok(mut guard) = self.valid_note_kinds.write() {
*guard = note_kinds;
}
}
pub(crate) fn validate_entity_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
let guard = self.valid_entity_kinds.read().map_err(|_| {
crate::RuntimeError::Internal("entity kind registry lock poisoned".into())
})?;
if guard.is_empty() {
return Ok(());
}
if guard.iter().any(|k| k == kind) {
Ok(())
} else {
Err(crate::RuntimeError::InvalidInput(format!(
"unknown entity kind {kind:?}; valid: {}",
guard.join(", ")
)))
}
}
pub(crate) fn validate_note_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
let guard = self.valid_note_kinds.read().map_err(|_| {
crate::RuntimeError::Internal("note kind registry lock poisoned".into())
})?;
if guard.is_empty() {
return Ok(());
}
if guard.iter().any(|k| k == kind) {
Ok(())
} else {
Err(crate::RuntimeError::InvalidInput(format!(
"unknown note kind {kind:?}; valid: {}",
guard.join(", ")
)))
}
}
pub(crate) fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
self.edge_rules
.read()
.map(|g| g.clone())
.unwrap_or_default()
}
pub fn default_embedder_name(&self) -> &str {
self.default_embedder_name.as_ref()
}
pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
let model = match name {
Some(raw) => parse_embedding_model_alias(raw)
.ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
None => self
.config
.embedding_model
.ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
};
let key = model.to_string();
let contains = self
.embedder_registry
.read()
.map(|reg| reg.contains(&key))
.unwrap_or(false);
if contains {
Ok(model)
} else {
Err(crate::RuntimeError::UnknownModel(
name.unwrap_or_else(|| self.default_embedder_name())
.to_string(),
))
}
}
pub fn registered_embedding_model_names(&self) -> Vec<String> {
self.embedder_registry
.read()
.map(|reg| reg.names())
.unwrap_or_default()
}
pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
let canonical_key = match parse_embedding_model_alias(name) {
Some(model) => model.to_string(),
None => name.to_owned(),
};
let entry = {
let registry = self.embedder_registry.read().map_err(|_| {
crate::RuntimeError::Internal("embedder registry lock poisoned".into())
})?;
registry
.get_entry(&canonical_key)
.ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
};
entry.resolve().await
}
pub fn register_embedder(
&self,
provider: impl crate::embedder_registry::EmbedderProvider + 'static,
) {
if let Ok(mut registry) = self.embedder_registry.write() {
registry.register(provider);
} else {
tracing::warn!(
"embedder registry lock poisoned — embedder {} not registered",
std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
);
}
}
pub async fn list_embedding_models(
&self,
engine_filter: Option<&str>,
) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
use khive_storage::{SqlStatement, SqlValue};
let (sql_text, params) = if let Some(engine) = engine_filter {
(
"SELECT engine_name, model_id, key_version, dim, status, \
activated_at, superseded_at \
FROM _embedding_models WHERE engine_name = ?1 \
ORDER BY engine_name, activated_at IS NULL, activated_at"
.to_string(),
vec![SqlValue::Text(engine.to_string())],
)
} else {
(
"SELECT engine_name, model_id, key_version, dim, status, \
activated_at, superseded_at \
FROM _embedding_models \
ORDER BY engine_name, activated_at IS NULL, activated_at"
.to_string(),
vec![],
)
};
let stmt = SqlStatement {
sql: sql_text,
params,
label: Some("list_embedding_models".into()),
};
let mut reader = self
.sql()
.reader()
.await
.map_err(crate::RuntimeError::Storage)?;
let rows = match reader.query_all(stmt).await {
Ok(rows) => rows,
Err(e) if e.to_string().contains("no such table: _embedding_models") => {
return Ok(Vec::new())
}
Err(e) => return Err(crate::RuntimeError::Storage(e)),
};
let mut records = Vec::with_capacity(rows.len());
for row in rows {
macro_rules! required_text {
($col:expr) => {
match row.get($col) {
Some(SqlValue::Text(s)) => s.clone(),
other => {
tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
continue;
}
}
};
}
let engine_name = required_text!("engine_name");
let model_id = required_text!("model_id");
let key_version = required_text!("key_version");
let dimensions = match row.get("dim") {
Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
Ok(d) => d,
Err(_) => {
tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
continue;
}
},
other => {
tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
continue;
}
};
let status = required_text!("status");
let activated_at = match row.get("activated_at") {
Some(SqlValue::Integer(n)) => Some(*n),
_ => None,
};
let superseded_at = match row.get("superseded_at") {
Some(SqlValue::Integer(n)) => Some(*n),
_ => None,
};
records.push(khive_db::EmbeddingModelRegistryRecord {
engine_name,
model_id,
key_version,
dimensions,
status,
activated_at,
superseded_at,
});
}
Ok(records)
}
}
pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
sanitize_key(&model.to_string())
}
pub(crate) fn sanitize_key(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
fn build_embedder_registry(
config: &RuntimeConfig,
) -> (crate::embedder_registry::EmbedderRegistry, Arc<str>) {
use crate::embedder_registry::{EmbedderRegistry, LatticeEmbedderProvider};
let mut registry = EmbedderRegistry::new();
for model in configured_embedding_models(config) {
registry.register(LatticeEmbedderProvider::new(model));
}
let default_embedder_name = config
.embedding_model
.map(|model| Arc::<str>::from(model.to_string()))
.unwrap_or_else(|| Arc::<str>::from(""));
(registry, default_embedder_name)
}
fn configured_embedding_models(config: &RuntimeConfig) -> Vec<EmbeddingModel> {
let mut models = Vec::new();
if let Some(model) = config.embedding_model {
models.push(model);
}
models.extend(config.additional_embedding_models.iter().copied());
models.sort_by_key(|model| model.to_string());
models.dedup();
models
}
fn register_configured_embedding_models(
backend: &StorageBackend,
config: &RuntimeConfig,
) -> RuntimeResult<()> {
for model in configured_embedding_models(config) {
backend.register_embedding_model(
&model.to_string(),
model.model_id(),
model.key_version(),
model.dimensions() as u32,
)?;
}
Ok(())
}
pub fn runtime_config_from_khive_config(
khive_cfg: &crate::engine_config::KhiveConfig,
base: RuntimeConfig,
) -> RuntimeConfig {
let default_namespace = match khive_cfg.actor.id.as_deref() {
Some(id) if !id.is_empty() => match Namespace::parse(id) {
Ok(ns) => {
tracing::debug!(actor_id = id, "actor.id from config sets default_namespace");
ns
}
Err(e) => {
panic!(
"actor.id {id:?} passed validation but Namespace::parse failed: {e}; \
this is a bug — KhiveConfig must be validated before calling \
runtime_config_from_khive_config"
);
}
},
_ => base.default_namespace.clone(),
};
if khive_cfg.engines.is_empty() {
return RuntimeConfig {
default_namespace,
..base
};
}
let mut embedding_model: Option<EmbeddingModel> = None;
let mut additional: Vec<EmbeddingModel> = Vec::new();
for engine in &khive_cfg.engines {
match parse_embedding_model_alias(&engine.model) {
Some(model) => {
if engine.default {
embedding_model = Some(model);
} else {
additional.push(model);
}
}
None => {
tracing::warn!(
engine = %engine.name,
model = %engine.model,
"engine config: unknown model name; engine will be skipped"
);
}
}
}
RuntimeConfig {
embedding_model,
additional_embedding_models: additional,
default_namespace,
..base
}
}
fn parse_embedding_model_list(s: &str) -> Vec<EmbeddingModel> {
parse_pack_list(s)
.into_iter()
.filter_map(|raw| {
let parsed = parse_embedding_model_alias(&raw);
if parsed.is_none() && !raw.trim().is_empty() {
tracing::warn!(
model = %raw,
"KHIVE_ADDITIONAL_EMBEDDING_MODELS contains unknown model name; ignored. \
Valid forms: short alias like 'paraphrase' or a fully-qualified key \
from lattice_embed::EmbeddingModel::from_str."
);
}
parsed
})
.collect()
}
pub(crate) fn parse_embedding_model_alias(name: &str) -> Option<EmbeddingModel> {
let normalized = name.trim().to_ascii_lowercase().replace('_', "-");
match normalized.as_str() {
"paraphrase" => Some(EmbeddingModel::ParaphraseMultilingualMiniLmL12V2),
_ => normalized.parse().ok(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_runtime_creates_successfully() {
let rt = KhiveRuntime::memory().expect("memory runtime should create");
assert!(rt.config().db_path.is_none());
}
#[test]
fn file_runtime_creates_successfully() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.db");
let config = RuntimeConfig {
db_path: Some(path.clone()),
default_namespace: Namespace::parse("test").unwrap(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::main(),
};
let rt = KhiveRuntime::new(config).expect("file runtime should create");
assert!(path.exists());
assert_eq!(rt.config().default_namespace.as_str(), "test");
}
#[test]
fn from_backend_uses_provided_backend() {
let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
let config = RuntimeConfig {
db_path: None,
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::new("lore"),
};
let rt = KhiveRuntime::from_backend(backend, config);
assert_eq!(rt.backend_id().as_str(), "lore");
assert!(rt.config().db_path.is_none());
}
#[test]
fn backend_id_defaults_to_main() {
let rt = KhiveRuntime::memory().unwrap();
assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
}
#[test]
fn store_accessors_return_ok() {
let rt = KhiveRuntime::memory().unwrap();
let tok = NamespaceToken::local();
assert!(rt.entities(&tok).is_ok());
assert!(rt.graph(&tok).is_ok());
assert!(rt.notes(&tok).is_ok());
assert!(rt.events(&tok).is_ok());
}
#[test]
fn vectors_returns_unconfigured_without_model() {
let rt = KhiveRuntime::memory().unwrap();
let tok = NamespaceToken::local();
match rt.vectors(&tok) {
Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
Err(other) => panic!("expected Unconfigured, got {:?}", other),
Ok(_) => panic!("expected Err, got Ok"),
}
}
#[test]
fn vec_model_key_sanitizes_dots_and_dashes() {
assert_eq!(
vec_model_key(EmbeddingModel::BgeSmallEnV15),
"bge_small_en_v1_5"
);
assert_eq!(
vec_model_key(EmbeddingModel::BgeBaseEnV15),
"bge_base_en_v1_5"
);
assert_eq!(
vec_model_key(EmbeddingModel::AllMiniLmL6V2),
"all_minilm_l6_v2"
);
}
#[test]
fn default_config_uses_allow_all_gate() {
let cfg = RuntimeConfig::default();
assert_eq!(cfg.default_namespace.as_str(), "local");
let _: GateRef = cfg.gate.clone();
}
#[test]
fn parse_pack_list_handles_comma_and_whitespace() {
assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
assert_eq!(
parse_pack_list("kg,gtd"),
vec!["kg".to_string(), "gtd".to_string()]
);
assert_eq!(
parse_pack_list(" kg , gtd "),
vec!["kg".to_string(), "gtd".to_string()]
);
assert_eq!(
parse_pack_list("kg gtd"),
vec!["kg".to_string(), "gtd".to_string()]
);
assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
assert_eq!(parse_pack_list(""), Vec::<String>::new());
}
#[test]
fn default_config_packs_loads_all_production_packs() {
let prior = std::env::var("KHIVE_PACKS").ok();
unsafe {
std::env::remove_var("KHIVE_PACKS");
}
let cfg = RuntimeConfig::default();
assert!(cfg.packs.contains(&"kg".to_string()));
assert!(cfg.packs.contains(&"gtd".to_string()));
assert!(cfg.packs.contains(&"memory".to_string()));
assert!(cfg.packs.contains(&"brain".to_string()));
assert!(cfg.packs.contains(&"comm".to_string()));
assert!(cfg.packs.contains(&"schedule".to_string()));
assert!(cfg.packs.contains(&"knowledge".to_string()));
assert_eq!(cfg.packs.len(), 7);
if let Some(v) = prior {
unsafe {
std::env::set_var("KHIVE_PACKS", v);
}
}
}
#[test]
fn default_config_uses_minilm_when_env_unset() {
let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
unsafe {
std::env::remove_var("KHIVE_EMBEDDING_MODEL");
}
let cfg = RuntimeConfig::default();
assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
if let Some(v) = prior {
unsafe {
std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
}
}
}
use crate::engine_config::{ActorConfig, KhiveConfig};
fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
KhiveConfig {
engines: vec![],
actor: ActorConfig {
id: Some(id.to_string()),
display_name: None,
},
}
}
#[test]
fn runtime_config_from_khive_config_applies_actor_id_as_default_namespace() {
let base = RuntimeConfig {
db_path: None,
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::main(),
};
let cfg = khive_cfg_with_actor("lambda:khive");
let result = runtime_config_from_khive_config(&cfg, base);
assert_eq!(result.default_namespace.as_str(), "lambda:khive");
}
#[test]
fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
let base = RuntimeConfig {
db_path: None,
default_namespace: Namespace::parse("lambda:base").unwrap(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::main(),
};
let cfg = KhiveConfig {
engines: vec![],
actor: ActorConfig {
id: Some(String::new()),
display_name: None,
},
};
let result = runtime_config_from_khive_config(&cfg, base);
assert_eq!(
result.default_namespace.as_str(),
"lambda:base",
"empty actor.id must not override base namespace"
);
}
#[test]
fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
let base = RuntimeConfig {
db_path: None,
default_namespace: Namespace::parse("lambda:base").unwrap(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::main(),
};
let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
assert_eq!(
result.default_namespace.as_str(),
"lambda:base",
"absent actor.id must not override base namespace"
);
}
#[test]
fn runtime_config_from_khive_config_actor_id_with_engines() {
let base = RuntimeConfig {
db_path: None,
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: BackendId::main(),
};
let cfg = KhiveConfig {
engines: vec![crate::engine_config::EngineConfig {
name: "default".to_string(),
model: "all-minilm-l6-v2".to_string(),
default: true,
fusion_weight: None,
dims: None,
}],
actor: ActorConfig {
id: Some("lambda:test".to_string()),
display_name: None,
},
};
let result = runtime_config_from_khive_config(&cfg, base);
assert_eq!(result.default_namespace.as_str(), "lambda:test");
assert!(result.embedding_model.is_some());
}
#[tokio::test]
async fn list_embedding_models_returns_empty_when_table_absent() {
let rt = KhiveRuntime::memory().expect("memory runtime");
let records = rt
.list_embedding_models(None)
.await
.expect("list ok on empty table");
assert!(records.is_empty());
}
#[tokio::test]
async fn list_embedding_models_returns_row_after_insert() {
use khive_storage::{SqlStatement, SqlValue};
let rt = KhiveRuntime::memory().expect("memory runtime");
let sql = rt.sql();
let now = 1_000_000i64;
let id = uuid::Uuid::new_v4();
let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
let mut writer = sql.writer().await.expect("writer");
writer
.execute(SqlStatement {
sql: "INSERT INTO _embedding_models \
(id, engine_name, model_id, key_version, dim, output_dim, status, \
activated_at, superseded_at, superseded_by, canonical_key, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
.into(),
params: vec![
SqlValue::Blob(id.as_bytes().to_vec()),
SqlValue::Text("test_engine".into()),
SqlValue::Text("test-model-v1".into()),
SqlValue::Text("v1".into()),
SqlValue::Integer(384),
SqlValue::Text("active".into()),
SqlValue::Integer(now),
SqlValue::Blob(canonical_key),
SqlValue::Integer(now),
],
label: None,
})
.await
.expect("insert row");
drop(writer);
let records = rt.list_embedding_models(None).await.expect("list ok");
assert_eq!(records.len(), 1);
assert_eq!(records[0].engine_name, "test_engine");
assert_eq!(records[0].model_id, "test-model-v1");
assert_eq!(records[0].key_version, "v1");
assert_eq!(records[0].dimensions, 384);
assert_eq!(records[0].status, "active");
let filtered = rt
.list_embedding_models(Some("test_engine"))
.await
.expect("filter ok");
assert_eq!(filtered.len(), 1);
let no_match = rt
.list_embedding_models(Some("other_engine"))
.await
.expect("no-match ok");
assert!(no_match.is_empty());
}
}