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