1use std::sync::{Arc, RwLock};
7
8use khive_db::StorageBackend;
9use khive_gate::{ActorRef, AllowAllGate, GateRequest};
10use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
11use khive_types::{EdgeEndpointRule, Namespace};
12use lattice_embed::{EmbeddingModel, EmbeddingService};
13
14use crate::config::{
15 build_embedder_registry, parse_embedding_model_alias, register_configured_embedding_models,
16 sanitize_key, vec_model_key,
17};
18use crate::error::{RuntimeError, RuntimeResult};
19
20pub type EntityTypeValidatorFn =
29 Arc<dyn Fn(&str, Option<&str>) -> Result<Option<String>, RuntimeError> + Send + Sync>;
30
31pub use crate::config::{
32 parse_pack_list, runtime_config_from_khive_config, BackendId, NamespaceToken, RuntimeConfig,
33};
34
35#[derive(Clone)]
42pub struct KhiveRuntime {
43 backend: Arc<StorageBackend>,
44 core_backend: Option<Arc<StorageBackend>>,
48 config: RuntimeConfig,
49 embedder_registry: Arc<std::sync::RwLock<crate::embedder_registry::EmbedderRegistry>>,
56 default_embedder_name: Arc<str>,
57 edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
61 valid_entity_kinds: Arc<RwLock<Vec<String>>>,
69 valid_note_kinds: Arc<RwLock<Vec<String>>>,
70 entity_type_validator: Arc<RwLock<Option<EntityTypeValidatorFn>>>,
77}
78
79impl KhiveRuntime {
80 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
86 let backend = match &config.db_path {
87 Some(path) => {
88 if let Some(parent) = path.parent() {
89 std::fs::create_dir_all(parent).ok();
90 }
91 StorageBackend::sqlite(path)?
92 }
93 None => StorageBackend::memory()?,
94 };
95 {
102 let mut writer = backend.pool().try_writer()?;
103 khive_db::run_migrations(writer.conn_mut())?;
104 }
105 register_configured_embedding_models(&backend, &config)?;
106 let (registry, default_embedder_name) = build_embedder_registry(&config);
107 Ok(Self {
108 backend: Arc::new(backend),
109 core_backend: None,
110 config,
111 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
112 default_embedder_name,
113 edge_rules: Arc::new(RwLock::new(Vec::new())),
114 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
115 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
116 entity_type_validator: Arc::new(RwLock::new(None)),
117 })
118 }
119
120 pub fn new_readonly(config: RuntimeConfig) -> RuntimeResult<Self> {
126 let backend = match &config.db_path {
127 Some(path) => StorageBackend::sqlite(path)?,
128 None => StorageBackend::memory()?,
129 };
130 {
131 let mut writer = backend.pool().try_writer()?;
132 khive_db::run_migrations(writer.conn_mut())?;
133 }
134 let (registry, default_embedder_name) = build_embedder_registry(&config);
135 Ok(Self {
136 backend: Arc::new(backend),
137 core_backend: None,
138 config,
139 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
140 default_embedder_name,
141 edge_rules: Arc::new(RwLock::new(Vec::new())),
142 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
143 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
144 entity_type_validator: Arc::new(RwLock::new(None)),
145 })
146 }
147
148 pub fn from_backend(backend: Arc<StorageBackend>, config: RuntimeConfig) -> Self {
158 if let Err(err) = register_configured_embedding_models(&backend, &config) {
159 tracing::warn!(error = %err, "failed to register configured embedding models");
160 }
161 let (registry, default_embedder_name) = build_embedder_registry(&config);
162 Self {
163 backend,
164 core_backend: None,
165 config,
166 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
167 default_embedder_name,
168 edge_rules: Arc::new(RwLock::new(Vec::new())),
169 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
170 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
171 entity_type_validator: Arc::new(RwLock::new(None)),
172 }
173 }
174
175 pub fn with_core_backend(mut self, core: Arc<StorageBackend>) -> Self {
184 debug_assert_ne!(
185 self.config.backend_id.as_str(),
186 BackendId::MAIN,
187 "with_core_backend must not be called on the main runtime"
188 );
189 self.core_backend = Some(core);
190 self
191 }
192
193 pub fn core(&self) -> KhiveRuntime {
213 match &self.core_backend {
214 None => self.clone(),
215 Some(main_arc) => {
216 let mut core_config = self.config.clone();
217 core_config.backend_id = BackendId::main();
218 KhiveRuntime {
219 backend: main_arc.clone(),
220 core_backend: None,
221 config: core_config,
222 embedder_registry: self.embedder_registry.clone(),
223 default_embedder_name: self.default_embedder_name.clone(),
224 edge_rules: self.edge_rules.clone(),
225 valid_entity_kinds: self.valid_entity_kinds.clone(),
226 valid_note_kinds: self.valid_note_kinds.clone(),
227 entity_type_validator: self.entity_type_validator.clone(),
228 }
229 }
230 }
231 }
232
233 pub fn memory() -> RuntimeResult<Self> {
235 Self::new(RuntimeConfig {
236 db_path: None,
237 default_namespace: Namespace::local(),
238 embedding_model: None,
239 additional_embedding_models: vec![],
240 gate: Arc::new(AllowAllGate),
241 packs: vec!["kg".to_string()],
242 backend_id: BackendId::main(),
243 brain_profile: None,
244 visible_namespaces: vec![],
245 allowed_outbound_namespaces: vec![],
246 actor_id: None,
247 })
248 }
249
250 pub fn backend_id(&self) -> &BackendId {
255 &self.config.backend_id
256 }
257
258 pub fn visible_namespaces(&self) -> &[Namespace] {
265 &self.config.visible_namespaces
266 }
267
268 pub fn config(&self) -> &RuntimeConfig {
270 &self.config
271 }
272
273 pub fn backend(&self) -> &StorageBackend {
275 &self.backend
276 }
277
278 pub fn backend_data_dir(&self) -> Option<std::path::PathBuf> {
281 self.backend.data_dir()
282 }
283
284 pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
288 Ok(self
289 .backend
290 .entities_for_namespace(token.namespace().as_str())?)
291 }
292
293 pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
295 Ok(self
296 .backend
297 .graph_for_namespace(token.namespace().as_str())?)
298 }
299
300 pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
302 Ok(self
303 .backend
304 .notes_for_namespace(token.namespace().as_str())?)
305 }
306
307 pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
309 Ok(self
310 .backend
311 .events_for_namespace(token.namespace().as_str())?)
312 }
313
314 pub fn sql(&self) -> Arc<dyn SqlAccess> {
316 self.backend.sql()
317 }
318
319 pub fn vectors(
323 &self,
324 token: &NamespaceToken,
325 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
326 let model = self.resolve_embedding_model(None)?;
327 self.vectors_for_embedding_model(token, model)
328 }
329
330 pub fn vectors_for_model(
338 &self,
339 token: &NamespaceToken,
340 model_name: &str,
341 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
342 if let Some(model) = parse_embedding_model_alias(model_name) {
344 let key = model.to_string();
347 let in_registry = self
348 .embedder_registry
349 .read()
350 .map(|reg| reg.contains(&key))
351 .unwrap_or(false);
352 if in_registry {
353 return self.vectors_for_embedding_model(token, model);
354 }
355 }
356 let dims = {
359 let registry = self.embedder_registry.read().map_err(|_| {
360 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
361 })?;
362 registry
363 .get_provider(model_name)
364 .map(|p| p.dimensions())
365 .ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
366 };
367 let model_key = sanitize_key(model_name);
368 Ok(self.backend.vectors_for_namespace(
369 &model_key,
370 model_name,
371 dims,
372 token.namespace().as_str(),
373 )?)
374 }
375
376 fn vectors_for_embedding_model(
377 &self,
378 token: &NamespaceToken,
379 model: EmbeddingModel,
380 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
381 Ok(self.backend.vectors_for_namespace(
382 &vec_model_key(model),
383 &model.to_string(),
384 model.dimensions(),
385 token.namespace().as_str(),
386 )?)
387 }
388
389 pub fn text(
391 &self,
392 token: &NamespaceToken,
393 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
394 let _ = token;
395 Ok(self.backend.text("entities")?)
396 }
397
398 pub fn text_for_notes(
400 &self,
401 token: &NamespaceToken,
402 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
403 let _ = token;
404 Ok(self.backend.text("notes")?)
405 }
406
407 pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
423 let actor = match self.config.actor_id.as_deref() {
424 Some(id) if !id.trim().is_empty() => ActorRef::new("actor", id),
425 _ => ActorRef::anonymous(),
426 };
427 let req = GateRequest::new(
428 actor.clone(),
429 ns.clone(),
430 "authorize",
431 serde_json::Value::Null,
432 );
433 match self.config.gate.check(&req) {
434 Ok(ref decision) if decision.is_allow() => {
435 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
436 if !obligations.is_empty() {
437 tracing::debug!(
438 namespace = %ns.as_str(),
439 "authorize: obligations={:?}",
440 obligations
441 );
442 }
443 }
444 Ok(NamespaceToken::mint_authorized(ns, actor))
445 }
446 Ok(khive_gate::GateDecision::Deny { reason }) => {
447 Err(crate::RuntimeError::PermissionDenied {
448 verb: "authorize".to_string(),
449 reason,
450 })
451 }
452 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
453 verb: "authorize".to_string(),
454 reason: "gate denied".to_string(),
455 }),
456 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
457 }
458 }
459
460 pub fn authorize_with_visibility(
475 &self,
476 primary: Namespace,
477 extra_visible: Vec<Namespace>,
478 ) -> RuntimeResult<NamespaceToken> {
479 let actor = match self.config.actor_id.as_deref() {
480 Some(id) if !id.trim().is_empty() => ActorRef::new("actor", id),
481 _ => ActorRef::anonymous(),
482 };
483 let req = GateRequest::new(
484 actor.clone(),
485 primary.clone(),
486 "authorize",
487 serde_json::Value::Null,
488 );
489 match self.config.gate.check(&req) {
490 Ok(ref decision) if decision.is_allow() => {
491 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
492 if !obligations.is_empty() {
493 tracing::debug!(
494 namespace = %primary.as_str(),
495 "authorize_with_visibility: obligations={:?}",
496 obligations
497 );
498 }
499 }
500 Ok(NamespaceToken::mint_with_visibility(
501 primary,
502 extra_visible,
503 actor,
504 ))
505 }
506 Ok(khive_gate::GateDecision::Deny { reason }) => {
507 Err(crate::RuntimeError::PermissionDenied {
508 verb: "authorize".to_string(),
509 reason,
510 })
511 }
512 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
513 verb: "authorize".to_string(),
514 reason: "gate denied".to_string(),
515 }),
516 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
517 }
518 }
519
520 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
526 if let Ok(mut guard) = self.edge_rules.write() {
527 *guard = rules;
528 }
529 }
530
531 pub fn install_kind_registry(&self, entity_kinds: Vec<String>, note_kinds: Vec<String>) {
541 if let Ok(mut guard) = self.valid_entity_kinds.write() {
542 *guard = entity_kinds;
543 }
544 if let Ok(mut guard) = self.valid_note_kinds.write() {
545 *guard = note_kinds;
546 }
547 }
548
549 pub(crate) fn validate_entity_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
554 let guard = self.valid_entity_kinds.read().map_err(|_| {
555 crate::RuntimeError::Internal("entity kind registry lock poisoned".into())
556 })?;
557 if guard.is_empty() {
558 return Ok(());
559 }
560 if guard.iter().any(|k| k == kind) {
561 Ok(())
562 } else {
563 Err(crate::RuntimeError::InvalidInput(format!(
564 "unknown entity kind {kind:?}; valid: {}",
565 guard.join(", ")
566 )))
567 }
568 }
569
570 pub(crate) fn validate_note_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
575 let guard = self.valid_note_kinds.read().map_err(|_| {
576 crate::RuntimeError::Internal("note kind registry lock poisoned".into())
577 })?;
578 if guard.is_empty() {
579 return Ok(());
580 }
581 if guard.iter().any(|k| k == kind) {
582 Ok(())
583 } else {
584 Err(crate::RuntimeError::InvalidInput(format!(
585 "unknown note kind {kind:?}; valid: {}",
586 guard.join(", ")
587 )))
588 }
589 }
590
591 pub fn install_entity_type_validator(&self, f: EntityTypeValidatorFn) {
601 if let Ok(mut guard) = self.entity_type_validator.write() {
602 *guard = Some(f);
603 }
604 }
605
606 pub(crate) fn validate_entity_type_for_kind(
611 &self,
612 kind: &str,
613 entity_type: Option<&str>,
614 ) -> crate::RuntimeResult<Option<String>> {
615 let guard = self.entity_type_validator.read().map_err(|_| {
616 crate::RuntimeError::Internal("entity type validator lock poisoned".into())
617 })?;
618 match guard.as_ref() {
619 None => Ok(entity_type.map(str::to_string)),
620 Some(validate) => validate(kind, entity_type),
621 }
622 }
623
624 pub(crate) fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
626 self.edge_rules
627 .read()
628 .map(|g| g.clone())
629 .unwrap_or_default()
630 }
631
632 pub fn default_embedder_name(&self) -> &str {
634 self.default_embedder_name.as_ref()
635 }
636
637 pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
642 let model = match name {
643 Some(raw) => parse_embedding_model_alias(raw)
644 .ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
645 None => self
646 .config
647 .embedding_model
648 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
649 };
650 let key = model.to_string();
651 let contains = self
652 .embedder_registry
653 .read()
654 .map(|reg| reg.contains(&key))
655 .unwrap_or(false);
656 if contains {
657 Ok(model)
658 } else {
659 Err(crate::RuntimeError::UnknownModel(
660 name.unwrap_or_else(|| self.default_embedder_name())
661 .to_string(),
662 ))
663 }
664 }
665
666 pub fn registered_embedding_model_names(&self) -> Vec<String> {
674 self.embedder_registry
675 .read()
676 .map(|reg| reg.names())
677 .unwrap_or_default()
678 }
679
680 pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
693 let canonical_key = match parse_embedding_model_alias(name) {
698 Some(model) => model.to_string(),
699 None => name.to_owned(),
700 };
701 let entry = {
704 let registry = self.embedder_registry.read().map_err(|_| {
705 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
706 })?;
707 registry
708 .get_entry(&canonical_key)
709 .ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
710 };
711 entry.resolve().await
712 }
713
714 pub fn register_embedder(
727 &self,
728 provider: impl crate::embedder_registry::EmbedderProvider + 'static,
729 ) {
730 if let Ok(mut registry) = self.embedder_registry.write() {
731 registry.register(provider);
732 } else {
733 tracing::warn!(
734 "embedder registry lock poisoned — embedder {} not registered",
735 std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
736 );
737 }
738 }
739
740 pub async fn list_embedding_models(
747 &self,
748 engine_filter: Option<&str>,
749 ) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
750 use khive_storage::{SqlStatement, SqlValue};
751
752 let (sql_text, params) = if let Some(engine) = engine_filter {
753 (
754 "SELECT engine_name, model_id, key_version, dim, status, \
755 activated_at, superseded_at \
756 FROM _embedding_models WHERE engine_name = ?1 \
757 ORDER BY engine_name, activated_at IS NULL, activated_at"
758 .to_string(),
759 vec![SqlValue::Text(engine.to_string())],
760 )
761 } else {
762 (
763 "SELECT engine_name, model_id, key_version, dim, status, \
764 activated_at, superseded_at \
765 FROM _embedding_models \
766 ORDER BY engine_name, activated_at IS NULL, activated_at"
767 .to_string(),
768 vec![],
769 )
770 };
771
772 let stmt = SqlStatement {
773 sql: sql_text,
774 params,
775 label: Some("list_embedding_models".into()),
776 };
777
778 let mut reader = self
779 .sql()
780 .reader()
781 .await
782 .map_err(crate::RuntimeError::Storage)?;
783
784 let rows = match reader.query_all(stmt).await {
785 Ok(rows) => rows,
786 Err(e) if e.to_string().contains("no such table: _embedding_models") => {
787 return Ok(Vec::new())
788 }
789 Err(e) => return Err(crate::RuntimeError::Storage(e)),
790 };
791
792 let mut records = Vec::with_capacity(rows.len());
793 for row in rows {
794 macro_rules! required_text {
795 ($col:expr) => {
796 match row.get($col) {
797 Some(SqlValue::Text(s)) => s.clone(),
798 other => {
799 tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
800 continue;
801 }
802 }
803 };
804 }
805 let engine_name = required_text!("engine_name");
806 let model_id = required_text!("model_id");
807 let key_version = required_text!("key_version");
808 let dimensions = match row.get("dim") {
809 Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
810 Ok(d) => d,
811 Err(_) => {
812 tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
813 continue;
814 }
815 },
816 other => {
817 tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
818 continue;
819 }
820 };
821 let status = required_text!("status");
822 let activated_at = match row.get("activated_at") {
823 Some(SqlValue::Integer(n)) => Some(*n),
824 _ => None,
825 };
826 let superseded_at = match row.get("superseded_at") {
827 Some(SqlValue::Integer(n)) => Some(*n),
828 _ => None,
829 };
830 records.push(khive_db::EmbeddingModelRegistryRecord {
831 engine_name,
832 model_id,
833 key_version,
834 dimensions,
835 status,
836 activated_at,
837 superseded_at,
838 });
839 }
840
841 Ok(records)
842 }
843}
844
845#[cfg(test)]
849mod tests {
850 use super::*;
851 use khive_gate::GateRef;
852
853 #[test]
854 fn memory_runtime_creates_successfully() {
855 let rt = KhiveRuntime::memory().expect("memory runtime should create");
856 assert!(rt.config().db_path.is_none());
857 }
858
859 #[test]
860 fn backend_data_dir_returns_none_for_memory_backend() {
861 let rt = KhiveRuntime::memory().expect("memory runtime");
862 assert!(rt.backend_data_dir().is_none());
863 }
864
865 #[test]
866 fn backend_data_dir_returns_parent_dir_for_file_backend() {
867 let dir = tempfile::tempdir().unwrap();
868 let path = dir.path().join("test.db");
869 let config = RuntimeConfig {
870 db_path: Some(path),
871 default_namespace: Namespace::local(),
872 embedding_model: None,
873 additional_embedding_models: vec![],
874 gate: Arc::new(AllowAllGate),
875 packs: vec!["kg".to_string()],
876 backend_id: BackendId::main(),
877 brain_profile: None,
878 visible_namespaces: vec![],
879 allowed_outbound_namespaces: vec![],
880 actor_id: None,
881 };
882 let rt = KhiveRuntime::new(config).expect("file runtime");
883 let data_dir = rt
884 .backend_data_dir()
885 .expect("file backend must return Some");
886 assert_eq!(data_dir, dir.path());
887 }
888
889 #[test]
890 fn backend_data_dir_returns_none_for_from_backend_with_memory() {
891 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
892 let config = RuntimeConfig {
893 db_path: None,
894 default_namespace: Namespace::local(),
895 embedding_model: None,
896 additional_embedding_models: vec![],
897 gate: Arc::new(AllowAllGate),
898 packs: vec!["kg".to_string()],
899 backend_id: BackendId::main(),
900 brain_profile: None,
901 visible_namespaces: vec![],
902 allowed_outbound_namespaces: vec![],
903 actor_id: None,
904 };
905 let rt = KhiveRuntime::from_backend(backend, config);
906 assert!(rt.backend_data_dir().is_none());
907 }
908
909 #[test]
910 fn file_runtime_creates_successfully() {
911 let dir = tempfile::tempdir().unwrap();
912 let path = dir.path().join("test.db");
913 let config = RuntimeConfig {
914 db_path: Some(path.clone()),
915 default_namespace: Namespace::parse("test").unwrap(),
916 embedding_model: None,
917 additional_embedding_models: vec![],
918 gate: Arc::new(AllowAllGate),
919 packs: vec!["kg".to_string()],
920 backend_id: BackendId::main(),
921 brain_profile: None,
922 visible_namespaces: vec![],
923 allowed_outbound_namespaces: vec![],
924 actor_id: None,
925 };
926 let rt = KhiveRuntime::new(config).expect("file runtime should create");
927 assert!(path.exists());
928 assert_eq!(rt.config().default_namespace.as_str(), "test");
929 }
930
931 #[test]
932 fn from_backend_uses_provided_backend() {
933 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
934 let config = RuntimeConfig {
935 db_path: None,
936 default_namespace: Namespace::local(),
937 embedding_model: None,
938 additional_embedding_models: vec![],
939 gate: Arc::new(AllowAllGate),
940 packs: vec!["kg".to_string()],
941 backend_id: BackendId::new("lore"),
942 brain_profile: None,
943 visible_namespaces: vec![],
944 allowed_outbound_namespaces: vec![],
945 actor_id: None,
946 };
947 let rt = KhiveRuntime::from_backend(backend, config);
948 assert_eq!(rt.backend_id().as_str(), "lore");
949 assert!(rt.config().db_path.is_none());
950 }
951
952 #[test]
953 fn backend_id_defaults_to_main() {
954 let rt = KhiveRuntime::memory().unwrap();
955 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
956 }
957
958 #[test]
959 fn store_accessors_return_ok() {
960 let rt = KhiveRuntime::memory().unwrap();
961 let tok = NamespaceToken::local();
962 assert!(rt.entities(&tok).is_ok());
963 assert!(rt.graph(&tok).is_ok());
964 assert!(rt.notes(&tok).is_ok());
965 assert!(rt.events(&tok).is_ok());
966 }
967
968 #[test]
969 fn vectors_returns_unconfigured_without_model() {
970 let rt = KhiveRuntime::memory().unwrap();
971 let tok = NamespaceToken::local();
972 match rt.vectors(&tok) {
973 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
974 Err(other) => panic!("expected Unconfigured, got {:?}", other),
975 Ok(_) => panic!("expected Err, got Ok"),
976 }
977 }
978
979 #[test]
980 fn vec_model_key_sanitizes_dots_and_dashes() {
981 assert_eq!(
982 vec_model_key(EmbeddingModel::BgeSmallEnV15),
983 "bge_small_en_v1_5"
984 );
985 assert_eq!(
986 vec_model_key(EmbeddingModel::BgeBaseEnV15),
987 "bge_base_en_v1_5"
988 );
989 assert_eq!(
990 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
991 "all_minilm_l6_v2"
992 );
993 }
994
995 #[test]
996 fn default_config_uses_allow_all_gate() {
997 let cfg = RuntimeConfig::default();
998 assert_eq!(cfg.default_namespace.as_str(), "local");
999 let _: GateRef = cfg.gate.clone();
1000 }
1001
1002 #[test]
1003 fn parse_pack_list_handles_comma_and_whitespace() {
1004 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
1005 assert_eq!(
1006 parse_pack_list("kg,gtd"),
1007 vec!["kg".to_string(), "gtd".to_string()]
1008 );
1009 assert_eq!(
1010 parse_pack_list(" kg , gtd "),
1011 vec!["kg".to_string(), "gtd".to_string()]
1012 );
1013 assert_eq!(
1014 parse_pack_list("kg gtd"),
1015 vec!["kg".to_string(), "gtd".to_string()]
1016 );
1017 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
1018 assert_eq!(parse_pack_list(""), Vec::<String>::new());
1019 }
1020
1021 #[test]
1022 fn default_config_packs_loads_all_production_packs() {
1023 let prior = std::env::var("KHIVE_PACKS").ok();
1024 unsafe {
1026 std::env::remove_var("KHIVE_PACKS");
1027 }
1028 let cfg = RuntimeConfig::default();
1029 assert!(cfg.packs.contains(&"kg".to_string()));
1030 assert!(cfg.packs.contains(&"gtd".to_string()));
1031 assert!(cfg.packs.contains(&"memory".to_string()));
1032 assert!(cfg.packs.contains(&"brain".to_string()));
1033 assert!(cfg.packs.contains(&"comm".to_string()));
1034 assert!(cfg.packs.contains(&"schedule".to_string()));
1035 assert!(cfg.packs.contains(&"knowledge".to_string()));
1036 assert!(cfg.packs.contains(&"session".to_string()));
1039 assert_eq!(cfg.packs.len(), 8);
1040 if let Some(v) = prior {
1041 unsafe {
1043 std::env::set_var("KHIVE_PACKS", v);
1044 }
1045 }
1046 }
1047
1048 #[test]
1049 fn default_config_uses_minilm_when_env_unset() {
1050 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
1051 unsafe {
1054 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
1055 }
1056 let cfg = RuntimeConfig::default();
1057 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
1058 if let Some(v) = prior {
1059 unsafe {
1061 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
1062 }
1063 }
1064 }
1065
1066 use crate::engine_config::{ActorConfig, KhiveConfig};
1069
1070 fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
1071 KhiveConfig {
1072 engines: vec![],
1073 actor: ActorConfig {
1074 id: Some(id.to_string()),
1075 display_name: None,
1076 ..Default::default()
1077 },
1078 ..KhiveConfig::default()
1079 }
1080 }
1081
1082 #[test]
1083 fn runtime_config_from_khive_config_actor_id_does_not_override_default_namespace() {
1084 let base = RuntimeConfig {
1089 db_path: None,
1090 default_namespace: Namespace::local(),
1091 embedding_model: None,
1092 additional_embedding_models: vec![],
1093 gate: Arc::new(AllowAllGate),
1094 packs: vec!["kg".to_string()],
1095 backend_id: BackendId::main(),
1096 brain_profile: None,
1097 visible_namespaces: vec![],
1098 allowed_outbound_namespaces: vec![],
1099 actor_id: None,
1100 };
1101 let cfg = khive_cfg_with_actor("lambda:khive");
1102 let result = runtime_config_from_khive_config(&cfg, base);
1103 assert_eq!(
1104 result.default_namespace.as_str(),
1105 "local",
1106 "actor.id must not become default_namespace (ADR-007 Rev 4 Rule 0); writes pin to local"
1107 );
1108 }
1109
1110 #[test]
1111 fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
1112 let base = RuntimeConfig {
1113 db_path: None,
1114 default_namespace: Namespace::parse("lambda:base").unwrap(),
1115 embedding_model: None,
1116 additional_embedding_models: vec![],
1117 gate: Arc::new(AllowAllGate),
1118 packs: vec!["kg".to_string()],
1119 backend_id: BackendId::main(),
1120 brain_profile: None,
1121 visible_namespaces: vec![],
1122 allowed_outbound_namespaces: vec![],
1123 actor_id: None,
1124 };
1125 let cfg = KhiveConfig {
1126 engines: vec![],
1127 actor: ActorConfig {
1128 id: Some(String::new()),
1129 display_name: None,
1130 ..Default::default()
1131 },
1132 ..KhiveConfig::default()
1133 };
1134 let result = runtime_config_from_khive_config(&cfg, base);
1135 assert_eq!(
1136 result.default_namespace.as_str(),
1137 "lambda:base",
1138 "empty actor.id must not override base namespace"
1139 );
1140 }
1141
1142 #[test]
1143 fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
1144 let base = RuntimeConfig {
1145 db_path: None,
1146 default_namespace: Namespace::parse("lambda:base").unwrap(),
1147 embedding_model: None,
1148 additional_embedding_models: vec![],
1149 gate: Arc::new(AllowAllGate),
1150 packs: vec!["kg".to_string()],
1151 backend_id: BackendId::main(),
1152 brain_profile: None,
1153 visible_namespaces: vec![],
1154 allowed_outbound_namespaces: vec![],
1155 actor_id: None,
1156 };
1157 let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
1159 assert_eq!(
1160 result.default_namespace.as_str(),
1161 "lambda:base",
1162 "absent actor.id must not override base namespace"
1163 );
1164 }
1165
1166 #[test]
1167 fn runtime_config_from_khive_config_actor_id_with_engines() {
1168 let base = RuntimeConfig {
1169 db_path: None,
1170 default_namespace: Namespace::local(),
1171 embedding_model: None,
1172 additional_embedding_models: vec![],
1173 gate: Arc::new(AllowAllGate),
1174 packs: vec!["kg".to_string()],
1175 backend_id: BackendId::main(),
1176 brain_profile: None,
1177 visible_namespaces: vec![],
1178 allowed_outbound_namespaces: vec![],
1179 actor_id: None,
1180 };
1181 let cfg = KhiveConfig {
1182 engines: vec![crate::engine_config::EngineConfig {
1183 name: "default".to_string(),
1184 model: "all-minilm-l6-v2".to_string(),
1185 default: true,
1186 fusion_weight: None,
1187 dims: None,
1188 }],
1189 actor: ActorConfig {
1190 id: Some("lambda:test".to_string()),
1191 display_name: None,
1192 ..Default::default()
1193 },
1194 ..KhiveConfig::default()
1195 };
1196 let result = runtime_config_from_khive_config(&cfg, base);
1197 assert_eq!(
1198 result.default_namespace.as_str(),
1199 "local",
1200 "actor.id must not override default_namespace (ADR-007 Rev 4 Rule 0); \
1201 writes pin to local; engine config is still applied"
1202 );
1203 assert!(result.embedding_model.is_some());
1204 }
1205
1206 fn migrated_memory_backend() -> Arc<StorageBackend> {
1212 let backend = StorageBackend::memory().expect("memory backend");
1213 {
1214 let mut writer = backend.pool().try_writer().expect("writer");
1215 khive_db::run_migrations(writer.conn_mut()).expect("migrations");
1216 }
1217 Arc::new(backend)
1218 }
1219
1220 fn secondary_config() -> RuntimeConfig {
1221 RuntimeConfig {
1222 db_path: None,
1223 default_namespace: Namespace::local(),
1224 embedding_model: None,
1225 additional_embedding_models: vec![],
1226 gate: Arc::new(AllowAllGate),
1227 packs: vec!["kg".to_string()],
1228 backend_id: BackendId::new("lore"),
1229 brain_profile: None,
1230 visible_namespaces: vec![],
1231 allowed_outbound_namespaces: vec![],
1232 actor_id: None,
1233 }
1234 }
1235
1236 #[test]
1237 fn core_on_main_runtime_returns_same_backend_id() {
1238 let rt = KhiveRuntime::memory().unwrap();
1240 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
1241 let core_rt = rt.core();
1242 assert_eq!(core_rt.backend_id().as_str(), BackendId::MAIN);
1243 }
1244
1245 #[tokio::test]
1246 async fn core_on_main_runtime_round_trips_note() {
1247 let rt = KhiveRuntime::memory().unwrap();
1250 let tok = NamespaceToken::local();
1251
1252 let note = rt
1253 .core()
1254 .create_note(
1255 &tok,
1256 "observation",
1257 None,
1258 "adr073-main-round-trip",
1259 None,
1260 None,
1261 vec![],
1262 )
1263 .await
1264 .expect("create_note via core()");
1265
1266 let found = rt
1267 .notes(&tok)
1268 .expect("notes store")
1269 .get_note(note.id)
1270 .await
1271 .expect("get_note");
1272
1273 assert!(
1274 found.is_some(),
1275 "note written via core() must be visible through original rt"
1276 );
1277 }
1278
1279 #[tokio::test]
1291 async fn cross_backend_split_note_to_main_aux_to_secondary() {
1292 use khive_storage::{SqlStatement, SqlValue};
1293
1294 let main_arc = migrated_memory_backend();
1296 let secondary_arc = migrated_memory_backend();
1297
1298 let main_config = RuntimeConfig {
1299 db_path: None,
1300 default_namespace: Namespace::local(),
1301 embedding_model: None,
1302 additional_embedding_models: vec![],
1303 gate: Arc::new(AllowAllGate),
1304 packs: vec!["kg".to_string()],
1305 backend_id: BackendId::main(),
1306 brain_profile: None,
1307 visible_namespaces: vec![],
1308 allowed_outbound_namespaces: vec![],
1309 actor_id: None,
1310 };
1311
1312 let rt_main = KhiveRuntime::from_backend(main_arc.clone(), main_config);
1313 let rt_secondary = KhiveRuntime::from_backend(secondary_arc, secondary_config())
1314 .with_core_backend(main_arc.clone());
1315
1316 let tok = NamespaceToken::local();
1317
1318 let note = rt_secondary
1321 .core()
1322 .create_note(
1323 &tok,
1324 "observation",
1325 None,
1326 "adr073-split-test",
1327 None,
1328 None,
1329 vec![],
1330 )
1331 .await
1332 .expect("create_note via core()");
1333 let note_id = note.id;
1334
1335 let in_main = rt_main
1337 .notes(&tok)
1338 .expect("main notes store")
1339 .get_note(note_id)
1340 .await
1341 .expect("get_note from main");
1342 assert!(
1343 in_main.is_some(),
1344 "note written via core() must appear in main backend A"
1345 );
1346
1347 let in_secondary = rt_secondary
1349 .notes(&tok)
1350 .expect("secondary notes store")
1351 .get_note(note_id)
1352 .await
1353 .expect("get_note from secondary");
1354 assert!(
1355 in_secondary.is_none(),
1356 "note written to main via core() must NOT appear in secondary backend B"
1357 );
1358
1359 {
1363 let mut writer = rt_secondary.sql().writer().await.expect("secondary writer");
1364 writer
1365 .execute(SqlStatement {
1366 sql: "CREATE TABLE IF NOT EXISTS _test_adr073_aux \
1367 (marker TEXT PRIMARY KEY)"
1368 .into(),
1369 params: vec![],
1370 label: None,
1371 })
1372 .await
1373 .expect("create aux table in B");
1374 writer
1375 .execute(SqlStatement {
1376 sql: "INSERT INTO _test_adr073_aux VALUES (?1)".into(),
1377 params: vec![SqlValue::Text("b-side-sentinel".into())],
1378 label: None,
1379 })
1380 .await
1381 .expect("insert into aux table in B");
1382 }
1383
1384 let mut reader_b = rt_secondary.sql().reader().await.expect("secondary reader");
1386 let rows_b = reader_b
1387 .query_all(SqlStatement {
1388 sql: "SELECT marker FROM _test_adr073_aux".into(),
1389 params: vec![],
1390 label: None,
1391 })
1392 .await
1393 .expect("select from B");
1394 assert_eq!(rows_b.len(), 1, "aux row must exist in B");
1395 match rows_b[0].get("marker") {
1396 Some(SqlValue::Text(s)) => {
1397 assert_eq!(s, "b-side-sentinel", "sentinel value must match")
1398 }
1399 other => panic!("expected Text('b-side-sentinel'), got {other:?}"),
1400 }
1401
1402 let mut reader_a = rt_main.sql().reader().await.expect("main reader");
1404 let result_a = reader_a
1405 .query_all(SqlStatement {
1406 sql: "SELECT marker FROM _test_adr073_aux".into(),
1407 params: vec![],
1408 label: None,
1409 })
1410 .await;
1411 match result_a {
1413 Err(e) => assert!(
1414 e.to_string().contains("no such table"),
1415 "expected 'no such table' error from A, got: {e}"
1416 ),
1417 Ok(rows) => assert!(
1418 rows.is_empty(),
1419 "aux table must not have rows in A, got {} rows",
1420 rows.len()
1421 ),
1422 }
1423 }
1424
1425 #[test]
1426 fn constructors_leave_core_backend_none_by_behavior() {
1427 let rt_mem = KhiveRuntime::memory().unwrap();
1430 assert_eq!(rt_mem.core().backend_id().as_str(), BackendId::MAIN);
1431
1432 let backend = migrated_memory_backend();
1433 let rt_from = KhiveRuntime::from_backend(
1434 backend,
1435 RuntimeConfig {
1436 db_path: None,
1437 default_namespace: Namespace::local(),
1438 embedding_model: None,
1439 additional_embedding_models: vec![],
1440 gate: Arc::new(AllowAllGate),
1441 packs: vec!["kg".to_string()],
1442 backend_id: BackendId::new("lore"),
1443 brain_profile: None,
1444 visible_namespaces: vec![],
1445 allowed_outbound_namespaces: vec![],
1446 actor_id: None,
1447 },
1448 );
1449 assert_eq!(rt_from.core().backend_id().as_str(), "lore");
1452 }
1453
1454 #[test]
1455 fn with_core_backend_sets_core_then_core_returns_main_id() {
1456 let main_arc = migrated_memory_backend();
1458 let secondary_arc = migrated_memory_backend();
1459
1460 let rt_secondary = KhiveRuntime::from_backend(secondary_arc, secondary_config())
1461 .with_core_backend(main_arc);
1462
1463 assert_eq!(rt_secondary.backend_id().as_str(), "lore");
1464 assert_eq!(
1465 rt_secondary.core().backend_id().as_str(),
1466 BackendId::MAIN,
1467 "core() on a secondary runtime must return a main-bound handle"
1468 );
1469 }
1470
1471 #[tokio::test]
1472 async fn list_embedding_models_returns_empty_when_table_absent() {
1473 let rt = KhiveRuntime::memory().expect("memory runtime");
1476 let records = rt
1477 .list_embedding_models(None)
1478 .await
1479 .expect("list ok on empty table");
1480 assert!(records.is_empty());
1481 }
1482
1483 #[tokio::test]
1484 async fn list_embedding_models_returns_row_after_insert() {
1485 use khive_storage::{SqlStatement, SqlValue};
1486
1487 let rt = KhiveRuntime::memory().expect("memory runtime");
1488 let sql = rt.sql();
1489
1490 let now = 1_000_000i64;
1491 let id = uuid::Uuid::new_v4();
1492 let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
1493
1494 let mut writer = sql.writer().await.expect("writer");
1495 writer
1496 .execute(SqlStatement {
1497 sql: "INSERT INTO _embedding_models \
1498 (id, engine_name, model_id, key_version, dim, output_dim, status, \
1499 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
1500 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
1501 .into(),
1502 params: vec![
1503 SqlValue::Blob(id.as_bytes().to_vec()),
1504 SqlValue::Text("test_engine".into()),
1505 SqlValue::Text("test-model-v1".into()),
1506 SqlValue::Text("v1".into()),
1507 SqlValue::Integer(384),
1508 SqlValue::Text("active".into()),
1509 SqlValue::Integer(now),
1510 SqlValue::Blob(canonical_key),
1511 SqlValue::Integer(now),
1512 ],
1513 label: None,
1514 })
1515 .await
1516 .expect("insert row");
1517 drop(writer);
1518
1519 let records = rt.list_embedding_models(None).await.expect("list ok");
1520 assert_eq!(records.len(), 1);
1521 assert_eq!(records[0].engine_name, "test_engine");
1522 assert_eq!(records[0].model_id, "test-model-v1");
1523 assert_eq!(records[0].key_version, "v1");
1524 assert_eq!(records[0].dimensions, 384);
1525 assert_eq!(records[0].status, "active");
1526
1527 let filtered = rt
1529 .list_embedding_models(Some("test_engine"))
1530 .await
1531 .expect("filter ok");
1532 assert_eq!(filtered.len(), 1);
1533
1534 let no_match = rt
1536 .list_embedding_models(Some("other_engine"))
1537 .await
1538 .expect("no-match ok");
1539 assert!(no_match.is_empty());
1540 }
1541}