1use exo_core::{
23 Did, ExoError,
24 bcts::{BctsTransitionAdjudicator, BctsTransitionRequest},
25};
26use serde::{Deserialize, Serialize};
27
28use crate::{
29 cgr_trace::{CgrInvariantCheck, invariant_check},
30 invariants::{
31 ConstitutionalInvariant, InvariantContext, InvariantEngine, InvariantSet,
32 InvariantViolation, enforce_all, evaluate_all,
33 },
34 types::{
35 AuthorityChain, BailmentState, ConsentRecord, PermissionSet, Provenance, QuorumEvidence,
36 Role, TrustedAuthorityKeys, TrustedProvenanceKeys,
37 },
38};
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum Verdict {
47 Permitted,
48 Denied { violations: Vec<InvariantViolation> },
49 Escalated { reason: String },
50}
51
52impl Verdict {
53 pub fn is_permitted(&self) -> bool {
54 matches!(self, Verdict::Permitted)
55 }
56 pub fn is_denied(&self) -> bool {
57 matches!(self, Verdict::Denied { .. })
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ActionRequest {
68 pub actor: Did,
69 pub action: String,
70 pub required_permissions: PermissionSet,
71 pub is_self_grant: bool,
72 pub modifies_kernel: bool,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AdjudicationContext {
82 pub actor_roles: Vec<Role>,
83 pub authority_chain: AuthorityChain,
84 pub consent_records: Vec<ConsentRecord>,
85 pub bailment_state: BailmentState,
86 pub human_override_preserved: bool,
87 pub actor_permissions: PermissionSet,
88 pub trusted_authority_keys: TrustedAuthorityKeys,
89 pub trusted_provenance_keys: TrustedProvenanceKeys,
90 pub provenance: Option<Provenance>,
91 pub quorum_evidence: Option<QuorumEvidence>,
92 pub active_challenge_reason: Option<String>,
98}
99
100#[derive(Debug, Clone)]
106pub struct Kernel {
107 constitution_hash: [u8; 32],
108 invariant_engine: InvariantEngine,
109}
110
111impl Kernel {
112 #[must_use]
113 pub fn new(constitution: &[u8], invariants: InvariantSet) -> Self {
114 let hash = blake3::hash(constitution);
115 Self {
116 constitution_hash: *hash.as_bytes(),
117 invariant_engine: InvariantEngine::new(invariants),
118 }
119 }
120
121 fn invariant_context(
122 action: &ActionRequest,
123 context: &AdjudicationContext,
124 ) -> InvariantContext {
125 InvariantContext {
126 actor: action.actor.clone(),
127 actor_roles: context.actor_roles.clone(),
128 bailment_state: context.bailment_state.clone(),
129 consent_records: context.consent_records.clone(),
130 authority_chain: context.authority_chain.clone(),
131 is_self_grant: action.is_self_grant,
132 human_override_preserved: context.human_override_preserved,
133 kernel_modification_attempted: action.modifies_kernel,
134 quorum_evidence: context.quorum_evidence.clone(),
135 provenance: context.provenance.clone(),
136 actor_permissions: context.actor_permissions.clone(),
137 requested_permissions: action.required_permissions.clone(),
138 trusted_authority_keys: context.trusted_authority_keys.clone(),
139 trusted_provenance_keys: context.trusted_provenance_keys.clone(),
140 }
141 }
142
143 pub fn adjudicate(&self, action: &ActionRequest, context: &AdjudicationContext) -> Verdict {
144 let inv_ctx = Self::invariant_context(action, context);
145
146 match enforce_all(&self.invariant_engine, &inv_ctx) {
147 Ok(()) => match &context.active_challenge_reason {
148 Some(reason) => Verdict::Escalated {
149 reason: reason.clone(),
150 },
151 None => Verdict::Permitted,
152 },
153 Err(violations) => {
154 if let Some(reason) = &context.active_challenge_reason {
155 if violations.iter().all(is_challenge_pause_eligible) {
156 return Verdict::Escalated {
157 reason: reason.clone(),
158 };
159 }
160 }
161 verdict_for_violations(violations)
162 }
163 }
164 }
165
166 pub fn verify_kernel_integrity(&self, constitution: &[u8]) -> bool {
167 *blake3::hash(constitution).as_bytes() == self.constitution_hash
168 }
169
170 #[must_use]
171 pub fn constitution_hash(&self) -> &[u8; 32] {
172 &self.constitution_hash
173 }
174
175 #[must_use]
176 pub fn invariant_engine(&self) -> &InvariantEngine {
177 &self.invariant_engine
178 }
179
180 #[must_use]
182 pub fn evaluate_invariants(
183 &self,
184 action: &ActionRequest,
185 context: &AdjudicationContext,
186 ) -> Vec<CgrInvariantCheck> {
187 evaluate_all(
188 &self.invariant_engine,
189 &Self::invariant_context(action, context),
190 )
191 .into_iter()
192 .map(|(invariant, passed)| invariant_check(invariant, passed))
193 .collect()
194 }
195}
196
197fn is_challenge_pause_eligible(violation: &InvariantViolation) -> bool {
198 matches!(
199 violation.invariant,
200 ConstitutionalInvariant::QuorumLegitimate | ConstitutionalInvariant::AuthorityChainValid
201 )
202}
203
204fn verdict_for_violations(violations: Vec<InvariantViolation>) -> Verdict {
205 let needs_escalation = violations.iter().any(is_challenge_pause_eligible);
206 if needs_escalation && violations.len() == 1 {
207 Verdict::Escalated {
208 reason: violations[0].description.clone(),
209 }
210 } else {
211 Verdict::Denied { violations }
212 }
213}
214
215pub struct KernelBctsAdjudicator<'a> {
218 kernel: &'a Kernel,
219 action: &'a ActionRequest,
220 context: &'a AdjudicationContext,
221}
222
223impl<'a> KernelBctsAdjudicator<'a> {
224 #[must_use]
225 pub fn new(
226 kernel: &'a Kernel,
227 action: &'a ActionRequest,
228 context: &'a AdjudicationContext,
229 ) -> Self {
230 Self {
231 kernel,
232 action,
233 context,
234 }
235 }
236}
237
238impl BctsTransitionAdjudicator for KernelBctsAdjudicator<'_> {
239 fn adjudicate_transition(&self, request: &BctsTransitionRequest) -> exo_core::Result<()> {
240 if self.action.actor != request.actor_did {
241 return Err(ExoError::InvariantViolation {
242 description: format!(
243 "BCTS transition actor {} does not match adjudicated action actor {}",
244 request.actor_did, self.action.actor
245 ),
246 });
247 }
248
249 match self.kernel.adjudicate(self.action, self.context) {
250 Verdict::Permitted => Ok(()),
251 Verdict::Denied { violations } => {
252 let reason = violations
253 .iter()
254 .map(|v| format!("{}: {}", v.invariant.id(), v.description))
255 .collect::<Vec<_>>()
256 .join("; ");
257 Err(ExoError::InvariantViolation {
258 description: format!("BCTS transition denied by kernel: {reason}"),
259 })
260 }
261 Verdict::Escalated { reason } => Err(ExoError::InvariantViolation {
262 description: format!("BCTS transition escalated by kernel: {reason}"),
263 }),
264 }
265 }
266}
267
268#[cfg(test)]
273#[allow(clippy::expect_used, clippy::unwrap_used)]
274mod tests {
275 use super::*;
276 use crate::{
277 invariants::{authority_link_signature_message, provenance_signature_message},
278 types::{
279 AuthorityLink, GovernmentBranch, Permission, QuorumVote, TrustedAuthorityKeys,
280 TrustedProvenanceKeys,
281 },
282 };
283
284 const CONSTITUTION: &[u8] = b"We the people of the EXOCHAIN...";
285
286 fn did(s: &str) -> Did {
287 Did::new(s).expect("valid DID")
288 }
289
290 fn signed_link(grantor_str: &str, grantee: &Did) -> AuthorityLink {
291 let (pk, sk) = exo_core::crypto::generate_keypair();
292 let grantor = did(grantor_str);
293 let permissions = PermissionSet::new(vec![Permission::new("read")]);
294 let mut link = AuthorityLink {
295 grantor,
296 grantee: grantee.clone(),
297 permissions,
298 signature: Vec::new(),
299 grantor_public_key: Some(pk.as_bytes().to_vec()),
300 };
301 let message = authority_link_signature_message(&link).expect("canonical link payload");
302 let signature = exo_core::crypto::sign(message.as_bytes(), &sk);
303 link.signature = signature.to_bytes().to_vec();
304 link
305 }
306
307 fn signed_provenance(actor: &Did) -> (Provenance, exo_core::PublicKey) {
308 let (pk, sk) = exo_core::crypto::generate_keypair();
309 let timestamp = "2025-01-01T00:00:00Z".to_owned();
310 let action_hash = vec![1, 2, 3];
311 let mut provenance = Provenance {
312 actor: actor.clone(),
313 timestamp,
314 action_hash,
315 signature: Vec::new(),
316 public_key: Some(pk.as_bytes().to_vec()),
317 voice_kind: None,
318 independence: None,
319 review_order: None,
320 };
321 let message =
322 provenance_signature_message(&provenance).expect("canonical provenance payload");
323 let signature = exo_core::crypto::sign(message.as_bytes(), &sk);
324 provenance.signature = signature.to_bytes().to_vec();
325 (provenance, pk)
326 }
327
328 fn test_kernel() -> Kernel {
329 Kernel::new(CONSTITUTION, InvariantSet::all())
330 }
331
332 fn valid_action(actor: &Did) -> ActionRequest {
333 ActionRequest {
334 actor: actor.clone(),
335 action: "read medical record".into(),
336 required_permissions: PermissionSet::new(vec![Permission::new("read")]),
337 is_self_grant: false,
338 modifies_kernel: false,
339 }
340 }
341
342 fn valid_context(actor: &Did) -> AdjudicationContext {
343 let authority_chain = AuthorityChain {
344 links: vec![signed_link("did:exo:root", actor)],
345 };
346 let mut trusted_authority_keys = TrustedAuthorityKeys::default();
347 for link in &authority_chain.links {
348 if let Some(public_key) = &link.grantor_public_key {
349 trusted_authority_keys.insert(link.grantor.clone(), vec![public_key.clone()]);
350 }
351 }
352 let (provenance, provenance_public_key) = signed_provenance(actor);
353 let mut trusted_provenance_keys = TrustedProvenanceKeys::default();
354 trusted_provenance_keys.insert(
355 actor.clone(),
356 vec![provenance_public_key.as_bytes().to_vec()],
357 );
358 AdjudicationContext {
359 actor_roles: vec![Role {
360 name: "judge".into(),
361 branch: GovernmentBranch::Judicial,
362 }],
363 authority_chain,
364 consent_records: vec![ConsentRecord {
365 subject: did("did:exo:bailor"),
366 granted_to: actor.clone(),
367 scope: "data:read".into(),
368 active: true,
369 }],
370 bailment_state: BailmentState::Active {
371 bailor: did("did:exo:bailor"),
372 bailee: actor.clone(),
373 scope: "data:read".into(),
374 },
375 human_override_preserved: true,
376 actor_permissions: PermissionSet::new(vec![Permission::new("read")]),
377 trusted_authority_keys,
378 trusted_provenance_keys,
379 provenance: Some(provenance),
380 quorum_evidence: None,
381 active_challenge_reason: None,
382 }
383 }
384
385 #[test]
386 fn kernel_hashes_constitution() {
387 let kernel = test_kernel();
388 assert_eq!(
389 kernel.constitution_hash(),
390 blake3::hash(CONSTITUTION).as_bytes()
391 );
392 }
393
394 #[test]
395 fn verify_integrity_matches() {
396 assert!(test_kernel().verify_kernel_integrity(CONSTITUTION));
397 }
398
399 #[test]
400 fn verify_integrity_fails_tampered() {
401 assert!(!test_kernel().verify_kernel_integrity(b"TAMPERED"));
402 }
403
404 #[test]
405 fn cp1_separation_denies_multi_branch() {
406 let kernel = test_kernel();
407 let actor = did("did:exo:actor1");
408 let mut ctx = valid_context(&actor);
409 ctx.actor_roles = vec![
410 Role {
411 name: "senator".into(),
412 branch: GovernmentBranch::Legislative,
413 },
414 Role {
415 name: "judge".into(),
416 branch: GovernmentBranch::Judicial,
417 },
418 ];
419 assert!(kernel.adjudicate(&valid_action(&actor), &ctx).is_denied());
420 }
421
422 #[test]
423 fn cp1_separation_permits_single_branch() {
424 let kernel = test_kernel();
425 let actor = did("did:exo:actor1");
426 assert!(
427 kernel
428 .adjudicate(&valid_action(&actor), &valid_context(&actor))
429 .is_permitted()
430 );
431 }
432
433 #[test]
434 fn cp2_consent_denies_no_bailment() {
435 let kernel = test_kernel();
436 let actor = did("did:exo:actor1");
437 let mut ctx = valid_context(&actor);
438 ctx.bailment_state = BailmentState::None;
439 assert!(kernel.adjudicate(&valid_action(&actor), &ctx).is_denied());
440 }
441
442 #[test]
443 fn cp2_consent_permits_active() {
444 let kernel = test_kernel();
445 let actor = did("did:exo:actor1");
446 assert!(
447 kernel
448 .adjudicate(&valid_action(&actor), &valid_context(&actor))
449 .is_permitted()
450 );
451 }
452
453 #[test]
454 fn cp3_no_self_grant_denies() {
455 let kernel = test_kernel();
456 let actor = did("did:exo:actor1");
457 let mut action = valid_action(&actor);
458 action.is_self_grant = true;
459 assert!(
460 kernel
461 .adjudicate(&action, &valid_context(&actor))
462 .is_denied()
463 );
464 }
465
466 #[test]
467 fn cp3_no_self_grant_permits() {
468 let kernel = test_kernel();
469 let actor = did("did:exo:actor1");
470 assert!(
471 kernel
472 .adjudicate(&valid_action(&actor), &valid_context(&actor))
473 .is_permitted()
474 );
475 }
476
477 #[test]
478 fn cp4_human_override_denies() {
479 let kernel = test_kernel();
480 let actor = did("did:exo:actor1");
481 let mut ctx = valid_context(&actor);
482 ctx.human_override_preserved = false;
483 assert!(kernel.adjudicate(&valid_action(&actor), &ctx).is_denied());
484 }
485
486 #[test]
487 fn cp4_human_override_permits() {
488 let kernel = test_kernel();
489 let actor = did("did:exo:actor1");
490 assert!(
491 kernel
492 .adjudicate(&valid_action(&actor), &valid_context(&actor))
493 .is_permitted()
494 );
495 }
496
497 #[test]
498 fn cp5_kernel_immutability_denies() {
499 let kernel = test_kernel();
500 let actor = did("did:exo:actor1");
501 let mut action = valid_action(&actor);
502 action.modifies_kernel = true;
503 assert!(
504 kernel
505 .adjudicate(&action, &valid_context(&actor))
506 .is_denied()
507 );
508 }
509
510 #[test]
511 fn cp5_kernel_immutability_permits() {
512 let kernel = test_kernel();
513 let actor = did("did:exo:actor1");
514 assert!(
515 kernel
516 .adjudicate(&valid_action(&actor), &valid_context(&actor))
517 .is_permitted()
518 );
519 }
520
521 #[test]
522 fn escalation_for_quorum_violation() {
523 let kernel = test_kernel();
524 let actor = did("did:exo:actor1");
525 let mut ctx = valid_context(&actor);
526 ctx.quorum_evidence = Some(QuorumEvidence {
527 threshold: 3,
528 votes: vec![
529 QuorumVote {
530 voter: did("did:exo:v1"),
531 approved: true,
532 signature: vec![1],
533 provenance: None,
534 },
535 QuorumVote {
536 voter: did("did:exo:v2"),
537 approved: false,
538 signature: vec![2],
539 provenance: None,
540 },
541 ],
542 });
543 match kernel.adjudicate(&valid_action(&actor), &ctx) {
544 Verdict::Escalated { reason } => assert!(reason.contains("Quorum")),
545 other => panic!("Expected Escalated, got {:?}", other),
546 }
547 }
548
549 #[test]
550 fn verdict_helpers() {
551 assert!(Verdict::Permitted.is_permitted());
552 assert!(!Verdict::Permitted.is_denied());
553 let denied = Verdict::Denied { violations: vec![] };
554 assert!(denied.is_denied());
555 assert!(!denied.is_permitted());
556 }
557
558 #[test]
559 fn kernel_engine_accessor() {
560 assert_eq!(
561 test_kernel()
562 .invariant_engine()
563 .invariant_set
564 .invariants
565 .len(),
566 8
567 );
568 }
569
570 mod no_admin_bypass {
584 use super::*;
585
586 #[test]
589 fn dev_scaffold_context_is_deny_all() {
590 let kernel = test_kernel();
591 let actor = did("did:exo:any-actor");
592 let scaffold_ctx = AdjudicationContext {
593 actor_roles: vec![],
594 authority_chain: AuthorityChain::default(),
595 consent_records: vec![],
596 bailment_state: BailmentState::None,
597 human_override_preserved: true,
598 actor_permissions: PermissionSet::new(vec![Permission::new("vote")]),
599 trusted_authority_keys: TrustedAuthorityKeys::default(),
600 trusted_provenance_keys: TrustedProvenanceKeys::default(),
601 provenance: None,
602 quorum_evidence: None,
603 active_challenge_reason: None,
604 };
605 assert!(
606 kernel
607 .adjudicate(&valid_action(&actor), &scaffold_ctx)
608 .is_denied(),
609 "WO-009: dev-scaffold context must be denied — BailmentState::None \
610 fails ConsentRequired invariant"
611 );
612 }
613
614 #[test]
617 fn all_government_branches_simultaneously_denied() {
618 let kernel = test_kernel();
619 let actor = did("did:exo:multi-branch-admin");
620 let mut ctx = valid_context(&actor);
621 ctx.actor_roles = vec![
622 Role {
623 name: "executive-admin".into(),
624 branch: GovernmentBranch::Executive,
625 },
626 Role {
627 name: "legislator".into(),
628 branch: GovernmentBranch::Legislative,
629 },
630 Role {
631 name: "judge".into(),
632 branch: GovernmentBranch::Judicial,
633 },
634 ];
635 assert!(
636 kernel.adjudicate(&valid_action(&actor), &ctx).is_denied(),
637 "WO-009: omnipotent multi-branch actor must be denied by SeparationOfPowers"
638 );
639 }
640
641 #[test]
645 fn maximum_permissions_cannot_bypass_consent() {
646 let kernel = test_kernel();
647 let actor = did("did:exo:permission-inflated");
648 let mut ctx = valid_context(&actor);
649 ctx.actor_permissions = PermissionSet::new(vec![
650 Permission::new("read"),
651 Permission::new("write"),
652 Permission::new("admin"),
653 Permission::new("execute"),
654 Permission::new("override"),
655 ]);
656 ctx.bailment_state = BailmentState::None;
657 assert!(
658 kernel.adjudicate(&valid_action(&actor), &ctx).is_denied(),
659 "WO-009: inflated permission set must not bypass ConsentRequired invariant"
660 );
661 }
662
663 #[test]
666 fn missing_required_permission_not_permitted() {
667 let kernel = test_kernel();
668 let actor = did("did:exo:scope-mismatch");
669 let mut action = valid_action(&actor);
670 action.required_permissions = PermissionSet::new(vec![Permission::new("advance_pace")]);
671 let verdict = kernel.adjudicate(&action, &valid_context(&actor));
672 assert!(
673 !verdict.is_permitted(),
674 "F-010: requested permission absent from authority evidence must not be permitted"
675 );
676 }
677
678 #[test]
683 fn empty_authority_chain_not_permitted() {
684 let kernel = test_kernel();
685 let actor = did("did:exo:no-chain");
686 let mut ctx = valid_context(&actor);
687 ctx.authority_chain = AuthorityChain::default();
688 let verdict = kernel.adjudicate(&valid_action(&actor), &ctx);
689 assert!(
690 !verdict.is_permitted(),
691 "WO-009: empty authority chain must not be permitted \
692 (escalated or denied, never Permitted)"
693 );
694 }
695
696 #[test]
699 fn human_override_suppression_is_non_bypassable() {
700 let kernel = test_kernel();
701 let actor = did("did:exo:override-suppressor");
702 let mut ctx = valid_context(&actor);
703 ctx.human_override_preserved = false;
704 assert!(
705 kernel.adjudicate(&valid_action(&actor), &ctx).is_denied(),
706 "WO-009: human override suppression must always be denied by HumanOverride"
707 );
708 }
709
710 #[test]
713 fn kernel_modification_always_denied() {
714 let kernel = test_kernel();
715 let actor = did("did:exo:kernel-patcher");
716 let mut action = valid_action(&actor);
717 action.modifies_kernel = true;
718 assert!(
719 kernel
720 .adjudicate(&action, &valid_context(&actor))
721 .is_denied(),
722 "WO-009: modifies_kernel must always be denied by KernelImmutability"
723 );
724 }
725 }
726
727 mod challenge_paths {
732 use super::*;
733
734 #[test]
737 fn active_challenge_escalates_not_denies() {
738 let kernel = test_kernel();
739 let actor = did("did:exo:actor1");
740 let mut ctx = valid_context(&actor);
741 ctx.active_challenge_reason =
742 Some("SybilChallenge/CoordinatedManipulation: action under review".into());
743 match kernel.adjudicate(&valid_action(&actor), &ctx) {
744 Verdict::Escalated { reason } => {
745 assert!(
746 reason.contains("SybilChallenge"),
747 "escalation reason must identify the challenge"
748 );
749 }
750 other => panic!(
751 "WO-005: active challenge must produce Escalated, got {:?}",
752 other
753 ),
754 }
755 }
756
757 #[test]
759 fn no_challenge_is_not_escalated() {
760 let kernel = test_kernel();
761 let actor = did("did:exo:actor1");
762 let ctx = valid_context(&actor);
763 assert!(
764 kernel
765 .adjudicate(&valid_action(&actor), &ctx)
766 .is_permitted(),
767 "WO-005: no active challenge must not cause escalation"
768 );
769 }
770
771 #[test]
774 fn challenge_can_pause_authority_chain_review() {
775 let kernel = test_kernel();
776 let actor = did("did:exo:actor1");
777 let mut ctx = valid_context(&actor);
778 ctx.authority_chain = AuthorityChain::default();
779 ctx.active_challenge_reason =
780 Some("SybilChallenge/QuorumContamination: pause-eligible".into());
781 match kernel.adjudicate(&valid_action(&actor), &ctx) {
782 Verdict::Escalated { .. } => {}
783 other => panic!(
784 "WO-005: challenge must pause authority review, got {:?}",
785 other
786 ),
787 }
788 }
789
790 #[test]
792 fn challenge_does_not_override_kernel_modification_denial() {
793 let kernel = test_kernel();
794 let actor = did("did:exo:actor1");
795 let mut action = valid_action(&actor);
796 action.modifies_kernel = true;
797 let mut ctx = valid_context(&actor);
798 ctx.active_challenge_reason =
799 Some("SybilChallenge/KernelPatch: action under review".into());
800 match kernel.adjudicate(&action, &ctx) {
801 Verdict::Denied { violations } => assert!(
802 violations
803 .iter()
804 .any(|v| v.invariant == ConstitutionalInvariant::KernelImmutability),
805 "kernel modification denial must be preserved: {violations:?}"
806 ),
807 other => panic!(
808 "WO-005: challenge must not suppress KernelImmutability denial, got {:?}",
809 other
810 ),
811 }
812 }
813
814 #[test]
817 fn challenge_does_not_override_human_override_denial() {
818 let kernel = test_kernel();
819 let actor = did("did:exo:actor1");
820 let mut ctx = valid_context(&actor);
821 ctx.human_override_preserved = false;
822 ctx.active_challenge_reason =
823 Some("SybilChallenge/HumanOverride: action under review".into());
824 match kernel.adjudicate(&valid_action(&actor), &ctx) {
825 Verdict::Denied { violations } => assert!(
826 violations
827 .iter()
828 .any(|v| v.invariant == ConstitutionalInvariant::HumanOverride),
829 "human override denial must be preserved: {violations:?}"
830 ),
831 other => panic!(
832 "WO-005: challenge must not suppress HumanOverride denial, got {:?}",
833 other
834 ),
835 }
836 }
837 }
838}