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    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// ---------------------------------------------------------------------------
40// Verdict
41// ---------------------------------------------------------------------------
42
43/// Result of kernel adjudication: permitted, denied with violations, or escalated for review.
44#[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// ---------------------------------------------------------------------------
61// Action request
62// ---------------------------------------------------------------------------
63
64/// A request submitted to the kernel for adjudication against constitutional invariants.
65#[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// ---------------------------------------------------------------------------
75// Adjudication context
76// ---------------------------------------------------------------------------
77
78/// Contextual evidence (roles, authority chain, consent, etc.) supplied alongside an action request.
79#[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    /// When set, the action is under an active Sybil challenge hold.
92    /// The kernel can pause otherwise valid or reviewable authority/quorum
93    /// cases as `Verdict::Escalated`, but final constitutional denials still
94    /// win after invariant checks run.
95    /// Populate from `ContestHold::escalation_reason()` in exo-escalation.
96    pub active_challenge_reason: Option<String>,
97}
98
99// ---------------------------------------------------------------------------
100// Kernel
101// ---------------------------------------------------------------------------
102
103/// Immutable constitutional governance kernel that adjudicates actions against invariants.
104#[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
191/// Adapter that binds a BCTS transition boundary to a concrete kernel
192/// adjudication request and context.
193pub 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// ===========================================================================
245// Tests
246// ===========================================================================
247
248#[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    // -----------------------------------------------------------------------
547    // WO-009: No-Admin Preservation
548    //
549    // CR-001 §8.9 — "No admins is ratified as a definitional guardrail."
550    // Any implementation shortcut creating a de facto admin bypass of AEGIS
551    // SHALL be prohibited.
552    //
553    // Audit finding (2026-03-30): no bypass paths found in any crate.
554    // Kernel::adjudicate is the single adjudication codepath.  The tests
555    // below explicitly verify that known escalation patterns — inflated
556    // permissions, multi-branch roles, empty authority chains, suppressed
557    // human oversight, and kernel modification attempts — are all denied.
558    // -----------------------------------------------------------------------
559    mod no_admin_bypass {
560        use super::*;
561
562        /// WO-009 §1: The gateway dev-scaffold context (BailmentState::None +
563        /// empty AuthorityChain) MUST be denied.  It is NOT a bypass path.
564        #[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        /// WO-009 §2: Holding all three constitutional branches simultaneously
591        /// is denied by SeparationOfPowers.  No omnipotent admin role exists.
592        #[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        /// WO-009 §3: Inflated permission sets cannot override ConsentRequired.
618        /// No permission label — including "admin" or "override" — bypasses
619        /// bailment enforcement.
620        #[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        /// F-010: required permissions must be backed by the actor context and
640        /// by the signed authority chain, not merely supplied on the action.
641        #[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        /// WO-009 §4: An empty authority chain is never permitted, even when all
655        /// other context fields are valid.  Per kernel escalation rules, an
656        /// isolated AuthorityChainValid violation escalates (not denies) — the
657        /// important WO-009 guarantee is that it is NOT `Permitted`.
658        #[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        /// WO-009 §5: human_override_preserved = false is always denied.
673        /// No admin path can suppress human oversight of AEGIS.
674        #[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        /// WO-009 §6: modifies_kernel = true is always denied.
687        /// Kernel immutability is unconditional — no escalation path exists.
688        #[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    // -----------------------------------------------------------------------
704    // WO-005: Challenge paths — active Sybil holds pause reviewable actions
705    // without suppressing final constitutional denials.
706    // -----------------------------------------------------------------------
707    mod challenge_paths {
708        use super::*;
709
710        /// WO-005: An otherwise valid action under an active Sybil challenge
711        /// returns Verdict::Escalated so it is paused pending review.
712        #[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        /// WO-005: Without a challenge, the same context produces Permitted.
734        #[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        /// WO-005: Challenge escalation can pause reviewable authority-chain
748        /// failures while the challenge is pending.
749        #[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        /// Challenge holds must not suppress final constitutional denials.
767        #[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        /// Human override suppression is a final denial even when another
791        /// challenge is active.
792        #[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}