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 {
104 &self.namespace
105 }
106
107 pub fn actor(&self) -> &ActorRef {
108 &self.actor
109 }
110
111 pub fn with_namespace(&self, ns: Namespace) -> Self {
117 Self::mint_authorized(ns, self.actor.clone())
118 }
119}
120
121#[derive(Clone, Debug)]
129pub struct RuntimeConfig {
130 pub db_path: Option<std::path::PathBuf>,
136 pub default_namespace: Namespace,
138 pub embedding_model: Option<EmbeddingModel>,
145 pub additional_embedding_models: Vec<EmbeddingModel>,
152 pub gate: GateRef,
156 pub packs: Vec<String>,
162 pub backend_id: BackendId,
167}
168
169pub fn parse_pack_list(s: &str) -> Vec<String> {
173 s.split(|c: char| c == ',' || c.is_whitespace())
174 .map(str::trim)
175 .filter(|s| !s.is_empty())
176 .map(str::to_owned)
177 .collect()
178}
179
180impl Default for RuntimeConfig {
181 fn default() -> Self {
182 let db_path = std::env::var("HOME")
183 .ok()
184 .map(|h| std::path::PathBuf::from(h).join(".khive/khive-graph.db"));
185 let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
186 .ok()
187 .and_then(|s| s.parse().ok())
188 .or(Some(EmbeddingModel::AllMiniLmL6V2));
189 let additional_embedding_models = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS")
190 .ok()
191 .map(|s| parse_embedding_model_list(&s))
192 .unwrap_or_else(|| vec![EmbeddingModel::ParaphraseMultilingualMiniLmL12V2]);
193 let packs = std::env::var("KHIVE_PACKS")
194 .ok()
195 .map(|s| parse_pack_list(&s))
196 .filter(|v| !v.is_empty())
197 .unwrap_or_else(|| {
198 vec![
199 "kg",
200 "gtd",
201 "memory",
202 "brain",
203 "comm",
204 "schedule",
205 "knowledge",
206 ]
207 .into_iter()
208 .map(String::from)
209 .collect()
210 });
211 Self {
212 db_path,
213 default_namespace: Namespace::local(),
214 embedding_model,
215 additional_embedding_models,
216 gate: Arc::new(AllowAllGate),
217 packs,
218 backend_id: BackendId::main(),
219 }
220 }
221}
222
223#[derive(Clone)]
230pub struct KhiveRuntime {
231 backend: Arc<StorageBackend>,
232 config: RuntimeConfig,
233 embedder_registry: Arc<std::sync::RwLock<crate::embedder_registry::EmbedderRegistry>>,
240 default_embedder_name: Arc<str>,
241 edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
246}
247
248impl KhiveRuntime {
249 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
255 let backend = match &config.db_path {
256 Some(path) => {
257 if let Some(parent) = path.parent() {
258 std::fs::create_dir_all(parent).ok();
259 }
260 StorageBackend::sqlite(path)?
261 }
262 None => StorageBackend::memory()?,
263 };
264 {
271 let mut writer = backend.pool().try_writer()?;
272 khive_db::run_migrations(writer.conn_mut())?;
273 }
274 register_configured_embedding_models(&backend, &config)?;
275 let (registry, default_embedder_name) = build_embedder_registry(&config);
276 Ok(Self {
277 backend: Arc::new(backend),
278 config,
279 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
280 default_embedder_name,
281 edge_rules: Arc::new(RwLock::new(Vec::new())),
282 })
283 }
284
285 pub fn new_readonly(config: RuntimeConfig) -> RuntimeResult<Self> {
291 let backend = match &config.db_path {
292 Some(path) => StorageBackend::sqlite(path)?,
293 None => StorageBackend::memory()?,
294 };
295 {
296 let mut writer = backend.pool().try_writer()?;
297 khive_db::run_migrations(writer.conn_mut())?;
298 }
299 let (registry, default_embedder_name) = build_embedder_registry(&config);
300 Ok(Self {
301 backend: Arc::new(backend),
302 config,
303 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
304 default_embedder_name,
305 edge_rules: Arc::new(RwLock::new(Vec::new())),
306 })
307 }
308
309 pub fn from_backend(backend: Arc<StorageBackend>, config: RuntimeConfig) -> Self {
319 if let Err(err) = register_configured_embedding_models(&backend, &config) {
320 tracing::warn!(error = %err, "failed to register configured embedding models");
321 }
322 let (registry, default_embedder_name) = build_embedder_registry(&config);
323 Self {
324 backend,
325 config,
326 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
327 default_embedder_name,
328 edge_rules: Arc::new(RwLock::new(Vec::new())),
329 }
330 }
331
332 pub fn memory() -> RuntimeResult<Self> {
334 Self::new(RuntimeConfig {
335 db_path: None,
336 default_namespace: Namespace::local(),
337 embedding_model: None,
338 additional_embedding_models: vec![],
339 gate: Arc::new(AllowAllGate),
340 packs: vec!["kg".to_string()],
341 backend_id: BackendId::main(),
342 })
343 }
344
345 pub fn backend_id(&self) -> &BackendId {
350 &self.config.backend_id
351 }
352
353 pub fn config(&self) -> &RuntimeConfig {
355 &self.config
356 }
357
358 pub fn backend(&self) -> &StorageBackend {
360 &self.backend
361 }
362
363 pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
367 Ok(self
368 .backend
369 .entities_for_namespace(token.namespace().as_str())?)
370 }
371
372 pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
374 Ok(self
375 .backend
376 .graph_for_namespace(token.namespace().as_str())?)
377 }
378
379 pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
381 Ok(self
382 .backend
383 .notes_for_namespace(token.namespace().as_str())?)
384 }
385
386 pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
388 Ok(self
389 .backend
390 .events_for_namespace(token.namespace().as_str())?)
391 }
392
393 pub fn sql(&self) -> Arc<dyn SqlAccess> {
395 self.backend.sql()
396 }
397
398 pub fn vectors(
402 &self,
403 token: &NamespaceToken,
404 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
405 let model = self.resolve_embedding_model(None)?;
406 self.vectors_for_embedding_model(token, model)
407 }
408
409 pub fn vectors_for_model(
417 &self,
418 token: &NamespaceToken,
419 model_name: &str,
420 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
421 if let Some(model) = parse_embedding_model_alias(model_name) {
423 let key = model.to_string();
426 let in_registry = self
427 .embedder_registry
428 .read()
429 .map(|reg| reg.contains(&key))
430 .unwrap_or(false);
431 if in_registry {
432 return self.vectors_for_embedding_model(token, model);
433 }
434 }
435 let dims = {
438 let registry = self.embedder_registry.read().map_err(|_| {
439 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
440 })?;
441 registry
442 .get_provider(model_name)
443 .map(|p| p.dimensions())
444 .ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
445 };
446 let model_key = sanitize_key(model_name);
447 Ok(self.backend.vectors_for_namespace(
448 &model_key,
449 model_name,
450 dims,
451 token.namespace().as_str(),
452 )?)
453 }
454
455 fn vectors_for_embedding_model(
456 &self,
457 token: &NamespaceToken,
458 model: EmbeddingModel,
459 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
460 Ok(self.backend.vectors_for_namespace(
461 &vec_model_key(model),
462 &model.to_string(),
463 model.dimensions(),
464 token.namespace().as_str(),
465 )?)
466 }
467
468 pub fn text(
470 &self,
471 token: &NamespaceToken,
472 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
473 let key = format!("entities_{}", sanitize_key(token.namespace().as_str()));
474 Ok(self.backend.text(&key)?)
475 }
476
477 pub fn text_for_notes(
479 &self,
480 token: &NamespaceToken,
481 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
482 let key = format!("notes_{}", sanitize_key(token.namespace().as_str()));
483 Ok(self.backend.text(&key)?)
484 }
485
486 pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
493 let actor = ActorRef::anonymous();
494 let req = GateRequest::new(
495 actor.clone(),
496 ns.clone(),
497 "authorize",
498 serde_json::Value::Null,
499 );
500 match self.config.gate.check(&req) {
501 Ok(ref decision) if decision.is_allow() => {
502 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
503 if !obligations.is_empty() {
504 tracing::debug!(
505 namespace = %ns.as_str(),
506 "authorize: obligations={:?}",
507 obligations
508 );
509 }
510 }
511 Ok(NamespaceToken::mint_authorized(ns, actor))
512 }
513 Ok(khive_gate::GateDecision::Deny { reason }) => {
514 Err(crate::RuntimeError::PermissionDenied {
515 verb: "authorize".to_string(),
516 reason,
517 })
518 }
519 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
520 verb: "authorize".to_string(),
521 reason: "gate denied".to_string(),
522 }),
523 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
524 }
525 }
526
527 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
534 if let Ok(mut guard) = self.edge_rules.write() {
535 *guard = rules;
536 }
537 }
538
539 pub(crate) fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
541 self.edge_rules
542 .read()
543 .map(|g| g.clone())
544 .unwrap_or_default()
545 }
546
547 pub fn default_embedder_name(&self) -> &str {
549 self.default_embedder_name.as_ref()
550 }
551
552 pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
557 let model = match name {
558 Some(raw) => parse_embedding_model_alias(raw)
559 .ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
560 None => self
561 .config
562 .embedding_model
563 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
564 };
565 let key = model.to_string();
566 let contains = self
567 .embedder_registry
568 .read()
569 .map(|reg| reg.contains(&key))
570 .unwrap_or(false);
571 if contains {
572 Ok(model)
573 } else {
574 Err(crate::RuntimeError::UnknownModel(
575 name.unwrap_or_else(|| self.default_embedder_name())
576 .to_string(),
577 ))
578 }
579 }
580
581 pub fn registered_embedding_model_names(&self) -> Vec<String> {
589 self.embedder_registry
590 .read()
591 .map(|reg| reg.names())
592 .unwrap_or_default()
593 }
594
595 pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
608 let canonical_key = match parse_embedding_model_alias(name) {
613 Some(model) => model.to_string(),
614 None => name.to_owned(),
615 };
616 let entry = {
619 let registry = self.embedder_registry.read().map_err(|_| {
620 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
621 })?;
622 registry
623 .get_entry(&canonical_key)
624 .ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
625 };
626 entry.resolve().await
627 }
628
629 pub fn register_embedder(
642 &self,
643 provider: impl crate::embedder_registry::EmbedderProvider + 'static,
644 ) {
645 if let Ok(mut registry) = self.embedder_registry.write() {
646 registry.register(provider);
647 } else {
648 tracing::warn!(
649 "embedder registry lock poisoned — embedder {} not registered",
650 std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
651 );
652 }
653 }
654
655 pub async fn list_embedding_models(
662 &self,
663 engine_filter: Option<&str>,
664 ) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
665 use khive_storage::{SqlStatement, SqlValue};
666
667 let (sql_text, params) = if let Some(engine) = engine_filter {
668 (
669 "SELECT engine_name, model_id, key_version, dim, status, \
670 activated_at, superseded_at \
671 FROM _embedding_models WHERE engine_name = ?1 \
672 ORDER BY engine_name, activated_at IS NULL, activated_at"
673 .to_string(),
674 vec![SqlValue::Text(engine.to_string())],
675 )
676 } else {
677 (
678 "SELECT engine_name, model_id, key_version, dim, status, \
679 activated_at, superseded_at \
680 FROM _embedding_models \
681 ORDER BY engine_name, activated_at IS NULL, activated_at"
682 .to_string(),
683 vec![],
684 )
685 };
686
687 let stmt = SqlStatement {
688 sql: sql_text,
689 params,
690 label: Some("list_embedding_models".into()),
691 };
692
693 let mut reader = self
694 .sql()
695 .reader()
696 .await
697 .map_err(crate::RuntimeError::Storage)?;
698
699 let rows = match reader.query_all(stmt).await {
700 Ok(rows) => rows,
701 Err(e) if e.to_string().contains("no such table: _embedding_models") => {
702 return Ok(Vec::new())
703 }
704 Err(e) => return Err(crate::RuntimeError::Storage(e)),
705 };
706
707 let mut records = Vec::with_capacity(rows.len());
708 for row in rows {
709 macro_rules! required_text {
710 ($col:expr) => {
711 match row.get($col) {
712 Some(SqlValue::Text(s)) => s.clone(),
713 other => {
714 tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
715 continue;
716 }
717 }
718 };
719 }
720 let engine_name = required_text!("engine_name");
721 let model_id = required_text!("model_id");
722 let key_version = required_text!("key_version");
723 let dimensions = match row.get("dim") {
724 Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
725 Ok(d) => d,
726 Err(_) => {
727 tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
728 continue;
729 }
730 },
731 other => {
732 tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
733 continue;
734 }
735 };
736 let status = required_text!("status");
737 let activated_at = match row.get("activated_at") {
738 Some(SqlValue::Integer(n)) => Some(*n),
739 _ => None,
740 };
741 let superseded_at = match row.get("superseded_at") {
742 Some(SqlValue::Integer(n)) => Some(*n),
743 _ => None,
744 };
745 records.push(khive_db::EmbeddingModelRegistryRecord {
746 engine_name,
747 model_id,
748 key_version,
749 dimensions,
750 status,
751 activated_at,
752 superseded_at,
753 });
754 }
755
756 Ok(records)
757 }
758}
759
760pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
763 sanitize_key(&model.to_string())
764}
765
766pub(crate) fn sanitize_key(s: &str) -> String {
767 s.chars()
768 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
769 .collect()
770}
771
772fn build_embedder_registry(
773 config: &RuntimeConfig,
774) -> (crate::embedder_registry::EmbedderRegistry, Arc<str>) {
775 use crate::embedder_registry::{EmbedderRegistry, LatticeEmbedderProvider};
776 let mut registry = EmbedderRegistry::new();
777 for model in configured_embedding_models(config) {
778 registry.register(LatticeEmbedderProvider::new(model));
779 }
780 let default_embedder_name = config
781 .embedding_model
782 .map(|model| Arc::<str>::from(model.to_string()))
783 .unwrap_or_else(|| Arc::<str>::from(""));
784 (registry, default_embedder_name)
785}
786
787fn configured_embedding_models(config: &RuntimeConfig) -> Vec<EmbeddingModel> {
788 let mut models = Vec::new();
789 if let Some(model) = config.embedding_model {
790 models.push(model);
791 }
792 models.extend(config.additional_embedding_models.iter().copied());
793 models.sort_by_key(|model| model.to_string());
794 models.dedup();
795 models
796}
797
798fn register_configured_embedding_models(
799 backend: &StorageBackend,
800 config: &RuntimeConfig,
801) -> RuntimeResult<()> {
802 for model in configured_embedding_models(config) {
803 backend.register_embedding_model(
804 &model.to_string(),
805 model.model_id(),
806 model.key_version(),
807 model.dimensions() as u32,
808 )?;
809 }
810 Ok(())
811}
812
813pub fn runtime_config_from_khive_config(
829 khive_cfg: &crate::engine_config::KhiveConfig,
830 base: RuntimeConfig,
831) -> RuntimeConfig {
832 let default_namespace = match khive_cfg.actor.id.as_deref() {
838 Some(id) if !id.is_empty() => match Namespace::parse(id) {
839 Ok(ns) => {
840 tracing::debug!(actor_id = id, "actor.id from config sets default_namespace");
841 ns
842 }
843 Err(e) => {
844 panic!(
845 "actor.id {id:?} passed validation but Namespace::parse failed: {e}; \
846 this is a bug — KhiveConfig must be validated before calling \
847 runtime_config_from_khive_config"
848 );
849 }
850 },
851 _ => base.default_namespace.clone(),
852 };
853
854 if khive_cfg.engines.is_empty() {
855 return RuntimeConfig {
856 default_namespace,
857 ..base
858 };
859 }
860
861 let mut embedding_model: Option<EmbeddingModel> = None;
862 let mut additional: Vec<EmbeddingModel> = Vec::new();
863
864 for engine in &khive_cfg.engines {
865 match parse_embedding_model_alias(&engine.model) {
866 Some(model) => {
867 if engine.default {
868 embedding_model = Some(model);
869 } else {
870 additional.push(model);
871 }
872 }
873 None => {
874 tracing::warn!(
875 engine = %engine.name,
876 model = %engine.model,
877 "engine config: unknown model name; engine will be skipped"
878 );
879 }
880 }
881 }
882
883 RuntimeConfig {
884 embedding_model,
885 additional_embedding_models: additional,
886 default_namespace,
887 ..base
888 }
889}
890
891fn parse_embedding_model_list(s: &str) -> Vec<EmbeddingModel> {
893 parse_pack_list(s)
894 .into_iter()
895 .filter_map(|raw| {
896 let parsed = parse_embedding_model_alias(&raw);
897 if parsed.is_none() && !raw.trim().is_empty() {
898 tracing::warn!(
903 model = %raw,
904 "KHIVE_ADDITIONAL_EMBEDDING_MODELS contains unknown model name; ignored. \
905 Valid forms: short alias like 'paraphrase' or a fully-qualified key \
906 from lattice_embed::EmbeddingModel::from_str."
907 );
908 }
909 parsed
910 })
911 .collect()
912}
913
914pub(crate) fn parse_embedding_model_alias(name: &str) -> Option<EmbeddingModel> {
915 let normalized = name.trim().to_ascii_lowercase().replace('_', "-");
916 match normalized.as_str() {
917 "paraphrase" => Some(EmbeddingModel::ParaphraseMultilingualMiniLmL12V2),
918 _ => normalized.parse().ok(),
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925
926 #[test]
927 fn memory_runtime_creates_successfully() {
928 let rt = KhiveRuntime::memory().expect("memory runtime should create");
929 assert!(rt.config().db_path.is_none());
930 }
931
932 #[test]
933 fn file_runtime_creates_successfully() {
934 let dir = tempfile::tempdir().unwrap();
935 let path = dir.path().join("test.db");
936 let config = RuntimeConfig {
937 db_path: Some(path.clone()),
938 default_namespace: Namespace::parse("test").unwrap(),
939 embedding_model: None,
940 additional_embedding_models: vec![],
941 gate: Arc::new(AllowAllGate),
942 packs: vec!["kg".to_string()],
943 backend_id: BackendId::main(),
944 };
945 let rt = KhiveRuntime::new(config).expect("file runtime should create");
946 assert!(path.exists());
947 assert_eq!(rt.config().default_namespace.as_str(), "test");
948 }
949
950 #[test]
951 fn from_backend_uses_provided_backend() {
952 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
953 let config = RuntimeConfig {
954 db_path: None,
955 default_namespace: Namespace::local(),
956 embedding_model: None,
957 additional_embedding_models: vec![],
958 gate: Arc::new(AllowAllGate),
959 packs: vec!["kg".to_string()],
960 backend_id: BackendId::new("lore"),
961 };
962 let rt = KhiveRuntime::from_backend(backend, config);
963 assert_eq!(rt.backend_id().as_str(), "lore");
964 assert!(rt.config().db_path.is_none());
965 }
966
967 #[test]
968 fn backend_id_defaults_to_main() {
969 let rt = KhiveRuntime::memory().unwrap();
970 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
971 }
972
973 #[test]
974 fn store_accessors_return_ok() {
975 let rt = KhiveRuntime::memory().unwrap();
976 let tok = NamespaceToken::local();
977 assert!(rt.entities(&tok).is_ok());
978 assert!(rt.graph(&tok).is_ok());
979 assert!(rt.notes(&tok).is_ok());
980 assert!(rt.events(&tok).is_ok());
981 }
982
983 #[test]
984 fn vectors_returns_unconfigured_without_model() {
985 let rt = KhiveRuntime::memory().unwrap();
986 let tok = NamespaceToken::local();
987 match rt.vectors(&tok) {
988 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
989 Err(other) => panic!("expected Unconfigured, got {:?}", other),
990 Ok(_) => panic!("expected Err, got Ok"),
991 }
992 }
993
994 #[test]
995 fn vec_model_key_sanitizes_dots_and_dashes() {
996 assert_eq!(
997 vec_model_key(EmbeddingModel::BgeSmallEnV15),
998 "bge_small_en_v1_5"
999 );
1000 assert_eq!(
1001 vec_model_key(EmbeddingModel::BgeBaseEnV15),
1002 "bge_base_en_v1_5"
1003 );
1004 assert_eq!(
1005 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
1006 "all_minilm_l6_v2"
1007 );
1008 }
1009
1010 #[test]
1011 fn default_config_uses_allow_all_gate() {
1012 let cfg = RuntimeConfig::default();
1013 assert_eq!(cfg.default_namespace.as_str(), "local");
1014 let _: GateRef = cfg.gate.clone();
1015 }
1016
1017 #[test]
1018 fn parse_pack_list_handles_comma_and_whitespace() {
1019 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
1020 assert_eq!(
1021 parse_pack_list("kg,gtd"),
1022 vec!["kg".to_string(), "gtd".to_string()]
1023 );
1024 assert_eq!(
1025 parse_pack_list(" kg , gtd "),
1026 vec!["kg".to_string(), "gtd".to_string()]
1027 );
1028 assert_eq!(
1029 parse_pack_list("kg gtd"),
1030 vec!["kg".to_string(), "gtd".to_string()]
1031 );
1032 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
1033 assert_eq!(parse_pack_list(""), Vec::<String>::new());
1034 }
1035
1036 #[test]
1037 fn default_config_packs_loads_all_production_packs() {
1038 let prior = std::env::var("KHIVE_PACKS").ok();
1039 unsafe {
1041 std::env::remove_var("KHIVE_PACKS");
1042 }
1043 let cfg = RuntimeConfig::default();
1044 assert!(cfg.packs.contains(&"kg".to_string()));
1045 assert!(cfg.packs.contains(&"gtd".to_string()));
1046 assert!(cfg.packs.contains(&"memory".to_string()));
1047 assert!(cfg.packs.contains(&"brain".to_string()));
1048 assert!(cfg.packs.contains(&"comm".to_string()));
1049 assert!(cfg.packs.contains(&"schedule".to_string()));
1050 assert!(cfg.packs.contains(&"knowledge".to_string()));
1051 assert_eq!(cfg.packs.len(), 7);
1052 if let Some(v) = prior {
1053 unsafe {
1055 std::env::set_var("KHIVE_PACKS", v);
1056 }
1057 }
1058 }
1059
1060 #[test]
1061 fn default_config_uses_minilm_when_env_unset() {
1062 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
1063 unsafe {
1066 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
1067 }
1068 let cfg = RuntimeConfig::default();
1069 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
1070 if let Some(v) = prior {
1071 unsafe {
1073 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
1074 }
1075 }
1076 }
1077
1078 use crate::engine_config::{ActorConfig, KhiveConfig};
1081
1082 fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
1083 KhiveConfig {
1084 engines: vec![],
1085 actor: ActorConfig {
1086 id: Some(id.to_string()),
1087 display_name: None,
1088 },
1089 }
1090 }
1091
1092 #[test]
1093 fn runtime_config_from_khive_config_applies_actor_id_as_default_namespace() {
1094 let base = RuntimeConfig {
1095 db_path: None,
1096 default_namespace: Namespace::local(),
1097 embedding_model: None,
1098 additional_embedding_models: vec![],
1099 gate: Arc::new(AllowAllGate),
1100 packs: vec!["kg".to_string()],
1101 backend_id: BackendId::main(),
1102 };
1103 let cfg = khive_cfg_with_actor("lambda:khive");
1104 let result = runtime_config_from_khive_config(&cfg, base);
1105 assert_eq!(result.default_namespace.as_str(), "lambda:khive");
1106 }
1107
1108 #[test]
1109 fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
1110 let base = RuntimeConfig {
1111 db_path: None,
1112 default_namespace: Namespace::parse("lambda:base").unwrap(),
1113 embedding_model: None,
1114 additional_embedding_models: vec![],
1115 gate: Arc::new(AllowAllGate),
1116 packs: vec!["kg".to_string()],
1117 backend_id: BackendId::main(),
1118 };
1119 let cfg = KhiveConfig {
1120 engines: vec![],
1121 actor: ActorConfig {
1122 id: Some(String::new()),
1123 display_name: None,
1124 },
1125 };
1126 let result = runtime_config_from_khive_config(&cfg, base);
1127 assert_eq!(
1128 result.default_namespace.as_str(),
1129 "lambda:base",
1130 "empty actor.id must not override base namespace"
1131 );
1132 }
1133
1134 #[test]
1135 fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
1136 let base = RuntimeConfig {
1137 db_path: None,
1138 default_namespace: Namespace::parse("lambda:base").unwrap(),
1139 embedding_model: None,
1140 additional_embedding_models: vec![],
1141 gate: Arc::new(AllowAllGate),
1142 packs: vec!["kg".to_string()],
1143 backend_id: BackendId::main(),
1144 };
1145 let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
1147 assert_eq!(
1148 result.default_namespace.as_str(),
1149 "lambda:base",
1150 "absent actor.id must not override base namespace"
1151 );
1152 }
1153
1154 #[test]
1155 fn runtime_config_from_khive_config_actor_id_with_engines() {
1156 let base = RuntimeConfig {
1157 db_path: None,
1158 default_namespace: Namespace::local(),
1159 embedding_model: None,
1160 additional_embedding_models: vec![],
1161 gate: Arc::new(AllowAllGate),
1162 packs: vec!["kg".to_string()],
1163 backend_id: BackendId::main(),
1164 };
1165 let cfg = KhiveConfig {
1166 engines: vec![crate::engine_config::EngineConfig {
1167 name: "default".to_string(),
1168 model: "all-minilm-l6-v2".to_string(),
1169 default: true,
1170 fusion_weight: None,
1171 dims: None,
1172 }],
1173 actor: ActorConfig {
1174 id: Some("lambda:test".to_string()),
1175 display_name: None,
1176 },
1177 };
1178 let result = runtime_config_from_khive_config(&cfg, base);
1179 assert_eq!(result.default_namespace.as_str(), "lambda:test");
1180 assert!(result.embedding_model.is_some());
1181 }
1182
1183 #[tokio::test]
1186 async fn list_embedding_models_returns_empty_when_table_absent() {
1187 let rt = KhiveRuntime::memory().expect("memory runtime");
1190 let records = rt
1191 .list_embedding_models(None)
1192 .await
1193 .expect("list ok on empty table");
1194 assert!(records.is_empty());
1195 }
1196
1197 #[tokio::test]
1198 async fn list_embedding_models_returns_row_after_insert() {
1199 use khive_storage::{SqlStatement, SqlValue};
1200
1201 let rt = KhiveRuntime::memory().expect("memory runtime");
1202 let sql = rt.sql();
1203
1204 let now = 1_000_000i64;
1205 let id = uuid::Uuid::new_v4();
1206 let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
1207
1208 let mut writer = sql.writer().await.expect("writer");
1209 writer
1210 .execute(SqlStatement {
1211 sql: "INSERT INTO _embedding_models \
1212 (id, engine_name, model_id, key_version, dim, output_dim, status, \
1213 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
1214 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
1215 .into(),
1216 params: vec![
1217 SqlValue::Blob(id.as_bytes().to_vec()),
1218 SqlValue::Text("test_engine".into()),
1219 SqlValue::Text("test-model-v1".into()),
1220 SqlValue::Text("v1".into()),
1221 SqlValue::Integer(384),
1222 SqlValue::Text("active".into()),
1223 SqlValue::Integer(now),
1224 SqlValue::Blob(canonical_key),
1225 SqlValue::Integer(now),
1226 ],
1227 label: None,
1228 })
1229 .await
1230 .expect("insert row");
1231 drop(writer);
1232
1233 let records = rt.list_embedding_models(None).await.expect("list ok");
1234 assert_eq!(records.len(), 1);
1235 assert_eq!(records[0].engine_name, "test_engine");
1236 assert_eq!(records[0].model_id, "test-model-v1");
1237 assert_eq!(records[0].key_version, "v1");
1238 assert_eq!(records[0].dimensions, 384);
1239 assert_eq!(records[0].status, "active");
1240
1241 let filtered = rt
1243 .list_embedding_models(Some("test_engine"))
1244 .await
1245 .expect("filter ok");
1246 assert_eq!(filtered.len(), 1);
1247
1248 let no_match = rt
1250 .list_embedding_models(Some("other_engine"))
1251 .await
1252 .expect("no-match ok");
1253 assert!(no_match.is_empty());
1254 }
1255}