1use std::collections::HashMap;
19
20use serde::{Deserialize, Serialize};
21use tracing::debug;
22
23use crate::environment::{Environment, EnvironmentClass};
24
25#[non_exhaustive]
31#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum GovernanceRuleType {
34 #[default]
36 General,
37 BrowserPolicy,
39 BindingThread,
46}
47
48impl std::fmt::Display for GovernanceRuleType {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 GovernanceRuleType::General => write!(f, "general"),
52 GovernanceRuleType::BrowserPolicy => write!(f, "browser_policy"),
53 GovernanceRuleType::BindingThread => write!(f, "binding_thread"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct GovernanceRule {
61 pub id: String,
63
64 pub description: String,
66
67 pub branch: GovernanceBranch,
69
70 pub severity: RuleSeverity,
72
73 #[serde(default = "default_true")]
75 pub active: bool,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub reference_url: Option<String>,
80
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub sop_category: Option<String>,
84
85 #[serde(default)]
89 pub rule_type: GovernanceRuleType,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub action_selector: Option<String>,
99
100 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub tool_selector: Option<String>,
107
108 #[serde(default)]
116 pub force_on_match: bool,
117}
118
119pub const OVERLAY_DENY_TAG: &str = "project-overlay:deny";
123
124pub const OVERLAY_APPROVAL_TAG: &str = "project-overlay:approval";
128
129impl GovernanceRule {
130 pub fn filter_by_category<'a>(
132 rules: &'a [GovernanceRule],
133 category: &str,
134 ) -> Vec<&'a GovernanceRule> {
135 rules
136 .iter()
137 .filter(|r| r.sop_category.as_deref() == Some(category))
138 .collect()
139 }
140
141 pub fn filter_by_type<'a>(
143 rules: &'a [GovernanceRule],
144 rule_type: &GovernanceRuleType,
145 ) -> Vec<&'a GovernanceRule> {
146 rules
147 .iter()
148 .filter(|r| &r.rule_type == rule_type)
149 .collect()
150 }
151
152 pub fn selectors_match(&self, request: &GovernanceRequest) -> bool {
159 if let Some(sel) = self.action_selector.as_deref()
160 && !selector_matches(sel, &request.action)
161 {
162 return false;
163 }
164 if let Some(sel) = self.tool_selector.as_deref() {
165 let tool = request
166 .context
167 .get("tool")
168 .or_else(|| request.context.get("gate_action"))
169 .map(String::as_str)
170 .unwrap_or("");
171 if !selector_matches(sel, tool) {
172 return false;
173 }
174 }
175 true
176 }
177
178 pub fn browser_policy(id: impl Into<String>, description: impl Into<String>) -> Self {
180 Self {
181 id: id.into(),
182 description: description.into(),
183 branch: GovernanceBranch::Legislative,
184 severity: RuleSeverity::Blocking,
185 active: true,
186 reference_url: None,
187 sop_category: Some("browser_policy".into()),
188 rule_type: GovernanceRuleType::BrowserPolicy,
189 action_selector: None,
190 tool_selector: None,
191 force_on_match: false,
192 }
193 }
194
195 pub fn binding_thread(description: impl Into<String>) -> Self {
200 Self {
201 id: BINDING_THREAD_RULE_ID.into(),
202 description: description.into(),
203 branch: GovernanceBranch::Legislative,
204 severity: RuleSeverity::Blocking,
205 active: true,
206 reference_url: None,
207 sop_category: Some("ethics".into()),
208 rule_type: GovernanceRuleType::BindingThread,
209 action_selector: None,
210 tool_selector: None,
211 force_on_match: false,
212 }
213 }
214
215 pub fn spawn_requires_approval() -> Self {
224 Self {
225 id: "SPAWN-APPROVAL".into(),
226 description: "Agent spawn requires in-conversation user approval (Defer + grant)"
227 .into(),
228 branch: GovernanceBranch::Legislative,
229 severity: RuleSeverity::Blocking,
230 active: true,
231 reference_url: None,
232 sop_category: Some("multi_agent".into()),
233 rule_type: GovernanceRuleType::General,
234 action_selector: Some("tool.agent_spawn".into()),
235 tool_selector: Some("agent_spawn".into()),
236 force_on_match: true,
237 }
238 }
239}
240
241pub fn selector_matches(selector: &str, candidate: &str) -> bool {
247 if selector.is_empty() {
248 return true;
249 }
250 if let Some(prefix) = selector.strip_suffix('*') {
251 return candidate.starts_with(prefix);
252 }
253 selector == candidate
254}
255
256pub const BINDING_THREAD_RULE_ID: &str = "soul.binding_thread_intact";
258
259pub const BINDING_THREAD_MISMATCH_REASON: &str = "binding-thread mismatch";
261
262pub fn browser_policy_default_rules() -> Vec<GovernanceRule> {
268 vec![
269 GovernanceRule::browser_policy(
270 "BP-001",
271 "Browser nodes may only publish/subscribe to public topics (public.*)",
272 ),
273 GovernanceRule::browser_policy(
274 "BP-002",
275 "Browser nodes cannot spawn child agents without capability elevation",
276 ),
277 GovernanceRule::browser_policy(
278 "BP-003",
279 "Browser nodes cannot enable network access without capability elevation",
280 ),
281 ]
282}
283
284pub fn binding_thread_default_rules() -> Vec<GovernanceRule> {
290 vec![GovernanceRule::binding_thread(
291 "SOUL.md must contain the compile-time binding-thread excerpt; \
292 mismatch hard-refuses agent turns",
293 )]
294}
295
296fn default_true() -> bool {
297 true
298}
299
300#[non_exhaustive]
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub enum GovernanceBranch {
304 Legislative,
306 Executive,
308 Judicial,
310}
311
312impl std::fmt::Display for GovernanceBranch {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 match self {
315 GovernanceBranch::Legislative => write!(f, "legislative"),
316 GovernanceBranch::Executive => write!(f, "executive"),
317 GovernanceBranch::Judicial => write!(f, "judicial"),
318 }
319 }
320}
321
322#[non_exhaustive]
324#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
325pub enum RuleSeverity {
326 Advisory,
328 Warning,
330 Blocking,
332 Critical,
334}
335
336impl std::fmt::Display for RuleSeverity {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 match self {
339 RuleSeverity::Advisory => write!(f, "advisory"),
340 RuleSeverity::Warning => write!(f, "warning"),
341 RuleSeverity::Blocking => write!(f, "blocking"),
342 RuleSeverity::Critical => write!(f, "critical"),
343 }
344 }
345}
346
347#[derive(Debug, Clone, Default, Serialize, Deserialize)]
353pub struct EffectVector {
354 #[serde(default)]
356 pub risk: f64,
357
358 #[serde(default)]
360 pub fairness: f64,
361
362 #[serde(default)]
364 pub privacy: f64,
365
366 #[serde(default)]
368 pub novelty: f64,
369
370 #[serde(default)]
372 pub security: f64,
373}
374
375impl EffectVector {
376 pub fn magnitude(&self) -> f64 {
383 (self.risk * self.risk
384 + self.fairness * self.fairness
385 + self.privacy * self.privacy
386 + self.novelty * self.novelty
387 + self.security * self.security)
388 .sqrt()
389 }
390
391 #[cfg(feature = "ecc")]
404 pub fn score(&self, model: Option<&crate::eml_kernel::GovernanceScorerModel>) -> f64 {
405 match model {
406 Some(m) => m.predict(
407 self.risk,
408 self.fairness,
409 self.privacy,
410 self.novelty,
411 self.security,
412 ),
413 None => self.magnitude(),
414 }
415 }
416
417 #[cfg(not(feature = "ecc"))]
419 pub fn score(&self) -> f64 {
420 self.magnitude()
421 }
422
423 pub fn any_exceeds(&self, threshold: f64) -> bool {
425 self.risk > threshold
426 || self.fairness > threshold
427 || self.privacy > threshold
428 || self.novelty > threshold
429 || self.security > threshold
430 }
431
432 pub fn max_dimension(&self) -> f64 {
434 [
435 self.risk,
436 self.fairness,
437 self.privacy,
438 self.novelty,
439 self.security,
440 ]
441 .into_iter()
442 .fold(0.0_f64, f64::max)
443 }
444
445 pub fn to_context_json(&self) -> serde_json::Value {
448 serde_json::json!({
449 "risk": self.risk,
450 "fairness": self.fairness,
451 "privacy": self.privacy,
452 "novelty": self.novelty,
453 "security": self.security,
454 })
455 }
456
457 pub fn for_gate(kind: GateEffectKind) -> Self {
462 kind.effect_vector()
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum GateEffectKind {
471 AuthCredentialRegister,
473 AuthCredentialRotate,
475 AuthTokenIssue,
477 AuthTokenRevoke,
479 ConfigSet,
481 ConfigDelete,
483 ConfigSecretSet,
485 A2aRouting,
487 CronAdd,
489 CronRemove,
491}
492
493impl GateEffectKind {
494 pub fn effect_vector(self) -> EffectVector {
496 match self {
497 Self::AuthCredentialRegister => EffectVector {
499 risk: 0.5,
500 fairness: 0.0,
501 privacy: 0.6,
502 novelty: 0.1,
503 security: 0.8,
504 },
505 Self::AuthCredentialRotate => EffectVector {
506 risk: 0.55,
507 fairness: 0.0,
508 privacy: 0.5,
509 novelty: 0.15,
510 security: 0.85,
511 },
512 Self::AuthTokenIssue => EffectVector {
513 risk: 0.4,
514 fairness: 0.0,
515 privacy: 0.4,
516 novelty: 0.05,
517 security: 0.7,
518 },
519 Self::AuthTokenRevoke => EffectVector {
520 risk: 0.45,
521 fairness: 0.1,
522 privacy: 0.2,
523 novelty: 0.05,
524 security: 0.75,
525 },
526 Self::ConfigSet => EffectVector {
528 risk: 0.35,
529 fairness: 0.0,
530 privacy: 0.2,
531 novelty: 0.15,
532 security: 0.4,
533 },
534 Self::ConfigDelete => EffectVector {
535 risk: 0.4,
536 fairness: 0.0,
537 privacy: 0.15,
538 novelty: 0.1,
539 security: 0.45,
540 },
541 Self::ConfigSecretSet => EffectVector {
542 risk: 0.55,
543 fairness: 0.0,
544 privacy: 0.85,
545 novelty: 0.1,
546 security: 0.9,
547 },
548 Self::A2aRouting => EffectVector {
550 risk: 0.25,
551 fairness: 0.0,
552 privacy: 0.1,
553 novelty: 0.05,
554 security: 0.3,
555 },
556 Self::CronAdd => EffectVector {
558 risk: 0.2,
559 fairness: 0.0,
560 privacy: 0.0,
561 novelty: 0.1,
562 security: 0.1,
563 },
564 Self::CronRemove => EffectVector {
565 risk: 0.2,
566 fairness: 0.0,
567 privacy: 0.0,
568 novelty: 0.05,
569 security: 0.1,
570 },
571 }
572 }
573
574 pub fn action_prefix(self) -> &'static str {
576 match self {
577 Self::AuthCredentialRegister
578 | Self::AuthCredentialRotate
579 | Self::AuthTokenIssue
580 | Self::AuthTokenRevoke => "auth.",
581 Self::ConfigSet | Self::ConfigDelete | Self::ConfigSecretSet => "config.",
582 Self::A2aRouting => "ipc.",
583 Self::CronAdd | Self::CronRemove => "cron.",
584 }
585 }
586}
587
588pub fn with_effect_context(mut base: serde_json::Value, effect: &EffectVector) -> serde_json::Value {
590 if let Some(obj) = base.as_object_mut() {
591 obj.insert("effect".into(), effect.to_context_json());
592 }
593 base
594}
595
596#[non_exhaustive]
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
599pub enum GovernanceDecision {
600 Permit,
602 PermitWithWarning(String),
604 EscalateToHuman(String),
606 Deny(String),
608}
609
610impl std::fmt::Display for GovernanceDecision {
611 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612 match self {
613 GovernanceDecision::Permit => write!(f, "permit"),
614 GovernanceDecision::PermitWithWarning(msg) => write!(f, "permit (warning: {msg})"),
615 GovernanceDecision::EscalateToHuman(msg) => write!(f, "escalate ({msg})"),
616 GovernanceDecision::Deny(reason) => write!(f, "deny: {reason}"),
617 }
618 }
619}
620
621#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
627pub struct GatePrincipal {
628 #[serde(default)]
631 pub agent_id: String,
632
633 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub user_id: Option<String>,
636
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub parent_agent_id: Option<String>,
640
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub conv_id: Option<String>,
644
645 #[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")]
652 project_id: Option<String>,
653
654 #[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")]
658 instance_id: Option<String>,
659}
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq)]
663pub enum AttestSource {
664 BoundKernel,
666 TokenScope,
668 UserForward,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq)]
684pub struct ProjectAttestation {
685 project_id: String,
686 source: AttestSource,
687}
688
689impl ProjectAttestation {
690 pub fn from_verified(project_id: impl Into<String>, source: AttestSource) -> Self {
693 Self {
694 project_id: project_id.into(),
695 source,
696 }
697 }
698
699 pub fn project_id(&self) -> &str {
701 &self.project_id
702 }
703
704 pub fn source(&self) -> AttestSource {
706 self.source
707 }
708}
709
710impl GatePrincipal {
711 pub fn agent(agent_id: impl Into<String>) -> Self {
713 Self {
714 agent_id: agent_id.into(),
715 user_id: None,
716 parent_agent_id: None,
717 conv_id: None,
718 project_id: None,
719 instance_id: None,
720 }
721 }
722
723 pub fn with_project(mut self, project: &ProjectAttestation) -> Self {
725 self.project_id = Some(project.project_id().to_owned());
726 self
727 }
728
729 pub(crate) fn without_attribution(mut self) -> Self {
732 self.project_id = None;
733 self.instance_id = None;
734 self
735 }
736
737 pub fn project_id(&self) -> Option<&str> {
739 self.project_id.as_deref()
740 }
741
742 pub fn instance_id(&self) -> Option<&str> {
744 self.instance_id.as_deref()
745 }
746
747 pub fn with_instance(mut self, instance_id: impl Into<String>) -> Self {
749 self.instance_id = Some(instance_id.into());
750 self
751 }
752
753 pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
755 self.user_id = Some(user_id.into());
756 self
757 }
758
759 pub fn with_parent(mut self, parent_agent_id: impl Into<String>) -> Self {
761 self.parent_agent_id = Some(parent_agent_id.into());
762 self
763 }
764
765 pub fn with_conv(mut self, conv_id: impl Into<String>) -> Self {
767 self.conv_id = Some(conv_id.into());
768 self
769 }
770
771 pub fn eval_key(&self) -> &str {
773 if !self.agent_id.is_empty() {
774 self.agent_id.as_str()
775 } else {
776 self.user_id.as_deref().unwrap_or("")
777 }
778 }
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct GovernanceRequest {
784 pub agent_id: String,
786
787 pub action: String,
789
790 #[serde(default)]
792 pub effect: EffectVector,
793
794 #[serde(default)]
796 pub context: std::collections::HashMap<String, String>,
797
798 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub node_id: Option<String>,
801
802 #[serde(default, skip_serializing_if = "Option::is_none")]
806 pub principal: Option<GatePrincipal>,
807}
808
809impl GovernanceRequest {
810 pub fn new(agent_id: impl Into<String>, action: impl Into<String>) -> Self {
812 let agent_id = agent_id.into();
813 Self {
814 agent_id: agent_id.clone(),
815 action: action.into(),
816 effect: EffectVector::default(),
817 context: std::collections::HashMap::new(),
818 node_id: None,
819 principal: Some(GatePrincipal::agent(agent_id)),
820 }
821 .attributed()
822 }
823
824 pub fn with_node_id(mut self, node_id: impl Into<String>) -> Self {
826 self.node_id = Some(node_id.into());
827 self
828 }
829
830 pub fn with_effect(mut self, effect: EffectVector) -> Self {
832 self.effect = effect;
833 self
834 }
835
836 pub fn with_principal(mut self, principal: GatePrincipal) -> Self {
838 if self.agent_id.is_empty() && !principal.agent_id.is_empty() {
839 self.agent_id = principal.agent_id.clone();
840 }
841 self.principal = Some(principal);
842 self
843 }
844
845 pub fn with_tool_context(
860 mut self,
861 tool_name: impl Into<String>,
862 gate_action: impl Into<String>,
863 effect: EffectVector,
864 pid: u64,
865 ) -> Self {
866 self.context.insert("tool".into(), tool_name.into());
867 self.context
868 .insert("gate_action".into(), gate_action.into());
869 self.context.insert("pid".into(), pid.to_string());
870 self.effect = effect;
871 self
872 }
873
874 pub fn with_context_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
879 let key = key.into();
880 if !crate::governance_project::RESERVED_CONTEXT_KEYS.contains(&key.as_str()) {
881 self.context.insert(key, value.into());
882 }
883 self
884 }
885
886 pub fn resolved_principal(&self) -> GatePrincipal {
889 let p = self.base_principal();
890 match crate::governance_project::instance_project() {
891 Some((att, inst)) => p.with_project(att).with_instance(inst),
892 None => p,
893 }
894 }
895
896 pub(crate) fn base_principal(&self) -> GatePrincipal {
898 if let Some(p) = &self.principal {
899 let mut p = p.clone();
900 if p.agent_id.is_empty() {
901 p.agent_id = self.agent_id.clone();
902 }
903 return p;
904 }
905 GatePrincipal::agent(self.agent_id.clone())
906 }
907}
908
909fn apply_binding_thread_rule(
915 rule: &GovernanceRule,
916 request: &GovernanceRequest,
917) -> Option<String> {
918 if request.action != BINDING_THREAD_RULE_ID && request.action != rule.id {
919 return None;
920 }
921
922 let status = request
923 .context
924 .get("binding_thread_status")
925 .map(|s| s.as_str())
926 .unwrap_or("");
927 let mismatch = status == "mismatch"
928 || request.effect.security >= 0.99
931 || (status.is_empty() && request.effect.magnitude() > 0.8);
932
933 if mismatch {
934 Some(BINDING_THREAD_MISMATCH_REASON.to_string())
935 } else {
936 None
937 }
938}
939
940fn apply_browser_policy_rule(
945 rule: &GovernanceRule,
946 request: &GovernanceRequest,
947) -> Option<String> {
948 let platform = request
949 .context
950 .get("platform")
951 .map(|s| s.as_str())
952 .unwrap_or("");
953
954 if platform != "browser" {
956 return None;
957 }
958
959 let action = request.action.as_str();
960
961 if action.starts_with("ipc.topic")
963 || action == "ipc.subscribe"
964 || action == "ipc.publish"
965 || action == "topic.subscribe"
966 || action == "topic.publish"
967 {
968 let topic = request
969 .context
970 .get("topic")
971 .map(|s| s.as_str())
972 .unwrap_or("");
973 if !crate::capability::is_public_topic(topic) {
974 return Some(format!(
975 "browser_policy '{}': browser nodes may only access public topics (got '{topic}')",
976 rule.id
977 ));
978 }
979 return None;
980 }
981
982 if (action == "agent.spawn"
984 || action == "tool.agent_spawn"
985 || action.starts_with("agent.spawn"))
986 && (rule.id == "BP-002" || rule.description.to_lowercase().contains("spawn"))
987 {
988 return Some(format!(
989 "browser_policy '{}': browser nodes cannot spawn without elevation",
990 rule.id
991 ));
992 }
993
994 if (action == "network.enable" || action == "capability.network")
996 && (rule.id == "BP-003" || rule.description.to_lowercase().contains("network"))
997 {
998 return Some(format!(
999 "browser_policy '{}': browser nodes cannot enable network without elevation",
1000 rule.id
1001 ));
1002 }
1003
1004 if action == "capability.elevate" {
1006 if request.context.get("can_spawn").map(|s| s.as_str()) == Some("true")
1007 && (rule.id == "BP-002" || rule.description.to_lowercase().contains("spawn"))
1008 {
1009 return Some(format!(
1010 "browser_policy '{}': browser nodes cannot spawn without elevation",
1011 rule.id
1012 ));
1013 }
1014 if request.context.get("can_network").map(|s| s.as_str()) == Some("true")
1015 && (rule.id == "BP-003" || rule.description.to_lowercase().contains("network"))
1016 {
1017 return Some(format!(
1018 "browser_policy '{}': browser nodes cannot enable network without elevation",
1019 rule.id
1020 ));
1021 }
1022 }
1023
1024 None
1025}
1026
1027#[derive(Debug, Clone, Serialize, Deserialize)]
1029pub struct GovernanceResult {
1030 pub decision: GovernanceDecision,
1032
1033 pub evaluated_rules: Vec<String>,
1035
1036 pub effect: EffectVector,
1038
1039 pub threshold_exceeded: bool,
1041
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1047 pub principal: Option<GatePrincipal>,
1048}
1049
1050pub struct GovernanceEngine {
1061 rules: Vec<GovernanceRule>,
1062 risk_threshold: f64,
1063 human_approval_required: bool,
1064 eval_limiter: crate::rate_limit::KeyedRateLimiter,
1066 #[cfg(feature = "ecc")]
1073 scorer: Option<crate::eml_kernel::GovernanceScorerModel>,
1074}
1075
1076impl GovernanceEngine {
1077 pub fn new(risk_threshold: f64, human_approval_required: bool) -> Self {
1079 Self {
1080 rules: Vec::new(),
1081 risk_threshold,
1082 human_approval_required,
1083 eval_limiter: crate::rate_limit::KeyedRateLimiter::new(
1084 crate::rate_limit::DEFAULT_GOVERNANCE_EVAL_RATE,
1085 ),
1086 #[cfg(feature = "ecc")]
1087 scorer: None,
1088 }
1089 }
1090
1091 pub fn open() -> Self {
1096 Self {
1097 rules: Vec::new(),
1098 risk_threshold: 1.0,
1099 human_approval_required: false,
1100 eval_limiter: crate::rate_limit::KeyedRateLimiter::new(
1101 crate::rate_limit::DEFAULT_GOVERNANCE_EVAL_RATE,
1102 ),
1103 #[cfg(feature = "ecc")]
1104 scorer: None,
1105 }
1106 }
1107
1108 pub fn with_eval_rate_limit(
1112 self,
1113 config: crate::rate_limit::RateLimitConfig,
1114 ) -> Self {
1115 self.set_eval_rate_limit(config);
1116 self
1117 }
1118
1119 pub fn set_eval_rate_limit(&self, config: crate::rate_limit::RateLimitConfig) {
1121 self.eval_limiter.set_config(config);
1122 }
1123
1124 pub fn eval_rate_limit(&self) -> crate::rate_limit::RateLimitConfig {
1126 self.eval_limiter.config()
1127 }
1128
1129 pub fn eval_principal_key(request: &GovernanceRequest) -> &str {
1134 if !request.agent_id.is_empty() {
1135 return request.agent_id.as_str();
1136 }
1137 if let Some(p) = request.principal.as_ref() {
1138 let k = p.eval_key();
1139 if !k.is_empty() {
1140 return k;
1141 }
1142 }
1143 request.node_id.as_deref().unwrap_or("")
1144 }
1145
1146 #[cfg(feature = "ecc")]
1154 pub fn with_scorer(mut self, scorer: crate::eml_kernel::GovernanceScorerModel) -> Self {
1155 self.scorer = Some(scorer);
1156 self
1157 }
1158
1159 #[cfg(feature = "ecc")]
1162 pub fn scorer(&self) -> Option<&crate::eml_kernel::GovernanceScorerModel> {
1163 self.scorer.as_ref()
1164 }
1165
1166 pub fn add_rule(&mut self, rule: GovernanceRule) {
1168 debug!(rule_id = %rule.id, branch = %rule.branch, "adding governance rule");
1169 self.rules.push(rule);
1170 }
1171
1172 #[cfg(feature = "exochain")]
1176 pub(crate) fn inherit_config(&mut self, old: &GovernanceEngine) {
1177 self.set_eval_rate_limit(old.eval_rate_limit());
1178 #[cfg(feature = "ecc")]
1179 {
1180 self.scorer = old.scorer.clone();
1181 }
1182 }
1183
1184 pub fn rules(&self) -> &[GovernanceRule] {
1186 &self.rules
1187 }
1188
1189 pub fn human_approval_required(&self) -> bool {
1191 self.human_approval_required
1192 }
1193
1194 pub fn active_rules(&self) -> Vec<&GovernanceRule> {
1196 self.rules.iter().filter(|r| r.active).collect()
1197 }
1198
1199 pub fn rules_by_branch(&self, branch: &GovernanceBranch) -> Vec<&GovernanceRule> {
1201 self.rules
1202 .iter()
1203 .filter(|r| r.active && &r.branch == branch)
1204 .collect()
1205 }
1206
1207 pub fn evaluate(&self, request: &GovernanceRequest) -> GovernanceResult {
1235 let attributed = request.resolved_principal();
1236 let principal = Self::eval_principal_key(request);
1238 if !self.eval_limiter.check(principal) {
1239 debug!(
1240 principal = %principal,
1241 action = %request.action,
1242 "governance evaluation rate limited"
1243 );
1244 return GovernanceResult {
1245 decision: GovernanceDecision::Deny(format!(
1246 "rate limited: evaluation quota exceeded for principal '{principal}'"
1247 )),
1248 evaluated_rules: Vec::new(),
1249 effect: request.effect.clone(),
1250 threshold_exceeded: false,
1251 principal: Some(attributed),
1252 };
1253 }
1254
1255 #[cfg(feature = "ecc")]
1256 let magnitude = request.effect.score(self.scorer.as_ref());
1257 #[cfg(not(feature = "ecc"))]
1258 let magnitude = request.effect.score();
1259 let threshold_exceeded = magnitude > self.risk_threshold;
1260
1261 let mut evaluated_rules = Vec::new();
1262 let mut has_warning = false;
1263 let mut has_blocking = false;
1264 let mut hard_deny = false;
1265 let mut needs_approval = false;
1266 let mut other_blocking = false;
1269 let mut blocking_reason = String::new();
1270
1271 for rule in self.active_rules() {
1272 if !rule.selectors_match(request) {
1276 continue;
1277 }
1278 evaluated_rules.push(rule.id.clone());
1279
1280 if rule.rule_type == GovernanceRuleType::BrowserPolicy {
1283 if let Some(reason) = apply_browser_policy_rule(rule, request) {
1284 has_blocking = true;
1285 other_blocking = true;
1286 hard_deny |= rule.sop_category.as_deref() == Some(OVERLAY_DENY_TAG);
1287 blocking_reason = reason;
1288 if !matches!(
1291 rule.severity,
1292 RuleSeverity::Blocking | RuleSeverity::Critical
1293 ) {
1294 has_warning = true;
1296 has_blocking = false;
1297 }
1298 }
1299 continue;
1300 }
1301
1302 if rule.rule_type == GovernanceRuleType::BindingThread {
1304 if let Some(reason) = apply_binding_thread_rule(rule, request) {
1305 match rule.severity {
1306 RuleSeverity::Blocking | RuleSeverity::Critical => {
1307 has_blocking = true;
1308 other_blocking = true;
1309 hard_deny |= rule.sop_category.as_deref() == Some(OVERLAY_DENY_TAG);
1310 blocking_reason = reason;
1314 }
1315 RuleSeverity::Warning => {
1316 has_warning = true;
1317 }
1318 RuleSeverity::Advisory => {}
1319 }
1320 }
1321 continue;
1322 }
1323
1324 let applies = threshold_exceeded || rule.force_on_match;
1326 match rule.severity {
1327 RuleSeverity::Blocking | RuleSeverity::Critical => {
1328 if applies {
1329 has_blocking = true;
1330 match rule.sop_category.as_deref() {
1331 Some(OVERLAY_DENY_TAG) => hard_deny = true,
1332 Some(OVERLAY_APPROVAL_TAG) => needs_approval = true,
1333 _ => other_blocking = true,
1334 }
1335 blocking_reason = if rule.force_on_match && !threshold_exceeded {
1336 format!(
1337 "rule '{}': selector force-match on action '{}' / tool",
1338 rule.id, request.action
1339 )
1340 } else {
1341 format!(
1342 "rule '{}': effect magnitude {magnitude:.2} > threshold {:.2}",
1343 rule.id, self.risk_threshold
1344 )
1345 };
1346 }
1347 }
1348 RuleSeverity::Warning => {
1349 if applies {
1350 has_warning = true;
1351 }
1352 }
1353 RuleSeverity::Advisory => {}
1354 }
1355 }
1356
1357 let decision = if has_blocking {
1358 if !hard_deny && (self.human_approval_required || (needs_approval && !other_blocking)) {
1359 GovernanceDecision::EscalateToHuman(blocking_reason)
1360 } else {
1361 GovernanceDecision::Deny(blocking_reason)
1362 }
1363 } else if threshold_exceeded && has_warning {
1364 GovernanceDecision::PermitWithWarning(format!(
1365 "effect magnitude {magnitude:.2} approaches threshold {:.2}",
1366 self.risk_threshold
1367 ))
1368 } else {
1369 GovernanceDecision::Permit
1370 };
1371
1372 GovernanceResult {
1373 decision,
1374 evaluated_rules,
1375 effect: request.effect.clone(),
1376 threshold_exceeded,
1377 principal: Some(attributed),
1378 }
1379 }
1380
1381 pub fn browser_policy_rules(&self) -> Vec<&GovernanceRule> {
1383 self.rules
1384 .iter()
1385 .filter(|r| r.active && r.rule_type == GovernanceRuleType::BrowserPolicy)
1386 .collect()
1387 }
1388
1389 pub fn binding_thread_rules(&self) -> Vec<&GovernanceRule> {
1391 self.rules
1392 .iter()
1393 .filter(|r| r.active && r.rule_type == GovernanceRuleType::BindingThread)
1394 .collect()
1395 }
1396
1397 pub fn risk_threshold(&self) -> f64 {
1399 self.risk_threshold
1400 }
1401
1402 pub fn rule_count(&self) -> usize {
1404 self.rules.len()
1405 }
1406
1407 pub fn evaluate_in_environment(
1419 &self,
1420 request: &GovernanceRequest,
1421 env: &Environment,
1422 ) -> GovernanceResult {
1423 let adjusted_threshold = match &env.class {
1424 EnvironmentClass::Development => {
1425 env.governance.risk_threshold
1427 }
1428 EnvironmentClass::Staging => {
1429 env.governance.risk_threshold
1431 }
1432 EnvironmentClass::Production => {
1433 env.governance.risk_threshold * 0.5
1435 }
1436 EnvironmentClass::Custom { risk_threshold, .. } => {
1437 *risk_threshold
1439 }
1440 };
1441
1442 let mut result = self.evaluate(request);
1444
1445 #[cfg(feature = "ecc")]
1449 let magnitude = request.effect.score(self.scorer.as_ref());
1450 #[cfg(not(feature = "ecc"))]
1451 let magnitude = request.effect.score();
1452 if magnitude > adjusted_threshold {
1453 result.threshold_exceeded = true;
1454 result.decision = GovernanceDecision::Deny(format!(
1455 "effect magnitude {magnitude:.2} exceeds {} environment threshold {adjusted_threshold:.2}",
1456 env.class,
1457 ));
1458 }
1459
1460 result
1461 }
1462
1463 #[cfg(feature = "exochain")]
1471 pub fn evaluate_logged(
1472 &self,
1473 request: &GovernanceRequest,
1474 chain: Option<&crate::chain::ChainManager>,
1475 ) -> GovernanceResult {
1476 let result = self.evaluate(request);
1477 if let Some(cm) = chain {
1478 Self::chain_log_result(cm, request, &result);
1479 }
1480 result
1481 }
1482
1483 #[cfg(feature = "exochain")]
1485 pub fn evaluate_in_environment_logged(
1486 &self,
1487 request: &GovernanceRequest,
1488 env: &Environment,
1489 chain: Option<&crate::chain::ChainManager>,
1490 ) -> GovernanceResult {
1491 let result = self.evaluate_in_environment(request, env);
1492 if let Some(cm) = chain {
1493 Self::chain_log_result(cm, request, &result);
1494 }
1495 result
1496 }
1497
1498 #[cfg(feature = "exochain")]
1503 pub fn chain_log_result(
1504 cm: &crate::chain::ChainManager,
1505 request: &GovernanceRequest,
1506 result: &GovernanceResult,
1507 ) {
1508 use crate::chain::GovernanceDecisionEvent;
1509
1510 let decision_str = match &result.decision {
1511 GovernanceDecision::Permit => "Permit".to_owned(),
1512 GovernanceDecision::PermitWithWarning(_) => "PermitWithWarning".to_owned(),
1513 GovernanceDecision::EscalateToHuman(_) => "EscalateToHuman".to_owned(),
1514 GovernanceDecision::Deny(_) => "Deny".to_owned(),
1515 };
1516
1517 let principal = result
1518 .principal
1519 .clone()
1520 .unwrap_or_else(|| request.resolved_principal());
1521 let event = GovernanceDecisionEvent {
1522 agent_id: request.agent_id.clone(),
1523 action: request.action.clone(),
1524 decision: decision_str,
1525 effect_magnitude: request.effect.magnitude(),
1526 threshold_exceeded: result.threshold_exceeded,
1527 evaluated_rules: result.evaluated_rules.clone(),
1528 timestamp: chrono::Utc::now(),
1529 principal: Some(principal),
1530 };
1531 cm.append_loggable(&event);
1532 }
1533}
1534
1535#[non_exhaustive]
1543#[derive(Debug, Clone, Serialize, Deserialize)]
1544pub enum TrajectoryOutcome {
1545 Success {
1547 reward: f64,
1549 },
1550 Failure {
1552 reason: String,
1554 },
1555 Pending,
1557}
1558
1559#[derive(Debug, Clone, Serialize, Deserialize)]
1561pub struct TrajectoryRecord {
1562 pub agent_id: String,
1564 pub action: String,
1566 pub context: serde_json::Value,
1568 pub outcome: TrajectoryOutcome,
1570 pub timestamp: chrono::DateTime<chrono::Utc>,
1572}
1573
1574pub struct TrajectoryRecorder {
1580 records: Vec<TrajectoryRecord>,
1581 max_records: usize,
1582}
1583
1584impl TrajectoryRecorder {
1585 pub fn new(max_records: usize) -> Self {
1587 Self {
1588 records: Vec::new(),
1589 max_records,
1590 }
1591 }
1592
1593 pub fn record(&mut self, record: TrajectoryRecord) {
1595 if self.records.len() >= self.max_records {
1596 self.records.remove(0); }
1598 self.records.push(record);
1599 }
1600
1601 pub fn agent_trajectory(&self, agent_id: &str) -> Vec<&TrajectoryRecord> {
1603 self.records
1604 .iter()
1605 .filter(|r| r.agent_id == agent_id)
1606 .collect()
1607 }
1608
1609 pub fn extract_patterns(&self) -> Vec<(String, usize)> {
1612 let mut action_counts: HashMap<String, usize> = HashMap::new();
1613 for record in &self.records {
1614 if matches!(record.outcome, TrajectoryOutcome::Success { .. }) {
1615 *action_counts.entry(record.action.clone()).or_default() += 1;
1616 }
1617 }
1618 let mut patterns: Vec<_> = action_counts.into_iter().collect();
1619 patterns.sort_by_key(|b| std::cmp::Reverse(b.1));
1620 patterns
1621 }
1622
1623 pub fn len(&self) -> usize {
1625 self.records.len()
1626 }
1627
1628 pub fn is_empty(&self) -> bool {
1630 self.records.is_empty()
1631 }
1632}
1633
1634#[cfg(feature = "exochain")]
1645impl GovernanceDecision {
1646 pub fn to_rvf_policy_check(&self) -> rvf_types::witness::PolicyCheck {
1652 match self {
1653 GovernanceDecision::Permit => rvf_types::witness::PolicyCheck::Allowed,
1654 GovernanceDecision::PermitWithWarning(_) => rvf_types::witness::PolicyCheck::Confirmed,
1655 GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::PolicyCheck::Confirmed,
1656 GovernanceDecision::Deny(_) => rvf_types::witness::PolicyCheck::Denied,
1657 }
1658 }
1659}
1660
1661#[cfg(feature = "exochain")]
1662impl GovernanceEngine {
1663 pub fn to_rvf_mode(&self) -> rvf_types::witness::GovernanceMode {
1669 if self.risk_threshold >= 1.0 {
1670 rvf_types::witness::GovernanceMode::Autonomous
1671 } else if self.human_approval_required {
1672 rvf_types::witness::GovernanceMode::Approved
1673 } else {
1674 rvf_types::witness::GovernanceMode::Restricted
1675 }
1676 }
1677
1678 pub fn to_rvf_policy(&self) -> rvf_runtime::GovernancePolicy {
1683 match self.to_rvf_mode() {
1684 rvf_types::witness::GovernanceMode::Restricted => {
1685 rvf_runtime::GovernancePolicy::restricted()
1686 }
1687 rvf_types::witness::GovernanceMode::Approved => {
1688 rvf_runtime::GovernancePolicy::approved()
1689 }
1690 rvf_types::witness::GovernanceMode::Autonomous => {
1691 rvf_runtime::GovernancePolicy::autonomous()
1692 }
1693 }
1694 }
1695}
1696
1697#[cfg(feature = "exochain")]
1698impl GovernanceResult {
1699 pub fn to_rvf_task_outcome(&self) -> rvf_types::witness::TaskOutcome {
1704 match &self.decision {
1705 GovernanceDecision::Permit | GovernanceDecision::PermitWithWarning(_) => {
1706 rvf_types::witness::TaskOutcome::Solved
1707 }
1708 GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::TaskOutcome::Skipped,
1709 GovernanceDecision::Deny(_) => rvf_types::witness::TaskOutcome::Failed,
1710 }
1711 }
1712}
1713
1714#[cfg(test)]
1715mod tests {
1716 use super::*;
1717
1718 fn make_rule(id: &str, severity: RuleSeverity, branch: GovernanceBranch) -> GovernanceRule {
1719 GovernanceRule {
1720 id: id.into(),
1721 description: format!("Test rule {id}"),
1722 branch,
1723 severity,
1724 active: true,
1725 reference_url: None,
1726 sop_category: None,
1727 rule_type: Default::default(),
1728 action_selector: None,
1729 tool_selector: None,
1730 force_on_match: false,
1731 }
1732 }
1733
1734 #[test]
1735 fn effect_vector_magnitude() {
1736 let v = EffectVector {
1737 risk: 0.3,
1738 fairness: 0.4,
1739 privacy: 0.0,
1740 novelty: 0.0,
1741 security: 0.0,
1742 };
1743 assert!((v.magnitude() - 0.5).abs() < 0.001);
1744 }
1745
1746 #[test]
1747 fn effect_vector_zero() {
1748 let v = EffectVector::default();
1749 assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
1750 }
1751
1752 #[test]
1753 #[cfg(feature = "ecc")]
1754 fn effect_score_untrained_matches_magnitude() {
1755 let v = EffectVector {
1758 risk: 0.5,
1759 fairness: 0.3,
1760 privacy: 0.2,
1761 novelty: 0.1,
1762 security: 0.4,
1763 };
1764
1765 let expected = v.magnitude();
1766 assert_eq!(v.score(None), expected);
1767
1768 let untrained = crate::eml_kernel::GovernanceScorerModel::new();
1769 assert!(!untrained.is_trained());
1770 assert_eq!(v.score(Some(&untrained)), expected);
1771 }
1772
1773 #[test]
1774 #[cfg(feature = "ecc")]
1775 fn effect_score_trained_uses_model() {
1776 let v = EffectVector {
1781 risk: 1.0,
1782 fairness: 1.0,
1783 privacy: 1.0,
1784 novelty: 1.0,
1785 security: 1.0,
1786 };
1787
1788 let scorer = crate::eml_kernel::GovernanceScorerModel::new();
1789 let untrained_score = v.score(Some(&scorer));
1790 assert_eq!(untrained_score, v.magnitude());
1791
1792 let mut json = serde_json::to_value(&scorer).unwrap();
1794 if let Some(inner) = json.get_mut("inner").and_then(|v| v.as_object_mut()) {
1795 inner.insert("trained".into(), serde_json::Value::Bool(true));
1796 }
1797 let forced: crate::eml_kernel::GovernanceScorerModel =
1798 serde_json::from_value(json).expect("patched scorer must deserialize");
1799 assert!(forced.is_trained(), "patched scorer should report trained");
1800
1801 let trained_score = v.score(Some(&forced));
1806 assert_ne!(
1810 trained_score, untrained_score,
1811 "trained scorer must dispatch to the model, not L2"
1812 );
1813 }
1814
1815 #[test]
1816 fn effect_any_exceeds() {
1817 let v = EffectVector {
1818 risk: 0.8,
1819 ..Default::default()
1820 };
1821 assert!(v.any_exceeds(0.5));
1822 assert!(!v.any_exceeds(0.9));
1823 }
1824
1825 #[test]
1826 fn effect_max_dimension() {
1827 let v = EffectVector {
1828 risk: 0.2,
1829 fairness: 0.5,
1830 privacy: 0.3,
1831 novelty: 0.1,
1832 security: 0.4,
1833 };
1834 assert!((v.max_dimension() - 0.5).abs() < f64::EPSILON);
1835 }
1836
1837 #[test]
1838 fn governance_branch_display() {
1839 assert_eq!(GovernanceBranch::Legislative.to_string(), "legislative");
1840 assert_eq!(GovernanceBranch::Executive.to_string(), "executive");
1841 assert_eq!(GovernanceBranch::Judicial.to_string(), "judicial");
1842 }
1843
1844 #[test]
1845 fn rule_severity_ordering() {
1846 assert!(RuleSeverity::Advisory < RuleSeverity::Warning);
1847 assert!(RuleSeverity::Warning < RuleSeverity::Blocking);
1848 assert!(RuleSeverity::Blocking < RuleSeverity::Critical);
1849 }
1850
1851 #[test]
1852 fn governance_decision_display() {
1853 assert_eq!(GovernanceDecision::Permit.to_string(), "permit");
1854 assert!(
1855 GovernanceDecision::Deny("too risky".into())
1856 .to_string()
1857 .contains("too risky")
1858 );
1859 }
1860
1861 #[test]
1862 fn open_engine_permits_everything() {
1863 let engine = GovernanceEngine::open();
1864 let request = GovernanceRequest {
1865 agent_id: "agent-1".into(),
1866 action: "deploy".into(),
1867 effect: EffectVector {
1868 risk: 0.9,
1869 security: 0.9,
1870 ..Default::default()
1871 },
1872 context: Default::default(),
1873 node_id: None,
1874 principal: None,
1875 };
1876 let result = engine.evaluate(&request);
1877 assert_eq!(result.decision, GovernanceDecision::Permit);
1878 }
1879
1880 #[test]
1881 fn eval_rate_limit_blocks_per_principal() {
1882 let engine = GovernanceEngine::open().with_eval_rate_limit(
1884 crate::rate_limit::RateLimitConfig {
1885 max_per_window: 2,
1886 window: std::time::Duration::from_secs(60),
1887 },
1888 );
1889 let req = GovernanceRequest::new("agent-flood", "noop");
1890 assert_eq!(engine.evaluate(&req).decision, GovernanceDecision::Permit);
1891 assert_eq!(engine.evaluate(&req).decision, GovernanceDecision::Permit);
1892 let limited = engine.evaluate(&req);
1893 match limited.decision {
1894 GovernanceDecision::Deny(ref msg) => {
1895 assert!(
1896 msg.contains("rate limited"),
1897 "expected rate-limited deny, got {msg}"
1898 );
1899 }
1900 other => panic!("expected Deny(rate limited), got {other:?}"),
1901 }
1902 assert!(limited.evaluated_rules.is_empty());
1903 }
1904
1905 #[test]
1906 fn eval_rate_limit_is_per_principal() {
1907 let engine = GovernanceEngine::open().with_eval_rate_limit(
1908 crate::rate_limit::RateLimitConfig {
1909 max_per_window: 1,
1910 window: std::time::Duration::from_secs(60),
1911 },
1912 );
1913 let a = GovernanceRequest::new("agent-a", "noop");
1914 let b = GovernanceRequest::new("agent-b", "noop");
1915 assert_eq!(engine.evaluate(&a).decision, GovernanceDecision::Permit);
1916 assert!(matches!(
1918 engine.evaluate(&a).decision,
1919 GovernanceDecision::Deny(_)
1920 ));
1921 assert_eq!(engine.evaluate(&b).decision, GovernanceDecision::Permit);
1923 }
1924
1925 #[test]
1926 fn eval_rate_limit_configurable_and_disableable() {
1927 let engine = GovernanceEngine::new(0.5, false).with_eval_rate_limit(
1928 crate::rate_limit::RateLimitConfig::unlimited(),
1929 );
1930 assert!(!engine.eval_rate_limit().is_enabled());
1931 let req = GovernanceRequest::new("agent-1", "noop");
1932 for _ in 0..20 {
1933 assert_eq!(engine.evaluate(&req).decision, GovernanceDecision::Permit);
1934 }
1935 }
1936
1937 #[test]
1938 fn eval_principal_key_prefers_agent_id() {
1939 let mut req = GovernanceRequest::new("agent-x", "act");
1940 req = req.with_node_id("node-y");
1941 assert_eq!(GovernanceEngine::eval_principal_key(&req), "agent-x");
1942 let mut empty_agent = GovernanceRequest::new("", "act");
1943 empty_agent.node_id = Some("node-y".into());
1944 assert_eq!(
1945 GovernanceEngine::eval_principal_key(&empty_agent),
1946 "node-y"
1947 );
1948 }
1949
1950 #[test]
1951 fn blocking_rule_denies() {
1952 let mut engine = GovernanceEngine::new(0.5, false);
1953 engine.add_rule(make_rule(
1954 "security-check",
1955 RuleSeverity::Blocking,
1956 GovernanceBranch::Judicial,
1957 ));
1958
1959 let request = GovernanceRequest {
1960 agent_id: "agent-1".into(),
1961 action: "deploy".into(),
1962 effect: EffectVector {
1963 risk: 0.6,
1964 ..Default::default()
1965 },
1966 context: Default::default(),
1967 node_id: None,
1968 principal: None,
1969 };
1970 let result = engine.evaluate(&request);
1971 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1972 assert!(result.threshold_exceeded);
1973 }
1974
1975 #[test]
1976 #[cfg(feature = "ecc")]
1977 fn engine_with_untrained_scorer_preserves_behavior() {
1978 let mut baseline = GovernanceEngine::new(0.5, false);
1981 baseline.add_rule(make_rule(
1982 "security-check",
1983 RuleSeverity::Blocking,
1984 GovernanceBranch::Judicial,
1985 ));
1986 let mut wired = GovernanceEngine::new(0.5, false)
1987 .with_scorer(crate::eml_kernel::GovernanceScorerModel::new());
1988 wired.add_rule(make_rule(
1989 "security-check",
1990 RuleSeverity::Blocking,
1991 GovernanceBranch::Judicial,
1992 ));
1993
1994 for effect in [
1995 EffectVector {
1996 risk: 0.6,
1997 ..Default::default()
1998 },
1999 EffectVector {
2000 risk: 0.1,
2001 fairness: 0.1,
2002 ..Default::default()
2003 },
2004 EffectVector::default(),
2005 ] {
2006 let req = GovernanceRequest {
2007 agent_id: "a".into(),
2008 action: "x".into(),
2009 effect,
2010 context: Default::default(),
2011 node_id: None,
2012 principal: None,
2013 };
2014 assert_eq!(
2015 baseline.evaluate(&req).decision,
2016 wired.evaluate(&req).decision,
2017 "untrained scorer must reproduce L2 behaviour"
2018 );
2019 }
2020 }
2021
2022 #[test]
2023 #[cfg(feature = "ecc")]
2024 fn engine_with_trained_scorer_uses_model_scalar() {
2025 let scorer = crate::eml_kernel::GovernanceScorerModel::new();
2029 let mut json = serde_json::to_value(&scorer).unwrap();
2030 if let Some(inner) = json.get_mut("inner").and_then(|v| v.as_object_mut()) {
2031 inner.insert("trained".into(), serde_json::Value::Bool(true));
2032 }
2033 let forced: crate::eml_kernel::GovernanceScorerModel =
2034 serde_json::from_value(json).unwrap();
2035 assert!(forced.is_trained());
2036
2037 let effect = EffectVector {
2038 risk: 1.0,
2039 ..Default::default()
2040 };
2041 let l2 = effect.magnitude();
2042 let model_scalar = effect.score(Some(&forced));
2043 assert_ne!(
2047 model_scalar, l2,
2048 "trained scorer must not return L2 magnitude"
2049 );
2050
2051 let (lo, hi) = if model_scalar < l2 {
2055 (model_scalar, l2)
2056 } else {
2057 (l2, model_scalar)
2058 };
2059 let mid = (lo + hi) / 2.0;
2060 let engine = GovernanceEngine::new(mid, false).with_scorer(forced);
2061 let request = GovernanceRequest {
2062 agent_id: "a".into(),
2063 action: "x".into(),
2064 effect,
2065 context: Default::default(),
2066 node_id: None,
2067 principal: None,
2068 };
2069 let result = engine.evaluate(&request);
2070 assert_eq!(result.threshold_exceeded, model_scalar > mid);
2075 }
2076
2077 #[test]
2078 fn blocking_with_human_approval_escalates() {
2079 let mut engine = GovernanceEngine::new(0.5, true);
2080 engine.add_rule(make_rule(
2081 "security-check",
2082 RuleSeverity::Blocking,
2083 GovernanceBranch::Judicial,
2084 ));
2085
2086 let request = GovernanceRequest {
2087 agent_id: "agent-1".into(),
2088 action: "deploy".into(),
2089 effect: EffectVector {
2090 risk: 0.6,
2091 ..Default::default()
2092 },
2093 context: Default::default(),
2094 node_id: None,
2095 principal: None,
2096 };
2097 let result = engine.evaluate(&request);
2098 assert!(matches!(
2099 result.decision,
2100 GovernanceDecision::EscalateToHuman(_)
2101 ));
2102 }
2103
2104 #[test]
2105 fn warning_rule_permits_with_warning() {
2106 let mut engine = GovernanceEngine::new(0.5, false);
2107 engine.add_rule(make_rule(
2108 "risk-check",
2109 RuleSeverity::Warning,
2110 GovernanceBranch::Executive,
2111 ));
2112
2113 let request = GovernanceRequest {
2114 agent_id: "agent-1".into(),
2115 action: "deploy".into(),
2116 effect: EffectVector {
2117 risk: 0.6,
2118 ..Default::default()
2119 },
2120 context: Default::default(),
2121 node_id: None,
2122 principal: None,
2123 };
2124 let result = engine.evaluate(&request);
2125 assert!(matches!(
2126 result.decision,
2127 GovernanceDecision::PermitWithWarning(_)
2128 ));
2129 }
2130
2131 #[test]
2132 fn below_threshold_permits() {
2133 let mut engine = GovernanceEngine::new(0.5, false);
2134 engine.add_rule(make_rule(
2135 "security-check",
2136 RuleSeverity::Blocking,
2137 GovernanceBranch::Judicial,
2138 ));
2139
2140 let request = GovernanceRequest {
2141 agent_id: "agent-1".into(),
2142 action: "read".into(),
2143 effect: EffectVector {
2144 risk: 0.1,
2145 ..Default::default()
2146 },
2147 context: Default::default(),
2148 node_id: None,
2149 principal: None,
2150 };
2151 let result = engine.evaluate(&request);
2152 assert_eq!(result.decision, GovernanceDecision::Permit);
2153 assert!(!result.threshold_exceeded);
2154 }
2155
2156 #[test]
2157 fn rules_by_branch() {
2158 let mut engine = GovernanceEngine::new(0.5, false);
2159 engine.add_rule(make_rule(
2160 "r1",
2161 RuleSeverity::Warning,
2162 GovernanceBranch::Legislative,
2163 ));
2164 engine.add_rule(make_rule(
2165 "r2",
2166 RuleSeverity::Blocking,
2167 GovernanceBranch::Judicial,
2168 ));
2169 engine.add_rule(make_rule(
2170 "r3",
2171 RuleSeverity::Advisory,
2172 GovernanceBranch::Judicial,
2173 ));
2174
2175 let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
2176 assert_eq!(judicial.len(), 2);
2177 let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
2178 assert_eq!(legislative.len(), 1);
2179 }
2180
2181 #[test]
2182 fn inactive_rules_excluded() {
2183 let mut engine = GovernanceEngine::new(0.5, false);
2184 engine.add_rule(GovernanceRule {
2185 id: "disabled".into(),
2186 description: "Disabled rule".into(),
2187 branch: GovernanceBranch::Judicial,
2188 severity: RuleSeverity::Blocking,
2189 active: false,
2190 reference_url: None,
2191 sop_category: None,
2192 rule_type: Default::default(),
2193 action_selector: None,
2194 tool_selector: None,
2195 force_on_match: false,
2196 });
2197 assert_eq!(engine.active_rules().len(), 0);
2198 }
2199
2200 #[test]
2201 fn governance_rule_serde_roundtrip() {
2202 let rule = make_rule("sec-1", RuleSeverity::Critical, GovernanceBranch::Judicial);
2203 let json = serde_json::to_string(&rule).unwrap();
2204 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
2205 assert_eq!(restored.id, "sec-1");
2206 assert!(restored.active);
2207 }
2208
2209 #[test]
2210 fn governance_request_serde_roundtrip() {
2211 let request = GovernanceRequest {
2212 agent_id: "agent-1".into(),
2213 action: "deploy".into(),
2214 effect: EffectVector {
2215 risk: 0.5,
2216 privacy: 0.3,
2217 ..Default::default()
2218 },
2219 context: std::collections::HashMap::from([("env".into(), "prod".into())]),
2220 node_id: None,
2221 principal: None,
2222 };
2223 let json = serde_json::to_string(&request).unwrap();
2224 let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
2225 assert_eq!(restored.agent_id, "agent-1");
2226 assert!((restored.effect.risk - 0.5).abs() < f64::EPSILON);
2227 assert!(restored.node_id.is_none());
2228 }
2229
2230 #[test]
2231 fn governance_request_with_node_id() {
2232 let request = GovernanceRequest::new("agent-1", "deploy")
2233 .with_node_id("node-42")
2234 .with_effect(EffectVector {
2235 risk: 0.1,
2236 ..Default::default()
2237 });
2238
2239 assert_eq!(request.node_id.as_deref(), Some("node-42"));
2240 assert_eq!(request.agent_id, "agent-1");
2241
2242 let json = serde_json::to_string(&request).unwrap();
2244 assert!(json.contains("node-42"));
2245 let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
2246 assert_eq!(restored.node_id.as_deref(), Some("node-42"));
2247 }
2248
2249 #[test]
2250 fn governance_request_without_node_id_deserializes() {
2251 let json = r#"{"agent_id":"a","action":"b","effect":{},"context":{}}"#;
2253 let request: GovernanceRequest = serde_json::from_str(json).unwrap();
2254 assert!(request.node_id.is_none());
2255 }
2256
2257 #[test]
2258 fn governance_request_builder() {
2259 let request = GovernanceRequest::new("agent-1", "deploy");
2260 assert_eq!(request.agent_id, "agent-1");
2261 assert_eq!(request.action, "deploy");
2262 assert!(request.node_id.is_none());
2263 assert!((request.effect.magnitude() - 0.0).abs() < f64::EPSILON);
2264 }
2265
2266 #[test]
2267 fn effect_vector_serde_roundtrip() {
2268 let v = EffectVector {
2269 risk: 0.1,
2270 fairness: 0.2,
2271 privacy: 0.3,
2272 novelty: 0.4,
2273 security: 0.5,
2274 };
2275 let json = serde_json::to_string(&v).unwrap();
2276 let restored: EffectVector = serde_json::from_str(&json).unwrap();
2277 assert!((restored.security - 0.5).abs() < f64::EPSILON);
2278 }
2279
2280 #[test]
2281 fn filter_rules_by_sop_category() {
2282 let rules = vec![
2283 GovernanceRule {
2284 id: "SOP-L001".into(),
2285 description: "test".into(),
2286 branch: GovernanceBranch::Legislative,
2287 severity: RuleSeverity::Blocking,
2288 active: true,
2289 reference_url: Some("https://example.com".into()),
2290 sop_category: Some("governance".into()),
2291 rule_type: Default::default(),
2292 action_selector: None,
2293 tool_selector: None,
2294 force_on_match: false,
2295 },
2296 GovernanceRule {
2297 id: "SOP-J001".into(),
2298 description: "test".into(),
2299 branch: GovernanceBranch::Judicial,
2300 severity: RuleSeverity::Blocking,
2301 active: true,
2302 reference_url: Some("https://example.com".into()),
2303 sop_category: Some("ethics".into()),
2304 rule_type: Default::default(),
2305 action_selector: None,
2306 tool_selector: None,
2307 force_on_match: false,
2308 },
2309 GovernanceRule {
2310 id: "GOV-001".into(),
2311 description: "test".into(),
2312 branch: GovernanceBranch::Judicial,
2313 severity: RuleSeverity::Blocking,
2314 active: true,
2315 reference_url: None,
2316 sop_category: None,
2317 rule_type: Default::default(),
2318 action_selector: None,
2319 tool_selector: None,
2320 force_on_match: false,
2321 },
2322 ];
2323 let ethics = GovernanceRule::filter_by_category(&rules, "ethics");
2324 assert_eq!(ethics.len(), 1);
2325 assert_eq!(ethics[0].id, "SOP-J001");
2326
2327 let governance = GovernanceRule::filter_by_category(&rules, "governance");
2328 assert_eq!(governance.len(), 1);
2329
2330 let none = GovernanceRule::filter_by_category(&rules, "nonexistent");
2331 assert!(none.is_empty());
2332 }
2333
2334 #[test]
2335 fn governance_rule_with_sop_serde() {
2336 let rule = GovernanceRule {
2337 id: "SOP-L001".into(),
2338 description: "test".into(),
2339 branch: GovernanceBranch::Legislative,
2340 severity: RuleSeverity::Blocking,
2341 active: true,
2342 reference_url: Some("https://example.com/sop".into()),
2343 sop_category: Some("governance".into()),
2344 rule_type: Default::default(),
2345 action_selector: None,
2346 tool_selector: None,
2347 force_on_match: false,
2348 };
2349 let json = serde_json::to_string(&rule).unwrap();
2350 assert!(json.contains("reference_url"));
2351 assert!(json.contains("sop_category"));
2352 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
2353 assert_eq!(
2354 restored.reference_url,
2355 Some("https://example.com/sop".into())
2356 );
2357 }
2358
2359 #[test]
2360 fn governance_rule_without_sop_backward_compat() {
2361 let json = r#"{"id":"GOV-001","description":"test","branch":"Judicial","severity":"Blocking","active":true}"#;
2363 let rule: GovernanceRule = serde_json::from_str(json).unwrap();
2364 assert!(rule.reference_url.is_none());
2365 assert!(rule.sop_category.is_none());
2366 assert_eq!(rule.rule_type, GovernanceRuleType::General);
2367 }
2368
2369 #[test]
2372 fn browser_policy_default_rules_shape() {
2373 let rules = browser_policy_default_rules();
2374 assert_eq!(rules.len(), 3);
2375 for rule in &rules {
2376 assert_eq!(rule.rule_type, GovernanceRuleType::BrowserPolicy);
2377 assert_eq!(rule.sop_category.as_deref(), Some("browser_policy"));
2378 assert_eq!(rule.severity, RuleSeverity::Blocking);
2379 assert!(rule.active);
2380 }
2381 let ids: Vec<_> = rules.iter().map(|r| r.id.as_str()).collect();
2382 assert_eq!(ids, vec!["BP-001", "BP-002", "BP-003"]);
2383 }
2384
2385 #[test]
2386 fn browser_policy_denies_non_public_topic_for_browser() {
2387 let mut engine = GovernanceEngine::new(0.7, false);
2388 for rule in browser_policy_default_rules() {
2389 engine.add_rule(rule);
2390 }
2391
2392 let request = GovernanceRequest::new("browser-agent", "ipc.topic.subscribe")
2393 .with_context_entry("platform", "browser")
2394 .with_context_entry("topic", "admin.secrets");
2395
2396 let result = engine.evaluate(&request);
2397 assert!(
2398 matches!(result.decision, GovernanceDecision::Deny(ref r) if r.contains("public")),
2399 "browser non-public topic must be denied, got {:?}",
2400 result.decision
2401 );
2402 }
2403
2404 #[test]
2405 fn browser_policy_allows_public_topic_for_browser() {
2406 let mut engine = GovernanceEngine::new(0.7, false);
2407 for rule in browser_policy_default_rules() {
2408 engine.add_rule(rule);
2409 }
2410
2411 let request = GovernanceRequest::new("browser-agent", "ipc.topic.subscribe")
2412 .with_context_entry("platform", "browser")
2413 .with_context_entry("topic", "public.events");
2414
2415 let result = engine.evaluate(&request);
2416 assert_eq!(
2417 result.decision,
2418 GovernanceDecision::Permit,
2419 "browser public topic must be permitted, got {:?}",
2420 result.decision
2421 );
2422 }
2423
2424 #[test]
2425 fn browser_policy_does_not_apply_to_native_platform() {
2426 let mut engine = GovernanceEngine::new(0.7, false);
2427 for rule in browser_policy_default_rules() {
2428 engine.add_rule(rule);
2429 }
2430
2431 let request = GovernanceRequest::new("native-agent", "ipc.topic.subscribe")
2433 .with_context_entry("platform", "native")
2434 .with_context_entry("topic", "admin.secrets");
2435
2436 let result = engine.evaluate(&request);
2437 assert_eq!(result.decision, GovernanceDecision::Permit);
2438 }
2439
2440 #[test]
2441 fn browser_policy_denies_spawn_for_browser() {
2442 let mut engine = GovernanceEngine::new(0.7, false);
2443 for rule in browser_policy_default_rules() {
2444 engine.add_rule(rule);
2445 }
2446
2447 let request = GovernanceRequest::new("browser-agent", "agent.spawn")
2448 .with_context_entry("platform", "browser");
2449
2450 let result = engine.evaluate(&request);
2451 assert!(
2452 matches!(result.decision, GovernanceDecision::Deny(ref r) if r.contains("spawn")),
2453 "browser spawn must be denied, got {:?}",
2454 result.decision
2455 );
2456 }
2457
2458 #[test]
2459 fn filter_by_browser_policy_type() {
2460 let rules = browser_policy_default_rules();
2461 let filtered =
2462 GovernanceRule::filter_by_type(&rules, &GovernanceRuleType::BrowserPolicy);
2463 assert_eq!(filtered.len(), 3);
2464 let general = GovernanceRule::filter_by_type(&rules, &GovernanceRuleType::General);
2465 assert!(general.is_empty());
2466 }
2467
2468 fn make_sop_rule(
2473 id: &str,
2474 severity: RuleSeverity,
2475 branch: GovernanceBranch,
2476 category: Option<&str>,
2477 ) -> GovernanceRule {
2478 GovernanceRule {
2479 id: id.into(),
2480 description: format!("Genesis rule {id}"),
2481 branch,
2482 severity,
2483 active: true,
2484 reference_url: None,
2485 sop_category: category.map(|c| c.into()),
2486 rule_type: Default::default(),
2487 action_selector: None,
2488 tool_selector: None,
2489 force_on_match: false,
2490 }
2491 }
2492
2493 fn genesis_engine() -> GovernanceEngine {
2496 let mut engine = GovernanceEngine::new(0.7, false);
2497
2498 engine.add_rule(make_sop_rule(
2501 "GOV-001",
2502 RuleSeverity::Blocking,
2503 GovernanceBranch::Judicial,
2504 None,
2505 ));
2506 engine.add_rule(make_sop_rule(
2507 "GOV-002",
2508 RuleSeverity::Blocking,
2509 GovernanceBranch::Judicial,
2510 None,
2511 ));
2512 engine.add_rule(make_sop_rule(
2514 "GOV-003",
2515 RuleSeverity::Warning,
2516 GovernanceBranch::Legislative,
2517 None,
2518 ));
2519 engine.add_rule(make_sop_rule(
2521 "GOV-004",
2522 RuleSeverity::Advisory,
2523 GovernanceBranch::Executive,
2524 None,
2525 ));
2526 engine.add_rule(make_sop_rule(
2528 "GOV-005",
2529 RuleSeverity::Warning,
2530 GovernanceBranch::Legislative,
2531 None,
2532 ));
2533 engine.add_rule(make_sop_rule(
2535 "GOV-006",
2536 RuleSeverity::Blocking,
2537 GovernanceBranch::Executive,
2538 None,
2539 ));
2540 engine.add_rule(make_sop_rule(
2542 "GOV-007",
2543 RuleSeverity::Advisory,
2544 GovernanceBranch::Judicial,
2545 None,
2546 ));
2547
2548 engine.add_rule(make_sop_rule(
2550 "SOP-L001",
2551 RuleSeverity::Blocking,
2552 GovernanceBranch::Legislative,
2553 Some("governance"),
2554 ));
2555 engine.add_rule(make_sop_rule(
2556 "SOP-L002",
2557 RuleSeverity::Warning,
2558 GovernanceBranch::Legislative,
2559 Some("governance"),
2560 ));
2561 engine.add_rule(make_sop_rule(
2562 "SOP-L003",
2563 RuleSeverity::Warning,
2564 GovernanceBranch::Legislative,
2565 Some("engineering"),
2566 ));
2567 engine.add_rule(make_sop_rule(
2568 "SOP-L004",
2569 RuleSeverity::Advisory,
2570 GovernanceBranch::Legislative,
2571 Some("lifecycle"),
2572 ));
2573 engine.add_rule(make_sop_rule(
2574 "SOP-L005",
2575 RuleSeverity::Blocking,
2576 GovernanceBranch::Legislative,
2577 Some("ethics"),
2578 ));
2579 engine.add_rule(make_sop_rule(
2580 "SOP-L006",
2581 RuleSeverity::Warning,
2582 GovernanceBranch::Legislative,
2583 Some("governance"),
2584 ));
2585
2586 engine.add_rule(make_sop_rule(
2588 "SOP-E001",
2589 RuleSeverity::Warning,
2590 GovernanceBranch::Executive,
2591 Some("engineering"),
2592 ));
2593 engine.add_rule(make_sop_rule(
2594 "SOP-E002",
2595 RuleSeverity::Blocking,
2596 GovernanceBranch::Executive,
2597 Some("lifecycle"),
2598 ));
2599 engine.add_rule(make_sop_rule(
2600 "SOP-E003",
2601 RuleSeverity::Warning,
2602 GovernanceBranch::Executive,
2603 Some("security"),
2604 ));
2605 engine.add_rule(make_sop_rule(
2606 "SOP-E004",
2607 RuleSeverity::Advisory,
2608 GovernanceBranch::Executive,
2609 Some("lifecycle"),
2610 ));
2611 engine.add_rule(make_sop_rule(
2612 "SOP-E005",
2613 RuleSeverity::Advisory,
2614 GovernanceBranch::Executive,
2615 Some("governance"),
2616 ));
2617
2618 engine.add_rule(make_sop_rule(
2620 "SOP-J001",
2621 RuleSeverity::Blocking,
2622 GovernanceBranch::Judicial,
2623 Some("ethics"),
2624 ));
2625 engine.add_rule(make_sop_rule(
2626 "SOP-J002",
2627 RuleSeverity::Warning,
2628 GovernanceBranch::Judicial,
2629 Some("ethics"),
2630 ));
2631 engine.add_rule(make_sop_rule(
2632 "SOP-J003",
2633 RuleSeverity::Warning,
2634 GovernanceBranch::Judicial,
2635 Some("lifecycle"),
2636 ));
2637 engine.add_rule(make_sop_rule(
2638 "SOP-J004",
2639 RuleSeverity::Advisory,
2640 GovernanceBranch::Judicial,
2641 Some("quality"),
2642 ));
2643
2644 for rule in browser_policy_default_rules() {
2646 engine.add_rule(rule);
2647 }
2648
2649 for rule in binding_thread_default_rules() {
2651 engine.add_rule(rule);
2652 }
2653
2654 engine
2655 }
2656
2657 #[test]
2658 fn genesis_has_26_rules() {
2659 let engine = genesis_engine();
2660 assert_eq!(engine.rule_count(), 26);
2662 }
2663
2664 #[test]
2665 fn binding_thread_rule_denies_mismatch() {
2666 let engine = genesis_engine();
2667 let request = GovernanceRequest {
2668 agent_id: "concierge".into(),
2669 action: BINDING_THREAD_RULE_ID.into(),
2670 effect: EffectVector {
2671 security: 1.0,
2672 risk: 0.9,
2673 ..Default::default()
2674 },
2675 context: std::collections::HashMap::from([(
2676 "binding_thread_status".into(),
2677 "mismatch".into(),
2678 )]),
2679 node_id: None,
2680 principal: None,
2681 };
2682 let result = engine.evaluate(&request);
2683 match result.decision {
2684 GovernanceDecision::Deny(reason) => {
2685 assert_eq!(reason, BINDING_THREAD_MISMATCH_REASON);
2686 }
2687 other => panic!("expected Deny(binding-thread mismatch), got {other:?}"),
2688 }
2689 assert!(
2690 result
2691 .evaluated_rules
2692 .iter()
2693 .any(|id| id == BINDING_THREAD_RULE_ID)
2694 );
2695 }
2696
2697 #[test]
2698 fn binding_thread_rule_permits_ok() {
2699 let engine = genesis_engine();
2700 let request = GovernanceRequest {
2701 agent_id: "concierge".into(),
2702 action: BINDING_THREAD_RULE_ID.into(),
2703 effect: EffectVector::default(),
2704 context: std::collections::HashMap::from([(
2705 "binding_thread_status".into(),
2706 "ok".into(),
2707 )]),
2708 node_id: None,
2709 principal: None,
2710 };
2711 let result = engine.evaluate(&request);
2712 assert!(
2713 matches!(result.decision, GovernanceDecision::Permit),
2714 "intact binding thread must Permit, got {:?}",
2715 result.decision
2716 );
2717 }
2718
2719 #[test]
2720 fn binding_thread_default_rules_shape() {
2721 let rules = binding_thread_default_rules();
2722 assert_eq!(rules.len(), 1);
2723 assert_eq!(rules[0].id, BINDING_THREAD_RULE_ID);
2724 assert_eq!(rules[0].rule_type, GovernanceRuleType::BindingThread);
2725 assert_eq!(rules[0].severity, RuleSeverity::Blocking);
2726 assert!(rules[0].active);
2727 }
2728
2729 #[test]
2730 fn genesis_high_risk_operation_blocked() {
2731 let engine = genesis_engine();
2732 let request = GovernanceRequest {
2733 agent_id: "agent-1".into(),
2734 action: "deploy-to-prod".into(),
2735 effect: EffectVector {
2736 risk: 0.9,
2737 ..Default::default()
2738 },
2739 context: Default::default(),
2740 node_id: None,
2741 principal: None,
2742 };
2743 let result = engine.evaluate(&request);
2744 assert!(
2745 matches!(result.decision, GovernanceDecision::Deny(_)),
2746 "high-risk operation should be denied, got {:?}",
2747 result.decision,
2748 );
2749 assert!(result.threshold_exceeded);
2750 }
2751
2752 #[test]
2753 fn genesis_low_risk_operation_permitted() {
2754 let engine = genesis_engine();
2755 let request = GovernanceRequest {
2756 agent_id: "agent-1".into(),
2757 action: "read-file".into(),
2758 effect: EffectVector {
2759 risk: 0.1,
2760 ..Default::default()
2761 },
2762 context: Default::default(),
2763 node_id: None,
2764 principal: None,
2765 };
2766 let result = engine.evaluate(&request);
2767 assert_eq!(result.decision, GovernanceDecision::Permit);
2768 assert!(!result.threshold_exceeded);
2769 }
2770
2771 #[test]
2772 fn genesis_privacy_violation_triggers_enforcement() {
2773 let engine = genesis_engine();
2774 let request = GovernanceRequest {
2775 agent_id: "agent-1".into(),
2776 action: "access-user-data".into(),
2777 effect: EffectVector {
2778 privacy: 0.8,
2779 ..Default::default()
2780 },
2781 context: Default::default(),
2782 node_id: None,
2783 principal: None,
2784 };
2785 let result = engine.evaluate(&request);
2786 assert!(
2788 matches!(
2789 result.decision,
2790 GovernanceDecision::Deny(_) | GovernanceDecision::PermitWithWarning(_)
2791 ),
2792 "privacy violation should trigger warning or deny, got {:?}",
2793 result.decision,
2794 );
2795 assert!(result.threshold_exceeded);
2796 }
2797
2798 #[test]
2799 fn genesis_security_sensitive_blocked() {
2800 let engine = genesis_engine();
2802 let request = GovernanceRequest {
2803 agent_id: "agent-1".into(),
2804 action: "modify-firewall".into(),
2805 effect: EffectVector {
2806 security: 0.9,
2807 risk: 0.5,
2808 ..Default::default()
2809 },
2810 context: Default::default(),
2811 node_id: None,
2812 principal: None,
2813 };
2814 let result = engine.evaluate(&request);
2815 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
2817 }
2818
2819 #[test]
2820 fn genesis_fairness_bias_blocked() {
2821 let engine = genesis_engine();
2823 let request = GovernanceRequest {
2824 agent_id: "ml-agent".into(),
2825 action: "evaluate-candidate".into(),
2826 effect: EffectVector {
2827 fairness: 0.9,
2828 ..Default::default()
2829 },
2830 context: Default::default(),
2831 node_id: None,
2832 principal: None,
2833 };
2834 let result = engine.evaluate(&request);
2835 assert!(
2836 matches!(result.decision, GovernanceDecision::Deny(_)),
2837 "fairness violation should be blocked by SOP-J001, got {:?}",
2838 result.decision,
2839 );
2840 }
2841
2842 #[test]
2843 fn genesis_agent_spawn_blocked_when_risky() {
2844 let engine = genesis_engine();
2846 let request = GovernanceRequest {
2847 agent_id: "orchestrator".into(),
2848 action: "agent.spawn".into(),
2849 effect: EffectVector {
2850 risk: 0.8,
2851 novelty: 0.5,
2852 ..Default::default()
2853 },
2854 context: Default::default(),
2855 node_id: None,
2856 principal: None,
2857 };
2858 let result = engine.evaluate(&request);
2859 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
2861 }
2862
2863 #[test]
2864 fn genesis_agent_spawn_permitted_when_safe() {
2865 let engine = genesis_engine();
2867 let request = GovernanceRequest {
2868 agent_id: "orchestrator".into(),
2869 action: "agent.spawn".into(),
2870 effect: EffectVector {
2871 risk: 0.1,
2872 ..Default::default()
2873 },
2874 context: Default::default(),
2875 node_id: None,
2876 principal: None,
2877 };
2878 let result = engine.evaluate(&request);
2879 assert_eq!(result.decision, GovernanceDecision::Permit);
2880 }
2881
2882 #[test]
2883 fn genesis_data_protection_blocks_high_privacy() {
2884 let engine = genesis_engine();
2886 let request = GovernanceRequest {
2887 agent_id: "data-agent".into(),
2888 action: "export-pii".into(),
2889 effect: EffectVector {
2890 privacy: 0.9,
2891 risk: 0.3,
2892 ..Default::default()
2893 },
2894 context: Default::default(),
2895 node_id: None,
2896 principal: None,
2897 };
2898 let result = engine.evaluate(&request);
2899 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
2901 }
2902
2903 #[test]
2904 fn genesis_with_human_approval_escalates() {
2905 let mut engine = GovernanceEngine::new(0.7, true);
2907 engine.add_rule(make_sop_rule(
2908 "GOV-001",
2909 RuleSeverity::Blocking,
2910 GovernanceBranch::Judicial,
2911 None,
2912 ));
2913
2914 let request = GovernanceRequest {
2915 agent_id: "agent-1".into(),
2916 action: "high-risk-op".into(),
2917 effect: EffectVector {
2918 risk: 0.9,
2919 ..Default::default()
2920 },
2921 context: Default::default(),
2922 node_id: None,
2923 principal: None,
2924 };
2925 let result = engine.evaluate(&request);
2926 assert!(
2927 matches!(result.decision, GovernanceDecision::EscalateToHuman(_)),
2928 "with human_approval, blocking should escalate, got {:?}",
2929 result.decision,
2930 );
2931 }
2932
2933 #[test]
2934 fn genesis_all_branches_represented() {
2935 let engine = genesis_engine();
2936 let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
2937 let executive = engine.rules_by_branch(&GovernanceBranch::Executive);
2938 let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
2939
2940 assert_eq!(
2943 legislative.len(),
2944 12,
2945 "legislative should have 12 rules, got {}",
2946 legislative.len()
2947 );
2948 assert_eq!(
2950 executive.len(),
2951 7,
2952 "executive should have 7 rules, got {}",
2953 executive.len()
2954 );
2955 assert_eq!(
2957 judicial.len(),
2958 7,
2959 "judicial should have 7 rules, got {}",
2960 judicial.len()
2961 );
2962 }
2963
2964 #[test]
2965 fn genesis_blocking_rules_count() {
2966 let engine = genesis_engine();
2967 let blocking_count = engine
2968 .active_rules()
2969 .iter()
2970 .filter(|r| matches!(r.severity, RuleSeverity::Blocking | RuleSeverity::Critical))
2971 .count();
2972 assert_eq!(blocking_count, 11, "should have exactly 11 blocking rules");
2975 }
2976
2977 #[test]
2978 fn genesis_warning_rules_count() {
2979 let engine = genesis_engine();
2980 let warning_count = engine
2981 .active_rules()
2982 .iter()
2983 .filter(|r| matches!(r.severity, RuleSeverity::Warning))
2984 .count();
2985 assert_eq!(warning_count, 9, "should have exactly 9 warning rules");
2988 }
2989
2990 #[test]
2991 fn genesis_advisory_rules_count() {
2992 let engine = genesis_engine();
2993 let advisory_count = engine
2994 .active_rules()
2995 .iter()
2996 .filter(|r| matches!(r.severity, RuleSeverity::Advisory))
2997 .count();
2998 assert_eq!(advisory_count, 6, "should have exactly 6 advisory rules");
3000 }
3001
3002 #[test]
3003 fn genesis_moderate_risk_with_only_warnings_permits_with_warning() {
3004 let mut engine = GovernanceEngine::new(0.7, false);
3006 engine.add_rule(make_sop_rule(
3007 "SOP-L002",
3008 RuleSeverity::Warning,
3009 GovernanceBranch::Legislative,
3010 Some("governance"),
3011 ));
3012 engine.add_rule(make_sop_rule(
3013 "SOP-E001",
3014 RuleSeverity::Warning,
3015 GovernanceBranch::Executive,
3016 Some("engineering"),
3017 ));
3018
3019 let request = GovernanceRequest {
3020 agent_id: "dev-agent".into(),
3021 action: "write-code".into(),
3022 effect: EffectVector {
3023 risk: 0.5,
3024 novelty: 0.5,
3025 ..Default::default()
3026 },
3027 context: Default::default(),
3028 node_id: None,
3029 principal: None,
3030 };
3031 let result = engine.evaluate(&request);
3032 assert!(
3034 matches!(result.decision, GovernanceDecision::PermitWithWarning(_)),
3035 "warning-only rules above threshold should PermitWithWarning, got {:?}",
3036 result.decision,
3037 );
3038 }
3039
3040 #[test]
3041 fn genesis_advisory_only_permits_above_threshold() {
3042 let mut engine = GovernanceEngine::new(0.7, false);
3044 engine.add_rule(make_sop_rule(
3045 "GOV-004",
3046 RuleSeverity::Advisory,
3047 GovernanceBranch::Executive,
3048 None,
3049 ));
3050 engine.add_rule(make_sop_rule(
3051 "GOV-007",
3052 RuleSeverity::Advisory,
3053 GovernanceBranch::Judicial,
3054 None,
3055 ));
3056
3057 let request = GovernanceRequest {
3058 agent_id: "agent-1".into(),
3059 action: "novel-action".into(),
3060 effect: EffectVector {
3061 novelty: 0.9,
3062 ..Default::default()
3063 },
3064 context: Default::default(),
3065 node_id: None,
3066 principal: None,
3067 };
3068 let result = engine.evaluate(&request);
3069 assert_eq!(
3070 result.decision,
3071 GovernanceDecision::Permit,
3072 "advisory-only rules should still permit, got {:?}",
3073 result.decision
3074 );
3075 assert!(result.threshold_exceeded);
3076 }
3077
3078 #[test]
3079 fn genesis_evaluates_all_26_rules() {
3080 let engine = genesis_engine();
3081 let request = GovernanceRequest {
3082 agent_id: "agent-1".into(),
3083 action: "any-action".into(),
3084 effect: EffectVector {
3085 risk: 0.9,
3086 ..Default::default()
3087 },
3088 context: Default::default(),
3089 node_id: None,
3090 principal: None,
3091 };
3092 let result = engine.evaluate(&request);
3093 assert_eq!(
3094 result.evaluated_rules.len(),
3095 26,
3096 "all 26 rules should be evaluated, got {}",
3097 result.evaluated_rules.len(),
3098 );
3099 }
3100
3101 #[test]
3102 fn genesis_sop_categories_present() {
3103 let engine = genesis_engine();
3104 let all_rules = engine.active_rules();
3105 let categorized: Vec<_> = all_rules
3106 .iter()
3107 .filter(|r| r.sop_category.is_some())
3108 .collect();
3109 assert_eq!(
3111 categorized.len(),
3112 19,
3113 "15 SOP + 3 browser_policy + 1 binding_thread rules should have categories"
3114 );
3115
3116 let categories: std::collections::HashSet<_> = categorized
3117 .iter()
3118 .map(|r| r.sop_category.as_deref().unwrap())
3119 .collect();
3120 assert!(categories.contains("governance"));
3121 assert!(categories.contains("ethics"));
3122 assert!(categories.contains("engineering"));
3123 assert!(categories.contains("lifecycle"));
3124 assert!(categories.contains("security"));
3125 assert!(categories.contains("quality"));
3126 assert!(categories.contains("browser_policy"));
3127 }
3128
3129 #[test]
3130 fn genesis_multi_dimension_high_effect_denied() {
3131 let engine = genesis_engine();
3133 let request = GovernanceRequest {
3134 agent_id: "agent-1".into(),
3135 action: "risky-novel-private".into(),
3136 effect: EffectVector {
3137 risk: 0.4,
3138 privacy: 0.4,
3139 novelty: 0.4,
3140 security: 0.4,
3141 fairness: 0.0,
3142 },
3143 context: Default::default(),
3144 node_id: None,
3145 principal: None,
3146 };
3147 let result = engine.evaluate(&request);
3148 assert!(
3150 matches!(result.decision, GovernanceDecision::Deny(_)),
3151 "combined multi-dimension effect should exceed threshold and deny, got {:?}",
3152 result.decision,
3153 );
3154 assert!(result.threshold_exceeded);
3155 }
3156
3157 fn make_env(
3160 class: crate::environment::EnvironmentClass,
3161 risk_threshold: f64,
3162 ) -> crate::environment::Environment {
3163 crate::environment::Environment {
3164 id: format!("{class}"),
3165 name: format!("{class}"),
3166 class,
3167 governance: crate::environment::GovernanceScope {
3168 risk_threshold,
3169 ..Default::default()
3170 },
3171 labels: std::collections::HashMap::new(),
3172 }
3173 }
3174
3175 #[test]
3176 fn dev_environment_allows_higher_risk() {
3177 use crate::environment::EnvironmentClass;
3178
3179 let engine = GovernanceEngine::open();
3182
3183 let dev_env = make_env(EnvironmentClass::Development, 0.9);
3185
3186 let request = GovernanceRequest::new("agent-1", "deploy").with_effect(EffectVector {
3188 risk: 0.8,
3189 ..Default::default()
3190 });
3191
3192 let result = engine.evaluate_in_environment(&request, &dev_env);
3193 assert_eq!(
3195 result.decision,
3196 GovernanceDecision::Permit,
3197 "dev should allow risk 0.8 with threshold 0.9, got {:?}",
3198 result.decision,
3199 );
3200 }
3201
3202 #[test]
3203 fn prod_environment_blocks_high_risk() {
3204 use crate::environment::EnvironmentClass;
3205
3206 let mut engine = GovernanceEngine::new(1.0, false);
3207 engine.add_rule(make_rule(
3208 "sec-check",
3209 RuleSeverity::Blocking,
3210 GovernanceBranch::Judicial,
3211 ));
3212
3213 let prod_env = make_env(EnvironmentClass::Production, 0.6);
3215
3216 let request = GovernanceRequest::new("agent-1", "deploy").with_effect(EffectVector {
3218 risk: 0.4,
3219 ..Default::default()
3220 });
3221
3222 let result = engine.evaluate_in_environment(&request, &prod_env);
3223 assert!(
3225 matches!(result.decision, GovernanceDecision::Deny(_)),
3226 "prod should deny risk 0.4 with adjusted threshold 0.3, got {:?}",
3227 result.decision,
3228 );
3229 }
3230
3231 #[test]
3232 fn staging_uses_normal_thresholds() {
3233 use crate::environment::EnvironmentClass;
3234
3235 let mut engine = GovernanceEngine::new(1.0, false);
3236 engine.add_rule(make_rule(
3237 "sec-check",
3238 RuleSeverity::Blocking,
3239 GovernanceBranch::Judicial,
3240 ));
3241
3242 let staging_env = make_env(EnvironmentClass::Staging, 0.6);
3244
3245 let request = GovernanceRequest::new("agent-1", "test-deploy").with_effect(EffectVector {
3247 risk: 0.5,
3248 ..Default::default()
3249 });
3250
3251 let result = engine.evaluate_in_environment(&request, &staging_env);
3252 assert_eq!(
3254 result.decision,
3255 GovernanceDecision::Permit,
3256 "staging should allow risk 0.5 with threshold 0.6, got {:?}",
3257 result.decision,
3258 );
3259
3260 let request2 = GovernanceRequest::new("agent-1", "test-deploy").with_effect(EffectVector {
3262 risk: 0.7,
3263 ..Default::default()
3264 });
3265 let result2 = engine.evaluate_in_environment(&request2, &staging_env);
3266 assert!(
3268 matches!(result2.decision, GovernanceDecision::Deny(_)),
3269 "staging should deny risk 0.7 with threshold 0.6, got {:?}",
3270 result2.decision,
3271 );
3272 }
3273
3274 #[test]
3275 fn environment_governance_same_request_different_envs() {
3276 use crate::environment::EnvironmentClass;
3277
3278 let engine = GovernanceEngine::open();
3279
3280 let dev = make_env(EnvironmentClass::Development, 0.9);
3281 let prod = make_env(EnvironmentClass::Production, 0.6);
3282
3283 let request = GovernanceRequest::new("agent-1", "write-file").with_effect(EffectVector {
3285 risk: 0.5,
3286 ..Default::default()
3287 });
3288
3289 let dev_result = engine.evaluate_in_environment(&request, &dev);
3290 let prod_result = engine.evaluate_in_environment(&request, &prod);
3291
3292 assert_eq!(dev_result.decision, GovernanceDecision::Permit);
3294 assert!(matches!(prod_result.decision, GovernanceDecision::Deny(_)));
3296 }
3297
3298 #[test]
3301 fn trajectory_records_and_retrieves() {
3302 use chrono::Utc;
3303 let mut recorder = TrajectoryRecorder::new(100);
3304 recorder.record(TrajectoryRecord {
3305 agent_id: "agent-1".into(),
3306 action: "tool.execute".into(),
3307 context: serde_json::json!({"tool": "read_file"}),
3308 outcome: TrajectoryOutcome::Success { reward: 1.0 },
3309 timestamp: Utc::now(),
3310 });
3311 assert_eq!(recorder.len(), 1);
3312 assert!(!recorder.is_empty());
3313 assert_eq!(recorder.agent_trajectory("agent-1").len(), 1);
3314 assert_eq!(recorder.agent_trajectory("agent-2").len(), 0);
3315 }
3316
3317 #[test]
3318 fn trajectory_extracts_patterns() {
3319 use chrono::Utc;
3320 let mut recorder = TrajectoryRecorder::new(100);
3321
3322 for _ in 0..5 {
3323 recorder.record(TrajectoryRecord {
3324 agent_id: "a".into(),
3325 action: "tool.execute".into(),
3326 context: serde_json::json!({}),
3327 outcome: TrajectoryOutcome::Success { reward: 1.0 },
3328 timestamp: Utc::now(),
3329 });
3330 }
3331 for _ in 0..3 {
3332 recorder.record(TrajectoryRecord {
3333 agent_id: "a".into(),
3334 action: "tool.read".into(),
3335 context: serde_json::json!({}),
3336 outcome: TrajectoryOutcome::Success { reward: 0.5 },
3337 timestamp: Utc::now(),
3338 });
3339 }
3340 recorder.record(TrajectoryRecord {
3341 agent_id: "a".into(),
3342 action: "tool.fail".into(),
3343 context: serde_json::json!({}),
3344 outcome: TrajectoryOutcome::Failure {
3345 reason: "err".into(),
3346 },
3347 timestamp: Utc::now(),
3348 });
3349
3350 let patterns = recorder.extract_patterns();
3351 assert_eq!(patterns[0].0, "tool.execute");
3352 assert_eq!(patterns[0].1, 5);
3353 assert_eq!(patterns[1].0, "tool.read");
3354 assert_eq!(patterns[1].1, 3);
3355 assert!(patterns.iter().all(|(a, _)| a != "tool.fail"));
3357 }
3358
3359 #[test]
3360 fn trajectory_max_records_eviction() {
3361 use chrono::Utc;
3362 let mut recorder = TrajectoryRecorder::new(3);
3363 for i in 0..5 {
3364 recorder.record(TrajectoryRecord {
3365 agent_id: format!("agent-{i}"),
3366 action: "act".into(),
3367 context: serde_json::json!({}),
3368 outcome: TrajectoryOutcome::Pending,
3369 timestamp: Utc::now(),
3370 });
3371 }
3372 assert_eq!(recorder.len(), 3);
3373 assert!(recorder.agent_trajectory("agent-0").is_empty());
3375 assert!(recorder.agent_trajectory("agent-1").is_empty());
3376 assert_eq!(recorder.agent_trajectory("agent-2").len(), 1);
3377 assert_eq!(recorder.agent_trajectory("agent-4").len(), 1);
3378 }
3379
3380 #[test]
3381 fn trajectory_empty_patterns() {
3382 let recorder = TrajectoryRecorder::new(10);
3383 assert!(recorder.is_empty());
3384 assert!(recorder.extract_patterns().is_empty());
3385 }
3386
3387 #[test]
3390 fn governance_branch_serde_roundtrip() {
3391 for branch in [
3392 GovernanceBranch::Legislative,
3393 GovernanceBranch::Executive,
3394 GovernanceBranch::Judicial,
3395 ] {
3396 let json = serde_json::to_string(&branch).unwrap();
3397 let restored: GovernanceBranch = serde_json::from_str(&json).unwrap();
3398 assert_eq!(restored, branch);
3399 }
3400 }
3401
3402 #[test]
3403 fn rule_severity_serde_roundtrip() {
3404 for severity in [
3405 RuleSeverity::Advisory,
3406 RuleSeverity::Warning,
3407 RuleSeverity::Blocking,
3408 RuleSeverity::Critical,
3409 ] {
3410 let json = serde_json::to_string(&severity).unwrap();
3411 let restored: RuleSeverity = serde_json::from_str(&json).unwrap();
3412 assert_eq!(restored, severity);
3413 }
3414 }
3415
3416 #[test]
3417 fn rule_severity_display() {
3418 assert_eq!(RuleSeverity::Advisory.to_string(), "advisory");
3419 assert_eq!(RuleSeverity::Warning.to_string(), "warning");
3420 assert_eq!(RuleSeverity::Blocking.to_string(), "blocking");
3421 assert_eq!(RuleSeverity::Critical.to_string(), "critical");
3422 }
3423
3424 #[test]
3425 fn governance_decision_serde_roundtrip_all_variants() {
3426 let variants = vec![
3427 GovernanceDecision::Permit,
3428 GovernanceDecision::PermitWithWarning("low risk".into()),
3429 GovernanceDecision::EscalateToHuman("needs review".into()),
3430 GovernanceDecision::Deny("policy violation".into()),
3431 ];
3432 for decision in variants {
3433 let json = serde_json::to_string(&decision).unwrap();
3434 let restored: GovernanceDecision = serde_json::from_str(&json).unwrap();
3435 assert_eq!(restored, decision);
3436 }
3437 }
3438
3439 #[test]
3440 fn governance_result_serde_roundtrip() {
3441 let result = GovernanceResult {
3442 decision: GovernanceDecision::Permit,
3443 evaluated_rules: vec!["rule-1".into(), "rule-2".into()],
3444 effect: EffectVector {
3445 risk: 0.3,
3446 ..Default::default()
3447 },
3448 threshold_exceeded: false,
3449 principal: None,
3450 };
3451 let json = serde_json::to_string(&result).unwrap();
3452 let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
3453 assert_eq!(restored.decision, GovernanceDecision::Permit);
3454 assert_eq!(restored.evaluated_rules.len(), 2);
3455 assert!(!restored.threshold_exceeded);
3456 }
3457
3458 #[test]
3459 fn governance_result_deny_serde_roundtrip() {
3460 let result = GovernanceResult {
3461 decision: GovernanceDecision::Deny("threshold exceeded".into()),
3462 evaluated_rules: vec![],
3463 effect: EffectVector {
3464 risk: 0.9,
3465 security: 0.8,
3466 ..Default::default()
3467 },
3468 threshold_exceeded: true,
3469 principal: None,
3470 };
3471 let json = serde_json::to_string(&result).unwrap();
3472 let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
3473 assert!(restored.threshold_exceeded);
3474 assert!(matches!(restored.decision, GovernanceDecision::Deny(_)));
3475 }
3476
3477 #[test]
3478 fn trajectory_outcome_serde_roundtrip() {
3479 let variants: Vec<TrajectoryOutcome> = vec![
3480 TrajectoryOutcome::Success { reward: 1.5 },
3481 TrajectoryOutcome::Failure {
3482 reason: "timeout".into(),
3483 },
3484 TrajectoryOutcome::Pending,
3485 ];
3486 for outcome in variants {
3487 let json = serde_json::to_string(&outcome).unwrap();
3488 let _restored: TrajectoryOutcome = serde_json::from_str(&json).unwrap();
3489 }
3490 }
3491
3492 #[test]
3493 fn trajectory_record_serde_roundtrip() {
3494 use chrono::Utc;
3495 let record = TrajectoryRecord {
3496 agent_id: "agent-1".into(),
3497 action: "tool.execute".into(),
3498 context: serde_json::json!({"tool": "read_file", "path": "/etc/hosts"}),
3499 outcome: TrajectoryOutcome::Success { reward: 1.0 },
3500 timestamp: Utc::now(),
3501 };
3502 let json = serde_json::to_string(&record).unwrap();
3503 let restored: TrajectoryRecord = serde_json::from_str(&json).unwrap();
3504 assert_eq!(restored.agent_id, "agent-1");
3505 assert_eq!(restored.action, "tool.execute");
3506 }
3507
3508 #[test]
3509 fn governance_rule_with_sop_fields_roundtrip() {
3510 let rule = GovernanceRule {
3511 id: "SOP-001".into(),
3512 description: "Do not access prod data".into(),
3513 branch: GovernanceBranch::Legislative,
3514 severity: RuleSeverity::Critical,
3515 active: true,
3516 reference_url: Some("https://sops.example.com/001".into()),
3517 sop_category: Some("data-access".into()),
3518 rule_type: Default::default(),
3519 action_selector: None,
3520 tool_selector: None,
3521 force_on_match: false,
3522 };
3523 let json = serde_json::to_string(&rule).unwrap();
3524 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
3525 assert_eq!(
3526 restored.reference_url.unwrap(),
3527 "https://sops.example.com/001"
3528 );
3529 assert_eq!(restored.sop_category.unwrap(), "data-access");
3530 }
3531
3532 #[test]
3533 fn effect_vector_default_is_zero() {
3534 let v = EffectVector::default();
3535 assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
3536 assert!(!v.any_exceeds(0.0));
3537 }
3538
3539 #[test]
3540 fn effect_vector_max_dimension() {
3541 let v = EffectVector {
3542 risk: 0.1,
3543 fairness: 0.5,
3544 privacy: 0.3,
3545 novelty: 0.9,
3546 security: 0.2,
3547 };
3548 assert!((v.max_dimension() - 0.9).abs() < f64::EPSILON);
3549 }
3550
3551 #[cfg(feature = "exochain")]
3552 mod rvf_bridge_tests {
3553 use super::*;
3554
3555 #[test]
3556 fn decision_to_policy_check() {
3557 use rvf_types::witness::PolicyCheck;
3558
3559 assert_eq!(
3560 GovernanceDecision::Permit.to_rvf_policy_check(),
3561 PolicyCheck::Allowed,
3562 );
3563 assert_eq!(
3564 GovernanceDecision::PermitWithWarning("low risk".into()).to_rvf_policy_check(),
3565 PolicyCheck::Confirmed,
3566 );
3567 assert_eq!(
3568 GovernanceDecision::EscalateToHuman("needs review".into()).to_rvf_policy_check(),
3569 PolicyCheck::Confirmed,
3570 );
3571 assert_eq!(
3572 GovernanceDecision::Deny("blocked".into()).to_rvf_policy_check(),
3573 PolicyCheck::Denied,
3574 );
3575 }
3576
3577 #[test]
3578 fn open_engine_maps_to_autonomous() {
3579 use rvf_types::witness::GovernanceMode;
3580
3581 let engine = GovernanceEngine::open();
3582 assert_eq!(engine.to_rvf_mode(), GovernanceMode::Autonomous);
3583 }
3584
3585 #[test]
3586 fn strict_engine_maps_to_restricted() {
3587 use rvf_types::witness::GovernanceMode;
3588
3589 let engine = GovernanceEngine::new(0.5, false);
3590 assert_eq!(engine.to_rvf_mode(), GovernanceMode::Restricted);
3591 }
3592
3593 #[test]
3594 fn human_approval_maps_to_approved() {
3595 use rvf_types::witness::GovernanceMode;
3596
3597 let engine = GovernanceEngine::new(0.5, true);
3598 assert_eq!(engine.to_rvf_mode(), GovernanceMode::Approved);
3599 }
3600
3601 #[test]
3602 fn to_rvf_policy_mode_matches() {
3603 use rvf_types::witness::GovernanceMode;
3604
3605 let open = GovernanceEngine::open();
3606 let policy = open.to_rvf_policy();
3607 assert_eq!(policy.mode, GovernanceMode::Autonomous);
3608
3609 let strict = GovernanceEngine::new(0.3, false);
3610 let policy = strict.to_rvf_policy();
3611 assert_eq!(policy.mode, GovernanceMode::Restricted);
3612
3613 let human = GovernanceEngine::new(0.3, true);
3614 let policy = human.to_rvf_policy();
3615 assert_eq!(policy.mode, GovernanceMode::Approved);
3616 }
3617
3618 #[test]
3619 fn governance_result_to_task_outcome() {
3620 use rvf_types::witness::TaskOutcome;
3621
3622 let permit_result = GovernanceResult {
3623 decision: GovernanceDecision::Permit,
3624 evaluated_rules: vec![],
3625 effect: EffectVector::default(),
3626 threshold_exceeded: false,
3627 principal: None,
3628 };
3629 assert_eq!(
3630 permit_result.to_rvf_task_outcome() as u8,
3631 TaskOutcome::Solved as u8
3632 );
3633
3634 let deny_result = GovernanceResult {
3635 decision: GovernanceDecision::Deny("blocked".into()),
3636 evaluated_rules: vec![],
3637 effect: EffectVector::default(),
3638 threshold_exceeded: true,
3639 principal: None,
3640 };
3641 assert_eq!(
3642 deny_result.to_rvf_task_outcome() as u8,
3643 TaskOutcome::Failed as u8
3644 );
3645
3646 let escalate_result = GovernanceResult {
3647 decision: GovernanceDecision::EscalateToHuman("review".into()),
3648 evaluated_rules: vec![],
3649 effect: EffectVector::default(),
3650 threshold_exceeded: true,
3651 principal: None,
3652 };
3653 assert_eq!(
3654 escalate_result.to_rvf_task_outcome() as u8,
3655 TaskOutcome::Skipped as u8
3656 );
3657 }
3658 }
3659
3660 #[cfg(feature = "exochain")]
3663 mod chain_logging_tests {
3664 use super::*;
3665 use std::sync::Arc;
3666
3667 #[test]
3668 fn evaluate_logged_records_permit() {
3669 let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
3670 let initial_len = cm.len();
3671
3672 let mut engine = GovernanceEngine::new(0.5, false);
3673 engine.add_rule(GovernanceRule {
3674 id: "sec".into(),
3675 description: "test".into(),
3676 branch: GovernanceBranch::Judicial,
3677 severity: RuleSeverity::Blocking,
3678 active: true,
3679 reference_url: None,
3680 sop_category: None,
3681 rule_type: Default::default(),
3682 action_selector: None,
3683 tool_selector: None,
3684 force_on_match: false,
3685 });
3686
3687 let request = GovernanceRequest {
3688 agent_id: "agent-1".into(),
3689 action: "tool.read".into(),
3690 effect: EffectVector {
3691 risk: 0.1,
3692 ..Default::default()
3693 },
3694 context: Default::default(),
3695 node_id: None,
3696 principal: None,
3697 };
3698
3699 let result = engine.evaluate_logged(&request, Some(&cm));
3700 assert!(matches!(result.decision, GovernanceDecision::Permit));
3701 assert_eq!(cm.len(), initial_len + 1);
3702
3703 let events = cm.tail(1);
3704 assert_eq!(events[0].kind, "governance.permit");
3705 assert_eq!(events[0].source, "governance");
3706 }
3707
3708 #[test]
3709 fn evaluate_logged_records_deny() {
3710 let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
3711
3712 let mut engine = GovernanceEngine::new(0.5, false);
3713 engine.add_rule(GovernanceRule {
3714 id: "sec".into(),
3715 description: "test".into(),
3716 branch: GovernanceBranch::Judicial,
3717 severity: RuleSeverity::Blocking,
3718 active: true,
3719 reference_url: None,
3720 sop_category: None,
3721 rule_type: Default::default(),
3722 action_selector: None,
3723 tool_selector: None,
3724 force_on_match: false,
3725 });
3726
3727 let request = GovernanceRequest {
3728 agent_id: "agent-1".into(),
3729 action: "tool.exec".into(),
3730 effect: EffectVector {
3731 risk: 0.9,
3732 ..Default::default()
3733 },
3734 context: Default::default(),
3735 node_id: None,
3736 principal: None,
3737 };
3738
3739 let result = engine.evaluate_logged(&request, Some(&cm));
3740 assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
3741
3742 let events = cm.tail(1);
3743 assert_eq!(events[0].kind, "governance.deny");
3744 let payload = events[0].payload.as_ref().unwrap();
3745 assert_eq!(payload["agent_id"], "agent-1");
3746 assert!(payload["threshold_exceeded"].as_bool().unwrap());
3747 }
3748
3749 #[test]
3750 fn evaluate_logged_without_chain_still_works() {
3751 let engine = GovernanceEngine::new(0.5, false);
3752 let request = GovernanceRequest {
3753 agent_id: "agent-1".into(),
3754 action: "tool.read".into(),
3755 effect: EffectVector::default(),
3756 context: Default::default(),
3757 node_id: None,
3758 principal: None,
3759 };
3760
3761 let result = engine.evaluate_logged(&request, None);
3763 assert!(matches!(result.decision, GovernanceDecision::Permit));
3764 }
3765
3766 #[test]
3767 fn chain_log_result_standalone() {
3768 let cm = crate::chain::ChainManager::new(0, 100);
3769 let initial_len = cm.len();
3770
3771 let mut engine = GovernanceEngine::new(0.5, true);
3772 engine.add_rule(GovernanceRule {
3773 id: "sec".into(),
3774 description: "test".into(),
3775 branch: GovernanceBranch::Judicial,
3776 severity: RuleSeverity::Blocking,
3777 active: true,
3778 reference_url: None,
3779 sop_category: None,
3780 rule_type: Default::default(),
3781 action_selector: None,
3782 tool_selector: None,
3783 force_on_match: false,
3784 });
3785
3786 let request = GovernanceRequest {
3787 agent_id: "agent-1".into(),
3788 action: "tool.exec".into(),
3789 effect: EffectVector {
3790 risk: 0.8,
3791 ..Default::default()
3792 },
3793 context: Default::default(),
3794 node_id: None,
3795 principal: None,
3796 };
3797
3798 let result = engine.evaluate(&request);
3799 GovernanceEngine::chain_log_result(&cm, &request, &result);
3800 assert_eq!(cm.len(), initial_len + 1);
3801
3802 let events = cm.tail(1);
3803 assert_eq!(events[0].kind, "governance.defer");
3804 }
3805 }
3806
3807 #[test]
3810 fn selector_matches_exact_and_prefix() {
3811 assert!(selector_matches("tool.agent_spawn", "tool.agent_spawn"));
3812 assert!(!selector_matches("tool.agent_spawn", "tool.exec"));
3813 assert!(selector_matches("tool.*", "tool.agent_spawn"));
3814 assert!(selector_matches("tool.agent_*", "tool.agent_spawn"));
3815 assert!(!selector_matches("tool.agent_*", "tool.exec"));
3816 assert!(selector_matches("", "anything"));
3817 }
3818
3819 #[test]
3820 fn action_selector_scopes_blocking_rule() {
3821 let mut engine = GovernanceEngine::new(0.5, false);
3822 let mut rule = make_rule(
3823 "spawn-only",
3824 RuleSeverity::Blocking,
3825 GovernanceBranch::Legislative,
3826 );
3827 rule.action_selector = Some("tool.agent_spawn".into());
3828 engine.add_rule(rule);
3829
3830 let other = GovernanceRequest {
3832 agent_id: "a".into(),
3833 action: "tool.exec".into(),
3834 effect: EffectVector {
3835 risk: 0.9,
3836 security: 0.9,
3837 ..Default::default()
3838 },
3839 context: Default::default(),
3840 node_id: None,
3841 principal: None,
3842 };
3843 assert_eq!(
3844 engine.evaluate(&other).decision,
3845 GovernanceDecision::Permit
3846 );
3847 assert!(engine.evaluate(&other).evaluated_rules.is_empty());
3848
3849 let spawn = GovernanceRequest {
3851 agent_id: "a".into(),
3852 action: "tool.agent_spawn".into(),
3853 effect: EffectVector {
3854 risk: 0.9,
3855 security: 0.9,
3856 ..Default::default()
3857 },
3858 context: Default::default(),
3859 node_id: None,
3860 principal: None,
3861 };
3862 assert!(matches!(
3863 engine.evaluate(&spawn).decision,
3864 GovernanceDecision::Deny(_)
3865 ));
3866 assert_eq!(
3867 engine.evaluate(&spawn).evaluated_rules,
3868 vec!["spawn-only".to_string()]
3869 );
3870 }
3871
3872 #[test]
3873 fn tool_selector_matches_context_tool() {
3874 let mut engine = GovernanceEngine::new(0.5, false);
3875 let mut rule = make_rule(
3876 "tool-spawn",
3877 RuleSeverity::Blocking,
3878 GovernanceBranch::Legislative,
3879 );
3880 rule.tool_selector = Some("agent_spawn".into());
3881 engine.add_rule(rule);
3882
3883 let miss = GovernanceRequest::new("a", "tool.exec")
3884 .with_effect(EffectVector {
3885 risk: 0.9,
3886 security: 0.9,
3887 ..Default::default()
3888 })
3889 .with_context_entry("tool", "exec");
3890 assert_eq!(engine.evaluate(&miss).decision, GovernanceDecision::Permit);
3891
3892 let hit = GovernanceRequest::new("a", "tool.exec")
3893 .with_effect(EffectVector {
3894 risk: 0.9,
3895 security: 0.9,
3896 ..Default::default()
3897 })
3898 .with_context_entry("tool", "agent_spawn");
3899 assert!(matches!(
3900 engine.evaluate(&hit).decision,
3901 GovernanceDecision::Deny(_)
3902 ));
3903 }
3904
3905 #[test]
3906 fn force_on_match_applies_without_magnitude() {
3907 let mut engine = GovernanceEngine::new(0.99, false); engine.add_rule(GovernanceRule::spawn_requires_approval());
3909
3910 let req = GovernanceRequest::new("agent-1", "tool.agent_spawn")
3912 .with_effect(EffectVector {
3913 risk: 0.1,
3914 ..Default::default()
3915 })
3916 .with_context_entry("tool", "agent_spawn");
3917 let result = engine.evaluate(&req);
3918 assert!(
3919 matches!(result.decision, GovernanceDecision::Deny(_)),
3920 "expected Deny from force_on_match, got {:?}",
3921 result.decision
3922 );
3923 assert!(!result.threshold_exceeded);
3924 assert!(result.evaluated_rules.contains(&"SPAWN-APPROVAL".into()));
3925 }
3926
3927 #[test]
3928 fn spawn_approval_escalates_when_human_approval_required() {
3929 let mut engine = GovernanceEngine::new(0.99, true);
3931 engine.add_rule(GovernanceRule::spawn_requires_approval());
3932
3933 let req = GovernanceRequest::new("agent-1", "tool.agent_spawn")
3934 .with_context_entry("tool", "agent_spawn");
3935 let result = engine.evaluate(&req);
3936 match result.decision {
3937 GovernanceDecision::EscalateToHuman(ref reason) => {
3938 assert!(
3939 reason.contains("SPAWN-APPROVAL") || reason.contains("force-match"),
3940 "reason={reason}"
3941 );
3942 }
3943 other => panic!("expected EscalateToHuman, got {other:?}"),
3944 }
3945 }
3946
3947 #[test]
3948 fn selector_backward_compat_magnitude_only_rules() {
3949 let mut engine = GovernanceEngine::new(0.5, false);
3951 engine.add_rule(make_rule(
3952 "global",
3953 RuleSeverity::Blocking,
3954 GovernanceBranch::Judicial,
3955 ));
3956 let req = GovernanceRequest {
3957 agent_id: "a".into(),
3958 action: "anything".into(),
3959 effect: EffectVector {
3960 risk: 0.9,
3961 ..Default::default()
3962 },
3963 context: Default::default(),
3964 node_id: None,
3965 principal: None,
3966 };
3967 assert!(matches!(
3968 engine.evaluate(&req).decision,
3969 GovernanceDecision::Deny(_)
3970 ));
3971 }
3972
3973 #[test]
3974 fn rule_serde_preserves_selectors() {
3975 let rule = GovernanceRule::spawn_requires_approval();
3976 let json = serde_json::to_string(&rule).unwrap();
3977 let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
3978 assert_eq!(
3979 restored.action_selector.as_deref(),
3980 Some("tool.agent_spawn")
3981 );
3982 assert_eq!(restored.tool_selector.as_deref(), Some("agent_spawn"));
3983 assert!(restored.force_on_match);
3984 let legacy = r#"{"id":"x","description":"d","branch":"Legislative","severity":"Blocking"}"#;
3986 let old: GovernanceRule = serde_json::from_str(legacy).unwrap();
3987 assert!(old.action_selector.is_none());
3988 assert!(!old.force_on_match);
3989 }
3990
3991 #[test]
3994 fn principal_propagates_to_result() {
3995 let engine = GovernanceEngine::open();
3996 let principal = GatePrincipal::agent("child-1")
3997 .with_user("user-alice")
3998 .with_parent("parent-agent")
3999 .with_conv("conv-P");
4000 let req = GovernanceRequest::new("child-1", "tool.read_file").with_principal(principal);
4001 let result = engine.evaluate(&req);
4002 let p = result.principal.expect("principal on result");
4003 assert_eq!(p.agent_id, "child-1");
4004 assert_eq!(p.user_id.as_deref(), Some("user-alice"));
4005 assert_eq!(p.parent_agent_id.as_deref(), Some("parent-agent"));
4006 assert_eq!(p.conv_id.as_deref(), Some("conv-P"));
4007 }
4008
4009 #[test]
4010 fn principal_synthesised_from_agent_id_when_absent() {
4011 let engine = GovernanceEngine::open();
4012 let req = GovernanceRequest {
4013 agent_id: "solo".into(),
4014 action: "noop".into(),
4015 effect: EffectVector::default(),
4016 context: Default::default(),
4017 node_id: None,
4018 principal: None,
4019 };
4020 let p = engine.evaluate(&req).principal.expect("synthesised");
4021 assert_eq!(p.agent_id, "solo");
4022 assert!(p.user_id.is_none());
4023 }
4024
4025 #[test]
4026 fn gate_principal_serde_roundtrip() {
4027 let p = GatePrincipal::agent("a")
4028 .with_user("u")
4029 .with_parent("p")
4030 .with_conv("c");
4031 let json = serde_json::to_string(&p).unwrap();
4032 let restored: GatePrincipal = serde_json::from_str(&json).unwrap();
4033 assert_eq!(restored, p);
4034 }
4035
4036 #[test]
4037 fn gate_principal_old_json_parses_and_new_fields_stay_off_the_wire() {
4038 let old = r#"{"agent_id":"a","user_id":"u","parent_agent_id":"p","conv_id":"c"}"#;
4040 let p: GatePrincipal = serde_json::from_str(old).unwrap();
4041 assert_eq!(p.project_id(), None);
4042 assert_eq!(p.instance_id(), None);
4043 assert_eq!(serde_json::to_string(&p).unwrap(), old);
4045 let bare: GatePrincipal = serde_json::from_str("{}").unwrap();
4047 assert_eq!(bare, GatePrincipal::default());
4048 }
4049
4050 #[test]
4051 fn gate_principal_project_and_instance_round_trip() {
4052 let att = ProjectAttestation::from_verified(
4053 "01JB8Z3Q0V6X9KQ4M2N7T5R1WD",
4054 AttestSource::UserForward,
4055 );
4056 assert_eq!(att.source(), AttestSource::UserForward);
4057 let p = GatePrincipal::agent("a")
4058 .with_project(&att)
4059 .with_instance("6a3803d5f059902a1c6dafbc9ba47292");
4060 let v = serde_json::to_value(&p).unwrap();
4061 assert_eq!(v["project_id"], "01JB8Z3Q0V6X9KQ4M2N7T5R1WD");
4062 assert_eq!(v["instance_id"], "6a3803d5f059902a1c6dafbc9ba47292");
4063 let back: GatePrincipal = serde_json::from_value(v).unwrap();
4064 assert_eq!(back.project_id(), None);
4066 assert_eq!(back.instance_id(), None);
4067 assert_eq!(back.agent_id, p.agent_id);
4068 }
4069
4070 #[test]
4073 fn gate_effect_kind_dimensions_pinned() {
4074 let auth = GateEffectKind::AuthCredentialRegister.effect_vector();
4075 assert!((auth.security - 0.8).abs() < 1e-12);
4076 assert!((auth.privacy - 0.6).abs() < 1e-12);
4077 assert!((auth.risk - 0.5).abs() < 1e-12);
4078
4079 let config = GateEffectKind::ConfigSet.effect_vector();
4080 assert!((config.risk - 0.35).abs() < 1e-12);
4081 assert!((config.security - 0.4).abs() < 1e-12);
4082
4083 let secret = GateEffectKind::ConfigSecretSet.effect_vector();
4084 assert!(secret.privacy > 0.8);
4085 assert!(secret.security > 0.8);
4086
4087 let a2a = GateEffectKind::A2aRouting.effect_vector();
4088 assert!((a2a.risk - 0.25).abs() < 1e-12);
4089
4090 let cron = GateEffectKind::CronAdd.effect_vector();
4091 assert!((cron.risk - 0.2).abs() < 1e-12);
4092 assert!((cron.security - 0.1).abs() < 1e-12);
4093
4094 let ctx = with_effect_context(
4096 serde_json::json!({"pid": 1}),
4097 &GateEffectKind::AuthTokenIssue.effect_vector(),
4098 );
4099 assert!(ctx.get("effect").is_some());
4100 assert_eq!(ctx["effect"]["security"], 0.7);
4101 assert_eq!(ctx["pid"], 1);
4102 }
4103}