1use std::collections::{HashMap, HashSet, VecDeque};
15use std::sync::Arc;
16use std::time::Instant;
17
18use crate::runtime::NamespaceToken;
19use async_trait::async_trait;
20use khive_gate::{ActorRef, AllowAllGate, AuditEvent, GateDecision, GateRef, GateRequest};
21use khive_storage::{Event, EventStore, EventView, SubstrateKind};
22use khive_types::{EventKind, EventOutcome, Namespace};
23use serde_json::Value;
24
25pub use khive_types::{
26 EdgeEndpointRule, EndpointKind, HandlerDef, NoteKindSpec, NoteLifecycleSpec, PackSchemaPlan,
27 ParamDef, VerbCategory, VerbPresentationPolicy, Visibility,
28};
29#[allow(deprecated)]
31pub use khive_types::VerbDef;
32
33use crate::validation::ValidationRule;
34
35#[derive(Debug, Default, Clone)]
45pub struct SchemaPlan {
46 pub pack: &'static str,
48 pub statements: &'static [&'static str],
52}
53
54impl SchemaPlan {
55 pub const fn empty() -> Self {
60 Self {
61 pack: "",
62 statements: &[],
63 }
64 }
65
66 pub fn is_empty(&self) -> bool {
68 self.statements.is_empty()
69 }
70}
71
72#[async_trait]
77pub trait DispatchHook: Send + Sync {
78 async fn on_dispatch(&self, view: &EventView);
83}
84
85use crate::error::{
86 CircularPackDependency, MissingPackDependencies, MissingPackDependency, RuntimeError,
87};
88use crate::KhiveRuntime;
89
90#[async_trait]
99pub trait PackRuntime: Send + Sync {
100 fn name(&self) -> &str;
102
103 fn note_kinds(&self) -> &'static [&'static str];
105
106 fn entity_kinds(&self) -> &'static [&'static str];
108
109 fn handlers(&self) -> &'static [HandlerDef];
111
112 fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
116 &[]
117 }
118
119 fn requires(&self) -> &'static [&'static str] {
122 &[]
123 }
124
125 fn note_kind_specs(&self) -> &'static [NoteKindSpec] {
132 &[]
133 }
134
135 fn kind_hook(&self, _kind: &str) -> Option<Arc<dyn KindHook>> {
143 None
144 }
145
146 fn schema_plan(&self) -> SchemaPlan {
162 SchemaPlan::empty()
163 }
164
165 fn validation_rules(&self) -> &'static [ValidationRule] {
173 &[]
174 }
175
176 fn register_embedders(&self, _runtime: &KhiveRuntime) {}
193
194 async fn warm(&self) {}
205
206 async fn dispatch(
213 &self,
214 verb: &str,
215 params: Value,
216 registry: &VerbRegistry,
217 token: &NamespaceToken,
218 ) -> Result<Value, RuntimeError>;
219}
220
221#[async_trait]
235pub trait KindHook: Send + Sync + std::fmt::Debug {
236 async fn prepare_create(
242 &self,
243 runtime: &KhiveRuntime,
244 args: &mut Value,
245 ) -> Result<(), RuntimeError>;
246
247 async fn after_create(
256 &self,
257 runtime: &KhiveRuntime,
258 id: uuid::Uuid,
259 args: &Value,
260 ) -> Result<(), RuntimeError>;
261}
262
263pub struct VerbRegistryBuilder {
268 packs: Vec<Box<dyn PackRuntime>>,
269 gate: GateRef,
270 default_namespace: String,
271 event_store: Option<Arc<dyn EventStore>>,
278 dispatch_hook: Option<Arc<dyn DispatchHook>>,
284}
285
286impl VerbRegistryBuilder {
287 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> {
614 for pack in self.packs.iter() {
615 for handler in pack.handlers().iter() {
616 if handler.name == verb {
617 let category = format!("{:?}", handler.category);
618 let params_arr: Vec<Value> = handler
619 .params
620 .iter()
621 .map(|p| {
622 serde_json::json!({
623 "name": p.name,
624 "type": p.param_type,
625 "required": p.required,
626 "description": p.description,
627 })
628 })
629 .collect();
630 if matches!(handler.visibility, Visibility::Subhandler) {
635 return Ok(serde_json::json!({
636 "verb": verb,
637 "pack": pack.name(),
638 "description": handler.description,
639 "category": category,
640 "params": params_arr,
641 "visibility": "internal",
642 "callable_via_mcp": false,
643 "note": "This is an internal subhandler. Calling it via the MCP \
644 request surface returns permission denied. It can only be \
645 invoked by internal runtime callers.",
646 }));
647 }
648 return Ok(serde_json::json!({
649 "verb": verb,
650 "pack": pack.name(),
651 "description": handler.description,
652 "category": category,
653 "params": params_arr,
654 }));
655 }
656 }
657 }
658 let available: Vec<&str> = self
661 .packs
662 .iter()
663 .flat_map(|p| p.handlers().iter())
664 .filter(|h| matches!(h.visibility, Visibility::Verb))
665 .map(|h| h.name)
666 .collect();
667 Err(RuntimeError::InvalidInput(format!(
668 "unknown verb {verb:?}; available: {}",
669 available.join(", ")
670 )))
671 }
672
673 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
710 if params.get("help").and_then(Value::as_bool) == Some(true) {
712 return self.describe_verb(verb);
713 }
714 let ns_str: String = params
717 .get("namespace")
718 .and_then(Value::as_str)
719 .map(str::to_string)
720 .unwrap_or_else(|| self.default_namespace.clone());
721 let ns = Namespace::parse(&ns_str)
722 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?;
723 let gate_req = GateRequest::new(ActorRef::anonymous(), ns, verb, params.clone());
724
725 let gate_blocked = match self.gate.check(&gate_req) {
731 Ok(decision) => {
732 let is_deny = matches!(decision, GateDecision::Deny { .. });
733
734 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
736 tracing::info!(
737 audit_event = %serde_json::to_string(&audit)
738 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
739 "gate.check"
740 );
741
742 if let Some(store) = &self.event_store {
744 let outcome = if is_deny {
745 EventOutcome::Denied
746 } else {
747 EventOutcome::Success
748 };
749 let audit_data = serde_json::to_value(&audit).unwrap_or_else(|e| {
750 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
751 serde_json::Value::Null
752 });
753 let mut storage_event = Event::new(
754 gate_req.namespace.as_str(),
755 verb,
756 EventKind::Audit,
757 SubstrateKind::Event,
758 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
759 )
760 .with_outcome(outcome)
761 .with_payload(audit_data);
762 if let Some(target_id) = target_id_from_args(&gate_req.args) {
763 storage_event = storage_event.with_target(target_id);
764 }
765 if let Err(store_err) = store.append_event(storage_event).await {
766 tracing::warn!(
767 verb,
768 error = %store_err,
769 "audit event store write failed (non-fatal)"
770 );
771 }
772 }
773
774 if is_deny {
775 let reason = match decision {
776 GateDecision::Deny { reason } => reason,
777 _ => String::new(),
778 };
779 Some(reason)
780 } else {
781 None
782 }
783 }
784 Err(err) => {
785 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
788 None
789 }
790 };
791
792 if let Some(reason) = gate_blocked {
794 return Err(RuntimeError::PermissionDenied {
795 verb: verb.to_string(),
796 reason,
797 });
798 }
799
800 let token = NamespaceToken::mint_authorized(
803 Namespace::parse(&ns_str)
804 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?,
805 ActorRef::anonymous(),
806 );
807
808 for pack in self.packs.iter() {
809 if let Some(handler_def) = pack.handlers().iter().find(|v| v.name == verb) {
810 let handler_accepts_namespace =
820 handler_def.params.iter().any(|p| p.name == "namespace");
821 let params = if !handler_accepts_namespace {
822 if let Value::Object(mut map) = params {
823 map.remove("namespace");
824 Value::Object(map)
825 } else {
826 params
827 }
828 } else {
829 params
830 };
831 let dispatch_start = Instant::now();
832 let result = pack.dispatch(verb, params, self, &token).await;
833 let dispatch_us = dispatch_start.elapsed().as_micros() as i64;
834
835 if let (Ok(ref ok_val), Some(hook)) = (&result, &self.dispatch_hook) {
837 let mut dispatch_event = Event::new(
838 ns_str.as_str(),
839 verb,
840 EventKind::Audit,
841 SubstrateKind::Event,
842 pack.name(),
843 )
844 .with_outcome(EventOutcome::Success)
845 .with_duration_us(dispatch_us);
846
847 if verb == "memory.recall" {
851 let first_note_id = ok_val
852 .as_array()
853 .and_then(|arr| arr.first())
854 .and_then(|v| v.get("note_id"))
855 .and_then(|v| v.as_str())
856 .and_then(|s| s.parse::<uuid::Uuid>().ok());
857 if let Some(note_id) = first_note_id {
858 dispatch_event = dispatch_event.with_target(note_id);
859 }
860 }
863
864 let dispatch_view = EventView {
865 event: dispatch_event,
866 observations: Vec::new(),
867 };
868 let hook = Arc::clone(hook);
869 hook.on_dispatch(&dispatch_view).await;
870 }
871
872 return result;
873 }
874 }
875 let available: Vec<&str> = self
878 .packs
879 .iter()
880 .flat_map(|p| p.handlers().iter())
881 .filter(|h| matches!(h.visibility, Visibility::Verb))
882 .map(|h| h.name)
883 .collect();
884 Err(RuntimeError::InvalidInput(format!(
885 "unknown verb {verb:?}; available: {}",
886 available.join(", ")
887 )))
888 }
889
890 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
897 for pack in self.packs.iter() {
898 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
899 if owns {
900 if let Some(hook) = pack.kind_hook(kind) {
901 return Some(hook);
902 }
903 }
904 }
905 None
906 }
907
908 pub fn all_verbs(&self) -> Vec<&'static HandlerDef> {
915 self.packs
916 .iter()
917 .flat_map(|p| p.handlers().iter())
918 .filter(|h| matches!(h.visibility, Visibility::Verb))
919 .collect()
920 }
921
922 pub fn all_verbs_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
929 self.packs
930 .iter()
931 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
932 .filter(|(_, h)| matches!(h.visibility, Visibility::Verb))
933 .collect()
934 }
935
936 pub fn all_handlers_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
942 self.packs
943 .iter()
944 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
945 .collect()
946 }
947
948 pub fn all_note_kinds(&self) -> Vec<&'static str> {
951 let mut seen = std::collections::HashSet::new();
952 self.packs
953 .iter()
954 .flat_map(|p| p.note_kinds().iter().copied())
955 .filter(|k| seen.insert(*k))
956 .collect()
957 }
958
959 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
962 let mut seen = std::collections::HashSet::new();
963 self.packs
964 .iter()
965 .flat_map(|p| p.entity_kinds().iter().copied())
966 .filter(|k| seen.insert(*k))
967 .collect()
968 }
969
970 pub fn pack_names(&self) -> Vec<&str> {
972 self.packs.iter().map(|p| p.name()).collect()
973 }
974
975 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
977 self.packs
978 .iter()
979 .find(|p| p.name() == name)
980 .map(|p| p.requires())
981 }
982
983 pub fn pack_note_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
988 self.packs
989 .iter()
990 .find(|p| p.name() == name)
991 .map(|p| p.note_kinds())
992 }
993
994 pub fn pack_entity_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
999 self.packs
1000 .iter()
1001 .find(|p| p.name() == name)
1002 .map(|p| p.entity_kinds())
1003 }
1004
1005 pub fn pack_verbs(&self, name: &str) -> Option<&'static [HandlerDef]> {
1011 self.packs
1012 .iter()
1013 .find(|p| p.name() == name)
1014 .map(|p| p.handlers())
1015 }
1016
1017 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
1023 self.packs
1024 .iter()
1025 .flat_map(|p| p.edge_rules().iter().copied())
1026 .collect()
1027 }
1028
1029 pub fn all_note_kind_specs(&self) -> Vec<&'static NoteKindSpec> {
1033 self.packs
1034 .iter()
1035 .flat_map(|p| p.note_kind_specs().iter())
1036 .collect()
1037 }
1038
1039 pub fn all_validation_rules(&self) -> Vec<&'static ValidationRule> {
1045 self.packs
1046 .iter()
1047 .flat_map(|p| p.validation_rules().iter())
1048 .collect()
1049 }
1050
1051 pub fn all_schema_plans(&self) -> Vec<SchemaPlan> {
1058 self.packs.iter().map(|p| p.schema_plan()).collect()
1059 }
1060
1061 pub fn call_register_embedders(&self, runtime: &KhiveRuntime) {
1072 for pack in self.packs.iter() {
1073 pack.register_embedders(runtime);
1074 }
1075 }
1076
1077 pub async fn call_warm_all(&self) {
1081 for pack in self.packs.iter() {
1082 pack.warm().await;
1083 }
1084 }
1085
1086 pub fn presentation_policy_for(&self, verb: &str) -> khive_types::VerbPresentationPolicy {
1093 for pack in self.packs.iter() {
1094 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1095 return handler.presentation_policy();
1096 }
1097 }
1098 khive_types::VerbPresentationPolicy::Standard
1099 }
1100
1101 pub fn is_subhandler_verb(&self, verb: &str) -> bool {
1108 for pack in self.packs.iter() {
1109 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1110 return matches!(handler.visibility, Visibility::Subhandler);
1111 }
1112 }
1113 false
1114 }
1115
1116 pub fn apply_schema_plans(&self, backend: &khive_db::StorageBackend) {
1129 for plan in self.all_schema_plans() {
1130 if plan.is_empty() {
1131 continue;
1132 }
1133 if let Err(e) = backend.apply_pack_ddl_statements(plan.statements) {
1134 tracing::warn!(
1135 pack = plan.pack,
1136 error = %e,
1137 "failed to apply pack schema plan at startup (non-fatal)"
1138 );
1139 }
1140 }
1141 }
1142}
1143
1144pub trait PackFactory: Send + Sync + 'static {
1154 fn name(&self) -> &'static str;
1156
1157 fn requires(&self) -> &'static [&'static str] {
1164 &[]
1165 }
1166
1167 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime>;
1169}
1170
1171pub struct PackRegistration(pub &'static dyn PackFactory);
1176
1177inventory::collect!(PackRegistration);
1178
1179#[derive(Debug)]
1181pub enum PackLoadError {
1182 UnknownPack(String),
1184 MissingDependency {
1186 pack: String,
1188 dep: String,
1190 },
1191}
1192
1193impl std::fmt::Display for PackLoadError {
1194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195 match self {
1196 PackLoadError::UnknownPack(name) => write!(f, "unknown pack {name:?}"),
1197 PackLoadError::MissingDependency { pack, dep } => write!(
1198 f,
1199 "pack {pack:?} requires {dep:?}, which is not in the requested pack list; \
1200 add --pack {dep} before --pack {pack}"
1201 ),
1202 }
1203 }
1204}
1205
1206impl std::error::Error for PackLoadError {}
1207
1208pub struct PackRegistry;
1213
1214impl PackRegistry {
1215 pub fn discovered_names() -> Vec<&'static str> {
1217 inventory::iter::<PackRegistration>
1218 .into_iter()
1219 .map(|r| r.0.name())
1220 .collect()
1221 }
1222
1223 pub fn register_packs(
1236 names: &[String],
1237 runtime: KhiveRuntime,
1238 builder: &mut VerbRegistryBuilder,
1239 ) -> Result<(), PackLoadError> {
1240 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
1242 .into_iter()
1243 .map(|r| r.0)
1244 .collect();
1245 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
1246 all.iter().copied().find(|f| f.name() == name)
1247 };
1248
1249 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
1251 for name in names {
1252 factory_for(name.as_str()).ok_or_else(|| PackLoadError::UnknownPack(name.clone()))?;
1253 }
1254
1255 for name in names {
1258 let factory = factory_for(name.as_str()).unwrap(); for &dep in factory.requires() {
1260 if !requested.contains(dep) {
1261 return Err(PackLoadError::MissingDependency {
1262 pack: name.clone(),
1263 dep: dep.to_string(),
1264 });
1265 }
1266 }
1267 }
1268
1269 for name in names {
1272 let factory = factory_for(name.as_str()).unwrap(); builder.register_boxed(factory.create(runtime.clone()));
1274 }
1275
1276 Ok(())
1277 }
1278}
1279
1280fn target_id_from_args(args: &serde_json::Value) -> Option<uuid::Uuid> {
1281 args.get("target_id")
1282 .and_then(serde_json::Value::as_str)
1283 .and_then(|s| s.parse::<uuid::Uuid>().ok())
1284}
1285
1286#[cfg(test)]
1287mod tests {
1288 use super::*;
1289 use khive_types::Pack;
1290
1291 struct AlphaPack;
1292
1293 impl Pack for AlphaPack {
1294 const NAME: &'static str = "alpha";
1295 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
1296 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
1297 const HANDLERS: &'static [HandlerDef] = &[
1298 HandlerDef {
1299 name: "create",
1300 description: "create a widget",
1301 visibility: Visibility::Verb,
1302 category: VerbCategory::Commissive,
1303 params: &[],
1304 },
1305 HandlerDef {
1306 name: "list",
1307 description: "list widgets",
1308 visibility: Visibility::Verb,
1309 category: VerbCategory::Assertive,
1310 params: &[],
1311 },
1312 ];
1313 }
1314
1315 #[async_trait]
1316 impl PackRuntime for AlphaPack {
1317 fn name(&self) -> &str {
1318 AlphaPack::NAME
1319 }
1320 fn note_kinds(&self) -> &'static [&'static str] {
1321 AlphaPack::NOTE_KINDS
1322 }
1323 fn entity_kinds(&self) -> &'static [&'static str] {
1324 AlphaPack::ENTITY_KINDS
1325 }
1326 fn handlers(&self) -> &'static [HandlerDef] {
1327 AlphaPack::HANDLERS
1328 }
1329 async fn dispatch(
1330 &self,
1331 verb: &str,
1332 _params: Value,
1333 _registry: &VerbRegistry,
1334 _token: &NamespaceToken,
1335 ) -> Result<Value, RuntimeError> {
1336 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
1337 }
1338 }
1339
1340 struct BetaPack;
1341
1342 impl Pack for BetaPack {
1343 const NAME: &'static str = "beta";
1344 const NOTE_KINDS: &'static [&'static str] = &["alert"];
1345 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
1346 const HANDLERS: &'static [HandlerDef] = &[
1347 HandlerDef {
1348 name: "notify",
1349 description: "send alert",
1350 visibility: Visibility::Verb,
1351 category: VerbCategory::Commissive,
1352 params: &[],
1353 },
1354 HandlerDef {
1358 name: "create",
1359 description: "beta internal create (subhandler)",
1360 visibility: Visibility::Subhandler,
1361 category: VerbCategory::Commissive,
1362 params: &[],
1363 },
1364 ];
1365 }
1366
1367 fn build_registry() -> VerbRegistry {
1373 let mut builder = VerbRegistryBuilder::new();
1374 builder.register(AlphaPack);
1375 builder.register(BetaPack);
1376 builder.build().expect("registry builds without collision")
1377 }
1378
1379 struct CollidingPack;
1382
1383 impl Pack for CollidingPack {
1384 const NAME: &'static str = "colliding";
1385 const NOTE_KINDS: &'static [&'static str] = &[];
1386 const ENTITY_KINDS: &'static [&'static str] = &[];
1387 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1388 name: "create",
1389 description: "duplicate Verb-visibility create",
1390 visibility: Visibility::Verb,
1391 category: VerbCategory::Commissive,
1392 params: &[],
1393 }];
1394 }
1395
1396 #[async_trait]
1397 impl PackRuntime for CollidingPack {
1398 fn name(&self) -> &str {
1399 Self::NAME
1400 }
1401 fn note_kinds(&self) -> &'static [&'static str] {
1402 Self::NOTE_KINDS
1403 }
1404 fn entity_kinds(&self) -> &'static [&'static str] {
1405 Self::ENTITY_KINDS
1406 }
1407 fn handlers(&self) -> &'static [HandlerDef] {
1408 Self::HANDLERS
1409 }
1410 async fn dispatch(
1411 &self,
1412 verb: &str,
1413 _params: Value,
1414 _registry: &VerbRegistry,
1415 _token: &NamespaceToken,
1416 ) -> Result<Value, RuntimeError> {
1417 Ok(serde_json::json!({ "pack": "colliding", "verb": verb }))
1418 }
1419 }
1420
1421 #[async_trait]
1422 impl PackRuntime for BetaPack {
1423 fn name(&self) -> &str {
1424 BetaPack::NAME
1425 }
1426 fn note_kinds(&self) -> &'static [&'static str] {
1427 BetaPack::NOTE_KINDS
1428 }
1429 fn entity_kinds(&self) -> &'static [&'static str] {
1430 BetaPack::ENTITY_KINDS
1431 }
1432 fn handlers(&self) -> &'static [HandlerDef] {
1433 BetaPack::HANDLERS
1434 }
1435 async fn dispatch(
1436 &self,
1437 verb: &str,
1438 _params: Value,
1439 _registry: &VerbRegistry,
1440 _token: &NamespaceToken,
1441 ) -> Result<Value, RuntimeError> {
1442 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
1443 }
1444 }
1445
1446 #[tokio::test]
1447 async fn dispatch_routes_to_correct_pack() {
1448 let reg = build_registry();
1449
1450 let res = reg.dispatch("list", Value::Null).await.unwrap();
1451 assert_eq!(res["pack"], "alpha");
1452
1453 let res = reg.dispatch("notify", Value::Null).await.unwrap();
1454 assert_eq!(res["pack"], "beta");
1455 }
1456
1457 #[test]
1461 fn verb_collision_is_boot_time_error() {
1462 let mut builder = VerbRegistryBuilder::new();
1463 builder.register(AlphaPack);
1464 builder.register(CollidingPack);
1465 let err = builder
1466 .build()
1467 .err()
1468 .expect("duplicate Verb-visibility handler must be rejected at build time");
1469 assert!(
1470 matches!(err, RuntimeError::VerbCollision { ref verb, .. } if verb == "create"),
1471 "expected VerbCollision for 'create', got {err:?}"
1472 );
1473 let msg = err.to_string();
1474 assert!(
1475 msg.contains("create"),
1476 "error must name the colliding verb: {msg}"
1477 );
1478 assert!(
1479 msg.contains("alpha") || msg.contains("colliding"),
1480 "error must name one of the conflicting packs: {msg}"
1481 );
1482 }
1483
1484 #[test]
1488 fn subhandler_same_name_across_packs_is_not_a_collision() {
1489 struct SubhandlerPack;
1490 impl Pack for SubhandlerPack {
1491 const NAME: &'static str = "subhandler_pack";
1492 const NOTE_KINDS: &'static [&'static str] = &[];
1493 const ENTITY_KINDS: &'static [&'static str] = &[];
1494 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1495 name: "create",
1496 description: "internal create",
1497 visibility: Visibility::Subhandler,
1498 category: VerbCategory::Commissive,
1499 params: &[],
1500 }];
1501 }
1502 #[async_trait]
1503 impl PackRuntime for SubhandlerPack {
1504 fn name(&self) -> &str {
1505 Self::NAME
1506 }
1507 fn note_kinds(&self) -> &'static [&'static str] {
1508 Self::NOTE_KINDS
1509 }
1510 fn entity_kinds(&self) -> &'static [&'static str] {
1511 Self::ENTITY_KINDS
1512 }
1513 fn handlers(&self) -> &'static [HandlerDef] {
1514 Self::HANDLERS
1515 }
1516 async fn dispatch(
1517 &self,
1518 verb: &str,
1519 _: Value,
1520 _: &VerbRegistry,
1521 _: &NamespaceToken,
1522 ) -> Result<Value, RuntimeError> {
1523 Ok(serde_json::json!({"pack": "subhandler_pack", "verb": verb}))
1524 }
1525 }
1526 let mut builder = VerbRegistryBuilder::new();
1527 builder.register(AlphaPack); builder.register(SubhandlerPack); builder
1530 .build()
1531 .expect("subhandler same name must NOT be a collision");
1532 }
1533
1534 #[tokio::test]
1535 async fn dispatch_unknown_verb_returns_error() {
1536 let reg = build_registry();
1537
1538 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
1539 let msg = err.to_string();
1540 assert!(msg.contains("explode"));
1541 assert!(msg.contains("create"));
1542 }
1543
1544 #[test]
1549 fn all_verbs_aggregates_across_packs_excludes_subhandlers() {
1550 let reg = build_registry();
1551 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
1552 assert_eq!(verbs, vec!["create", "list", "notify"]);
1554 }
1555
1556 #[test]
1557 fn all_verbs_with_names_pairs_pack_name_excludes_subhandlers() {
1558 let reg = build_registry();
1559 let pairs: Vec<(&str, &str)> = reg
1560 .all_verbs_with_names()
1561 .iter()
1562 .map(|(pack, v)| (*pack, v.name))
1563 .collect();
1564 assert_eq!(
1566 pairs,
1567 vec![("alpha", "create"), ("alpha", "list"), ("beta", "notify"),]
1568 );
1569 }
1570
1571 #[test]
1572 fn all_handlers_with_names_includes_subhandlers() {
1573 let reg = build_registry();
1574 let pairs: Vec<(&str, &str)> = reg
1575 .all_handlers_with_names()
1576 .iter()
1577 .map(|(pack, v)| (*pack, v.name))
1578 .collect();
1579 assert_eq!(
1581 pairs,
1582 vec![
1583 ("alpha", "create"),
1584 ("alpha", "list"),
1585 ("beta", "notify"),
1586 ("beta", "create"),
1587 ]
1588 );
1589 }
1590
1591 #[test]
1592 fn note_kinds_are_ordered() {
1593 let reg = build_registry();
1594 let kinds = reg.all_note_kinds();
1595 assert_eq!(kinds, vec!["memo", "log", "alert"]);
1596 }
1597
1598 #[test]
1599 fn note_kind_duplicate_rejected_at_build_time() {
1600 struct DupPack;
1601
1602 impl khive_types::Pack for DupPack {
1603 const NAME: &'static str = "dup";
1604 const NOTE_KINDS: &'static [&'static str] = &["memo"];
1606 const ENTITY_KINDS: &'static [&'static str] = &[];
1607 const HANDLERS: &'static [HandlerDef] = &[];
1608 }
1609
1610 #[async_trait]
1611 impl PackRuntime for DupPack {
1612 fn name(&self) -> &str {
1613 Self::NAME
1614 }
1615 fn note_kinds(&self) -> &'static [&'static str] {
1616 Self::NOTE_KINDS
1617 }
1618 fn entity_kinds(&self) -> &'static [&'static str] {
1619 Self::ENTITY_KINDS
1620 }
1621 fn handlers(&self) -> &'static [HandlerDef] {
1622 Self::HANDLERS
1623 }
1624 async fn dispatch(
1625 &self,
1626 _verb: &str,
1627 _params: Value,
1628 _registry: &VerbRegistry,
1629 _token: &NamespaceToken,
1630 ) -> Result<Value, RuntimeError> {
1631 Ok(Value::Null)
1632 }
1633 }
1634
1635 let mut builder = VerbRegistryBuilder::new();
1636 builder.register(AlphaPack);
1637 builder.register(DupPack);
1638 let err = builder
1639 .build()
1640 .err()
1641 .expect("duplicate note kind must be rejected");
1642 let msg = err.to_string();
1643 assert!(
1644 msg.contains("memo"),
1645 "error must name the duplicate kind: {msg}"
1646 );
1647 assert!(
1648 msg.contains("alpha") || msg.contains("dup"),
1649 "error must name one of the conflicting packs: {msg}"
1650 );
1651 }
1652
1653 #[test]
1654 fn entity_kinds_are_deduplicated() {
1655 let reg = build_registry();
1656 let kinds = reg.all_entity_kinds();
1657 assert_eq!(kinds, vec!["widget", "gadget"]);
1658 }
1659
1660 use khive_gate::{Gate, GateError};
1663 use std::sync::atomic::{AtomicUsize, Ordering};
1664 use std::sync::Arc;
1665
1666 #[derive(Default, Debug)]
1667 struct CountingGate {
1668 calls: AtomicUsize,
1669 deny_verb: Option<&'static str>,
1670 }
1671
1672 impl Gate for CountingGate {
1673 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1674 self.calls.fetch_add(1, Ordering::SeqCst);
1675 if Some(req.verb.as_str()) == self.deny_verb {
1676 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
1677 } else {
1678 Ok(GateDecision::allow())
1679 }
1680 }
1681 }
1682
1683 #[tokio::test]
1684 async fn dispatch_consults_the_gate() {
1685 let gate = Arc::new(CountingGate::default());
1686 let mut builder = VerbRegistryBuilder::new();
1687 builder.register(AlphaPack);
1688 builder.with_gate(gate.clone());
1689 let reg = builder.build().expect("registry builds");
1690
1691 reg.dispatch("list", Value::Null).await.unwrap();
1692 reg.dispatch("create", Value::Null).await.unwrap();
1693 assert_eq!(
1694 gate.calls.load(Ordering::SeqCst),
1695 2,
1696 "gate should be consulted once per dispatch"
1697 );
1698 }
1699
1700 #[tokio::test]
1701 async fn dispatch_returns_permission_denied_on_deny_v03() {
1702 let gate = Arc::new(CountingGate {
1703 calls: AtomicUsize::new(0),
1704 deny_verb: Some("create"),
1705 });
1706 let mut builder = VerbRegistryBuilder::new();
1707 builder.register(AlphaPack);
1708 builder.with_gate(gate.clone());
1709 let reg = builder.build().expect("registry builds");
1710
1711 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1713 assert!(
1714 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
1715 "expected PermissionDenied, got {err:?}"
1716 );
1717 let msg = err.to_string();
1718 assert!(
1719 msg.contains("create"),
1720 "error message must name the verb: {msg}"
1721 );
1722 assert!(
1723 msg.contains("test deny for create"),
1724 "error message must carry the deny reason: {msg}"
1725 );
1726 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
1727 }
1728
1729 #[tokio::test]
1730 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
1731 let gate = Arc::new(CountingGate {
1733 calls: AtomicUsize::new(0),
1734 deny_verb: Some("create"),
1735 });
1736 let mut builder = VerbRegistryBuilder::new();
1737 builder.register(AlphaPack);
1738 builder.with_gate(gate.clone());
1739 let reg = builder.build().expect("registry builds");
1740
1741 let res = reg.dispatch("list", Value::Null).await.unwrap();
1742 assert_eq!(res["pack"], "alpha");
1743 }
1744
1745 #[tokio::test]
1746 async fn dispatch_uses_allow_all_gate_by_default() {
1747 let reg = build_registry();
1749 let res = reg.dispatch("list", Value::Null).await.unwrap();
1750 assert_eq!(res["pack"], "alpha");
1751 }
1752
1753 #[derive(Default, Debug)]
1756 struct NamespaceCapturingGate {
1757 seen: std::sync::Mutex<Vec<String>>,
1758 }
1759
1760 impl Gate for NamespaceCapturingGate {
1761 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1762 self.seen
1763 .lock()
1764 .unwrap()
1765 .push(req.namespace.as_str().to_string());
1766 Ok(GateDecision::allow())
1767 }
1768 }
1769
1770 #[tokio::test]
1771 async fn dispatch_propagates_params_namespace_to_gate() {
1772 let gate = Arc::new(NamespaceCapturingGate::default());
1773 let mut builder = VerbRegistryBuilder::new();
1774 builder.register(AlphaPack);
1775 builder.with_gate(gate.clone());
1776 builder.with_default_namespace("tenant-x");
1777 let reg = builder.build().expect("registry builds");
1778
1779 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
1781 .await
1782 .unwrap();
1783 reg.dispatch("list", Value::Null).await.unwrap();
1785 let err = reg
1787 .dispatch("list", serde_json::json!({"namespace": ""}))
1788 .await
1789 .unwrap_err();
1790 assert!(
1791 matches!(err, RuntimeError::InvalidInput(_)),
1792 "empty namespace must return InvalidInput, got {err:?}"
1793 );
1794
1795 let seen = gate.seen.lock().unwrap().clone();
1796 assert_eq!(seen, vec!["tenant-y", "tenant-x"]);
1797 }
1798
1799 #[tokio::test]
1800 async fn dispatch_falls_back_to_local_when_no_default_set() {
1801 let gate = Arc::new(NamespaceCapturingGate::default());
1803 let mut builder = VerbRegistryBuilder::new();
1804 builder.register(AlphaPack);
1805 builder.with_gate(gate.clone());
1806 let reg = builder.build().expect("registry builds");
1807
1808 reg.dispatch("list", Value::Null).await.unwrap();
1809 let seen = gate.seen.lock().unwrap().clone();
1810 assert_eq!(seen, vec!["local"]);
1811 }
1812
1813 use khive_gate::{AuditDecision, AuditEvent, Obligation};
1816
1817 #[derive(Default, Debug)]
1819 struct AuditCapturingGate {
1820 events: std::sync::Mutex<Vec<AuditEvent>>,
1821 deny_verb: Option<&'static str>,
1822 }
1823
1824 impl Gate for AuditCapturingGate {
1825 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1826 let decision = if Some(req.verb.as_str()) == self.deny_verb {
1827 GateDecision::deny("test deny")
1828 } else {
1829 GateDecision::allow_with(vec![Obligation::Audit {
1830 tag: format!("{}.check", req.verb),
1831 }])
1832 };
1833 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
1835 self.events.lock().unwrap().push(ev);
1836 Ok(decision)
1837 }
1838
1839 fn impl_name(&self) -> &'static str {
1840 "AuditCapturingGate"
1841 }
1842 }
1843
1844 #[tokio::test]
1845 async fn dispatch_emits_one_audit_event_per_call() {
1846 let gate = Arc::new(AuditCapturingGate::default());
1847 let mut builder = VerbRegistryBuilder::new();
1848 builder.register(AlphaPack);
1849 builder.with_gate(gate.clone());
1850 let reg = builder.build().expect("registry builds");
1851
1852 reg.dispatch("list", Value::Null).await.unwrap();
1853 reg.dispatch("create", Value::Null).await.unwrap();
1854
1855 let evs = gate.events.lock().unwrap();
1856 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
1857 }
1858
1859 #[tokio::test]
1860 async fn dispatch_audit_event_allow_carries_obligations() {
1861 let gate = Arc::new(AuditCapturingGate::default());
1862 let mut builder = VerbRegistryBuilder::new();
1863 builder.register(AlphaPack);
1864 builder.with_gate(gate.clone());
1865 let reg = builder.build().expect("registry builds");
1866
1867 reg.dispatch("list", Value::Null).await.unwrap();
1868
1869 let evs = gate.events.lock().unwrap();
1870 let ev = &evs[0];
1871 assert_eq!(ev.verb, "list");
1872 assert_eq!(ev.decision, AuditDecision::Allow);
1873 assert!(ev.deny_reason.is_none());
1874 assert_eq!(ev.obligations.len(), 1);
1875 assert_eq!(ev.gate_impl, "AuditCapturingGate");
1876 }
1877
1878 #[tokio::test]
1879 async fn dispatch_audit_event_deny_carries_reason() {
1880 let gate = Arc::new(AuditCapturingGate {
1881 events: Default::default(),
1882 deny_verb: Some("create"),
1883 });
1884 let mut builder = VerbRegistryBuilder::new();
1885 builder.register(AlphaPack);
1886 builder.with_gate(gate.clone());
1887 let reg = builder.build().expect("registry builds");
1888
1889 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1892 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
1893
1894 let evs = gate.events.lock().unwrap();
1895 let ev = &evs[0];
1896 assert_eq!(ev.verb, "create");
1897 assert_eq!(ev.decision, AuditDecision::Deny);
1898 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
1899 assert!(ev.obligations.is_empty());
1900 }
1901
1902 #[tokio::test]
1903 async fn dispatch_audit_event_fields_match_gate_request() {
1904 let gate = Arc::new(AuditCapturingGate::default());
1905 let mut builder = VerbRegistryBuilder::new();
1906 builder.register(AlphaPack);
1907 builder.with_gate(gate.clone());
1908 builder.with_default_namespace("tenant-z");
1909 let reg = builder.build().expect("registry builds");
1910
1911 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1912 .await
1913 .unwrap();
1914
1915 let evs = gate.events.lock().unwrap();
1916 let ev = &evs[0];
1917 assert_eq!(ev.namespace, "tenant-q");
1919 assert_eq!(ev.verb, "list");
1920 assert_eq!(ev.actor.kind, "anonymous");
1921 }
1922
1923 use std::sync::{Mutex as StdMutex, Once, OnceLock};
1936
1937 use serial_test::serial;
1938 use tracing::field::{Field, Visit};
1939
1940 #[derive(Clone, Debug, Default)]
1941 struct CapturedEvent {
1942 message: Option<String>,
1943 audit_event: Option<String>,
1944 }
1945
1946 #[derive(Default)]
1947 struct CapturedEventVisitor(CapturedEvent);
1948
1949 impl Visit for CapturedEventVisitor {
1950 fn record_str(&mut self, field: &Field, value: &str) {
1951 match field.name() {
1952 "message" => self.0.message = Some(value.to_string()),
1953 "audit_event" => self.0.audit_event = Some(value.to_string()),
1954 _ => {}
1955 }
1956 }
1957
1958 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1959 let formatted = format!("{value:?}");
1965 let cleaned = formatted
1966 .trim_start_matches('"')
1967 .trim_end_matches('"')
1968 .to_string();
1969 match field.name() {
1970 "message" => self.0.message = Some(cleaned),
1971 "audit_event" => self.0.audit_event = Some(cleaned),
1972 _ => {}
1973 }
1974 }
1975 }
1976
1977 struct CaptureSubscriber {
1990 events: Arc<StdMutex<Vec<CapturedEvent>>>,
1991 }
1992
1993 impl CaptureSubscriber {
1994 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
1995 Self { events }
1996 }
1997 }
1998
1999 impl tracing::Subscriber for CaptureSubscriber {
2000 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
2001 true
2002 }
2003 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
2004 tracing::span::Id::from_u64(1)
2005 }
2006 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
2007 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
2008 fn event(&self, event: &tracing::Event<'_>) {
2009 let mut visitor = CapturedEventVisitor::default();
2010 event.record(&mut visitor);
2011 self.events.lock().unwrap().push(visitor.0);
2012 }
2013 fn enter(&self, _: &tracing::span::Id) {}
2014 fn exit(&self, _: &tracing::span::Id) {}
2015 }
2016
2017 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
2027 static GLOBAL_INIT: Once = Once::new();
2028
2029 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
2030 GLOBAL_INIT.call_once(|| {
2031 let buffer = Arc::new(StdMutex::new(Vec::new()));
2032 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
2033 let _ = tracing::subscriber::set_global_default(subscriber);
2038 let _ = GLOBAL_CAPTURE.set(buffer);
2039 });
2040 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
2041 }
2042
2043 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
2048 where
2049 Fut: std::future::Future<Output = ()>,
2050 {
2051 let buffer = global_capture();
2052 buffer.lock().unwrap().clear();
2053
2054 let rt = tokio::runtime::Builder::new_current_thread()
2055 .enable_all()
2056 .build()
2057 .expect("build current-thread tokio runtime");
2058 rt.block_on(future);
2059
2060 let result = buffer.lock().unwrap().clone();
2061 result
2062 }
2063
2064 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
2071 events
2072 .iter()
2073 .filter(|e| e.message.as_deref() == Some("gate.check"))
2074 .filter(|e| {
2075 e.audit_event
2076 .as_deref()
2077 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
2078 .and_then(|v| {
2079 v.get("gate_impl")
2080 .and_then(|g| g.as_str().map(|s| s.to_string()))
2081 })
2082 .as_deref()
2083 == Some(gate_impl)
2084 })
2085 .cloned()
2086 .collect()
2087 }
2088
2089 #[test]
2090 #[serial]
2091 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
2092 #[derive(Debug)]
2093 struct TracingAllowGate;
2094 impl Gate for TracingAllowGate {
2095 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
2096 Ok(GateDecision::allow())
2097 }
2098 fn impl_name(&self) -> &'static str {
2099 "TracingAllowGate"
2100 }
2101 }
2102
2103 let events = capture_dispatch_events(async {
2104 let mut builder = VerbRegistryBuilder::new();
2105 builder.register(AlphaPack);
2106 builder.with_gate(Arc::new(TracingAllowGate));
2107 builder.with_default_namespace("tenant-default");
2108 let reg = builder.build().expect("registry builds");
2109 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
2110 .await
2111 .unwrap();
2112 });
2113
2114 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
2115 assert_eq!(
2116 gate_events.len(),
2117 1,
2118 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
2119 );
2120 let payload = gate_events[0]
2121 .audit_event
2122 .as_ref()
2123 .expect("gate.check event must carry an audit_event field");
2124 let audit: khive_gate::AuditEvent =
2125 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2126 assert_eq!(audit.decision, AuditDecision::Allow);
2127 assert_eq!(audit.verb, "list");
2128 assert_eq!(audit.namespace, "tenant-q");
2129 assert_eq!(audit.gate_impl, "TracingAllowGate");
2130 assert!(
2131 audit.deny_reason.is_none(),
2132 "deny_reason must be None on Allow"
2133 );
2134 }
2135
2136 use crate::runtime::NamespaceToken;
2139 use async_trait::async_trait;
2140 use khive_storage::{
2141 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
2142 };
2143 use khive_types::EventOutcome;
2144
2145 #[derive(Default, Debug)]
2147 struct MemoryEventStore {
2148 events: std::sync::Mutex<Vec<Event>>,
2149 }
2150
2151 #[async_trait]
2152 impl EventStore for MemoryEventStore {
2153 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
2154 self.events.lock().unwrap().push(event);
2155 Ok(())
2156 }
2157 async fn append_events(
2158 &self,
2159 events: Vec<Event>,
2160 ) -> khive_storage::StorageResult<BatchWriteSummary> {
2161 let attempted = events.len() as u64;
2162 let affected = attempted;
2163 self.events.lock().unwrap().extend(events);
2164 Ok(BatchWriteSummary {
2165 attempted,
2166 affected,
2167 failed: 0,
2168 first_error: String::new(),
2169 })
2170 }
2171 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
2172 Ok(self
2173 .events
2174 .lock()
2175 .unwrap()
2176 .iter()
2177 .find(|e| e.id == id)
2178 .cloned())
2179 }
2180 async fn query_events(
2181 &self,
2182 _filter: EventFilter,
2183 _page: PageRequest,
2184 ) -> khive_storage::StorageResult<Page<Event>> {
2185 let items = self.events.lock().unwrap().clone();
2186 let total = items.len() as u64;
2187 Ok(Page {
2188 items,
2189 total: Some(total),
2190 })
2191 }
2192 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
2193 Ok(self.events.lock().unwrap().len() as u64)
2194 }
2195 }
2196
2197 #[tokio::test]
2198 async fn allow_all_gate_default_remains_backward_compatible() {
2199 let mut builder = VerbRegistryBuilder::new();
2201 builder.register(AlphaPack);
2202 let reg = builder.build().expect("registry builds");
2203
2204 let res = reg.dispatch("list", Value::Null).await.unwrap();
2205 assert_eq!(
2206 res["pack"], "alpha",
2207 "AllowAllGate must allow every verb — backward compat guarantee"
2208 );
2209 let res = reg.dispatch("create", Value::Null).await.unwrap();
2210 assert_eq!(res["pack"], "alpha");
2211 }
2212
2213 #[tokio::test]
2214 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
2215 #[derive(Debug)]
2216 struct AlwaysDenyGate;
2217 impl Gate for AlwaysDenyGate {
2218 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2219 Ok(GateDecision::deny("test: always deny"))
2220 }
2221 }
2222
2223 #[derive(Debug)]
2225 struct TrackedPack {
2226 invoked: Arc<AtomicUsize>,
2227 }
2228
2229 impl khive_types::Pack for TrackedPack {
2230 const NAME: &'static str = "tracked";
2231 const NOTE_KINDS: &'static [&'static str] = &[];
2232 const ENTITY_KINDS: &'static [&'static str] = &[];
2233 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2234 name: "guarded",
2235 description: "a guarded verb",
2236 visibility: Visibility::Verb,
2237 category: VerbCategory::Assertive,
2238 params: &[],
2239 }];
2240 }
2241
2242 #[async_trait]
2243 impl PackRuntime for TrackedPack {
2244 fn name(&self) -> &str {
2245 Self::NAME
2246 }
2247 fn note_kinds(&self) -> &'static [&'static str] {
2248 Self::NOTE_KINDS
2249 }
2250 fn entity_kinds(&self) -> &'static [&'static str] {
2251 Self::ENTITY_KINDS
2252 }
2253 fn handlers(&self) -> &'static [HandlerDef] {
2254 Self::HANDLERS
2255 }
2256 async fn dispatch(
2257 &self,
2258 _verb: &str,
2259 _params: Value,
2260 _registry: &VerbRegistry,
2261 _token: &NamespaceToken,
2262 ) -> Result<Value, RuntimeError> {
2263 self.invoked.fetch_add(1, Ordering::SeqCst);
2264 Ok(serde_json::json!({"invoked": true}))
2265 }
2266 }
2267
2268 let invoked = Arc::new(AtomicUsize::new(0));
2269 let mut builder = VerbRegistryBuilder::new();
2270 builder.register(TrackedPack {
2271 invoked: invoked.clone(),
2272 });
2273 builder.with_gate(Arc::new(AlwaysDenyGate));
2274 let reg = builder.build().expect("registry builds");
2275
2276 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
2277 assert!(
2278 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
2279 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
2280 );
2281 assert_eq!(
2282 invoked.load(Ordering::SeqCst),
2283 0,
2284 "pack dispatch MUST NOT be invoked when gate denies"
2285 );
2286 }
2287
2288 #[tokio::test]
2289 async fn audit_event_persists_to_event_store_on_allow() {
2290 let store = Arc::new(MemoryEventStore::default());
2291 let mut builder = VerbRegistryBuilder::new();
2292 builder.register(AlphaPack);
2293 builder.with_event_store(store.clone());
2294 let reg = builder.build().expect("registry builds");
2295
2296 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2297 .await
2298 .unwrap();
2299
2300 let count = store.count_events(EventFilter::default()).await.unwrap();
2301 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
2302
2303 let page = store
2304 .query_events(
2305 EventFilter::default(),
2306 PageRequest {
2307 limit: 10,
2308 offset: 0,
2309 },
2310 )
2311 .await
2312 .unwrap();
2313 let ev = &page.items[0];
2314 assert_eq!(ev.verb, "list");
2315 assert_eq!(ev.namespace, "test-ns");
2316 assert_eq!(ev.substrate, SubstrateKind::Event);
2317 assert_eq!(ev.outcome, EventOutcome::Success);
2318 }
2319
2320 #[tokio::test]
2321 async fn audit_event_persists_to_event_store_on_deny() {
2322 #[derive(Debug)]
2323 struct AlwaysDenyGate;
2324 impl Gate for AlwaysDenyGate {
2325 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2326 Ok(GateDecision::deny("denied by test"))
2327 }
2328 }
2329
2330 let store = Arc::new(MemoryEventStore::default());
2331 let mut builder = VerbRegistryBuilder::new();
2332 builder.register(AlphaPack);
2333 builder.with_gate(Arc::new(AlwaysDenyGate));
2334 builder.with_event_store(store.clone());
2335 let reg = builder.build().expect("registry builds");
2336
2337 let err = reg
2339 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2340 .await
2341 .unwrap_err();
2342 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2343
2344 let count = store.count_events(EventFilter::default()).await.unwrap();
2345 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
2346
2347 let page = store
2348 .query_events(
2349 EventFilter::default(),
2350 PageRequest {
2351 limit: 10,
2352 offset: 0,
2353 },
2354 )
2355 .await
2356 .unwrap();
2357 let ev = &page.items[0];
2358 assert_eq!(ev.verb, "list");
2359 assert_eq!(ev.outcome, EventOutcome::Denied);
2360 }
2361
2362 #[tokio::test]
2363 async fn gate_error_does_not_persist_to_event_store() {
2364 #[derive(Debug)]
2365 struct FailingGate;
2366 impl Gate for FailingGate {
2367 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
2368 Err(khive_gate::GateError::Internal("gate broken".into()))
2369 }
2370 }
2371
2372 let store = Arc::new(MemoryEventStore::default());
2373 let mut builder = VerbRegistryBuilder::new();
2374 builder.register(AlphaPack);
2375 builder.with_gate(Arc::new(FailingGate));
2376 builder.with_event_store(store.clone());
2377 let reg = builder.build().expect("registry builds");
2378
2379 let res = reg.dispatch("list", Value::Null).await.unwrap();
2381 assert_eq!(
2382 res["pack"], "alpha",
2383 "gate error must fail-open, not block dispatch"
2384 );
2385
2386 let count = store.count_events(EventFilter::default()).await.unwrap();
2387 assert_eq!(
2388 count, 0,
2389 "gate infrastructure error must NOT produce an audit event in EventStore"
2390 );
2391 }
2392
2393 #[tokio::test]
2394 async fn no_event_store_configured_tracing_only() {
2395 let mut builder = VerbRegistryBuilder::new();
2399 builder.register(AlphaPack);
2400 let reg = builder.build().expect("registry builds");
2401
2402 let res = reg.dispatch("list", Value::Null).await.unwrap();
2403 assert_eq!(res["pack"], "alpha");
2404 }
2405
2406 #[test]
2407 #[serial]
2408 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
2409 #[derive(Debug)]
2410 struct TracingDenyGate;
2411 impl Gate for TracingDenyGate {
2412 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2413 Ok(GateDecision::deny("denied by test gate"))
2414 }
2415 fn impl_name(&self) -> &'static str {
2416 "TracingDenyGate"
2417 }
2418 }
2419
2420 let events = capture_dispatch_events(async {
2421 let mut builder = VerbRegistryBuilder::new();
2422 builder.register(AlphaPack);
2423 builder.with_gate(Arc::new(TracingDenyGate));
2424 let reg = builder.build().expect("registry builds");
2425 let _ = reg.dispatch("create", serde_json::Value::Null).await;
2428 });
2429
2430 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
2431 assert_eq!(
2432 gate_events.len(),
2433 1,
2434 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
2435 );
2436 let payload = gate_events[0]
2437 .audit_event
2438 .as_ref()
2439 .expect("gate.check event must carry an audit_event field on Deny");
2440 let audit: khive_gate::AuditEvent =
2441 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2442 assert_eq!(audit.decision, AuditDecision::Deny);
2443 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
2444 assert_eq!(audit.gate_impl, "TracingDenyGate");
2445 let payload_json: serde_json::Value =
2449 serde_json::from_str(payload).expect("payload must be valid JSON");
2450 assert_eq!(
2451 payload_json["obligations"],
2452 serde_json::Value::Array(Vec::new()),
2453 "obligations must be `[]` on Deny on the tracing payload, not omitted"
2454 );
2455 }
2456
2457 #[tokio::test]
2465 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
2466 #[derive(Debug)]
2467 struct DenyGateWithName;
2468 impl Gate for DenyGateWithName {
2469 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2470 Ok(GateDecision::deny("policy: write forbidden for anon"))
2471 }
2472 fn impl_name(&self) -> &'static str {
2473 "DenyGateWithName"
2474 }
2475 }
2476
2477 let store = Arc::new(MemoryEventStore::default());
2478 let mut builder = VerbRegistryBuilder::new();
2479 builder.register(AlphaPack);
2480 builder.with_gate(Arc::new(DenyGateWithName));
2481 builder.with_event_store(store.clone());
2482 let reg = builder.build().expect("registry builds");
2483
2484 let err = reg
2486 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2487 .await
2488 .unwrap_err();
2489 assert!(
2490 matches!(err, RuntimeError::PermissionDenied { .. }),
2491 "expected PermissionDenied, got {err:?}"
2492 );
2493
2494 let page = store
2496 .query_events(
2497 EventFilter::default(),
2498 PageRequest {
2499 limit: 10,
2500 offset: 0,
2501 },
2502 )
2503 .await
2504 .unwrap();
2505 assert_eq!(
2506 page.items.len(),
2507 1,
2508 "one audit event must be persisted on deny"
2509 );
2510
2511 let ev = &page.items[0];
2512 assert_eq!(ev.outcome, EventOutcome::Denied);
2513
2514 let data = &ev.payload;
2516
2517 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2518 .expect("Event.payload must deserialize to AuditEvent");
2519
2520 assert_eq!(
2521 audit.deny_reason.as_deref(),
2522 Some("policy: write forbidden for anon"),
2523 "deny_reason must be preserved through EventStore"
2524 );
2525 assert_eq!(
2526 audit.gate_impl, "DenyGateWithName",
2527 "gate_impl must be preserved through EventStore"
2528 );
2529 assert_eq!(
2530 audit.decision,
2531 khive_gate::AuditDecision::Deny,
2532 "decision field must be preserved through EventStore"
2533 );
2534 }
2535
2536 #[tokio::test]
2537 async fn audit_envelope_round_trips_obligations_through_event_store() {
2538 use khive_gate::Obligation;
2539
2540 #[derive(Debug)]
2541 struct ObligationGate;
2542 impl Gate for ObligationGate {
2543 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2544 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2545 tag: "billing.meter".into(),
2546 }]))
2547 }
2548 fn impl_name(&self) -> &'static str {
2549 "ObligationGate"
2550 }
2551 }
2552
2553 let store = Arc::new(MemoryEventStore::default());
2554 let mut builder = VerbRegistryBuilder::new();
2555 builder.register(AlphaPack);
2556 builder.with_gate(Arc::new(ObligationGate));
2557 builder.with_event_store(store.clone());
2558 let reg = builder.build().expect("registry builds");
2559
2560 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2561 .await
2562 .unwrap();
2563
2564 let page = store
2565 .query_events(
2566 EventFilter::default(),
2567 PageRequest {
2568 limit: 10,
2569 offset: 0,
2570 },
2571 )
2572 .await
2573 .unwrap();
2574 assert_eq!(page.items.len(), 1);
2575
2576 let ev = &page.items[0];
2577 assert_eq!(ev.outcome, EventOutcome::Success);
2578
2579 let data = &ev.payload;
2580
2581 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2582 .expect("Event.payload must deserialize to AuditEvent");
2583
2584 assert_eq!(audit.gate_impl, "ObligationGate");
2585 assert_eq!(
2586 audit.obligations.len(),
2587 1,
2588 "obligations must be preserved through EventStore"
2589 );
2590 match &audit.obligations[0] {
2591 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
2592 other => panic!("expected Audit obligation, got {other:?}"),
2593 }
2594 }
2595
2596 #[tokio::test]
2604 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
2605 #[derive(Debug)]
2606 struct SqlTestDenyGate;
2607 impl Gate for SqlTestDenyGate {
2608 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2609 Ok(GateDecision::deny("sql-path: write denied"))
2610 }
2611 fn impl_name(&self) -> &'static str {
2612 "SqlTestDenyGate"
2613 }
2614 }
2615
2616 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2620 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2621 let sql_store = rt
2622 .events(&test_tok)
2623 .expect("events_for_namespace must succeed");
2624
2625 let mut builder = VerbRegistryBuilder::new();
2626 builder.register(AlphaPack);
2627 builder.with_gate(Arc::new(SqlTestDenyGate));
2628 builder.with_event_store(sql_store.clone());
2629 let reg = builder.build().expect("registry builds");
2630
2631 let err = reg
2633 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2634 .await
2635 .unwrap_err();
2636 assert!(
2637 matches!(err, RuntimeError::PermissionDenied { .. }),
2638 "expected PermissionDenied, got {err:?}"
2639 );
2640
2641 let page = sql_store
2643 .query_events(
2644 EventFilter::default(),
2645 PageRequest {
2646 limit: 10,
2647 offset: 0,
2648 },
2649 )
2650 .await
2651 .unwrap();
2652 assert_eq!(
2653 page.items.len(),
2654 1,
2655 "one audit event must be persisted on deny through SqlEventStore"
2656 );
2657
2658 let ev = &page.items[0];
2659 assert_eq!(ev.outcome, EventOutcome::Denied);
2660
2661 let data = &ev.payload;
2665
2666 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2667 .expect("Event.payload must deserialize to AuditEvent after SQL round-trip");
2668
2669 assert_eq!(
2670 audit.deny_reason.as_deref(),
2671 Some("sql-path: write denied"),
2672 "deny_reason must survive the SQL text round-trip"
2673 );
2674 assert_eq!(
2675 audit.gate_impl, "SqlTestDenyGate",
2676 "gate_impl must survive the SQL text round-trip"
2677 );
2678 assert_eq!(
2679 audit.decision,
2680 khive_gate::AuditDecision::Deny,
2681 "decision field must survive the SQL text round-trip"
2682 );
2683 assert!(
2686 audit.obligations.is_empty(),
2687 "obligations must be preserved as empty [] through SQL round-trip"
2688 );
2689 }
2690
2691 #[tokio::test]
2703 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
2704 use khive_gate::Obligation;
2705
2706 #[derive(Debug)]
2707 struct SqlTestAllowWithObligationGate;
2708 impl Gate for SqlTestAllowWithObligationGate {
2709 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2710 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2711 tag: "sql-path-billing.meter".into(),
2712 }]))
2713 }
2714 fn impl_name(&self) -> &'static str {
2715 "SqlTestAllowWithObligationGate"
2716 }
2717 }
2718
2719 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2720 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2721 let sql_store = rt
2722 .events(&test_tok)
2723 .expect("events_for_namespace must succeed");
2724
2725 let mut builder = VerbRegistryBuilder::new();
2726 builder.register(AlphaPack);
2727 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
2728 builder.with_event_store(sql_store.clone());
2729 let reg = builder.build().expect("registry builds");
2730
2731 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2733 .await
2734 .expect("dispatch must succeed when gate allows");
2735
2736 let page = sql_store
2738 .query_events(
2739 EventFilter::default(),
2740 PageRequest {
2741 limit: 10,
2742 offset: 0,
2743 },
2744 )
2745 .await
2746 .unwrap();
2747 assert_eq!(
2748 page.items.len(),
2749 1,
2750 "one audit event must be persisted on allow through SqlEventStore"
2751 );
2752
2753 let ev = &page.items[0];
2754 assert_eq!(ev.outcome, EventOutcome::Success);
2755
2756 let data = &ev.payload;
2757
2758 let obligations_raw = data
2763 .get("obligations")
2764 .expect("Event.data JSON must contain 'obligations' key");
2765 let obligations_arr = obligations_raw
2766 .as_array()
2767 .expect("'obligations' must be a JSON array");
2768 assert!(
2769 !obligations_arr.is_empty(),
2770 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
2771 );
2772
2773 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2776 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
2777
2778 assert_eq!(
2779 audit.gate_impl, "SqlTestAllowWithObligationGate",
2780 "gate_impl must survive the SQL text round-trip"
2781 );
2782 assert_eq!(
2783 audit.decision,
2784 khive_gate::AuditDecision::Allow,
2785 "decision field must survive the SQL text round-trip"
2786 );
2787 assert_eq!(
2788 audit.obligations.len(),
2789 1,
2790 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
2791 );
2792 match &audit.obligations[0] {
2793 Obligation::Audit { tag } => assert_eq!(
2794 tag, "sql-path-billing.meter",
2795 "Audit obligation tag must survive the SQL text round-trip"
2796 ),
2797 other => panic!("expected Audit obligation, got {other:?}"),
2798 }
2799 }
2800
2801 #[tokio::test]
2809 async fn audit_event_payload_shape_for_create_verb_matches_adr035_envelope() {
2810 let store = Arc::new(MemoryEventStore::default());
2811 let mut builder = VerbRegistryBuilder::new();
2812 builder.register(AlphaPack);
2813 builder.with_event_store(store.clone());
2814 builder.with_default_namespace("test-ns");
2815 let reg = builder.build().expect("registry builds");
2816
2817 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
2820 .await
2821 .unwrap();
2822
2823 let count = store.count_events(EventFilter::default()).await.unwrap();
2824 assert_eq!(count, 1, "exactly one audit event for one dispatch");
2825
2826 let page = store
2827 .query_events(
2828 EventFilter::default(),
2829 PageRequest {
2830 limit: 10,
2831 offset: 0,
2832 },
2833 )
2834 .await
2835 .unwrap();
2836 let ev = &page.items[0];
2837
2838 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
2840 assert_eq!(
2841 ev.outcome,
2842 EventOutcome::Success,
2843 "ev.outcome must be Success on allow"
2844 );
2845 assert_eq!(
2846 ev.namespace, "test-ns",
2847 "ev.namespace must match the dispatch namespace"
2848 );
2849
2850 let data = &ev.payload;
2852
2853 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2854 .expect("ev.payload must deserialize to AuditEvent");
2855
2856 assert_eq!(
2857 audit.decision,
2858 khive_gate::AuditDecision::Allow,
2859 "AuditEvent.decision must be Allow"
2860 );
2861 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
2862 assert_eq!(
2863 audit.namespace, "test-ns",
2864 "AuditEvent.namespace must be preserved"
2865 );
2866 assert_eq!(
2867 audit.gate_impl, "AllowAllGate",
2868 "AuditEvent.gate_impl must name the gate implementation"
2869 );
2870 assert!(
2871 audit.deny_reason.is_none(),
2872 "AuditEvent.deny_reason must be None on Allow"
2873 );
2874 let payload_json: serde_json::Value =
2876 serde_json::from_value(data.clone()).expect("data must be valid JSON");
2877 assert_eq!(
2878 payload_json["obligations"],
2879 serde_json::Value::Array(Vec::new()),
2880 "obligations must be [] on AllowAllGate (wire-shape rule ADR-018)"
2881 );
2882 }
2883
2884 #[tokio::test]
2886 async fn audit_event_threads_target_id_from_dispatch_args() {
2887 let store = Arc::new(MemoryEventStore::default());
2888 let target = uuid::Uuid::new_v4();
2889 let mut builder = VerbRegistryBuilder::new();
2890 builder.register(AlphaPack);
2891 builder.with_event_store(store.clone());
2892 builder.with_default_namespace("test-ns");
2893 let reg = builder.build().expect("registry builds");
2894
2895 reg.dispatch(
2896 "create",
2897 serde_json::json!({"namespace": "test-ns", "target_id": target}),
2898 )
2899 .await
2900 .unwrap();
2901
2902 let page = store
2903 .query_events(
2904 EventFilter::default(),
2905 PageRequest {
2906 offset: 0,
2907 limit: 10,
2908 },
2909 )
2910 .await
2911 .unwrap();
2912 assert_eq!(
2913 page.items[0].target_id,
2914 Some(target),
2915 "#282: audit event must carry target_id from dispatch params"
2916 );
2917 }
2918}
2919
2920#[cfg(test)]
2923mod dep_tests {
2924 use super::*;
2925 use async_trait::async_trait;
2926 use khive_types::Pack;
2927 use serde_json::Value;
2928
2929 struct KgDepPack;
2930 struct MemoryDepPack;
2931 struct ADepPack;
2932 struct BDepPack;
2933
2934 impl Pack for KgDepPack {
2935 const NAME: &'static str = "kg_dep";
2936 const NOTE_KINDS: &'static [&'static str] = &["observation"];
2937 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
2938 const HANDLERS: &'static [HandlerDef] = &[];
2939 }
2940
2941 impl Pack for MemoryDepPack {
2942 const NAME: &'static str = "memory_dep";
2943 const NOTE_KINDS: &'static [&'static str] = &["memory"];
2944 const ENTITY_KINDS: &'static [&'static str] = &[];
2945 const HANDLERS: &'static [HandlerDef] = &[];
2946 const REQUIRES: &'static [&'static str] = &["kg_dep"];
2947 }
2948
2949 impl Pack for ADepPack {
2950 const NAME: &'static str = "pack_a";
2951 const NOTE_KINDS: &'static [&'static str] = &[];
2952 const ENTITY_KINDS: &'static [&'static str] = &[];
2953 const HANDLERS: &'static [HandlerDef] = &[];
2954 const REQUIRES: &'static [&'static str] = &["pack_b"];
2955 }
2956
2957 impl Pack for BDepPack {
2958 const NAME: &'static str = "pack_b";
2959 const NOTE_KINDS: &'static [&'static str] = &[];
2960 const ENTITY_KINDS: &'static [&'static str] = &[];
2961 const HANDLERS: &'static [HandlerDef] = &[];
2962 const REQUIRES: &'static [&'static str] = &["pack_a"];
2963 }
2964
2965 #[async_trait]
2966 impl PackRuntime for KgDepPack {
2967 fn name(&self) -> &str {
2968 Self::NAME
2969 }
2970 fn note_kinds(&self) -> &'static [&'static str] {
2971 Self::NOTE_KINDS
2972 }
2973 fn entity_kinds(&self) -> &'static [&'static str] {
2974 Self::ENTITY_KINDS
2975 }
2976 fn handlers(&self) -> &'static [HandlerDef] {
2977 Self::HANDLERS
2978 }
2979 async fn dispatch(
2980 &self,
2981 verb: &str,
2982 _: Value,
2983 _: &VerbRegistry,
2984 _: &NamespaceToken,
2985 ) -> Result<Value, RuntimeError> {
2986 Err(RuntimeError::InvalidInput(format!(
2987 "KgDepPack has no verbs: {verb}"
2988 )))
2989 }
2990 }
2991
2992 #[async_trait]
2993 impl PackRuntime for MemoryDepPack {
2994 fn name(&self) -> &str {
2995 Self::NAME
2996 }
2997 fn note_kinds(&self) -> &'static [&'static str] {
2998 Self::NOTE_KINDS
2999 }
3000 fn entity_kinds(&self) -> &'static [&'static str] {
3001 Self::ENTITY_KINDS
3002 }
3003 fn handlers(&self) -> &'static [HandlerDef] {
3004 Self::HANDLERS
3005 }
3006 fn requires(&self) -> &'static [&'static str] {
3007 Self::REQUIRES
3008 }
3009 async fn dispatch(
3010 &self,
3011 verb: &str,
3012 _: Value,
3013 _: &VerbRegistry,
3014 _: &NamespaceToken,
3015 ) -> Result<Value, RuntimeError> {
3016 Err(RuntimeError::InvalidInput(format!(
3017 "MemoryDepPack has no verbs: {verb}"
3018 )))
3019 }
3020 }
3021
3022 #[async_trait]
3023 impl PackRuntime for ADepPack {
3024 fn name(&self) -> &str {
3025 Self::NAME
3026 }
3027 fn note_kinds(&self) -> &'static [&'static str] {
3028 Self::NOTE_KINDS
3029 }
3030 fn entity_kinds(&self) -> &'static [&'static str] {
3031 Self::ENTITY_KINDS
3032 }
3033 fn handlers(&self) -> &'static [HandlerDef] {
3034 Self::HANDLERS
3035 }
3036 fn requires(&self) -> &'static [&'static str] {
3037 Self::REQUIRES
3038 }
3039 async fn dispatch(
3040 &self,
3041 verb: &str,
3042 _: Value,
3043 _: &VerbRegistry,
3044 _: &NamespaceToken,
3045 ) -> Result<Value, RuntimeError> {
3046 Err(RuntimeError::InvalidInput(format!(
3047 "ADepPack has no verbs: {verb}"
3048 )))
3049 }
3050 }
3051
3052 #[async_trait]
3053 impl PackRuntime for BDepPack {
3054 fn name(&self) -> &str {
3055 Self::NAME
3056 }
3057 fn note_kinds(&self) -> &'static [&'static str] {
3058 Self::NOTE_KINDS
3059 }
3060 fn entity_kinds(&self) -> &'static [&'static str] {
3061 Self::ENTITY_KINDS
3062 }
3063 fn handlers(&self) -> &'static [HandlerDef] {
3064 Self::HANDLERS
3065 }
3066 fn requires(&self) -> &'static [&'static str] {
3067 Self::REQUIRES
3068 }
3069 async fn dispatch(
3070 &self,
3071 verb: &str,
3072 _: Value,
3073 _: &VerbRegistry,
3074 _: &NamespaceToken,
3075 ) -> Result<Value, RuntimeError> {
3076 Err(RuntimeError::InvalidInput(format!(
3077 "BDepPack has no verbs: {verb}"
3078 )))
3079 }
3080 }
3081
3082 #[test]
3083 fn test_pack_deps_happy_path() {
3084 let mut builder = VerbRegistryBuilder::new();
3085 builder.register(MemoryDepPack);
3086 builder.register(KgDepPack);
3087 let reg = builder
3088 .build()
3089 .expect("kg_dep satisfies memory_dep dependency");
3090 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
3091 let names = reg.pack_names();
3092 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
3093 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
3094 assert!(
3095 kg_pos < mem_pos,
3096 "kg_dep must be loaded before memory_dep; order: {names:?}"
3097 );
3098 }
3099
3100 #[test]
3101 fn test_pack_deps_missing() {
3102 let mut builder = VerbRegistryBuilder::new();
3103 builder.register(MemoryDepPack);
3104 let err = match builder.build() {
3105 Ok(_) => panic!("expected Err, got Ok"),
3106 Err(e) => e,
3107 };
3108 assert!(
3109 matches!(err, RuntimeError::MissingPackDependency(_)),
3110 "expected MissingPackDependency, got {err:?}"
3111 );
3112 let msg = err.to_string();
3113 assert!(
3114 msg.contains("memory_dep"),
3115 "error must name the dependent pack: {msg}"
3116 );
3117 assert!(
3118 msg.contains("kg_dep"),
3119 "error must name the missing dep: {msg}"
3120 );
3121 }
3122
3123 #[test]
3124 fn test_pack_deps_circular() {
3125 let mut builder = VerbRegistryBuilder::new();
3126 builder.register(ADepPack);
3127 builder.register(BDepPack);
3128 let err = match builder.build() {
3129 Ok(_) => panic!("expected Err, got Ok"),
3130 Err(e) => e,
3131 };
3132 assert!(
3133 matches!(err, RuntimeError::CircularPackDependency(_)),
3134 "expected CircularPackDependency, got {err:?}"
3135 );
3136 let msg = err.to_string();
3137 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
3138 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
3139 }
3140
3141 #[test]
3142 fn test_pack_deps_no_deps() {
3143 struct NoDepsA;
3144 struct NoDepsB;
3145
3146 impl Pack for NoDepsA {
3147 const NAME: &'static str = "no_deps_a";
3148 const NOTE_KINDS: &'static [&'static str] = &[];
3149 const ENTITY_KINDS: &'static [&'static str] = &[];
3150 const HANDLERS: &'static [HandlerDef] = &[];
3151 }
3152
3153 impl Pack for NoDepsB {
3154 const NAME: &'static str = "no_deps_b";
3155 const NOTE_KINDS: &'static [&'static str] = &[];
3156 const ENTITY_KINDS: &'static [&'static str] = &[];
3157 const HANDLERS: &'static [HandlerDef] = &[];
3158 }
3159
3160 #[async_trait]
3161 impl PackRuntime for NoDepsA {
3162 fn name(&self) -> &str {
3163 Self::NAME
3164 }
3165 fn note_kinds(&self) -> &'static [&'static str] {
3166 Self::NOTE_KINDS
3167 }
3168 fn entity_kinds(&self) -> &'static [&'static str] {
3169 Self::ENTITY_KINDS
3170 }
3171 fn handlers(&self) -> &'static [HandlerDef] {
3172 Self::HANDLERS
3173 }
3174 async fn dispatch(
3175 &self,
3176 verb: &str,
3177 _: Value,
3178 _: &VerbRegistry,
3179 _: &NamespaceToken,
3180 ) -> Result<Value, RuntimeError> {
3181 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
3182 }
3183 }
3184
3185 #[async_trait]
3186 impl PackRuntime for NoDepsB {
3187 fn name(&self) -> &str {
3188 Self::NAME
3189 }
3190 fn note_kinds(&self) -> &'static [&'static str] {
3191 Self::NOTE_KINDS
3192 }
3193 fn entity_kinds(&self) -> &'static [&'static str] {
3194 Self::ENTITY_KINDS
3195 }
3196 fn handlers(&self) -> &'static [HandlerDef] {
3197 Self::HANDLERS
3198 }
3199 async fn dispatch(
3200 &self,
3201 verb: &str,
3202 _: Value,
3203 _: &VerbRegistry,
3204 _: &NamespaceToken,
3205 ) -> Result<Value, RuntimeError> {
3206 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
3207 }
3208 }
3209
3210 let mut builder = VerbRegistryBuilder::new();
3211 builder.register(NoDepsA);
3212 builder.register(NoDepsB);
3213 let reg = builder.build().expect("packs with REQUIRES=&[] build");
3214 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
3215 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
3216 }
3217}
3218
3219#[cfg(test)]
3222mod hook_tests {
3223 use super::*;
3224 use async_trait::async_trait;
3225 use khive_types::Pack;
3226 use std::sync::atomic::{AtomicUsize, Ordering};
3227 use std::sync::Mutex as StdMutex;
3228
3229 struct SimplePack;
3230
3231 impl Pack for SimplePack {
3232 const NAME: &'static str = "simple";
3233 const NOTE_KINDS: &'static [&'static str] = &[];
3234 const ENTITY_KINDS: &'static [&'static str] = &[];
3235 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
3236 name: "ping",
3237 description: "ping",
3238 visibility: Visibility::Verb,
3239 category: VerbCategory::Assertive,
3240 params: &[],
3241 }];
3242 }
3243
3244 #[async_trait]
3245 impl PackRuntime for SimplePack {
3246 fn name(&self) -> &str {
3247 SimplePack::NAME
3248 }
3249 fn note_kinds(&self) -> &'static [&'static str] {
3250 SimplePack::NOTE_KINDS
3251 }
3252 fn entity_kinds(&self) -> &'static [&'static str] {
3253 SimplePack::ENTITY_KINDS
3254 }
3255 fn handlers(&self) -> &'static [HandlerDef] {
3256 SimplePack::HANDLERS
3257 }
3258 async fn dispatch(
3259 &self,
3260 verb: &str,
3261 _params: Value,
3262 _registry: &VerbRegistry,
3263 _token: &NamespaceToken,
3264 ) -> Result<Value, RuntimeError> {
3265 Ok(serde_json::json!({ "verb": verb }))
3266 }
3267 }
3268
3269 #[derive(Default)]
3271 struct CountingHook {
3272 calls: AtomicUsize,
3273 last_verb: StdMutex<String>,
3274 }
3275
3276 #[async_trait]
3277 impl DispatchHook for CountingHook {
3278 async fn on_dispatch(&self, view: &EventView) {
3279 self.calls.fetch_add(1, Ordering::SeqCst);
3280 *self.last_verb.lock().unwrap() = view.event.verb.clone();
3281 }
3282 }
3283
3284 #[tokio::test]
3285 async fn dispatch_hook_fires_on_successful_dispatch() {
3286 let hook = Arc::new(CountingHook::default());
3287 let mut builder = VerbRegistryBuilder::new();
3288 builder.register(SimplePack);
3289 builder.with_dispatch_hook(hook.clone());
3290 let reg = builder.build().expect("registry builds");
3291
3292 reg.dispatch("ping", Value::Null).await.unwrap();
3293
3294 assert_eq!(
3295 hook.calls.load(Ordering::SeqCst),
3296 1,
3297 "hook must fire once per successful dispatch"
3298 );
3299 assert_eq!(
3300 hook.last_verb.lock().unwrap().as_str(),
3301 "ping",
3302 "hook event must carry the dispatched verb"
3303 );
3304 }
3305
3306 #[tokio::test]
3307 async fn dispatch_hook_fires_multiple_times() {
3308 let hook = Arc::new(CountingHook::default());
3309 let mut builder = VerbRegistryBuilder::new();
3310 builder.register(SimplePack);
3311 builder.with_dispatch_hook(hook.clone());
3312 let reg = builder.build().expect("registry builds");
3313
3314 reg.dispatch("ping", Value::Null).await.unwrap();
3315 reg.dispatch("ping", Value::Null).await.unwrap();
3316 reg.dispatch("ping", Value::Null).await.unwrap();
3317
3318 assert_eq!(
3319 hook.calls.load(Ordering::SeqCst),
3320 3,
3321 "hook must fire once per successful dispatch"
3322 );
3323 }
3324
3325 #[tokio::test]
3326 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
3327 let hook = Arc::new(CountingHook::default());
3328 let mut builder = VerbRegistryBuilder::new();
3329 builder.register(SimplePack);
3330 builder.with_dispatch_hook(hook.clone());
3331 let reg = builder.build().expect("registry builds");
3332
3333 let _ = reg.dispatch("nonexistent", Value::Null).await;
3334
3335 assert_eq!(
3336 hook.calls.load(Ordering::SeqCst),
3337 0,
3338 "hook must NOT fire for unknown verb (dispatch returns error)"
3339 );
3340 }
3341
3342 #[tokio::test]
3343 async fn dispatch_hook_does_not_fire_on_gate_deny() {
3344 use khive_gate::{Gate, GateDecision, GateError};
3345
3346 #[derive(Debug)]
3347 struct AlwaysDenyGate;
3348 impl Gate for AlwaysDenyGate {
3349 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3350 Ok(GateDecision::deny("test deny"))
3351 }
3352 }
3353
3354 let hook = Arc::new(CountingHook::default());
3355 let mut builder = VerbRegistryBuilder::new();
3356 builder.register(SimplePack);
3357 builder.with_gate(Arc::new(AlwaysDenyGate));
3358 builder.with_dispatch_hook(hook.clone());
3359 let reg = builder.build().expect("registry builds");
3360
3361 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
3362 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
3363
3364 assert_eq!(
3365 hook.calls.load(Ordering::SeqCst),
3366 0,
3367 "hook must NOT fire when gate denies dispatch"
3368 );
3369 }
3370
3371 #[tokio::test]
3372 async fn dispatch_hook_event_carries_namespace_from_params() {
3373 let hook = Arc::new(CountingHook::default());
3374
3375 #[derive(Default)]
3376 struct NsCapturingHook {
3377 ns: StdMutex<String>,
3378 }
3379
3380 #[async_trait]
3381 impl DispatchHook for NsCapturingHook {
3382 async fn on_dispatch(&self, view: &EventView) {
3383 *self.ns.lock().unwrap() = view.event.namespace.clone();
3384 }
3385 }
3386
3387 let ns_hook = Arc::new(NsCapturingHook::default());
3388 let mut builder = VerbRegistryBuilder::new();
3389 builder.register(SimplePack);
3390 builder.with_dispatch_hook(ns_hook.clone());
3391 let reg = builder.build().expect("registry builds");
3392
3393 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
3394 .await
3395 .unwrap();
3396
3397 assert_eq!(
3398 ns_hook.ns.lock().unwrap().as_str(),
3399 "tenant-abc",
3400 "dispatch hook event must carry the resolved namespace"
3401 );
3402
3403 drop(hook);
3405 }
3406
3407 #[tokio::test]
3408 async fn no_dispatch_hook_configured_dispatch_succeeds() {
3409 let mut builder = VerbRegistryBuilder::new();
3411 builder.register(SimplePack);
3412 let reg = builder.build().expect("registry builds");
3414
3415 let res = reg.dispatch("ping", Value::Null).await.unwrap();
3416 assert_eq!(res["verb"], "ping");
3417 }
3418}
3419
3420#[cfg(test)]
3423mod help_tests {
3424 use super::*;
3425 use async_trait::async_trait;
3426 use khive_types::Pack;
3427 use std::sync::{
3428 atomic::{AtomicUsize, Ordering},
3429 Arc,
3430 };
3431
3432 static CREATE_PARAMS: [ParamDef; 2] = [
3437 ParamDef {
3438 name: "kind",
3439 param_type: "string",
3440 required: true,
3441 description: "Granular kind (concept | document | ...).",
3442 },
3443 ParamDef {
3444 name: "name",
3445 param_type: "string",
3446 required: false,
3447 description: "Human-readable name.",
3448 },
3449 ];
3450
3451 static RECALL_PARAMS: [ParamDef; 2] = [
3452 ParamDef {
3453 name: "query",
3454 param_type: "string",
3455 required: true,
3456 description: "Semantic recall query.",
3457 },
3458 ParamDef {
3459 name: "limit",
3460 param_type: "integer",
3461 required: false,
3462 description: "Maximum memories to return.",
3463 },
3464 ];
3465
3466 static EMBED_PARAMS: [ParamDef; 0] = [];
3469
3470 struct HelpPack {
3471 invocations: Arc<AtomicUsize>,
3472 }
3473
3474 impl Pack for HelpPack {
3475 const NAME: &'static str = "helptest";
3476 const NOTE_KINDS: &'static [&'static str] = &[];
3477 const ENTITY_KINDS: &'static [&'static str] = &[];
3478 const HANDLERS: &'static [HandlerDef] = &[
3479 HandlerDef {
3480 name: "create",
3481 description: "Create an entity or note",
3482 visibility: Visibility::Verb,
3483 category: VerbCategory::Commissive,
3484 params: &CREATE_PARAMS,
3485 },
3486 HandlerDef {
3487 name: "recall",
3488 description: "Recall memory notes with decay-aware hybrid ranking",
3489 visibility: Visibility::Verb,
3490 category: VerbCategory::Assertive,
3491 params: &RECALL_PARAMS,
3492 },
3493 HandlerDef {
3496 name: "recall.embed",
3497 description: "Return the embedding vector used by memory recall",
3498 visibility: Visibility::Subhandler,
3499 category: VerbCategory::Assertive,
3500 params: &EMBED_PARAMS,
3501 },
3502 ];
3503 }
3504
3505 #[async_trait]
3506 impl PackRuntime for HelpPack {
3507 fn name(&self) -> &str {
3508 HelpPack::NAME
3509 }
3510 fn note_kinds(&self) -> &'static [&'static str] {
3511 HelpPack::NOTE_KINDS
3512 }
3513 fn entity_kinds(&self) -> &'static [&'static str] {
3514 HelpPack::ENTITY_KINDS
3515 }
3516 fn handlers(&self) -> &'static [HandlerDef] {
3517 HelpPack::HANDLERS
3518 }
3519 async fn dispatch(
3520 &self,
3521 verb: &str,
3522 _params: Value,
3523 _registry: &VerbRegistry,
3524 _token: &NamespaceToken,
3525 ) -> Result<Value, RuntimeError> {
3526 self.invocations.fetch_add(1, Ordering::SeqCst);
3527 Ok(serde_json::json!({ "pack": "helptest", "verb": verb }))
3528 }
3529 }
3530
3531 fn build_help_registry(invocations: Arc<AtomicUsize>) -> VerbRegistry {
3532 let mut builder = VerbRegistryBuilder::new();
3533 builder.register(HelpPack { invocations });
3534 builder.build().expect("help registry builds")
3535 }
3536
3537 #[tokio::test]
3540 async fn test_help_true_returns_schema_for_kg_create() {
3541 let invocations = Arc::new(AtomicUsize::new(0));
3542 let reg = build_help_registry(invocations.clone());
3543
3544 let result = reg
3545 .dispatch("create", serde_json::json!({ "help": true }))
3546 .await
3547 .expect("help=true must succeed for a known verb");
3548
3549 assert_eq!(result["verb"], "create", "envelope must name the verb");
3551 assert_eq!(
3552 result["pack"], "helptest",
3553 "envelope must name the owning pack"
3554 );
3555 assert!(
3556 result["description"].as_str().is_some(),
3557 "description must be a string"
3558 );
3559
3560 let params = result["params"]
3562 .as_array()
3563 .expect("params must be a JSON array");
3564 assert!(!params.is_empty(), "params array must not be empty");
3565
3566 let kind_param = params.iter().find(|p| p["name"] == "kind");
3568 assert!(
3569 kind_param.is_some(),
3570 "params array must include the 'kind' parameter"
3571 );
3572 let kind_param = kind_param.unwrap();
3573 assert_eq!(
3574 kind_param["required"],
3575 serde_json::json!(true),
3576 "'kind' must be required"
3577 );
3578 assert_eq!(kind_param["type"], "string", "'kind' type must be 'string'");
3579 }
3580
3581 #[tokio::test]
3583 async fn test_help_true_returns_schema_for_recall() {
3584 let invocations = Arc::new(AtomicUsize::new(0));
3585 let reg = build_help_registry(invocations.clone());
3586
3587 let result = reg
3588 .dispatch("recall", serde_json::json!({ "help": true }))
3589 .await
3590 .expect("help=true must succeed for recall");
3591
3592 assert_eq!(result["verb"], "recall");
3593 assert_eq!(result["pack"], "helptest");
3594
3595 let params = result["params"]
3596 .as_array()
3597 .expect("params must be a JSON array");
3598
3599 let query_param = params.iter().find(|p| p["name"] == "query");
3601 assert!(query_param.is_some(), "params must include 'query'");
3602 let query_param = query_param.unwrap();
3603 assert_eq!(
3604 query_param["required"],
3605 serde_json::json!(true),
3606 "'query' must be required"
3607 );
3608
3609 let limit_param = params.iter().find(|p| p["name"] == "limit");
3611 assert!(limit_param.is_some(), "params must include 'limit'");
3612 let limit_param = limit_param.unwrap();
3613 assert_eq!(
3614 limit_param["required"],
3615 serde_json::json!(false),
3616 "'limit' must be optional"
3617 );
3618 }
3619
3620 #[tokio::test]
3623 async fn test_help_true_does_not_execute_the_verb() {
3624 let invocations = Arc::new(AtomicUsize::new(0));
3625 let reg = build_help_registry(invocations.clone());
3626
3627 reg.dispatch("create", serde_json::json!({ "help": true }))
3629 .await
3630 .expect("help=true must succeed");
3631 reg.dispatch("recall", serde_json::json!({ "help": true }))
3632 .await
3633 .expect("help=true must succeed");
3634
3635 assert_eq!(
3636 invocations.load(Ordering::SeqCst),
3637 0,
3638 "pack dispatch MUST NOT be invoked when help=true; \
3639 got {} invocation(s)",
3640 invocations.load(Ordering::SeqCst)
3641 );
3642
3643 reg.dispatch("create", serde_json::json!({}))
3645 .await
3646 .expect("normal dispatch must succeed");
3647 assert_eq!(
3648 invocations.load(Ordering::SeqCst),
3649 1,
3650 "pack dispatch must fire exactly once for a normal call"
3651 );
3652 }
3653
3654 #[tokio::test]
3663 async fn help_true_on_subhandler_returns_callable_via_mcp_false() {
3664 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3665
3666 let result = reg
3667 .dispatch("recall.embed", serde_json::json!({ "help": true }))
3668 .await
3669 .expect("help=true on subhandler must succeed (no permission check on help path)");
3670
3671 assert_eq!(
3672 result["callable_via_mcp"],
3673 serde_json::json!(false),
3674 "subhandler help must carry callable_via_mcp: false"
3675 );
3676 assert_eq!(
3677 result["visibility"], "internal",
3678 "subhandler help must carry visibility: internal"
3679 );
3680 assert_eq!(result["verb"], "recall.embed");
3683 assert_eq!(result["pack"], "helptest");
3684 }
3685
3686 #[tokio::test]
3688 async fn help_true_on_public_verb_does_not_have_callable_via_mcp_false() {
3689 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3690
3691 let result = reg
3692 .dispatch("create", serde_json::json!({ "help": true }))
3693 .await
3694 .expect("help=true on public verb must succeed");
3695
3696 assert_ne!(
3698 result.get("callable_via_mcp"),
3699 Some(&serde_json::json!(false)),
3700 "public verb help must NOT carry callable_via_mcp: false"
3701 );
3702 assert_ne!(
3704 result.get("visibility"),
3705 Some(&serde_json::json!("internal")),
3706 "public verb help must NOT carry visibility: internal"
3707 );
3708 }
3709
3710 #[tokio::test]
3712 async fn help_true_on_unknown_verb_returns_error() {
3713 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3714
3715 let err = reg
3716 .dispatch("nonexistent_verb", serde_json::json!({ "help": true }))
3717 .await
3718 .unwrap_err();
3719
3720 assert!(
3721 matches!(err, RuntimeError::InvalidInput(_)),
3722 "help=true on unknown verb must return InvalidInput, got {err:?}"
3723 );
3724 let msg = err.to_string();
3725 assert!(
3726 msg.contains("nonexistent_verb"),
3727 "error must name the unknown verb: {msg}"
3728 );
3729 }
3730
3731 #[tokio::test]
3733 async fn help_true_on_subhandler_includes_params_field() {
3734 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3735
3736 let result = reg
3737 .dispatch("recall.embed", serde_json::json!({ "help": true }))
3738 .await
3739 .expect("help=true on subhandler must succeed");
3740
3741 let params = result
3743 .get("params")
3744 .expect("subhandler help must include 'params' field");
3745 assert!(
3746 params.is_array(),
3747 "subhandler help params must be a JSON array"
3748 );
3749 }
3750
3751 #[tokio::test]
3757 async fn help_true_unknown_verb_available_list_excludes_subhandlers() {
3758 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3759
3760 let err = reg
3761 .dispatch("not_a_verb", serde_json::json!({ "help": true }))
3762 .await
3763 .unwrap_err();
3764
3765 let msg = err.to_string();
3766 assert!(
3769 !msg.contains("recall.embed"),
3770 "unknown-verb help error must not advertise subhandler recall.embed: {msg}"
3771 );
3772 assert!(
3774 msg.contains("create"),
3775 "unknown-verb help error must still list public verb 'create': {msg}"
3776 );
3777 assert!(
3778 msg.contains("recall"),
3779 "unknown-verb help error must still list public verb 'recall': {msg}"
3780 );
3781 }
3782
3783 #[tokio::test]
3785 async fn dispatch_unknown_verb_available_list_excludes_subhandlers() {
3786 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
3787
3788 let err = reg
3789 .dispatch("not_a_verb", serde_json::json!({}))
3790 .await
3791 .unwrap_err();
3792
3793 let msg = err.to_string();
3794 assert!(
3797 !msg.contains("recall.embed"),
3798 "dispatch unknown-verb error must not advertise subhandler recall.embed: {msg}"
3799 );
3800 assert!(
3802 msg.contains("create"),
3803 "dispatch unknown-verb error must still list public verb 'create': {msg}"
3804 );
3805 assert!(
3806 msg.contains("recall"),
3807 "dispatch unknown-verb error must still list public verb 'recall': {msg}"
3808 );
3809 }
3810}