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 use std::sync::{Mutex as StdMutex, Once, OnceLock};
1884
1885 use serial_test::serial;
1886 use tracing::field::{Field, Visit};
1887
1888 #[derive(Clone, Debug, Default)]
1889 struct CapturedEvent {
1890 message: Option<String>,
1891 audit_event: Option<String>,
1892 }
1893
1894 #[derive(Default)]
1895 struct CapturedEventVisitor(CapturedEvent);
1896
1897 impl Visit for CapturedEventVisitor {
1898 fn record_str(&mut self, field: &Field, value: &str) {
1899 match field.name() {
1900 "message" => self.0.message = Some(value.to_string()),
1901 "audit_event" => self.0.audit_event = Some(value.to_string()),
1902 _ => {}
1903 }
1904 }
1905
1906 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1907 let formatted = format!("{value:?}");
1913 let cleaned = formatted
1914 .trim_start_matches('"')
1915 .trim_end_matches('"')
1916 .to_string();
1917 match field.name() {
1918 "message" => self.0.message = Some(cleaned),
1919 "audit_event" => self.0.audit_event = Some(cleaned),
1920 _ => {}
1921 }
1922 }
1923 }
1924
1925 struct CaptureSubscriber {
1938 events: Arc<StdMutex<Vec<CapturedEvent>>>,
1939 }
1940
1941 impl CaptureSubscriber {
1942 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
1943 Self { events }
1944 }
1945 }
1946
1947 impl tracing::Subscriber for CaptureSubscriber {
1948 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
1949 true
1950 }
1951 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1952 tracing::span::Id::from_u64(1)
1953 }
1954 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
1955 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
1956 fn event(&self, event: &tracing::Event<'_>) {
1957 let mut visitor = CapturedEventVisitor::default();
1958 event.record(&mut visitor);
1959 self.events.lock().unwrap().push(visitor.0);
1960 }
1961 fn enter(&self, _: &tracing::span::Id) {}
1962 fn exit(&self, _: &tracing::span::Id) {}
1963 }
1964
1965 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
1975 static GLOBAL_INIT: Once = Once::new();
1976
1977 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
1978 GLOBAL_INIT.call_once(|| {
1979 let buffer = Arc::new(StdMutex::new(Vec::new()));
1980 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
1981 let _ = tracing::subscriber::set_global_default(subscriber);
1986 let _ = GLOBAL_CAPTURE.set(buffer);
1987 });
1988 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
1989 }
1990
1991 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
1996 where
1997 Fut: std::future::Future<Output = ()>,
1998 {
1999 let buffer = global_capture();
2000 buffer.lock().unwrap().clear();
2001
2002 let rt = tokio::runtime::Builder::new_current_thread()
2003 .enable_all()
2004 .build()
2005 .expect("build current-thread tokio runtime");
2006 rt.block_on(future);
2007
2008 let result = buffer.lock().unwrap().clone();
2009 result
2010 }
2011
2012 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
2019 events
2020 .iter()
2021 .filter(|e| e.message.as_deref() == Some("gate.check"))
2022 .filter(|e| {
2023 e.audit_event
2024 .as_deref()
2025 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
2026 .and_then(|v| {
2027 v.get("gate_impl")
2028 .and_then(|g| g.as_str().map(|s| s.to_string()))
2029 })
2030 .as_deref()
2031 == Some(gate_impl)
2032 })
2033 .cloned()
2034 .collect()
2035 }
2036
2037 #[test]
2038 #[serial]
2039 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
2040 #[derive(Debug)]
2041 struct TracingAllowGate;
2042 impl Gate for TracingAllowGate {
2043 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
2044 Ok(GateDecision::allow())
2045 }
2046 fn impl_name(&self) -> &'static str {
2047 "TracingAllowGate"
2048 }
2049 }
2050
2051 let events = capture_dispatch_events(async {
2052 let mut builder = VerbRegistryBuilder::new();
2053 builder.register(AlphaPack);
2054 builder.with_gate(Arc::new(TracingAllowGate));
2055 builder.with_default_namespace("tenant-default");
2056 let reg = builder.build().expect("registry builds");
2057 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
2058 .await
2059 .unwrap();
2060 });
2061
2062 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
2063 assert_eq!(
2064 gate_events.len(),
2065 1,
2066 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
2067 );
2068 let payload = gate_events[0]
2069 .audit_event
2070 .as_ref()
2071 .expect("gate.check event must carry an audit_event field");
2072 let audit: khive_gate::AuditEvent =
2073 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2074 assert_eq!(audit.decision, AuditDecision::Allow);
2075 assert_eq!(audit.verb, "list");
2076 assert_eq!(audit.namespace, "tenant-q");
2077 assert_eq!(audit.gate_impl, "TracingAllowGate");
2078 assert!(
2079 audit.deny_reason.is_none(),
2080 "deny_reason must be None on Allow"
2081 );
2082 }
2083
2084 use crate::runtime::NamespaceToken;
2087 use async_trait::async_trait;
2088 use khive_storage::{
2089 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
2090 };
2091 use khive_types::EventOutcome;
2092
2093 #[derive(Default, Debug)]
2095 struct MemoryEventStore {
2096 events: std::sync::Mutex<Vec<Event>>,
2097 }
2098
2099 #[async_trait]
2100 impl EventStore for MemoryEventStore {
2101 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
2102 self.events.lock().unwrap().push(event);
2103 Ok(())
2104 }
2105 async fn append_events(
2106 &self,
2107 events: Vec<Event>,
2108 ) -> khive_storage::StorageResult<BatchWriteSummary> {
2109 let attempted = events.len() as u64;
2110 let affected = attempted;
2111 self.events.lock().unwrap().extend(events);
2112 Ok(BatchWriteSummary {
2113 attempted,
2114 affected,
2115 failed: 0,
2116 first_error: String::new(),
2117 })
2118 }
2119 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
2120 Ok(self
2121 .events
2122 .lock()
2123 .unwrap()
2124 .iter()
2125 .find(|e| e.id == id)
2126 .cloned())
2127 }
2128 async fn query_events(
2129 &self,
2130 _filter: EventFilter,
2131 _page: PageRequest,
2132 ) -> khive_storage::StorageResult<Page<Event>> {
2133 let items = self.events.lock().unwrap().clone();
2134 let total = items.len() as u64;
2135 Ok(Page {
2136 items,
2137 total: Some(total),
2138 })
2139 }
2140 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
2141 Ok(self.events.lock().unwrap().len() as u64)
2142 }
2143 }
2144
2145 #[tokio::test]
2146 async fn allow_all_gate_default_remains_backward_compatible() {
2147 let mut builder = VerbRegistryBuilder::new();
2149 builder.register(AlphaPack);
2150 let reg = builder.build().expect("registry builds");
2151
2152 let res = reg.dispatch("list", Value::Null).await.unwrap();
2153 assert_eq!(
2154 res["pack"], "alpha",
2155 "AllowAllGate must allow every verb — backward compat guarantee"
2156 );
2157 let res = reg.dispatch("create", Value::Null).await.unwrap();
2158 assert_eq!(res["pack"], "alpha");
2159 }
2160
2161 #[tokio::test]
2162 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
2163 #[derive(Debug)]
2164 struct AlwaysDenyGate;
2165 impl Gate for AlwaysDenyGate {
2166 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2167 Ok(GateDecision::deny("test: always deny"))
2168 }
2169 }
2170
2171 #[derive(Debug)]
2173 struct TrackedPack {
2174 invoked: Arc<AtomicUsize>,
2175 }
2176
2177 impl khive_types::Pack for TrackedPack {
2178 const NAME: &'static str = "tracked";
2179 const NOTE_KINDS: &'static [&'static str] = &[];
2180 const ENTITY_KINDS: &'static [&'static str] = &[];
2181 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2182 name: "guarded",
2183 description: "a guarded verb",
2184 visibility: Visibility::Verb,
2185 category: VerbCategory::Assertive,
2186 params: &[],
2187 }];
2188 }
2189
2190 #[async_trait]
2191 impl PackRuntime for TrackedPack {
2192 fn name(&self) -> &str {
2193 Self::NAME
2194 }
2195 fn note_kinds(&self) -> &'static [&'static str] {
2196 Self::NOTE_KINDS
2197 }
2198 fn entity_kinds(&self) -> &'static [&'static str] {
2199 Self::ENTITY_KINDS
2200 }
2201 fn handlers(&self) -> &'static [HandlerDef] {
2202 Self::HANDLERS
2203 }
2204 async fn dispatch(
2205 &self,
2206 _verb: &str,
2207 _params: Value,
2208 _registry: &VerbRegistry,
2209 _token: &NamespaceToken,
2210 ) -> Result<Value, RuntimeError> {
2211 self.invoked.fetch_add(1, Ordering::SeqCst);
2212 Ok(serde_json::json!({"invoked": true}))
2213 }
2214 }
2215
2216 let invoked = Arc::new(AtomicUsize::new(0));
2217 let mut builder = VerbRegistryBuilder::new();
2218 builder.register(TrackedPack {
2219 invoked: invoked.clone(),
2220 });
2221 builder.with_gate(Arc::new(AlwaysDenyGate));
2222 let reg = builder.build().expect("registry builds");
2223
2224 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
2225 assert!(
2226 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
2227 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
2228 );
2229 assert_eq!(
2230 invoked.load(Ordering::SeqCst),
2231 0,
2232 "pack dispatch MUST NOT be invoked when gate denies"
2233 );
2234 }
2235
2236 #[tokio::test]
2237 async fn audit_event_persists_to_event_store_on_allow() {
2238 let store = Arc::new(MemoryEventStore::default());
2239 let mut builder = VerbRegistryBuilder::new();
2240 builder.register(AlphaPack);
2241 builder.with_event_store(store.clone());
2242 let reg = builder.build().expect("registry builds");
2243
2244 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2245 .await
2246 .unwrap();
2247
2248 let count = store.count_events(EventFilter::default()).await.unwrap();
2249 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
2250
2251 let page = store
2252 .query_events(
2253 EventFilter::default(),
2254 PageRequest {
2255 limit: 10,
2256 offset: 0,
2257 },
2258 )
2259 .await
2260 .unwrap();
2261 let ev = &page.items[0];
2262 assert_eq!(ev.verb, "list");
2263 assert_eq!(ev.namespace, "test-ns");
2264 assert_eq!(ev.substrate, SubstrateKind::Event);
2265 assert_eq!(ev.outcome, EventOutcome::Success);
2266 }
2267
2268 #[tokio::test]
2269 async fn audit_event_persists_to_event_store_on_deny() {
2270 #[derive(Debug)]
2271 struct AlwaysDenyGate;
2272 impl Gate for AlwaysDenyGate {
2273 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2274 Ok(GateDecision::deny("denied by test"))
2275 }
2276 }
2277
2278 let store = Arc::new(MemoryEventStore::default());
2279 let mut builder = VerbRegistryBuilder::new();
2280 builder.register(AlphaPack);
2281 builder.with_gate(Arc::new(AlwaysDenyGate));
2282 builder.with_event_store(store.clone());
2283 let reg = builder.build().expect("registry builds");
2284
2285 let err = reg
2287 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2288 .await
2289 .unwrap_err();
2290 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2291
2292 let count = store.count_events(EventFilter::default()).await.unwrap();
2293 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
2294
2295 let page = store
2296 .query_events(
2297 EventFilter::default(),
2298 PageRequest {
2299 limit: 10,
2300 offset: 0,
2301 },
2302 )
2303 .await
2304 .unwrap();
2305 let ev = &page.items[0];
2306 assert_eq!(ev.verb, "list");
2307 assert_eq!(ev.outcome, EventOutcome::Denied);
2308 }
2309
2310 #[tokio::test]
2311 async fn gate_error_does_not_persist_to_event_store() {
2312 #[derive(Debug)]
2313 struct FailingGate;
2314 impl Gate for FailingGate {
2315 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
2316 Err(khive_gate::GateError::Internal("gate broken".into()))
2317 }
2318 }
2319
2320 let store = Arc::new(MemoryEventStore::default());
2321 let mut builder = VerbRegistryBuilder::new();
2322 builder.register(AlphaPack);
2323 builder.with_gate(Arc::new(FailingGate));
2324 builder.with_event_store(store.clone());
2325 let reg = builder.build().expect("registry builds");
2326
2327 let res = reg.dispatch("list", Value::Null).await.unwrap();
2329 assert_eq!(
2330 res["pack"], "alpha",
2331 "gate error must fail-open, not block dispatch"
2332 );
2333
2334 let count = store.count_events(EventFilter::default()).await.unwrap();
2335 assert_eq!(
2336 count, 0,
2337 "gate infrastructure error must NOT produce an audit event in EventStore"
2338 );
2339 }
2340
2341 #[tokio::test]
2342 async fn no_event_store_configured_tracing_only() {
2343 let mut builder = VerbRegistryBuilder::new();
2347 builder.register(AlphaPack);
2348 let reg = builder.build().expect("registry builds");
2349
2350 let res = reg.dispatch("list", Value::Null).await.unwrap();
2351 assert_eq!(res["pack"], "alpha");
2352 }
2353
2354 #[test]
2355 #[serial]
2356 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
2357 #[derive(Debug)]
2358 struct TracingDenyGate;
2359 impl Gate for TracingDenyGate {
2360 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2361 Ok(GateDecision::deny("denied by test gate"))
2362 }
2363 fn impl_name(&self) -> &'static str {
2364 "TracingDenyGate"
2365 }
2366 }
2367
2368 let events = capture_dispatch_events(async {
2369 let mut builder = VerbRegistryBuilder::new();
2370 builder.register(AlphaPack);
2371 builder.with_gate(Arc::new(TracingDenyGate));
2372 let reg = builder.build().expect("registry builds");
2373 let _ = reg.dispatch("create", serde_json::Value::Null).await;
2376 });
2377
2378 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
2379 assert_eq!(
2380 gate_events.len(),
2381 1,
2382 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
2383 );
2384 let payload = gate_events[0]
2385 .audit_event
2386 .as_ref()
2387 .expect("gate.check event must carry an audit_event field on Deny");
2388 let audit: khive_gate::AuditEvent =
2389 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2390 assert_eq!(audit.decision, AuditDecision::Deny);
2391 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
2392 assert_eq!(audit.gate_impl, "TracingDenyGate");
2393 let payload_json: serde_json::Value =
2397 serde_json::from_str(payload).expect("payload must be valid JSON");
2398 assert_eq!(
2399 payload_json["obligations"],
2400 serde_json::Value::Array(Vec::new()),
2401 "obligations must be `[]` on Deny on the tracing payload, not omitted"
2402 );
2403 }
2404
2405 #[tokio::test]
2413 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
2414 #[derive(Debug)]
2415 struct DenyGateWithName;
2416 impl Gate for DenyGateWithName {
2417 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2418 Ok(GateDecision::deny("policy: write forbidden for anon"))
2419 }
2420 fn impl_name(&self) -> &'static str {
2421 "DenyGateWithName"
2422 }
2423 }
2424
2425 let store = Arc::new(MemoryEventStore::default());
2426 let mut builder = VerbRegistryBuilder::new();
2427 builder.register(AlphaPack);
2428 builder.with_gate(Arc::new(DenyGateWithName));
2429 builder.with_event_store(store.clone());
2430 let reg = builder.build().expect("registry builds");
2431
2432 let err = reg
2434 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2435 .await
2436 .unwrap_err();
2437 assert!(
2438 matches!(err, RuntimeError::PermissionDenied { .. }),
2439 "expected PermissionDenied, got {err:?}"
2440 );
2441
2442 let page = store
2444 .query_events(
2445 EventFilter::default(),
2446 PageRequest {
2447 limit: 10,
2448 offset: 0,
2449 },
2450 )
2451 .await
2452 .unwrap();
2453 assert_eq!(
2454 page.items.len(),
2455 1,
2456 "one audit event must be persisted on deny"
2457 );
2458
2459 let ev = &page.items[0];
2460 assert_eq!(ev.outcome, EventOutcome::Denied);
2461
2462 let data = &ev.payload;
2464
2465 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2466 .expect("Event.payload must deserialize to AuditEvent");
2467
2468 assert_eq!(
2469 audit.deny_reason.as_deref(),
2470 Some("policy: write forbidden for anon"),
2471 "deny_reason must be preserved through EventStore"
2472 );
2473 assert_eq!(
2474 audit.gate_impl, "DenyGateWithName",
2475 "gate_impl must be preserved through EventStore"
2476 );
2477 assert_eq!(
2478 audit.decision,
2479 khive_gate::AuditDecision::Deny,
2480 "decision field must be preserved through EventStore"
2481 );
2482 }
2483
2484 #[tokio::test]
2485 async fn audit_envelope_round_trips_obligations_through_event_store() {
2486 use khive_gate::Obligation;
2487
2488 #[derive(Debug)]
2489 struct ObligationGate;
2490 impl Gate for ObligationGate {
2491 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2492 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2493 tag: "billing.meter".into(),
2494 }]))
2495 }
2496 fn impl_name(&self) -> &'static str {
2497 "ObligationGate"
2498 }
2499 }
2500
2501 let store = Arc::new(MemoryEventStore::default());
2502 let mut builder = VerbRegistryBuilder::new();
2503 builder.register(AlphaPack);
2504 builder.with_gate(Arc::new(ObligationGate));
2505 builder.with_event_store(store.clone());
2506 let reg = builder.build().expect("registry builds");
2507
2508 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2509 .await
2510 .unwrap();
2511
2512 let page = store
2513 .query_events(
2514 EventFilter::default(),
2515 PageRequest {
2516 limit: 10,
2517 offset: 0,
2518 },
2519 )
2520 .await
2521 .unwrap();
2522 assert_eq!(page.items.len(), 1);
2523
2524 let ev = &page.items[0];
2525 assert_eq!(ev.outcome, EventOutcome::Success);
2526
2527 let data = &ev.payload;
2528
2529 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2530 .expect("Event.payload must deserialize to AuditEvent");
2531
2532 assert_eq!(audit.gate_impl, "ObligationGate");
2533 assert_eq!(
2534 audit.obligations.len(),
2535 1,
2536 "obligations must be preserved through EventStore"
2537 );
2538 match &audit.obligations[0] {
2539 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
2540 other => panic!("expected Audit obligation, got {other:?}"),
2541 }
2542 }
2543
2544 #[tokio::test]
2552 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
2553 #[derive(Debug)]
2554 struct SqlTestDenyGate;
2555 impl Gate for SqlTestDenyGate {
2556 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2557 Ok(GateDecision::deny("sql-path: write denied"))
2558 }
2559 fn impl_name(&self) -> &'static str {
2560 "SqlTestDenyGate"
2561 }
2562 }
2563
2564 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2568 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2569 let sql_store = rt
2570 .events(&test_tok)
2571 .expect("events_for_namespace must succeed");
2572
2573 let mut builder = VerbRegistryBuilder::new();
2574 builder.register(AlphaPack);
2575 builder.with_gate(Arc::new(SqlTestDenyGate));
2576 builder.with_event_store(sql_store.clone());
2577 let reg = builder.build().expect("registry builds");
2578
2579 let err = reg
2581 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2582 .await
2583 .unwrap_err();
2584 assert!(
2585 matches!(err, RuntimeError::PermissionDenied { .. }),
2586 "expected PermissionDenied, got {err:?}"
2587 );
2588
2589 let page = sql_store
2591 .query_events(
2592 EventFilter::default(),
2593 PageRequest {
2594 limit: 10,
2595 offset: 0,
2596 },
2597 )
2598 .await
2599 .unwrap();
2600 assert_eq!(
2601 page.items.len(),
2602 1,
2603 "one audit event must be persisted on deny through SqlEventStore"
2604 );
2605
2606 let ev = &page.items[0];
2607 assert_eq!(ev.outcome, EventOutcome::Denied);
2608
2609 let data = &ev.payload;
2613
2614 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2615 .expect("Event.payload must deserialize to AuditEvent after SQL round-trip");
2616
2617 assert_eq!(
2618 audit.deny_reason.as_deref(),
2619 Some("sql-path: write denied"),
2620 "deny_reason must survive the SQL text round-trip"
2621 );
2622 assert_eq!(
2623 audit.gate_impl, "SqlTestDenyGate",
2624 "gate_impl must survive the SQL text round-trip"
2625 );
2626 assert_eq!(
2627 audit.decision,
2628 khive_gate::AuditDecision::Deny,
2629 "decision field must survive the SQL text round-trip"
2630 );
2631 assert!(
2634 audit.obligations.is_empty(),
2635 "obligations must be preserved as empty [] through SQL round-trip"
2636 );
2637 }
2638
2639 #[tokio::test]
2651 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
2652 use khive_gate::Obligation;
2653
2654 #[derive(Debug)]
2655 struct SqlTestAllowWithObligationGate;
2656 impl Gate for SqlTestAllowWithObligationGate {
2657 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2658 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2659 tag: "sql-path-billing.meter".into(),
2660 }]))
2661 }
2662 fn impl_name(&self) -> &'static str {
2663 "SqlTestAllowWithObligationGate"
2664 }
2665 }
2666
2667 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2668 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2669 let sql_store = rt
2670 .events(&test_tok)
2671 .expect("events_for_namespace must succeed");
2672
2673 let mut builder = VerbRegistryBuilder::new();
2674 builder.register(AlphaPack);
2675 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
2676 builder.with_event_store(sql_store.clone());
2677 let reg = builder.build().expect("registry builds");
2678
2679 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2681 .await
2682 .expect("dispatch must succeed when gate allows");
2683
2684 let page = sql_store
2686 .query_events(
2687 EventFilter::default(),
2688 PageRequest {
2689 limit: 10,
2690 offset: 0,
2691 },
2692 )
2693 .await
2694 .unwrap();
2695 assert_eq!(
2696 page.items.len(),
2697 1,
2698 "one audit event must be persisted on allow through SqlEventStore"
2699 );
2700
2701 let ev = &page.items[0];
2702 assert_eq!(ev.outcome, EventOutcome::Success);
2703
2704 let data = &ev.payload;
2705
2706 let obligations_raw = data
2711 .get("obligations")
2712 .expect("Event.data JSON must contain 'obligations' key");
2713 let obligations_arr = obligations_raw
2714 .as_array()
2715 .expect("'obligations' must be a JSON array");
2716 assert!(
2717 !obligations_arr.is_empty(),
2718 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
2719 );
2720
2721 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2724 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
2725
2726 assert_eq!(
2727 audit.gate_impl, "SqlTestAllowWithObligationGate",
2728 "gate_impl must survive the SQL text round-trip"
2729 );
2730 assert_eq!(
2731 audit.decision,
2732 khive_gate::AuditDecision::Allow,
2733 "decision field must survive the SQL text round-trip"
2734 );
2735 assert_eq!(
2736 audit.obligations.len(),
2737 1,
2738 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
2739 );
2740 match &audit.obligations[0] {
2741 Obligation::Audit { tag } => assert_eq!(
2742 tag, "sql-path-billing.meter",
2743 "Audit obligation tag must survive the SQL text round-trip"
2744 ),
2745 other => panic!("expected Audit obligation, got {other:?}"),
2746 }
2747 }
2748
2749 #[tokio::test]
2757 async fn audit_event_payload_shape_for_create_verb() {
2758 let store = Arc::new(MemoryEventStore::default());
2759 let mut builder = VerbRegistryBuilder::new();
2760 builder.register(AlphaPack);
2761 builder.with_event_store(store.clone());
2762 builder.with_default_namespace("test-ns");
2763 let reg = builder.build().expect("registry builds");
2764
2765 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
2768 .await
2769 .unwrap();
2770
2771 let count = store.count_events(EventFilter::default()).await.unwrap();
2772 assert_eq!(count, 1, "exactly one audit event for one dispatch");
2773
2774 let page = store
2775 .query_events(
2776 EventFilter::default(),
2777 PageRequest {
2778 limit: 10,
2779 offset: 0,
2780 },
2781 )
2782 .await
2783 .unwrap();
2784 let ev = &page.items[0];
2785
2786 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
2788 assert_eq!(
2789 ev.outcome,
2790 EventOutcome::Success,
2791 "ev.outcome must be Success on allow"
2792 );
2793 assert_eq!(
2794 ev.namespace, "test-ns",
2795 "ev.namespace must match the dispatch namespace"
2796 );
2797
2798 let data = &ev.payload;
2800
2801 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2802 .expect("ev.payload must deserialize to AuditEvent");
2803
2804 assert_eq!(
2805 audit.decision,
2806 khive_gate::AuditDecision::Allow,
2807 "AuditEvent.decision must be Allow"
2808 );
2809 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
2810 assert_eq!(
2811 audit.namespace, "test-ns",
2812 "AuditEvent.namespace must be preserved"
2813 );
2814 assert_eq!(
2815 audit.gate_impl, "AllowAllGate",
2816 "AuditEvent.gate_impl must name the gate implementation"
2817 );
2818 assert!(
2819 audit.deny_reason.is_none(),
2820 "AuditEvent.deny_reason must be None on Allow"
2821 );
2822 let payload_json: serde_json::Value =
2824 serde_json::from_value(data.clone()).expect("data must be valid JSON");
2825 assert_eq!(
2826 payload_json["obligations"],
2827 serde_json::Value::Array(Vec::new()),
2828 "obligations must be [] on AllowAllGate"
2829 );
2830 }
2831
2832 #[tokio::test]
2834 async fn audit_event_threads_target_id_from_dispatch_args() {
2835 let store = Arc::new(MemoryEventStore::default());
2836 let target = uuid::Uuid::new_v4();
2837 let mut builder = VerbRegistryBuilder::new();
2838 builder.register(AlphaPack);
2839 builder.with_event_store(store.clone());
2840 builder.with_default_namespace("test-ns");
2841 let reg = builder.build().expect("registry builds");
2842
2843 reg.dispatch(
2844 "create",
2845 serde_json::json!({"namespace": "test-ns", "target_id": target}),
2846 )
2847 .await
2848 .unwrap();
2849
2850 let page = store
2851 .query_events(
2852 EventFilter::default(),
2853 PageRequest {
2854 offset: 0,
2855 limit: 10,
2856 },
2857 )
2858 .await
2859 .unwrap();
2860 assert_eq!(
2861 page.items[0].target_id,
2862 Some(target),
2863 "#282: audit event must carry target_id from dispatch params"
2864 );
2865 }
2866}
2867
2868#[cfg(test)]
2871mod dep_tests {
2872 use super::*;
2873 use async_trait::async_trait;
2874 use khive_types::Pack;
2875 use serde_json::Value;
2876
2877 struct KgDepPack;
2878 struct MemoryDepPack;
2879 struct ADepPack;
2880 struct BDepPack;
2881
2882 impl Pack for KgDepPack {
2883 const NAME: &'static str = "kg_dep";
2884 const NOTE_KINDS: &'static [&'static str] = &["observation"];
2885 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
2886 const HANDLERS: &'static [HandlerDef] = &[];
2887 }
2888
2889 impl Pack for MemoryDepPack {
2890 const NAME: &'static str = "memory_dep";
2891 const NOTE_KINDS: &'static [&'static str] = &["memory"];
2892 const ENTITY_KINDS: &'static [&'static str] = &[];
2893 const HANDLERS: &'static [HandlerDef] = &[];
2894 const REQUIRES: &'static [&'static str] = &["kg_dep"];
2895 }
2896
2897 impl Pack for ADepPack {
2898 const NAME: &'static str = "pack_a";
2899 const NOTE_KINDS: &'static [&'static str] = &[];
2900 const ENTITY_KINDS: &'static [&'static str] = &[];
2901 const HANDLERS: &'static [HandlerDef] = &[];
2902 const REQUIRES: &'static [&'static str] = &["pack_b"];
2903 }
2904
2905 impl Pack for BDepPack {
2906 const NAME: &'static str = "pack_b";
2907 const NOTE_KINDS: &'static [&'static str] = &[];
2908 const ENTITY_KINDS: &'static [&'static str] = &[];
2909 const HANDLERS: &'static [HandlerDef] = &[];
2910 const REQUIRES: &'static [&'static str] = &["pack_a"];
2911 }
2912
2913 #[async_trait]
2914 impl PackRuntime for KgDepPack {
2915 fn name(&self) -> &str {
2916 Self::NAME
2917 }
2918 fn note_kinds(&self) -> &'static [&'static str] {
2919 Self::NOTE_KINDS
2920 }
2921 fn entity_kinds(&self) -> &'static [&'static str] {
2922 Self::ENTITY_KINDS
2923 }
2924 fn handlers(&self) -> &'static [HandlerDef] {
2925 Self::HANDLERS
2926 }
2927 async fn dispatch(
2928 &self,
2929 verb: &str,
2930 _: Value,
2931 _: &VerbRegistry,
2932 _: &NamespaceToken,
2933 ) -> Result<Value, RuntimeError> {
2934 Err(RuntimeError::InvalidInput(format!(
2935 "KgDepPack has no verbs: {verb}"
2936 )))
2937 }
2938 }
2939
2940 #[async_trait]
2941 impl PackRuntime for MemoryDepPack {
2942 fn name(&self) -> &str {
2943 Self::NAME
2944 }
2945 fn note_kinds(&self) -> &'static [&'static str] {
2946 Self::NOTE_KINDS
2947 }
2948 fn entity_kinds(&self) -> &'static [&'static str] {
2949 Self::ENTITY_KINDS
2950 }
2951 fn handlers(&self) -> &'static [HandlerDef] {
2952 Self::HANDLERS
2953 }
2954 fn requires(&self) -> &'static [&'static str] {
2955 Self::REQUIRES
2956 }
2957 async fn dispatch(
2958 &self,
2959 verb: &str,
2960 _: Value,
2961 _: &VerbRegistry,
2962 _: &NamespaceToken,
2963 ) -> Result<Value, RuntimeError> {
2964 Err(RuntimeError::InvalidInput(format!(
2965 "MemoryDepPack has no verbs: {verb}"
2966 )))
2967 }
2968 }
2969
2970 #[async_trait]
2971 impl PackRuntime for ADepPack {
2972 fn name(&self) -> &str {
2973 Self::NAME
2974 }
2975 fn note_kinds(&self) -> &'static [&'static str] {
2976 Self::NOTE_KINDS
2977 }
2978 fn entity_kinds(&self) -> &'static [&'static str] {
2979 Self::ENTITY_KINDS
2980 }
2981 fn handlers(&self) -> &'static [HandlerDef] {
2982 Self::HANDLERS
2983 }
2984 fn requires(&self) -> &'static [&'static str] {
2985 Self::REQUIRES
2986 }
2987 async fn dispatch(
2988 &self,
2989 verb: &str,
2990 _: Value,
2991 _: &VerbRegistry,
2992 _: &NamespaceToken,
2993 ) -> Result<Value, RuntimeError> {
2994 Err(RuntimeError::InvalidInput(format!(
2995 "ADepPack has no verbs: {verb}"
2996 )))
2997 }
2998 }
2999
3000 #[async_trait]
3001 impl PackRuntime for BDepPack {
3002 fn name(&self) -> &str {
3003 Self::NAME
3004 }
3005 fn note_kinds(&self) -> &'static [&'static str] {
3006 Self::NOTE_KINDS
3007 }
3008 fn entity_kinds(&self) -> &'static [&'static str] {
3009 Self::ENTITY_KINDS
3010 }
3011 fn handlers(&self) -> &'static [HandlerDef] {
3012 Self::HANDLERS
3013 }
3014 fn requires(&self) -> &'static [&'static str] {
3015 Self::REQUIRES
3016 }
3017 async fn dispatch(
3018 &self,
3019 verb: &str,
3020 _: Value,
3021 _: &VerbRegistry,
3022 _: &NamespaceToken,
3023 ) -> Result<Value, RuntimeError> {
3024 Err(RuntimeError::InvalidInput(format!(
3025 "BDepPack has no verbs: {verb}"
3026 )))
3027 }
3028 }
3029
3030 #[test]
3031 fn test_pack_deps_happy_path() {
3032 let mut builder = VerbRegistryBuilder::new();
3033 builder.register(MemoryDepPack);
3034 builder.register(KgDepPack);
3035 let reg = builder
3036 .build()
3037 .expect("kg_dep satisfies memory_dep dependency");
3038 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
3039 let names = reg.pack_names();
3040 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
3041 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
3042 assert!(
3043 kg_pos < mem_pos,
3044 "kg_dep must be loaded before memory_dep; order: {names:?}"
3045 );
3046 }
3047
3048 #[test]
3049 fn test_pack_deps_missing() {
3050 let mut builder = VerbRegistryBuilder::new();
3051 builder.register(MemoryDepPack);
3052 let err = match builder.build() {
3053 Ok(_) => panic!("expected Err, got Ok"),
3054 Err(e) => e,
3055 };
3056 assert!(
3057 matches!(err, RuntimeError::MissingPackDependency(_)),
3058 "expected MissingPackDependency, got {err:?}"
3059 );
3060 let msg = err.to_string();
3061 assert!(
3062 msg.contains("memory_dep"),
3063 "error must name the dependent pack: {msg}"
3064 );
3065 assert!(
3066 msg.contains("kg_dep"),
3067 "error must name the missing dep: {msg}"
3068 );
3069 }
3070
3071 #[test]
3072 fn test_pack_deps_circular() {
3073 let mut builder = VerbRegistryBuilder::new();
3074 builder.register(ADepPack);
3075 builder.register(BDepPack);
3076 let err = match builder.build() {
3077 Ok(_) => panic!("expected Err, got Ok"),
3078 Err(e) => e,
3079 };
3080 assert!(
3081 matches!(err, RuntimeError::CircularPackDependency(_)),
3082 "expected CircularPackDependency, got {err:?}"
3083 );
3084 let msg = err.to_string();
3085 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
3086 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
3087 }
3088
3089 #[test]
3090 fn test_pack_deps_no_deps() {
3091 struct NoDepsA;
3092 struct NoDepsB;
3093
3094 impl Pack for NoDepsA {
3095 const NAME: &'static str = "no_deps_a";
3096 const NOTE_KINDS: &'static [&'static str] = &[];
3097 const ENTITY_KINDS: &'static [&'static str] = &[];
3098 const HANDLERS: &'static [HandlerDef] = &[];
3099 }
3100
3101 impl Pack for NoDepsB {
3102 const NAME: &'static str = "no_deps_b";
3103 const NOTE_KINDS: &'static [&'static str] = &[];
3104 const ENTITY_KINDS: &'static [&'static str] = &[];
3105 const HANDLERS: &'static [HandlerDef] = &[];
3106 }
3107
3108 #[async_trait]
3109 impl PackRuntime for NoDepsA {
3110 fn name(&self) -> &str {
3111 Self::NAME
3112 }
3113 fn note_kinds(&self) -> &'static [&'static str] {
3114 Self::NOTE_KINDS
3115 }
3116 fn entity_kinds(&self) -> &'static [&'static str] {
3117 Self::ENTITY_KINDS
3118 }
3119 fn handlers(&self) -> &'static [HandlerDef] {
3120 Self::HANDLERS
3121 }
3122 async fn dispatch(
3123 &self,
3124 verb: &str,
3125 _: Value,
3126 _: &VerbRegistry,
3127 _: &NamespaceToken,
3128 ) -> Result<Value, RuntimeError> {
3129 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
3130 }
3131 }
3132
3133 #[async_trait]
3134 impl PackRuntime for NoDepsB {
3135 fn name(&self) -> &str {
3136 Self::NAME
3137 }
3138 fn note_kinds(&self) -> &'static [&'static str] {
3139 Self::NOTE_KINDS
3140 }
3141 fn entity_kinds(&self) -> &'static [&'static str] {
3142 Self::ENTITY_KINDS
3143 }
3144 fn handlers(&self) -> &'static [HandlerDef] {
3145 Self::HANDLERS
3146 }
3147 async fn dispatch(
3148 &self,
3149 verb: &str,
3150 _: Value,
3151 _: &VerbRegistry,
3152 _: &NamespaceToken,
3153 ) -> Result<Value, RuntimeError> {
3154 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
3155 }
3156 }
3157
3158 let mut builder = VerbRegistryBuilder::new();
3159 builder.register(NoDepsA);
3160 builder.register(NoDepsB);
3161 let reg = builder.build().expect("packs with REQUIRES=&[] build");
3162 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
3163 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
3164 }
3165}
3166
3167#[cfg(test)]
3170mod hook_tests {
3171 use super::*;
3172 use async_trait::async_trait;
3173 use khive_types::Pack;
3174 use std::sync::atomic::{AtomicUsize, Ordering};
3175 use std::sync::Mutex as StdMutex;
3176
3177 struct SimplePack;
3178
3179 impl Pack for SimplePack {
3180 const NAME: &'static str = "simple";
3181 const NOTE_KINDS: &'static [&'static str] = &[];
3182 const ENTITY_KINDS: &'static [&'static str] = &[];
3183 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
3184 name: "ping",
3185 description: "ping",
3186 visibility: Visibility::Verb,
3187 category: VerbCategory::Assertive,
3188 params: &[],
3189 }];
3190 }
3191
3192 #[async_trait]
3193 impl PackRuntime for SimplePack {
3194 fn name(&self) -> &str {
3195 SimplePack::NAME
3196 }
3197 fn note_kinds(&self) -> &'static [&'static str] {
3198 SimplePack::NOTE_KINDS
3199 }
3200 fn entity_kinds(&self) -> &'static [&'static str] {
3201 SimplePack::ENTITY_KINDS
3202 }
3203 fn handlers(&self) -> &'static [HandlerDef] {
3204 SimplePack::HANDLERS
3205 }
3206 async fn dispatch(
3207 &self,
3208 verb: &str,
3209 _params: Value,
3210 _registry: &VerbRegistry,
3211 _token: &NamespaceToken,
3212 ) -> Result<Value, RuntimeError> {
3213 Ok(serde_json::json!({ "verb": verb }))
3214 }
3215 }
3216
3217 #[derive(Default)]
3219 struct CountingHook {
3220 calls: AtomicUsize,
3221 last_verb: StdMutex<String>,
3222 }
3223
3224 #[async_trait]
3225 impl DispatchHook for CountingHook {
3226 async fn on_dispatch(&self, view: &EventView) {
3227 self.calls.fetch_add(1, Ordering::SeqCst);
3228 *self.last_verb.lock().unwrap() = view.event.verb.clone();
3229 }
3230 }
3231
3232 #[tokio::test]
3233 async fn dispatch_hook_fires_on_successful_dispatch() {
3234 let hook = Arc::new(CountingHook::default());
3235 let mut builder = VerbRegistryBuilder::new();
3236 builder.register(SimplePack);
3237 builder.with_dispatch_hook(hook.clone());
3238 let reg = builder.build().expect("registry builds");
3239
3240 reg.dispatch("ping", Value::Null).await.unwrap();
3241
3242 assert_eq!(
3243 hook.calls.load(Ordering::SeqCst),
3244 1,
3245 "hook must fire once per successful dispatch"
3246 );
3247 assert_eq!(
3248 hook.last_verb.lock().unwrap().as_str(),
3249 "ping",
3250 "hook event must carry the dispatched verb"
3251 );
3252 }
3253
3254 #[tokio::test]
3255 async fn dispatch_hook_fires_multiple_times() {
3256 let hook = Arc::new(CountingHook::default());
3257 let mut builder = VerbRegistryBuilder::new();
3258 builder.register(SimplePack);
3259 builder.with_dispatch_hook(hook.clone());
3260 let reg = builder.build().expect("registry builds");
3261
3262 reg.dispatch("ping", Value::Null).await.unwrap();
3263 reg.dispatch("ping", Value::Null).await.unwrap();
3264 reg.dispatch("ping", Value::Null).await.unwrap();
3265
3266 assert_eq!(
3267 hook.calls.load(Ordering::SeqCst),
3268 3,
3269 "hook must fire once per successful dispatch"
3270 );
3271 }
3272
3273 #[tokio::test]
3274 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
3275 let hook = Arc::new(CountingHook::default());
3276 let mut builder = VerbRegistryBuilder::new();
3277 builder.register(SimplePack);
3278 builder.with_dispatch_hook(hook.clone());
3279 let reg = builder.build().expect("registry builds");
3280
3281 let _ = reg.dispatch("nonexistent", Value::Null).await;
3282
3283 assert_eq!(
3284 hook.calls.load(Ordering::SeqCst),
3285 0,
3286 "hook must NOT fire for unknown verb (dispatch returns error)"
3287 );
3288 }
3289
3290 #[tokio::test]
3291 async fn dispatch_hook_does_not_fire_on_gate_deny() {
3292 use khive_gate::{Gate, GateDecision, GateError};
3293
3294 #[derive(Debug)]
3295 struct AlwaysDenyGate;
3296 impl Gate for AlwaysDenyGate {
3297 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3298 Ok(GateDecision::deny("test deny"))
3299 }
3300 }
3301
3302 let hook = Arc::new(CountingHook::default());
3303 let mut builder = VerbRegistryBuilder::new();
3304 builder.register(SimplePack);
3305 builder.with_gate(Arc::new(AlwaysDenyGate));
3306 builder.with_dispatch_hook(hook.clone());
3307 let reg = builder.build().expect("registry builds");
3308
3309 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
3310 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
3311
3312 assert_eq!(
3313 hook.calls.load(Ordering::SeqCst),
3314 0,
3315 "hook must NOT fire when gate denies dispatch"
3316 );
3317 }
3318
3319 #[tokio::test]
3320 async fn dispatch_hook_event_carries_namespace_from_params() {
3321 let hook = Arc::new(CountingHook::default());
3322
3323 #[derive(Default)]
3324 struct NsCapturingHook {
3325 ns: StdMutex<String>,
3326 }
3327
3328 #[async_trait]
3329 impl DispatchHook for NsCapturingHook {
3330 async fn on_dispatch(&self, view: &EventView) {
3331 *self.ns.lock().unwrap() = view.event.namespace.clone();
3332 }
3333 }
3334
3335 let ns_hook = Arc::new(NsCapturingHook::default());
3336 let mut builder = VerbRegistryBuilder::new();
3337 builder.register(SimplePack);
3338 builder.with_dispatch_hook(ns_hook.clone());
3339 let reg = builder.build().expect("registry builds");
3340
3341 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
3342 .await
3343 .unwrap();
3344
3345 assert_eq!(
3346 ns_hook.ns.lock().unwrap().as_str(),
3347 "tenant-abc",
3348 "dispatch hook event must carry the resolved namespace"
3349 );
3350
3351 drop(hook);
3353 }
3354
3355 #[tokio::test]
3356 async fn no_dispatch_hook_configured_dispatch_succeeds() {
3357 let mut builder = VerbRegistryBuilder::new();
3359 builder.register(SimplePack);
3360 let reg = builder.build().expect("registry builds");
3362
3363 let res = reg.dispatch("ping", Value::Null).await.unwrap();
3364 assert_eq!(res["verb"], "ping");
3365 }
3366}
3367
3368#[cfg(test)]
3371mod help_tests {
3372 use super::*;
3373 use async_trait::async_trait;
3374 use khive_types::Pack;
3375 use std::sync::{
3376 atomic::{AtomicUsize, Ordering},
3377 Arc,
3378 };
3379
3380 static CREATE_PARAMS: [ParamDef; 2] = [
3385 ParamDef {
3386 name: "kind",
3387 param_type: "string",
3388 required: true,
3389 description: "Granular kind (concept | document | ...).",
3390 },
3391 ParamDef {
3392 name: "name",
3393 param_type: "string",
3394 required: false,
3395 description: "Human-readable name.",
3396 },
3397 ];
3398
3399 static RECALL_PARAMS: [ParamDef; 2] = [
3400 ParamDef {
3401 name: "query",
3402 param_type: "string",
3403 required: true,
3404 description: "Semantic recall query.",
3405 },
3406 ParamDef {
3407 name: "limit",
3408 param_type: "integer",
3409 required: false,
3410 description: "Maximum memories to return.",
3411 },
3412 ];
3413
3414 static EMBED_PARAMS: [ParamDef; 0] = [];
3417
3418 struct HelpPack {
3419 invocations: Arc<AtomicUsize>,
3420 }
3421
3422 impl Pack for HelpPack {
3423 const NAME: &'static str = "helptest";
3424 const NOTE_KINDS: &'static [&'static str] = &[];
3425 const ENTITY_KINDS: &'static [&'static str] = &[];
3426 const HANDLERS: &'static [HandlerDef] = &[
3427 HandlerDef {
3428 name: "create",
3429 description: "Create an entity or note",
3430 visibility: Visibility::Verb,
3431 category: VerbCategory::Commissive,
3432 params: &CREATE_PARAMS,
3433 },
3434 HandlerDef {
3435 name: "recall",
3436 description: "Recall memory notes with decay-aware hybrid ranking",
3437 visibility: Visibility::Verb,
3438 category: VerbCategory::Assertive,
3439 params: &RECALL_PARAMS,
3440 },
3441 HandlerDef {
3444 name: "recall.embed",
3445 description: "Return the embedding vector used by memory recall",
3446 visibility: Visibility::Subhandler,
3447 category: VerbCategory::Assertive,
3448 params: &EMBED_PARAMS,
3449 },
3450 ];
3451 }
3452
3453 #[async_trait]
3454 impl PackRuntime for HelpPack {
3455 fn name(&self) -> &str {
3456 HelpPack::NAME
3457 }
3458 fn note_kinds(&self) -> &'static [&'static str] {
3459 HelpPack::NOTE_KINDS
3460 }
3461 fn entity_kinds(&self) -> &'static [&'static str] {
3462 HelpPack::ENTITY_KINDS
3463 }
3464 fn handlers(&self) -> &'static [HandlerDef] {
3465 HelpPack::HANDLERS
3466 }
3467 async fn dispatch(
3468 &self,
3469 verb: &str,
3470 _params: Value,
3471 _registry: &VerbRegistry,
3472 _token: &NamespaceToken,
3473 ) -> Result<Value, RuntimeError> {
3474 self.invocations.fetch_add(1, Ordering::SeqCst);
3475 Ok(serde_json::json!({ "pack": "helptest", "verb": verb }))
3476 }
3477 }
3478
3479 fn build_help_registry(invocations: Arc<AtomicUsize>) -> VerbRegistry {
3480 let mut builder = VerbRegistryBuilder::new();
3481 builder.register(HelpPack { invocations });
3482 builder.build().expect("help registry builds")
3483 }
3484
3485 #[tokio::test]
3488 async fn test_help_true_returns_schema_for_kg_create() {
3489 let invocations = Arc::new(AtomicUsize::new(0));
3490 let reg = build_help_registry(invocations.clone());
3491
3492 let result = reg
3493 .dispatch("create", serde_json::json!({ "help": true }))
3494 .await
3495 .expect("help=true must succeed for a known verb");
3496
3497 assert_eq!(result["verb"], "create", "envelope must name the verb");
3499 assert_eq!(
3500 result["pack"], "helptest",
3501 "envelope must name the owning pack"
3502 );
3503 assert!(
3504 result["description"].as_str().is_some(),
3505 "description must be a string"
3506 );
3507
3508 let params = result["params"]
3510 .as_array()
3511 .expect("params must be a JSON array");
3512 assert!(!params.is_empty(), "params array must not be empty");
3513
3514 let kind_param = params.iter().find(|p| p["name"] == "kind");
3516 assert!(
3517 kind_param.is_some(),
3518 "params array must include the 'kind' parameter"
3519 );
3520 let kind_param = kind_param.unwrap();
3521 assert_eq!(
3522 kind_param["required"],
3523 serde_json::json!(true),
3524 "'kind' must be required"
3525 );
3526 assert_eq!(kind_param["type"], "string", "'kind' type must be 'string'");
3527 }
3528
3529 #[tokio::test]
3531 async fn test_help_true_returns_schema_for_recall() {
3532 let invocations = Arc::new(AtomicUsize::new(0));
3533 let reg = build_help_registry(invocations.clone());
3534
3535 let result = reg
3536 .dispatch("recall", serde_json::json!({ "help": true }))
3537 .await
3538 .expect("help=true must succeed for recall");
3539
3540 assert_eq!(result["verb"], "recall");
3541 assert_eq!(result["pack"], "helptest");
3542
3543 let params = result["params"]
3544 .as_array()
3545 .expect("params must be a JSON array");
3546
3547 let query_param = params.iter().find(|p| p["name"] == "query");
3549 assert!(query_param.is_some(), "params must include 'query'");
3550 let query_param = query_param.unwrap();
3551 assert_eq!(
3552 query_param["required"],
3553 serde_json::json!(true),
3554 "'query' must be required"
3555 );
3556
3557 let limit_param = params.iter().find(|p| p["name"] == "limit");
3559 assert!(limit_param.is_some(), "params must include 'limit'");
3560 let limit_param = limit_param.unwrap();
3561 assert_eq!(
3562 limit_param["required"],
3563 serde_json::json!(false),
3564 "'limit' must be optional"
3565 );
3566 }
3567
3568 #[tokio::test]
3571 async fn test_help_true_does_not_execute_the_verb() {
3572 let invocations = Arc::new(AtomicUsize::new(0));
3573 let reg = build_help_registry(invocations.clone());
3574
3575 reg.dispatch("create", serde_json::json!({ "help": true }))
3577 .await
3578 .expect("help=true must succeed");
3579 reg.dispatch("recall", serde_json::json!({ "help": true }))
3580 .await
3581 .expect("help=true must succeed");
3582
3583 assert_eq!(
3584 invocations.load(Ordering::SeqCst),
3585 0,
3586 "pack dispatch MUST NOT be invoked when help=true; \
3587 got {} invocation(s)",
3588 invocations.load(Ordering::SeqCst)
3589 );
3590
3591 reg.dispatch("create", serde_json::json!({}))
3593 .await
3594 .expect("normal dispatch must succeed");
3595 assert_eq!(
3596 invocations.load(Ordering::SeqCst),
3597 1,
3598 "pack dispatch must fire exactly once for a normal call"
3599 );
3600 }
3601
3602 #[tokio::test]
3611 async fn help_true_on_subhandler_returns_callable_via_mcp_false() {
3612 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3613
3614 let result = reg
3615 .dispatch("recall.embed", serde_json::json!({ "help": true }))
3616 .await
3617 .expect("help=true on subhandler must succeed (no permission check on help path)");
3618
3619 assert_eq!(
3620 result["callable_via_mcp"],
3621 serde_json::json!(false),
3622 "subhandler help must carry callable_via_mcp: false"
3623 );
3624 assert_eq!(
3625 result["visibility"], "internal",
3626 "subhandler help must carry visibility: internal"
3627 );
3628 assert_eq!(result["verb"], "recall.embed");
3631 assert_eq!(result["pack"], "helptest");
3632 }
3633
3634 #[tokio::test]
3636 async fn help_true_on_public_verb_does_not_have_callable_via_mcp_false() {
3637 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3638
3639 let result = reg
3640 .dispatch("create", serde_json::json!({ "help": true }))
3641 .await
3642 .expect("help=true on public verb must succeed");
3643
3644 assert_ne!(
3646 result.get("callable_via_mcp"),
3647 Some(&serde_json::json!(false)),
3648 "public verb help must NOT carry callable_via_mcp: false"
3649 );
3650 assert_ne!(
3652 result.get("visibility"),
3653 Some(&serde_json::json!("internal")),
3654 "public verb help must NOT carry visibility: internal"
3655 );
3656 }
3657
3658 #[tokio::test]
3660 async fn help_true_on_unknown_verb_returns_error() {
3661 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3662
3663 let err = reg
3664 .dispatch("nonexistent_verb", serde_json::json!({ "help": true }))
3665 .await
3666 .unwrap_err();
3667
3668 assert!(
3669 matches!(err, RuntimeError::InvalidInput(_)),
3670 "help=true on unknown verb must return InvalidInput, got {err:?}"
3671 );
3672 let msg = err.to_string();
3673 assert!(
3674 msg.contains("nonexistent_verb"),
3675 "error must name the unknown verb: {msg}"
3676 );
3677 }
3678
3679 #[tokio::test]
3681 async fn help_true_on_subhandler_includes_params_field() {
3682 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3683
3684 let result = reg
3685 .dispatch("recall.embed", serde_json::json!({ "help": true }))
3686 .await
3687 .expect("help=true on subhandler must succeed");
3688
3689 let params = result
3691 .get("params")
3692 .expect("subhandler help must include 'params' field");
3693 assert!(
3694 params.is_array(),
3695 "subhandler help params must be a JSON array"
3696 );
3697 }
3698
3699 #[tokio::test]
3705 async fn help_true_unknown_verb_available_list_excludes_subhandlers() {
3706 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3707
3708 let err = reg
3709 .dispatch("not_a_verb", serde_json::json!({ "help": true }))
3710 .await
3711 .unwrap_err();
3712
3713 let msg = err.to_string();
3714 assert!(
3717 !msg.contains("recall.embed"),
3718 "unknown-verb help error must not advertise subhandler recall.embed: {msg}"
3719 );
3720 assert!(
3722 msg.contains("create"),
3723 "unknown-verb help error must still list public verb 'create': {msg}"
3724 );
3725 assert!(
3726 msg.contains("recall"),
3727 "unknown-verb help error must still list public verb 'recall': {msg}"
3728 );
3729 }
3730
3731 #[tokio::test]
3733 async fn dispatch_unknown_verb_available_list_excludes_subhandlers() {
3734 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3735
3736 let err = reg
3737 .dispatch("not_a_verb", serde_json::json!({}))
3738 .await
3739 .unwrap_err();
3740
3741 let msg = err.to_string();
3742 assert!(
3745 !msg.contains("recall.embed"),
3746 "dispatch unknown-verb error must not advertise subhandler recall.embed: {msg}"
3747 );
3748 assert!(
3750 msg.contains("create"),
3751 "dispatch unknown-verb error must still list public verb 'create': {msg}"
3752 );
3753 assert!(
3754 msg.contains("recall"),
3755 "dispatch unknown-verb error must still list public verb 'recall': {msg}"
3756 );
3757 }
3758}