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::{ActorRef, 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 async fn warm(&self) {}
204
205 async fn dispatch(
212 &self,
213 verb: &str,
214 params: Value,
215 registry: &VerbRegistry,
216 token: &NamespaceToken,
217 ) -> Result<Value, RuntimeError>;
218}
219
220#[async_trait]
234pub trait KindHook: Send + Sync + std::fmt::Debug {
235 async fn prepare_create(
241 &self,
242 runtime: &KhiveRuntime,
243 args: &mut Value,
244 ) -> Result<(), RuntimeError>;
245
246 async fn after_create(
255 &self,
256 runtime: &KhiveRuntime,
257 id: uuid::Uuid,
258 args: &Value,
259 ) -> Result<(), RuntimeError>;
260}
261
262pub struct VerbRegistryBuilder {
267 packs: Vec<Box<dyn PackRuntime>>,
268 gate: GateRef,
269 default_namespace: String,
270 event_store: Option<Arc<dyn EventStore>>,
277 dispatch_hook: Option<Arc<dyn DispatchHook>>,
283}
284
285impl VerbRegistryBuilder {
286 pub fn new() -> Self {
288 Self {
289 packs: Vec::new(),
290 gate: std::sync::Arc::new(AllowAllGate),
291 default_namespace: Namespace::local().as_str().to_string(),
292 event_store: None,
293 dispatch_hook: None,
294 }
295 }
296
297 pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
300 self.packs.push(Box::new(pack));
301 self
302 }
303
304 pub(crate) fn register_boxed(&mut self, pack: Box<dyn PackRuntime>) -> &mut Self {
311 self.packs.push(pack);
312 self
313 }
314
315 pub fn with_gate(&mut self, gate: GateRef) -> &mut Self {
322 self.gate = gate;
323 self
324 }
325
326 pub fn with_default_namespace(&mut self, ns: impl Into<String>) -> &mut Self {
331 self.default_namespace = ns.into();
332 self
333 }
334
335 pub fn with_event_store(&mut self, store: Arc<dyn EventStore>) -> &mut Self {
344 self.event_store = Some(store);
345 self
346 }
347
348 pub fn with_dispatch_hook(&mut self, hook: Arc<dyn DispatchHook>) -> &mut Self {
359 self.dispatch_hook = Some(hook);
360 self
361 }
362
363 pub fn build(self) -> Result<VerbRegistry, RuntimeError> {
369 let packs = self.packs;
370 let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(packs.len());
371 for (idx, pack) in packs.iter().enumerate() {
372 if let Some(prev_idx) = name_to_idx.insert(pack.name(), idx) {
373 return Err(RuntimeError::PackRedeclared {
374 name: pack.name().to_string(),
375 first_idx: prev_idx,
376 second_idx: idx,
377 });
378 }
379 }
380
381 let mut missing: Vec<MissingPackDependency> = Vec::new();
382 let mut indegree = vec![0usize; packs.len()];
383 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); packs.len()];
384
385 for (idx, pack) in packs.iter().enumerate() {
386 for &requires in pack.requires() {
387 match name_to_idx.get(requires).copied() {
388 Some(dep_idx) => {
389 dependents[dep_idx].push(idx);
390 indegree[idx] += 1;
391 }
392 None => missing.push(MissingPackDependency {
393 from: pack.name().to_string(),
394 requires: requires.to_string(),
395 }),
396 }
397 }
398 }
399
400 if !missing.is_empty() {
401 return if missing.len() == 1 {
402 Err(RuntimeError::MissingPackDependency(missing.remove(0)))
403 } else {
404 Err(RuntimeError::MissingPackDependencies(
405 MissingPackDependencies { missing },
406 ))
407 };
408 }
409
410 let mut ready: VecDeque<usize> = indegree
411 .iter()
412 .enumerate()
413 .filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
414 .collect();
415 let mut ordered_indices = Vec::with_capacity(packs.len());
416
417 while let Some(idx) = ready.pop_front() {
418 ordered_indices.push(idx);
419 for &dep_idx in &dependents[idx] {
420 indegree[dep_idx] -= 1;
421 if indegree[dep_idx] == 0 {
422 ready.push_back(dep_idx);
423 }
424 }
425 }
426
427 if ordered_indices.len() != packs.len() {
428 let cycle_nodes: HashSet<usize> = indegree
429 .iter()
430 .enumerate()
431 .filter_map(|(idx, degree)| (*degree > 0).then_some(idx))
432 .collect();
433 let cycle = find_pack_dependency_cycle(&packs, &name_to_idx, &cycle_nodes);
434 return Err(RuntimeError::CircularPackDependency(
435 CircularPackDependency { cycle },
436 ));
437 }
438
439 let mut slots: Vec<Option<Box<dyn PackRuntime>>> = packs.into_iter().map(Some).collect();
440 let ordered_packs: Vec<Box<dyn PackRuntime>> = ordered_indices
441 .into_iter()
442 .map(|idx| slots[idx].take().expect("topological index must exist"))
443 .collect();
444
445 validate_unique_note_kinds(&ordered_packs)?;
446 validate_unique_verb_names(&ordered_packs)?;
447
448 Ok(VerbRegistry {
449 packs: Arc::new(ordered_packs),
450 gate: self.gate,
451 default_namespace: self.default_namespace,
452 event_store: self.event_store,
453 dispatch_hook: self.dispatch_hook,
454 })
455 }
456}
457
458fn validate_unique_note_kinds(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
464 let mut seen: HashMap<&str, &str> = HashMap::new();
465 for pack in packs {
466 for &kind in pack.note_kinds() {
467 if let Some(first_pack) = seen.insert(kind, pack.name()) {
468 return Err(RuntimeError::InvalidInput(format!(
469 "duplicate note kind {kind:?}: claimed by both {first_pack:?} and {:?}",
470 pack.name()
471 )));
472 }
473 }
474 }
475 Ok(())
476}
477
478fn validate_unique_verb_names(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
486 let mut seen: HashMap<&str, &str> = HashMap::new();
487 for pack in packs {
488 for handler in pack.handlers() {
489 if !matches!(handler.visibility, Visibility::Verb) {
490 continue;
491 }
492 if let Some(first_pack) = seen.insert(handler.name, pack.name()) {
493 return Err(RuntimeError::VerbCollision {
494 verb: handler.name.to_string(),
495 first_pack: first_pack.to_string(),
496 second_pack: pack.name().to_string(),
497 });
498 }
499 }
500 }
501 Ok(())
502}
503
504fn find_pack_dependency_cycle(
505 packs: &[Box<dyn PackRuntime>],
506 name_to_idx: &HashMap<&str, usize>,
507 cycle_nodes: &HashSet<usize>,
508) -> Vec<String> {
509 fn visit(
510 idx: usize,
511 packs: &[Box<dyn PackRuntime>],
512 name_to_idx: &HashMap<&str, usize>,
513 cycle_nodes: &HashSet<usize>,
514 visiting: &mut Vec<usize>,
515 visited: &mut HashSet<usize>,
516 ) -> Option<Vec<String>> {
517 if let Some(pos) = visiting.iter().position(|&seen| seen == idx) {
518 let mut cycle: Vec<String> = visiting[pos..]
519 .iter()
520 .map(|&i| packs[i].name().to_string())
521 .collect();
522 cycle.push(packs[idx].name().to_string());
523 return Some(cycle);
524 }
525 if !visited.insert(idx) {
526 return None;
527 }
528 visiting.push(idx);
529 for &req in packs[idx].requires() {
530 let Some(&dep_idx) = name_to_idx.get(req) else {
531 continue;
532 };
533 if cycle_nodes.contains(&dep_idx) {
534 if let Some(cycle) =
535 visit(dep_idx, packs, name_to_idx, cycle_nodes, visiting, visited)
536 {
537 return Some(cycle);
538 }
539 }
540 }
541 visiting.pop();
542 None
543 }
544
545 let mut visited = HashSet::new();
546 for &idx in cycle_nodes {
547 let mut visiting = Vec::new();
548 if let Some(cycle) = visit(
549 idx,
550 packs,
551 name_to_idx,
552 cycle_nodes,
553 &mut visiting,
554 &mut visited,
555 ) {
556 return cycle;
557 }
558 }
559 cycle_nodes
560 .iter()
561 .map(|&idx| packs[idx].name().to_string())
562 .collect()
563}
564
565impl Default for VerbRegistryBuilder {
566 fn default() -> Self {
567 Self::new()
568 }
569}
570
571#[derive(Clone)]
575pub struct VerbRegistry {
576 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
577 gate: GateRef,
578 default_namespace: String,
579 event_store: Option<Arc<dyn EventStore>>,
581 dispatch_hook: Option<Arc<dyn DispatchHook>>,
583}
584
585impl VerbRegistry {
586 pub fn describe_verb(&self, verb: &str) -> Result<Value, RuntimeError> {
593 for pack in self.packs.iter() {
594 for handler in pack.handlers().iter() {
595 if handler.name == verb {
596 let category = format!("{:?}", handler.category);
597 let params_arr: Vec<Value> = handler
598 .params
599 .iter()
600 .map(|p| {
601 serde_json::json!({
602 "name": p.name,
603 "type": p.param_type,
604 "required": p.required,
605 "description": p.description,
606 })
607 })
608 .collect();
609 if matches!(handler.visibility, Visibility::Subhandler) {
614 return Ok(serde_json::json!({
615 "verb": verb,
616 "pack": pack.name(),
617 "description": handler.description,
618 "category": category,
619 "params": params_arr,
620 "visibility": "internal",
621 "callable_via_mcp": false,
622 "note": "This is an internal subhandler. Calling it via the MCP \
623 request surface returns permission denied. It can only be \
624 invoked by internal runtime callers.",
625 }));
626 }
627 return Ok(serde_json::json!({
628 "verb": verb,
629 "pack": pack.name(),
630 "description": handler.description,
631 "category": category,
632 "params": params_arr,
633 }));
634 }
635 }
636 }
637 let available: Vec<&str> = self
640 .packs
641 .iter()
642 .flat_map(|p| p.handlers().iter())
643 .filter(|h| matches!(h.visibility, Visibility::Verb))
644 .map(|h| h.name)
645 .collect();
646 Err(RuntimeError::InvalidInput(format!(
647 "unknown verb {verb:?}; available: {}",
648 available.join(", ")
649 )))
650 }
651
652 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
658 if params.get("help").and_then(Value::as_bool) == Some(true) {
660 return self.describe_verb(verb);
661 }
662 let ns_str: String = params
665 .get("namespace")
666 .and_then(Value::as_str)
667 .map(str::to_string)
668 .unwrap_or_else(|| self.default_namespace.clone());
669 let ns = Namespace::parse(&ns_str)
670 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?;
671 let gate_req = GateRequest::new(ActorRef::anonymous(), ns, verb, params.clone());
672
673 let gate_blocked = match self.gate.check(&gate_req) {
679 Ok(decision) => {
680 let is_deny = matches!(decision, GateDecision::Deny { .. });
681
682 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
684 tracing::info!(
685 audit_event = %serde_json::to_string(&audit)
686 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
687 "gate.check"
688 );
689
690 if let Some(store) = &self.event_store {
692 let outcome = if is_deny {
693 EventOutcome::Denied
694 } else {
695 EventOutcome::Success
696 };
697 let audit_data = serde_json::to_value(&audit).unwrap_or_else(|e| {
698 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
699 serde_json::Value::Null
700 });
701 let mut storage_event = Event::new(
702 gate_req.namespace.as_str(),
703 verb,
704 EventKind::Audit,
705 SubstrateKind::Event,
706 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
707 )
708 .with_outcome(outcome)
709 .with_payload(audit_data);
710 if let Some(target_id) = target_id_from_args(&gate_req.args) {
711 storage_event = storage_event.with_target(target_id);
712 }
713 if let Err(store_err) = store.append_event(storage_event).await {
714 tracing::warn!(
715 verb,
716 error = %store_err,
717 "audit event store write failed (non-fatal)"
718 );
719 }
720 }
721
722 if is_deny {
723 let reason = match decision {
724 GateDecision::Deny { reason } => reason,
725 _ => String::new(),
726 };
727 Some(reason)
728 } else {
729 None
730 }
731 }
732 Err(err) => {
733 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
736 None
737 }
738 };
739
740 if let Some(reason) = gate_blocked {
742 return Err(RuntimeError::PermissionDenied {
743 verb: verb.to_string(),
744 reason,
745 });
746 }
747
748 let token = NamespaceToken::mint_authorized(
751 Namespace::parse(&ns_str)
752 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?,
753 ActorRef::anonymous(),
754 );
755
756 for pack in self.packs.iter() {
757 if let Some(handler_def) = pack.handlers().iter().find(|v| v.name == verb) {
758 let handler_accepts_namespace =
768 handler_def.params.iter().any(|p| p.name == "namespace");
769 let params = if !handler_accepts_namespace {
770 if let Value::Object(mut map) = params {
771 map.remove("namespace");
772 Value::Object(map)
773 } else {
774 params
775 }
776 } else {
777 params
778 };
779 let dispatch_start = Instant::now();
780 let result = pack.dispatch(verb, params, self, &token).await;
781 let dispatch_us = dispatch_start.elapsed().as_micros() as i64;
782
783 if let (Ok(ref ok_val), Some(hook)) = (&result, &self.dispatch_hook) {
785 let mut dispatch_event = Event::new(
786 ns_str.as_str(),
787 verb,
788 EventKind::Audit,
789 SubstrateKind::Event,
790 pack.name(),
791 )
792 .with_outcome(EventOutcome::Success)
793 .with_duration_us(dispatch_us);
794
795 if verb == "memory.recall" {
799 let first_note_id = ok_val
800 .as_array()
801 .and_then(|arr| arr.first())
802 .and_then(|v| v.get("note_id"))
803 .and_then(|v| v.as_str())
804 .and_then(|s| s.parse::<uuid::Uuid>().ok());
805 if let Some(note_id) = first_note_id {
806 dispatch_event = dispatch_event.with_target(note_id);
807 }
808 }
811
812 let dispatch_view = EventView {
813 event: dispatch_event,
814 observations: Vec::new(),
815 };
816 let hook = Arc::clone(hook);
817 hook.on_dispatch(&dispatch_view).await;
818 }
819
820 return result;
821 }
822 }
823 let available: Vec<&str> = self
826 .packs
827 .iter()
828 .flat_map(|p| p.handlers().iter())
829 .filter(|h| matches!(h.visibility, Visibility::Verb))
830 .map(|h| h.name)
831 .collect();
832 Err(RuntimeError::InvalidInput(format!(
833 "unknown verb {verb:?}; available: {}",
834 available.join(", ")
835 )))
836 }
837
838 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
845 for pack in self.packs.iter() {
846 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
847 if owns {
848 if let Some(hook) = pack.kind_hook(kind) {
849 return Some(hook);
850 }
851 }
852 }
853 None
854 }
855
856 pub fn all_verbs(&self) -> Vec<&'static HandlerDef> {
862 self.packs
863 .iter()
864 .flat_map(|p| p.handlers().iter())
865 .filter(|h| matches!(h.visibility, Visibility::Verb))
866 .collect()
867 }
868
869 pub fn all_verbs_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
876 self.packs
877 .iter()
878 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
879 .filter(|(_, h)| matches!(h.visibility, Visibility::Verb))
880 .collect()
881 }
882
883 pub fn all_handlers_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
889 self.packs
890 .iter()
891 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
892 .collect()
893 }
894
895 pub fn all_note_kinds(&self) -> Vec<&'static str> {
898 let mut seen = std::collections::HashSet::new();
899 self.packs
900 .iter()
901 .flat_map(|p| p.note_kinds().iter().copied())
902 .filter(|k| seen.insert(*k))
903 .collect()
904 }
905
906 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
909 let mut seen = std::collections::HashSet::new();
910 self.packs
911 .iter()
912 .flat_map(|p| p.entity_kinds().iter().copied())
913 .filter(|k| seen.insert(*k))
914 .collect()
915 }
916
917 pub fn pack_names(&self) -> Vec<&str> {
919 self.packs.iter().map(|p| p.name()).collect()
920 }
921
922 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
924 self.packs
925 .iter()
926 .find(|p| p.name() == name)
927 .map(|p| p.requires())
928 }
929
930 pub fn pack_note_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
935 self.packs
936 .iter()
937 .find(|p| p.name() == name)
938 .map(|p| p.note_kinds())
939 }
940
941 pub fn pack_entity_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
946 self.packs
947 .iter()
948 .find(|p| p.name() == name)
949 .map(|p| p.entity_kinds())
950 }
951
952 pub fn pack_verbs(&self, name: &str) -> Option<&'static [HandlerDef]> {
957 self.packs
958 .iter()
959 .find(|p| p.name() == name)
960 .map(|p| p.handlers())
961 }
962
963 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
969 self.packs
970 .iter()
971 .flat_map(|p| p.edge_rules().iter().copied())
972 .collect()
973 }
974
975 pub fn all_note_kind_specs(&self) -> Vec<&'static NoteKindSpec> {
979 self.packs
980 .iter()
981 .flat_map(|p| p.note_kind_specs().iter())
982 .collect()
983 }
984
985 pub fn all_validation_rules(&self) -> Vec<&'static ValidationRule> {
991 self.packs
992 .iter()
993 .flat_map(|p| p.validation_rules().iter())
994 .collect()
995 }
996
997 pub fn all_schema_plans(&self) -> Vec<SchemaPlan> {
1004 self.packs.iter().map(|p| p.schema_plan()).collect()
1005 }
1006
1007 pub fn call_register_embedders(&self, runtime: &KhiveRuntime) {
1017 for pack in self.packs.iter() {
1018 pack.register_embedders(runtime);
1019 }
1020 }
1021
1022 pub async fn call_warm_all(&self) {
1026 for pack in self.packs.iter() {
1027 pack.warm().await;
1028 }
1029 }
1030
1031 pub fn presentation_policy_for(&self, verb: &str) -> khive_types::VerbPresentationPolicy {
1038 for pack in self.packs.iter() {
1039 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1040 return handler.presentation_policy();
1041 }
1042 }
1043 khive_types::VerbPresentationPolicy::Standard
1044 }
1045
1046 pub fn is_subhandler_verb(&self, verb: &str) -> bool {
1053 for pack in self.packs.iter() {
1054 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1055 return matches!(handler.visibility, Visibility::Subhandler);
1056 }
1057 }
1058 false
1059 }
1060
1061 pub fn apply_schema_plans(&self, backend: &khive_db::StorageBackend) {
1074 for plan in self.all_schema_plans() {
1075 if plan.is_empty() {
1076 continue;
1077 }
1078 if let Err(e) = backend.apply_pack_ddl_statements(plan.statements) {
1079 tracing::warn!(
1080 pack = plan.pack,
1081 error = %e,
1082 "failed to apply pack schema plan at startup (non-fatal)"
1083 );
1084 }
1085 }
1086 }
1087}
1088
1089pub trait PackFactory: Send + Sync + 'static {
1099 fn name(&self) -> &'static str;
1101
1102 fn requires(&self) -> &'static [&'static str] {
1109 &[]
1110 }
1111
1112 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime>;
1114}
1115
1116pub struct PackRegistration(pub &'static dyn PackFactory);
1120
1121inventory::collect!(PackRegistration);
1122
1123#[derive(Debug)]
1125pub enum PackLoadError {
1126 UnknownPack(String),
1128 MissingDependency {
1130 pack: String,
1132 dep: String,
1134 },
1135}
1136
1137impl std::fmt::Display for PackLoadError {
1138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1139 match self {
1140 PackLoadError::UnknownPack(name) => write!(f, "unknown pack {name:?}"),
1141 PackLoadError::MissingDependency { pack, dep } => write!(
1142 f,
1143 "pack {pack:?} requires {dep:?}, which is not in the requested pack list; \
1144 add --pack {dep} before --pack {pack}"
1145 ),
1146 }
1147 }
1148}
1149
1150impl std::error::Error for PackLoadError {}
1151
1152pub struct PackRegistry;
1157
1158impl PackRegistry {
1159 pub fn discovered_names() -> Vec<&'static str> {
1161 inventory::iter::<PackRegistration>
1162 .into_iter()
1163 .map(|r| r.0.name())
1164 .collect()
1165 }
1166
1167 pub fn register_packs(
1180 names: &[String],
1181 runtime: KhiveRuntime,
1182 builder: &mut VerbRegistryBuilder,
1183 ) -> Result<(), PackLoadError> {
1184 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
1186 .into_iter()
1187 .map(|r| r.0)
1188 .collect();
1189 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
1190 all.iter().copied().find(|f| f.name() == name)
1191 };
1192
1193 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
1195 for name in names {
1196 factory_for(name.as_str()).ok_or_else(|| PackLoadError::UnknownPack(name.clone()))?;
1197 }
1198
1199 for name in names {
1202 let factory = factory_for(name.as_str()).unwrap(); for &dep in factory.requires() {
1204 if !requested.contains(dep) {
1205 return Err(PackLoadError::MissingDependency {
1206 pack: name.clone(),
1207 dep: dep.to_string(),
1208 });
1209 }
1210 }
1211 }
1212
1213 for name in names {
1216 let factory = factory_for(name.as_str()).unwrap(); builder.register_boxed(factory.create(runtime.clone()));
1218 }
1219
1220 Ok(())
1221 }
1222}
1223
1224fn target_id_from_args(args: &serde_json::Value) -> Option<uuid::Uuid> {
1225 args.get("target_id")
1226 .and_then(serde_json::Value::as_str)
1227 .and_then(|s| s.parse::<uuid::Uuid>().ok())
1228}
1229
1230#[cfg(test)]
1236mod tests {
1237 use super::*;
1238 use khive_types::Pack;
1239
1240 struct AlphaPack;
1241
1242 impl Pack for AlphaPack {
1243 const NAME: &'static str = "alpha";
1244 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
1245 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
1246 const HANDLERS: &'static [HandlerDef] = &[
1247 HandlerDef {
1248 name: "create",
1249 description: "create a widget",
1250 visibility: Visibility::Verb,
1251 category: VerbCategory::Commissive,
1252 params: &[],
1253 },
1254 HandlerDef {
1255 name: "list",
1256 description: "list widgets",
1257 visibility: Visibility::Verb,
1258 category: VerbCategory::Assertive,
1259 params: &[],
1260 },
1261 ];
1262 }
1263
1264 #[async_trait]
1265 impl PackRuntime for AlphaPack {
1266 fn name(&self) -> &str {
1267 AlphaPack::NAME
1268 }
1269 fn note_kinds(&self) -> &'static [&'static str] {
1270 AlphaPack::NOTE_KINDS
1271 }
1272 fn entity_kinds(&self) -> &'static [&'static str] {
1273 AlphaPack::ENTITY_KINDS
1274 }
1275 fn handlers(&self) -> &'static [HandlerDef] {
1276 AlphaPack::HANDLERS
1277 }
1278 async fn dispatch(
1279 &self,
1280 verb: &str,
1281 _params: Value,
1282 _registry: &VerbRegistry,
1283 _token: &NamespaceToken,
1284 ) -> Result<Value, RuntimeError> {
1285 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
1286 }
1287 }
1288
1289 struct BetaPack;
1290
1291 impl Pack for BetaPack {
1292 const NAME: &'static str = "beta";
1293 const NOTE_KINDS: &'static [&'static str] = &["alert"];
1294 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
1295 const HANDLERS: &'static [HandlerDef] = &[
1296 HandlerDef {
1297 name: "notify",
1298 description: "send alert",
1299 visibility: Visibility::Verb,
1300 category: VerbCategory::Commissive,
1301 params: &[],
1302 },
1303 HandlerDef {
1307 name: "create",
1308 description: "beta internal create (subhandler)",
1309 visibility: Visibility::Subhandler,
1310 category: VerbCategory::Commissive,
1311 params: &[],
1312 },
1313 ];
1314 }
1315
1316 fn build_registry() -> VerbRegistry {
1322 let mut builder = VerbRegistryBuilder::new();
1323 builder.register(AlphaPack);
1324 builder.register(BetaPack);
1325 builder.build().expect("registry builds without collision")
1326 }
1327
1328 struct CollidingPack;
1331
1332 impl Pack for CollidingPack {
1333 const NAME: &'static str = "colliding";
1334 const NOTE_KINDS: &'static [&'static str] = &[];
1335 const ENTITY_KINDS: &'static [&'static str] = &[];
1336 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1337 name: "create",
1338 description: "duplicate Verb-visibility create",
1339 visibility: Visibility::Verb,
1340 category: VerbCategory::Commissive,
1341 params: &[],
1342 }];
1343 }
1344
1345 #[async_trait]
1346 impl PackRuntime for CollidingPack {
1347 fn name(&self) -> &str {
1348 Self::NAME
1349 }
1350 fn note_kinds(&self) -> &'static [&'static str] {
1351 Self::NOTE_KINDS
1352 }
1353 fn entity_kinds(&self) -> &'static [&'static str] {
1354 Self::ENTITY_KINDS
1355 }
1356 fn handlers(&self) -> &'static [HandlerDef] {
1357 Self::HANDLERS
1358 }
1359 async fn dispatch(
1360 &self,
1361 verb: &str,
1362 _params: Value,
1363 _registry: &VerbRegistry,
1364 _token: &NamespaceToken,
1365 ) -> Result<Value, RuntimeError> {
1366 Ok(serde_json::json!({ "pack": "colliding", "verb": verb }))
1367 }
1368 }
1369
1370 #[async_trait]
1371 impl PackRuntime for BetaPack {
1372 fn name(&self) -> &str {
1373 BetaPack::NAME
1374 }
1375 fn note_kinds(&self) -> &'static [&'static str] {
1376 BetaPack::NOTE_KINDS
1377 }
1378 fn entity_kinds(&self) -> &'static [&'static str] {
1379 BetaPack::ENTITY_KINDS
1380 }
1381 fn handlers(&self) -> &'static [HandlerDef] {
1382 BetaPack::HANDLERS
1383 }
1384 async fn dispatch(
1385 &self,
1386 verb: &str,
1387 _params: Value,
1388 _registry: &VerbRegistry,
1389 _token: &NamespaceToken,
1390 ) -> Result<Value, RuntimeError> {
1391 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
1392 }
1393 }
1394
1395 #[tokio::test]
1396 async fn dispatch_routes_to_correct_pack() {
1397 let reg = build_registry();
1398
1399 let res = reg.dispatch("list", Value::Null).await.unwrap();
1400 assert_eq!(res["pack"], "alpha");
1401
1402 let res = reg.dispatch("notify", Value::Null).await.unwrap();
1403 assert_eq!(res["pack"], "beta");
1404 }
1405
1406 #[test]
1410 fn verb_collision_is_boot_time_error() {
1411 let mut builder = VerbRegistryBuilder::new();
1412 builder.register(AlphaPack);
1413 builder.register(CollidingPack);
1414 let err = builder
1415 .build()
1416 .err()
1417 .expect("duplicate Verb-visibility handler must be rejected at build time");
1418 assert!(
1419 matches!(err, RuntimeError::VerbCollision { ref verb, .. } if verb == "create"),
1420 "expected VerbCollision for 'create', got {err:?}"
1421 );
1422 let msg = err.to_string();
1423 assert!(
1424 msg.contains("create"),
1425 "error must name the colliding verb: {msg}"
1426 );
1427 assert!(
1428 msg.contains("alpha") || msg.contains("colliding"),
1429 "error must name one of the conflicting packs: {msg}"
1430 );
1431 }
1432
1433 #[test]
1437 fn subhandler_same_name_across_packs_is_not_a_collision() {
1438 struct SubhandlerPack;
1439 impl Pack for SubhandlerPack {
1440 const NAME: &'static str = "subhandler_pack";
1441 const NOTE_KINDS: &'static [&'static str] = &[];
1442 const ENTITY_KINDS: &'static [&'static str] = &[];
1443 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1444 name: "create",
1445 description: "internal create",
1446 visibility: Visibility::Subhandler,
1447 category: VerbCategory::Commissive,
1448 params: &[],
1449 }];
1450 }
1451 #[async_trait]
1452 impl PackRuntime for SubhandlerPack {
1453 fn name(&self) -> &str {
1454 Self::NAME
1455 }
1456 fn note_kinds(&self) -> &'static [&'static str] {
1457 Self::NOTE_KINDS
1458 }
1459 fn entity_kinds(&self) -> &'static [&'static str] {
1460 Self::ENTITY_KINDS
1461 }
1462 fn handlers(&self) -> &'static [HandlerDef] {
1463 Self::HANDLERS
1464 }
1465 async fn dispatch(
1466 &self,
1467 verb: &str,
1468 _: Value,
1469 _: &VerbRegistry,
1470 _: &NamespaceToken,
1471 ) -> Result<Value, RuntimeError> {
1472 Ok(serde_json::json!({"pack": "subhandler_pack", "verb": verb}))
1473 }
1474 }
1475 let mut builder = VerbRegistryBuilder::new();
1476 builder.register(AlphaPack); builder.register(SubhandlerPack); builder
1479 .build()
1480 .expect("subhandler same name must NOT be a collision");
1481 }
1482
1483 #[tokio::test]
1484 async fn dispatch_unknown_verb_returns_error() {
1485 let reg = build_registry();
1486
1487 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
1488 let msg = err.to_string();
1489 assert!(msg.contains("explode"));
1490 assert!(msg.contains("create"));
1491 }
1492
1493 #[test]
1498 fn all_verbs_aggregates_across_packs_excludes_subhandlers() {
1499 let reg = build_registry();
1500 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
1501 assert_eq!(verbs, vec!["create", "list", "notify"]);
1503 }
1504
1505 #[test]
1506 fn all_verbs_with_names_pairs_pack_name_excludes_subhandlers() {
1507 let reg = build_registry();
1508 let pairs: Vec<(&str, &str)> = reg
1509 .all_verbs_with_names()
1510 .iter()
1511 .map(|(pack, v)| (*pack, v.name))
1512 .collect();
1513 assert_eq!(
1515 pairs,
1516 vec![("alpha", "create"), ("alpha", "list"), ("beta", "notify"),]
1517 );
1518 }
1519
1520 #[test]
1521 fn all_handlers_with_names_includes_subhandlers() {
1522 let reg = build_registry();
1523 let pairs: Vec<(&str, &str)> = reg
1524 .all_handlers_with_names()
1525 .iter()
1526 .map(|(pack, v)| (*pack, v.name))
1527 .collect();
1528 assert_eq!(
1530 pairs,
1531 vec![
1532 ("alpha", "create"),
1533 ("alpha", "list"),
1534 ("beta", "notify"),
1535 ("beta", "create"),
1536 ]
1537 );
1538 }
1539
1540 #[test]
1541 fn note_kinds_are_ordered() {
1542 let reg = build_registry();
1543 let kinds = reg.all_note_kinds();
1544 assert_eq!(kinds, vec!["memo", "log", "alert"]);
1545 }
1546
1547 #[test]
1548 fn note_kind_duplicate_rejected_at_build_time() {
1549 struct DupPack;
1550
1551 impl khive_types::Pack for DupPack {
1552 const NAME: &'static str = "dup";
1553 const NOTE_KINDS: &'static [&'static str] = &["memo"];
1555 const ENTITY_KINDS: &'static [&'static str] = &[];
1556 const HANDLERS: &'static [HandlerDef] = &[];
1557 }
1558
1559 #[async_trait]
1560 impl PackRuntime for DupPack {
1561 fn name(&self) -> &str {
1562 Self::NAME
1563 }
1564 fn note_kinds(&self) -> &'static [&'static str] {
1565 Self::NOTE_KINDS
1566 }
1567 fn entity_kinds(&self) -> &'static [&'static str] {
1568 Self::ENTITY_KINDS
1569 }
1570 fn handlers(&self) -> &'static [HandlerDef] {
1571 Self::HANDLERS
1572 }
1573 async fn dispatch(
1574 &self,
1575 _verb: &str,
1576 _params: Value,
1577 _registry: &VerbRegistry,
1578 _token: &NamespaceToken,
1579 ) -> Result<Value, RuntimeError> {
1580 Ok(Value::Null)
1581 }
1582 }
1583
1584 let mut builder = VerbRegistryBuilder::new();
1585 builder.register(AlphaPack);
1586 builder.register(DupPack);
1587 let err = builder
1588 .build()
1589 .err()
1590 .expect("duplicate note kind must be rejected");
1591 let msg = err.to_string();
1592 assert!(
1593 msg.contains("memo"),
1594 "error must name the duplicate kind: {msg}"
1595 );
1596 assert!(
1597 msg.contains("alpha") || msg.contains("dup"),
1598 "error must name one of the conflicting packs: {msg}"
1599 );
1600 }
1601
1602 #[test]
1603 fn entity_kinds_are_deduplicated() {
1604 let reg = build_registry();
1605 let kinds = reg.all_entity_kinds();
1606 assert_eq!(kinds, vec!["widget", "gadget"]);
1607 }
1608
1609 use khive_gate::{Gate, GateError};
1612 use std::sync::atomic::{AtomicUsize, Ordering};
1613 use std::sync::Arc;
1614
1615 #[derive(Default, Debug)]
1616 struct CountingGate {
1617 calls: AtomicUsize,
1618 deny_verb: Option<&'static str>,
1619 }
1620
1621 impl Gate for CountingGate {
1622 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1623 self.calls.fetch_add(1, Ordering::SeqCst);
1624 if Some(req.verb.as_str()) == self.deny_verb {
1625 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
1626 } else {
1627 Ok(GateDecision::allow())
1628 }
1629 }
1630 }
1631
1632 #[tokio::test]
1633 async fn dispatch_consults_the_gate() {
1634 let gate = Arc::new(CountingGate::default());
1635 let mut builder = VerbRegistryBuilder::new();
1636 builder.register(AlphaPack);
1637 builder.with_gate(gate.clone());
1638 let reg = builder.build().expect("registry builds");
1639
1640 reg.dispatch("list", Value::Null).await.unwrap();
1641 reg.dispatch("create", Value::Null).await.unwrap();
1642 assert_eq!(
1643 gate.calls.load(Ordering::SeqCst),
1644 2,
1645 "gate should be consulted once per dispatch"
1646 );
1647 }
1648
1649 #[tokio::test]
1650 async fn dispatch_returns_permission_denied_on_deny_v03() {
1651 let gate = Arc::new(CountingGate {
1652 calls: AtomicUsize::new(0),
1653 deny_verb: Some("create"),
1654 });
1655 let mut builder = VerbRegistryBuilder::new();
1656 builder.register(AlphaPack);
1657 builder.with_gate(gate.clone());
1658 let reg = builder.build().expect("registry builds");
1659
1660 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1662 assert!(
1663 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
1664 "expected PermissionDenied, got {err:?}"
1665 );
1666 let msg = err.to_string();
1667 assert!(
1668 msg.contains("create"),
1669 "error message must name the verb: {msg}"
1670 );
1671 assert!(
1672 msg.contains("test deny for create"),
1673 "error message must carry the deny reason: {msg}"
1674 );
1675 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
1676 }
1677
1678 #[tokio::test]
1679 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
1680 let gate = Arc::new(CountingGate {
1682 calls: AtomicUsize::new(0),
1683 deny_verb: Some("create"),
1684 });
1685 let mut builder = VerbRegistryBuilder::new();
1686 builder.register(AlphaPack);
1687 builder.with_gate(gate.clone());
1688 let reg = builder.build().expect("registry builds");
1689
1690 let res = reg.dispatch("list", Value::Null).await.unwrap();
1691 assert_eq!(res["pack"], "alpha");
1692 }
1693
1694 #[tokio::test]
1695 async fn dispatch_uses_allow_all_gate_by_default() {
1696 let reg = build_registry();
1698 let res = reg.dispatch("list", Value::Null).await.unwrap();
1699 assert_eq!(res["pack"], "alpha");
1700 }
1701
1702 #[derive(Default, Debug)]
1705 struct NamespaceCapturingGate {
1706 seen: std::sync::Mutex<Vec<String>>,
1707 }
1708
1709 impl Gate for NamespaceCapturingGate {
1710 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1711 self.seen
1712 .lock()
1713 .unwrap()
1714 .push(req.namespace.as_str().to_string());
1715 Ok(GateDecision::allow())
1716 }
1717 }
1718
1719 #[tokio::test]
1720 async fn dispatch_propagates_params_namespace_to_gate() {
1721 let gate = Arc::new(NamespaceCapturingGate::default());
1722 let mut builder = VerbRegistryBuilder::new();
1723 builder.register(AlphaPack);
1724 builder.with_gate(gate.clone());
1725 builder.with_default_namespace("tenant-x");
1726 let reg = builder.build().expect("registry builds");
1727
1728 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
1730 .await
1731 .unwrap();
1732 reg.dispatch("list", Value::Null).await.unwrap();
1734 let err = reg
1736 .dispatch("list", serde_json::json!({"namespace": ""}))
1737 .await
1738 .unwrap_err();
1739 assert!(
1740 matches!(err, RuntimeError::InvalidInput(_)),
1741 "empty namespace must return InvalidInput, got {err:?}"
1742 );
1743
1744 let seen = gate.seen.lock().unwrap().clone();
1745 assert_eq!(seen, vec!["tenant-y", "tenant-x"]);
1746 }
1747
1748 #[tokio::test]
1749 async fn dispatch_falls_back_to_local_when_no_default_set() {
1750 let gate = Arc::new(NamespaceCapturingGate::default());
1752 let mut builder = VerbRegistryBuilder::new();
1753 builder.register(AlphaPack);
1754 builder.with_gate(gate.clone());
1755 let reg = builder.build().expect("registry builds");
1756
1757 reg.dispatch("list", Value::Null).await.unwrap();
1758 let seen = gate.seen.lock().unwrap().clone();
1759 assert_eq!(seen, vec!["local"]);
1760 }
1761
1762 use khive_gate::{AuditDecision, AuditEvent, Obligation};
1765
1766 #[derive(Default, Debug)]
1768 struct AuditCapturingGate {
1769 events: std::sync::Mutex<Vec<AuditEvent>>,
1770 deny_verb: Option<&'static str>,
1771 }
1772
1773 impl Gate for AuditCapturingGate {
1774 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1775 let decision = if Some(req.verb.as_str()) == self.deny_verb {
1776 GateDecision::deny("test deny")
1777 } else {
1778 GateDecision::allow_with(vec![Obligation::Audit {
1779 tag: format!("{}.check", req.verb),
1780 }])
1781 };
1782 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
1784 self.events.lock().unwrap().push(ev);
1785 Ok(decision)
1786 }
1787
1788 fn impl_name(&self) -> &'static str {
1789 "AuditCapturingGate"
1790 }
1791 }
1792
1793 #[tokio::test]
1794 async fn dispatch_emits_one_audit_event_per_call() {
1795 let gate = Arc::new(AuditCapturingGate::default());
1796 let mut builder = VerbRegistryBuilder::new();
1797 builder.register(AlphaPack);
1798 builder.with_gate(gate.clone());
1799 let reg = builder.build().expect("registry builds");
1800
1801 reg.dispatch("list", Value::Null).await.unwrap();
1802 reg.dispatch("create", Value::Null).await.unwrap();
1803
1804 let evs = gate.events.lock().unwrap();
1805 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
1806 }
1807
1808 #[tokio::test]
1809 async fn dispatch_audit_event_allow_carries_obligations() {
1810 let gate = Arc::new(AuditCapturingGate::default());
1811 let mut builder = VerbRegistryBuilder::new();
1812 builder.register(AlphaPack);
1813 builder.with_gate(gate.clone());
1814 let reg = builder.build().expect("registry builds");
1815
1816 reg.dispatch("list", Value::Null).await.unwrap();
1817
1818 let evs = gate.events.lock().unwrap();
1819 let ev = &evs[0];
1820 assert_eq!(ev.verb, "list");
1821 assert_eq!(ev.decision, AuditDecision::Allow);
1822 assert!(ev.deny_reason.is_none());
1823 assert_eq!(ev.obligations.len(), 1);
1824 assert_eq!(ev.gate_impl, "AuditCapturingGate");
1825 }
1826
1827 #[tokio::test]
1828 async fn dispatch_audit_event_deny_carries_reason() {
1829 let gate = Arc::new(AuditCapturingGate {
1830 events: Default::default(),
1831 deny_verb: Some("create"),
1832 });
1833 let mut builder = VerbRegistryBuilder::new();
1834 builder.register(AlphaPack);
1835 builder.with_gate(gate.clone());
1836 let reg = builder.build().expect("registry builds");
1837
1838 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1841 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
1842
1843 let evs = gate.events.lock().unwrap();
1844 let ev = &evs[0];
1845 assert_eq!(ev.verb, "create");
1846 assert_eq!(ev.decision, AuditDecision::Deny);
1847 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
1848 assert!(ev.obligations.is_empty());
1849 }
1850
1851 #[tokio::test]
1852 async fn dispatch_audit_event_fields_match_gate_request() {
1853 let gate = Arc::new(AuditCapturingGate::default());
1854 let mut builder = VerbRegistryBuilder::new();
1855 builder.register(AlphaPack);
1856 builder.with_gate(gate.clone());
1857 builder.with_default_namespace("tenant-z");
1858 let reg = builder.build().expect("registry builds");
1859
1860 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1861 .await
1862 .unwrap();
1863
1864 let evs = gate.events.lock().unwrap();
1865 let ev = &evs[0];
1866 assert_eq!(ev.namespace, "tenant-q");
1868 assert_eq!(ev.verb, "list");
1869 assert_eq!(ev.actor.kind, "anonymous");
1870 }
1871
1872 #[tokio::test]
1885 async fn rego_gate_missing_entrypoint_returns_permission_denied() {
1886 use khive_gate_rego::RegoGate;
1887
1888 let policy = r#"
1894 package khive.gate
1895 import rego.v1
1896 verdict := "allow"
1897 "#;
1898 let gate = Arc::new(RegoGate::from_policy_str(policy).expect("policy compiles"));
1899
1900 let mut builder = VerbRegistryBuilder::new();
1901 builder.register(AlphaPack);
1902 builder.with_gate(gate);
1903 let reg = builder.build().expect("registry builds");
1904
1905 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1906 assert!(
1907 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
1908 "expected PermissionDenied for missing rego entrypoint, got {err:?}"
1909 );
1910 }
1911
1912 use std::sync::{Mutex as StdMutex, Once, OnceLock};
1924
1925 use serial_test::serial;
1926 use tracing::field::{Field, Visit};
1927
1928 #[derive(Clone, Debug, Default)]
1929 struct CapturedEvent {
1930 message: Option<String>,
1931 audit_event: Option<String>,
1932 }
1933
1934 #[derive(Default)]
1935 struct CapturedEventVisitor(CapturedEvent);
1936
1937 impl Visit for CapturedEventVisitor {
1938 fn record_str(&mut self, field: &Field, value: &str) {
1939 match field.name() {
1940 "message" => self.0.message = Some(value.to_string()),
1941 "audit_event" => self.0.audit_event = Some(value.to_string()),
1942 _ => {}
1943 }
1944 }
1945
1946 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1947 let formatted = format!("{value:?}");
1953 let cleaned = formatted
1954 .trim_start_matches('"')
1955 .trim_end_matches('"')
1956 .to_string();
1957 match field.name() {
1958 "message" => self.0.message = Some(cleaned),
1959 "audit_event" => self.0.audit_event = Some(cleaned),
1960 _ => {}
1961 }
1962 }
1963 }
1964
1965 struct CaptureSubscriber {
1978 events: Arc<StdMutex<Vec<CapturedEvent>>>,
1979 }
1980
1981 impl CaptureSubscriber {
1982 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
1983 Self { events }
1984 }
1985 }
1986
1987 impl tracing::Subscriber for CaptureSubscriber {
1988 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
1989 true
1990 }
1991 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1992 tracing::span::Id::from_u64(1)
1993 }
1994 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
1995 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
1996 fn event(&self, event: &tracing::Event<'_>) {
1997 let mut visitor = CapturedEventVisitor::default();
1998 event.record(&mut visitor);
1999 self.events.lock().unwrap().push(visitor.0);
2000 }
2001 fn enter(&self, _: &tracing::span::Id) {}
2002 fn exit(&self, _: &tracing::span::Id) {}
2003 }
2004
2005 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
2015 static GLOBAL_INIT: Once = Once::new();
2016
2017 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
2018 GLOBAL_INIT.call_once(|| {
2019 let buffer = Arc::new(StdMutex::new(Vec::new()));
2020 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
2021 let _ = tracing::subscriber::set_global_default(subscriber);
2026 let _ = GLOBAL_CAPTURE.set(buffer);
2027 });
2028 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
2029 }
2030
2031 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
2036 where
2037 Fut: std::future::Future<Output = ()>,
2038 {
2039 let buffer = global_capture();
2040 buffer.lock().unwrap().clear();
2041
2042 let rt = tokio::runtime::Builder::new_current_thread()
2043 .enable_all()
2044 .build()
2045 .expect("build current-thread tokio runtime");
2046 rt.block_on(future);
2047
2048 let result = buffer.lock().unwrap().clone();
2049 result
2050 }
2051
2052 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
2059 events
2060 .iter()
2061 .filter(|e| e.message.as_deref() == Some("gate.check"))
2062 .filter(|e| {
2063 e.audit_event
2064 .as_deref()
2065 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
2066 .and_then(|v| {
2067 v.get("gate_impl")
2068 .and_then(|g| g.as_str().map(|s| s.to_string()))
2069 })
2070 .as_deref()
2071 == Some(gate_impl)
2072 })
2073 .cloned()
2074 .collect()
2075 }
2076
2077 #[test]
2078 #[serial]
2079 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
2080 #[derive(Debug)]
2081 struct TracingAllowGate;
2082 impl Gate for TracingAllowGate {
2083 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
2084 Ok(GateDecision::allow())
2085 }
2086 fn impl_name(&self) -> &'static str {
2087 "TracingAllowGate"
2088 }
2089 }
2090
2091 let events = capture_dispatch_events(async {
2092 let mut builder = VerbRegistryBuilder::new();
2093 builder.register(AlphaPack);
2094 builder.with_gate(Arc::new(TracingAllowGate));
2095 builder.with_default_namespace("tenant-default");
2096 let reg = builder.build().expect("registry builds");
2097 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
2098 .await
2099 .unwrap();
2100 });
2101
2102 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
2103 assert_eq!(
2104 gate_events.len(),
2105 1,
2106 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
2107 );
2108 let payload = gate_events[0]
2109 .audit_event
2110 .as_ref()
2111 .expect("gate.check event must carry an audit_event field");
2112 let audit: khive_gate::AuditEvent =
2113 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2114 assert_eq!(audit.decision, AuditDecision::Allow);
2115 assert_eq!(audit.verb, "list");
2116 assert_eq!(audit.namespace, "tenant-q");
2117 assert_eq!(audit.gate_impl, "TracingAllowGate");
2118 assert!(
2119 audit.deny_reason.is_none(),
2120 "deny_reason must be None on Allow"
2121 );
2122 }
2123
2124 use crate::runtime::NamespaceToken;
2127 use async_trait::async_trait;
2128 use khive_storage::{
2129 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
2130 };
2131 use khive_types::EventOutcome;
2132
2133 #[derive(Default, Debug)]
2135 struct MemoryEventStore {
2136 events: std::sync::Mutex<Vec<Event>>,
2137 }
2138
2139 #[async_trait]
2140 impl EventStore for MemoryEventStore {
2141 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
2142 self.events.lock().unwrap().push(event);
2143 Ok(())
2144 }
2145 async fn append_events(
2146 &self,
2147 events: Vec<Event>,
2148 ) -> khive_storage::StorageResult<BatchWriteSummary> {
2149 let attempted = events.len() as u64;
2150 let affected = attempted;
2151 self.events.lock().unwrap().extend(events);
2152 Ok(BatchWriteSummary {
2153 attempted,
2154 affected,
2155 failed: 0,
2156 first_error: String::new(),
2157 })
2158 }
2159 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
2160 Ok(self
2161 .events
2162 .lock()
2163 .unwrap()
2164 .iter()
2165 .find(|e| e.id == id)
2166 .cloned())
2167 }
2168 async fn query_events(
2169 &self,
2170 _filter: EventFilter,
2171 _page: PageRequest,
2172 ) -> khive_storage::StorageResult<Page<Event>> {
2173 let items = self.events.lock().unwrap().clone();
2174 let total = items.len() as u64;
2175 Ok(Page {
2176 items,
2177 total: Some(total),
2178 })
2179 }
2180 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
2181 Ok(self.events.lock().unwrap().len() as u64)
2182 }
2183 }
2184
2185 #[tokio::test]
2186 async fn allow_all_gate_default_remains_backward_compatible() {
2187 let mut builder = VerbRegistryBuilder::new();
2189 builder.register(AlphaPack);
2190 let reg = builder.build().expect("registry builds");
2191
2192 let res = reg.dispatch("list", Value::Null).await.unwrap();
2193 assert_eq!(
2194 res["pack"], "alpha",
2195 "AllowAllGate must allow every verb — backward compat guarantee"
2196 );
2197 let res = reg.dispatch("create", Value::Null).await.unwrap();
2198 assert_eq!(res["pack"], "alpha");
2199 }
2200
2201 #[tokio::test]
2202 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
2203 #[derive(Debug)]
2204 struct AlwaysDenyGate;
2205 impl Gate for AlwaysDenyGate {
2206 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2207 Ok(GateDecision::deny("test: always deny"))
2208 }
2209 }
2210
2211 #[derive(Debug)]
2213 struct TrackedPack {
2214 invoked: Arc<AtomicUsize>,
2215 }
2216
2217 impl khive_types::Pack for TrackedPack {
2218 const NAME: &'static str = "tracked";
2219 const NOTE_KINDS: &'static [&'static str] = &[];
2220 const ENTITY_KINDS: &'static [&'static str] = &[];
2221 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2222 name: "guarded",
2223 description: "a guarded verb",
2224 visibility: Visibility::Verb,
2225 category: VerbCategory::Assertive,
2226 params: &[],
2227 }];
2228 }
2229
2230 #[async_trait]
2231 impl PackRuntime for TrackedPack {
2232 fn name(&self) -> &str {
2233 Self::NAME
2234 }
2235 fn note_kinds(&self) -> &'static [&'static str] {
2236 Self::NOTE_KINDS
2237 }
2238 fn entity_kinds(&self) -> &'static [&'static str] {
2239 Self::ENTITY_KINDS
2240 }
2241 fn handlers(&self) -> &'static [HandlerDef] {
2242 Self::HANDLERS
2243 }
2244 async fn dispatch(
2245 &self,
2246 _verb: &str,
2247 _params: Value,
2248 _registry: &VerbRegistry,
2249 _token: &NamespaceToken,
2250 ) -> Result<Value, RuntimeError> {
2251 self.invoked.fetch_add(1, Ordering::SeqCst);
2252 Ok(serde_json::json!({"invoked": true}))
2253 }
2254 }
2255
2256 let invoked = Arc::new(AtomicUsize::new(0));
2257 let mut builder = VerbRegistryBuilder::new();
2258 builder.register(TrackedPack {
2259 invoked: invoked.clone(),
2260 });
2261 builder.with_gate(Arc::new(AlwaysDenyGate));
2262 let reg = builder.build().expect("registry builds");
2263
2264 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
2265 assert!(
2266 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
2267 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
2268 );
2269 assert_eq!(
2270 invoked.load(Ordering::SeqCst),
2271 0,
2272 "pack dispatch MUST NOT be invoked when gate denies"
2273 );
2274 }
2275
2276 #[tokio::test]
2277 async fn audit_event_persists_to_event_store_on_allow() {
2278 let store = Arc::new(MemoryEventStore::default());
2279 let mut builder = VerbRegistryBuilder::new();
2280 builder.register(AlphaPack);
2281 builder.with_event_store(store.clone());
2282 let reg = builder.build().expect("registry builds");
2283
2284 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2285 .await
2286 .unwrap();
2287
2288 let count = store.count_events(EventFilter::default()).await.unwrap();
2289 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
2290
2291 let page = store
2292 .query_events(
2293 EventFilter::default(),
2294 PageRequest {
2295 limit: 10,
2296 offset: 0,
2297 },
2298 )
2299 .await
2300 .unwrap();
2301 let ev = &page.items[0];
2302 assert_eq!(ev.verb, "list");
2303 assert_eq!(ev.namespace, "test-ns");
2304 assert_eq!(ev.substrate, SubstrateKind::Event);
2305 assert_eq!(ev.outcome, EventOutcome::Success);
2306 }
2307
2308 #[tokio::test]
2309 async fn audit_event_persists_to_event_store_on_deny() {
2310 #[derive(Debug)]
2311 struct AlwaysDenyGate;
2312 impl Gate for AlwaysDenyGate {
2313 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2314 Ok(GateDecision::deny("denied by test"))
2315 }
2316 }
2317
2318 let store = Arc::new(MemoryEventStore::default());
2319 let mut builder = VerbRegistryBuilder::new();
2320 builder.register(AlphaPack);
2321 builder.with_gate(Arc::new(AlwaysDenyGate));
2322 builder.with_event_store(store.clone());
2323 let reg = builder.build().expect("registry builds");
2324
2325 let err = reg
2327 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2328 .await
2329 .unwrap_err();
2330 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2331
2332 let count = store.count_events(EventFilter::default()).await.unwrap();
2333 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
2334
2335 let page = store
2336 .query_events(
2337 EventFilter::default(),
2338 PageRequest {
2339 limit: 10,
2340 offset: 0,
2341 },
2342 )
2343 .await
2344 .unwrap();
2345 let ev = &page.items[0];
2346 assert_eq!(ev.verb, "list");
2347 assert_eq!(ev.outcome, EventOutcome::Denied);
2348 }
2349
2350 #[tokio::test]
2351 async fn gate_error_does_not_persist_to_event_store() {
2352 #[derive(Debug)]
2353 struct FailingGate;
2354 impl Gate for FailingGate {
2355 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
2356 Err(khive_gate::GateError::Internal("gate broken".into()))
2357 }
2358 }
2359
2360 let store = Arc::new(MemoryEventStore::default());
2361 let mut builder = VerbRegistryBuilder::new();
2362 builder.register(AlphaPack);
2363 builder.with_gate(Arc::new(FailingGate));
2364 builder.with_event_store(store.clone());
2365 let reg = builder.build().expect("registry builds");
2366
2367 let res = reg.dispatch("list", Value::Null).await.unwrap();
2369 assert_eq!(
2370 res["pack"], "alpha",
2371 "gate error must fail-open, not block dispatch"
2372 );
2373
2374 let count = store.count_events(EventFilter::default()).await.unwrap();
2375 assert_eq!(
2376 count, 0,
2377 "gate infrastructure error must NOT produce an audit event in EventStore"
2378 );
2379 }
2380
2381 #[tokio::test]
2382 async fn no_event_store_configured_tracing_only() {
2383 let mut builder = VerbRegistryBuilder::new();
2387 builder.register(AlphaPack);
2388 let reg = builder.build().expect("registry builds");
2389
2390 let res = reg.dispatch("list", Value::Null).await.unwrap();
2391 assert_eq!(res["pack"], "alpha");
2392 }
2393
2394 #[test]
2395 #[serial]
2396 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
2397 #[derive(Debug)]
2398 struct TracingDenyGate;
2399 impl Gate for TracingDenyGate {
2400 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2401 Ok(GateDecision::deny("denied by test gate"))
2402 }
2403 fn impl_name(&self) -> &'static str {
2404 "TracingDenyGate"
2405 }
2406 }
2407
2408 let events = capture_dispatch_events(async {
2409 let mut builder = VerbRegistryBuilder::new();
2410 builder.register(AlphaPack);
2411 builder.with_gate(Arc::new(TracingDenyGate));
2412 let reg = builder.build().expect("registry builds");
2413 let _ = reg.dispatch("create", serde_json::Value::Null).await;
2416 });
2417
2418 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
2419 assert_eq!(
2420 gate_events.len(),
2421 1,
2422 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
2423 );
2424 let payload = gate_events[0]
2425 .audit_event
2426 .as_ref()
2427 .expect("gate.check event must carry an audit_event field on Deny");
2428 let audit: khive_gate::AuditEvent =
2429 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2430 assert_eq!(audit.decision, AuditDecision::Deny);
2431 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
2432 assert_eq!(audit.gate_impl, "TracingDenyGate");
2433 let payload_json: serde_json::Value =
2437 serde_json::from_str(payload).expect("payload must be valid JSON");
2438 assert_eq!(
2439 payload_json["obligations"],
2440 serde_json::Value::Array(Vec::new()),
2441 "obligations must be `[]` on Deny on the tracing payload, not omitted"
2442 );
2443 }
2444
2445 #[tokio::test]
2453 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
2454 #[derive(Debug)]
2455 struct DenyGateWithName;
2456 impl Gate for DenyGateWithName {
2457 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2458 Ok(GateDecision::deny("policy: write forbidden for anon"))
2459 }
2460 fn impl_name(&self) -> &'static str {
2461 "DenyGateWithName"
2462 }
2463 }
2464
2465 let store = Arc::new(MemoryEventStore::default());
2466 let mut builder = VerbRegistryBuilder::new();
2467 builder.register(AlphaPack);
2468 builder.with_gate(Arc::new(DenyGateWithName));
2469 builder.with_event_store(store.clone());
2470 let reg = builder.build().expect("registry builds");
2471
2472 let err = reg
2474 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2475 .await
2476 .unwrap_err();
2477 assert!(
2478 matches!(err, RuntimeError::PermissionDenied { .. }),
2479 "expected PermissionDenied, got {err:?}"
2480 );
2481
2482 let page = store
2484 .query_events(
2485 EventFilter::default(),
2486 PageRequest {
2487 limit: 10,
2488 offset: 0,
2489 },
2490 )
2491 .await
2492 .unwrap();
2493 assert_eq!(
2494 page.items.len(),
2495 1,
2496 "one audit event must be persisted on deny"
2497 );
2498
2499 let ev = &page.items[0];
2500 assert_eq!(ev.outcome, EventOutcome::Denied);
2501
2502 let data = &ev.payload;
2504
2505 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2506 .expect("Event.payload must deserialize to AuditEvent");
2507
2508 assert_eq!(
2509 audit.deny_reason.as_deref(),
2510 Some("policy: write forbidden for anon"),
2511 "deny_reason must be preserved through EventStore"
2512 );
2513 assert_eq!(
2514 audit.gate_impl, "DenyGateWithName",
2515 "gate_impl must be preserved through EventStore"
2516 );
2517 assert_eq!(
2518 audit.decision,
2519 khive_gate::AuditDecision::Deny,
2520 "decision field must be preserved through EventStore"
2521 );
2522 }
2523
2524 #[tokio::test]
2525 async fn audit_envelope_round_trips_obligations_through_event_store() {
2526 use khive_gate::Obligation;
2527
2528 #[derive(Debug)]
2529 struct ObligationGate;
2530 impl Gate for ObligationGate {
2531 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2532 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2533 tag: "billing.meter".into(),
2534 }]))
2535 }
2536 fn impl_name(&self) -> &'static str {
2537 "ObligationGate"
2538 }
2539 }
2540
2541 let store = Arc::new(MemoryEventStore::default());
2542 let mut builder = VerbRegistryBuilder::new();
2543 builder.register(AlphaPack);
2544 builder.with_gate(Arc::new(ObligationGate));
2545 builder.with_event_store(store.clone());
2546 let reg = builder.build().expect("registry builds");
2547
2548 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2549 .await
2550 .unwrap();
2551
2552 let page = store
2553 .query_events(
2554 EventFilter::default(),
2555 PageRequest {
2556 limit: 10,
2557 offset: 0,
2558 },
2559 )
2560 .await
2561 .unwrap();
2562 assert_eq!(page.items.len(), 1);
2563
2564 let ev = &page.items[0];
2565 assert_eq!(ev.outcome, EventOutcome::Success);
2566
2567 let data = &ev.payload;
2568
2569 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2570 .expect("Event.payload must deserialize to AuditEvent");
2571
2572 assert_eq!(audit.gate_impl, "ObligationGate");
2573 assert_eq!(
2574 audit.obligations.len(),
2575 1,
2576 "obligations must be preserved through EventStore"
2577 );
2578 match &audit.obligations[0] {
2579 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
2580 other => panic!("expected Audit obligation, got {other:?}"),
2581 }
2582 }
2583
2584 #[tokio::test]
2592 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
2593 #[derive(Debug)]
2594 struct SqlTestDenyGate;
2595 impl Gate for SqlTestDenyGate {
2596 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2597 Ok(GateDecision::deny("sql-path: write denied"))
2598 }
2599 fn impl_name(&self) -> &'static str {
2600 "SqlTestDenyGate"
2601 }
2602 }
2603
2604 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2608 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2609 let sql_store = rt
2610 .events(&test_tok)
2611 .expect("events_for_namespace must succeed");
2612
2613 let mut builder = VerbRegistryBuilder::new();
2614 builder.register(AlphaPack);
2615 builder.with_gate(Arc::new(SqlTestDenyGate));
2616 builder.with_event_store(sql_store.clone());
2617 let reg = builder.build().expect("registry builds");
2618
2619 let err = reg
2621 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2622 .await
2623 .unwrap_err();
2624 assert!(
2625 matches!(err, RuntimeError::PermissionDenied { .. }),
2626 "expected PermissionDenied, got {err:?}"
2627 );
2628
2629 let page = sql_store
2631 .query_events(
2632 EventFilter::default(),
2633 PageRequest {
2634 limit: 10,
2635 offset: 0,
2636 },
2637 )
2638 .await
2639 .unwrap();
2640 assert_eq!(
2641 page.items.len(),
2642 1,
2643 "one audit event must be persisted on deny through SqlEventStore"
2644 );
2645
2646 let ev = &page.items[0];
2647 assert_eq!(ev.outcome, EventOutcome::Denied);
2648
2649 let data = &ev.payload;
2653
2654 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2655 .expect("Event.payload must deserialize to AuditEvent after SQL round-trip");
2656
2657 assert_eq!(
2658 audit.deny_reason.as_deref(),
2659 Some("sql-path: write denied"),
2660 "deny_reason must survive the SQL text round-trip"
2661 );
2662 assert_eq!(
2663 audit.gate_impl, "SqlTestDenyGate",
2664 "gate_impl must survive the SQL text round-trip"
2665 );
2666 assert_eq!(
2667 audit.decision,
2668 khive_gate::AuditDecision::Deny,
2669 "decision field must survive the SQL text round-trip"
2670 );
2671 assert!(
2674 audit.obligations.is_empty(),
2675 "obligations must be preserved as empty [] through SQL round-trip"
2676 );
2677 }
2678
2679 #[tokio::test]
2691 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
2692 use khive_gate::Obligation;
2693
2694 #[derive(Debug)]
2695 struct SqlTestAllowWithObligationGate;
2696 impl Gate for SqlTestAllowWithObligationGate {
2697 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2698 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2699 tag: "sql-path-billing.meter".into(),
2700 }]))
2701 }
2702 fn impl_name(&self) -> &'static str {
2703 "SqlTestAllowWithObligationGate"
2704 }
2705 }
2706
2707 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2708 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2709 let sql_store = rt
2710 .events(&test_tok)
2711 .expect("events_for_namespace must succeed");
2712
2713 let mut builder = VerbRegistryBuilder::new();
2714 builder.register(AlphaPack);
2715 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
2716 builder.with_event_store(sql_store.clone());
2717 let reg = builder.build().expect("registry builds");
2718
2719 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2721 .await
2722 .expect("dispatch must succeed when gate allows");
2723
2724 let page = sql_store
2726 .query_events(
2727 EventFilter::default(),
2728 PageRequest {
2729 limit: 10,
2730 offset: 0,
2731 },
2732 )
2733 .await
2734 .unwrap();
2735 assert_eq!(
2736 page.items.len(),
2737 1,
2738 "one audit event must be persisted on allow through SqlEventStore"
2739 );
2740
2741 let ev = &page.items[0];
2742 assert_eq!(ev.outcome, EventOutcome::Success);
2743
2744 let data = &ev.payload;
2745
2746 let obligations_raw = data
2751 .get("obligations")
2752 .expect("Event.data JSON must contain 'obligations' key");
2753 let obligations_arr = obligations_raw
2754 .as_array()
2755 .expect("'obligations' must be a JSON array");
2756 assert!(
2757 !obligations_arr.is_empty(),
2758 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
2759 );
2760
2761 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2764 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
2765
2766 assert_eq!(
2767 audit.gate_impl, "SqlTestAllowWithObligationGate",
2768 "gate_impl must survive the SQL text round-trip"
2769 );
2770 assert_eq!(
2771 audit.decision,
2772 khive_gate::AuditDecision::Allow,
2773 "decision field must survive the SQL text round-trip"
2774 );
2775 assert_eq!(
2776 audit.obligations.len(),
2777 1,
2778 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
2779 );
2780 match &audit.obligations[0] {
2781 Obligation::Audit { tag } => assert_eq!(
2782 tag, "sql-path-billing.meter",
2783 "Audit obligation tag must survive the SQL text round-trip"
2784 ),
2785 other => panic!("expected Audit obligation, got {other:?}"),
2786 }
2787 }
2788
2789 #[tokio::test]
2797 async fn audit_event_payload_shape_for_create_verb() {
2798 let store = Arc::new(MemoryEventStore::default());
2799 let mut builder = VerbRegistryBuilder::new();
2800 builder.register(AlphaPack);
2801 builder.with_event_store(store.clone());
2802 builder.with_default_namespace("test-ns");
2803 let reg = builder.build().expect("registry builds");
2804
2805 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
2808 .await
2809 .unwrap();
2810
2811 let count = store.count_events(EventFilter::default()).await.unwrap();
2812 assert_eq!(count, 1, "exactly one audit event for one dispatch");
2813
2814 let page = store
2815 .query_events(
2816 EventFilter::default(),
2817 PageRequest {
2818 limit: 10,
2819 offset: 0,
2820 },
2821 )
2822 .await
2823 .unwrap();
2824 let ev = &page.items[0];
2825
2826 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
2828 assert_eq!(
2829 ev.outcome,
2830 EventOutcome::Success,
2831 "ev.outcome must be Success on allow"
2832 );
2833 assert_eq!(
2834 ev.namespace, "test-ns",
2835 "ev.namespace must match the dispatch namespace"
2836 );
2837
2838 let data = &ev.payload;
2840
2841 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2842 .expect("ev.payload must deserialize to AuditEvent");
2843
2844 assert_eq!(
2845 audit.decision,
2846 khive_gate::AuditDecision::Allow,
2847 "AuditEvent.decision must be Allow"
2848 );
2849 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
2850 assert_eq!(
2851 audit.namespace, "test-ns",
2852 "AuditEvent.namespace must be preserved"
2853 );
2854 assert_eq!(
2855 audit.gate_impl, "AllowAllGate",
2856 "AuditEvent.gate_impl must name the gate implementation"
2857 );
2858 assert!(
2859 audit.deny_reason.is_none(),
2860 "AuditEvent.deny_reason must be None on Allow"
2861 );
2862 let payload_json: serde_json::Value =
2864 serde_json::from_value(data.clone()).expect("data must be valid JSON");
2865 assert_eq!(
2866 payload_json["obligations"],
2867 serde_json::Value::Array(Vec::new()),
2868 "obligations must be [] on AllowAllGate"
2869 );
2870 }
2871
2872 #[tokio::test]
2874 async fn audit_event_threads_target_id_from_dispatch_args() {
2875 let store = Arc::new(MemoryEventStore::default());
2876 let target = uuid::Uuid::new_v4();
2877 let mut builder = VerbRegistryBuilder::new();
2878 builder.register(AlphaPack);
2879 builder.with_event_store(store.clone());
2880 builder.with_default_namespace("test-ns");
2881 let reg = builder.build().expect("registry builds");
2882
2883 reg.dispatch(
2884 "create",
2885 serde_json::json!({"namespace": "test-ns", "target_id": target}),
2886 )
2887 .await
2888 .unwrap();
2889
2890 let page = store
2891 .query_events(
2892 EventFilter::default(),
2893 PageRequest {
2894 offset: 0,
2895 limit: 10,
2896 },
2897 )
2898 .await
2899 .unwrap();
2900 assert_eq!(
2901 page.items[0].target_id,
2902 Some(target),
2903 "#282: audit event must carry target_id from dispatch params"
2904 );
2905 }
2906}
2907
2908#[cfg(test)]
2911mod dep_tests {
2912 use super::*;
2913 use async_trait::async_trait;
2914 use khive_types::Pack;
2915 use serde_json::Value;
2916
2917 struct KgDepPack;
2918 struct MemoryDepPack;
2919 struct ADepPack;
2920 struct BDepPack;
2921
2922 impl Pack for KgDepPack {
2923 const NAME: &'static str = "kg_dep";
2924 const NOTE_KINDS: &'static [&'static str] = &["observation"];
2925 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
2926 const HANDLERS: &'static [HandlerDef] = &[];
2927 }
2928
2929 impl Pack for MemoryDepPack {
2930 const NAME: &'static str = "memory_dep";
2931 const NOTE_KINDS: &'static [&'static str] = &["memory"];
2932 const ENTITY_KINDS: &'static [&'static str] = &[];
2933 const HANDLERS: &'static [HandlerDef] = &[];
2934 const REQUIRES: &'static [&'static str] = &["kg_dep"];
2935 }
2936
2937 impl Pack for ADepPack {
2938 const NAME: &'static str = "pack_a";
2939 const NOTE_KINDS: &'static [&'static str] = &[];
2940 const ENTITY_KINDS: &'static [&'static str] = &[];
2941 const HANDLERS: &'static [HandlerDef] = &[];
2942 const REQUIRES: &'static [&'static str] = &["pack_b"];
2943 }
2944
2945 impl Pack for BDepPack {
2946 const NAME: &'static str = "pack_b";
2947 const NOTE_KINDS: &'static [&'static str] = &[];
2948 const ENTITY_KINDS: &'static [&'static str] = &[];
2949 const HANDLERS: &'static [HandlerDef] = &[];
2950 const REQUIRES: &'static [&'static str] = &["pack_a"];
2951 }
2952
2953 #[async_trait]
2954 impl PackRuntime for KgDepPack {
2955 fn name(&self) -> &str {
2956 Self::NAME
2957 }
2958 fn note_kinds(&self) -> &'static [&'static str] {
2959 Self::NOTE_KINDS
2960 }
2961 fn entity_kinds(&self) -> &'static [&'static str] {
2962 Self::ENTITY_KINDS
2963 }
2964 fn handlers(&self) -> &'static [HandlerDef] {
2965 Self::HANDLERS
2966 }
2967 async fn dispatch(
2968 &self,
2969 verb: &str,
2970 _: Value,
2971 _: &VerbRegistry,
2972 _: &NamespaceToken,
2973 ) -> Result<Value, RuntimeError> {
2974 Err(RuntimeError::InvalidInput(format!(
2975 "KgDepPack has no verbs: {verb}"
2976 )))
2977 }
2978 }
2979
2980 #[async_trait]
2981 impl PackRuntime for MemoryDepPack {
2982 fn name(&self) -> &str {
2983 Self::NAME
2984 }
2985 fn note_kinds(&self) -> &'static [&'static str] {
2986 Self::NOTE_KINDS
2987 }
2988 fn entity_kinds(&self) -> &'static [&'static str] {
2989 Self::ENTITY_KINDS
2990 }
2991 fn handlers(&self) -> &'static [HandlerDef] {
2992 Self::HANDLERS
2993 }
2994 fn requires(&self) -> &'static [&'static str] {
2995 Self::REQUIRES
2996 }
2997 async fn dispatch(
2998 &self,
2999 verb: &str,
3000 _: Value,
3001 _: &VerbRegistry,
3002 _: &NamespaceToken,
3003 ) -> Result<Value, RuntimeError> {
3004 Err(RuntimeError::InvalidInput(format!(
3005 "MemoryDepPack has no verbs: {verb}"
3006 )))
3007 }
3008 }
3009
3010 #[async_trait]
3011 impl PackRuntime for ADepPack {
3012 fn name(&self) -> &str {
3013 Self::NAME
3014 }
3015 fn note_kinds(&self) -> &'static [&'static str] {
3016 Self::NOTE_KINDS
3017 }
3018 fn entity_kinds(&self) -> &'static [&'static str] {
3019 Self::ENTITY_KINDS
3020 }
3021 fn handlers(&self) -> &'static [HandlerDef] {
3022 Self::HANDLERS
3023 }
3024 fn requires(&self) -> &'static [&'static str] {
3025 Self::REQUIRES
3026 }
3027 async fn dispatch(
3028 &self,
3029 verb: &str,
3030 _: Value,
3031 _: &VerbRegistry,
3032 _: &NamespaceToken,
3033 ) -> Result<Value, RuntimeError> {
3034 Err(RuntimeError::InvalidInput(format!(
3035 "ADepPack has no verbs: {verb}"
3036 )))
3037 }
3038 }
3039
3040 #[async_trait]
3041 impl PackRuntime for BDepPack {
3042 fn name(&self) -> &str {
3043 Self::NAME
3044 }
3045 fn note_kinds(&self) -> &'static [&'static str] {
3046 Self::NOTE_KINDS
3047 }
3048 fn entity_kinds(&self) -> &'static [&'static str] {
3049 Self::ENTITY_KINDS
3050 }
3051 fn handlers(&self) -> &'static [HandlerDef] {
3052 Self::HANDLERS
3053 }
3054 fn requires(&self) -> &'static [&'static str] {
3055 Self::REQUIRES
3056 }
3057 async fn dispatch(
3058 &self,
3059 verb: &str,
3060 _: Value,
3061 _: &VerbRegistry,
3062 _: &NamespaceToken,
3063 ) -> Result<Value, RuntimeError> {
3064 Err(RuntimeError::InvalidInput(format!(
3065 "BDepPack has no verbs: {verb}"
3066 )))
3067 }
3068 }
3069
3070 #[test]
3071 fn test_pack_deps_happy_path() {
3072 let mut builder = VerbRegistryBuilder::new();
3073 builder.register(MemoryDepPack);
3074 builder.register(KgDepPack);
3075 let reg = builder
3076 .build()
3077 .expect("kg_dep satisfies memory_dep dependency");
3078 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
3079 let names = reg.pack_names();
3080 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
3081 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
3082 assert!(
3083 kg_pos < mem_pos,
3084 "kg_dep must be loaded before memory_dep; order: {names:?}"
3085 );
3086 }
3087
3088 #[test]
3089 fn test_pack_deps_missing() {
3090 let mut builder = VerbRegistryBuilder::new();
3091 builder.register(MemoryDepPack);
3092 let err = match builder.build() {
3093 Ok(_) => panic!("expected Err, got Ok"),
3094 Err(e) => e,
3095 };
3096 assert!(
3097 matches!(err, RuntimeError::MissingPackDependency(_)),
3098 "expected MissingPackDependency, got {err:?}"
3099 );
3100 let msg = err.to_string();
3101 assert!(
3102 msg.contains("memory_dep"),
3103 "error must name the dependent pack: {msg}"
3104 );
3105 assert!(
3106 msg.contains("kg_dep"),
3107 "error must name the missing dep: {msg}"
3108 );
3109 }
3110
3111 #[test]
3112 fn test_pack_deps_circular() {
3113 let mut builder = VerbRegistryBuilder::new();
3114 builder.register(ADepPack);
3115 builder.register(BDepPack);
3116 let err = match builder.build() {
3117 Ok(_) => panic!("expected Err, got Ok"),
3118 Err(e) => e,
3119 };
3120 assert!(
3121 matches!(err, RuntimeError::CircularPackDependency(_)),
3122 "expected CircularPackDependency, got {err:?}"
3123 );
3124 let msg = err.to_string();
3125 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
3126 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
3127 }
3128
3129 #[test]
3130 fn test_pack_deps_no_deps() {
3131 struct NoDepsA;
3132 struct NoDepsB;
3133
3134 impl Pack for NoDepsA {
3135 const NAME: &'static str = "no_deps_a";
3136 const NOTE_KINDS: &'static [&'static str] = &[];
3137 const ENTITY_KINDS: &'static [&'static str] = &[];
3138 const HANDLERS: &'static [HandlerDef] = &[];
3139 }
3140
3141 impl Pack for NoDepsB {
3142 const NAME: &'static str = "no_deps_b";
3143 const NOTE_KINDS: &'static [&'static str] = &[];
3144 const ENTITY_KINDS: &'static [&'static str] = &[];
3145 const HANDLERS: &'static [HandlerDef] = &[];
3146 }
3147
3148 #[async_trait]
3149 impl PackRuntime for NoDepsA {
3150 fn name(&self) -> &str {
3151 Self::NAME
3152 }
3153 fn note_kinds(&self) -> &'static [&'static str] {
3154 Self::NOTE_KINDS
3155 }
3156 fn entity_kinds(&self) -> &'static [&'static str] {
3157 Self::ENTITY_KINDS
3158 }
3159 fn handlers(&self) -> &'static [HandlerDef] {
3160 Self::HANDLERS
3161 }
3162 async fn dispatch(
3163 &self,
3164 verb: &str,
3165 _: Value,
3166 _: &VerbRegistry,
3167 _: &NamespaceToken,
3168 ) -> Result<Value, RuntimeError> {
3169 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
3170 }
3171 }
3172
3173 #[async_trait]
3174 impl PackRuntime for NoDepsB {
3175 fn name(&self) -> &str {
3176 Self::NAME
3177 }
3178 fn note_kinds(&self) -> &'static [&'static str] {
3179 Self::NOTE_KINDS
3180 }
3181 fn entity_kinds(&self) -> &'static [&'static str] {
3182 Self::ENTITY_KINDS
3183 }
3184 fn handlers(&self) -> &'static [HandlerDef] {
3185 Self::HANDLERS
3186 }
3187 async fn dispatch(
3188 &self,
3189 verb: &str,
3190 _: Value,
3191 _: &VerbRegistry,
3192 _: &NamespaceToken,
3193 ) -> Result<Value, RuntimeError> {
3194 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
3195 }
3196 }
3197
3198 let mut builder = VerbRegistryBuilder::new();
3199 builder.register(NoDepsA);
3200 builder.register(NoDepsB);
3201 let reg = builder.build().expect("packs with REQUIRES=&[] build");
3202 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
3203 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
3204 }
3205}
3206
3207#[cfg(test)]
3210mod hook_tests {
3211 use super::*;
3212 use async_trait::async_trait;
3213 use khive_types::Pack;
3214 use std::sync::atomic::{AtomicUsize, Ordering};
3215 use std::sync::Mutex as StdMutex;
3216
3217 struct SimplePack;
3218
3219 impl Pack for SimplePack {
3220 const NAME: &'static str = "simple";
3221 const NOTE_KINDS: &'static [&'static str] = &[];
3222 const ENTITY_KINDS: &'static [&'static str] = &[];
3223 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
3224 name: "ping",
3225 description: "ping",
3226 visibility: Visibility::Verb,
3227 category: VerbCategory::Assertive,
3228 params: &[],
3229 }];
3230 }
3231
3232 #[async_trait]
3233 impl PackRuntime for SimplePack {
3234 fn name(&self) -> &str {
3235 SimplePack::NAME
3236 }
3237 fn note_kinds(&self) -> &'static [&'static str] {
3238 SimplePack::NOTE_KINDS
3239 }
3240 fn entity_kinds(&self) -> &'static [&'static str] {
3241 SimplePack::ENTITY_KINDS
3242 }
3243 fn handlers(&self) -> &'static [HandlerDef] {
3244 SimplePack::HANDLERS
3245 }
3246 async fn dispatch(
3247 &self,
3248 verb: &str,
3249 _params: Value,
3250 _registry: &VerbRegistry,
3251 _token: &NamespaceToken,
3252 ) -> Result<Value, RuntimeError> {
3253 Ok(serde_json::json!({ "verb": verb }))
3254 }
3255 }
3256
3257 #[derive(Default)]
3259 struct CountingHook {
3260 calls: AtomicUsize,
3261 last_verb: StdMutex<String>,
3262 }
3263
3264 #[async_trait]
3265 impl DispatchHook for CountingHook {
3266 async fn on_dispatch(&self, view: &EventView) {
3267 self.calls.fetch_add(1, Ordering::SeqCst);
3268 *self.last_verb.lock().unwrap() = view.event.verb.clone();
3269 }
3270 }
3271
3272 #[tokio::test]
3273 async fn dispatch_hook_fires_on_successful_dispatch() {
3274 let hook = Arc::new(CountingHook::default());
3275 let mut builder = VerbRegistryBuilder::new();
3276 builder.register(SimplePack);
3277 builder.with_dispatch_hook(hook.clone());
3278 let reg = builder.build().expect("registry builds");
3279
3280 reg.dispatch("ping", Value::Null).await.unwrap();
3281
3282 assert_eq!(
3283 hook.calls.load(Ordering::SeqCst),
3284 1,
3285 "hook must fire once per successful dispatch"
3286 );
3287 assert_eq!(
3288 hook.last_verb.lock().unwrap().as_str(),
3289 "ping",
3290 "hook event must carry the dispatched verb"
3291 );
3292 }
3293
3294 #[tokio::test]
3295 async fn dispatch_hook_fires_multiple_times() {
3296 let hook = Arc::new(CountingHook::default());
3297 let mut builder = VerbRegistryBuilder::new();
3298 builder.register(SimplePack);
3299 builder.with_dispatch_hook(hook.clone());
3300 let reg = builder.build().expect("registry builds");
3301
3302 reg.dispatch("ping", Value::Null).await.unwrap();
3303 reg.dispatch("ping", Value::Null).await.unwrap();
3304 reg.dispatch("ping", Value::Null).await.unwrap();
3305
3306 assert_eq!(
3307 hook.calls.load(Ordering::SeqCst),
3308 3,
3309 "hook must fire once per successful dispatch"
3310 );
3311 }
3312
3313 #[tokio::test]
3314 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
3315 let hook = Arc::new(CountingHook::default());
3316 let mut builder = VerbRegistryBuilder::new();
3317 builder.register(SimplePack);
3318 builder.with_dispatch_hook(hook.clone());
3319 let reg = builder.build().expect("registry builds");
3320
3321 let _ = reg.dispatch("nonexistent", Value::Null).await;
3322
3323 assert_eq!(
3324 hook.calls.load(Ordering::SeqCst),
3325 0,
3326 "hook must NOT fire for unknown verb (dispatch returns error)"
3327 );
3328 }
3329
3330 #[tokio::test]
3331 async fn dispatch_hook_does_not_fire_on_gate_deny() {
3332 use khive_gate::{Gate, GateDecision, GateError};
3333
3334 #[derive(Debug)]
3335 struct AlwaysDenyGate;
3336 impl Gate for AlwaysDenyGate {
3337 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3338 Ok(GateDecision::deny("test deny"))
3339 }
3340 }
3341
3342 let hook = Arc::new(CountingHook::default());
3343 let mut builder = VerbRegistryBuilder::new();
3344 builder.register(SimplePack);
3345 builder.with_gate(Arc::new(AlwaysDenyGate));
3346 builder.with_dispatch_hook(hook.clone());
3347 let reg = builder.build().expect("registry builds");
3348
3349 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
3350 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
3351
3352 assert_eq!(
3353 hook.calls.load(Ordering::SeqCst),
3354 0,
3355 "hook must NOT fire when gate denies dispatch"
3356 );
3357 }
3358
3359 #[tokio::test]
3360 async fn dispatch_hook_event_carries_namespace_from_params() {
3361 let hook = Arc::new(CountingHook::default());
3362
3363 #[derive(Default)]
3364 struct NsCapturingHook {
3365 ns: StdMutex<String>,
3366 }
3367
3368 #[async_trait]
3369 impl DispatchHook for NsCapturingHook {
3370 async fn on_dispatch(&self, view: &EventView) {
3371 *self.ns.lock().unwrap() = view.event.namespace.clone();
3372 }
3373 }
3374
3375 let ns_hook = Arc::new(NsCapturingHook::default());
3376 let mut builder = VerbRegistryBuilder::new();
3377 builder.register(SimplePack);
3378 builder.with_dispatch_hook(ns_hook.clone());
3379 let reg = builder.build().expect("registry builds");
3380
3381 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
3382 .await
3383 .unwrap();
3384
3385 assert_eq!(
3386 ns_hook.ns.lock().unwrap().as_str(),
3387 "tenant-abc",
3388 "dispatch hook event must carry the resolved namespace"
3389 );
3390
3391 drop(hook);
3393 }
3394
3395 #[tokio::test]
3396 async fn no_dispatch_hook_configured_dispatch_succeeds() {
3397 let mut builder = VerbRegistryBuilder::new();
3399 builder.register(SimplePack);
3400 let reg = builder.build().expect("registry builds");
3402
3403 let res = reg.dispatch("ping", Value::Null).await.unwrap();
3404 assert_eq!(res["verb"], "ping");
3405 }
3406}
3407
3408#[cfg(test)]
3411mod help_tests {
3412 use super::*;
3413 use async_trait::async_trait;
3414 use khive_types::Pack;
3415 use std::sync::{
3416 atomic::{AtomicUsize, Ordering},
3417 Arc,
3418 };
3419
3420 static CREATE_PARAMS: [ParamDef; 2] = [
3425 ParamDef {
3426 name: "kind",
3427 param_type: "string",
3428 required: true,
3429 description: "Granular kind (concept | document | ...).",
3430 },
3431 ParamDef {
3432 name: "name",
3433 param_type: "string",
3434 required: false,
3435 description: "Human-readable name.",
3436 },
3437 ];
3438
3439 static RECALL_PARAMS: [ParamDef; 2] = [
3440 ParamDef {
3441 name: "query",
3442 param_type: "string",
3443 required: true,
3444 description: "Semantic recall query.",
3445 },
3446 ParamDef {
3447 name: "limit",
3448 param_type: "integer",
3449 required: false,
3450 description: "Maximum memories to return.",
3451 },
3452 ];
3453
3454 static EMBED_PARAMS: [ParamDef; 0] = [];
3457
3458 struct HelpPack {
3459 invocations: Arc<AtomicUsize>,
3460 }
3461
3462 impl Pack for HelpPack {
3463 const NAME: &'static str = "helptest";
3464 const NOTE_KINDS: &'static [&'static str] = &[];
3465 const ENTITY_KINDS: &'static [&'static str] = &[];
3466 const HANDLERS: &'static [HandlerDef] = &[
3467 HandlerDef {
3468 name: "create",
3469 description: "Create an entity or note",
3470 visibility: Visibility::Verb,
3471 category: VerbCategory::Commissive,
3472 params: &CREATE_PARAMS,
3473 },
3474 HandlerDef {
3475 name: "recall",
3476 description: "Recall memory notes with decay-aware hybrid ranking",
3477 visibility: Visibility::Verb,
3478 category: VerbCategory::Assertive,
3479 params: &RECALL_PARAMS,
3480 },
3481 HandlerDef {
3484 name: "recall.embed",
3485 description: "Return the embedding vector used by memory recall",
3486 visibility: Visibility::Subhandler,
3487 category: VerbCategory::Assertive,
3488 params: &EMBED_PARAMS,
3489 },
3490 ];
3491 }
3492
3493 #[async_trait]
3494 impl PackRuntime for HelpPack {
3495 fn name(&self) -> &str {
3496 HelpPack::NAME
3497 }
3498 fn note_kinds(&self) -> &'static [&'static str] {
3499 HelpPack::NOTE_KINDS
3500 }
3501 fn entity_kinds(&self) -> &'static [&'static str] {
3502 HelpPack::ENTITY_KINDS
3503 }
3504 fn handlers(&self) -> &'static [HandlerDef] {
3505 HelpPack::HANDLERS
3506 }
3507 async fn dispatch(
3508 &self,
3509 verb: &str,
3510 _params: Value,
3511 _registry: &VerbRegistry,
3512 _token: &NamespaceToken,
3513 ) -> Result<Value, RuntimeError> {
3514 self.invocations.fetch_add(1, Ordering::SeqCst);
3515 Ok(serde_json::json!({ "pack": "helptest", "verb": verb }))
3516 }
3517 }
3518
3519 fn build_help_registry(invocations: Arc<AtomicUsize>) -> VerbRegistry {
3520 let mut builder = VerbRegistryBuilder::new();
3521 builder.register(HelpPack { invocations });
3522 builder.build().expect("help registry builds")
3523 }
3524
3525 #[tokio::test]
3528 async fn test_help_true_returns_schema_for_kg_create() {
3529 let invocations = Arc::new(AtomicUsize::new(0));
3530 let reg = build_help_registry(invocations.clone());
3531
3532 let result = reg
3533 .dispatch("create", serde_json::json!({ "help": true }))
3534 .await
3535 .expect("help=true must succeed for a known verb");
3536
3537 assert_eq!(result["verb"], "create", "envelope must name the verb");
3539 assert_eq!(
3540 result["pack"], "helptest",
3541 "envelope must name the owning pack"
3542 );
3543 assert!(
3544 result["description"].as_str().is_some(),
3545 "description must be a string"
3546 );
3547
3548 let params = result["params"]
3550 .as_array()
3551 .expect("params must be a JSON array");
3552 assert!(!params.is_empty(), "params array must not be empty");
3553
3554 let kind_param = params.iter().find(|p| p["name"] == "kind");
3556 assert!(
3557 kind_param.is_some(),
3558 "params array must include the 'kind' parameter"
3559 );
3560 let kind_param = kind_param.unwrap();
3561 assert_eq!(
3562 kind_param["required"],
3563 serde_json::json!(true),
3564 "'kind' must be required"
3565 );
3566 assert_eq!(kind_param["type"], "string", "'kind' type must be 'string'");
3567 }
3568
3569 #[tokio::test]
3571 async fn test_help_true_returns_schema_for_recall() {
3572 let invocations = Arc::new(AtomicUsize::new(0));
3573 let reg = build_help_registry(invocations.clone());
3574
3575 let result = reg
3576 .dispatch("recall", serde_json::json!({ "help": true }))
3577 .await
3578 .expect("help=true must succeed for recall");
3579
3580 assert_eq!(result["verb"], "recall");
3581 assert_eq!(result["pack"], "helptest");
3582
3583 let params = result["params"]
3584 .as_array()
3585 .expect("params must be a JSON array");
3586
3587 let query_param = params.iter().find(|p| p["name"] == "query");
3589 assert!(query_param.is_some(), "params must include 'query'");
3590 let query_param = query_param.unwrap();
3591 assert_eq!(
3592 query_param["required"],
3593 serde_json::json!(true),
3594 "'query' must be required"
3595 );
3596
3597 let limit_param = params.iter().find(|p| p["name"] == "limit");
3599 assert!(limit_param.is_some(), "params must include 'limit'");
3600 let limit_param = limit_param.unwrap();
3601 assert_eq!(
3602 limit_param["required"],
3603 serde_json::json!(false),
3604 "'limit' must be optional"
3605 );
3606 }
3607
3608 #[tokio::test]
3611 async fn test_help_true_does_not_execute_the_verb() {
3612 let invocations = Arc::new(AtomicUsize::new(0));
3613 let reg = build_help_registry(invocations.clone());
3614
3615 reg.dispatch("create", serde_json::json!({ "help": true }))
3617 .await
3618 .expect("help=true must succeed");
3619 reg.dispatch("recall", serde_json::json!({ "help": true }))
3620 .await
3621 .expect("help=true must succeed");
3622
3623 assert_eq!(
3624 invocations.load(Ordering::SeqCst),
3625 0,
3626 "pack dispatch MUST NOT be invoked when help=true; \
3627 got {} invocation(s)",
3628 invocations.load(Ordering::SeqCst)
3629 );
3630
3631 reg.dispatch("create", serde_json::json!({}))
3633 .await
3634 .expect("normal dispatch must succeed");
3635 assert_eq!(
3636 invocations.load(Ordering::SeqCst),
3637 1,
3638 "pack dispatch must fire exactly once for a normal call"
3639 );
3640 }
3641
3642 #[tokio::test]
3651 async fn help_true_on_subhandler_returns_callable_via_mcp_false() {
3652 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3653
3654 let result = reg
3655 .dispatch("recall.embed", serde_json::json!({ "help": true }))
3656 .await
3657 .expect("help=true on subhandler must succeed (no permission check on help path)");
3658
3659 assert_eq!(
3660 result["callable_via_mcp"],
3661 serde_json::json!(false),
3662 "subhandler help must carry callable_via_mcp: false"
3663 );
3664 assert_eq!(
3665 result["visibility"], "internal",
3666 "subhandler help must carry visibility: internal"
3667 );
3668 assert_eq!(result["verb"], "recall.embed");
3671 assert_eq!(result["pack"], "helptest");
3672 }
3673
3674 #[tokio::test]
3676 async fn help_true_on_public_verb_does_not_have_callable_via_mcp_false() {
3677 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3678
3679 let result = reg
3680 .dispatch("create", serde_json::json!({ "help": true }))
3681 .await
3682 .expect("help=true on public verb must succeed");
3683
3684 assert_ne!(
3686 result.get("callable_via_mcp"),
3687 Some(&serde_json::json!(false)),
3688 "public verb help must NOT carry callable_via_mcp: false"
3689 );
3690 assert_ne!(
3692 result.get("visibility"),
3693 Some(&serde_json::json!("internal")),
3694 "public verb help must NOT carry visibility: internal"
3695 );
3696 }
3697
3698 #[tokio::test]
3700 async fn help_true_on_unknown_verb_returns_error() {
3701 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3702
3703 let err = reg
3704 .dispatch("nonexistent_verb", serde_json::json!({ "help": true }))
3705 .await
3706 .unwrap_err();
3707
3708 assert!(
3709 matches!(err, RuntimeError::InvalidInput(_)),
3710 "help=true on unknown verb must return InvalidInput, got {err:?}"
3711 );
3712 let msg = err.to_string();
3713 assert!(
3714 msg.contains("nonexistent_verb"),
3715 "error must name the unknown verb: {msg}"
3716 );
3717 }
3718
3719 #[tokio::test]
3721 async fn help_true_on_subhandler_includes_params_field() {
3722 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3723
3724 let result = reg
3725 .dispatch("recall.embed", serde_json::json!({ "help": true }))
3726 .await
3727 .expect("help=true on subhandler must succeed");
3728
3729 let params = result
3731 .get("params")
3732 .expect("subhandler help must include 'params' field");
3733 assert!(
3734 params.is_array(),
3735 "subhandler help params must be a JSON array"
3736 );
3737 }
3738
3739 #[tokio::test]
3745 async fn help_true_unknown_verb_available_list_excludes_subhandlers() {
3746 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3747
3748 let err = reg
3749 .dispatch("not_a_verb", serde_json::json!({ "help": true }))
3750 .await
3751 .unwrap_err();
3752
3753 let msg = err.to_string();
3754 assert!(
3757 !msg.contains("recall.embed"),
3758 "unknown-verb help error must not advertise subhandler recall.embed: {msg}"
3759 );
3760 assert!(
3762 msg.contains("create"),
3763 "unknown-verb help error must still list public verb 'create': {msg}"
3764 );
3765 assert!(
3766 msg.contains("recall"),
3767 "unknown-verb help error must still list public verb 'recall': {msg}"
3768 );
3769 }
3770
3771 #[tokio::test]
3773 async fn dispatch_unknown_verb_available_list_excludes_subhandlers() {
3774 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3775
3776 let err = reg
3777 .dispatch("not_a_verb", serde_json::json!({}))
3778 .await
3779 .unwrap_err();
3780
3781 let msg = err.to_string();
3782 assert!(
3785 !msg.contains("recall.embed"),
3786 "dispatch unknown-verb error must not advertise subhandler recall.embed: {msg}"
3787 );
3788 assert!(
3790 msg.contains("create"),
3791 "dispatch unknown-verb error must still list public verb 'create': {msg}"
3792 );
3793 assert!(
3794 msg.contains("recall"),
3795 "dispatch unknown-verb error must still list public verb 'recall': {msg}"
3796 );
3797 }
3798}