1use serde::{Deserialize, Serialize};
10
11#[non_exhaustive]
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum GateDecision {
15 Permit {
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 token: Option<Vec<u8>>,
20 },
21 Defer {
23 reason: String,
25 },
26 Deny {
28 reason: String,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 receipt: Option<Vec<u8>>,
33 },
34}
35
36impl GateDecision {
37 pub fn is_permit(&self) -> bool {
39 matches!(self, GateDecision::Permit { .. })
40 }
41
42 pub fn is_deny(&self) -> bool {
44 matches!(self, GateDecision::Deny { .. })
45 }
46}
47
48pub trait GateBackend: Send + Sync {
56 fn check(
66 &self,
67 agent_id: &str,
68 action: &str,
69 context: &serde_json::Value,
70 ) -> GateDecision;
71}
72
73pub struct CapabilityGate {
78 process_table: std::sync::Arc<crate::process::ProcessTable>,
79}
80
81impl CapabilityGate {
82 pub fn new(process_table: std::sync::Arc<crate::process::ProcessTable>) -> Self {
84 Self { process_table }
85 }
86}
87
88impl GateBackend for CapabilityGate {
89 fn check(
90 &self,
91 _agent_id: &str,
92 action: &str,
93 context: &serde_json::Value,
94 ) -> GateDecision {
95 let pid = context
97 .get("pid")
98 .and_then(|v| v.as_u64())
99 .unwrap_or(0);
100
101 let checker = crate::capability::CapabilityChecker::new(
102 std::sync::Arc::clone(&self.process_table),
103 );
104
105 let result = if action.starts_with("tool.") {
107 let tool_name = action.strip_prefix("tool.").unwrap_or(action);
108 checker.check_tool_access(pid, tool_name, None, None)
109 } else if action.starts_with("ipc.") {
110 let target_pid = context
111 .get("target_pid")
112 .and_then(|v| v.as_u64())
113 .unwrap_or(0);
114 checker.check_ipc_target(pid, target_pid)
115 } else if action.starts_with("service.") {
116 let service_name = action.strip_prefix("service.").unwrap_or(action);
117 checker.check_service_access(pid, service_name, None)
118 } else {
119 return GateDecision::Permit { token: None };
121 };
122
123 match result {
124 Ok(()) => GateDecision::Permit { token: None },
125 Err(e) => GateDecision::Deny {
126 reason: e.to_string(),
127 receipt: None,
128 },
129 }
130 }
131}
132
133#[cfg(feature = "tilezero")]
138pub use tilezero_gate::TileZeroGate;
139
140#[cfg(feature = "tilezero")]
141mod tilezero_gate {
142 use super::{GateBackend, GateDecision};
143 use std::sync::Arc;
144
145 use cognitum_gate_tilezero::{
146 ActionContext, ActionMetadata, ActionTarget,
147 GateDecision as TzDecision, TileZero,
148 };
149
150 pub struct TileZeroGate {
156 tilezero: Arc<TileZero>,
157 chain: Option<Arc<crate::chain::ChainManager>>,
158 }
159
160 impl TileZeroGate {
161 pub fn new(
170 tilezero: Arc<TileZero>,
171 chain: Option<Arc<crate::chain::ChainManager>>,
172 ) -> Self {
173 Self { tilezero, chain }
174 }
175
176 #[cfg(test)]
178 pub(crate) fn chain(&self) -> Option<&Arc<crate::chain::ChainManager>> {
179 self.chain.as_ref()
180 }
181
182 pub(crate) fn build_action_context(
184 agent_id: &str,
185 action: &str,
186 context: &serde_json::Value,
187 ) -> ActionContext {
188 ActionContext {
189 action_id: uuid::Uuid::new_v4().to_string(),
190 action_type: action.to_owned(),
191 target: ActionTarget {
192 device: context
193 .get("device")
194 .and_then(|v| v.as_str())
195 .map(String::from),
196 path: context
197 .get("path")
198 .and_then(|v| v.as_str())
199 .map(String::from),
200 extra: Default::default(),
201 },
202 context: ActionMetadata {
203 agent_id: agent_id.to_owned(),
204 session_id: context
205 .get("session_id")
206 .and_then(|v| v.as_str())
207 .map(String::from),
208 prior_actions: Vec::new(),
209 urgency: context
210 .get("urgency")
211 .and_then(|v| v.as_str())
212 .unwrap_or("normal")
213 .to_owned(),
214 },
215 }
216 }
217 }
218
219 impl GateBackend for TileZeroGate {
220 fn check(
221 &self,
222 agent_id: &str,
223 action: &str,
224 context: &serde_json::Value,
225 ) -> GateDecision {
226 let action_ctx = Self::build_action_context(agent_id, action, context);
227
228 let token = tokio::task::block_in_place(|| {
231 tokio::runtime::Handle::current()
232 .block_on(self.tilezero.decide(&action_ctx))
233 });
234
235 let token_bytes = serde_json::to_vec(&token).ok();
237
238 let decision = match token.decision {
240 TzDecision::Permit => GateDecision::Permit {
241 token: token_bytes,
242 },
243 TzDecision::Defer => GateDecision::Defer {
244 reason: format!(
245 "TileZero deferred: coherence uncertain (seq={})",
246 token.sequence,
247 ),
248 },
249 TzDecision::Deny => GateDecision::Deny {
250 reason: format!(
251 "TileZero denied: coherence below threshold (seq={})",
252 token.sequence,
253 ),
254 receipt: token_bytes,
255 },
256 };
257
258 if let Some(ref cm) = self.chain {
260 let event_kind = match &decision {
261 GateDecision::Permit { .. } => "gate.permit",
262 GateDecision::Defer { .. } => "gate.defer",
263 GateDecision::Deny { .. } => "gate.deny",
264 };
265 cm.append(
266 "gate",
267 event_kind,
268 Some(serde_json::json!({
269 "agent_id": agent_id,
270 "action": action,
271 "sequence": token.sequence,
272 "witness_hash": token.witness_hash.iter()
273 .map(|b| format!("{b:02x}"))
274 .collect::<String>(),
275 })),
276 );
277 }
278
279 decision
280 }
281 }
282}
283
284pub struct GovernanceGate {
294 engine: crate::governance::GovernanceEngine,
295 chain: Option<std::sync::Arc<crate::chain::ChainManager>>,
296}
297
298impl GovernanceGate {
299 pub fn new(risk_threshold: f64, human_approval: bool) -> Self {
301 Self {
302 engine: crate::governance::GovernanceEngine::new(risk_threshold, human_approval),
303 chain: None,
304 }
305 }
306
307 pub fn open() -> Self {
309 Self {
310 engine: crate::governance::GovernanceEngine::open(),
311 chain: None,
312 }
313 }
314
315 pub fn with_chain(mut self, cm: std::sync::Arc<crate::chain::ChainManager>) -> Self {
317 self.chain = Some(cm);
318 self
319 }
320
321 pub fn add_rule(mut self, rule: crate::governance::GovernanceRule) -> Self {
323 self.engine.add_rule(rule);
324 self
325 }
326
327 pub fn engine(&self) -> &crate::governance::GovernanceEngine {
329 &self.engine
330 }
331
332 pub fn verify_governance_genesis(&self) -> Option<u64> {
337 let cm = self.chain.as_ref()?;
338 let events = cm.tail(0); events
340 .iter()
341 .find(|e| e.kind == "governance.genesis")
342 .and_then(|e| {
343 e.payload
344 .as_ref()
345 .and_then(|p| p.get("genesis_seq"))
346 .and_then(|v| v.as_u64())
347 })
348 }
349
350 fn extract_effect(context: &serde_json::Value) -> crate::governance::EffectVector {
355 context
356 .get("effect")
357 .and_then(|v| serde_json::from_value::<crate::governance::EffectVector>(v.clone()).ok())
358 .unwrap_or_default()
359 }
360
361 fn extract_context(context: &serde_json::Value) -> std::collections::HashMap<String, String> {
363 let mut map = std::collections::HashMap::new();
364 if let Some(obj) = context.as_object() {
365 for (k, v) in obj {
366 if k == "effect" {
367 continue; }
369 if let Some(s) = v.as_str() {
370 map.insert(k.clone(), s.to_owned());
371 } else {
372 map.insert(k.clone(), v.to_string());
373 }
374 }
375 }
376 map
377 }
378}
379
380impl GateBackend for GovernanceGate {
381 fn check(
382 &self,
383 agent_id: &str,
384 action: &str,
385 context: &serde_json::Value,
386 ) -> GateDecision {
387 let effect = Self::extract_effect(context);
388 let ctx_map = Self::extract_context(context);
389
390 let request = crate::governance::GovernanceRequest {
391 agent_id: agent_id.to_owned(),
392 action: action.to_owned(),
393 effect,
394 context: ctx_map,
395 node_id: None,
396 };
397
398 let result = self.engine.evaluate(&request);
399
400 let decision = match &result.decision {
401 crate::governance::GovernanceDecision::Permit => GateDecision::Permit { token: None },
402 crate::governance::GovernanceDecision::PermitWithWarning(_) => {
403 GateDecision::Permit { token: None }
404 }
405 crate::governance::GovernanceDecision::EscalateToHuman(reason) => {
406 GateDecision::Defer {
407 reason: reason.clone(),
408 }
409 }
410 crate::governance::GovernanceDecision::Deny(reason) => GateDecision::Deny {
411 reason: reason.clone(),
412 receipt: None,
413 },
414 };
415
416 if let Some(ref cm) = self.chain {
418 let (event_kind, extra) = match &result.decision {
419 crate::governance::GovernanceDecision::Permit => {
420 ("governance.permit", serde_json::json!({}))
421 }
422 crate::governance::GovernanceDecision::PermitWithWarning(w) => {
423 ("governance.warn", serde_json::json!({"warning": w}))
424 }
425 crate::governance::GovernanceDecision::EscalateToHuman(r) => {
426 ("governance.defer", serde_json::json!({"reason": r}))
427 }
428 crate::governance::GovernanceDecision::Deny(r) => {
429 ("governance.deny", serde_json::json!({"reason": r}))
430 }
431 };
432
433 let mut payload = serde_json::json!({
434 "agent_id": agent_id,
435 "action": action,
436 "effect": {
437 "risk": request.effect.risk,
438 "fairness": request.effect.fairness,
439 "privacy": request.effect.privacy,
440 "novelty": request.effect.novelty,
441 "security": request.effect.security,
442 },
443 "threshold_exceeded": result.threshold_exceeded,
444 "evaluated_rules": result.evaluated_rules,
445 });
446
447 if let Some(obj) = payload.as_object_mut()
448 && let Some(extra_obj) = extra.as_object()
449 {
450 for (k, v) in extra_obj {
451 obj.insert(k.clone(), v.clone());
452 }
453 }
454
455 cm.append("governance", event_kind, Some(payload));
456 }
457
458 decision
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::capability::AgentCapabilities;
466 use crate::process::{ProcessEntry, ProcessState, ProcessTable, ResourceUsage};
467 use std::sync::Arc;
468 use tokio_util::sync::CancellationToken;
469
470 fn make_gate_with_agent(caps: AgentCapabilities) -> (CapabilityGate, u64) {
471 let table = Arc::new(ProcessTable::new(16));
472 let entry = ProcessEntry {
473 pid: 0,
474 agent_id: "test-agent".to_owned(),
475 state: ProcessState::Running,
476 capabilities: caps,
477 resource_usage: ResourceUsage::default(),
478 cancel_token: CancellationToken::new(),
479 parent_pid: None,
480 };
481 let pid = table.insert(entry).unwrap();
482 (CapabilityGate::new(table), pid)
483 }
484
485 #[test]
486 fn capability_gate_permits_default() {
487 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
488 let ctx = serde_json::json!({"pid": pid});
489 let decision = gate.check("test-agent", "tool.read_file", &ctx);
490 assert!(decision.is_permit());
491 }
492
493 #[test]
494 fn capability_gate_denies_no_tools() {
495 let caps = AgentCapabilities {
496 can_exec_tools: false,
497 ..Default::default()
498 };
499 let (gate, pid) = make_gate_with_agent(caps);
500 let ctx = serde_json::json!({"pid": pid});
501 let decision = gate.check("test-agent", "tool.read_file", &ctx);
502 assert!(decision.is_deny());
503 }
504
505 #[test]
506 fn capability_gate_denies_ipc_disabled() {
507 let caps = AgentCapabilities {
508 can_ipc: false,
509 ..Default::default()
510 };
511 let (gate, pid) = make_gate_with_agent(caps);
512 let ctx = serde_json::json!({"pid": pid, "target_pid": 999});
513 let decision = gate.check("test-agent", "ipc.send", &ctx);
514 assert!(decision.is_deny());
515 }
516
517 #[test]
518 fn capability_gate_unknown_action_permits() {
519 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
520 let ctx = serde_json::json!({"pid": pid});
521 let decision = gate.check("test-agent", "custom.action", &ctx);
522 assert!(decision.is_permit());
523 }
524
525 #[test]
526 fn gate_decision_serde_roundtrip() {
527 let decisions = vec![
528 GateDecision::Permit { token: Some(vec![1, 2, 3]) },
529 GateDecision::Defer { reason: "need review".into() },
530 GateDecision::Deny { reason: "denied".into(), receipt: None },
531 ];
532 for d in decisions {
533 let json = serde_json::to_string(&d).unwrap();
534 let _: GateDecision = serde_json::from_str(&json).unwrap();
535 }
536 }
537
538 use crate::governance::{GovernanceBranch, GovernanceRule, RuleSeverity};
541
542 #[test]
543 fn governance_gate_permits_low_risk() {
544 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
545 id: "security-check".into(),
546 description: "Block high-risk actions".into(),
547 branch: GovernanceBranch::Judicial,
548 severity: RuleSeverity::Blocking,
549 active: true,
550 reference_url: None,
551 sop_category: None,
552 });
553
554 let ctx = serde_json::json!({
555 "effect": { "risk": 0.1, "security": 0.05 }
556 });
557 let decision = gate.check("agent-1", "tool.read_file", &ctx);
558 assert!(decision.is_permit());
559 }
560
561 #[test]
562 fn governance_gate_denies_high_risk() {
563 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
564 id: "security-check".into(),
565 description: "Block high-risk actions".into(),
566 branch: GovernanceBranch::Judicial,
567 severity: RuleSeverity::Blocking,
568 active: true,
569 reference_url: None,
570 sop_category: None,
571 });
572
573 let ctx = serde_json::json!({
574 "effect": { "risk": 0.8, "security": 0.6 }
575 });
576 let decision = gate.check("agent-1", "tool.exec", &ctx);
577 assert!(decision.is_deny());
578 }
579
580 #[test]
581 fn governance_gate_defers_with_human_approval() {
582 let gate = GovernanceGate::new(0.5, true).add_rule(GovernanceRule {
583 id: "security-check".into(),
584 description: "Block high-risk actions".into(),
585 branch: GovernanceBranch::Judicial,
586 severity: RuleSeverity::Blocking,
587 active: true,
588 reference_url: None,
589 sop_category: None,
590 });
591
592 let ctx = serde_json::json!({
593 "effect": { "risk": 0.8 }
594 });
595 let decision = gate.check("agent-1", "tool.exec", &ctx);
596 assert!(matches!(decision, GateDecision::Defer { .. }));
597 }
598
599 #[test]
600 fn governance_gate_warns_on_threshold() {
601 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
602 id: "risk-check".into(),
603 description: "Warn on risky actions".into(),
604 branch: GovernanceBranch::Executive,
605 severity: RuleSeverity::Warning,
606 active: true,
607 reference_url: None,
608 sop_category: None,
609 });
610
611 let ctx = serde_json::json!({
612 "effect": { "risk": 0.8 }
613 });
614 let decision = gate.check("agent-1", "tool.deploy", &ctx);
616 assert!(decision.is_permit());
617 }
618
619 #[test]
620 fn governance_gate_logs_to_chain() {
621 let cm = Arc::new(crate::chain::ChainManager::new(0, 10));
622 let initial_len = cm.len();
623
624 let gate = GovernanceGate::new(0.5, false)
625 .with_chain(cm.clone())
626 .add_rule(GovernanceRule {
627 id: "sec".into(),
628 description: "test".into(),
629 branch: GovernanceBranch::Judicial,
630 severity: RuleSeverity::Blocking,
631 active: true,
632 reference_url: None,
633 sop_category: None,
634 });
635
636 let ctx = serde_json::json!({"effect": {"risk": 0.1}});
638 gate.check("agent-1", "tool.read", &ctx);
639 assert_eq!(cm.len(), initial_len + 1);
640
641 let events = cm.tail(1);
642 assert_eq!(events[0].kind, "governance.permit");
643 assert_eq!(events[0].source, "governance");
644
645 let ctx = serde_json::json!({"effect": {"risk": 0.9}});
647 gate.check("agent-1", "tool.exec", &ctx);
648 let events = cm.tail(1);
649 assert_eq!(events[0].kind, "governance.deny");
650
651 let payload = events[0].payload.as_ref().unwrap();
652 assert_eq!(payload["agent_id"], "agent-1");
653 assert_eq!(payload["action"], "tool.exec");
654 assert!(payload["threshold_exceeded"].as_bool().unwrap());
655 }
656
657 #[test]
658 fn governance_gate_open_permits_all() {
659 let gate = GovernanceGate::open();
660 let ctx = serde_json::json!({
661 "effect": { "risk": 0.99, "security": 0.99 }
662 });
663 let decision = gate.check("agent-1", "tool.dangerous", &ctx);
664 assert!(decision.is_permit());
665 }
666
667 #[test]
668 fn governance_gate_extracts_effect_from_context() {
669 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
670 id: "sec".into(),
671 description: "test".into(),
672 branch: GovernanceBranch::Judicial,
673 severity: RuleSeverity::Blocking,
674 active: true,
675 reference_url: None,
676 sop_category: None,
677 });
678
679 let ctx = serde_json::json!({
681 "pid": 1,
682 "effect": {
683 "risk": 0.7,
684 "fairness": 0.0,
685 "privacy": 0.3,
686 "novelty": 0.0,
687 "security": 0.0
688 }
689 });
690 let decision = gate.check("agent-1", "tool.exec", &ctx);
691 assert!(decision.is_deny());
693
694 let ctx_no_effect = serde_json::json!({"pid": 1});
696 let decision = gate.check("agent-1", "tool.exec", &ctx_no_effect);
697 assert!(decision.is_permit());
698 }
699
700 #[test]
703 fn replay_attack_same_context_twice() {
704 let cm = Arc::new(crate::chain::ChainManager::new(0, 10));
708 let gate = GovernanceGate::new(0.5, false)
709 .with_chain(cm.clone())
710 .add_rule(GovernanceRule {
711 id: "sec".into(),
712 description: "test".into(),
713 branch: GovernanceBranch::Judicial,
714 severity: RuleSeverity::Blocking,
715 active: true,
716 reference_url: None,
717 sop_category: None,
718 });
719
720 let ctx = serde_json::json!({"effect": {"risk": 0.1}});
721
722 let d1 = gate.check("agent-1", "tool.read", &ctx);
723 let initial_len = cm.len();
724 let d2 = gate.check("agent-1", "tool.read", &ctx);
725
726 assert!(d1.is_permit());
728 assert!(d2.is_permit());
729
730 assert_eq!(cm.len(), initial_len + 1);
732 }
733
734 #[test]
735 fn replay_attack_chain_records_each_invocation() {
736 let cm = Arc::new(crate::chain::ChainManager::new(0, 10));
737 let gate = GovernanceGate::new(0.5, false)
738 .with_chain(cm.clone())
739 .add_rule(GovernanceRule {
740 id: "sec".into(),
741 description: "block risky".into(),
742 branch: GovernanceBranch::Judicial,
743 severity: RuleSeverity::Blocking,
744 active: true,
745 reference_url: None,
746 sop_category: None,
747 });
748
749 let ctx = serde_json::json!({"effect": {"risk": 0.9}});
750 let before = cm.len();
751 gate.check("agent-1", "tool.exec", &ctx);
752 gate.check("agent-1", "tool.exec", &ctx);
753 gate.check("agent-1", "tool.exec", &ctx);
754 assert_eq!(cm.len(), before + 3);
756 }
757
758 #[test]
759 fn invalid_capability_empty_action() {
760 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
761 let ctx = serde_json::json!({"pid": pid});
762 let decision = gate.check("test-agent", "", &ctx);
764 assert!(decision.is_permit());
765 }
766
767 #[test]
768 fn invalid_capability_very_long_action() {
769 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
770 let ctx = serde_json::json!({"pid": pid});
771 let long_action = "tool.".to_owned() + &"x".repeat(10_000);
772 let decision = gate.check("test-agent", &long_action, &ctx);
773 assert!(decision.is_permit());
775 }
776
777 #[test]
778 fn invalid_capability_special_characters() {
779 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
780 let ctx = serde_json::json!({"pid": pid});
781 let decision = gate.check("test-agent", "tool.\0\x01\u{FEFF}", &ctx);
783 assert!(decision.is_permit());
784 }
785
786 #[test]
787 fn invalid_capability_action_with_path_traversal() {
788 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
789 let ctx = serde_json::json!({"pid": pid});
790 let decision = gate.check("test-agent", "tool.../../etc/passwd", &ctx);
791 assert!(decision.is_permit());
793 }
794
795 #[test]
796 fn permission_escalation_no_tool_access() {
797 let caps = AgentCapabilities {
798 can_exec_tools: false,
799 can_ipc: false,
800 can_spawn: false,
801 ..Default::default()
802 };
803 let (gate, pid) = make_gate_with_agent(caps);
804 let ctx = serde_json::json!({"pid": pid});
805
806 assert!(gate.check("agent", "tool.shell_exec", &ctx).is_deny());
808 assert!(gate.check("agent", "tool.read_file", &ctx).is_deny());
809 assert!(gate.check("agent", "tool.write_file", &ctx).is_deny());
810
811 let ipc_ctx = serde_json::json!({"pid": pid, "target_pid": 999});
813 assert!(gate.check("agent", "ipc.send", &ipc_ctx).is_deny());
814 }
815
816 #[test]
817 fn permission_escalation_service_access_denied() {
818 let (gate, pid) = make_gate_with_agent(AgentCapabilities::default());
821 let ctx = serde_json::json!({"pid": pid});
822 let decision = gate.check("agent", "service.nonexistent_service", &ctx);
824 assert!(decision.is_permit() || decision.is_deny());
826 }
827
828 #[test]
829 fn governance_gate_missing_pid_defaults_to_zero() {
830 let (gate, _pid) = make_gate_with_agent(AgentCapabilities::default());
831 let ctx = serde_json::json!({});
833 let decision = gate.check("test-agent", "tool.read", &ctx);
834 let _ = decision;
837 }
838
839 #[test]
840 fn governance_gate_concurrent_checks() {
841 let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
842 let gate = Arc::new(
843 GovernanceGate::new(0.5, false)
844 .with_chain(cm.clone())
845 .add_rule(GovernanceRule {
846 id: "sec".into(),
847 description: "test".into(),
848 branch: GovernanceBranch::Judicial,
849 severity: RuleSeverity::Blocking,
850 active: true,
851 reference_url: None,
852 sop_category: None,
853 }),
854 );
855
856 let before = cm.len();
857
858 std::thread::scope(|s| {
859 for i in 0..10 {
860 let gate = Arc::clone(&gate);
861 s.spawn(move || {
862 let ctx = serde_json::json!({"effect": {"risk": 0.1 * (i as f64)}});
863 gate.check(&format!("agent-{i}"), "tool.check", &ctx);
864 });
865 }
866 });
867
868 assert_eq!(cm.len(), before + 10);
870 }
871
872 #[test]
873 fn governance_gate_risk_boundary_at_threshold() {
874 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
876 id: "sec".into(),
877 description: "boundary test".into(),
878 branch: GovernanceBranch::Judicial,
879 severity: RuleSeverity::Blocking,
880 active: true,
881 reference_url: None,
882 sop_category: None,
883 });
884
885 let ctx = serde_json::json!({"effect": {"risk": 0.5}});
887 let decision = gate.check("agent", "tool.exec", &ctx);
888 assert!(decision.is_permit());
890
891 let ctx_above = serde_json::json!({"effect": {"risk": 0.51}});
893 let decision_above = gate.check("agent", "tool.exec", &ctx_above);
894 assert!(decision_above.is_deny());
895 }
896
897 #[test]
898 fn gate_decision_deny_reason_preserved() {
899 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
900 id: "sec".into(),
901 description: "test deny reason".into(),
902 branch: GovernanceBranch::Judicial,
903 severity: RuleSeverity::Blocking,
904 active: true,
905 reference_url: None,
906 sop_category: None,
907 });
908
909 let ctx = serde_json::json!({"effect": {"risk": 0.9}});
910 let decision = gate.check("agent-1", "tool.danger", &ctx);
911 match decision {
912 GateDecision::Deny { reason, .. } => {
913 assert!(!reason.is_empty(), "deny reason should not be empty");
914 }
915 _ => panic!("expected deny decision for high-risk action"),
916 }
917 }
918
919 #[test]
920 fn gate_decision_defer_reason_preserved() {
921 let gate = GovernanceGate::new(0.5, true).add_rule(GovernanceRule {
922 id: "sec".into(),
923 description: "escalate test".into(),
924 branch: GovernanceBranch::Judicial,
925 severity: RuleSeverity::Blocking,
926 active: true,
927 reference_url: None,
928 sop_category: None,
929 });
930
931 let ctx = serde_json::json!({"effect": {"risk": 0.9}});
932 let decision = gate.check("agent-1", "tool.danger", &ctx);
933 match decision {
934 GateDecision::Defer { reason } => {
935 assert!(!reason.is_empty(), "defer reason should not be empty");
936 }
937 _ => panic!("expected defer decision for high-risk action with human approval"),
938 }
939 }
940
941 #[test]
942 fn governance_gate_inactive_rule_ignored() {
943 let gate = GovernanceGate::new(0.5, false).add_rule(GovernanceRule {
944 id: "inactive-rule".into(),
945 description: "this rule is inactive".into(),
946 branch: GovernanceBranch::Judicial,
947 severity: RuleSeverity::Blocking,
948 active: false,
949 reference_url: None,
950 sop_category: None,
951 });
952
953 let ctx = serde_json::json!({"effect": {"risk": 0.9}});
954 let decision = gate.check("agent-1", "tool.danger", &ctx);
955 let _ = decision;
958 }
959
960 #[test]
961 fn governance_gate_multiple_rules_evaluated() {
962 let gate = GovernanceGate::new(0.5, false)
963 .add_rule(GovernanceRule {
964 id: "rule-1".into(),
965 description: "first".into(),
966 branch: GovernanceBranch::Judicial,
967 severity: RuleSeverity::Blocking,
968 active: true,
969 reference_url: None,
970 sop_category: None,
971 })
972 .add_rule(GovernanceRule {
973 id: "rule-2".into(),
974 description: "second".into(),
975 branch: GovernanceBranch::Executive,
976 severity: RuleSeverity::Warning,
977 active: true,
978 reference_url: None,
979 sop_category: None,
980 });
981
982 let ctx = serde_json::json!({"effect": {"risk": 0.9}});
983 let decision = gate.check("agent-1", "tool.exec", &ctx);
984 assert!(decision.is_deny());
985 }
986
987}
988
989#[cfg(all(test, feature = "tilezero"))]
990mod tilezero_tests {
991 use super::*;
992 use std::sync::Arc;
993
994 fn make_tilezero_gate() -> TileZeroGate {
995 let thresholds = cognitum_gate_tilezero::GateThresholds::default();
996 let tz = Arc::new(cognitum_gate_tilezero::TileZero::new(thresholds));
997 TileZeroGate::new(tz, None)
998 }
999
1000 fn make_tilezero_gate_with_chain() -> TileZeroGate {
1001 let thresholds = cognitum_gate_tilezero::GateThresholds::default();
1002 let tz = Arc::new(cognitum_gate_tilezero::TileZero::new(thresholds));
1003 let cm = Arc::new(crate::chain::ChainManager::new(0, 10));
1004 TileZeroGate::new(tz, Some(cm))
1005 }
1006
1007 #[tokio::test(flavor = "multi_thread")]
1008 async fn tilezero_gate_returns_decision() {
1009 let gate = make_tilezero_gate();
1010 let ctx = serde_json::json!({"pid": 1});
1011 let decision = gate.check("test-agent", "tool.read_file", &ctx);
1012 assert!(
1015 decision.is_permit()
1016 || decision.is_deny()
1017 || matches!(decision, GateDecision::Defer { .. })
1018 );
1019 }
1020
1021 #[tokio::test(flavor = "multi_thread")]
1022 async fn tilezero_gate_includes_token_bytes() {
1023 let gate = make_tilezero_gate();
1024 let ctx = serde_json::json!({});
1025 let decision = gate.check("agent-1", "tool.search", &ctx);
1026
1027 match &decision {
1028 GateDecision::Permit { token } => {
1029 assert!(token.is_some());
1031 let bytes = token.as_ref().unwrap();
1032 let pt: cognitum_gate_tilezero::PermitToken =
1034 serde_json::from_slice(bytes).unwrap();
1035 assert_eq!(pt.sequence, 0);
1036 }
1037 GateDecision::Deny { receipt, .. } => {
1038 assert!(receipt.is_some());
1040 }
1041 GateDecision::Defer { .. } => {
1042 }
1044 }
1045 }
1046
1047 #[tokio::test(flavor = "multi_thread")]
1048 async fn tilezero_gate_logs_to_chain() {
1049 let gate = make_tilezero_gate_with_chain();
1050 let ctx = serde_json::json!({"urgency": "high"});
1051 let _decision = gate.check("agent-1", "tool.deploy", &ctx);
1052
1053 let chain = gate.chain().unwrap();
1055 let seq = chain.sequence();
1056 assert!(seq >= 1, "expected chain event, got seq={seq}");
1058 }
1059
1060 #[tokio::test(flavor = "multi_thread")]
1061 async fn tilezero_gate_sequential_decisions() {
1062 let gate = make_tilezero_gate();
1063 let ctx = serde_json::json!({});
1064
1065 let d1 = gate.check("agent-1", "tool.a", &ctx);
1067 let d2 = gate.check("agent-1", "tool.b", &ctx);
1068
1069 let is_valid = |d: &GateDecision| {
1071 d.is_permit() || d.is_deny() || matches!(d, GateDecision::Defer { .. })
1072 };
1073 assert!(is_valid(&d1));
1074 assert!(is_valid(&d2));
1075 }
1076
1077 #[tokio::test(flavor = "multi_thread")]
1078 async fn tilezero_gate_action_context_mapping() {
1079 let ctx = serde_json::json!({
1081 "device": "router-1",
1082 "path": "/config/acl",
1083 "session_id": "sess-42",
1084 "urgency": "critical",
1085 });
1086
1087 let action_ctx =
1088 tilezero_gate::TileZeroGate::build_action_context("agent-x", "tool.deploy", &ctx);
1089
1090 assert_eq!(action_ctx.action_type, "tool.deploy");
1091 assert_eq!(action_ctx.context.agent_id, "agent-x");
1092 assert_eq!(action_ctx.context.urgency, "critical");
1093 assert_eq!(action_ctx.context.session_id, Some("sess-42".into()));
1094 assert_eq!(action_ctx.target.device, Some("router-1".into()));
1095 assert_eq!(action_ctx.target.path, Some("/config/acl".into()));
1096 }
1097}