Skip to main content

exo_gatekeeper/
kernel.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! CGR (Constitutional Governance Runtime) Kernel.
18//!
19//! The kernel is immutable after initialization. It holds the invariant set
20//! and constitution hash, and adjudicates every action request.
21
22use 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// ---------------------------------------------------------------------------
41// Verdict
42// ---------------------------------------------------------------------------
43
44/// Result of kernel adjudication: permitted, denied with violations, or escalated for review.
45#[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// ---------------------------------------------------------------------------
62// Action request
63// ---------------------------------------------------------------------------
64
65/// A request submitted to the kernel for adjudication against constitutional invariants.
66#[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// ---------------------------------------------------------------------------
76// Adjudication context
77// ---------------------------------------------------------------------------
78
79/// Contextual evidence (roles, authority chain, consent, etc.) supplied alongside an action request.
80#[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    /// When set, the action is under an active Sybil challenge hold.
93    /// The kernel can pause otherwise valid or reviewable authority/quorum
94    /// cases as `Verdict::Escalated`, but final constitutional denials still
95    /// win after invariant checks run.
96    /// Populate from `ContestHold::escalation_reason()` in exo-escalation.
97    pub active_challenge_reason: Option<String>,
98}
99
100// ---------------------------------------------------------------------------
101// Kernel
102// ---------------------------------------------------------------------------
103
104/// Immutable constitutional governance kernel that adjudicates actions against invariants.
105#[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    /// Re-evaluate every configured invariant and return the live results.
181    #[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
215/// Adapter that binds a BCTS transition boundary to a concrete kernel
216/// adjudication request and context.
217pub 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// ===========================================================================
269// Tests
270// ===========================================================================
271
272#[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    // -----------------------------------------------------------------------
571    // WO-009: No-Admin Preservation
572    //
573    // CR-001 §8.9 — "No admins is ratified as a definitional guardrail."
574    // Any implementation shortcut creating a de facto admin bypass of AEGIS
575    // SHALL be prohibited.
576    //
577    // Audit finding (2026-03-30): no bypass paths found in any crate.
578    // Kernel::adjudicate is the single adjudication codepath.  The tests
579    // below explicitly verify that known escalation patterns — inflated
580    // permissions, multi-branch roles, empty authority chains, suppressed
581    // human oversight, and kernel modification attempts — are all denied.
582    // -----------------------------------------------------------------------
583    mod no_admin_bypass {
584        use super::*;
585
586        /// WO-009 §1: The gateway dev-scaffold context (BailmentState::None +
587        /// empty AuthorityChain) MUST be denied.  It is NOT a bypass path.
588        #[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        /// WO-009 §2: Holding all three constitutional branches simultaneously
615        /// is denied by SeparationOfPowers.  No omnipotent admin role exists.
616        #[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        /// WO-009 §3: Inflated permission sets cannot override ConsentRequired.
642        /// No permission label — including "admin" or "override" — bypasses
643        /// bailment enforcement.
644        #[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        /// F-010: required permissions must be backed by the actor context and
664        /// by the signed authority chain, not merely supplied on the action.
665        #[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        /// WO-009 §4: An empty authority chain is never permitted, even when all
679        /// other context fields are valid.  Per kernel escalation rules, an
680        /// isolated AuthorityChainValid violation escalates (not denies) — the
681        /// important WO-009 guarantee is that it is NOT `Permitted`.
682        #[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        /// WO-009 §5: human_override_preserved = false is always denied.
697        /// No admin path can suppress human oversight of AEGIS.
698        #[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        /// WO-009 §6: modifies_kernel = true is always denied.
711        /// Kernel immutability is unconditional — no escalation path exists.
712        #[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    // -----------------------------------------------------------------------
728    // WO-005: Challenge paths — active Sybil holds pause reviewable actions
729    // without suppressing final constitutional denials.
730    // -----------------------------------------------------------------------
731    mod challenge_paths {
732        use super::*;
733
734        /// WO-005: An otherwise valid action under an active Sybil challenge
735        /// returns Verdict::Escalated so it is paused pending review.
736        #[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        /// WO-005: Without a challenge, the same context produces Permitted.
758        #[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        /// WO-005: Challenge escalation can pause reviewable authority-chain
772        /// failures while the challenge is pending.
773        #[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        /// Challenge holds must not suppress final constitutional denials.
791        #[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        /// Human override suppression is a final denial even when another
815        /// challenge is active.
816        #[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}