1use std::collections::{HashMap, HashSet, VecDeque};
14use std::sync::Arc;
15use std::time::Instant;
16
17use crate::runtime::NamespaceToken;
18use async_trait::async_trait;
19use khive_gate::{AllowAllGate, AuditEvent, GateDecision, GateRef, GateRequest};
20use khive_storage::{Event, EventStore, EventView, SubstrateKind};
21use khive_types::{EventKind, EventOutcome, Namespace};
22use serde_json::Value;
23
24pub use khive_types::{
25 EdgeEndpointRule, EndpointKind, HandlerDef, NoteKindSpec, NoteLifecycleSpec, PackSchemaPlan,
26 ParamDef, VerbCategory, VerbPresentationPolicy, Visibility,
27};
28#[allow(deprecated)]
30pub use khive_types::VerbDef;
31
32use crate::validation::ValidationRule;
33
34#[derive(Debug, Default, Clone)]
44pub struct SchemaPlan {
45 pub pack: &'static str,
47 pub statements: &'static [&'static str],
51}
52
53impl SchemaPlan {
54 pub const fn empty() -> Self {
59 Self {
60 pack: "",
61 statements: &[],
62 }
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.statements.is_empty()
68 }
69}
70
71#[async_trait]
76pub trait DispatchHook: Send + Sync {
77 async fn on_dispatch(&self, view: &EventView);
82}
83
84use crate::error::{
85 CircularPackDependency, MissingPackDependencies, MissingPackDependency, RuntimeError,
86};
87use crate::KhiveRuntime;
88
89#[async_trait]
98pub trait PackRuntime: Send + Sync {
99 fn name(&self) -> &str;
101
102 fn note_kinds(&self) -> &'static [&'static str];
104
105 fn entity_kinds(&self) -> &'static [&'static str];
107
108 fn handlers(&self) -> &'static [HandlerDef];
110
111 fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
115 &[]
116 }
117
118 fn requires(&self) -> &'static [&'static str] {
121 &[]
122 }
123
124 fn note_kind_specs(&self) -> &'static [NoteKindSpec] {
131 &[]
132 }
133
134 fn kind_hook(&self, _kind: &str) -> Option<Arc<dyn KindHook>> {
142 None
143 }
144
145 fn schema_plan(&self) -> SchemaPlan {
161 SchemaPlan::empty()
162 }
163
164 fn validation_rules(&self) -> &'static [ValidationRule] {
172 &[]
173 }
174
175 fn register_embedders(&self, _runtime: &KhiveRuntime) {}
192
193 fn register_entity_type_validator(&self, _runtime: &KhiveRuntime) {}
205
206 fn register_note_mutation_hook(&self, _runtime: &KhiveRuntime) {}
221
222 async fn warm(&self) {}
233
234 async fn dispatch(
241 &self,
242 verb: &str,
243 params: Value,
244 registry: &VerbRegistry,
245 token: &NamespaceToken,
246 ) -> Result<Value, RuntimeError>;
247}
248
249#[async_trait]
263pub trait KindHook: Send + Sync + std::fmt::Debug {
264 async fn prepare_create(
270 &self,
271 runtime: &KhiveRuntime,
272 args: &mut Value,
273 ) -> Result<(), RuntimeError>;
274
275 async fn after_create(
284 &self,
285 runtime: &KhiveRuntime,
286 id: uuid::Uuid,
287 args: &Value,
288 ) -> Result<(), RuntimeError>;
289}
290
291#[async_trait]
299pub trait PackByIdResolver: Send + Sync {
300 async fn resolve_by_id(
310 &self,
311 id: uuid::Uuid,
312 ) -> Result<Option<crate::Resolved>, crate::RuntimeError>;
313
314 async fn resolve_by_id_including_deleted(
319 &self,
320 id: uuid::Uuid,
321 ) -> Result<Option<crate::Resolved>, crate::RuntimeError> {
322 self.resolve_by_id(id).await
323 }
324
325 async fn delete_by_id(
334 &self,
335 id: uuid::Uuid,
336 hard: bool,
337 ) -> Result<serde_json::Value, crate::RuntimeError>;
338}
339
340pub struct VerbRegistryBuilder {
345 packs: Vec<Box<dyn PackRuntime>>,
346 resolvers: Vec<(String, Box<dyn PackByIdResolver>)>,
347 gate: GateRef,
348 default_namespace: String,
349 visible_namespaces: Vec<Namespace>,
357 actor_id: Option<String>,
360 event_store: Option<Arc<dyn EventStore>>,
367 dispatch_hook: Option<Arc<dyn DispatchHook>>,
373}
374
375impl VerbRegistryBuilder {
376 pub fn new() -> Self {
378 Self {
379 packs: Vec::new(),
380 resolvers: Vec::new(),
381 gate: std::sync::Arc::new(AllowAllGate),
382 default_namespace: Namespace::local().as_str().to_string(),
383 visible_namespaces: vec![],
384 actor_id: None,
385 event_store: None,
386 dispatch_hook: None,
387 }
388 }
389
390 pub fn with_visible_namespaces(&mut self, ns: Vec<Namespace>) -> &mut Self {
398 self.visible_namespaces = ns;
399 self
400 }
401
402 pub fn with_actor_id(&mut self, actor_id: Option<String>) -> &mut Self {
409 self.actor_id = actor_id;
410 self
411 }
412
413 pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
416 self.packs.push(Box::new(pack));
417 self
418 }
419
420 pub(crate) fn register_boxed(&mut self, pack: Box<dyn PackRuntime>) -> &mut Self {
427 self.packs.push(pack);
428 self
429 }
430
431 pub fn register_resolver(
436 &mut self,
437 name: impl Into<String>,
438 resolver: Box<dyn PackByIdResolver>,
439 ) -> &mut Self {
440 self.resolvers.push((name.into(), resolver));
441 self
442 }
443
444 pub fn with_gate(&mut self, gate: GateRef) -> &mut Self {
451 self.gate = gate;
452 self
453 }
454
455 pub fn with_default_namespace(&mut self, ns: impl Into<String>) -> &mut Self {
460 self.default_namespace = ns.into();
461 self
462 }
463
464 pub fn with_event_store(&mut self, store: Arc<dyn EventStore>) -> &mut Self {
473 self.event_store = Some(store);
474 self
475 }
476
477 pub fn with_dispatch_hook(&mut self, hook: Arc<dyn DispatchHook>) -> &mut Self {
488 self.dispatch_hook = Some(hook);
489 self
490 }
491
492 pub fn build(self) -> Result<VerbRegistry, RuntimeError> {
498 let packs = self.packs;
499 let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(packs.len());
500 for (idx, pack) in packs.iter().enumerate() {
501 if let Some(prev_idx) = name_to_idx.insert(pack.name(), idx) {
502 return Err(RuntimeError::PackRedeclared {
503 name: pack.name().to_string(),
504 first_idx: prev_idx,
505 second_idx: idx,
506 });
507 }
508 }
509
510 let mut missing: Vec<MissingPackDependency> = Vec::new();
511 let mut indegree = vec![0usize; packs.len()];
512 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); packs.len()];
513
514 for (idx, pack) in packs.iter().enumerate() {
515 for &requires in pack.requires() {
516 match name_to_idx.get(requires).copied() {
517 Some(dep_idx) => {
518 dependents[dep_idx].push(idx);
519 indegree[idx] += 1;
520 }
521 None => missing.push(MissingPackDependency {
522 from: pack.name().to_string(),
523 requires: requires.to_string(),
524 }),
525 }
526 }
527 }
528
529 if !missing.is_empty() {
530 return if missing.len() == 1 {
531 Err(RuntimeError::MissingPackDependency(missing.remove(0)))
532 } else {
533 Err(RuntimeError::MissingPackDependencies(
534 MissingPackDependencies { missing },
535 ))
536 };
537 }
538
539 let mut ready: VecDeque<usize> = indegree
540 .iter()
541 .enumerate()
542 .filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
543 .collect();
544 let mut ordered_indices = Vec::with_capacity(packs.len());
545
546 while let Some(idx) = ready.pop_front() {
547 ordered_indices.push(idx);
548 for &dep_idx in &dependents[idx] {
549 indegree[dep_idx] -= 1;
550 if indegree[dep_idx] == 0 {
551 ready.push_back(dep_idx);
552 }
553 }
554 }
555
556 if ordered_indices.len() != packs.len() {
557 let cycle_nodes: HashSet<usize> = indegree
558 .iter()
559 .enumerate()
560 .filter_map(|(idx, degree)| (*degree > 0).then_some(idx))
561 .collect();
562 let cycle = find_pack_dependency_cycle(&packs, &name_to_idx, &cycle_nodes);
563 return Err(RuntimeError::CircularPackDependency(
564 CircularPackDependency { cycle },
565 ));
566 }
567
568 let mut slots: Vec<Option<Box<dyn PackRuntime>>> = packs.into_iter().map(Some).collect();
569 let ordered_packs: Vec<Box<dyn PackRuntime>> = ordered_indices
570 .into_iter()
571 .map(|idx| slots[idx].take().expect("topological index must exist"))
572 .collect();
573
574 validate_unique_note_kinds(&ordered_packs)?;
575 validate_unique_verb_names(&ordered_packs)?;
576
577 let available_verbs: Vec<&'static str> = ordered_packs
578 .iter()
579 .flat_map(|p| p.handlers().iter())
580 .filter(|h| matches!(h.visibility, Visibility::Verb))
581 .map(|h| h.name)
582 .collect();
583
584 Ok(VerbRegistry {
585 packs: Arc::new(ordered_packs),
586 resolvers: Arc::new(self.resolvers),
587 gate: self.gate,
588 default_namespace: self.default_namespace,
589 visible_namespaces: self.visible_namespaces,
590 actor_id: self.actor_id,
591 event_store: self.event_store,
592 dispatch_hook: self.dispatch_hook,
593 available_verbs: Arc::new(available_verbs),
594 reference_ring: Arc::new(crate::reference_ring::ReferenceRing::new()),
595 })
596 }
597}
598
599fn validate_unique_note_kinds(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
605 let mut seen: HashMap<&str, &str> = HashMap::new();
606 for pack in packs {
607 for &kind in pack.note_kinds() {
608 if let Some(first_pack) = seen.insert(kind, pack.name()) {
609 return Err(RuntimeError::InvalidInput(format!(
610 "duplicate note kind {kind:?}: claimed by both {first_pack:?} and {:?}",
611 pack.name()
612 )));
613 }
614 }
615 }
616 Ok(())
617}
618
619fn validate_unique_verb_names(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
626 let mut seen: HashMap<&str, &str> = HashMap::new();
627 for pack in packs {
628 for handler in pack.handlers() {
629 if !matches!(handler.visibility, Visibility::Verb) {
630 continue;
631 }
632 if let Some(first_pack) = seen.insert(handler.name, pack.name()) {
633 return Err(RuntimeError::VerbCollision {
634 verb: handler.name.to_string(),
635 first_pack: first_pack.to_string(),
636 second_pack: pack.name().to_string(),
637 });
638 }
639 }
640 }
641 Ok(())
642}
643
644fn find_pack_dependency_cycle(
645 packs: &[Box<dyn PackRuntime>],
646 name_to_idx: &HashMap<&str, usize>,
647 cycle_nodes: &HashSet<usize>,
648) -> Vec<String> {
649 fn visit(
650 idx: usize,
651 packs: &[Box<dyn PackRuntime>],
652 name_to_idx: &HashMap<&str, usize>,
653 cycle_nodes: &HashSet<usize>,
654 visiting: &mut Vec<usize>,
655 visited: &mut HashSet<usize>,
656 ) -> Option<Vec<String>> {
657 if let Some(pos) = visiting.iter().position(|&seen| seen == idx) {
658 let mut cycle: Vec<String> = visiting[pos..]
659 .iter()
660 .map(|&i| packs[i].name().to_string())
661 .collect();
662 cycle.push(packs[idx].name().to_string());
663 return Some(cycle);
664 }
665 if !visited.insert(idx) {
666 return None;
667 }
668 visiting.push(idx);
669 for &req in packs[idx].requires() {
670 let Some(&dep_idx) = name_to_idx.get(req) else {
671 continue;
672 };
673 if cycle_nodes.contains(&dep_idx) {
674 if let Some(cycle) =
675 visit(dep_idx, packs, name_to_idx, cycle_nodes, visiting, visited)
676 {
677 return Some(cycle);
678 }
679 }
680 }
681 visiting.pop();
682 None
683 }
684
685 let mut visited = HashSet::new();
686 for &idx in cycle_nodes {
687 let mut visiting = Vec::new();
688 if let Some(cycle) = visit(
689 idx,
690 packs,
691 name_to_idx,
692 cycle_nodes,
693 &mut visiting,
694 &mut visited,
695 ) {
696 return cycle;
697 }
698 }
699 cycle_nodes
700 .iter()
701 .map(|&idx| packs[idx].name().to_string())
702 .collect()
703}
704
705impl Default for VerbRegistryBuilder {
706 fn default() -> Self {
707 Self::new()
708 }
709}
710
711#[derive(Clone)]
715pub struct VerbRegistry {
716 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
717 resolvers: std::sync::Arc<Vec<(String, Box<dyn PackByIdResolver>)>>,
719 gate: GateRef,
720 default_namespace: String,
721 visible_namespaces: Vec<Namespace>,
728 actor_id: Option<String>,
732 event_store: Option<Arc<dyn EventStore>>,
734 dispatch_hook: Option<Arc<dyn DispatchHook>>,
736 available_verbs: Arc<Vec<&'static str>>,
741 reference_ring: Arc<crate::reference_ring::ReferenceRing>,
746}
747
748#[derive(Debug, Clone, Default)]
763pub struct RequestIdentity {
764 pub namespace: String,
768 pub actor_id: Option<String>,
772 pub visible_namespaces: Vec<String>,
778}
779
780#[derive(Debug)]
783pub struct PackSchemaCollisionError {
784 pub pack_a: &'static str,
786 pub pack_b: &'static str,
788 pub table: String,
790}
791
792impl std::fmt::Display for PackSchemaCollisionError {
793 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
794 if self.pack_a == self.pack_b {
795 write!(
796 f,
797 "pack schema boot failure for pack {:?}: {}",
798 self.pack_a, self.table
799 )
800 } else {
801 write!(
802 f,
803 "pack schema collision: packs {:?} and {:?} both declare table {:?} \
804 on the same backend — move one pack to a separate backend or rename the table",
805 self.pack_a, self.pack_b, self.table
806 )
807 }
808 }
809}
810
811impl std::error::Error for PackSchemaCollisionError {}
812
813fn extract_table_names(stmt: &str) -> Vec<String> {
819 let normalized = stmt.split_whitespace().collect::<Vec<_>>().join(" ");
820 let upper = normalized.to_ascii_uppercase();
821 let table_name = if let Some(rest) = upper.strip_prefix("CREATE VIRTUAL TABLE IF NOT EXISTS ") {
822 rest.split_whitespace().next()
823 } else if let Some(rest) = upper.strip_prefix("CREATE VIRTUAL TABLE ") {
824 rest.split_whitespace().next()
825 } else if let Some(rest) = upper.strip_prefix("CREATE TABLE IF NOT EXISTS ") {
826 rest.split_whitespace().next()
827 } else if let Some(rest) = upper.strip_prefix("CREATE TABLE ") {
828 rest.split_whitespace().next()
829 } else {
830 None
831 };
832 match table_name {
833 Some(name) => {
834 let clean = name.trim_matches(|c: char| c == '(' || c == ';');
835 if clean.is_empty() {
836 vec![]
837 } else {
838 vec![clean.to_ascii_lowercase()]
839 }
840 }
841 None => vec![],
842 }
843}
844
845impl VerbRegistry {
846 pub fn default_namespace(&self) -> &str {
852 &self.default_namespace
853 }
854
855 pub fn actor_id(&self) -> Option<&str> {
859 self.actor_id.as_deref()
860 }
861
862 pub fn visible_namespaces(&self) -> &[Namespace] {
866 &self.visible_namespaces
867 }
868
869 pub fn event_store(&self) -> Option<Arc<dyn EventStore>> {
878 self.event_store.clone()
879 }
880
881 pub fn describe_verb(&self, verb: &str) -> Result<Value, RuntimeError> {
888 for pack in self.packs.iter() {
889 for handler in pack.handlers().iter() {
890 if handler.name == verb {
891 let category = format!("{:?}", handler.category);
892 let params_arr: Vec<Value> = handler
893 .params
894 .iter()
895 .map(|p| {
896 serde_json::json!({
897 "name": p.name,
898 "type": p.param_type,
899 "required": p.required,
900 "description": p.description,
901 })
902 })
903 .collect();
904 if matches!(handler.visibility, Visibility::Subhandler) {
909 return Ok(serde_json::json!({
910 "verb": verb,
911 "pack": pack.name(),
912 "description": handler.description,
913 "category": category,
914 "params": params_arr,
915 "visibility": "internal",
916 "callable_via_mcp": false,
917 "note": "This is an internal subhandler. Calling it via the MCP \
918 request surface returns permission denied. It can only be \
919 invoked by internal runtime callers.",
920 }));
921 }
922 return Ok(serde_json::json!({
923 "verb": verb,
924 "pack": pack.name(),
925 "description": handler.description,
926 "category": category,
927 "params": params_arr,
928 }));
929 }
930 }
931 }
932 Err(RuntimeError::InvalidInput(format!(
936 "unknown verb {verb:?}; available: {}",
937 self.available_verbs.join(", ")
938 )))
939 }
940
941 pub fn authorize_namespace(&self, ns: Namespace) -> Result<(), RuntimeError> {
949 let actor = crate::actor_identity::resolve_actor(self.actor_id.as_deref());
950 let req = GateRequest::new(actor, ns, "authorize", serde_json::Value::Null);
951 match self.gate.check(&req) {
952 Ok(decision) if decision.is_allow() => Ok(()),
953 Ok(GateDecision::Deny { reason }) => Err(RuntimeError::PermissionDenied {
954 verb: "authorize".to_string(),
955 reason,
956 }),
957 Ok(_) => Err(RuntimeError::PermissionDenied {
958 verb: "authorize".to_string(),
959 reason: "gate denied".to_string(),
960 }),
961 Err(e) => Err(RuntimeError::Internal(format!("gate error: {e}"))),
962 }
963 }
964
965 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
975 self.dispatch_with_identity(verb, params, None).await
976 }
977
978 pub async fn dispatch_with_identity(
992 &self,
993 verb: &str,
994 params: Value,
995 identity: Option<RequestIdentity>,
996 ) -> Result<Value, RuntimeError> {
997 if params.get("help").and_then(Value::as_bool) == Some(true) {
999 return self.describe_verb(verb);
1000 }
1001 let explicit_namespace = params.get("namespace").is_some_and(Value::is_string);
1012 let default_namespace_str: &str = identity
1015 .as_ref()
1016 .map(|id| id.namespace.as_str())
1017 .unwrap_or(self.default_namespace.as_str());
1018 let ns = resolve_explicit_namespace(¶ms, default_namespace_str)?;
1019 let actor_id_str: Option<&str> = match identity.as_ref() {
1020 Some(id) => id.actor_id.as_deref(),
1021 None => self.actor_id.as_deref(),
1022 };
1023 let resolved_actor = crate::actor_identity::resolve_actor(actor_id_str);
1029 let gate_req = GateRequest::new(resolved_actor.clone(), ns.clone(), verb, params.clone());
1030
1031 let (gate_blocked, mut deferred_audit) = match self.gate.check(&gate_req) {
1037 Ok(decision) => {
1038 let is_deny = matches!(decision, GateDecision::Deny { .. });
1039
1040 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
1042 tracing::info!(
1043 audit_event = %serde_json::to_string(&audit)
1044 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
1045 "gate.check"
1046 );
1047
1048 if let Some(store) = &self.event_store {
1057 if crate::config_ledger::PENDING
1058 .swap(false, std::sync::atomic::Ordering::AcqRel)
1059 {
1060 for (key, value) in crate::config_ledger::drain_config_locked() {
1061 let payload = serde_json::json!({ "key": key, "value": value });
1062 let storage_event = Event::new(
1063 gate_req.namespace.as_str(),
1064 verb,
1065 EventKind::ConfigLocked,
1066 SubstrateKind::Event,
1067 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
1068 )
1069 .with_payload(payload);
1070 append_audit_event_best_effort(store, storage_event, verb).await;
1071 }
1072 }
1073 }
1074
1075 let defer_audit = !is_deny;
1090
1091 if !defer_audit {
1093 if let Some(store) = &self.event_store {
1094 let storage_event =
1095 build_audit_storage_event(&gate_req, &audit, EventOutcome::Denied);
1096 append_audit_event_best_effort(store, storage_event, verb).await;
1097 }
1098 }
1099
1100 let reason = if is_deny {
1101 let reason = match decision {
1102 GateDecision::Deny { reason } => reason,
1103 _ => String::new(),
1104 };
1105 Some(reason)
1106 } else {
1107 None
1108 };
1109 let deferred = if defer_audit { Some(audit) } else { None };
1110 (reason, deferred)
1111 }
1112 Err(err) => {
1113 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
1116 (None, None)
1117 }
1118 };
1119
1120 if let Some(reason) = gate_blocked {
1122 return Err(RuntimeError::PermissionDenied {
1123 verb: verb.to_string(),
1124 reason,
1125 });
1126 }
1127
1128 let token = if explicit_namespace {
1154 NamespaceToken::mint_with_visibility(ns.clone(), vec![], resolved_actor)
1156 } else {
1157 let primary = Namespace::local();
1159 let mut extra_visible: Vec<Namespace> = match identity.as_ref() {
1160 Some(id) => id
1161 .visible_namespaces
1162 .iter()
1163 .filter_map(|s| match Namespace::parse(s) {
1164 Ok(parsed) => Some(parsed),
1165 Err(e) => {
1166 tracing::warn!(
1167 namespace = %s,
1168 error = %e,
1169 "dispatch_with_identity: skipping invalid visible_namespace \
1170 entry from per-request identity"
1171 );
1172 None
1173 }
1174 })
1175 .collect(),
1176 None => self.visible_namespaces.clone(),
1177 };
1178 extra_visible.push(Namespace::local()); NamespaceToken::mint_with_visibility(primary, extra_visible, resolved_actor)
1180 };
1181
1182 for pack in self.packs.iter() {
1183 if let Some(handler_def) = pack.handlers().iter().find(|v| v.name == verb) {
1184 let handler_accepts_namespace =
1194 handler_def.params.iter().any(|p| p.name == "namespace");
1195 let params = if !handler_accepts_namespace {
1196 if let Value::Object(mut map) = params {
1197 map.remove("namespace");
1198 Value::Object(map)
1199 } else {
1200 params
1201 }
1202 } else {
1203 params
1204 };
1205 let dispatch_start = Instant::now();
1206 let result = pack.dispatch(verb, params, self, &token).await;
1207 let dispatch_us = dispatch_start.elapsed().as_micros() as i64;
1208
1209 if let Some(audit) = deferred_audit.take() {
1218 if let Some(store) = &self.event_store {
1219 let is_link_singleton =
1220 verb == "link" && gate_req.args.get("links").is_none();
1221 match &result {
1222 Ok(ok_val) if is_link_singleton => {
1223 match link_audit_success_from_result(audit.clone(), ok_val) {
1224 Some((edge_id, payload)) => {
1225 let storage_event = Event::new(
1226 gate_req.namespace.as_str(),
1227 gate_req.verb.as_str(),
1228 EventKind::Audit,
1229 SubstrateKind::Event,
1230 format!(
1231 "{}:{}",
1232 gate_req.actor.kind, gate_req.actor.id
1233 ),
1234 )
1235 .with_outcome(EventOutcome::Success)
1236 .with_target(edge_id)
1237 .with_payload(payload)
1238 .with_payload_schema_version(2)
1239 .with_duration_us(dispatch_us);
1240 append_audit_event_best_effort(store, storage_event, verb)
1241 .await;
1242 }
1243 None => {
1244 tracing::warn!(
1245 verb,
1246 "link audit v2 enrichment parse failed; \
1247 falling back to v1 audit shape"
1248 );
1249 let storage_event = build_audit_storage_event(
1250 &gate_req,
1251 &audit,
1252 EventOutcome::Success,
1253 )
1254 .with_duration_us(dispatch_us);
1255 append_audit_event_best_effort(store, storage_event, verb)
1256 .await;
1257 }
1258 }
1259 }
1260 _ => {
1261 let outcome = if result.is_ok() {
1267 EventOutcome::Success
1268 } else {
1269 EventOutcome::Error
1270 };
1271 let storage_event =
1272 build_audit_storage_event(&gate_req, &audit, outcome)
1273 .with_duration_us(dispatch_us);
1274 append_audit_event_best_effort(store, storage_event, verb).await;
1275 }
1276 }
1277 }
1278 }
1279
1280 if let (Ok(ref ok_val), Some(hook)) = (&result, &self.dispatch_hook) {
1282 let mut dispatch_event = Event::new(
1283 ns.as_str(),
1284 verb,
1285 EventKind::Audit,
1286 SubstrateKind::Event,
1287 pack.name(),
1288 )
1289 .with_outcome(EventOutcome::Success)
1290 .with_duration_us(dispatch_us);
1291
1292 if verb == "memory.recall" {
1296 let first_note_id = ok_val
1297 .as_array()
1298 .and_then(|arr| arr.first())
1299 .and_then(|v| v.get("id"))
1300 .and_then(|v| v.as_str())
1301 .and_then(|s| s.parse::<uuid::Uuid>().ok());
1302 if let Some(note_id) = first_note_id {
1303 dispatch_event = dispatch_event.with_target(note_id);
1304 }
1305 }
1308
1309 let dispatch_view = EventView {
1310 event: dispatch_event,
1311 observations: Vec::new(),
1312 };
1313 let hook = Arc::clone(hook);
1314 hook.on_dispatch(&dispatch_view).await;
1315 }
1316
1317 if let Ok(ref ok_val) = result {
1333 let admissions = crate::reference_ring::ring_admissions_for(verb, ok_val);
1334 if !admissions.is_empty() {
1335 let actor_key = format!("{}:{}", gate_req.actor.kind, gate_req.actor.id);
1336 for (id, name) in admissions {
1337 self.reference_ring.admit(
1338 token.namespace().as_str(),
1339 &actor_key,
1340 id,
1341 name,
1342 );
1343 }
1344 }
1345 }
1346
1347 return result;
1348 }
1349 }
1350
1351 if let Some(audit) = deferred_audit.take() {
1357 if let Some(store) = &self.event_store {
1358 let storage_event =
1362 build_audit_storage_event(&gate_req, &audit, EventOutcome::Error);
1363 append_audit_event_best_effort(store, storage_event, verb).await;
1364 }
1365 }
1366
1367 Err(RuntimeError::InvalidInput(format!(
1371 "unknown verb {verb:?}; available: {}",
1372 self.available_verbs.join(", ")
1373 )))
1374 }
1375
1376 pub fn resolvers(&self) -> &[(String, Box<dyn PackByIdResolver>)] {
1382 &self.resolvers
1383 }
1384
1385 pub fn reference_ring(&self) -> &Arc<crate::reference_ring::ReferenceRing> {
1390 &self.reference_ring
1391 }
1392
1393 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
1400 for pack in self.packs.iter() {
1401 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
1402 if owns {
1403 if let Some(hook) = pack.kind_hook(kind) {
1404 return Some(hook);
1405 }
1406 }
1407 }
1408 None
1409 }
1410
1411 pub fn all_verbs(&self) -> Vec<&'static HandlerDef> {
1417 self.packs
1418 .iter()
1419 .flat_map(|p| p.handlers().iter())
1420 .filter(|h| matches!(h.visibility, Visibility::Verb))
1421 .collect()
1422 }
1423
1424 pub fn all_verbs_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
1431 self.packs
1432 .iter()
1433 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
1434 .filter(|(_, h)| matches!(h.visibility, Visibility::Verb))
1435 .collect()
1436 }
1437
1438 pub fn all_handlers_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
1444 self.packs
1445 .iter()
1446 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
1447 .collect()
1448 }
1449
1450 pub fn all_note_kinds(&self) -> Vec<&'static str> {
1453 let mut seen = std::collections::HashSet::new();
1454 self.packs
1455 .iter()
1456 .flat_map(|p| p.note_kinds().iter().copied())
1457 .filter(|k| seen.insert(*k))
1458 .collect()
1459 }
1460
1461 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
1464 let mut seen = std::collections::HashSet::new();
1465 self.packs
1466 .iter()
1467 .flat_map(|p| p.entity_kinds().iter().copied())
1468 .filter(|k| seen.insert(*k))
1469 .collect()
1470 }
1471
1472 pub fn pack_names(&self) -> Vec<&str> {
1474 self.packs.iter().map(|p| p.name()).collect()
1475 }
1476
1477 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
1479 self.packs
1480 .iter()
1481 .find(|p| p.name() == name)
1482 .map(|p| p.requires())
1483 }
1484
1485 pub fn pack_note_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
1490 self.packs
1491 .iter()
1492 .find(|p| p.name() == name)
1493 .map(|p| p.note_kinds())
1494 }
1495
1496 pub fn pack_entity_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
1501 self.packs
1502 .iter()
1503 .find(|p| p.name() == name)
1504 .map(|p| p.entity_kinds())
1505 }
1506
1507 pub fn pack_verbs(&self, name: &str) -> Option<&'static [HandlerDef]> {
1512 self.packs
1513 .iter()
1514 .find(|p| p.name() == name)
1515 .map(|p| p.handlers())
1516 }
1517
1518 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
1524 self.packs
1525 .iter()
1526 .flat_map(|p| p.edge_rules().iter().copied())
1527 .collect()
1528 }
1529
1530 pub fn all_note_kind_specs(&self) -> Vec<&'static NoteKindSpec> {
1534 self.packs
1535 .iter()
1536 .flat_map(|p| p.note_kind_specs().iter())
1537 .collect()
1538 }
1539
1540 pub fn all_validation_rules(&self) -> Vec<&'static ValidationRule> {
1546 self.packs
1547 .iter()
1548 .flat_map(|p| p.validation_rules().iter())
1549 .collect()
1550 }
1551
1552 pub fn all_schema_plans(&self) -> Vec<SchemaPlan> {
1559 self.packs.iter().map(|p| p.schema_plan()).collect()
1560 }
1561
1562 pub fn call_register_embedders(&self, runtime: &KhiveRuntime) {
1572 for pack in self.packs.iter() {
1573 pack.register_embedders(runtime);
1574 }
1575 }
1576
1577 pub fn call_register_entity_type_validators(&self, runtime: &KhiveRuntime) {
1587 for pack in self.packs.iter() {
1588 pack.register_entity_type_validator(runtime);
1589 }
1590 }
1591
1592 pub fn call_register_note_mutation_hooks(&self, runtime: &KhiveRuntime) {
1603 for pack in self.packs.iter() {
1604 pack.register_note_mutation_hook(runtime);
1605 }
1606 }
1607
1608 pub async fn call_warm_all(&self) {
1612 for pack in self.packs.iter() {
1613 pack.warm().await;
1614 }
1615 }
1616
1617 pub fn presentation_policy_for(&self, verb: &str) -> khive_types::VerbPresentationPolicy {
1624 for pack in self.packs.iter() {
1625 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1626 return handler.presentation_policy();
1627 }
1628 }
1629 khive_types::VerbPresentationPolicy::Standard
1630 }
1631
1632 pub fn is_subhandler_verb(&self, verb: &str) -> bool {
1639 for pack in self.packs.iter() {
1640 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1641 return matches!(handler.visibility, Visibility::Subhandler);
1642 }
1643 }
1644 false
1645 }
1646
1647 pub fn apply_schema_plans(&self, backend: &khive_db::StorageBackend) {
1659 for plan in self.all_schema_plans() {
1660 if plan.is_empty() {
1661 continue;
1662 }
1663 if let Err(e) = backend.apply_pack_ddl_statements(plan.statements) {
1664 tracing::warn!(
1665 pack = plan.pack,
1666 error = %e,
1667 "failed to apply pack schema plan at startup (non-fatal)"
1668 );
1669 }
1670 }
1671 }
1672
1673 pub fn all_schema_plans_named(&self) -> Vec<(&'static str, SchemaPlan)> {
1679 self.packs
1680 .iter()
1681 .map(|p| {
1682 let plan = p.schema_plan();
1683 (plan.pack, plan)
1684 })
1685 .collect()
1686 }
1687
1688 pub fn apply_schema_plans_with_map(
1701 &self,
1702 backend_for_pack: &HashMap<&str, &khive_db::StorageBackend>,
1703 default_backend: &khive_db::StorageBackend,
1704 ) -> Result<(), crate::PackSchemaCollisionError> {
1705 let mut claimed: HashMap<(*const (), String), &'static str> = HashMap::new();
1708
1709 for (pack_name, plan) in self.all_schema_plans_named() {
1710 if plan.is_empty() {
1711 continue;
1712 }
1713 let backend = backend_for_pack
1714 .get(pack_name)
1715 .copied()
1716 .unwrap_or(default_backend);
1717 let backend_ptr = std::sync::Arc::as_ptr(&backend.pool_arc()) as *const ();
1718
1719 for stmt in plan.statements {
1721 for table_name in extract_table_names(stmt) {
1722 let key = (backend_ptr, table_name.clone());
1723 match claimed.entry(key) {
1724 std::collections::hash_map::Entry::Vacant(e) => {
1725 e.insert(pack_name);
1726 }
1727 std::collections::hash_map::Entry::Occupied(e) => {
1728 let prior_pack = *e.get();
1729 return Err(crate::PackSchemaCollisionError {
1730 pack_a: prior_pack,
1731 pack_b: pack_name,
1732 table: table_name,
1733 });
1734 }
1735 }
1736 }
1737 }
1738
1739 backend
1740 .apply_pack_ddl_statements(plan.statements)
1741 .map_err(|e| crate::PackSchemaCollisionError {
1742 pack_a: pack_name,
1743 pack_b: pack_name,
1744 table: format!("DDL error: {e}"),
1745 })?;
1746 }
1747 Ok(())
1748 }
1749}
1750
1751pub struct PackInstall {
1759 pub runtime: Box<dyn PackRuntime>,
1761 pub resolver: Option<Box<dyn PackByIdResolver>>,
1763 pub dispatch_hook: Option<Arc<dyn DispatchHook>>,
1765}
1766
1767pub trait PackFactory: Send + Sync + 'static {
1775 fn name(&self) -> &'static str;
1777
1778 fn requires(&self) -> &'static [&'static str] {
1785 &[]
1786 }
1787
1788 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime>;
1790
1791 fn create_install(&self, runtime: KhiveRuntime) -> PackInstall {
1800 let resolver = self.create_resolver(runtime.clone());
1801 PackInstall {
1802 runtime: self.create(runtime),
1803 resolver,
1804 dispatch_hook: None,
1805 }
1806 }
1807
1808 fn create_resolver(&self, _runtime: KhiveRuntime) -> Option<Box<dyn PackByIdResolver>> {
1814 None
1815 }
1816}
1817
1818pub struct PackRegistration(pub &'static dyn PackFactory);
1822
1823inventory::collect!(PackRegistration);
1824
1825#[derive(Debug)]
1827pub enum PackLoadError {
1828 UnknownPack(String),
1830 MissingDependency {
1832 pack: String,
1834 dep: String,
1836 },
1837}
1838
1839impl std::fmt::Display for PackLoadError {
1840 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1841 match self {
1842 PackLoadError::UnknownPack(name) => write!(f, "unknown pack {name:?}"),
1843 PackLoadError::MissingDependency { pack, dep } => write!(
1844 f,
1845 "pack {pack:?} requires {dep:?}, which is not in the requested pack list; \
1846 add --pack {dep} before --pack {pack}"
1847 ),
1848 }
1849 }
1850}
1851
1852impl std::error::Error for PackLoadError {}
1853
1854pub struct PackRegistry;
1859
1860impl PackRegistry {
1861 pub fn discovered_names() -> Vec<&'static str> {
1863 inventory::iter::<PackRegistration>
1864 .into_iter()
1865 .map(|r| r.0.name())
1866 .collect()
1867 }
1868
1869 pub fn register_packs(
1882 names: &[String],
1883 runtime: KhiveRuntime,
1884 builder: &mut VerbRegistryBuilder,
1885 ) -> Result<(), PackLoadError> {
1886 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
1888 .into_iter()
1889 .map(|r| r.0)
1890 .collect();
1891 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
1892 all.iter().copied().find(|f| f.name() == name)
1893 };
1894
1895 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
1897 for name in names {
1898 factory_for(name.as_str()).ok_or_else(|| PackLoadError::UnknownPack(name.clone()))?;
1899 }
1900
1901 for name in names {
1904 let factory = factory_for(name.as_str()).unwrap(); for &dep in factory.requires() {
1906 if !requested.contains(dep) {
1907 return Err(PackLoadError::MissingDependency {
1908 pack: name.clone(),
1909 dep: dep.to_string(),
1910 });
1911 }
1912 }
1913 }
1914
1915 for name in names {
1918 let factory = factory_for(name.as_str()).unwrap(); let install = factory.create_install(runtime.clone());
1920 builder.register_boxed(install.runtime);
1921 if let Some(resolver) = install.resolver {
1922 builder.register_resolver(name.clone(), resolver);
1923 }
1924 if let Some(hook) = install.dispatch_hook {
1925 builder.with_dispatch_hook(hook);
1926 }
1927 }
1928
1929 Ok(())
1930 }
1931
1932 pub fn register_packs_with_runtimes(
1942 names: &[String],
1943 runtimes: &HashMap<String, KhiveRuntime>,
1944 default_runtime: &KhiveRuntime,
1945 builder: &mut VerbRegistryBuilder,
1946 ) -> Result<(), PackLoadError> {
1947 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
1948 .into_iter()
1949 .map(|r| r.0)
1950 .collect();
1951 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
1952 all.iter().copied().find(|f| f.name() == name)
1953 };
1954
1955 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
1956 for name in names {
1957 factory_for(name.as_str()).ok_or_else(|| PackLoadError::UnknownPack(name.clone()))?;
1958 }
1959
1960 for name in names {
1961 let factory = factory_for(name.as_str()).unwrap();
1962 for &dep in factory.requires() {
1963 if !requested.contains(dep) {
1964 return Err(PackLoadError::MissingDependency {
1965 pack: name.clone(),
1966 dep: dep.to_string(),
1967 });
1968 }
1969 }
1970 }
1971
1972 for name in names {
1973 let factory = factory_for(name.as_str()).unwrap();
1974 let runtime = runtimes
1975 .get(name.as_str())
1976 .cloned()
1977 .unwrap_or_else(|| default_runtime.clone());
1978 let install = factory.create_install(runtime);
1979 builder.register_boxed(install.runtime);
1980 if let Some(resolver) = install.resolver {
1981 builder.register_resolver(name.clone(), resolver);
1982 }
1983 if let Some(hook) = install.dispatch_hook {
1984 builder.with_dispatch_hook(hook);
1985 }
1986 }
1987
1988 Ok(())
1989 }
1990}
1991
1992fn target_id_from_args(args: &serde_json::Value) -> Option<uuid::Uuid> {
1993 args.get("target_id")
1994 .and_then(serde_json::Value::as_str)
1995 .and_then(|s| s.parse::<uuid::Uuid>().ok())
1996}
1997
1998fn build_audit_storage_event(
2004 gate_req: &GateRequest,
2005 audit: &AuditEvent,
2006 outcome: EventOutcome,
2007) -> Event {
2008 let audit_data = serde_json::to_value(audit).unwrap_or_else(|e| {
2009 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
2010 serde_json::Value::Null
2011 });
2012 let mut storage_event = Event::new(
2013 gate_req.namespace.as_str(),
2014 gate_req.verb.as_str(),
2015 EventKind::Audit,
2016 SubstrateKind::Event,
2017 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
2018 )
2019 .with_outcome(outcome)
2020 .with_payload(audit_data);
2021 if let Some(target_id) = target_id_from_args(&gate_req.args) {
2022 storage_event = storage_event.with_target(target_id);
2023 }
2024 storage_event
2025}
2026
2027async fn append_audit_event_best_effort(store: &Arc<dyn EventStore>, event: Event, verb: &str) {
2032 if let Err(store_err) = store.append_event(event).await {
2033 tracing::warn!(
2034 verb,
2035 error = %store_err,
2036 "audit event store write failed (non-fatal)"
2037 );
2038 }
2039}
2040
2041#[derive(Debug, Clone, serde::Serialize)]
2047struct LinkAuditSuccessV2 {
2048 #[serde(flatten)]
2049 audit: AuditEvent,
2050 edge_id: uuid::Uuid,
2051 source_id: uuid::Uuid,
2052 target_id: uuid::Uuid,
2053 relation: String,
2054 weight: f64,
2055}
2056
2057fn link_audit_success_from_result(
2064 audit: AuditEvent,
2065 result: &serde_json::Value,
2066) -> Option<(uuid::Uuid, serde_json::Value)> {
2067 let edge_id = result.get("id")?.as_str()?.parse::<uuid::Uuid>().ok()?;
2068 let source_id = result
2069 .get("source_id")?
2070 .as_str()?
2071 .parse::<uuid::Uuid>()
2072 .ok()?;
2073 let target_id = result
2074 .get("target_id")?
2075 .as_str()?
2076 .parse::<uuid::Uuid>()
2077 .ok()?;
2078 let relation = result.get("relation")?.as_str()?.to_string();
2079 let weight = result.get("weight")?.as_f64()?;
2080 let enriched = LinkAuditSuccessV2 {
2081 audit,
2082 edge_id,
2083 source_id,
2084 target_id,
2085 relation,
2086 weight,
2087 };
2088 let payload = serde_json::to_value(&enriched).ok()?;
2089 Some((edge_id, payload))
2090}
2091
2092pub fn resolve_explicit_namespace(
2107 params: &Value,
2108 default_namespace: &str,
2109) -> Result<Namespace, RuntimeError> {
2110 match params.get("namespace") {
2111 None => Namespace::parse(default_namespace)
2112 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}"))),
2113 Some(Value::String(ns_str)) => Namespace::parse(ns_str)
2114 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace {ns_str:?}: {e}"))),
2115 Some(other) => Err(RuntimeError::InvalidInput(format!(
2116 "invalid namespace: expected string when present, got {}",
2117 json_type_name(other),
2118 ))),
2119 }
2120}
2121
2122pub fn json_type_name(v: &Value) -> &'static str {
2125 match v {
2126 Value::Null => "null",
2127 Value::Bool(_) => "boolean",
2128 Value::Number(_) => "number",
2129 Value::String(_) => "string",
2130 Value::Array(_) => "array",
2131 Value::Object(_) => "object",
2132 }
2133}
2134
2135#[cfg(test)]
2141mod tests {
2142 use super::*;
2143 use crate::ActorRef;
2144 use khive_types::Pack;
2145
2146 struct AlphaPack;
2147
2148 impl Pack for AlphaPack {
2149 const NAME: &'static str = "alpha";
2150 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
2151 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
2152 const HANDLERS: &'static [HandlerDef] = &[
2153 HandlerDef {
2154 name: "create",
2155 description: "create a widget",
2156 visibility: Visibility::Verb,
2157 category: VerbCategory::Commissive,
2158 params: &[],
2159 },
2160 HandlerDef {
2161 name: "list",
2162 description: "list widgets",
2163 visibility: Visibility::Verb,
2164 category: VerbCategory::Assertive,
2165 params: &[],
2166 },
2167 ];
2168 }
2169
2170 #[async_trait]
2171 impl PackRuntime for AlphaPack {
2172 fn name(&self) -> &str {
2173 AlphaPack::NAME
2174 }
2175 fn note_kinds(&self) -> &'static [&'static str] {
2176 AlphaPack::NOTE_KINDS
2177 }
2178 fn entity_kinds(&self) -> &'static [&'static str] {
2179 AlphaPack::ENTITY_KINDS
2180 }
2181 fn handlers(&self) -> &'static [HandlerDef] {
2182 AlphaPack::HANDLERS
2183 }
2184 async fn dispatch(
2185 &self,
2186 verb: &str,
2187 _params: Value,
2188 _registry: &VerbRegistry,
2189 _token: &NamespaceToken,
2190 ) -> Result<Value, RuntimeError> {
2191 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
2192 }
2193 }
2194
2195 struct SleepingPack;
2199
2200 impl Pack for SleepingPack {
2201 const NAME: &'static str = "sleeping";
2202 const NOTE_KINDS: &'static [&'static str] = &[];
2203 const ENTITY_KINDS: &'static [&'static str] = &[];
2204 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2205 name: "slow_op",
2206 description: "sleeps before returning",
2207 visibility: Visibility::Verb,
2208 category: VerbCategory::Assertive,
2209 params: &[],
2210 }];
2211 }
2212
2213 #[async_trait]
2214 impl PackRuntime for SleepingPack {
2215 fn name(&self) -> &str {
2216 SleepingPack::NAME
2217 }
2218 fn note_kinds(&self) -> &'static [&'static str] {
2219 SleepingPack::NOTE_KINDS
2220 }
2221 fn entity_kinds(&self) -> &'static [&'static str] {
2222 SleepingPack::ENTITY_KINDS
2223 }
2224 fn handlers(&self) -> &'static [HandlerDef] {
2225 SleepingPack::HANDLERS
2226 }
2227 async fn dispatch(
2228 &self,
2229 verb: &str,
2230 _params: Value,
2231 _registry: &VerbRegistry,
2232 _token: &NamespaceToken,
2233 ) -> Result<Value, RuntimeError> {
2234 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2235 Ok(serde_json::json!({ "pack": "sleeping", "verb": verb }))
2236 }
2237 }
2238
2239 struct BetaPack;
2240
2241 impl Pack for BetaPack {
2242 const NAME: &'static str = "beta";
2243 const NOTE_KINDS: &'static [&'static str] = &["alert"];
2244 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
2245 const HANDLERS: &'static [HandlerDef] = &[
2246 HandlerDef {
2247 name: "notify",
2248 description: "send alert",
2249 visibility: Visibility::Verb,
2250 category: VerbCategory::Commissive,
2251 params: &[],
2252 },
2253 HandlerDef {
2257 name: "create",
2258 description: "beta internal create (subhandler)",
2259 visibility: Visibility::Subhandler,
2260 category: VerbCategory::Commissive,
2261 params: &[],
2262 },
2263 ];
2264 }
2265
2266 fn build_registry() -> VerbRegistry {
2272 let mut builder = VerbRegistryBuilder::new();
2273 builder.register(AlphaPack);
2274 builder.register(BetaPack);
2275 builder.build().expect("registry builds without collision")
2276 }
2277
2278 struct CollidingPack;
2281
2282 impl Pack for CollidingPack {
2283 const NAME: &'static str = "colliding";
2284 const NOTE_KINDS: &'static [&'static str] = &[];
2285 const ENTITY_KINDS: &'static [&'static str] = &[];
2286 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2287 name: "create",
2288 description: "duplicate Verb-visibility create",
2289 visibility: Visibility::Verb,
2290 category: VerbCategory::Commissive,
2291 params: &[],
2292 }];
2293 }
2294
2295 #[async_trait]
2296 impl PackRuntime for CollidingPack {
2297 fn name(&self) -> &str {
2298 Self::NAME
2299 }
2300 fn note_kinds(&self) -> &'static [&'static str] {
2301 Self::NOTE_KINDS
2302 }
2303 fn entity_kinds(&self) -> &'static [&'static str] {
2304 Self::ENTITY_KINDS
2305 }
2306 fn handlers(&self) -> &'static [HandlerDef] {
2307 Self::HANDLERS
2308 }
2309 async fn dispatch(
2310 &self,
2311 verb: &str,
2312 _params: Value,
2313 _registry: &VerbRegistry,
2314 _token: &NamespaceToken,
2315 ) -> Result<Value, RuntimeError> {
2316 Ok(serde_json::json!({ "pack": "colliding", "verb": verb }))
2317 }
2318 }
2319
2320 #[async_trait]
2321 impl PackRuntime for BetaPack {
2322 fn name(&self) -> &str {
2323 BetaPack::NAME
2324 }
2325 fn note_kinds(&self) -> &'static [&'static str] {
2326 BetaPack::NOTE_KINDS
2327 }
2328 fn entity_kinds(&self) -> &'static [&'static str] {
2329 BetaPack::ENTITY_KINDS
2330 }
2331 fn handlers(&self) -> &'static [HandlerDef] {
2332 BetaPack::HANDLERS
2333 }
2334 async fn dispatch(
2335 &self,
2336 verb: &str,
2337 _params: Value,
2338 _registry: &VerbRegistry,
2339 _token: &NamespaceToken,
2340 ) -> Result<Value, RuntimeError> {
2341 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
2342 }
2343 }
2344
2345 #[tokio::test]
2346 async fn dispatch_routes_to_correct_pack() {
2347 let reg = build_registry();
2348
2349 let res = reg.dispatch("list", Value::Null).await.unwrap();
2350 assert_eq!(res["pack"], "alpha");
2351
2352 let res = reg.dispatch("notify", Value::Null).await.unwrap();
2353 assert_eq!(res["pack"], "beta");
2354 }
2355
2356 #[test]
2360 fn verb_collision_is_boot_time_error() {
2361 let mut builder = VerbRegistryBuilder::new();
2362 builder.register(AlphaPack);
2363 builder.register(CollidingPack);
2364 let err = builder
2365 .build()
2366 .err()
2367 .expect("duplicate Verb-visibility handler must be rejected at build time");
2368 assert!(
2369 matches!(err, RuntimeError::VerbCollision { ref verb, .. } if verb == "create"),
2370 "expected VerbCollision for 'create', got {err:?}"
2371 );
2372 let msg = err.to_string();
2373 assert!(
2374 msg.contains("create"),
2375 "error must name the colliding verb: {msg}"
2376 );
2377 assert!(
2378 msg.contains("alpha") || msg.contains("colliding"),
2379 "error must name one of the conflicting packs: {msg}"
2380 );
2381 }
2382
2383 #[test]
2387 fn subhandler_same_name_across_packs_is_not_a_collision() {
2388 struct SubhandlerPack;
2389 impl Pack for SubhandlerPack {
2390 const NAME: &'static str = "subhandler_pack";
2391 const NOTE_KINDS: &'static [&'static str] = &[];
2392 const ENTITY_KINDS: &'static [&'static str] = &[];
2393 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2394 name: "create",
2395 description: "internal create",
2396 visibility: Visibility::Subhandler,
2397 category: VerbCategory::Commissive,
2398 params: &[],
2399 }];
2400 }
2401 #[async_trait]
2402 impl PackRuntime for SubhandlerPack {
2403 fn name(&self) -> &str {
2404 Self::NAME
2405 }
2406 fn note_kinds(&self) -> &'static [&'static str] {
2407 Self::NOTE_KINDS
2408 }
2409 fn entity_kinds(&self) -> &'static [&'static str] {
2410 Self::ENTITY_KINDS
2411 }
2412 fn handlers(&self) -> &'static [HandlerDef] {
2413 Self::HANDLERS
2414 }
2415 async fn dispatch(
2416 &self,
2417 verb: &str,
2418 _: Value,
2419 _: &VerbRegistry,
2420 _: &NamespaceToken,
2421 ) -> Result<Value, RuntimeError> {
2422 Ok(serde_json::json!({"pack": "subhandler_pack", "verb": verb}))
2423 }
2424 }
2425 let mut builder = VerbRegistryBuilder::new();
2426 builder.register(AlphaPack); builder.register(SubhandlerPack); builder
2429 .build()
2430 .expect("subhandler same name must NOT be a collision");
2431 }
2432
2433 #[tokio::test]
2434 async fn dispatch_unknown_verb_returns_error() {
2435 let reg = build_registry();
2436
2437 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
2438 let msg = err.to_string();
2439 assert!(msg.contains("explode"));
2440 assert!(msg.contains("create"));
2441 }
2442
2443 #[test]
2448 fn all_verbs_aggregates_across_packs_excludes_subhandlers() {
2449 let reg = build_registry();
2450 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
2451 assert_eq!(verbs, vec!["create", "list", "notify"]);
2453 }
2454
2455 #[test]
2456 fn all_verbs_with_names_pairs_pack_name_excludes_subhandlers() {
2457 let reg = build_registry();
2458 let pairs: Vec<(&str, &str)> = reg
2459 .all_verbs_with_names()
2460 .iter()
2461 .map(|(pack, v)| (*pack, v.name))
2462 .collect();
2463 assert_eq!(
2465 pairs,
2466 vec![("alpha", "create"), ("alpha", "list"), ("beta", "notify"),]
2467 );
2468 }
2469
2470 #[test]
2471 fn all_handlers_with_names_includes_subhandlers() {
2472 let reg = build_registry();
2473 let pairs: Vec<(&str, &str)> = reg
2474 .all_handlers_with_names()
2475 .iter()
2476 .map(|(pack, v)| (*pack, v.name))
2477 .collect();
2478 assert_eq!(
2480 pairs,
2481 vec![
2482 ("alpha", "create"),
2483 ("alpha", "list"),
2484 ("beta", "notify"),
2485 ("beta", "create"),
2486 ]
2487 );
2488 }
2489
2490 #[test]
2491 fn note_kinds_are_ordered() {
2492 let reg = build_registry();
2493 let kinds = reg.all_note_kinds();
2494 assert_eq!(kinds, vec!["memo", "log", "alert"]);
2495 }
2496
2497 #[test]
2498 fn note_kind_duplicate_rejected_at_build_time() {
2499 struct DupPack;
2500
2501 impl khive_types::Pack for DupPack {
2502 const NAME: &'static str = "dup";
2503 const NOTE_KINDS: &'static [&'static str] = &["memo"];
2505 const ENTITY_KINDS: &'static [&'static str] = &[];
2506 const HANDLERS: &'static [HandlerDef] = &[];
2507 }
2508
2509 #[async_trait]
2510 impl PackRuntime for DupPack {
2511 fn name(&self) -> &str {
2512 Self::NAME
2513 }
2514 fn note_kinds(&self) -> &'static [&'static str] {
2515 Self::NOTE_KINDS
2516 }
2517 fn entity_kinds(&self) -> &'static [&'static str] {
2518 Self::ENTITY_KINDS
2519 }
2520 fn handlers(&self) -> &'static [HandlerDef] {
2521 Self::HANDLERS
2522 }
2523 async fn dispatch(
2524 &self,
2525 _verb: &str,
2526 _params: Value,
2527 _registry: &VerbRegistry,
2528 _token: &NamespaceToken,
2529 ) -> Result<Value, RuntimeError> {
2530 Ok(Value::Null)
2531 }
2532 }
2533
2534 let mut builder = VerbRegistryBuilder::new();
2535 builder.register(AlphaPack);
2536 builder.register(DupPack);
2537 let err = builder
2538 .build()
2539 .err()
2540 .expect("duplicate note kind must be rejected");
2541 let msg = err.to_string();
2542 assert!(
2543 msg.contains("memo"),
2544 "error must name the duplicate kind: {msg}"
2545 );
2546 assert!(
2547 msg.contains("alpha") || msg.contains("dup"),
2548 "error must name one of the conflicting packs: {msg}"
2549 );
2550 }
2551
2552 #[test]
2553 fn entity_kinds_are_deduplicated() {
2554 let reg = build_registry();
2555 let kinds = reg.all_entity_kinds();
2556 assert_eq!(kinds, vec!["widget", "gadget"]);
2557 }
2558
2559 use khive_gate::{Gate, GateError};
2562 use std::sync::atomic::{AtomicUsize, Ordering};
2563 use std::sync::Arc;
2564
2565 #[derive(Default, Debug)]
2566 struct CountingGate {
2567 calls: AtomicUsize,
2568 deny_verb: Option<&'static str>,
2569 }
2570
2571 impl Gate for CountingGate {
2572 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2573 self.calls.fetch_add(1, Ordering::SeqCst);
2574 if Some(req.verb.as_str()) == self.deny_verb {
2575 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
2576 } else {
2577 Ok(GateDecision::allow())
2578 }
2579 }
2580 }
2581
2582 #[tokio::test]
2583 async fn dispatch_consults_the_gate() {
2584 let gate = Arc::new(CountingGate::default());
2585 let mut builder = VerbRegistryBuilder::new();
2586 builder.register(AlphaPack);
2587 builder.with_gate(gate.clone());
2588 let reg = builder.build().expect("registry builds");
2589
2590 reg.dispatch("list", Value::Null).await.unwrap();
2591 reg.dispatch("create", Value::Null).await.unwrap();
2592 assert_eq!(
2593 gate.calls.load(Ordering::SeqCst),
2594 2,
2595 "gate should be consulted once per dispatch"
2596 );
2597 }
2598
2599 #[tokio::test]
2600 async fn dispatch_returns_permission_denied_on_deny_v03() {
2601 let gate = Arc::new(CountingGate {
2602 calls: AtomicUsize::new(0),
2603 deny_verb: Some("create"),
2604 });
2605 let mut builder = VerbRegistryBuilder::new();
2606 builder.register(AlphaPack);
2607 builder.with_gate(gate.clone());
2608 let reg = builder.build().expect("registry builds");
2609
2610 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
2612 assert!(
2613 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
2614 "expected PermissionDenied, got {err:?}"
2615 );
2616 let msg = err.to_string();
2617 assert!(
2618 msg.contains("create"),
2619 "error message must name the verb: {msg}"
2620 );
2621 assert!(
2622 msg.contains("test deny for create"),
2623 "error message must carry the deny reason: {msg}"
2624 );
2625 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
2626 }
2627
2628 #[tokio::test]
2629 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
2630 let gate = Arc::new(CountingGate {
2632 calls: AtomicUsize::new(0),
2633 deny_verb: Some("create"),
2634 });
2635 let mut builder = VerbRegistryBuilder::new();
2636 builder.register(AlphaPack);
2637 builder.with_gate(gate.clone());
2638 let reg = builder.build().expect("registry builds");
2639
2640 let res = reg.dispatch("list", Value::Null).await.unwrap();
2641 assert_eq!(res["pack"], "alpha");
2642 }
2643
2644 #[tokio::test]
2645 async fn dispatch_uses_allow_all_gate_by_default() {
2646 let reg = build_registry();
2648 let res = reg.dispatch("list", Value::Null).await.unwrap();
2649 assert_eq!(res["pack"], "alpha");
2650 }
2651
2652 #[derive(Default, Debug)]
2655 struct NamespaceCapturingGate {
2656 seen: std::sync::Mutex<Vec<String>>,
2657 }
2658
2659 impl Gate for NamespaceCapturingGate {
2660 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2661 self.seen
2662 .lock()
2663 .unwrap()
2664 .push(req.namespace.as_str().to_string());
2665 Ok(GateDecision::allow())
2666 }
2667 }
2668
2669 #[tokio::test]
2670 async fn dispatch_propagates_params_namespace_to_gate() {
2671 let gate = Arc::new(NamespaceCapturingGate::default());
2672 let mut builder = VerbRegistryBuilder::new();
2673 builder.register(AlphaPack);
2674 builder.with_gate(gate.clone());
2675 builder.with_default_namespace("tenant-x");
2676 let reg = builder.build().expect("registry builds");
2677
2678 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
2680 .await
2681 .unwrap();
2682 reg.dispatch("list", Value::Null).await.unwrap();
2684 let err = reg
2686 .dispatch("list", serde_json::json!({"namespace": ""}))
2687 .await
2688 .unwrap_err();
2689 assert!(
2690 matches!(err, RuntimeError::InvalidInput(_)),
2691 "empty namespace must return InvalidInput, got {err:?}"
2692 );
2693
2694 let seen = gate.seen.lock().unwrap().clone();
2695 assert_eq!(seen, vec!["tenant-y", "tenant-x"]);
2696 }
2697
2698 #[tokio::test]
2699 async fn dispatch_falls_back_to_local_when_no_default_set() {
2700 let gate = Arc::new(NamespaceCapturingGate::default());
2702 let mut builder = VerbRegistryBuilder::new();
2703 builder.register(AlphaPack);
2704 builder.with_gate(gate.clone());
2705 let reg = builder.build().expect("registry builds");
2706
2707 reg.dispatch("list", Value::Null).await.unwrap();
2708 let seen = gate.seen.lock().unwrap().clone();
2709 assert_eq!(seen, vec!["local"]);
2710 }
2711
2712 #[tokio::test]
2718 async fn namespace_null_rejected_not_coerced() {
2719 let cases: Vec<(&str, Value)> = vec![
2720 ("null", Value::Null),
2721 ("number", serde_json::json!(42)),
2722 ("boolean", serde_json::json!(true)),
2723 ("array", serde_json::json!(["local"])),
2724 ("object", serde_json::json!({"ns": "local"})),
2725 ];
2726
2727 for (label, ns_value) in cases {
2728 let gate = Arc::new(NamespaceCapturingGate::default());
2729 let mut builder = VerbRegistryBuilder::new();
2730 builder.register(AlphaPack);
2731 builder.with_gate(gate.clone());
2732 builder.with_default_namespace("tenant-x");
2733 let reg = builder.build().expect("registry builds");
2734
2735 let err = reg
2736 .dispatch("list", serde_json::json!({"namespace": ns_value}))
2737 .await
2738 .unwrap_err();
2739 assert!(
2740 matches!(err, RuntimeError::InvalidInput(_)),
2741 "case {label}: expected InvalidInput, got {err:?}"
2742 );
2743
2744 let seen = gate.seen.lock().unwrap().clone();
2748 assert!(
2749 seen.is_empty(),
2750 "case {label}: gate must not be consulted for malformed namespace, saw {seen:?}"
2751 );
2752 }
2753 }
2754
2755 use khive_gate::{AuditDecision, AuditEvent, Obligation};
2758
2759 #[derive(Default, Debug)]
2761 struct AuditCapturingGate {
2762 events: std::sync::Mutex<Vec<AuditEvent>>,
2763 deny_verb: Option<&'static str>,
2764 }
2765
2766 impl Gate for AuditCapturingGate {
2767 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2768 let decision = if Some(req.verb.as_str()) == self.deny_verb {
2769 GateDecision::deny("test deny")
2770 } else {
2771 GateDecision::allow_with(vec![Obligation::Audit {
2772 tag: format!("{}.check", req.verb),
2773 }])
2774 };
2775 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
2777 self.events.lock().unwrap().push(ev);
2778 Ok(decision)
2779 }
2780
2781 fn impl_name(&self) -> &'static str {
2782 "AuditCapturingGate"
2783 }
2784 }
2785
2786 #[tokio::test]
2787 async fn dispatch_emits_one_audit_event_per_call() {
2788 let gate = Arc::new(AuditCapturingGate::default());
2789 let mut builder = VerbRegistryBuilder::new();
2790 builder.register(AlphaPack);
2791 builder.with_gate(gate.clone());
2792 let reg = builder.build().expect("registry builds");
2793
2794 reg.dispatch("list", Value::Null).await.unwrap();
2795 reg.dispatch("create", Value::Null).await.unwrap();
2796
2797 let evs = gate.events.lock().unwrap();
2798 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
2799 }
2800
2801 #[tokio::test]
2802 async fn dispatch_audit_event_allow_carries_obligations() {
2803 let gate = Arc::new(AuditCapturingGate::default());
2804 let mut builder = VerbRegistryBuilder::new();
2805 builder.register(AlphaPack);
2806 builder.with_gate(gate.clone());
2807 let reg = builder.build().expect("registry builds");
2808
2809 reg.dispatch("list", Value::Null).await.unwrap();
2810
2811 let evs = gate.events.lock().unwrap();
2812 let ev = &evs[0];
2813 assert_eq!(ev.verb, "list");
2814 assert_eq!(ev.decision, AuditDecision::Allow);
2815 assert!(ev.deny_reason.is_none());
2816 assert_eq!(ev.obligations.len(), 1);
2817 assert_eq!(ev.gate_impl, "AuditCapturingGate");
2818 }
2819
2820 #[tokio::test]
2821 async fn dispatch_audit_event_deny_carries_reason() {
2822 let gate = Arc::new(AuditCapturingGate {
2823 events: Default::default(),
2824 deny_verb: Some("create"),
2825 });
2826 let mut builder = VerbRegistryBuilder::new();
2827 builder.register(AlphaPack);
2828 builder.with_gate(gate.clone());
2829 let reg = builder.build().expect("registry builds");
2830
2831 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
2834 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2835
2836 let evs = gate.events.lock().unwrap();
2837 let ev = &evs[0];
2838 assert_eq!(ev.verb, "create");
2839 assert_eq!(ev.decision, AuditDecision::Deny);
2840 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
2841 assert!(ev.obligations.is_empty());
2842 }
2843
2844 #[tokio::test]
2845 async fn dispatch_audit_event_fields_match_gate_request() {
2846 let gate = Arc::new(AuditCapturingGate::default());
2847 let mut builder = VerbRegistryBuilder::new();
2848 builder.register(AlphaPack);
2849 builder.with_gate(gate.clone());
2850 builder.with_default_namespace("tenant-z");
2851 let reg = builder.build().expect("registry builds");
2852
2853 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
2854 .await
2855 .unwrap();
2856
2857 let evs = gate.events.lock().unwrap();
2858 let ev = &evs[0];
2859 assert_eq!(ev.namespace, "tenant-q");
2861 assert_eq!(ev.verb, "list");
2862 assert_eq!(ev.actor.kind, "anonymous");
2863 }
2864
2865 #[derive(Default, Debug)]
2869 struct ActorCapturingGate {
2870 requests: std::sync::Mutex<Vec<GateRequest>>,
2871 }
2872
2873 impl Gate for ActorCapturingGate {
2874 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2875 self.requests.lock().unwrap().push(req.clone());
2876 Ok(GateDecision::allow())
2877 }
2878 }
2879
2880 #[tokio::test]
2884 async fn gate_request_carries_configured_actor_when_actor_id_is_set() {
2885 let gate = Arc::new(ActorCapturingGate::default());
2886 let mut builder = VerbRegistryBuilder::new();
2887 builder.register(AlphaPack);
2888 builder.with_gate(gate.clone());
2889 builder.with_actor_id(Some("team-abc:implementer".to_string()));
2890 let reg = builder.build().expect("registry builds");
2891
2892 reg.dispatch("list", Value::Null).await.unwrap();
2893
2894 let reqs = gate.requests.lock().unwrap();
2895 assert_eq!(reqs.len(), 1);
2896 let req = &reqs[0];
2897 assert_eq!(
2898 req.actor.kind, "actor",
2899 "gate request must carry kind='actor' when actor_id is configured"
2900 );
2901 assert_eq!(
2902 req.actor.id, "team-abc:implementer",
2903 "gate request must carry the configured actor id"
2904 );
2905 }
2906
2907 #[tokio::test]
2910 async fn gate_request_carries_anonymous_when_no_actor_id_configured() {
2911 let gate = Arc::new(ActorCapturingGate::default());
2912 let mut builder = VerbRegistryBuilder::new();
2913 builder.register(AlphaPack);
2914 builder.with_gate(gate.clone());
2915 let reg = builder.build().expect("registry builds");
2917
2918 reg.dispatch("list", Value::Null).await.unwrap();
2919
2920 let reqs = gate.requests.lock().unwrap();
2921 assert_eq!(reqs.len(), 1);
2922 let req = &reqs[0];
2923 assert_eq!(
2924 req.actor.kind, "anonymous",
2925 "gate request must carry anonymous actor when no actor_id is configured"
2926 );
2927 assert_eq!(req.actor.id, "local");
2928 }
2929
2930 struct TokenCapturingPack {
2933 actors: Arc<std::sync::Mutex<Vec<khive_gate::ActorRef>>>,
2934 }
2935
2936 impl Pack for TokenCapturingPack {
2937 const NAME: &'static str = "alpha";
2938 const NOTE_KINDS: &'static [&'static str] = &[];
2939 const ENTITY_KINDS: &'static [&'static str] = &[];
2940 const HANDLERS: &'static [HandlerDef] = AlphaPack::HANDLERS;
2941 }
2942
2943 #[async_trait]
2944 impl PackRuntime for TokenCapturingPack {
2945 fn name(&self) -> &str {
2946 Self::NAME
2947 }
2948 fn note_kinds(&self) -> &'static [&'static str] {
2949 Self::NOTE_KINDS
2950 }
2951 fn entity_kinds(&self) -> &'static [&'static str] {
2952 Self::ENTITY_KINDS
2953 }
2954 fn handlers(&self) -> &'static [HandlerDef] {
2955 Self::HANDLERS
2956 }
2957 async fn dispatch(
2958 &self,
2959 verb: &str,
2960 _params: Value,
2961 _registry: &VerbRegistry,
2962 token: &NamespaceToken,
2963 ) -> Result<Value, RuntimeError> {
2964 self.actors.lock().unwrap().push(token.actor().clone());
2965 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
2966 }
2967 }
2968
2969 #[tokio::test]
2978 async fn gate_actor_and_token_actor_are_identical_when_actor_id_is_set() {
2979 let gate = Arc::new(ActorCapturingGate::default());
2980 let actors = Arc::new(std::sync::Mutex::new(Vec::new()));
2981 let pack = TokenCapturingPack {
2982 actors: actors.clone(),
2983 };
2984 let mut builder = VerbRegistryBuilder::new();
2985 builder.register(pack);
2986 builder.with_gate(gate.clone());
2987 builder.with_actor_id(Some("actor-alpha".to_string()));
2988 let reg = builder.build().expect("registry builds");
2989
2990 reg.dispatch("list", Value::Null).await.unwrap();
2991
2992 let reqs = gate.requests.lock().unwrap();
2993 let gate_actor = reqs[0].actor.clone();
2994 drop(reqs);
2995
2996 let captured = actors.lock().unwrap();
2997 let token_actor = captured[0].clone();
2998
2999 assert_eq!(
3000 gate_actor.kind, token_actor.kind,
3001 "gate request actor and storage token actor must carry the same kind"
3002 );
3003 assert_eq!(
3004 gate_actor.id, token_actor.id,
3005 "gate request actor and storage token actor must carry the same id"
3006 );
3007 assert_eq!(gate_actor.id, "actor-alpha");
3008 }
3009
3010 #[tokio::test]
3013 async fn gate_actor_and_token_actor_are_identical_when_anonymous() {
3014 let gate = Arc::new(ActorCapturingGate::default());
3015 let actors = Arc::new(std::sync::Mutex::new(Vec::new()));
3016 let pack = TokenCapturingPack {
3017 actors: actors.clone(),
3018 };
3019 let mut builder = VerbRegistryBuilder::new();
3020 builder.register(pack);
3021 builder.with_gate(gate.clone());
3022 let reg = builder.build().expect("registry builds");
3023
3024 reg.dispatch("list", Value::Null).await.unwrap();
3025
3026 let reqs = gate.requests.lock().unwrap();
3027 let gate_actor = reqs[0].actor.clone();
3028 drop(reqs);
3029
3030 let captured = actors.lock().unwrap();
3031 let token_actor = captured[0].clone();
3032
3033 assert_eq!(gate_actor.kind, token_actor.kind);
3034 assert_eq!(gate_actor.id, token_actor.id);
3035 assert_eq!(gate_actor.id, "local");
3036 }
3037
3038 #[tokio::test]
3052 async fn rego_gate_missing_entrypoint_returns_permission_denied() {
3053 use khive_gate_rego::RegoGate;
3054
3055 let policy = r#"
3061 package khive.gate
3062 import rego.v1
3063 verdict := "allow"
3064 "#;
3065 let gate = Arc::new(RegoGate::from_policy_str(policy).expect("policy compiles"));
3066
3067 let mut builder = VerbRegistryBuilder::new();
3068 builder.register(AlphaPack);
3069 builder.with_gate(gate);
3070 let reg = builder.build().expect("registry builds");
3071
3072 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
3073 assert!(
3074 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
3075 "expected PermissionDenied for missing rego entrypoint, got {err:?}"
3076 );
3077 }
3078
3079 use std::sync::{Mutex as StdMutex, Once, OnceLock};
3091
3092 use serial_test::serial;
3093 use tracing::field::{Field, Visit};
3094
3095 #[derive(Clone, Debug, Default)]
3096 struct CapturedEvent {
3097 message: Option<String>,
3098 audit_event: Option<String>,
3099 }
3100
3101 #[derive(Default)]
3102 struct CapturedEventVisitor(CapturedEvent);
3103
3104 impl Visit for CapturedEventVisitor {
3105 fn record_str(&mut self, field: &Field, value: &str) {
3106 match field.name() {
3107 "message" => self.0.message = Some(value.to_string()),
3108 "audit_event" => self.0.audit_event = Some(value.to_string()),
3109 _ => {}
3110 }
3111 }
3112
3113 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
3114 let formatted = format!("{value:?}");
3120 let cleaned = formatted
3121 .trim_start_matches('"')
3122 .trim_end_matches('"')
3123 .to_string();
3124 match field.name() {
3125 "message" => self.0.message = Some(cleaned),
3126 "audit_event" => self.0.audit_event = Some(cleaned),
3127 _ => {}
3128 }
3129 }
3130 }
3131
3132 struct CaptureSubscriber {
3145 events: Arc<StdMutex<Vec<CapturedEvent>>>,
3146 }
3147
3148 impl CaptureSubscriber {
3149 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
3150 Self { events }
3151 }
3152 }
3153
3154 impl tracing::Subscriber for CaptureSubscriber {
3155 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
3156 true
3157 }
3158 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
3159 tracing::span::Id::from_u64(1)
3160 }
3161 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
3162 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
3163 fn event(&self, event: &tracing::Event<'_>) {
3164 let mut visitor = CapturedEventVisitor::default();
3165 event.record(&mut visitor);
3166 self.events.lock().unwrap().push(visitor.0);
3167 }
3168 fn enter(&self, _: &tracing::span::Id) {}
3169 fn exit(&self, _: &tracing::span::Id) {}
3170 }
3171
3172 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
3182 static GLOBAL_INIT: Once = Once::new();
3183
3184 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
3185 GLOBAL_INIT.call_once(|| {
3186 let buffer = Arc::new(StdMutex::new(Vec::new()));
3187 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
3188 let _ = tracing::subscriber::set_global_default(subscriber);
3193 let _ = GLOBAL_CAPTURE.set(buffer);
3194 });
3195 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
3196 }
3197
3198 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
3203 where
3204 Fut: std::future::Future<Output = ()>,
3205 {
3206 let buffer = global_capture();
3207 buffer.lock().unwrap().clear();
3208
3209 let rt = tokio::runtime::Builder::new_current_thread()
3210 .enable_all()
3211 .build()
3212 .expect("build current-thread tokio runtime");
3213 rt.block_on(future);
3214
3215 let result = buffer.lock().unwrap().clone();
3216 result
3217 }
3218
3219 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
3226 events
3227 .iter()
3228 .filter(|e| e.message.as_deref() == Some("gate.check"))
3229 .filter(|e| {
3230 e.audit_event
3231 .as_deref()
3232 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
3233 .and_then(|v| {
3234 v.get("gate_impl")
3235 .and_then(|g| g.as_str().map(|s| s.to_string()))
3236 })
3237 .as_deref()
3238 == Some(gate_impl)
3239 })
3240 .cloned()
3241 .collect()
3242 }
3243
3244 #[test]
3245 #[serial]
3246 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
3247 #[derive(Debug)]
3248 struct TracingAllowGate;
3249 impl Gate for TracingAllowGate {
3250 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
3251 Ok(GateDecision::allow())
3252 }
3253 fn impl_name(&self) -> &'static str {
3254 "TracingAllowGate"
3255 }
3256 }
3257
3258 let events = capture_dispatch_events(async {
3259 let mut builder = VerbRegistryBuilder::new();
3260 builder.register(AlphaPack);
3261 builder.with_gate(Arc::new(TracingAllowGate));
3262 builder.with_default_namespace("tenant-default");
3263 let reg = builder.build().expect("registry builds");
3264 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
3265 .await
3266 .unwrap();
3267 });
3268
3269 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
3270 assert_eq!(
3271 gate_events.len(),
3272 1,
3273 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
3274 );
3275 let payload = gate_events[0]
3276 .audit_event
3277 .as_ref()
3278 .expect("gate.check event must carry an audit_event field");
3279 let audit: khive_gate::AuditEvent =
3280 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
3281 assert_eq!(audit.decision, AuditDecision::Allow);
3282 assert_eq!(audit.verb, "list");
3283 assert_eq!(audit.namespace, "tenant-q");
3284 assert_eq!(audit.gate_impl, "TracingAllowGate");
3285 assert!(
3286 audit.deny_reason.is_none(),
3287 "deny_reason must be None on Allow"
3288 );
3289 }
3290
3291 use crate::runtime::NamespaceToken;
3294 use async_trait::async_trait;
3295 use khive_storage::{
3296 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
3297 };
3298 use khive_types::EventOutcome;
3299
3300 #[derive(Default, Debug)]
3302 struct MemoryEventStore {
3303 events: std::sync::Mutex<Vec<Event>>,
3304 }
3305
3306 #[async_trait]
3307 impl EventStore for MemoryEventStore {
3308 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
3309 self.events.lock().unwrap().push(event);
3310 Ok(())
3311 }
3312 async fn append_events(
3313 &self,
3314 events: Vec<Event>,
3315 ) -> khive_storage::StorageResult<BatchWriteSummary> {
3316 let attempted = events.len() as u64;
3317 let affected = attempted;
3318 self.events.lock().unwrap().extend(events);
3319 Ok(BatchWriteSummary {
3320 attempted,
3321 affected,
3322 failed: 0,
3323 first_error: String::new(),
3324 })
3325 }
3326 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
3327 Ok(self
3328 .events
3329 .lock()
3330 .unwrap()
3331 .iter()
3332 .find(|e| e.id == id)
3333 .cloned())
3334 }
3335 async fn query_events(
3336 &self,
3337 _filter: EventFilter,
3338 _page: PageRequest,
3339 ) -> khive_storage::StorageResult<Page<Event>> {
3340 let items = self.events.lock().unwrap().clone();
3341 let total = items.len() as u64;
3342 Ok(Page {
3343 items,
3344 total: Some(total),
3345 })
3346 }
3347 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
3348 Ok(self.events.lock().unwrap().len() as u64)
3349 }
3350 }
3351
3352 #[tokio::test]
3353 async fn allow_all_gate_default_remains_backward_compatible() {
3354 let mut builder = VerbRegistryBuilder::new();
3356 builder.register(AlphaPack);
3357 let reg = builder.build().expect("registry builds");
3358
3359 let res = reg.dispatch("list", Value::Null).await.unwrap();
3360 assert_eq!(
3361 res["pack"], "alpha",
3362 "AllowAllGate must allow every verb — backward compat guarantee"
3363 );
3364 let res = reg.dispatch("create", Value::Null).await.unwrap();
3365 assert_eq!(res["pack"], "alpha");
3366 }
3367
3368 #[tokio::test]
3369 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
3370 #[derive(Debug)]
3371 struct AlwaysDenyGate;
3372 impl Gate for AlwaysDenyGate {
3373 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3374 Ok(GateDecision::deny("test: always deny"))
3375 }
3376 }
3377
3378 #[derive(Debug)]
3380 struct TrackedPack {
3381 invoked: Arc<AtomicUsize>,
3382 }
3383
3384 impl khive_types::Pack for TrackedPack {
3385 const NAME: &'static str = "tracked";
3386 const NOTE_KINDS: &'static [&'static str] = &[];
3387 const ENTITY_KINDS: &'static [&'static str] = &[];
3388 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
3389 name: "guarded",
3390 description: "a guarded verb",
3391 visibility: Visibility::Verb,
3392 category: VerbCategory::Assertive,
3393 params: &[],
3394 }];
3395 }
3396
3397 #[async_trait]
3398 impl PackRuntime for TrackedPack {
3399 fn name(&self) -> &str {
3400 Self::NAME
3401 }
3402 fn note_kinds(&self) -> &'static [&'static str] {
3403 Self::NOTE_KINDS
3404 }
3405 fn entity_kinds(&self) -> &'static [&'static str] {
3406 Self::ENTITY_KINDS
3407 }
3408 fn handlers(&self) -> &'static [HandlerDef] {
3409 Self::HANDLERS
3410 }
3411 async fn dispatch(
3412 &self,
3413 _verb: &str,
3414 _params: Value,
3415 _registry: &VerbRegistry,
3416 _token: &NamespaceToken,
3417 ) -> Result<Value, RuntimeError> {
3418 self.invoked.fetch_add(1, Ordering::SeqCst);
3419 Ok(serde_json::json!({"invoked": true}))
3420 }
3421 }
3422
3423 let invoked = Arc::new(AtomicUsize::new(0));
3424 let mut builder = VerbRegistryBuilder::new();
3425 builder.register(TrackedPack {
3426 invoked: invoked.clone(),
3427 });
3428 builder.with_gate(Arc::new(AlwaysDenyGate));
3429 let reg = builder.build().expect("registry builds");
3430
3431 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
3432 assert!(
3433 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
3434 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
3435 );
3436 assert_eq!(
3437 invoked.load(Ordering::SeqCst),
3438 0,
3439 "pack dispatch MUST NOT be invoked when gate denies"
3440 );
3441 }
3442
3443 #[tokio::test]
3444 async fn audit_event_persists_to_event_store_on_allow() {
3445 let store = Arc::new(MemoryEventStore::default());
3446 let mut builder = VerbRegistryBuilder::new();
3447 builder.register(AlphaPack);
3448 builder.with_event_store(store.clone());
3449 let reg = builder.build().expect("registry builds");
3450
3451 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3452 .await
3453 .unwrap();
3454
3455 let count = store.count_events(EventFilter::default()).await.unwrap();
3456 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
3457
3458 let page = store
3459 .query_events(
3460 EventFilter::default(),
3461 PageRequest {
3462 limit: 10,
3463 offset: 0,
3464 },
3465 )
3466 .await
3467 .unwrap();
3468 let ev = &page.items[0];
3469 assert_eq!(ev.verb, "list");
3470 assert_eq!(ev.namespace, "test-ns");
3471 assert_eq!(ev.substrate, SubstrateKind::Event);
3472 assert_eq!(ev.outcome, EventOutcome::Success);
3473 }
3474
3475 #[tokio::test]
3476 async fn audit_event_duration_us_reflects_measured_dispatch_time() {
3477 let store = Arc::new(MemoryEventStore::default());
3483 let mut builder = VerbRegistryBuilder::new();
3484 builder.register(SleepingPack);
3485 builder.with_event_store(store.clone());
3486 let reg = builder.build().expect("registry builds");
3487
3488 reg.dispatch("slow_op", serde_json::json!({}))
3489 .await
3490 .unwrap();
3491
3492 let page = store
3493 .query_events(
3494 EventFilter::default(),
3495 PageRequest {
3496 limit: 10,
3497 offset: 0,
3498 },
3499 )
3500 .await
3501 .unwrap();
3502 assert_eq!(page.items.len(), 1);
3503 let ev = &page.items[0];
3504 assert!(
3505 ev.duration_us >= 10_000,
3506 "duration_us must reflect the ~20ms measured dispatch time, got {}",
3507 ev.duration_us
3508 );
3509 }
3510
3511 #[tokio::test]
3512 async fn dispatch_unknown_verb_allowed_by_gate_still_persists_audit_row() {
3513 let store = Arc::new(MemoryEventStore::default());
3518 let mut builder = VerbRegistryBuilder::new();
3519 builder.register(AlphaPack);
3520 builder.with_event_store(store.clone());
3521 let reg = builder.build().expect("registry builds");
3522
3523 let result = reg.dispatch("no_such_verb", serde_json::json!({})).await;
3524 assert!(result.is_err(), "unknown verb must still return an error");
3525
3526 let count = store.count_events(EventFilter::default()).await.unwrap();
3527 assert_eq!(
3528 count, 1,
3529 "an allowed-but-unknown verb must still persist one audit row"
3530 );
3531 let page = store
3532 .query_events(
3533 EventFilter::default(),
3534 PageRequest {
3535 limit: 10,
3536 offset: 0,
3537 },
3538 )
3539 .await
3540 .unwrap();
3541 assert_eq!(page.items[0].duration_us, 0);
3542 assert_eq!(page.items[0].outcome, EventOutcome::Error);
3546 }
3547
3548 #[tokio::test]
3549 async fn audit_event_persists_to_event_store_on_deny() {
3550 #[derive(Debug)]
3551 struct AlwaysDenyGate;
3552 impl Gate for AlwaysDenyGate {
3553 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3554 Ok(GateDecision::deny("denied by test"))
3555 }
3556 }
3557
3558 let store = Arc::new(MemoryEventStore::default());
3559 let mut builder = VerbRegistryBuilder::new();
3560 builder.register(AlphaPack);
3561 builder.with_gate(Arc::new(AlwaysDenyGate));
3562 builder.with_event_store(store.clone());
3563 let reg = builder.build().expect("registry builds");
3564
3565 let err = reg
3567 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3568 .await
3569 .unwrap_err();
3570 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
3571
3572 let count = store.count_events(EventFilter::default()).await.unwrap();
3573 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
3574
3575 let page = store
3576 .query_events(
3577 EventFilter::default(),
3578 PageRequest {
3579 limit: 10,
3580 offset: 0,
3581 },
3582 )
3583 .await
3584 .unwrap();
3585 let ev = &page.items[0];
3586 assert_eq!(ev.verb, "list");
3587 assert_eq!(ev.outcome, EventOutcome::Denied);
3588 }
3589
3590 #[tokio::test]
3591 async fn gate_error_does_not_persist_to_event_store() {
3592 #[derive(Debug)]
3593 struct FailingGate;
3594 impl Gate for FailingGate {
3595 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
3596 Err(khive_gate::GateError::Internal("gate broken".into()))
3597 }
3598 }
3599
3600 let store = Arc::new(MemoryEventStore::default());
3601 let mut builder = VerbRegistryBuilder::new();
3602 builder.register(AlphaPack);
3603 builder.with_gate(Arc::new(FailingGate));
3604 builder.with_event_store(store.clone());
3605 let reg = builder.build().expect("registry builds");
3606
3607 let res = reg.dispatch("list", Value::Null).await.unwrap();
3609 assert_eq!(
3610 res["pack"], "alpha",
3611 "gate error must fail-open, not block dispatch"
3612 );
3613
3614 let count = store.count_events(EventFilter::default()).await.unwrap();
3615 assert_eq!(
3616 count, 0,
3617 "gate infrastructure error must NOT produce an audit event in EventStore"
3618 );
3619 }
3620
3621 #[tokio::test]
3622 async fn no_event_store_configured_tracing_only() {
3623 let mut builder = VerbRegistryBuilder::new();
3627 builder.register(AlphaPack);
3628 let reg = builder.build().expect("registry builds");
3629
3630 let res = reg.dispatch("list", Value::Null).await.unwrap();
3631 assert_eq!(res["pack"], "alpha");
3632 }
3633
3634 #[test]
3635 #[serial]
3636 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
3637 #[derive(Debug)]
3638 struct TracingDenyGate;
3639 impl Gate for TracingDenyGate {
3640 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3641 Ok(GateDecision::deny("denied by test gate"))
3642 }
3643 fn impl_name(&self) -> &'static str {
3644 "TracingDenyGate"
3645 }
3646 }
3647
3648 let events = capture_dispatch_events(async {
3649 let mut builder = VerbRegistryBuilder::new();
3650 builder.register(AlphaPack);
3651 builder.with_gate(Arc::new(TracingDenyGate));
3652 let reg = builder.build().expect("registry builds");
3653 let _ = reg.dispatch("create", serde_json::Value::Null).await;
3656 });
3657
3658 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
3659 assert_eq!(
3660 gate_events.len(),
3661 1,
3662 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
3663 );
3664 let payload = gate_events[0]
3665 .audit_event
3666 .as_ref()
3667 .expect("gate.check event must carry an audit_event field on Deny");
3668 let audit: khive_gate::AuditEvent =
3669 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
3670 assert_eq!(audit.decision, AuditDecision::Deny);
3671 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
3672 assert_eq!(audit.gate_impl, "TracingDenyGate");
3673 let payload_json: serde_json::Value =
3677 serde_json::from_str(payload).expect("payload must be valid JSON");
3678 assert_eq!(
3679 payload_json["obligations"],
3680 serde_json::Value::Array(Vec::new()),
3681 "obligations must be `[]` on Deny on the tracing payload, not omitted"
3682 );
3683 }
3684
3685 #[tokio::test]
3692 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
3693 #[derive(Debug)]
3694 struct DenyGateWithName;
3695 impl Gate for DenyGateWithName {
3696 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3697 Ok(GateDecision::deny("policy: write forbidden for anon"))
3698 }
3699 fn impl_name(&self) -> &'static str {
3700 "DenyGateWithName"
3701 }
3702 }
3703
3704 let store = Arc::new(MemoryEventStore::default());
3705 let mut builder = VerbRegistryBuilder::new();
3706 builder.register(AlphaPack);
3707 builder.with_gate(Arc::new(DenyGateWithName));
3708 builder.with_event_store(store.clone());
3709 let reg = builder.build().expect("registry builds");
3710
3711 let err = reg
3713 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3714 .await
3715 .unwrap_err();
3716 assert!(
3717 matches!(err, RuntimeError::PermissionDenied { .. }),
3718 "expected PermissionDenied, got {err:?}"
3719 );
3720
3721 let page = store
3723 .query_events(
3724 EventFilter::default(),
3725 PageRequest {
3726 limit: 10,
3727 offset: 0,
3728 },
3729 )
3730 .await
3731 .unwrap();
3732 assert_eq!(
3733 page.items.len(),
3734 1,
3735 "one audit event must be persisted on deny"
3736 );
3737
3738 let ev = &page.items[0];
3739 assert_eq!(ev.outcome, EventOutcome::Denied);
3740
3741 let data = &ev.payload;
3743
3744 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3745 .expect("Event.payload must deserialize to AuditEvent");
3746
3747 assert_eq!(
3748 audit.deny_reason.as_deref(),
3749 Some("policy: write forbidden for anon"),
3750 "deny_reason must be preserved through EventStore"
3751 );
3752 assert_eq!(
3753 audit.gate_impl, "DenyGateWithName",
3754 "gate_impl must be preserved through EventStore"
3755 );
3756 assert_eq!(
3757 audit.decision,
3758 khive_gate::AuditDecision::Deny,
3759 "decision field must be preserved through EventStore"
3760 );
3761 }
3762
3763 #[tokio::test]
3764 async fn audit_envelope_round_trips_obligations_through_event_store() {
3765 use khive_gate::Obligation;
3766
3767 #[derive(Debug)]
3768 struct ObligationGate;
3769 impl Gate for ObligationGate {
3770 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3771 Ok(GateDecision::allow_with(vec![Obligation::Audit {
3772 tag: "billing.meter".into(),
3773 }]))
3774 }
3775 fn impl_name(&self) -> &'static str {
3776 "ObligationGate"
3777 }
3778 }
3779
3780 let store = Arc::new(MemoryEventStore::default());
3781 let mut builder = VerbRegistryBuilder::new();
3782 builder.register(AlphaPack);
3783 builder.with_gate(Arc::new(ObligationGate));
3784 builder.with_event_store(store.clone());
3785 let reg = builder.build().expect("registry builds");
3786
3787 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3788 .await
3789 .unwrap();
3790
3791 let page = store
3792 .query_events(
3793 EventFilter::default(),
3794 PageRequest {
3795 limit: 10,
3796 offset: 0,
3797 },
3798 )
3799 .await
3800 .unwrap();
3801 assert_eq!(page.items.len(), 1);
3802
3803 let ev = &page.items[0];
3804 assert_eq!(ev.outcome, EventOutcome::Success);
3805
3806 let data = &ev.payload;
3807
3808 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3809 .expect("Event.payload must deserialize to AuditEvent");
3810
3811 assert_eq!(audit.gate_impl, "ObligationGate");
3812 assert_eq!(
3813 audit.obligations.len(),
3814 1,
3815 "obligations must be preserved through EventStore"
3816 );
3817 match &audit.obligations[0] {
3818 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
3819 other => panic!("expected Audit obligation, got {other:?}"),
3820 }
3821 }
3822
3823 #[tokio::test]
3831 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
3832 #[derive(Debug)]
3833 struct SqlTestDenyGate;
3834 impl Gate for SqlTestDenyGate {
3835 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3836 Ok(GateDecision::deny("sql-path: write denied"))
3837 }
3838 fn impl_name(&self) -> &'static str {
3839 "SqlTestDenyGate"
3840 }
3841 }
3842
3843 let rt = KhiveRuntime::memory().expect("in-memory runtime");
3847 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
3848 let sql_store = rt
3849 .events(&test_tok)
3850 .expect("events_for_namespace must succeed");
3851
3852 let mut builder = VerbRegistryBuilder::new();
3853 builder.register(AlphaPack);
3854 builder.with_gate(Arc::new(SqlTestDenyGate));
3855 builder.with_event_store(sql_store.clone());
3856 let reg = builder.build().expect("registry builds");
3857
3858 let err = reg
3860 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3861 .await
3862 .unwrap_err();
3863 assert!(
3864 matches!(err, RuntimeError::PermissionDenied { .. }),
3865 "expected PermissionDenied, got {err:?}"
3866 );
3867
3868 let page = sql_store
3870 .query_events(
3871 EventFilter::default(),
3872 PageRequest {
3873 limit: 10,
3874 offset: 0,
3875 },
3876 )
3877 .await
3878 .unwrap();
3879 assert_eq!(
3880 page.items.len(),
3881 1,
3882 "one audit event must be persisted on deny through SqlEventStore"
3883 );
3884
3885 let ev = &page.items[0];
3886 assert_eq!(ev.outcome, EventOutcome::Denied);
3887
3888 let data = &ev.payload;
3892
3893 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3894 .expect("Event.payload must deserialize to AuditEvent after SQL round-trip");
3895
3896 assert_eq!(
3897 audit.deny_reason.as_deref(),
3898 Some("sql-path: write denied"),
3899 "deny_reason must survive the SQL text round-trip"
3900 );
3901 assert_eq!(
3902 audit.gate_impl, "SqlTestDenyGate",
3903 "gate_impl must survive the SQL text round-trip"
3904 );
3905 assert_eq!(
3906 audit.decision,
3907 khive_gate::AuditDecision::Deny,
3908 "decision field must survive the SQL text round-trip"
3909 );
3910 assert!(
3913 audit.obligations.is_empty(),
3914 "obligations must be preserved as empty [] through SQL round-trip"
3915 );
3916 }
3917
3918 #[tokio::test]
3930 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
3931 use khive_gate::Obligation;
3932
3933 #[derive(Debug)]
3934 struct SqlTestAllowWithObligationGate;
3935 impl Gate for SqlTestAllowWithObligationGate {
3936 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3937 Ok(GateDecision::allow_with(vec![Obligation::Audit {
3938 tag: "sql-path-billing.meter".into(),
3939 }]))
3940 }
3941 fn impl_name(&self) -> &'static str {
3942 "SqlTestAllowWithObligationGate"
3943 }
3944 }
3945
3946 let rt = KhiveRuntime::memory().expect("in-memory runtime");
3947 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
3948 let sql_store = rt
3949 .events(&test_tok)
3950 .expect("events_for_namespace must succeed");
3951
3952 let mut builder = VerbRegistryBuilder::new();
3953 builder.register(AlphaPack);
3954 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
3955 builder.with_event_store(sql_store.clone());
3956 let reg = builder.build().expect("registry builds");
3957
3958 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3960 .await
3961 .expect("dispatch must succeed when gate allows");
3962
3963 let page = sql_store
3965 .query_events(
3966 EventFilter::default(),
3967 PageRequest {
3968 limit: 10,
3969 offset: 0,
3970 },
3971 )
3972 .await
3973 .unwrap();
3974 assert_eq!(
3975 page.items.len(),
3976 1,
3977 "one audit event must be persisted on allow through SqlEventStore"
3978 );
3979
3980 let ev = &page.items[0];
3981 assert_eq!(ev.outcome, EventOutcome::Success);
3982
3983 let data = &ev.payload;
3984
3985 let obligations_raw = data
3990 .get("obligations")
3991 .expect("Event.data JSON must contain 'obligations' key");
3992 let obligations_arr = obligations_raw
3993 .as_array()
3994 .expect("'obligations' must be a JSON array");
3995 assert!(
3996 !obligations_arr.is_empty(),
3997 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
3998 );
3999
4000 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
4003 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
4004
4005 assert_eq!(
4006 audit.gate_impl, "SqlTestAllowWithObligationGate",
4007 "gate_impl must survive the SQL text round-trip"
4008 );
4009 assert_eq!(
4010 audit.decision,
4011 khive_gate::AuditDecision::Allow,
4012 "decision field must survive the SQL text round-trip"
4013 );
4014 assert_eq!(
4015 audit.obligations.len(),
4016 1,
4017 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
4018 );
4019 match &audit.obligations[0] {
4020 Obligation::Audit { tag } => assert_eq!(
4021 tag, "sql-path-billing.meter",
4022 "Audit obligation tag must survive the SQL text round-trip"
4023 ),
4024 other => panic!("expected Audit obligation, got {other:?}"),
4025 }
4026 }
4027
4028 #[tokio::test]
4036 async fn audit_event_payload_shape_for_create_verb() {
4037 let store = Arc::new(MemoryEventStore::default());
4038 let mut builder = VerbRegistryBuilder::new();
4039 builder.register(AlphaPack);
4040 builder.with_event_store(store.clone());
4041 builder.with_default_namespace("test-ns");
4042 let reg = builder.build().expect("registry builds");
4043
4044 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
4047 .await
4048 .unwrap();
4049
4050 let count = store.count_events(EventFilter::default()).await.unwrap();
4051 assert_eq!(count, 1, "exactly one audit event for one dispatch");
4052
4053 let page = store
4054 .query_events(
4055 EventFilter::default(),
4056 PageRequest {
4057 limit: 10,
4058 offset: 0,
4059 },
4060 )
4061 .await
4062 .unwrap();
4063 let ev = &page.items[0];
4064
4065 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
4067 assert_eq!(
4068 ev.outcome,
4069 EventOutcome::Success,
4070 "ev.outcome must be Success on allow"
4071 );
4072 assert_eq!(
4073 ev.namespace, "test-ns",
4074 "ev.namespace must match the dispatch namespace"
4075 );
4076
4077 let data = &ev.payload;
4079
4080 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
4081 .expect("ev.payload must deserialize to AuditEvent");
4082
4083 assert_eq!(
4084 audit.decision,
4085 khive_gate::AuditDecision::Allow,
4086 "AuditEvent.decision must be Allow"
4087 );
4088 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
4089 assert_eq!(
4090 audit.namespace, "test-ns",
4091 "AuditEvent.namespace must be preserved"
4092 );
4093 assert_eq!(
4094 audit.gate_impl, "AllowAllGate",
4095 "AuditEvent.gate_impl must name the gate implementation"
4096 );
4097 assert!(
4098 audit.deny_reason.is_none(),
4099 "AuditEvent.deny_reason must be None on Allow"
4100 );
4101 let payload_json: serde_json::Value =
4103 serde_json::from_value(data.clone()).expect("data must be valid JSON");
4104 assert_eq!(
4105 payload_json["obligations"],
4106 serde_json::Value::Array(Vec::new()),
4107 "obligations must be [] on AllowAllGate"
4108 );
4109 }
4110
4111 #[tokio::test]
4113 async fn audit_event_threads_target_id_from_dispatch_args() {
4114 let store = Arc::new(MemoryEventStore::default());
4115 let target = uuid::Uuid::new_v4();
4116 let mut builder = VerbRegistryBuilder::new();
4117 builder.register(AlphaPack);
4118 builder.with_event_store(store.clone());
4119 builder.with_default_namespace("test-ns");
4120 let reg = builder.build().expect("registry builds");
4121
4122 reg.dispatch(
4123 "create",
4124 serde_json::json!({"namespace": "test-ns", "target_id": target}),
4125 )
4126 .await
4127 .unwrap();
4128
4129 let page = store
4130 .query_events(
4131 EventFilter::default(),
4132 PageRequest {
4133 offset: 0,
4134 limit: 10,
4135 },
4136 )
4137 .await
4138 .unwrap();
4139 assert_eq!(
4140 page.items[0].target_id,
4141 Some(target),
4142 "#282: audit event must carry target_id from dispatch params"
4143 );
4144 }
4145
4146 struct LinkResultPack {
4152 result: std::sync::Mutex<Option<Result<Value, RuntimeError>>>,
4153 }
4154
4155 impl LinkResultPack {
4156 fn ok(value: Value) -> Self {
4157 Self {
4158 result: std::sync::Mutex::new(Some(Ok(value))),
4159 }
4160 }
4161 fn err(message: &str) -> Self {
4162 Self {
4163 result: std::sync::Mutex::new(Some(Err(RuntimeError::InvalidInput(
4164 message.to_string(),
4165 )))),
4166 }
4167 }
4168 }
4169
4170 impl khive_types::Pack for LinkResultPack {
4171 const NAME: &'static str = "kg";
4172 const NOTE_KINDS: &'static [&'static str] = &[];
4173 const ENTITY_KINDS: &'static [&'static str] = &[];
4174 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
4175 name: "link",
4176 description: "test link handler",
4177 visibility: Visibility::Verb,
4178 category: VerbCategory::Commissive,
4179 params: &[],
4180 }];
4181 }
4182
4183 #[async_trait]
4184 impl PackRuntime for LinkResultPack {
4185 fn name(&self) -> &str {
4186 Self::NAME
4187 }
4188 fn note_kinds(&self) -> &'static [&'static str] {
4189 Self::NOTE_KINDS
4190 }
4191 fn entity_kinds(&self) -> &'static [&'static str] {
4192 Self::ENTITY_KINDS
4193 }
4194 fn handlers(&self) -> &'static [HandlerDef] {
4195 Self::HANDLERS
4196 }
4197 async fn dispatch(
4198 &self,
4199 _verb: &str,
4200 _params: Value,
4201 _registry: &VerbRegistry,
4202 _token: &NamespaceToken,
4203 ) -> Result<Value, RuntimeError> {
4204 self.result
4205 .lock()
4206 .unwrap()
4207 .take()
4208 .expect("LinkResultPack dispatch called more than once in a test")
4209 }
4210 }
4211
4212 #[tokio::test]
4213 async fn link_audit_enriches_successful_singleton_with_edge_v2() {
4214 let store = Arc::new(MemoryEventStore::default());
4215 let edge_id = uuid::Uuid::new_v4();
4216 let source_id = uuid::Uuid::new_v4();
4217 let target_id = uuid::Uuid::new_v4();
4218 let edge_json = serde_json::json!({
4219 "id": edge_id,
4220 "namespace": "local",
4221 "source_id": source_id,
4222 "target_id": target_id,
4223 "relation": "depends_on",
4224 "weight": 1.0,
4225 });
4226 let mut builder = VerbRegistryBuilder::new();
4227 builder.register(LinkResultPack::ok(edge_json));
4228 builder.with_event_store(store.clone());
4229 builder.with_default_namespace("test-ns");
4230 let reg = builder.build().expect("registry builds");
4231
4232 reg.dispatch(
4233 "link",
4234 serde_json::json!({
4235 "source_id": source_id,
4236 "target_id": target_id,
4237 "relation": "depends_on",
4238 }),
4239 )
4240 .await
4241 .unwrap();
4242
4243 let count = store.count_events(EventFilter::default()).await.unwrap();
4244 assert_eq!(
4245 count, 1,
4246 "exactly one deferred audit row must be persisted for a successful singleton link"
4247 );
4248 let page = store
4249 .query_events(
4250 EventFilter::default(),
4251 PageRequest {
4252 limit: 10,
4253 offset: 0,
4254 },
4255 )
4256 .await
4257 .unwrap();
4258 let ev = &page.items[0];
4259 assert_eq!(ev.verb, "link");
4260 assert_eq!(ev.outcome, EventOutcome::Success);
4261 assert_eq!(
4262 ev.payload_schema_version, 2,
4263 "successful singleton link uses audit schema v2"
4264 );
4265 assert_eq!(
4266 ev.target_id,
4267 Some(edge_id),
4268 "target_id must be the created/resolved edge id, not a raw caller arg"
4269 );
4270 assert_eq!(ev.payload["edge_id"], serde_json::json!(edge_id));
4271 assert_eq!(ev.payload["source_id"], serde_json::json!(source_id));
4272 assert_eq!(ev.payload["target_id"], serde_json::json!(target_id));
4273 assert_eq!(ev.payload["relation"], "depends_on");
4274 assert_eq!(ev.payload["weight"], 1.0);
4275 assert_eq!(ev.payload["verb"], "link");
4277 assert_eq!(ev.payload["decision"], "allow");
4278 assert!(ev.payload.get("gate_impl").is_some());
4279 }
4280
4281 #[tokio::test]
4282 async fn link_audit_falls_back_to_v1_when_dispatch_fails() {
4283 let store = Arc::new(MemoryEventStore::default());
4284 let mut builder = VerbRegistryBuilder::new();
4285 builder.register(LinkResultPack::err("target endpoint not found"));
4286 builder.with_event_store(store.clone());
4287 builder.with_default_namespace("test-ns");
4288 let reg = builder.build().expect("registry builds");
4289
4290 let err = reg
4291 .dispatch(
4292 "link",
4293 serde_json::json!({
4294 "source_id": "note:alpha",
4295 "target_id": "note:missing",
4296 "relation": "depends_on",
4297 }),
4298 )
4299 .await
4300 .unwrap_err();
4301 assert!(
4302 matches!(err, RuntimeError::InvalidInput(ref msg) if msg.contains("not found")),
4303 "the original dispatch error must be returned unchanged"
4304 );
4305
4306 let page = store
4307 .query_events(
4308 EventFilter::default(),
4309 PageRequest {
4310 limit: 10,
4311 offset: 0,
4312 },
4313 )
4314 .await
4315 .unwrap();
4316 assert_eq!(
4317 page.items.len(),
4318 1,
4319 "a v1 fallback audit row must still be persisted on dispatch failure"
4320 );
4321 let ev = &page.items[0];
4322 assert_eq!(
4323 ev.payload_schema_version, 1,
4324 "failed link keeps the v1 audit shape"
4325 );
4326 assert_eq!(
4329 ev.outcome,
4330 EventOutcome::Error,
4331 "outcome reflects the dispatch result (Err), not the gate decision (Allow)"
4332 );
4333 assert!(
4334 ev.duration_us >= 0,
4335 "duration_us must still be populated (measured, not the Event::new \
4336 default sentinel) on a failed dispatch"
4337 );
4338 assert!(
4339 ev.target_id.is_none(),
4340 "non-UUID caller-supplied ids do not spuriously populate target_id"
4341 );
4342 assert!(
4343 ev.payload.get("edge_id").is_none(),
4344 "v1 fallback must not carry edge enrichment fields"
4345 );
4346 let _: khive_gate::AuditEvent = serde_json::from_value(ev.payload.clone())
4347 .expect("v1 fallback payload must deserialize as AuditEvent");
4348 }
4349
4350 #[tokio::test]
4351 async fn link_audit_falls_back_to_v1_when_result_missing_edge_fields() {
4352 let store = Arc::new(MemoryEventStore::default());
4353 let target_arg = uuid::Uuid::new_v4();
4354 let mut builder = VerbRegistryBuilder::new();
4355 builder.register(LinkResultPack::ok(serde_json::json!({"ok": true})));
4356 builder.with_event_store(store.clone());
4357 builder.with_default_namespace("test-ns");
4358 let reg = builder.build().expect("registry builds");
4359
4360 reg.dispatch(
4361 "link",
4362 serde_json::json!({
4363 "source_id": uuid::Uuid::new_v4(),
4364 "target_id": target_arg,
4365 "relation": "depends_on",
4366 }),
4367 )
4368 .await
4369 .unwrap();
4370
4371 let page = store
4372 .query_events(
4373 EventFilter::default(),
4374 PageRequest {
4375 limit: 10,
4376 offset: 0,
4377 },
4378 )
4379 .await
4380 .unwrap();
4381 assert_eq!(page.items.len(), 1);
4382 let ev = &page.items[0];
4383 assert_eq!(
4384 ev.payload_schema_version, 1,
4385 "an unparsable success result falls back to v1 rather than dropping the audit row"
4386 );
4387 assert_eq!(ev.outcome, EventOutcome::Success);
4388 assert_eq!(
4389 ev.target_id,
4390 Some(target_arg),
4391 "v1 fallback still extracts target_id from the raw dispatch args"
4392 );
4393 assert!(ev.payload.get("edge_id").is_none());
4394 }
4395
4396 #[tokio::test]
4397 async fn link_audit_bulk_links_get_no_enrichment() {
4398 let store = Arc::new(MemoryEventStore::default());
4399 let mut builder = VerbRegistryBuilder::new();
4400 builder.register(LinkResultPack::ok(serde_json::json!({
4401 "attempted": 2, "created": 2, "skipped": 0, "failed": 0
4402 })));
4403 builder.with_event_store(store.clone());
4404 builder.with_default_namespace("test-ns");
4405 let reg = builder.build().expect("registry builds");
4406
4407 reg.dispatch(
4408 "link",
4409 serde_json::json!({
4410 "links": [
4411 {"source_id": "a", "target_id": "b", "relation": "depends_on"},
4412 {"source_id": "c", "target_id": "d", "relation": "depends_on"},
4413 ],
4414 }),
4415 )
4416 .await
4417 .unwrap();
4418
4419 let count = store.count_events(EventFilter::default()).await.unwrap();
4420 assert_eq!(
4421 count, 1,
4422 "bulk `links` gets exactly one v1 audit row (deferred until dispatch \
4423 resolves like every other Allow-outcome row since ADR-103 Stage 1, \
4424 but never v2-enriched — enrichment is singleton-`link`-only)"
4425 );
4426 let page = store
4427 .query_events(
4428 EventFilter::default(),
4429 PageRequest {
4430 limit: 10,
4431 offset: 0,
4432 },
4433 )
4434 .await
4435 .unwrap();
4436 let ev = &page.items[0];
4437 assert_eq!(
4438 ev.payload_schema_version, 1,
4439 "bulk link mode is out of scope for #676's events.target_id enrichment"
4440 );
4441 assert!(ev.target_id.is_none());
4442 }
4443
4444 #[test]
4445 fn link_audit_success_from_result_extracts_edge_fields() {
4446 let gate_req = GateRequest::new(
4447 ActorRef::anonymous(),
4448 Namespace::local(),
4449 "link",
4450 serde_json::json!({}),
4451 );
4452 let decision = GateDecision::Allow {
4453 obligations: vec![],
4454 };
4455 let audit = AuditEvent::from_check(&gate_req, &decision, "AllowAllGate");
4456
4457 let edge_id = uuid::Uuid::new_v4();
4458 let source_id = uuid::Uuid::new_v4();
4459 let target_id = uuid::Uuid::new_v4();
4460 let result = serde_json::json!({
4461 "id": edge_id,
4462 "source_id": source_id,
4463 "target_id": target_id,
4464 "relation": "depends_on",
4465 "weight": 0.5,
4466 });
4467
4468 let (returned_id, payload) = link_audit_success_from_result(audit, &result)
4469 .expect("well-formed edge JSON must produce an enriched payload");
4470 assert_eq!(returned_id, edge_id);
4471 assert_eq!(payload["edge_id"], serde_json::json!(edge_id));
4472 assert_eq!(payload["relation"], "depends_on");
4473 assert_eq!(payload["weight"], 0.5);
4474 assert_eq!(
4475 payload["verb"], "link",
4476 "v1 AuditEvent fields must flatten into the v2 payload"
4477 );
4478 }
4479
4480 #[test]
4481 fn link_audit_success_from_result_rejects_incomplete_or_malformed_result() {
4482 let gate_req = GateRequest::new(
4483 ActorRef::anonymous(),
4484 Namespace::local(),
4485 "link",
4486 serde_json::json!({}),
4487 );
4488 let decision = GateDecision::Allow {
4489 obligations: vec![],
4490 };
4491 let audit = AuditEvent::from_check(&gate_req, &decision, "AllowAllGate");
4492
4493 assert!(
4494 link_audit_success_from_result(
4495 audit.clone(),
4496 &serde_json::json!({"id": uuid::Uuid::new_v4()}),
4497 )
4498 .is_none(),
4499 "missing source_id/target_id/relation/weight must not enrich"
4500 );
4501 assert!(
4502 link_audit_success_from_result(audit, &serde_json::json!({"id": "not-a-uuid"}))
4503 .is_none(),
4504 "a non-UUID id must not enrich"
4505 );
4506 }
4507}
4508
4509#[cfg(test)]
4512mod dep_tests {
4513 use super::*;
4514 use async_trait::async_trait;
4515 use khive_types::Pack;
4516 use serde_json::Value;
4517
4518 struct KgDepPack;
4519 struct MemoryDepPack;
4520 struct ADepPack;
4521 struct BDepPack;
4522
4523 impl Pack for KgDepPack {
4524 const NAME: &'static str = "kg_dep";
4525 const NOTE_KINDS: &'static [&'static str] = &["observation"];
4526 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
4527 const HANDLERS: &'static [HandlerDef] = &[];
4528 }
4529
4530 impl Pack for MemoryDepPack {
4531 const NAME: &'static str = "memory_dep";
4532 const NOTE_KINDS: &'static [&'static str] = &["memory"];
4533 const ENTITY_KINDS: &'static [&'static str] = &[];
4534 const HANDLERS: &'static [HandlerDef] = &[];
4535 const REQUIRES: &'static [&'static str] = &["kg_dep"];
4536 }
4537
4538 impl Pack for ADepPack {
4539 const NAME: &'static str = "pack_a";
4540 const NOTE_KINDS: &'static [&'static str] = &[];
4541 const ENTITY_KINDS: &'static [&'static str] = &[];
4542 const HANDLERS: &'static [HandlerDef] = &[];
4543 const REQUIRES: &'static [&'static str] = &["pack_b"];
4544 }
4545
4546 impl Pack for BDepPack {
4547 const NAME: &'static str = "pack_b";
4548 const NOTE_KINDS: &'static [&'static str] = &[];
4549 const ENTITY_KINDS: &'static [&'static str] = &[];
4550 const HANDLERS: &'static [HandlerDef] = &[];
4551 const REQUIRES: &'static [&'static str] = &["pack_a"];
4552 }
4553
4554 #[async_trait]
4555 impl PackRuntime for KgDepPack {
4556 fn name(&self) -> &str {
4557 Self::NAME
4558 }
4559 fn note_kinds(&self) -> &'static [&'static str] {
4560 Self::NOTE_KINDS
4561 }
4562 fn entity_kinds(&self) -> &'static [&'static str] {
4563 Self::ENTITY_KINDS
4564 }
4565 fn handlers(&self) -> &'static [HandlerDef] {
4566 Self::HANDLERS
4567 }
4568 async fn dispatch(
4569 &self,
4570 verb: &str,
4571 _: Value,
4572 _: &VerbRegistry,
4573 _: &NamespaceToken,
4574 ) -> Result<Value, RuntimeError> {
4575 Err(RuntimeError::InvalidInput(format!(
4576 "KgDepPack has no verbs: {verb}"
4577 )))
4578 }
4579 }
4580
4581 #[async_trait]
4582 impl PackRuntime for MemoryDepPack {
4583 fn name(&self) -> &str {
4584 Self::NAME
4585 }
4586 fn note_kinds(&self) -> &'static [&'static str] {
4587 Self::NOTE_KINDS
4588 }
4589 fn entity_kinds(&self) -> &'static [&'static str] {
4590 Self::ENTITY_KINDS
4591 }
4592 fn handlers(&self) -> &'static [HandlerDef] {
4593 Self::HANDLERS
4594 }
4595 fn requires(&self) -> &'static [&'static str] {
4596 Self::REQUIRES
4597 }
4598 async fn dispatch(
4599 &self,
4600 verb: &str,
4601 _: Value,
4602 _: &VerbRegistry,
4603 _: &NamespaceToken,
4604 ) -> Result<Value, RuntimeError> {
4605 Err(RuntimeError::InvalidInput(format!(
4606 "MemoryDepPack has no verbs: {verb}"
4607 )))
4608 }
4609 }
4610
4611 #[async_trait]
4612 impl PackRuntime for ADepPack {
4613 fn name(&self) -> &str {
4614 Self::NAME
4615 }
4616 fn note_kinds(&self) -> &'static [&'static str] {
4617 Self::NOTE_KINDS
4618 }
4619 fn entity_kinds(&self) -> &'static [&'static str] {
4620 Self::ENTITY_KINDS
4621 }
4622 fn handlers(&self) -> &'static [HandlerDef] {
4623 Self::HANDLERS
4624 }
4625 fn requires(&self) -> &'static [&'static str] {
4626 Self::REQUIRES
4627 }
4628 async fn dispatch(
4629 &self,
4630 verb: &str,
4631 _: Value,
4632 _: &VerbRegistry,
4633 _: &NamespaceToken,
4634 ) -> Result<Value, RuntimeError> {
4635 Err(RuntimeError::InvalidInput(format!(
4636 "ADepPack has no verbs: {verb}"
4637 )))
4638 }
4639 }
4640
4641 #[async_trait]
4642 impl PackRuntime for BDepPack {
4643 fn name(&self) -> &str {
4644 Self::NAME
4645 }
4646 fn note_kinds(&self) -> &'static [&'static str] {
4647 Self::NOTE_KINDS
4648 }
4649 fn entity_kinds(&self) -> &'static [&'static str] {
4650 Self::ENTITY_KINDS
4651 }
4652 fn handlers(&self) -> &'static [HandlerDef] {
4653 Self::HANDLERS
4654 }
4655 fn requires(&self) -> &'static [&'static str] {
4656 Self::REQUIRES
4657 }
4658 async fn dispatch(
4659 &self,
4660 verb: &str,
4661 _: Value,
4662 _: &VerbRegistry,
4663 _: &NamespaceToken,
4664 ) -> Result<Value, RuntimeError> {
4665 Err(RuntimeError::InvalidInput(format!(
4666 "BDepPack has no verbs: {verb}"
4667 )))
4668 }
4669 }
4670
4671 #[test]
4672 fn test_pack_deps_happy_path() {
4673 let mut builder = VerbRegistryBuilder::new();
4674 builder.register(MemoryDepPack);
4675 builder.register(KgDepPack);
4676 let reg = builder
4677 .build()
4678 .expect("kg_dep satisfies memory_dep dependency");
4679 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
4680 let names = reg.pack_names();
4681 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
4682 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
4683 assert!(
4684 kg_pos < mem_pos,
4685 "kg_dep must be loaded before memory_dep; order: {names:?}"
4686 );
4687 }
4688
4689 #[test]
4690 fn test_pack_deps_missing() {
4691 let mut builder = VerbRegistryBuilder::new();
4692 builder.register(MemoryDepPack);
4693 let err = match builder.build() {
4694 Ok(_) => panic!("expected Err, got Ok"),
4695 Err(e) => e,
4696 };
4697 assert!(
4698 matches!(err, RuntimeError::MissingPackDependency(_)),
4699 "expected MissingPackDependency, got {err:?}"
4700 );
4701 let msg = err.to_string();
4702 assert!(
4703 msg.contains("memory_dep"),
4704 "error must name the dependent pack: {msg}"
4705 );
4706 assert!(
4707 msg.contains("kg_dep"),
4708 "error must name the missing dep: {msg}"
4709 );
4710 }
4711
4712 #[test]
4713 fn test_pack_deps_circular() {
4714 let mut builder = VerbRegistryBuilder::new();
4715 builder.register(ADepPack);
4716 builder.register(BDepPack);
4717 let err = match builder.build() {
4718 Ok(_) => panic!("expected Err, got Ok"),
4719 Err(e) => e,
4720 };
4721 assert!(
4722 matches!(err, RuntimeError::CircularPackDependency(_)),
4723 "expected CircularPackDependency, got {err:?}"
4724 );
4725 let msg = err.to_string();
4726 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
4727 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
4728 }
4729
4730 #[test]
4731 fn test_pack_deps_no_deps() {
4732 struct NoDepsA;
4733 struct NoDepsB;
4734
4735 impl Pack for NoDepsA {
4736 const NAME: &'static str = "no_deps_a";
4737 const NOTE_KINDS: &'static [&'static str] = &[];
4738 const ENTITY_KINDS: &'static [&'static str] = &[];
4739 const HANDLERS: &'static [HandlerDef] = &[];
4740 }
4741
4742 impl Pack for NoDepsB {
4743 const NAME: &'static str = "no_deps_b";
4744 const NOTE_KINDS: &'static [&'static str] = &[];
4745 const ENTITY_KINDS: &'static [&'static str] = &[];
4746 const HANDLERS: &'static [HandlerDef] = &[];
4747 }
4748
4749 #[async_trait]
4750 impl PackRuntime for NoDepsA {
4751 fn name(&self) -> &str {
4752 Self::NAME
4753 }
4754 fn note_kinds(&self) -> &'static [&'static str] {
4755 Self::NOTE_KINDS
4756 }
4757 fn entity_kinds(&self) -> &'static [&'static str] {
4758 Self::ENTITY_KINDS
4759 }
4760 fn handlers(&self) -> &'static [HandlerDef] {
4761 Self::HANDLERS
4762 }
4763 async fn dispatch(
4764 &self,
4765 verb: &str,
4766 _: Value,
4767 _: &VerbRegistry,
4768 _: &NamespaceToken,
4769 ) -> Result<Value, RuntimeError> {
4770 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
4771 }
4772 }
4773
4774 #[async_trait]
4775 impl PackRuntime for NoDepsB {
4776 fn name(&self) -> &str {
4777 Self::NAME
4778 }
4779 fn note_kinds(&self) -> &'static [&'static str] {
4780 Self::NOTE_KINDS
4781 }
4782 fn entity_kinds(&self) -> &'static [&'static str] {
4783 Self::ENTITY_KINDS
4784 }
4785 fn handlers(&self) -> &'static [HandlerDef] {
4786 Self::HANDLERS
4787 }
4788 async fn dispatch(
4789 &self,
4790 verb: &str,
4791 _: Value,
4792 _: &VerbRegistry,
4793 _: &NamespaceToken,
4794 ) -> Result<Value, RuntimeError> {
4795 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
4796 }
4797 }
4798
4799 let mut builder = VerbRegistryBuilder::new();
4800 builder.register(NoDepsA);
4801 builder.register(NoDepsB);
4802 let reg = builder.build().expect("packs with REQUIRES=&[] build");
4803 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
4804 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
4805 }
4806}
4807
4808#[cfg(test)]
4811mod hook_tests {
4812 use super::*;
4813 use async_trait::async_trait;
4814 use khive_types::Pack;
4815 use std::sync::atomic::{AtomicUsize, Ordering};
4816 use std::sync::Mutex as StdMutex;
4817
4818 struct SimplePack;
4819
4820 impl Pack for SimplePack {
4821 const NAME: &'static str = "simple";
4822 const NOTE_KINDS: &'static [&'static str] = &[];
4823 const ENTITY_KINDS: &'static [&'static str] = &[];
4824 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
4825 name: "ping",
4826 description: "ping",
4827 visibility: Visibility::Verb,
4828 category: VerbCategory::Assertive,
4829 params: &[],
4830 }];
4831 }
4832
4833 #[async_trait]
4834 impl PackRuntime for SimplePack {
4835 fn name(&self) -> &str {
4836 SimplePack::NAME
4837 }
4838 fn note_kinds(&self) -> &'static [&'static str] {
4839 SimplePack::NOTE_KINDS
4840 }
4841 fn entity_kinds(&self) -> &'static [&'static str] {
4842 SimplePack::ENTITY_KINDS
4843 }
4844 fn handlers(&self) -> &'static [HandlerDef] {
4845 SimplePack::HANDLERS
4846 }
4847 async fn dispatch(
4848 &self,
4849 verb: &str,
4850 _params: Value,
4851 _registry: &VerbRegistry,
4852 _token: &NamespaceToken,
4853 ) -> Result<Value, RuntimeError> {
4854 Ok(serde_json::json!({ "verb": verb }))
4855 }
4856 }
4857
4858 #[derive(Default)]
4860 struct CountingHook {
4861 calls: AtomicUsize,
4862 last_verb: StdMutex<String>,
4863 }
4864
4865 #[async_trait]
4866 impl DispatchHook for CountingHook {
4867 async fn on_dispatch(&self, view: &EventView) {
4868 self.calls.fetch_add(1, Ordering::SeqCst);
4869 *self.last_verb.lock().unwrap() = view.event.verb.clone();
4870 }
4871 }
4872
4873 #[tokio::test]
4874 async fn dispatch_hook_fires_on_successful_dispatch() {
4875 let hook = Arc::new(CountingHook::default());
4876 let mut builder = VerbRegistryBuilder::new();
4877 builder.register(SimplePack);
4878 builder.with_dispatch_hook(hook.clone());
4879 let reg = builder.build().expect("registry builds");
4880
4881 reg.dispatch("ping", Value::Null).await.unwrap();
4882
4883 assert_eq!(
4884 hook.calls.load(Ordering::SeqCst),
4885 1,
4886 "hook must fire once per successful dispatch"
4887 );
4888 assert_eq!(
4889 hook.last_verb.lock().unwrap().as_str(),
4890 "ping",
4891 "hook event must carry the dispatched verb"
4892 );
4893 }
4894
4895 #[tokio::test]
4896 async fn dispatch_hook_fires_multiple_times() {
4897 let hook = Arc::new(CountingHook::default());
4898 let mut builder = VerbRegistryBuilder::new();
4899 builder.register(SimplePack);
4900 builder.with_dispatch_hook(hook.clone());
4901 let reg = builder.build().expect("registry builds");
4902
4903 reg.dispatch("ping", Value::Null).await.unwrap();
4904 reg.dispatch("ping", Value::Null).await.unwrap();
4905 reg.dispatch("ping", Value::Null).await.unwrap();
4906
4907 assert_eq!(
4908 hook.calls.load(Ordering::SeqCst),
4909 3,
4910 "hook must fire once per successful dispatch"
4911 );
4912 }
4913
4914 #[tokio::test]
4915 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
4916 let hook = Arc::new(CountingHook::default());
4917 let mut builder = VerbRegistryBuilder::new();
4918 builder.register(SimplePack);
4919 builder.with_dispatch_hook(hook.clone());
4920 let reg = builder.build().expect("registry builds");
4921
4922 let _ = reg.dispatch("nonexistent", Value::Null).await;
4923
4924 assert_eq!(
4925 hook.calls.load(Ordering::SeqCst),
4926 0,
4927 "hook must NOT fire for unknown verb (dispatch returns error)"
4928 );
4929 }
4930
4931 #[tokio::test]
4932 async fn dispatch_hook_does_not_fire_on_gate_deny() {
4933 use khive_gate::{Gate, GateDecision, GateError};
4934
4935 #[derive(Debug)]
4936 struct AlwaysDenyGate;
4937 impl Gate for AlwaysDenyGate {
4938 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
4939 Ok(GateDecision::deny("test deny"))
4940 }
4941 }
4942
4943 let hook = Arc::new(CountingHook::default());
4944 let mut builder = VerbRegistryBuilder::new();
4945 builder.register(SimplePack);
4946 builder.with_gate(Arc::new(AlwaysDenyGate));
4947 builder.with_dispatch_hook(hook.clone());
4948 let reg = builder.build().expect("registry builds");
4949
4950 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
4951 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
4952
4953 assert_eq!(
4954 hook.calls.load(Ordering::SeqCst),
4955 0,
4956 "hook must NOT fire when gate denies dispatch"
4957 );
4958 }
4959
4960 #[tokio::test]
4961 async fn dispatch_hook_event_carries_namespace_from_params() {
4962 let hook = Arc::new(CountingHook::default());
4963
4964 #[derive(Default)]
4965 struct NsCapturingHook {
4966 ns: StdMutex<String>,
4967 }
4968
4969 #[async_trait]
4970 impl DispatchHook for NsCapturingHook {
4971 async fn on_dispatch(&self, view: &EventView) {
4972 *self.ns.lock().unwrap() = view.event.namespace.clone();
4973 }
4974 }
4975
4976 let ns_hook = Arc::new(NsCapturingHook::default());
4977 let mut builder = VerbRegistryBuilder::new();
4978 builder.register(SimplePack);
4979 builder.with_dispatch_hook(ns_hook.clone());
4980 let reg = builder.build().expect("registry builds");
4981
4982 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
4983 .await
4984 .unwrap();
4985
4986 assert_eq!(
4987 ns_hook.ns.lock().unwrap().as_str(),
4988 "tenant-abc",
4989 "dispatch hook event must carry the resolved namespace"
4990 );
4991
4992 drop(hook);
4994 }
4995
4996 #[tokio::test]
4997 async fn no_dispatch_hook_configured_dispatch_succeeds() {
4998 let mut builder = VerbRegistryBuilder::new();
5000 builder.register(SimplePack);
5001 let reg = builder.build().expect("registry builds");
5003
5004 let res = reg.dispatch("ping", Value::Null).await.unwrap();
5005 assert_eq!(res["verb"], "ping");
5006 }
5007}
5008
5009#[cfg(test)]
5012mod help_tests {
5013 use super::*;
5014 use async_trait::async_trait;
5015 use khive_types::Pack;
5016 use std::sync::{
5017 atomic::{AtomicUsize, Ordering},
5018 Arc,
5019 };
5020
5021 static CREATE_PARAMS: [ParamDef; 2] = [
5026 ParamDef {
5027 name: "kind",
5028 param_type: "string",
5029 required: true,
5030 description: "Granular kind (concept | document | ...).",
5031 },
5032 ParamDef {
5033 name: "name",
5034 param_type: "string",
5035 required: false,
5036 description: "Human-readable name.",
5037 },
5038 ];
5039
5040 static RECALL_PARAMS: [ParamDef; 2] = [
5041 ParamDef {
5042 name: "query",
5043 param_type: "string",
5044 required: true,
5045 description: "Semantic recall query.",
5046 },
5047 ParamDef {
5048 name: "limit",
5049 param_type: "integer",
5050 required: false,
5051 description: "Maximum memories to return.",
5052 },
5053 ];
5054
5055 static EMBED_PARAMS: [ParamDef; 0] = [];
5058
5059 struct HelpPack {
5060 invocations: Arc<AtomicUsize>,
5061 }
5062
5063 impl Pack for HelpPack {
5064 const NAME: &'static str = "helptest";
5065 const NOTE_KINDS: &'static [&'static str] = &[];
5066 const ENTITY_KINDS: &'static [&'static str] = &[];
5067 const HANDLERS: &'static [HandlerDef] = &[
5068 HandlerDef {
5069 name: "create",
5070 description: "Create an entity or note",
5071 visibility: Visibility::Verb,
5072 category: VerbCategory::Commissive,
5073 params: &CREATE_PARAMS,
5074 },
5075 HandlerDef {
5076 name: "recall",
5077 description: "Recall memory notes with decay-aware hybrid ranking",
5078 visibility: Visibility::Verb,
5079 category: VerbCategory::Assertive,
5080 params: &RECALL_PARAMS,
5081 },
5082 HandlerDef {
5085 name: "recall.embed",
5086 description: "Return the embedding vector used by memory recall",
5087 visibility: Visibility::Subhandler,
5088 category: VerbCategory::Assertive,
5089 params: &EMBED_PARAMS,
5090 },
5091 ];
5092 }
5093
5094 #[async_trait]
5095 impl PackRuntime for HelpPack {
5096 fn name(&self) -> &str {
5097 HelpPack::NAME
5098 }
5099 fn note_kinds(&self) -> &'static [&'static str] {
5100 HelpPack::NOTE_KINDS
5101 }
5102 fn entity_kinds(&self) -> &'static [&'static str] {
5103 HelpPack::ENTITY_KINDS
5104 }
5105 fn handlers(&self) -> &'static [HandlerDef] {
5106 HelpPack::HANDLERS
5107 }
5108 async fn dispatch(
5109 &self,
5110 verb: &str,
5111 _params: Value,
5112 _registry: &VerbRegistry,
5113 _token: &NamespaceToken,
5114 ) -> Result<Value, RuntimeError> {
5115 self.invocations.fetch_add(1, Ordering::SeqCst);
5116 Ok(serde_json::json!({ "pack": "helptest", "verb": verb }))
5117 }
5118 }
5119
5120 fn build_help_registry(invocations: Arc<AtomicUsize>) -> VerbRegistry {
5121 let mut builder = VerbRegistryBuilder::new();
5122 builder.register(HelpPack { invocations });
5123 builder.build().expect("help registry builds")
5124 }
5125
5126 #[tokio::test]
5129 async fn test_help_true_returns_schema_for_kg_create() {
5130 let invocations = Arc::new(AtomicUsize::new(0));
5131 let reg = build_help_registry(invocations.clone());
5132
5133 let result = reg
5134 .dispatch("create", serde_json::json!({ "help": true }))
5135 .await
5136 .expect("help=true must succeed for a known verb");
5137
5138 assert_eq!(result["verb"], "create", "envelope must name the verb");
5140 assert_eq!(
5141 result["pack"], "helptest",
5142 "envelope must name the owning pack"
5143 );
5144 assert!(
5145 result["description"].as_str().is_some(),
5146 "description must be a string"
5147 );
5148
5149 let params = result["params"]
5151 .as_array()
5152 .expect("params must be a JSON array");
5153 assert!(!params.is_empty(), "params array must not be empty");
5154
5155 let kind_param = params.iter().find(|p| p["name"] == "kind");
5157 assert!(
5158 kind_param.is_some(),
5159 "params array must include the 'kind' parameter"
5160 );
5161 let kind_param = kind_param.unwrap();
5162 assert_eq!(
5163 kind_param["required"],
5164 serde_json::json!(true),
5165 "'kind' must be required"
5166 );
5167 assert_eq!(kind_param["type"], "string", "'kind' type must be 'string'");
5168 }
5169
5170 #[tokio::test]
5172 async fn test_help_true_returns_schema_for_recall() {
5173 let invocations = Arc::new(AtomicUsize::new(0));
5174 let reg = build_help_registry(invocations.clone());
5175
5176 let result = reg
5177 .dispatch("recall", serde_json::json!({ "help": true }))
5178 .await
5179 .expect("help=true must succeed for recall");
5180
5181 assert_eq!(result["verb"], "recall");
5182 assert_eq!(result["pack"], "helptest");
5183
5184 let params = result["params"]
5185 .as_array()
5186 .expect("params must be a JSON array");
5187
5188 let query_param = params.iter().find(|p| p["name"] == "query");
5190 assert!(query_param.is_some(), "params must include 'query'");
5191 let query_param = query_param.unwrap();
5192 assert_eq!(
5193 query_param["required"],
5194 serde_json::json!(true),
5195 "'query' must be required"
5196 );
5197
5198 let limit_param = params.iter().find(|p| p["name"] == "limit");
5200 assert!(limit_param.is_some(), "params must include 'limit'");
5201 let limit_param = limit_param.unwrap();
5202 assert_eq!(
5203 limit_param["required"],
5204 serde_json::json!(false),
5205 "'limit' must be optional"
5206 );
5207 }
5208
5209 #[tokio::test]
5212 async fn test_help_true_does_not_execute_the_verb() {
5213 let invocations = Arc::new(AtomicUsize::new(0));
5214 let reg = build_help_registry(invocations.clone());
5215
5216 reg.dispatch("create", serde_json::json!({ "help": true }))
5218 .await
5219 .expect("help=true must succeed");
5220 reg.dispatch("recall", serde_json::json!({ "help": true }))
5221 .await
5222 .expect("help=true must succeed");
5223
5224 assert_eq!(
5225 invocations.load(Ordering::SeqCst),
5226 0,
5227 "pack dispatch MUST NOT be invoked when help=true; \
5228 got {} invocation(s)",
5229 invocations.load(Ordering::SeqCst)
5230 );
5231
5232 reg.dispatch("create", serde_json::json!({}))
5234 .await
5235 .expect("normal dispatch must succeed");
5236 assert_eq!(
5237 invocations.load(Ordering::SeqCst),
5238 1,
5239 "pack dispatch must fire exactly once for a normal call"
5240 );
5241 }
5242
5243 #[tokio::test]
5252 async fn help_true_on_subhandler_returns_callable_via_mcp_false() {
5253 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
5254
5255 let result = reg
5256 .dispatch("recall.embed", serde_json::json!({ "help": true }))
5257 .await
5258 .expect("help=true on subhandler must succeed (no permission check on help path)");
5259
5260 assert_eq!(
5261 result["callable_via_mcp"],
5262 serde_json::json!(false),
5263 "subhandler help must carry callable_via_mcp: false"
5264 );
5265 assert_eq!(
5266 result["visibility"], "internal",
5267 "subhandler help must carry visibility: internal"
5268 );
5269 assert_eq!(result["verb"], "recall.embed");
5272 assert_eq!(result["pack"], "helptest");
5273 }
5274
5275 #[tokio::test]
5277 async fn help_true_on_public_verb_does_not_have_callable_via_mcp_false() {
5278 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
5279
5280 let result = reg
5281 .dispatch("create", serde_json::json!({ "help": true }))
5282 .await
5283 .expect("help=true on public verb must succeed");
5284
5285 assert_ne!(
5287 result.get("callable_via_mcp"),
5288 Some(&serde_json::json!(false)),
5289 "public verb help must NOT carry callable_via_mcp: false"
5290 );
5291 assert_ne!(
5293 result.get("visibility"),
5294 Some(&serde_json::json!("internal")),
5295 "public verb help must NOT carry visibility: internal"
5296 );
5297 }
5298
5299 #[tokio::test]
5301 async fn help_true_on_unknown_verb_returns_error() {
5302 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
5303
5304 let err = reg
5305 .dispatch("nonexistent_verb", serde_json::json!({ "help": true }))
5306 .await
5307 .unwrap_err();
5308
5309 assert!(
5310 matches!(err, RuntimeError::InvalidInput(_)),
5311 "help=true on unknown verb must return InvalidInput, got {err:?}"
5312 );
5313 let msg = err.to_string();
5314 assert!(
5315 msg.contains("nonexistent_verb"),
5316 "error must name the unknown verb: {msg}"
5317 );
5318 }
5319
5320 #[tokio::test]
5322 async fn help_true_on_subhandler_includes_params_field() {
5323 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
5324
5325 let result = reg
5326 .dispatch("recall.embed", serde_json::json!({ "help": true }))
5327 .await
5328 .expect("help=true on subhandler must succeed");
5329
5330 let params = result
5332 .get("params")
5333 .expect("subhandler help must include 'params' field");
5334 assert!(
5335 params.is_array(),
5336 "subhandler help params must be a JSON array"
5337 );
5338 }
5339
5340 #[tokio::test]
5345 async fn help_true_unknown_verb_available_list_excludes_subhandlers() {
5346 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
5347
5348 let err = reg
5349 .dispatch("not_a_verb", serde_json::json!({ "help": true }))
5350 .await
5351 .unwrap_err();
5352
5353 let msg = err.to_string();
5354 assert!(
5357 !msg.contains("recall.embed"),
5358 "unknown-verb help error must not advertise subhandler recall.embed: {msg}"
5359 );
5360 assert!(
5362 msg.contains("create"),
5363 "unknown-verb help error must still list public verb 'create': {msg}"
5364 );
5365 assert!(
5366 msg.contains("recall"),
5367 "unknown-verb help error must still list public verb 'recall': {msg}"
5368 );
5369 }
5370
5371 #[tokio::test]
5373 async fn dispatch_unknown_verb_available_list_excludes_subhandlers() {
5374 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
5375
5376 let err = reg
5377 .dispatch("not_a_verb", serde_json::json!({}))
5378 .await
5379 .unwrap_err();
5380
5381 let msg = err.to_string();
5382 assert!(
5385 !msg.contains("recall.embed"),
5386 "dispatch unknown-verb error must not advertise subhandler recall.embed: {msg}"
5387 );
5388 assert!(
5390 msg.contains("create"),
5391 "dispatch unknown-verb error must still list public verb 'create': {msg}"
5392 );
5393 assert!(
5394 msg.contains("recall"),
5395 "dispatch unknown-verb error must still list public verb 'recall': {msg}"
5396 );
5397 }
5398
5399 struct SchemaPack {
5403 pack_name: &'static str,
5404 statements: &'static [&'static str],
5405 }
5406
5407 impl Pack for SchemaPack {
5408 const NAME: &'static str = "schema-pack";
5409 const NOTE_KINDS: &'static [&'static str] = &[];
5410 const ENTITY_KINDS: &'static [&'static str] = &[];
5411 const HANDLERS: &'static [HandlerDef] = &[];
5412 }
5413
5414 #[async_trait]
5415 impl PackRuntime for SchemaPack {
5416 fn name(&self) -> &str {
5417 self.pack_name
5418 }
5419 fn note_kinds(&self) -> &'static [&'static str] {
5420 &[]
5421 }
5422 fn entity_kinds(&self) -> &'static [&'static str] {
5423 &[]
5424 }
5425 fn handlers(&self) -> &'static [HandlerDef] {
5426 &[]
5427 }
5428 fn schema_plan(&self) -> SchemaPlan {
5429 SchemaPlan {
5430 pack: self.pack_name,
5431 statements: self.statements,
5432 }
5433 }
5434 async fn dispatch(
5435 &self,
5436 verb: &str,
5437 _params: Value,
5438 _registry: &VerbRegistry,
5439 _token: &NamespaceToken,
5440 ) -> Result<Value, RuntimeError> {
5441 Ok(serde_json::json!({ "pack": self.pack_name, "verb": verb }))
5442 }
5443 }
5444
5445 #[test]
5448 fn all_schema_plans_named_returns_correct_pairs() {
5449 let mut builder = VerbRegistryBuilder::new();
5450 builder.register_boxed(Box::new(SchemaPack {
5451 pack_name: "alpha",
5452 statements: &["CREATE TABLE IF NOT EXISTS t_alpha (id INTEGER PRIMARY KEY)"],
5453 }));
5454 builder.register_boxed(Box::new(SchemaPack {
5455 pack_name: "beta",
5456 statements: &[],
5457 }));
5458 let reg = builder.build().expect("registry builds");
5459
5460 let named = reg.all_schema_plans_named();
5461 assert_eq!(named.len(), 2);
5462
5463 let alpha_entry = named.iter().find(|(n, _)| *n == "alpha");
5464 let beta_entry = named.iter().find(|(n, _)| *n == "beta");
5465
5466 assert!(alpha_entry.is_some(), "alpha must appear in named plans");
5467 assert!(beta_entry.is_some(), "beta must appear in named plans");
5468
5469 let (_, alpha_plan) = alpha_entry.unwrap();
5470 assert_eq!(alpha_plan.statements.len(), 1);
5471 assert!(!alpha_plan.is_empty());
5472
5473 let (_, beta_plan) = beta_entry.unwrap();
5474 assert!(beta_plan.is_empty());
5475 }
5476
5477 #[tokio::test]
5494 async fn apply_schema_plans_with_map_routes_to_correct_backend() {
5495 use khive_storage::types::{SqlStatement, SqlValue};
5496
5497 let default_backend = khive_db::StorageBackend::memory().expect("default memory backend");
5498 let pack_backend =
5499 khive_db::StorageBackend::memory().expect("pack-specific memory backend");
5500
5501 let mut builder = VerbRegistryBuilder::new();
5502 builder.register_boxed(Box::new(SchemaPack {
5503 pack_name: "routed",
5504 statements: &["CREATE TABLE IF NOT EXISTS t_routed (id INTEGER PRIMARY KEY)"],
5505 }));
5506 let reg = builder.build().expect("registry builds");
5507
5508 let mut backend_map: HashMap<&str, &khive_db::StorageBackend> = HashMap::new();
5509 backend_map.insert("routed", &pack_backend);
5510
5511 reg.apply_schema_plans_with_map(&backend_map, &default_backend)
5512 .expect("schema application must not collide");
5513
5514 let mut writer = pack_backend.sql().writer().await.expect("writer");
5516 let result = writer
5517 .execute(SqlStatement {
5518 sql: "INSERT INTO t_routed (id) VALUES (?1)".into(),
5519 params: vec![SqlValue::Integer(1)],
5520 label: None,
5521 })
5522 .await;
5523 assert!(
5524 result.is_ok(),
5525 "t_routed must exist on pack_backend after routing: {result:?}"
5526 );
5527
5528 let mut default_writer = default_backend.sql().writer().await.expect("writer");
5530 let default_result = default_writer
5531 .execute(SqlStatement {
5532 sql: "INSERT INTO t_routed (id) VALUES (?1)".into(),
5533 params: vec![SqlValue::Integer(2)],
5534 label: None,
5535 })
5536 .await;
5537 assert!(
5538 default_result.is_err(),
5539 "t_routed must NOT exist on default_backend (table should not be there)"
5540 );
5541 }
5542
5543 #[tokio::test]
5546 async fn apply_schema_plans_with_map_falls_back_to_default_for_unmapped_packs() {
5547 use khive_storage::types::{SqlStatement, SqlValue};
5548
5549 let default_backend = khive_db::StorageBackend::memory().expect("default memory backend");
5550
5551 let mut builder = VerbRegistryBuilder::new();
5552 builder.register_boxed(Box::new(SchemaPack {
5553 pack_name: "unmapped",
5554 statements: &["CREATE TABLE IF NOT EXISTS t_unmapped (id INTEGER PRIMARY KEY)"],
5555 }));
5556 let reg = builder.build().expect("registry builds");
5557
5558 let backend_map: HashMap<&str, &khive_db::StorageBackend> = HashMap::new();
5559 reg.apply_schema_plans_with_map(&backend_map, &default_backend)
5560 .expect("schema application must not collide");
5561
5562 let mut writer = default_backend.sql().writer().await.expect("writer");
5564 let result = writer
5565 .execute(SqlStatement {
5566 sql: "INSERT INTO t_unmapped (id) VALUES (?1)".into(),
5567 params: vec![SqlValue::Integer(1)],
5568 label: None,
5569 })
5570 .await;
5571 assert!(
5572 result.is_ok(),
5573 "t_unmapped must exist on default_backend for unmapped pack: {result:?}"
5574 );
5575 }
5576
5577 #[test]
5582 fn apply_schema_plans_with_map_collision_is_an_error() {
5583 let backend = khive_db::StorageBackend::memory().expect("memory backend");
5584 let empty_map: HashMap<&str, &khive_db::StorageBackend> = HashMap::new();
5585
5586 let mut builder = VerbRegistryBuilder::new();
5587 builder.register_boxed(Box::new(SchemaPack {
5588 pack_name: "pack_alpha",
5589 statements: &["CREATE TABLE IF NOT EXISTS collision_table (id INTEGER PRIMARY KEY)"],
5590 }));
5591 builder.register_boxed(Box::new(SchemaPack {
5592 pack_name: "pack_beta",
5593 statements: &["CREATE TABLE IF NOT EXISTS collision_table (id INTEGER PRIMARY KEY)"],
5594 }));
5595 let registry = builder.build().expect("registry builds");
5596
5597 let result = registry.apply_schema_plans_with_map(&empty_map, &backend);
5598 assert!(
5599 result.is_err(),
5600 "two packs declaring the same table on the same backend must produce a collision error"
5601 );
5602 let err = result.unwrap_err();
5603 let msg = err.to_string();
5604 assert!(
5605 msg.contains("pack_alpha"),
5606 "collision error must name first pack; got: {msg}"
5607 );
5608 assert!(
5609 msg.contains("pack_beta"),
5610 "collision error must name second pack; got: {msg}"
5611 );
5612 assert!(
5613 msg.contains("collision_table"),
5614 "collision error must name the table; got: {msg}"
5615 );
5616 }
5617}