1use std::collections::{HashMap, HashSet, VecDeque};
15use std::sync::Arc;
16
17use async_trait::async_trait;
18use khive_gate::{ActorRef, AllowAllGate, AuditEvent, GateDecision, GateRef, GateRequest};
19use khive_storage::{Event, EventStore, SubstrateKind};
20use khive_types::{EventOutcome, Namespace};
21use serde_json::Value;
22
23pub use khive_types::{EdgeEndpointRule, EndpointKind, VerbDef};
24
25use crate::error::{
26 CircularPackDependency, MissingPackDependencies, MissingPackDependency, RuntimeError,
27};
28use crate::KhiveRuntime;
29
30#[async_trait]
39pub trait PackRuntime: Send + Sync {
40 fn name(&self) -> &str;
42
43 fn note_kinds(&self) -> &'static [&'static str];
45
46 fn entity_kinds(&self) -> &'static [&'static str];
48
49 fn verbs(&self) -> &'static [VerbDef];
51
52 fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
56 &[]
57 }
58
59 fn requires(&self) -> &'static [&'static str] {
62 &[]
63 }
64
65 fn kind_hook(&self, _kind: &str) -> Option<Arc<dyn KindHook>> {
73 None
74 }
75
76 async fn dispatch(
81 &self,
82 verb: &str,
83 params: Value,
84 registry: &VerbRegistry,
85 ) -> Result<Value, RuntimeError>;
86}
87
88#[async_trait]
102pub trait KindHook: Send + Sync + std::fmt::Debug {
103 async fn prepare_create(
109 &self,
110 runtime: &KhiveRuntime,
111 args: &mut Value,
112 ) -> Result<(), RuntimeError>;
113
114 async fn after_create(
123 &self,
124 runtime: &KhiveRuntime,
125 id: uuid::Uuid,
126 args: &Value,
127 ) -> Result<(), RuntimeError>;
128}
129
130pub struct VerbRegistryBuilder {
135 packs: Vec<Box<dyn PackRuntime>>,
136 gate: GateRef,
137 default_namespace: String,
138 event_store: Option<Arc<dyn EventStore>>,
145}
146
147impl VerbRegistryBuilder {
148 pub fn new() -> Self {
149 Self {
150 packs: Vec::new(),
151 gate: std::sync::Arc::new(AllowAllGate),
152 default_namespace: Namespace::default_ns().as_str().to_string(),
153 event_store: None,
154 }
155 }
156
157 pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
160 self.packs.push(Box::new(pack));
161 self
162 }
163
164 pub fn with_gate(&mut self, gate: GateRef) -> &mut Self {
169 self.gate = gate;
170 self
171 }
172
173 pub fn with_default_namespace(&mut self, ns: impl Into<String>) -> &mut Self {
178 self.default_namespace = ns.into();
179 self
180 }
181
182 pub fn with_event_store(&mut self, store: Arc<dyn EventStore>) -> &mut Self {
191 self.event_store = Some(store);
192 self
193 }
194
195 pub fn build(self) -> Result<VerbRegistry, RuntimeError> {
201 let packs = self.packs;
202 let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(packs.len());
203 for (idx, pack) in packs.iter().enumerate() {
204 if let Some(prev_idx) = name_to_idx.insert(pack.name(), idx) {
205 return Err(RuntimeError::PackRedeclared {
206 name: pack.name().to_string(),
207 first_idx: prev_idx,
208 second_idx: idx,
209 });
210 }
211 }
212
213 let mut missing: Vec<MissingPackDependency> = Vec::new();
214 let mut indegree = vec![0usize; packs.len()];
215 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); packs.len()];
216
217 for (idx, pack) in packs.iter().enumerate() {
218 for &requires in pack.requires() {
219 match name_to_idx.get(requires).copied() {
220 Some(dep_idx) => {
221 dependents[dep_idx].push(idx);
222 indegree[idx] += 1;
223 }
224 None => missing.push(MissingPackDependency {
225 from: pack.name().to_string(),
226 requires: requires.to_string(),
227 }),
228 }
229 }
230 }
231
232 if !missing.is_empty() {
233 return if missing.len() == 1 {
234 Err(RuntimeError::MissingPackDependency(missing.remove(0)))
235 } else {
236 Err(RuntimeError::MissingPackDependencies(
237 MissingPackDependencies { missing },
238 ))
239 };
240 }
241
242 let mut ready: VecDeque<usize> = indegree
243 .iter()
244 .enumerate()
245 .filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
246 .collect();
247 let mut ordered_indices = Vec::with_capacity(packs.len());
248
249 while let Some(idx) = ready.pop_front() {
250 ordered_indices.push(idx);
251 for &dep_idx in &dependents[idx] {
252 indegree[dep_idx] -= 1;
253 if indegree[dep_idx] == 0 {
254 ready.push_back(dep_idx);
255 }
256 }
257 }
258
259 if ordered_indices.len() != packs.len() {
260 let cycle_nodes: HashSet<usize> = indegree
261 .iter()
262 .enumerate()
263 .filter_map(|(idx, degree)| (*degree > 0).then_some(idx))
264 .collect();
265 let cycle = find_pack_dependency_cycle(&packs, &name_to_idx, &cycle_nodes);
266 return Err(RuntimeError::CircularPackDependency(
267 CircularPackDependency { cycle },
268 ));
269 }
270
271 let mut slots: Vec<Option<Box<dyn PackRuntime>>> = packs.into_iter().map(Some).collect();
272 let ordered_packs: Vec<Box<dyn PackRuntime>> = ordered_indices
273 .into_iter()
274 .map(|idx| slots[idx].take().expect("topological index must exist"))
275 .collect();
276
277 Ok(VerbRegistry {
278 packs: Arc::new(ordered_packs),
279 gate: self.gate,
280 default_namespace: self.default_namespace,
281 event_store: self.event_store,
282 })
283 }
284}
285
286fn find_pack_dependency_cycle(
287 packs: &[Box<dyn PackRuntime>],
288 name_to_idx: &HashMap<&str, usize>,
289 cycle_nodes: &HashSet<usize>,
290) -> Vec<String> {
291 fn visit(
292 idx: usize,
293 packs: &[Box<dyn PackRuntime>],
294 name_to_idx: &HashMap<&str, usize>,
295 cycle_nodes: &HashSet<usize>,
296 visiting: &mut Vec<usize>,
297 visited: &mut HashSet<usize>,
298 ) -> Option<Vec<String>> {
299 if let Some(pos) = visiting.iter().position(|&seen| seen == idx) {
300 let mut cycle: Vec<String> = visiting[pos..]
301 .iter()
302 .map(|&i| packs[i].name().to_string())
303 .collect();
304 cycle.push(packs[idx].name().to_string());
305 return Some(cycle);
306 }
307 if !visited.insert(idx) {
308 return None;
309 }
310 visiting.push(idx);
311 for &req in packs[idx].requires() {
312 let Some(&dep_idx) = name_to_idx.get(req) else {
313 continue;
314 };
315 if cycle_nodes.contains(&dep_idx) {
316 if let Some(cycle) =
317 visit(dep_idx, packs, name_to_idx, cycle_nodes, visiting, visited)
318 {
319 return Some(cycle);
320 }
321 }
322 }
323 visiting.pop();
324 None
325 }
326
327 let mut visited = HashSet::new();
328 for &idx in cycle_nodes {
329 let mut visiting = Vec::new();
330 if let Some(cycle) = visit(
331 idx,
332 packs,
333 name_to_idx,
334 cycle_nodes,
335 &mut visiting,
336 &mut visited,
337 ) {
338 return cycle;
339 }
340 }
341 cycle_nodes
342 .iter()
343 .map(|&idx| packs[idx].name().to_string())
344 .collect()
345}
346
347impl Default for VerbRegistryBuilder {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353#[derive(Clone)]
357pub struct VerbRegistry {
358 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
359 gate: GateRef,
360 default_namespace: String,
361 event_store: Option<Arc<dyn EventStore>>,
363}
364
365impl VerbRegistry {
366 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
397 let ns_str = params
398 .get("namespace")
399 .and_then(Value::as_str)
400 .unwrap_or(&self.default_namespace);
401 let gate_req = GateRequest::new(
402 ActorRef::anonymous(),
403 Namespace::new(ns_str),
404 verb,
405 params.clone(),
406 );
407
408 let gate_blocked = match self.gate.check(&gate_req) {
414 Ok(decision) => {
415 let is_deny = matches!(decision, GateDecision::Deny { .. });
416
417 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
419 tracing::info!(
420 audit_event = %serde_json::to_string(&audit)
421 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
422 "gate.check"
423 );
424
425 if let Some(store) = &self.event_store {
427 let outcome = if is_deny {
428 EventOutcome::Denied
429 } else {
430 EventOutcome::Success
431 };
432 let audit_data = serde_json::to_value(&audit).unwrap_or_else(|e| {
433 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
434 serde_json::Value::Null
435 });
436 let storage_event = Event::new(
437 gate_req.namespace.as_str(),
438 verb,
439 SubstrateKind::Event,
440 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
441 )
442 .with_outcome(outcome)
443 .with_data(audit_data);
444 if let Err(store_err) = store.append_event(storage_event).await {
445 tracing::warn!(
446 verb,
447 error = %store_err,
448 "audit event store write failed (non-fatal)"
449 );
450 }
451 }
452
453 if is_deny {
454 let reason = match decision {
455 GateDecision::Deny { reason } => reason,
456 _ => String::new(),
457 };
458 Some(reason)
459 } else {
460 None
461 }
462 }
463 Err(err) => {
464 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
467 None
468 }
469 };
470
471 if let Some(reason) = gate_blocked {
473 return Err(RuntimeError::PermissionDenied {
474 verb: verb.to_string(),
475 reason,
476 });
477 }
478
479 for pack in self.packs.iter() {
480 if pack.verbs().iter().any(|v| v.name == verb) {
481 return pack.dispatch(verb, params, self).await;
482 }
483 }
484 let available: Vec<&str> = self
485 .packs
486 .iter()
487 .flat_map(|p| p.verbs().iter().map(|v| v.name))
488 .collect();
489 Err(RuntimeError::InvalidInput(format!(
490 "unknown verb {verb:?}; available: {}",
491 available.join(", ")
492 )))
493 }
494
495 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
502 for pack in self.packs.iter() {
503 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
504 if owns {
505 if let Some(hook) = pack.kind_hook(kind) {
506 return Some(hook);
507 }
508 }
509 }
510 None
511 }
512
513 pub fn all_verbs(&self) -> Vec<&'static VerbDef> {
519 self.packs.iter().flat_map(|p| p.verbs().iter()).collect()
520 }
521
522 pub fn all_note_kinds(&self) -> Vec<&'static str> {
525 let mut seen = std::collections::HashSet::new();
526 self.packs
527 .iter()
528 .flat_map(|p| p.note_kinds().iter().copied())
529 .filter(|k| seen.insert(*k))
530 .collect()
531 }
532
533 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
536 let mut seen = std::collections::HashSet::new();
537 self.packs
538 .iter()
539 .flat_map(|p| p.entity_kinds().iter().copied())
540 .filter(|k| seen.insert(*k))
541 .collect()
542 }
543
544 pub fn pack_names(&self) -> Vec<&str> {
546 self.packs.iter().map(|p| p.name()).collect()
547 }
548
549 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
551 self.packs
552 .iter()
553 .find(|p| p.name() == name)
554 .map(|p| p.requires())
555 }
556
557 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
563 self.packs
564 .iter()
565 .flat_map(|p| p.edge_rules().iter().copied())
566 .collect()
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use khive_types::Pack;
574
575 struct AlphaPack;
576
577 impl Pack for AlphaPack {
578 const NAME: &'static str = "alpha";
579 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
580 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
581 const VERBS: &'static [VerbDef] = &[
582 VerbDef {
583 name: "create",
584 description: "create a widget",
585 },
586 VerbDef {
587 name: "list",
588 description: "list widgets",
589 },
590 ];
591 }
592
593 #[async_trait]
594 impl PackRuntime for AlphaPack {
595 fn name(&self) -> &str {
596 AlphaPack::NAME
597 }
598 fn note_kinds(&self) -> &'static [&'static str] {
599 AlphaPack::NOTE_KINDS
600 }
601 fn entity_kinds(&self) -> &'static [&'static str] {
602 AlphaPack::ENTITY_KINDS
603 }
604 fn verbs(&self) -> &'static [VerbDef] {
605 AlphaPack::VERBS
606 }
607 async fn dispatch(
608 &self,
609 verb: &str,
610 _params: Value,
611 _registry: &VerbRegistry,
612 ) -> Result<Value, RuntimeError> {
613 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
614 }
615 }
616
617 struct BetaPack;
618
619 impl Pack for BetaPack {
620 const NAME: &'static str = "beta";
621 const NOTE_KINDS: &'static [&'static str] = &["log", "alert"];
622 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
623 const VERBS: &'static [VerbDef] = &[
624 VerbDef {
625 name: "notify",
626 description: "send alert",
627 },
628 VerbDef {
629 name: "create",
630 description: "create a gadget",
631 },
632 ];
633 }
634
635 #[async_trait]
636 impl PackRuntime for BetaPack {
637 fn name(&self) -> &str {
638 BetaPack::NAME
639 }
640 fn note_kinds(&self) -> &'static [&'static str] {
641 BetaPack::NOTE_KINDS
642 }
643 fn entity_kinds(&self) -> &'static [&'static str] {
644 BetaPack::ENTITY_KINDS
645 }
646 fn verbs(&self) -> &'static [VerbDef] {
647 BetaPack::VERBS
648 }
649 async fn dispatch(
650 &self,
651 verb: &str,
652 _params: Value,
653 _registry: &VerbRegistry,
654 ) -> Result<Value, RuntimeError> {
655 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
656 }
657 }
658
659 fn build_registry() -> VerbRegistry {
660 let mut builder = VerbRegistryBuilder::new();
661 builder.register(AlphaPack);
662 builder.register(BetaPack);
663 builder.build().expect("registry builds")
664 }
665
666 #[tokio::test]
667 async fn dispatch_routes_to_correct_pack() {
668 let reg = build_registry();
669
670 let res = reg.dispatch("list", Value::Null).await.unwrap();
671 assert_eq!(res["pack"], "alpha");
672
673 let res = reg.dispatch("notify", Value::Null).await.unwrap();
674 assert_eq!(res["pack"], "beta");
675 }
676
677 #[tokio::test]
678 async fn dispatch_first_registered_wins_on_collision() {
679 let reg = build_registry();
680
681 let res = reg.dispatch("create", Value::Null).await.unwrap();
682 assert_eq!(res["pack"], "alpha", "first registered pack wins");
683 }
684
685 #[tokio::test]
686 async fn dispatch_unknown_verb_returns_error() {
687 let reg = build_registry();
688
689 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
690 let msg = err.to_string();
691 assert!(msg.contains("explode"));
692 assert!(msg.contains("create"));
693 }
694
695 #[test]
696 fn all_verbs_aggregates_across_packs() {
697 let reg = build_registry();
698 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
699 assert_eq!(verbs, vec!["create", "list", "notify", "create"]);
700 }
701
702 #[test]
703 fn note_kinds_are_deduplicated() {
704 let reg = build_registry();
705 let kinds = reg.all_note_kinds();
706 assert_eq!(kinds, vec!["memo", "log", "alert"]);
707 }
708
709 #[test]
710 fn entity_kinds_are_deduplicated() {
711 let reg = build_registry();
712 let kinds = reg.all_entity_kinds();
713 assert_eq!(kinds, vec!["widget", "gadget"]);
714 }
715
716 use khive_gate::{Gate, GateError};
719 use std::sync::atomic::{AtomicUsize, Ordering};
720 use std::sync::Arc;
721
722 #[derive(Default, Debug)]
723 struct CountingGate {
724 calls: AtomicUsize,
725 deny_verb: Option<&'static str>,
726 }
727
728 impl Gate for CountingGate {
729 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
730 self.calls.fetch_add(1, Ordering::SeqCst);
731 if Some(req.verb.as_str()) == self.deny_verb {
732 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
733 } else {
734 Ok(GateDecision::allow())
735 }
736 }
737 }
738
739 #[tokio::test]
740 async fn dispatch_consults_the_gate() {
741 let gate = Arc::new(CountingGate::default());
742 let mut builder = VerbRegistryBuilder::new();
743 builder.register(AlphaPack);
744 builder.with_gate(gate.clone());
745 let reg = builder.build().expect("registry builds");
746
747 reg.dispatch("list", Value::Null).await.unwrap();
748 reg.dispatch("create", Value::Null).await.unwrap();
749 assert_eq!(
750 gate.calls.load(Ordering::SeqCst),
751 2,
752 "gate should be consulted once per dispatch"
753 );
754 }
755
756 #[tokio::test]
757 async fn dispatch_returns_permission_denied_on_deny_v03() {
758 let gate = Arc::new(CountingGate {
759 calls: AtomicUsize::new(0),
760 deny_verb: Some("create"),
761 });
762 let mut builder = VerbRegistryBuilder::new();
763 builder.register(AlphaPack);
764 builder.with_gate(gate.clone());
765 let reg = builder.build().expect("registry builds");
766
767 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
769 assert!(
770 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
771 "expected PermissionDenied, got {err:?}"
772 );
773 let msg = err.to_string();
774 assert!(
775 msg.contains("create"),
776 "error message must name the verb: {msg}"
777 );
778 assert!(
779 msg.contains("test deny for create"),
780 "error message must carry the deny reason: {msg}"
781 );
782 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
783 }
784
785 #[tokio::test]
786 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
787 let gate = Arc::new(CountingGate {
789 calls: AtomicUsize::new(0),
790 deny_verb: Some("create"),
791 });
792 let mut builder = VerbRegistryBuilder::new();
793 builder.register(AlphaPack);
794 builder.with_gate(gate.clone());
795 let reg = builder.build().expect("registry builds");
796
797 let res = reg.dispatch("list", Value::Null).await.unwrap();
798 assert_eq!(res["pack"], "alpha");
799 }
800
801 #[tokio::test]
802 async fn dispatch_uses_allow_all_gate_by_default() {
803 let reg = build_registry();
805 let res = reg.dispatch("list", Value::Null).await.unwrap();
806 assert_eq!(res["pack"], "alpha");
807 }
808
809 #[derive(Default, Debug)]
812 struct NamespaceCapturingGate {
813 seen: std::sync::Mutex<Vec<String>>,
814 }
815
816 impl Gate for NamespaceCapturingGate {
817 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
818 self.seen
819 .lock()
820 .unwrap()
821 .push(req.namespace.as_str().to_string());
822 Ok(GateDecision::allow())
823 }
824 }
825
826 #[tokio::test]
827 async fn dispatch_propagates_params_namespace_to_gate() {
828 let gate = Arc::new(NamespaceCapturingGate::default());
829 let mut builder = VerbRegistryBuilder::new();
830 builder.register(AlphaPack);
831 builder.with_gate(gate.clone());
832 builder.with_default_namespace("tenant-x");
833 let reg = builder.build().expect("registry builds");
834
835 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
837 .await
838 .unwrap();
839 reg.dispatch("list", Value::Null).await.unwrap();
841 reg.dispatch("list", serde_json::json!({"namespace": ""}))
846 .await
847 .unwrap();
848
849 let seen = gate.seen.lock().unwrap().clone();
850 assert_eq!(seen, vec!["tenant-y", "tenant-x", ""]);
851 }
852
853 #[tokio::test]
854 async fn dispatch_falls_back_to_local_when_no_default_set() {
855 let gate = Arc::new(NamespaceCapturingGate::default());
857 let mut builder = VerbRegistryBuilder::new();
858 builder.register(AlphaPack);
859 builder.with_gate(gate.clone());
860 let reg = builder.build().expect("registry builds");
861
862 reg.dispatch("list", Value::Null).await.unwrap();
863 let seen = gate.seen.lock().unwrap().clone();
864 assert_eq!(seen, vec!["local"]);
865 }
866
867 use khive_gate::{AuditDecision, AuditEvent, Obligation};
870
871 #[derive(Default, Debug)]
873 struct AuditCapturingGate {
874 events: std::sync::Mutex<Vec<AuditEvent>>,
875 deny_verb: Option<&'static str>,
876 }
877
878 impl Gate for AuditCapturingGate {
879 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
880 let decision = if Some(req.verb.as_str()) == self.deny_verb {
881 GateDecision::deny("test deny")
882 } else {
883 GateDecision::allow_with(vec![Obligation::Audit {
884 tag: format!("{}.check", req.verb),
885 }])
886 };
887 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
889 self.events.lock().unwrap().push(ev);
890 Ok(decision)
891 }
892
893 fn impl_name(&self) -> &'static str {
894 "AuditCapturingGate"
895 }
896 }
897
898 #[tokio::test]
899 async fn dispatch_emits_one_audit_event_per_call() {
900 let gate = Arc::new(AuditCapturingGate::default());
901 let mut builder = VerbRegistryBuilder::new();
902 builder.register(AlphaPack);
903 builder.with_gate(gate.clone());
904 let reg = builder.build().expect("registry builds");
905
906 reg.dispatch("list", Value::Null).await.unwrap();
907 reg.dispatch("create", Value::Null).await.unwrap();
908
909 let evs = gate.events.lock().unwrap();
910 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
911 }
912
913 #[tokio::test]
914 async fn dispatch_audit_event_allow_carries_obligations() {
915 let gate = Arc::new(AuditCapturingGate::default());
916 let mut builder = VerbRegistryBuilder::new();
917 builder.register(AlphaPack);
918 builder.with_gate(gate.clone());
919 let reg = builder.build().expect("registry builds");
920
921 reg.dispatch("list", Value::Null).await.unwrap();
922
923 let evs = gate.events.lock().unwrap();
924 let ev = &evs[0];
925 assert_eq!(ev.verb, "list");
926 assert_eq!(ev.decision, AuditDecision::Allow);
927 assert!(ev.deny_reason.is_none());
928 assert_eq!(ev.obligations.len(), 1);
929 assert_eq!(ev.gate_impl, "AuditCapturingGate");
930 }
931
932 #[tokio::test]
933 async fn dispatch_audit_event_deny_carries_reason() {
934 let gate = Arc::new(AuditCapturingGate {
935 events: Default::default(),
936 deny_verb: Some("create"),
937 });
938 let mut builder = VerbRegistryBuilder::new();
939 builder.register(AlphaPack);
940 builder.with_gate(gate.clone());
941 let reg = builder.build().expect("registry builds");
942
943 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
946 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
947
948 let evs = gate.events.lock().unwrap();
949 let ev = &evs[0];
950 assert_eq!(ev.verb, "create");
951 assert_eq!(ev.decision, AuditDecision::Deny);
952 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
953 assert!(ev.obligations.is_empty());
954 }
955
956 #[tokio::test]
957 async fn dispatch_audit_event_fields_match_gate_request() {
958 let gate = Arc::new(AuditCapturingGate::default());
959 let mut builder = VerbRegistryBuilder::new();
960 builder.register(AlphaPack);
961 builder.with_gate(gate.clone());
962 builder.with_default_namespace("tenant-z");
963 let reg = builder.build().expect("registry builds");
964
965 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
966 .await
967 .unwrap();
968
969 let evs = gate.events.lock().unwrap();
970 let ev = &evs[0];
971 assert_eq!(ev.namespace, "tenant-q");
973 assert_eq!(ev.verb, "list");
974 assert_eq!(ev.actor.kind, "anonymous");
975 }
976
977 use std::sync::Mutex as StdMutex;
990 use tracing::field::{Field, Visit};
991 use tracing_subscriber::layer::SubscriberExt;
992 use tracing_subscriber::Layer;
993
994 #[derive(Clone, Debug, Default)]
995 struct CapturedEvent {
996 message: Option<String>,
997 audit_event: Option<String>,
998 }
999
1000 #[derive(Default)]
1001 struct CapturedEventVisitor(CapturedEvent);
1002
1003 impl Visit for CapturedEventVisitor {
1004 fn record_str(&mut self, field: &Field, value: &str) {
1005 match field.name() {
1006 "message" => self.0.message = Some(value.to_string()),
1007 "audit_event" => self.0.audit_event = Some(value.to_string()),
1008 _ => {}
1009 }
1010 }
1011
1012 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1013 let formatted = format!("{value:?}");
1019 let cleaned = formatted
1020 .trim_start_matches('"')
1021 .trim_end_matches('"')
1022 .to_string();
1023 match field.name() {
1024 "message" => self.0.message = Some(cleaned),
1025 "audit_event" => self.0.audit_event = Some(cleaned),
1026 _ => {}
1027 }
1028 }
1029 }
1030
1031 struct CaptureLayer(Arc<StdMutex<Vec<CapturedEvent>>>);
1032
1033 impl<S: tracing::Subscriber> Layer<S> for CaptureLayer {
1034 fn on_event(
1035 &self,
1036 event: &tracing::Event<'_>,
1037 _: tracing_subscriber::layer::Context<'_, S>,
1038 ) {
1039 let mut visitor = CapturedEventVisitor::default();
1040 event.record(&mut visitor);
1041 self.0.lock().unwrap().push(visitor.0);
1042 }
1043 }
1044
1045 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
1050 where
1051 Fut: std::future::Future<Output = ()>,
1052 {
1053 let captured: Arc<StdMutex<Vec<CapturedEvent>>> = Arc::new(StdMutex::new(Vec::new()));
1054 let subscriber = tracing_subscriber::registry().with(CaptureLayer(Arc::clone(&captured)));
1055
1056 tracing::subscriber::with_default(subscriber, || {
1057 let rt = tokio::runtime::Builder::new_current_thread()
1058 .enable_all()
1059 .build()
1060 .expect("build current-thread tokio runtime");
1061 rt.block_on(future);
1062 });
1063
1064 let guard = captured.lock().unwrap();
1065 guard.clone()
1066 }
1067
1068 fn gate_check_events(events: &[CapturedEvent]) -> Vec<&CapturedEvent> {
1070 events
1071 .iter()
1072 .filter(|e| e.message.as_deref() == Some("gate.check"))
1073 .collect()
1074 }
1075
1076 #[test]
1077 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
1078 let events = capture_dispatch_events(async {
1079 let mut builder = VerbRegistryBuilder::new();
1080 builder.register(AlphaPack);
1081 builder.with_gate(Arc::new(AllowAllGate));
1082 builder.with_default_namespace("tenant-default");
1083 let reg = builder.build().expect("registry builds");
1084 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1085 .await
1086 .unwrap();
1087 });
1088
1089 let gate_events = gate_check_events(&events);
1090 assert_eq!(
1091 gate_events.len(),
1092 1,
1093 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
1094 );
1095 let payload = gate_events[0]
1096 .audit_event
1097 .as_ref()
1098 .expect("gate.check event must carry an audit_event field");
1099 let audit: khive_gate::AuditEvent =
1100 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
1101 assert_eq!(audit.decision, AuditDecision::Allow);
1102 assert_eq!(audit.verb, "list");
1103 assert_eq!(audit.namespace, "tenant-q");
1104 assert_eq!(audit.gate_impl, "AllowAllGate");
1105 assert!(
1106 audit.deny_reason.is_none(),
1107 "deny_reason must be None on Allow"
1108 );
1109 }
1110
1111 use async_trait::async_trait;
1114 use khive_storage::{
1115 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
1116 };
1117 use khive_types::EventOutcome;
1118
1119 #[derive(Default, Debug)]
1121 struct MemoryEventStore {
1122 events: std::sync::Mutex<Vec<Event>>,
1123 }
1124
1125 #[async_trait]
1126 impl EventStore for MemoryEventStore {
1127 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
1128 self.events.lock().unwrap().push(event);
1129 Ok(())
1130 }
1131 async fn append_events(
1132 &self,
1133 events: Vec<Event>,
1134 ) -> khive_storage::StorageResult<BatchWriteSummary> {
1135 let attempted = events.len() as u64;
1136 let affected = attempted;
1137 self.events.lock().unwrap().extend(events);
1138 Ok(BatchWriteSummary {
1139 attempted,
1140 affected,
1141 failed: 0,
1142 first_error: String::new(),
1143 })
1144 }
1145 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
1146 Ok(self
1147 .events
1148 .lock()
1149 .unwrap()
1150 .iter()
1151 .find(|e| e.id == id)
1152 .cloned())
1153 }
1154 async fn query_events(
1155 &self,
1156 _filter: EventFilter,
1157 _page: PageRequest,
1158 ) -> khive_storage::StorageResult<Page<Event>> {
1159 let items = self.events.lock().unwrap().clone();
1160 let total = items.len() as u64;
1161 Ok(Page {
1162 items,
1163 total: Some(total),
1164 })
1165 }
1166 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
1167 Ok(self.events.lock().unwrap().len() as u64)
1168 }
1169 }
1170
1171 #[tokio::test]
1172 async fn allow_all_gate_default_remains_backward_compatible() {
1173 let mut builder = VerbRegistryBuilder::new();
1175 builder.register(AlphaPack);
1176 let reg = builder.build().expect("registry builds");
1177
1178 let res = reg.dispatch("list", Value::Null).await.unwrap();
1179 assert_eq!(
1180 res["pack"], "alpha",
1181 "AllowAllGate must allow every verb — backward compat guarantee"
1182 );
1183 let res = reg.dispatch("create", Value::Null).await.unwrap();
1184 assert_eq!(res["pack"], "alpha");
1185 }
1186
1187 #[tokio::test]
1188 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
1189 #[derive(Debug)]
1190 struct AlwaysDenyGate;
1191 impl Gate for AlwaysDenyGate {
1192 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1193 Ok(GateDecision::deny("test: always deny"))
1194 }
1195 }
1196
1197 #[derive(Debug)]
1199 struct TrackedPack {
1200 invoked: Arc<AtomicUsize>,
1201 }
1202
1203 impl khive_types::Pack for TrackedPack {
1204 const NAME: &'static str = "tracked";
1205 const NOTE_KINDS: &'static [&'static str] = &[];
1206 const ENTITY_KINDS: &'static [&'static str] = &[];
1207 const VERBS: &'static [VerbDef] = &[VerbDef {
1208 name: "guarded",
1209 description: "a guarded verb",
1210 }];
1211 }
1212
1213 #[async_trait]
1214 impl PackRuntime for TrackedPack {
1215 fn name(&self) -> &str {
1216 Self::NAME
1217 }
1218 fn note_kinds(&self) -> &'static [&'static str] {
1219 Self::NOTE_KINDS
1220 }
1221 fn entity_kinds(&self) -> &'static [&'static str] {
1222 Self::ENTITY_KINDS
1223 }
1224 fn verbs(&self) -> &'static [VerbDef] {
1225 Self::VERBS
1226 }
1227 async fn dispatch(
1228 &self,
1229 _verb: &str,
1230 _params: Value,
1231 _registry: &VerbRegistry,
1232 ) -> Result<Value, RuntimeError> {
1233 self.invoked.fetch_add(1, Ordering::SeqCst);
1234 Ok(serde_json::json!({"invoked": true}))
1235 }
1236 }
1237
1238 let invoked = Arc::new(AtomicUsize::new(0));
1239 let mut builder = VerbRegistryBuilder::new();
1240 builder.register(TrackedPack {
1241 invoked: invoked.clone(),
1242 });
1243 builder.with_gate(Arc::new(AlwaysDenyGate));
1244 let reg = builder.build().expect("registry builds");
1245
1246 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
1247 assert!(
1248 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
1249 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
1250 );
1251 assert_eq!(
1252 invoked.load(Ordering::SeqCst),
1253 0,
1254 "pack dispatch MUST NOT be invoked when gate denies"
1255 );
1256 }
1257
1258 #[tokio::test]
1259 async fn audit_event_persists_to_event_store_on_allow() {
1260 let store = Arc::new(MemoryEventStore::default());
1261 let mut builder = VerbRegistryBuilder::new();
1262 builder.register(AlphaPack);
1263 builder.with_event_store(store.clone());
1264 let reg = builder.build().expect("registry builds");
1265
1266 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1267 .await
1268 .unwrap();
1269
1270 let count = store.count_events(EventFilter::default()).await.unwrap();
1271 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
1272
1273 let page = store
1274 .query_events(
1275 EventFilter::default(),
1276 PageRequest {
1277 limit: 10,
1278 offset: 0,
1279 },
1280 )
1281 .await
1282 .unwrap();
1283 let ev = &page.items[0];
1284 assert_eq!(ev.verb, "list");
1285 assert_eq!(ev.namespace, "test-ns");
1286 assert_eq!(ev.substrate, SubstrateKind::Event);
1287 assert_eq!(ev.outcome, EventOutcome::Success);
1288 }
1289
1290 #[tokio::test]
1291 async fn audit_event_persists_to_event_store_on_deny() {
1292 #[derive(Debug)]
1293 struct AlwaysDenyGate;
1294 impl Gate for AlwaysDenyGate {
1295 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1296 Ok(GateDecision::deny("denied by test"))
1297 }
1298 }
1299
1300 let store = Arc::new(MemoryEventStore::default());
1301 let mut builder = VerbRegistryBuilder::new();
1302 builder.register(AlphaPack);
1303 builder.with_gate(Arc::new(AlwaysDenyGate));
1304 builder.with_event_store(store.clone());
1305 let reg = builder.build().expect("registry builds");
1306
1307 let err = reg
1309 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1310 .await
1311 .unwrap_err();
1312 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
1313
1314 let count = store.count_events(EventFilter::default()).await.unwrap();
1315 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
1316
1317 let page = store
1318 .query_events(
1319 EventFilter::default(),
1320 PageRequest {
1321 limit: 10,
1322 offset: 0,
1323 },
1324 )
1325 .await
1326 .unwrap();
1327 let ev = &page.items[0];
1328 assert_eq!(ev.verb, "list");
1329 assert_eq!(ev.outcome, EventOutcome::Denied);
1330 }
1331
1332 #[tokio::test]
1333 async fn gate_error_does_not_persist_to_event_store() {
1334 #[derive(Debug)]
1335 struct FailingGate;
1336 impl Gate for FailingGate {
1337 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
1338 Err(khive_gate::GateError::Internal("gate broken".into()))
1339 }
1340 }
1341
1342 let store = Arc::new(MemoryEventStore::default());
1343 let mut builder = VerbRegistryBuilder::new();
1344 builder.register(AlphaPack);
1345 builder.with_gate(Arc::new(FailingGate));
1346 builder.with_event_store(store.clone());
1347 let reg = builder.build().expect("registry builds");
1348
1349 let res = reg.dispatch("list", Value::Null).await.unwrap();
1351 assert_eq!(
1352 res["pack"], "alpha",
1353 "gate error must fail-open, not block dispatch"
1354 );
1355
1356 let count = store.count_events(EventFilter::default()).await.unwrap();
1357 assert_eq!(
1358 count, 0,
1359 "gate infrastructure error must NOT produce an audit event in EventStore"
1360 );
1361 }
1362
1363 #[tokio::test]
1364 async fn no_event_store_configured_tracing_only() {
1365 let mut builder = VerbRegistryBuilder::new();
1369 builder.register(AlphaPack);
1370 let reg = builder.build().expect("registry builds");
1371
1372 let res = reg.dispatch("list", Value::Null).await.unwrap();
1373 assert_eq!(res["pack"], "alpha");
1374 }
1375
1376 #[test]
1377 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
1378 #[derive(Debug)]
1379 struct AlwaysDenyGate;
1380 impl Gate for AlwaysDenyGate {
1381 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1382 Ok(GateDecision::deny("denied by test gate"))
1383 }
1384 fn impl_name(&self) -> &'static str {
1385 "AlwaysDenyGate"
1386 }
1387 }
1388
1389 let events = capture_dispatch_events(async {
1390 let mut builder = VerbRegistryBuilder::new();
1391 builder.register(AlphaPack);
1392 builder.with_gate(Arc::new(AlwaysDenyGate));
1393 let reg = builder.build().expect("registry builds");
1394 let _ = reg.dispatch("create", serde_json::Value::Null).await;
1397 });
1398
1399 let gate_events = gate_check_events(&events);
1400 assert_eq!(
1401 gate_events.len(),
1402 1,
1403 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
1404 );
1405 let payload = gate_events[0]
1406 .audit_event
1407 .as_ref()
1408 .expect("gate.check event must carry an audit_event field on Deny");
1409 let audit: khive_gate::AuditEvent =
1410 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
1411 assert_eq!(audit.decision, AuditDecision::Deny);
1412 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
1413 assert_eq!(audit.gate_impl, "AlwaysDenyGate");
1414 let payload_json: serde_json::Value =
1418 serde_json::from_str(payload).expect("payload must be valid JSON");
1419 assert_eq!(
1420 payload_json["obligations"],
1421 serde_json::Value::Array(Vec::new()),
1422 "obligations must be `[]` on Deny on the tracing payload, not omitted"
1423 );
1424 }
1425
1426 #[tokio::test]
1434 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
1435 #[derive(Debug)]
1436 struct DenyGateWithName;
1437 impl Gate for DenyGateWithName {
1438 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1439 Ok(GateDecision::deny("policy: write forbidden for anon"))
1440 }
1441 fn impl_name(&self) -> &'static str {
1442 "DenyGateWithName"
1443 }
1444 }
1445
1446 let store = Arc::new(MemoryEventStore::default());
1447 let mut builder = VerbRegistryBuilder::new();
1448 builder.register(AlphaPack);
1449 builder.with_gate(Arc::new(DenyGateWithName));
1450 builder.with_event_store(store.clone());
1451 let reg = builder.build().expect("registry builds");
1452
1453 let err = reg
1455 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1456 .await
1457 .unwrap_err();
1458 assert!(
1459 matches!(err, RuntimeError::PermissionDenied { .. }),
1460 "expected PermissionDenied, got {err:?}"
1461 );
1462
1463 let page = store
1465 .query_events(
1466 EventFilter::default(),
1467 PageRequest {
1468 limit: 10,
1469 offset: 0,
1470 },
1471 )
1472 .await
1473 .unwrap();
1474 assert_eq!(
1475 page.items.len(),
1476 1,
1477 "one audit event must be persisted on deny"
1478 );
1479
1480 let ev = &page.items[0];
1481 assert_eq!(ev.outcome, EventOutcome::Denied);
1482
1483 let data = ev
1485 .data
1486 .as_ref()
1487 .expect("Event.data must be Some — full AuditEvent envelope must be persisted");
1488
1489 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1490 .expect("Event.data must deserialize to AuditEvent");
1491
1492 assert_eq!(
1493 audit.deny_reason.as_deref(),
1494 Some("policy: write forbidden for anon"),
1495 "deny_reason must be preserved through EventStore"
1496 );
1497 assert_eq!(
1498 audit.gate_impl, "DenyGateWithName",
1499 "gate_impl must be preserved through EventStore"
1500 );
1501 assert_eq!(
1502 audit.decision,
1503 khive_gate::AuditDecision::Deny,
1504 "decision field must be preserved through EventStore"
1505 );
1506 }
1507
1508 #[tokio::test]
1509 async fn audit_envelope_round_trips_obligations_through_event_store() {
1510 use khive_gate::Obligation;
1511
1512 #[derive(Debug)]
1513 struct ObligationGate;
1514 impl Gate for ObligationGate {
1515 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1516 Ok(GateDecision::allow_with(vec![Obligation::Audit {
1517 tag: "billing.meter".into(),
1518 }]))
1519 }
1520 fn impl_name(&self) -> &'static str {
1521 "ObligationGate"
1522 }
1523 }
1524
1525 let store = Arc::new(MemoryEventStore::default());
1526 let mut builder = VerbRegistryBuilder::new();
1527 builder.register(AlphaPack);
1528 builder.with_gate(Arc::new(ObligationGate));
1529 builder.with_event_store(store.clone());
1530 let reg = builder.build().expect("registry builds");
1531
1532 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1533 .await
1534 .unwrap();
1535
1536 let page = store
1537 .query_events(
1538 EventFilter::default(),
1539 PageRequest {
1540 limit: 10,
1541 offset: 0,
1542 },
1543 )
1544 .await
1545 .unwrap();
1546 assert_eq!(page.items.len(), 1);
1547
1548 let ev = &page.items[0];
1549 assert_eq!(ev.outcome, EventOutcome::Success);
1550
1551 let data = ev
1552 .data
1553 .as_ref()
1554 .expect("Event.data must be Some — AuditEvent envelope must be persisted on allow");
1555
1556 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1557 .expect("Event.data must deserialize to AuditEvent");
1558
1559 assert_eq!(audit.gate_impl, "ObligationGate");
1560 assert_eq!(
1561 audit.obligations.len(),
1562 1,
1563 "obligations must be preserved through EventStore"
1564 );
1565 match &audit.obligations[0] {
1566 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
1567 other => panic!("expected Audit obligation, got {other:?}"),
1568 }
1569 }
1570
1571 #[tokio::test]
1579 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
1580 #[derive(Debug)]
1581 struct SqlTestDenyGate;
1582 impl Gate for SqlTestDenyGate {
1583 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1584 Ok(GateDecision::deny("sql-path: write denied"))
1585 }
1586 fn impl_name(&self) -> &'static str {
1587 "SqlTestDenyGate"
1588 }
1589 }
1590
1591 let rt = KhiveRuntime::memory().expect("in-memory runtime");
1595 let sql_store = rt
1596 .events(Some("test-ns"))
1597 .expect("events_for_namespace must succeed");
1598
1599 let mut builder = VerbRegistryBuilder::new();
1600 builder.register(AlphaPack);
1601 builder.with_gate(Arc::new(SqlTestDenyGate));
1602 builder.with_event_store(sql_store.clone());
1603 let reg = builder.build().expect("registry builds");
1604
1605 let err = reg
1607 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1608 .await
1609 .unwrap_err();
1610 assert!(
1611 matches!(err, RuntimeError::PermissionDenied { .. }),
1612 "expected PermissionDenied, got {err:?}"
1613 );
1614
1615 let page = sql_store
1617 .query_events(
1618 EventFilter::default(),
1619 PageRequest {
1620 limit: 10,
1621 offset: 0,
1622 },
1623 )
1624 .await
1625 .unwrap();
1626 assert_eq!(
1627 page.items.len(),
1628 1,
1629 "one audit event must be persisted on deny through SqlEventStore"
1630 );
1631
1632 let ev = &page.items[0];
1633 assert_eq!(ev.outcome, EventOutcome::Denied);
1634
1635 let data = ev
1639 .data
1640 .as_ref()
1641 .expect("Event.data must be Some — SqlEventStore must persist AuditEvent envelope");
1642
1643 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1644 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
1645
1646 assert_eq!(
1647 audit.deny_reason.as_deref(),
1648 Some("sql-path: write denied"),
1649 "deny_reason must survive the SQL text round-trip"
1650 );
1651 assert_eq!(
1652 audit.gate_impl, "SqlTestDenyGate",
1653 "gate_impl must survive the SQL text round-trip"
1654 );
1655 assert_eq!(
1656 audit.decision,
1657 khive_gate::AuditDecision::Deny,
1658 "decision field must survive the SQL text round-trip"
1659 );
1660 assert!(
1663 audit.obligations.is_empty(),
1664 "obligations must be preserved as empty [] through SQL round-trip"
1665 );
1666 }
1667
1668 #[tokio::test]
1680 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
1681 use khive_gate::Obligation;
1682
1683 #[derive(Debug)]
1684 struct SqlTestAllowWithObligationGate;
1685 impl Gate for SqlTestAllowWithObligationGate {
1686 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1687 Ok(GateDecision::allow_with(vec![Obligation::Audit {
1688 tag: "sql-path-billing.meter".into(),
1689 }]))
1690 }
1691 fn impl_name(&self) -> &'static str {
1692 "SqlTestAllowWithObligationGate"
1693 }
1694 }
1695
1696 let rt = KhiveRuntime::memory().expect("in-memory runtime");
1697 let sql_store = rt
1698 .events(Some("test-ns"))
1699 .expect("events_for_namespace must succeed");
1700
1701 let mut builder = VerbRegistryBuilder::new();
1702 builder.register(AlphaPack);
1703 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
1704 builder.with_event_store(sql_store.clone());
1705 let reg = builder.build().expect("registry builds");
1706
1707 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1709 .await
1710 .expect("dispatch must succeed when gate allows");
1711
1712 let page = sql_store
1714 .query_events(
1715 EventFilter::default(),
1716 PageRequest {
1717 limit: 10,
1718 offset: 0,
1719 },
1720 )
1721 .await
1722 .unwrap();
1723 assert_eq!(
1724 page.items.len(),
1725 1,
1726 "one audit event must be persisted on allow through SqlEventStore"
1727 );
1728
1729 let ev = &page.items[0];
1730 assert_eq!(ev.outcome, EventOutcome::Success);
1731
1732 let data = ev
1733 .data
1734 .as_ref()
1735 .expect("Event.data must be Some — SqlEventStore must persist AuditEvent envelope");
1736
1737 let obligations_raw = data
1742 .get("obligations")
1743 .expect("Event.data JSON must contain 'obligations' key");
1744 let obligations_arr = obligations_raw
1745 .as_array()
1746 .expect("'obligations' must be a JSON array");
1747 assert!(
1748 !obligations_arr.is_empty(),
1749 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
1750 );
1751
1752 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1755 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
1756
1757 assert_eq!(
1758 audit.gate_impl, "SqlTestAllowWithObligationGate",
1759 "gate_impl must survive the SQL text round-trip"
1760 );
1761 assert_eq!(
1762 audit.decision,
1763 khive_gate::AuditDecision::Allow,
1764 "decision field must survive the SQL text round-trip"
1765 );
1766 assert_eq!(
1767 audit.obligations.len(),
1768 1,
1769 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
1770 );
1771 match &audit.obligations[0] {
1772 Obligation::Audit { tag } => assert_eq!(
1773 tag, "sql-path-billing.meter",
1774 "Audit obligation tag must survive the SQL text round-trip"
1775 ),
1776 other => panic!("expected Audit obligation, got {other:?}"),
1777 }
1778 }
1779}
1780
1781#[cfg(test)]
1784mod dep_tests {
1785 use super::*;
1786 use async_trait::async_trait;
1787 use khive_types::Pack;
1788 use serde_json::Value;
1789
1790 struct KgDepPack;
1791 struct MemoryDepPack;
1792 struct ADepPack;
1793 struct BDepPack;
1794
1795 impl Pack for KgDepPack {
1796 const NAME: &'static str = "kg_dep";
1797 const NOTE_KINDS: &'static [&'static str] = &["observation"];
1798 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
1799 const VERBS: &'static [VerbDef] = &[];
1800 }
1801
1802 impl Pack for MemoryDepPack {
1803 const NAME: &'static str = "memory_dep";
1804 const NOTE_KINDS: &'static [&'static str] = &["memory"];
1805 const ENTITY_KINDS: &'static [&'static str] = &[];
1806 const VERBS: &'static [VerbDef] = &[];
1807 const REQUIRES: &'static [&'static str] = &["kg_dep"];
1808 }
1809
1810 impl Pack for ADepPack {
1811 const NAME: &'static str = "pack_a";
1812 const NOTE_KINDS: &'static [&'static str] = &[];
1813 const ENTITY_KINDS: &'static [&'static str] = &[];
1814 const VERBS: &'static [VerbDef] = &[];
1815 const REQUIRES: &'static [&'static str] = &["pack_b"];
1816 }
1817
1818 impl Pack for BDepPack {
1819 const NAME: &'static str = "pack_b";
1820 const NOTE_KINDS: &'static [&'static str] = &[];
1821 const ENTITY_KINDS: &'static [&'static str] = &[];
1822 const VERBS: &'static [VerbDef] = &[];
1823 const REQUIRES: &'static [&'static str] = &["pack_a"];
1824 }
1825
1826 #[async_trait]
1827 impl PackRuntime for KgDepPack {
1828 fn name(&self) -> &str {
1829 Self::NAME
1830 }
1831 fn note_kinds(&self) -> &'static [&'static str] {
1832 Self::NOTE_KINDS
1833 }
1834 fn entity_kinds(&self) -> &'static [&'static str] {
1835 Self::ENTITY_KINDS
1836 }
1837 fn verbs(&self) -> &'static [VerbDef] {
1838 Self::VERBS
1839 }
1840 async fn dispatch(
1841 &self,
1842 verb: &str,
1843 _: Value,
1844 _: &VerbRegistry,
1845 ) -> Result<Value, RuntimeError> {
1846 Err(RuntimeError::InvalidInput(format!(
1847 "KgDepPack has no verbs: {verb}"
1848 )))
1849 }
1850 }
1851
1852 #[async_trait]
1853 impl PackRuntime for MemoryDepPack {
1854 fn name(&self) -> &str {
1855 Self::NAME
1856 }
1857 fn note_kinds(&self) -> &'static [&'static str] {
1858 Self::NOTE_KINDS
1859 }
1860 fn entity_kinds(&self) -> &'static [&'static str] {
1861 Self::ENTITY_KINDS
1862 }
1863 fn verbs(&self) -> &'static [VerbDef] {
1864 Self::VERBS
1865 }
1866 fn requires(&self) -> &'static [&'static str] {
1867 Self::REQUIRES
1868 }
1869 async fn dispatch(
1870 &self,
1871 verb: &str,
1872 _: Value,
1873 _: &VerbRegistry,
1874 ) -> Result<Value, RuntimeError> {
1875 Err(RuntimeError::InvalidInput(format!(
1876 "MemoryDepPack has no verbs: {verb}"
1877 )))
1878 }
1879 }
1880
1881 #[async_trait]
1882 impl PackRuntime for ADepPack {
1883 fn name(&self) -> &str {
1884 Self::NAME
1885 }
1886 fn note_kinds(&self) -> &'static [&'static str] {
1887 Self::NOTE_KINDS
1888 }
1889 fn entity_kinds(&self) -> &'static [&'static str] {
1890 Self::ENTITY_KINDS
1891 }
1892 fn verbs(&self) -> &'static [VerbDef] {
1893 Self::VERBS
1894 }
1895 fn requires(&self) -> &'static [&'static str] {
1896 Self::REQUIRES
1897 }
1898 async fn dispatch(
1899 &self,
1900 verb: &str,
1901 _: Value,
1902 _: &VerbRegistry,
1903 ) -> Result<Value, RuntimeError> {
1904 Err(RuntimeError::InvalidInput(format!(
1905 "ADepPack has no verbs: {verb}"
1906 )))
1907 }
1908 }
1909
1910 #[async_trait]
1911 impl PackRuntime for BDepPack {
1912 fn name(&self) -> &str {
1913 Self::NAME
1914 }
1915 fn note_kinds(&self) -> &'static [&'static str] {
1916 Self::NOTE_KINDS
1917 }
1918 fn entity_kinds(&self) -> &'static [&'static str] {
1919 Self::ENTITY_KINDS
1920 }
1921 fn verbs(&self) -> &'static [VerbDef] {
1922 Self::VERBS
1923 }
1924 fn requires(&self) -> &'static [&'static str] {
1925 Self::REQUIRES
1926 }
1927 async fn dispatch(
1928 &self,
1929 verb: &str,
1930 _: Value,
1931 _: &VerbRegistry,
1932 ) -> Result<Value, RuntimeError> {
1933 Err(RuntimeError::InvalidInput(format!(
1934 "BDepPack has no verbs: {verb}"
1935 )))
1936 }
1937 }
1938
1939 #[test]
1940 fn test_pack_deps_happy_path() {
1941 let mut builder = VerbRegistryBuilder::new();
1942 builder.register(MemoryDepPack);
1943 builder.register(KgDepPack);
1944 let reg = builder
1945 .build()
1946 .expect("kg_dep satisfies memory_dep dependency");
1947 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
1948 let names = reg.pack_names();
1949 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
1950 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
1951 assert!(
1952 kg_pos < mem_pos,
1953 "kg_dep must be loaded before memory_dep; order: {names:?}"
1954 );
1955 }
1956
1957 #[test]
1958 fn test_pack_deps_missing() {
1959 let mut builder = VerbRegistryBuilder::new();
1960 builder.register(MemoryDepPack);
1961 let err = match builder.build() {
1962 Ok(_) => panic!("expected Err, got Ok"),
1963 Err(e) => e,
1964 };
1965 assert!(
1966 matches!(err, RuntimeError::MissingPackDependency(_)),
1967 "expected MissingPackDependency, got {err:?}"
1968 );
1969 let msg = err.to_string();
1970 assert!(
1971 msg.contains("memory_dep"),
1972 "error must name the dependent pack: {msg}"
1973 );
1974 assert!(
1975 msg.contains("kg_dep"),
1976 "error must name the missing dep: {msg}"
1977 );
1978 }
1979
1980 #[test]
1981 fn test_pack_deps_circular() {
1982 let mut builder = VerbRegistryBuilder::new();
1983 builder.register(ADepPack);
1984 builder.register(BDepPack);
1985 let err = match builder.build() {
1986 Ok(_) => panic!("expected Err, got Ok"),
1987 Err(e) => e,
1988 };
1989 assert!(
1990 matches!(err, RuntimeError::CircularPackDependency(_)),
1991 "expected CircularPackDependency, got {err:?}"
1992 );
1993 let msg = err.to_string();
1994 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
1995 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
1996 }
1997
1998 #[test]
1999 fn test_pack_deps_no_deps() {
2000 struct NoDepsA;
2001 struct NoDepsB;
2002
2003 impl Pack for NoDepsA {
2004 const NAME: &'static str = "no_deps_a";
2005 const NOTE_KINDS: &'static [&'static str] = &[];
2006 const ENTITY_KINDS: &'static [&'static str] = &[];
2007 const VERBS: &'static [VerbDef] = &[];
2008 }
2009
2010 impl Pack for NoDepsB {
2011 const NAME: &'static str = "no_deps_b";
2012 const NOTE_KINDS: &'static [&'static str] = &[];
2013 const ENTITY_KINDS: &'static [&'static str] = &[];
2014 const VERBS: &'static [VerbDef] = &[];
2015 }
2016
2017 #[async_trait]
2018 impl PackRuntime for NoDepsA {
2019 fn name(&self) -> &str {
2020 Self::NAME
2021 }
2022 fn note_kinds(&self) -> &'static [&'static str] {
2023 Self::NOTE_KINDS
2024 }
2025 fn entity_kinds(&self) -> &'static [&'static str] {
2026 Self::ENTITY_KINDS
2027 }
2028 fn verbs(&self) -> &'static [VerbDef] {
2029 Self::VERBS
2030 }
2031 async fn dispatch(
2032 &self,
2033 verb: &str,
2034 _: Value,
2035 _: &VerbRegistry,
2036 ) -> Result<Value, RuntimeError> {
2037 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
2038 }
2039 }
2040
2041 #[async_trait]
2042 impl PackRuntime for NoDepsB {
2043 fn name(&self) -> &str {
2044 Self::NAME
2045 }
2046 fn note_kinds(&self) -> &'static [&'static str] {
2047 Self::NOTE_KINDS
2048 }
2049 fn entity_kinds(&self) -> &'static [&'static str] {
2050 Self::ENTITY_KINDS
2051 }
2052 fn verbs(&self) -> &'static [VerbDef] {
2053 Self::VERBS
2054 }
2055 async fn dispatch(
2056 &self,
2057 verb: &str,
2058 _: Value,
2059 _: &VerbRegistry,
2060 ) -> Result<Value, RuntimeError> {
2061 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
2062 }
2063 }
2064
2065 let mut builder = VerbRegistryBuilder::new();
2066 builder.register(NoDepsA);
2067 builder.register(NoDepsB);
2068 let reg = builder.build().expect("packs with REQUIRES=&[] build");
2069 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
2070 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
2071 }
2072}