1use std::sync::{Arc, RwLock};
4
5use khive_db::StorageBackend;
6use khive_gate::{ActorRef, AllowAllGate, GateRef, GateRequest};
7use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
8use khive_types::{EdgeEndpointRule, Namespace};
9use lattice_embed::{EmbeddingModel, EmbeddingService};
10
11use crate::error::RuntimeResult;
12
13#[derive(Clone, Debug, PartialEq, Eq, Hash)]
26pub struct BackendId(pub String);
27
28impl BackendId {
29 pub const MAIN: &'static str = "main";
31
32 pub fn new(name: impl Into<String>) -> Self {
34 Self(name.into())
35 }
36
37 pub fn main() -> Self {
39 Self(Self::MAIN.to_string())
40 }
41
42 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48impl std::fmt::Display for BackendId {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(&self.0)
51 }
52}
53
54mod private {
57 #[derive(Clone, Debug)]
58 pub(crate) struct Sealed;
59}
60
61#[derive(Clone, Debug)]
67pub struct NamespaceToken {
68 namespace: Namespace,
69 actor: ActorRef,
70 _sealed: private::Sealed,
71}
72
73impl NamespaceToken {
74 pub(crate) fn mint_authorized(namespace: Namespace, actor: ActorRef) -> Self {
76 Self {
77 namespace,
78 actor,
79 _sealed: private::Sealed,
80 }
81 }
82
83 #[allow(dead_code)]
89 pub(crate) fn local() -> Self {
90 Self::mint_authorized(Namespace::local(), ActorRef::anonymous())
91 }
92
93 #[allow(dead_code)]
99 pub(crate) fn for_namespace(ns: Namespace) -> Self {
100 Self::mint_authorized(ns, ActorRef::anonymous())
101 }
102
103 pub fn namespace(&self) -> &Namespace {
105 &self.namespace
106 }
107
108 pub fn actor(&self) -> &ActorRef {
110 &self.actor
111 }
112
113 pub fn with_namespace(&self, ns: Namespace) -> Self {
119 Self::mint_authorized(ns, self.actor.clone())
120 }
121}
122
123#[derive(Clone, Debug)]
131pub struct RuntimeConfig {
132 pub db_path: Option<std::path::PathBuf>,
138 pub default_namespace: Namespace,
140 pub embedding_model: Option<EmbeddingModel>,
147 pub additional_embedding_models: Vec<EmbeddingModel>,
154 pub gate: GateRef,
158 pub packs: Vec<String>,
164 pub backend_id: BackendId,
169}
170
171pub fn parse_pack_list(s: &str) -> Vec<String> {
175 s.split(|c: char| c == ',' || c.is_whitespace())
176 .map(str::trim)
177 .filter(|s| !s.is_empty())
178 .map(str::to_owned)
179 .collect()
180}
181
182impl Default for RuntimeConfig {
183 fn default() -> Self {
184 let db_path = std::env::var("HOME")
185 .ok()
186 .map(|h| std::path::PathBuf::from(h).join(".khive/khive-graph.db"));
187 let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
188 .ok()
189 .and_then(|s| s.parse().ok())
190 .or(Some(EmbeddingModel::AllMiniLmL6V2));
191 let additional_embedding_models = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS")
192 .ok()
193 .map(|s| parse_embedding_model_list(&s))
194 .unwrap_or_else(|| vec![EmbeddingModel::ParaphraseMultilingualMiniLmL12V2]);
195 let packs = std::env::var("KHIVE_PACKS")
196 .ok()
197 .map(|s| parse_pack_list(&s))
198 .filter(|v| !v.is_empty())
199 .unwrap_or_else(|| {
200 vec![
201 "kg",
202 "gtd",
203 "memory",
204 "brain",
205 "comm",
206 "schedule",
207 "knowledge",
208 ]
209 .into_iter()
210 .map(String::from)
211 .collect()
212 });
213 Self {
214 db_path,
215 default_namespace: Namespace::local(),
216 embedding_model,
217 additional_embedding_models,
218 gate: Arc::new(AllowAllGate),
219 packs,
220 backend_id: BackendId::main(),
221 }
222 }
223}
224
225#[derive(Clone)]
232pub struct KhiveRuntime {
233 backend: Arc<StorageBackend>,
234 config: RuntimeConfig,
235 embedder_registry: Arc<std::sync::RwLock<crate::embedder_registry::EmbedderRegistry>>,
242 default_embedder_name: Arc<str>,
243 edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
247 valid_entity_kinds: Arc<RwLock<Vec<String>>>,
255 valid_note_kinds: Arc<RwLock<Vec<String>>>,
256}
257
258impl KhiveRuntime {
259 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
265 let backend = match &config.db_path {
266 Some(path) => {
267 if let Some(parent) = path.parent() {
268 std::fs::create_dir_all(parent).ok();
269 }
270 StorageBackend::sqlite(path)?
271 }
272 None => StorageBackend::memory()?,
273 };
274 {
281 let mut writer = backend.pool().try_writer()?;
282 khive_db::run_migrations(writer.conn_mut())?;
283 }
284 register_configured_embedding_models(&backend, &config)?;
285 let (registry, default_embedder_name) = build_embedder_registry(&config);
286 Ok(Self {
287 backend: Arc::new(backend),
288 config,
289 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
290 default_embedder_name,
291 edge_rules: Arc::new(RwLock::new(Vec::new())),
292 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
293 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
294 })
295 }
296
297 pub fn new_readonly(config: RuntimeConfig) -> RuntimeResult<Self> {
303 let backend = match &config.db_path {
304 Some(path) => StorageBackend::sqlite(path)?,
305 None => StorageBackend::memory()?,
306 };
307 {
308 let mut writer = backend.pool().try_writer()?;
309 khive_db::run_migrations(writer.conn_mut())?;
310 }
311 let (registry, default_embedder_name) = build_embedder_registry(&config);
312 Ok(Self {
313 backend: Arc::new(backend),
314 config,
315 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
316 default_embedder_name,
317 edge_rules: Arc::new(RwLock::new(Vec::new())),
318 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
319 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
320 })
321 }
322
323 pub fn from_backend(backend: Arc<StorageBackend>, config: RuntimeConfig) -> Self {
333 if let Err(err) = register_configured_embedding_models(&backend, &config) {
334 tracing::warn!(error = %err, "failed to register configured embedding models");
335 }
336 let (registry, default_embedder_name) = build_embedder_registry(&config);
337 Self {
338 backend,
339 config,
340 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
341 default_embedder_name,
342 edge_rules: Arc::new(RwLock::new(Vec::new())),
343 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
344 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
345 }
346 }
347
348 pub fn memory() -> RuntimeResult<Self> {
350 Self::new(RuntimeConfig {
351 db_path: None,
352 default_namespace: Namespace::local(),
353 embedding_model: None,
354 additional_embedding_models: vec![],
355 gate: Arc::new(AllowAllGate),
356 packs: vec!["kg".to_string()],
357 backend_id: BackendId::main(),
358 })
359 }
360
361 pub fn backend_id(&self) -> &BackendId {
366 &self.config.backend_id
367 }
368
369 pub fn config(&self) -> &RuntimeConfig {
371 &self.config
372 }
373
374 pub fn backend(&self) -> &StorageBackend {
376 &self.backend
377 }
378
379 pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
383 Ok(self
384 .backend
385 .entities_for_namespace(token.namespace().as_str())?)
386 }
387
388 pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
390 Ok(self
391 .backend
392 .graph_for_namespace(token.namespace().as_str())?)
393 }
394
395 pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
397 Ok(self
398 .backend
399 .notes_for_namespace(token.namespace().as_str())?)
400 }
401
402 pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
404 Ok(self
405 .backend
406 .events_for_namespace(token.namespace().as_str())?)
407 }
408
409 pub fn sql(&self) -> Arc<dyn SqlAccess> {
411 self.backend.sql()
412 }
413
414 pub fn vectors(
418 &self,
419 token: &NamespaceToken,
420 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
421 let model = self.resolve_embedding_model(None)?;
422 self.vectors_for_embedding_model(token, model)
423 }
424
425 pub fn vectors_for_model(
433 &self,
434 token: &NamespaceToken,
435 model_name: &str,
436 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
437 if let Some(model) = parse_embedding_model_alias(model_name) {
439 let key = model.to_string();
442 let in_registry = self
443 .embedder_registry
444 .read()
445 .map(|reg| reg.contains(&key))
446 .unwrap_or(false);
447 if in_registry {
448 return self.vectors_for_embedding_model(token, model);
449 }
450 }
451 let dims = {
454 let registry = self.embedder_registry.read().map_err(|_| {
455 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
456 })?;
457 registry
458 .get_provider(model_name)
459 .map(|p| p.dimensions())
460 .ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
461 };
462 let model_key = sanitize_key(model_name);
463 Ok(self.backend.vectors_for_namespace(
464 &model_key,
465 model_name,
466 dims,
467 token.namespace().as_str(),
468 )?)
469 }
470
471 fn vectors_for_embedding_model(
472 &self,
473 token: &NamespaceToken,
474 model: EmbeddingModel,
475 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
476 Ok(self.backend.vectors_for_namespace(
477 &vec_model_key(model),
478 &model.to_string(),
479 model.dimensions(),
480 token.namespace().as_str(),
481 )?)
482 }
483
484 pub fn text(
486 &self,
487 token: &NamespaceToken,
488 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
489 let key = format!("entities_{}", sanitize_key(token.namespace().as_str()));
490 Ok(self.backend.text(&key)?)
491 }
492
493 pub fn text_for_notes(
495 &self,
496 token: &NamespaceToken,
497 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
498 let key = format!("notes_{}", sanitize_key(token.namespace().as_str()));
499 Ok(self.backend.text(&key)?)
500 }
501
502 pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
509 let actor = ActorRef::anonymous();
510 let req = GateRequest::new(
511 actor.clone(),
512 ns.clone(),
513 "authorize",
514 serde_json::Value::Null,
515 );
516 match self.config.gate.check(&req) {
517 Ok(ref decision) if decision.is_allow() => {
518 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
519 if !obligations.is_empty() {
520 tracing::debug!(
521 namespace = %ns.as_str(),
522 "authorize: obligations={:?}",
523 obligations
524 );
525 }
526 }
527 Ok(NamespaceToken::mint_authorized(ns, actor))
528 }
529 Ok(khive_gate::GateDecision::Deny { reason }) => {
530 Err(crate::RuntimeError::PermissionDenied {
531 verb: "authorize".to_string(),
532 reason,
533 })
534 }
535 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
536 verb: "authorize".to_string(),
537 reason: "gate denied".to_string(),
538 }),
539 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
540 }
541 }
542
543 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
549 if let Ok(mut guard) = self.edge_rules.write() {
550 *guard = rules;
551 }
552 }
553
554 pub fn install_kind_registry(&self, entity_kinds: Vec<String>, note_kinds: Vec<String>) {
564 if let Ok(mut guard) = self.valid_entity_kinds.write() {
565 *guard = entity_kinds;
566 }
567 if let Ok(mut guard) = self.valid_note_kinds.write() {
568 *guard = note_kinds;
569 }
570 }
571
572 pub(crate) fn validate_entity_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
577 let guard = self.valid_entity_kinds.read().map_err(|_| {
578 crate::RuntimeError::Internal("entity kind registry lock poisoned".into())
579 })?;
580 if guard.is_empty() {
581 return Ok(());
582 }
583 if guard.iter().any(|k| k == kind) {
584 Ok(())
585 } else {
586 Err(crate::RuntimeError::InvalidInput(format!(
587 "unknown entity kind {kind:?}; valid: {}",
588 guard.join(", ")
589 )))
590 }
591 }
592
593 pub(crate) fn validate_note_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
598 let guard = self.valid_note_kinds.read().map_err(|_| {
599 crate::RuntimeError::Internal("note kind registry lock poisoned".into())
600 })?;
601 if guard.is_empty() {
602 return Ok(());
603 }
604 if guard.iter().any(|k| k == kind) {
605 Ok(())
606 } else {
607 Err(crate::RuntimeError::InvalidInput(format!(
608 "unknown note kind {kind:?}; valid: {}",
609 guard.join(", ")
610 )))
611 }
612 }
613
614 pub(crate) fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
616 self.edge_rules
617 .read()
618 .map(|g| g.clone())
619 .unwrap_or_default()
620 }
621
622 pub fn default_embedder_name(&self) -> &str {
624 self.default_embedder_name.as_ref()
625 }
626
627 pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
632 let model = match name {
633 Some(raw) => parse_embedding_model_alias(raw)
634 .ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
635 None => self
636 .config
637 .embedding_model
638 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
639 };
640 let key = model.to_string();
641 let contains = self
642 .embedder_registry
643 .read()
644 .map(|reg| reg.contains(&key))
645 .unwrap_or(false);
646 if contains {
647 Ok(model)
648 } else {
649 Err(crate::RuntimeError::UnknownModel(
650 name.unwrap_or_else(|| self.default_embedder_name())
651 .to_string(),
652 ))
653 }
654 }
655
656 pub fn registered_embedding_model_names(&self) -> Vec<String> {
664 self.embedder_registry
665 .read()
666 .map(|reg| reg.names())
667 .unwrap_or_default()
668 }
669
670 pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
683 let canonical_key = match parse_embedding_model_alias(name) {
688 Some(model) => model.to_string(),
689 None => name.to_owned(),
690 };
691 let entry = {
694 let registry = self.embedder_registry.read().map_err(|_| {
695 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
696 })?;
697 registry
698 .get_entry(&canonical_key)
699 .ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
700 };
701 entry.resolve().await
702 }
703
704 pub fn register_embedder(
717 &self,
718 provider: impl crate::embedder_registry::EmbedderProvider + 'static,
719 ) {
720 if let Ok(mut registry) = self.embedder_registry.write() {
721 registry.register(provider);
722 } else {
723 tracing::warn!(
724 "embedder registry lock poisoned — embedder {} not registered",
725 std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
726 );
727 }
728 }
729
730 pub async fn list_embedding_models(
737 &self,
738 engine_filter: Option<&str>,
739 ) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
740 use khive_storage::{SqlStatement, SqlValue};
741
742 let (sql_text, params) = if let Some(engine) = engine_filter {
743 (
744 "SELECT engine_name, model_id, key_version, dim, status, \
745 activated_at, superseded_at \
746 FROM _embedding_models WHERE engine_name = ?1 \
747 ORDER BY engine_name, activated_at IS NULL, activated_at"
748 .to_string(),
749 vec![SqlValue::Text(engine.to_string())],
750 )
751 } else {
752 (
753 "SELECT engine_name, model_id, key_version, dim, status, \
754 activated_at, superseded_at \
755 FROM _embedding_models \
756 ORDER BY engine_name, activated_at IS NULL, activated_at"
757 .to_string(),
758 vec![],
759 )
760 };
761
762 let stmt = SqlStatement {
763 sql: sql_text,
764 params,
765 label: Some("list_embedding_models".into()),
766 };
767
768 let mut reader = self
769 .sql()
770 .reader()
771 .await
772 .map_err(crate::RuntimeError::Storage)?;
773
774 let rows = match reader.query_all(stmt).await {
775 Ok(rows) => rows,
776 Err(e) if e.to_string().contains("no such table: _embedding_models") => {
777 return Ok(Vec::new())
778 }
779 Err(e) => return Err(crate::RuntimeError::Storage(e)),
780 };
781
782 let mut records = Vec::with_capacity(rows.len());
783 for row in rows {
784 macro_rules! required_text {
785 ($col:expr) => {
786 match row.get($col) {
787 Some(SqlValue::Text(s)) => s.clone(),
788 other => {
789 tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
790 continue;
791 }
792 }
793 };
794 }
795 let engine_name = required_text!("engine_name");
796 let model_id = required_text!("model_id");
797 let key_version = required_text!("key_version");
798 let dimensions = match row.get("dim") {
799 Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
800 Ok(d) => d,
801 Err(_) => {
802 tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
803 continue;
804 }
805 },
806 other => {
807 tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
808 continue;
809 }
810 };
811 let status = required_text!("status");
812 let activated_at = match row.get("activated_at") {
813 Some(SqlValue::Integer(n)) => Some(*n),
814 _ => None,
815 };
816 let superseded_at = match row.get("superseded_at") {
817 Some(SqlValue::Integer(n)) => Some(*n),
818 _ => None,
819 };
820 records.push(khive_db::EmbeddingModelRegistryRecord {
821 engine_name,
822 model_id,
823 key_version,
824 dimensions,
825 status,
826 activated_at,
827 superseded_at,
828 });
829 }
830
831 Ok(records)
832 }
833}
834
835pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
838 sanitize_key(&model.to_string())
839}
840
841pub(crate) fn sanitize_key(s: &str) -> String {
842 s.chars()
843 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
844 .collect()
845}
846
847fn build_embedder_registry(
848 config: &RuntimeConfig,
849) -> (crate::embedder_registry::EmbedderRegistry, Arc<str>) {
850 use crate::embedder_registry::{EmbedderRegistry, LatticeEmbedderProvider};
851 let mut registry = EmbedderRegistry::new();
852 for model in configured_embedding_models(config) {
853 registry.register(LatticeEmbedderProvider::new(model));
854 }
855 let default_embedder_name = config
856 .embedding_model
857 .map(|model| Arc::<str>::from(model.to_string()))
858 .unwrap_or_else(|| Arc::<str>::from(""));
859 (registry, default_embedder_name)
860}
861
862fn configured_embedding_models(config: &RuntimeConfig) -> Vec<EmbeddingModel> {
863 let mut models = Vec::new();
864 if let Some(model) = config.embedding_model {
865 models.push(model);
866 }
867 models.extend(config.additional_embedding_models.iter().copied());
868 models.sort_by_key(|model| model.to_string());
869 models.dedup();
870 models
871}
872
873fn register_configured_embedding_models(
874 backend: &StorageBackend,
875 config: &RuntimeConfig,
876) -> RuntimeResult<()> {
877 for model in configured_embedding_models(config) {
878 backend.register_embedding_model(
879 &model.to_string(),
880 model.model_id(),
881 model.key_version(),
882 model.dimensions() as u32,
883 )?;
884 }
885 Ok(())
886}
887
888pub fn runtime_config_from_khive_config(
904 khive_cfg: &crate::engine_config::KhiveConfig,
905 base: RuntimeConfig,
906) -> RuntimeConfig {
907 let default_namespace = match khive_cfg.actor.id.as_deref() {
913 Some(id) if !id.is_empty() => match Namespace::parse(id) {
914 Ok(ns) => {
915 tracing::debug!(actor_id = id, "actor.id from config sets default_namespace");
916 ns
917 }
918 Err(e) => {
919 panic!(
920 "actor.id {id:?} passed validation but Namespace::parse failed: {e}; \
921 this is a bug — KhiveConfig must be validated before calling \
922 runtime_config_from_khive_config"
923 );
924 }
925 },
926 _ => base.default_namespace.clone(),
927 };
928
929 if khive_cfg.engines.is_empty() {
930 return RuntimeConfig {
931 default_namespace,
932 ..base
933 };
934 }
935
936 let mut embedding_model: Option<EmbeddingModel> = None;
937 let mut additional: Vec<EmbeddingModel> = Vec::new();
938
939 for engine in &khive_cfg.engines {
940 match parse_embedding_model_alias(&engine.model) {
941 Some(model) => {
942 if engine.default {
943 embedding_model = Some(model);
944 } else {
945 additional.push(model);
946 }
947 }
948 None => {
949 tracing::warn!(
950 engine = %engine.name,
951 model = %engine.model,
952 "engine config: unknown model name; engine will be skipped"
953 );
954 }
955 }
956 }
957
958 RuntimeConfig {
959 embedding_model,
960 additional_embedding_models: additional,
961 default_namespace,
962 ..base
963 }
964}
965
966fn parse_embedding_model_list(s: &str) -> Vec<EmbeddingModel> {
968 parse_pack_list(s)
969 .into_iter()
970 .filter_map(|raw| {
971 let parsed = parse_embedding_model_alias(&raw);
972 if parsed.is_none() && !raw.trim().is_empty() {
973 tracing::warn!(
978 model = %raw,
979 "KHIVE_ADDITIONAL_EMBEDDING_MODELS contains unknown model name; ignored. \
980 Valid forms: short alias like 'paraphrase' or a fully-qualified key \
981 from lattice_embed::EmbeddingModel::from_str."
982 );
983 }
984 parsed
985 })
986 .collect()
987}
988
989pub(crate) fn parse_embedding_model_alias(name: &str) -> Option<EmbeddingModel> {
990 let normalized = name.trim().to_ascii_lowercase().replace('_', "-");
991 match normalized.as_str() {
992 "paraphrase" => Some(EmbeddingModel::ParaphraseMultilingualMiniLmL12V2),
993 _ => normalized.parse().ok(),
994 }
995}
996
997#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 #[test]
1005 fn memory_runtime_creates_successfully() {
1006 let rt = KhiveRuntime::memory().expect("memory runtime should create");
1007 assert!(rt.config().db_path.is_none());
1008 }
1009
1010 #[test]
1011 fn file_runtime_creates_successfully() {
1012 let dir = tempfile::tempdir().unwrap();
1013 let path = dir.path().join("test.db");
1014 let config = RuntimeConfig {
1015 db_path: Some(path.clone()),
1016 default_namespace: Namespace::parse("test").unwrap(),
1017 embedding_model: None,
1018 additional_embedding_models: vec![],
1019 gate: Arc::new(AllowAllGate),
1020 packs: vec!["kg".to_string()],
1021 backend_id: BackendId::main(),
1022 };
1023 let rt = KhiveRuntime::new(config).expect("file runtime should create");
1024 assert!(path.exists());
1025 assert_eq!(rt.config().default_namespace.as_str(), "test");
1026 }
1027
1028 #[test]
1029 fn from_backend_uses_provided_backend() {
1030 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
1031 let config = RuntimeConfig {
1032 db_path: None,
1033 default_namespace: Namespace::local(),
1034 embedding_model: None,
1035 additional_embedding_models: vec![],
1036 gate: Arc::new(AllowAllGate),
1037 packs: vec!["kg".to_string()],
1038 backend_id: BackendId::new("lore"),
1039 };
1040 let rt = KhiveRuntime::from_backend(backend, config);
1041 assert_eq!(rt.backend_id().as_str(), "lore");
1042 assert!(rt.config().db_path.is_none());
1043 }
1044
1045 #[test]
1046 fn backend_id_defaults_to_main() {
1047 let rt = KhiveRuntime::memory().unwrap();
1048 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
1049 }
1050
1051 #[test]
1052 fn store_accessors_return_ok() {
1053 let rt = KhiveRuntime::memory().unwrap();
1054 let tok = NamespaceToken::local();
1055 assert!(rt.entities(&tok).is_ok());
1056 assert!(rt.graph(&tok).is_ok());
1057 assert!(rt.notes(&tok).is_ok());
1058 assert!(rt.events(&tok).is_ok());
1059 }
1060
1061 #[test]
1062 fn vectors_returns_unconfigured_without_model() {
1063 let rt = KhiveRuntime::memory().unwrap();
1064 let tok = NamespaceToken::local();
1065 match rt.vectors(&tok) {
1066 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
1067 Err(other) => panic!("expected Unconfigured, got {:?}", other),
1068 Ok(_) => panic!("expected Err, got Ok"),
1069 }
1070 }
1071
1072 #[test]
1073 fn vec_model_key_sanitizes_dots_and_dashes() {
1074 assert_eq!(
1075 vec_model_key(EmbeddingModel::BgeSmallEnV15),
1076 "bge_small_en_v1_5"
1077 );
1078 assert_eq!(
1079 vec_model_key(EmbeddingModel::BgeBaseEnV15),
1080 "bge_base_en_v1_5"
1081 );
1082 assert_eq!(
1083 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
1084 "all_minilm_l6_v2"
1085 );
1086 }
1087
1088 #[test]
1089 fn default_config_uses_allow_all_gate() {
1090 let cfg = RuntimeConfig::default();
1091 assert_eq!(cfg.default_namespace.as_str(), "local");
1092 let _: GateRef = cfg.gate.clone();
1093 }
1094
1095 #[test]
1096 fn parse_pack_list_handles_comma_and_whitespace() {
1097 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
1098 assert_eq!(
1099 parse_pack_list("kg,gtd"),
1100 vec!["kg".to_string(), "gtd".to_string()]
1101 );
1102 assert_eq!(
1103 parse_pack_list(" kg , gtd "),
1104 vec!["kg".to_string(), "gtd".to_string()]
1105 );
1106 assert_eq!(
1107 parse_pack_list("kg gtd"),
1108 vec!["kg".to_string(), "gtd".to_string()]
1109 );
1110 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
1111 assert_eq!(parse_pack_list(""), Vec::<String>::new());
1112 }
1113
1114 #[test]
1115 fn default_config_packs_loads_all_production_packs() {
1116 let prior = std::env::var("KHIVE_PACKS").ok();
1117 unsafe {
1119 std::env::remove_var("KHIVE_PACKS");
1120 }
1121 let cfg = RuntimeConfig::default();
1122 assert!(cfg.packs.contains(&"kg".to_string()));
1123 assert!(cfg.packs.contains(&"gtd".to_string()));
1124 assert!(cfg.packs.contains(&"memory".to_string()));
1125 assert!(cfg.packs.contains(&"brain".to_string()));
1126 assert!(cfg.packs.contains(&"comm".to_string()));
1127 assert!(cfg.packs.contains(&"schedule".to_string()));
1128 assert!(cfg.packs.contains(&"knowledge".to_string()));
1129 assert_eq!(cfg.packs.len(), 7);
1130 if let Some(v) = prior {
1131 unsafe {
1133 std::env::set_var("KHIVE_PACKS", v);
1134 }
1135 }
1136 }
1137
1138 #[test]
1139 fn default_config_uses_minilm_when_env_unset() {
1140 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
1141 unsafe {
1144 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
1145 }
1146 let cfg = RuntimeConfig::default();
1147 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
1148 if let Some(v) = prior {
1149 unsafe {
1151 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
1152 }
1153 }
1154 }
1155
1156 use crate::engine_config::{ActorConfig, KhiveConfig};
1159
1160 fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
1161 KhiveConfig {
1162 engines: vec![],
1163 actor: ActorConfig {
1164 id: Some(id.to_string()),
1165 display_name: None,
1166 },
1167 }
1168 }
1169
1170 #[test]
1171 fn runtime_config_from_khive_config_applies_actor_id_as_default_namespace() {
1172 let base = RuntimeConfig {
1173 db_path: None,
1174 default_namespace: Namespace::local(),
1175 embedding_model: None,
1176 additional_embedding_models: vec![],
1177 gate: Arc::new(AllowAllGate),
1178 packs: vec!["kg".to_string()],
1179 backend_id: BackendId::main(),
1180 };
1181 let cfg = khive_cfg_with_actor("lambda:khive");
1182 let result = runtime_config_from_khive_config(&cfg, base);
1183 assert_eq!(result.default_namespace.as_str(), "lambda:khive");
1184 }
1185
1186 #[test]
1187 fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
1188 let base = RuntimeConfig {
1189 db_path: None,
1190 default_namespace: Namespace::parse("lambda:base").unwrap(),
1191 embedding_model: None,
1192 additional_embedding_models: vec![],
1193 gate: Arc::new(AllowAllGate),
1194 packs: vec!["kg".to_string()],
1195 backend_id: BackendId::main(),
1196 };
1197 let cfg = KhiveConfig {
1198 engines: vec![],
1199 actor: ActorConfig {
1200 id: Some(String::new()),
1201 display_name: None,
1202 },
1203 };
1204 let result = runtime_config_from_khive_config(&cfg, base);
1205 assert_eq!(
1206 result.default_namespace.as_str(),
1207 "lambda:base",
1208 "empty actor.id must not override base namespace"
1209 );
1210 }
1211
1212 #[test]
1213 fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
1214 let base = RuntimeConfig {
1215 db_path: None,
1216 default_namespace: Namespace::parse("lambda:base").unwrap(),
1217 embedding_model: None,
1218 additional_embedding_models: vec![],
1219 gate: Arc::new(AllowAllGate),
1220 packs: vec!["kg".to_string()],
1221 backend_id: BackendId::main(),
1222 };
1223 let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
1225 assert_eq!(
1226 result.default_namespace.as_str(),
1227 "lambda:base",
1228 "absent actor.id must not override base namespace"
1229 );
1230 }
1231
1232 #[test]
1233 fn runtime_config_from_khive_config_actor_id_with_engines() {
1234 let base = RuntimeConfig {
1235 db_path: None,
1236 default_namespace: Namespace::local(),
1237 embedding_model: None,
1238 additional_embedding_models: vec![],
1239 gate: Arc::new(AllowAllGate),
1240 packs: vec!["kg".to_string()],
1241 backend_id: BackendId::main(),
1242 };
1243 let cfg = KhiveConfig {
1244 engines: vec![crate::engine_config::EngineConfig {
1245 name: "default".to_string(),
1246 model: "all-minilm-l6-v2".to_string(),
1247 default: true,
1248 fusion_weight: None,
1249 dims: None,
1250 }],
1251 actor: ActorConfig {
1252 id: Some("lambda:test".to_string()),
1253 display_name: None,
1254 },
1255 };
1256 let result = runtime_config_from_khive_config(&cfg, base);
1257 assert_eq!(result.default_namespace.as_str(), "lambda:test");
1258 assert!(result.embedding_model.is_some());
1259 }
1260
1261 #[tokio::test]
1264 async fn list_embedding_models_returns_empty_when_table_absent() {
1265 let rt = KhiveRuntime::memory().expect("memory runtime");
1268 let records = rt
1269 .list_embedding_models(None)
1270 .await
1271 .expect("list ok on empty table");
1272 assert!(records.is_empty());
1273 }
1274
1275 #[tokio::test]
1276 async fn list_embedding_models_returns_row_after_insert() {
1277 use khive_storage::{SqlStatement, SqlValue};
1278
1279 let rt = KhiveRuntime::memory().expect("memory runtime");
1280 let sql = rt.sql();
1281
1282 let now = 1_000_000i64;
1283 let id = uuid::Uuid::new_v4();
1284 let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
1285
1286 let mut writer = sql.writer().await.expect("writer");
1287 writer
1288 .execute(SqlStatement {
1289 sql: "INSERT INTO _embedding_models \
1290 (id, engine_name, model_id, key_version, dim, output_dim, status, \
1291 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
1292 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
1293 .into(),
1294 params: vec![
1295 SqlValue::Blob(id.as_bytes().to_vec()),
1296 SqlValue::Text("test_engine".into()),
1297 SqlValue::Text("test-model-v1".into()),
1298 SqlValue::Text("v1".into()),
1299 SqlValue::Integer(384),
1300 SqlValue::Text("active".into()),
1301 SqlValue::Integer(now),
1302 SqlValue::Blob(canonical_key),
1303 SqlValue::Integer(now),
1304 ],
1305 label: None,
1306 })
1307 .await
1308 .expect("insert row");
1309 drop(writer);
1310
1311 let records = rt.list_embedding_models(None).await.expect("list ok");
1312 assert_eq!(records.len(), 1);
1313 assert_eq!(records[0].engine_name, "test_engine");
1314 assert_eq!(records[0].model_id, "test-model-v1");
1315 assert_eq!(records[0].key_version, "v1");
1316 assert_eq!(records[0].dimensions, 384);
1317 assert_eq!(records[0].status, "active");
1318
1319 let filtered = rt
1321 .list_embedding_models(Some("test_engine"))
1322 .await
1323 .expect("filter ok");
1324 assert_eq!(filtered.len(), 1);
1325
1326 let no_match = rt
1328 .list_embedding_models(Some("other_engine"))
1329 .await
1330 .expect("no-match ok");
1331 assert!(no_match.is_empty());
1332 }
1333}