Skip to main content

exochain_sdk/
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//! Constitutional Governance Runtime (CGR) Kernel interface.
18//!
19//! The **CGR kernel** is the heart of EXOCHAIN. Every action that matters —
20//! reading data, delegating authority, invoking a tool — is first submitted
21//! to the kernel, which checks the action against the constitution and the
22//! eight structural invariants (including `NoSelfGrant`, `ConsentRequired`,
23//! and `KernelImmutability`). Only if the kernel returns `Permitted` does the
24//! action run.
25//!
26//! [`ConstitutionalKernel`] is a simplified, ergonomic wrapper around
27//! [`exo_gatekeeper::Kernel`]. It initialises the kernel with the default
28//! EXOCHAIN constitution text and the full set of eight constitutional
29//! invariants. Adjudication requires caller-supplied authority signing material
30//! so the SDK never fabricates a trust root.
31//!
32//! ## Why use this module
33//!
34//! - You want to ask "is this action permitted?" without having to construct
35//!   the full [`exo_gatekeeper::AdjudicationContext`] by hand.
36//! - You want to exercise specific invariants (self-grant, kernel
37//!   modification, consent-required) in tests via the named `adjudicate_*`
38//!   helpers.
39//! - You want the same verdict enum to flow through your application and
40//!   your test vectors.
41//!
42//! ## Quick start
43//!
44//! ```
45//! use exochain_sdk::kernel::ConstitutionalKernel;
46//! use exo_core::Did;
47//!
48//! let authority = exochain_sdk::identity::Identity::generate("authority");
49//! let kernel = ConstitutionalKernel::with_authority_identity(authority);
50//! let actor = Did::new("did:exo:alice").expect("valid");
51//! let verdict = kernel.adjudicate(&actor, "data:medical:read");
52//! assert!(verdict.is_permitted());
53//! ```
54
55use std::sync::Arc;
56
57use exo_core::{Did, Hash256, PublicKey, Signature};
58use exo_gatekeeper::{
59    ActionRequest, AdjudicationContext, InvariantSet, Kernel, Verdict,
60    authority_link_signature_message, provenance_signature_message,
61    types::{
62        AuthorityChain, AuthorityLink, BailmentState, ConsentRecord, GovernmentBranch, Permission,
63        PermissionSet, Provenance, Role, TrustedAuthorityKeys, TrustedProvenanceKeys,
64    },
65};
66use serde::{Deserialize, Serialize};
67
68/// The default constitution bytes used by [`ConstitutionalKernel::new`].
69const DEFAULT_CONSTITUTION: &[u8] = b"EXOCHAIN Constitution v1.0: \
70    We the people of the EXOCHAIN fabric establish this constitution \
71    to secure the blessings of ordered, consented, and auditable agency.";
72
73/// Expected number of constitutional invariants enforced by the kernel.
74const INVARIANT_COUNT: usize = 8;
75
76/// Verdict returned by the SDK kernel.
77///
78/// This mirrors [`exo_gatekeeper::Verdict`] but flattens the violation list
79/// to a simple `Vec<String>` so SDK consumers do not need to depend on the
80/// full gatekeeper types.
81///
82/// # Examples
83///
84/// ```
85/// use exochain_sdk::kernel::KernelVerdict;
86///
87/// let ok = KernelVerdict::Permitted;
88/// assert!(ok.is_permitted());
89///
90/// let denied = KernelVerdict::Denied { violations: vec!["NoSelfGrant".into()] };
91/// assert!(denied.is_denied());
92///
93/// let escalated = KernelVerdict::Escalated { reason: "human review".into() };
94/// assert!(escalated.is_escalated());
95/// ```
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub enum KernelVerdict {
98    /// The action is permitted.
99    Permitted,
100    /// The action is denied — one or more invariants were violated.
101    Denied {
102        /// Human-readable descriptions of the violated invariants.
103        violations: Vec<String>,
104    },
105    /// The action has been escalated for review.
106    Escalated {
107        /// Human-readable reason for escalation.
108        reason: String,
109    },
110}
111
112impl KernelVerdict {
113    /// Returns `true` if the verdict is [`KernelVerdict::Permitted`].
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// # use exochain_sdk::kernel::KernelVerdict;
119    /// assert!(KernelVerdict::Permitted.is_permitted());
120    /// assert!(!KernelVerdict::Denied { violations: vec![] }.is_permitted());
121    /// ```
122    #[must_use]
123    pub fn is_permitted(&self) -> bool {
124        matches!(self, Self::Permitted)
125    }
126
127    /// Returns `true` if the verdict is [`KernelVerdict::Denied`].
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// # use exochain_sdk::kernel::KernelVerdict;
133    /// let v = KernelVerdict::Denied { violations: vec!["NoSelfGrant".into()] };
134    /// assert!(v.is_denied());
135    /// ```
136    #[must_use]
137    pub fn is_denied(&self) -> bool {
138        matches!(self, Self::Denied { .. })
139    }
140
141    /// Returns `true` if the verdict is [`KernelVerdict::Escalated`].
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// # use exochain_sdk::kernel::KernelVerdict;
147    /// let v = KernelVerdict::Escalated { reason: "human in the loop".into() };
148    /// assert!(v.is_escalated());
149    /// ```
150    #[must_use]
151    pub fn is_escalated(&self) -> bool {
152        matches!(self, Self::Escalated { .. })
153    }
154}
155
156/// An ergonomic wrapper around the CGR [`Kernel`].
157///
158/// Provides a minimal adjudication interface suitable for common SDK use
159/// cases: a single actor performing an action, with caller-supplied authority
160/// signing material, signed provenance, and an active bailment from that
161/// authority. Callers needing fine-grained control over the adjudication
162/// context should use [`exo_gatekeeper::Kernel`] directly.
163///
164/// # Examples
165///
166/// ```
167/// use exochain_sdk::kernel::ConstitutionalKernel;
168/// use exo_core::Did;
169///
170/// let authority = exochain_sdk::identity::Identity::generate("authority");
171/// let kernel = ConstitutionalKernel::with_authority_identity(authority);
172/// assert!(kernel.verify_integrity());
173/// assert_eq!(kernel.invariant_count(), 8);
174///
175/// let actor = Did::new("did:exo:alice").expect("valid");
176/// let verdict = kernel.adjudicate(&actor, "read:profile");
177/// assert!(verdict.is_permitted());
178/// ```
179pub struct ConstitutionalKernel {
180    inner: Kernel,
181    constitution: Vec<u8>,
182    authority: Option<KernelAuthority>,
183}
184
185type AuthoritySigner = Arc<dyn Fn(&[u8]) -> Signature + Send + Sync>;
186
187struct KernelAuthority {
188    did: Did,
189    public_key: PublicKey,
190    signer: AuthoritySigner,
191}
192
193impl ConstitutionalKernel {
194    /// Construct a new kernel with the default constitution and all eight
195    /// constitutional invariants.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// # use exochain_sdk::kernel::ConstitutionalKernel;
201    /// let kernel = ConstitutionalKernel::new();
202    /// assert_eq!(kernel.invariant_count(), 8);
203    /// assert!(kernel.verify_integrity());
204    /// ```
205    #[must_use]
206    pub fn new() -> Self {
207        Self {
208            inner: Kernel::new(DEFAULT_CONSTITUTION, InvariantSet::all()),
209            constitution: DEFAULT_CONSTITUTION.to_vec(),
210            authority: None,
211        }
212    }
213
214    /// Construct a new kernel with an authority signing identity.
215    ///
216    /// This is the common SDK path: the identity's DID becomes the authority
217    /// grantor and bailor for the default adjudication context, and its secret
218    /// key signs the canonical authority/provenance payloads that the kernel
219    /// verifies.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// # use exochain_sdk::{identity::Identity, kernel::ConstitutionalKernel};
225    /// let authority = Identity::generate("authority");
226    /// let kernel = ConstitutionalKernel::with_authority_identity(authority);
227    /// assert!(kernel.verify_integrity());
228    /// ```
229    #[must_use]
230    pub fn with_authority_identity(authority: crate::identity::Identity) -> Self {
231        let authority_did = authority.did().clone();
232        let authority_public_key = *authority.public_key();
233        Self::with_authority(
234            authority_did,
235            authority_public_key,
236            Arc::new(move |message: &[u8]| authority.sign(message)),
237        )
238    }
239
240    /// Construct a new kernel with caller-supplied authority signing material.
241    ///
242    /// The signer must produce an Ed25519 signature over the message bytes it
243    /// receives. The supplied public key is embedded in the adjudication
244    /// context, and the gatekeeper verifies every signature cryptographically.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// # use std::sync::Arc;
250    /// # use exochain_sdk::{crypto, kernel::ConstitutionalKernel};
251    /// let authority_did = crypto::Did::new("did:exo:authority").expect("valid");
252    /// let (authority_public_key, authority_secret_key) = crypto::generate_keypair();
253    /// let kernel = ConstitutionalKernel::with_authority(
254    ///     authority_did,
255    ///     authority_public_key,
256    ///     Arc::new(move |message: &[u8]| crypto::sign(message, &authority_secret_key)),
257    /// );
258    /// assert!(kernel.verify_integrity());
259    /// ```
260    #[must_use]
261    pub fn with_authority(
262        authority_did: Did,
263        authority_public_key: PublicKey,
264        authority_signer: AuthoritySigner,
265    ) -> Self {
266        Self {
267            inner: Kernel::new(DEFAULT_CONSTITUTION, InvariantSet::all()),
268            constitution: DEFAULT_CONSTITUTION.to_vec(),
269            authority: Some(KernelAuthority {
270                did: authority_did,
271                public_key: authority_public_key,
272                signer: authority_signer,
273            }),
274        }
275    }
276
277    /// Adjudicate `action` performed by `actor` using the signed SDK context.
278    ///
279    /// The SDK supplies a minimal signed default context:
280    /// - A single Judicial role for `actor`.
281    /// - A one-link authority chain from the configured authority to `actor`
282    ///   granting `read`.
283    /// - An active bailment from the configured authority to `actor` scoped to
284    ///   the requested permission set.
285    /// - Full human-override preservation.
286    /// - Signed provenance with timestamp `"sdk"`.
287    ///
288    /// If the kernel was created with [`Self::new`] rather than
289    /// [`Self::with_authority`] or [`Self::with_authority_identity`], this
290    /// method fails closed with a denied verdict.
291    ///
292    /// Callers needing richer context should reach for
293    /// [`exo_gatekeeper::Kernel`] directly.
294    ///
295    /// The action is flagged as `is_self_grant = false` and
296    /// `modifies_kernel = false` by default. Helpers are available for the
297    /// common deny-cases used in tests: see
298    /// [`Self::adjudicate_self_grant`],
299    /// [`Self::adjudicate_kernel_modification`], and
300    /// [`Self::adjudicate_without_bailment`].
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// use exochain_sdk::kernel::ConstitutionalKernel;
306    /// use exo_core::Did;
307    ///
308    /// let authority = exochain_sdk::identity::Identity::generate("authority");
309    /// let kernel = ConstitutionalKernel::with_authority_identity(authority);
310    /// let actor = Did::new("did:exo:alice").expect("valid");
311    /// let verdict = kernel.adjudicate(&actor, "data:read");
312    /// assert!(verdict.is_permitted());
313    /// ```
314    #[must_use]
315    pub fn adjudicate(&self, actor: &Did, action: &str) -> KernelVerdict {
316        self.adjudicate_internal(actor, action, false, false, true, true)
317    }
318
319    /// Same as [`Self::adjudicate`] but sets `is_self_grant = true` so the
320    /// kernel can enforce the `NoSelfGrant` invariant.
321    ///
322    /// Useful for exercising the invariant in tests: a permitted verdict
323    /// here would indicate a constitutional defect.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use exochain_sdk::kernel::ConstitutionalKernel;
329    /// use exo_core::Did;
330    ///
331    /// let authority = exochain_sdk::identity::Identity::generate("authority");
332    /// let kernel = ConstitutionalKernel::with_authority_identity(authority);
333    /// let actor = Did::new("did:exo:self-granter").expect("valid");
334    /// let verdict = kernel.adjudicate_self_grant(&actor, "escalate-self");
335    /// assert!(verdict.is_denied());
336    /// ```
337    #[must_use]
338    pub fn adjudicate_self_grant(&self, actor: &Did, action: &str) -> KernelVerdict {
339        self.adjudicate_internal(actor, action, true, false, true, true)
340    }
341
342    /// Same as [`Self::adjudicate`] but sets `modifies_kernel = true` so the
343    /// kernel can enforce the `KernelImmutability` invariant.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use exochain_sdk::kernel::ConstitutionalKernel;
349    /// use exo_core::Did;
350    ///
351    /// let authority = exochain_sdk::identity::Identity::generate("authority");
352    /// let kernel = ConstitutionalKernel::with_authority_identity(authority);
353    /// let actor = Did::new("did:exo:patcher").expect("valid");
354    /// let verdict = kernel.adjudicate_kernel_modification(&actor, "patch-kernel");
355    /// assert!(verdict.is_denied());
356    /// ```
357    #[must_use]
358    pub fn adjudicate_kernel_modification(&self, actor: &Did, action: &str) -> KernelVerdict {
359        self.adjudicate_internal(actor, action, false, true, true, true)
360    }
361
362    /// Same as [`Self::adjudicate`] but omits the default bailment so the
363    /// kernel can enforce the `ConsentRequired` invariant.
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use exochain_sdk::kernel::ConstitutionalKernel;
369    /// use exo_core::Did;
370    ///
371    /// let authority = exochain_sdk::identity::Identity::generate("authority");
372    /// let kernel = ConstitutionalKernel::with_authority_identity(authority);
373    /// let actor = Did::new("did:exo:unauth").expect("valid");
374    /// let verdict = kernel.adjudicate_without_bailment(&actor, "read-data");
375    /// assert!(verdict.is_denied());
376    /// ```
377    #[must_use]
378    pub fn adjudicate_without_bailment(&self, actor: &Did, action: &str) -> KernelVerdict {
379        self.adjudicate_internal(actor, action, false, false, false, true)
380    }
381
382    /// Verify that the kernel's stored constitution hash matches the
383    /// configured constitution text.
384    ///
385    /// Returns `false` if the constitution in memory has drifted from the
386    /// hash the kernel was initialised with — which should never happen in
387    /// practice, but is checked defensively because constitutional integrity
388    /// is a load-bearing invariant.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// # use exochain_sdk::kernel::ConstitutionalKernel;
394    /// let kernel = ConstitutionalKernel::new();
395    /// assert!(kernel.verify_integrity());
396    /// ```
397    #[must_use]
398    pub fn verify_integrity(&self) -> bool {
399        self.inner.verify_kernel_integrity(&self.constitution)
400    }
401
402    /// Number of constitutional invariants enforced by this kernel (always 8).
403    ///
404    /// # Examples
405    ///
406    /// ```
407    /// # use exochain_sdk::kernel::ConstitutionalKernel;
408    /// assert_eq!(ConstitutionalKernel::new().invariant_count(), 8);
409    /// ```
410    #[must_use]
411    pub fn invariant_count(&self) -> usize {
412        INVARIANT_COUNT
413    }
414
415    fn signed_authority_link(
416        authority: &KernelAuthority,
417        grantee: &Did,
418        permissions: &PermissionSet,
419    ) -> exo_core::Result<AuthorityLink> {
420        let mut link = AuthorityLink {
421            grantor: authority.did.clone(),
422            grantee: grantee.clone(),
423            permissions: permissions.clone(),
424            signature: Vec::new(),
425            grantor_public_key: Some(authority.public_key.as_bytes().to_vec()),
426        };
427        let message = authority_link_signature_message(&link)?;
428        let signature = (authority.signer)(message.as_bytes());
429        link.signature = signature.to_bytes().to_vec();
430        Ok(link)
431    }
432
433    fn signed_provenance(
434        authority: &KernelAuthority,
435        actor: &Did,
436        action: &str,
437    ) -> exo_core::Result<Provenance> {
438        let timestamp = "sdk".to_owned();
439        let action_hash = Hash256::digest(action.as_bytes()).as_bytes().to_vec();
440
441        let mut provenance = Provenance {
442            actor: actor.clone(),
443            timestamp,
444            action_hash,
445            signature: Vec::new(),
446            public_key: Some(authority.public_key.as_bytes().to_vec()),
447            voice_kind: None,
448            independence: None,
449            review_order: None,
450        };
451        let message = provenance_signature_message(&provenance)?;
452        let signature = (authority.signer)(message.as_bytes());
453        provenance.signature = signature.to_bytes().to_vec();
454        Ok(provenance)
455    }
456
457    fn permission_scope(permissions: &PermissionSet) -> String {
458        let mut labels: Vec<&str> = permissions
459            .permissions
460            .iter()
461            .map(|permission| permission.0.as_str())
462            .collect();
463        labels.sort_unstable();
464        labels.dedup();
465        labels.join(";")
466    }
467
468    fn adjudicate_internal(
469        &self,
470        actor: &Did,
471        action: &str,
472        is_self_grant: bool,
473        modifies_kernel: bool,
474        include_bailment: bool,
475        human_override_preserved: bool,
476    ) -> KernelVerdict {
477        let Some(authority) = self.authority.as_ref() else {
478            return KernelVerdict::Denied {
479                violations: vec![
480                    "AuthorityChainValid: SDK authority signer is required for adjudication".into(),
481                ],
482            };
483        };
484
485        let permissions = PermissionSet::new(vec![Permission::new("read")]);
486        let request = ActionRequest {
487            actor: actor.clone(),
488            action: action.to_owned(),
489            required_permissions: permissions.clone(),
490            is_self_grant,
491            modifies_kernel,
492        };
493
494        let scope = Self::permission_scope(&permissions);
495
496        let (bailment_state, consent_records) = if include_bailment {
497            (
498                BailmentState::Active {
499                    bailor: authority.did.clone(),
500                    bailee: actor.clone(),
501                    scope: scope.clone(),
502                },
503                vec![ConsentRecord {
504                    subject: authority.did.clone(),
505                    granted_to: actor.clone(),
506                    scope,
507                    active: true,
508                }],
509            )
510        } else {
511            (BailmentState::None, Vec::new())
512        };
513
514        let authority_link = match Self::signed_authority_link(authority, actor, &permissions) {
515            Ok(link) => link,
516            Err(err) => {
517                return KernelVerdict::Denied {
518                    violations: vec![format!(
519                        "AuthorityChainValid: canonical authority signature payload failed: {err}"
520                    )],
521                };
522            }
523        };
524        let provenance = match Self::signed_provenance(authority, actor, action) {
525            Ok(provenance) => provenance,
526            Err(err) => {
527                return KernelVerdict::Denied {
528                    violations: vec![format!(
529                        "ProvenanceVerifiable: canonical provenance signature payload failed: {err}"
530                    )],
531                };
532            }
533        };
534
535        let authority_chain = AuthorityChain {
536            links: vec![authority_link],
537        };
538        let mut trusted_authority_keys = TrustedAuthorityKeys::default();
539        for link in &authority_chain.links {
540            if let Some(public_key) = &link.grantor_public_key {
541                trusted_authority_keys.insert(link.grantor.clone(), vec![public_key.clone()]);
542            }
543        }
544        let mut trusted_provenance_keys = TrustedProvenanceKeys::default();
545        trusted_provenance_keys.insert(
546            actor.clone(),
547            vec![authority.public_key.as_bytes().to_vec()],
548        );
549
550        let context = AdjudicationContext {
551            actor_roles: vec![Role {
552                name: "judge".into(),
553                branch: GovernmentBranch::Judicial,
554            }],
555            authority_chain,
556            consent_records,
557            bailment_state,
558            human_override_preserved,
559            actor_permissions: permissions,
560            trusted_authority_keys,
561            trusted_provenance_keys,
562            provenance: Some(provenance),
563            quorum_evidence: None,
564            active_challenge_reason: None,
565        };
566
567        match self.inner.adjudicate(&request, &context) {
568            Verdict::Permitted => KernelVerdict::Permitted,
569            Verdict::Denied { violations } => KernelVerdict::Denied {
570                violations: violations
571                    .into_iter()
572                    .map(|v| format!("{}: {}", v.invariant.id(), v.description))
573                    .collect(),
574            },
575            Verdict::Escalated { reason } => KernelVerdict::Escalated { reason },
576        }
577    }
578}
579
580impl Default for ConstitutionalKernel {
581    fn default() -> Self {
582        Self::new()
583    }
584}
585
586impl core::fmt::Debug for ConstitutionalKernel {
587    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
588        f.debug_struct("ConstitutionalKernel")
589            .field("invariant_count", &INVARIANT_COUNT)
590            .finish()
591    }
592}
593
594// ===========================================================================
595// Tests
596// ===========================================================================
597
598#[cfg(test)]
599#[allow(clippy::expect_used, clippy::unwrap_used)]
600mod tests {
601    use super::*;
602
603    fn did(s: &str) -> Did {
604        Did::new(s).expect("valid DID")
605    }
606
607    fn signed_kernel() -> ConstitutionalKernel {
608        ConstitutionalKernel::with_authority_identity(crate::identity::Identity::generate(
609            "sdk-authority",
610        ))
611    }
612
613    #[test]
614    fn new_initialises_with_eight_invariants() {
615        let k = ConstitutionalKernel::new();
616        assert_eq!(k.invariant_count(), 8);
617    }
618
619    #[test]
620    fn verify_integrity_holds_after_new() {
621        let k = ConstitutionalKernel::new();
622        assert!(k.verify_integrity());
623    }
624
625    #[test]
626    fn default_matches_new() {
627        let a = ConstitutionalKernel::default();
628        let b = ConstitutionalKernel::new();
629        assert_eq!(a.invariant_count(), b.invariant_count());
630        assert_eq!(a.verify_integrity(), b.verify_integrity());
631    }
632
633    #[test]
634    fn valid_action_permitted() {
635        let k = signed_kernel();
636        let actor = did("did:exo:valid-actor");
637        let verdict = k.adjudicate(&actor, "read-medical-record");
638        assert!(
639            verdict.is_permitted(),
640            "expected Permitted, got {verdict:?}"
641        );
642    }
643
644    #[test]
645    fn adjudicate_without_authority_signer_fails_closed() {
646        let k = ConstitutionalKernel::new();
647        let actor = did("did:exo:valid-actor");
648        let verdict = k.adjudicate(&actor, "read-medical-record");
649        assert!(verdict.is_denied(), "expected Denied, got {verdict:?}");
650        match verdict {
651            KernelVerdict::Denied { violations } => {
652                assert!(violations.iter().any(|v| v.contains("authority signer")));
653            }
654            other => panic!("expected Denied, got {other:?}"),
655        }
656    }
657
658    #[test]
659    fn self_grant_denied() {
660        let k = signed_kernel();
661        let actor = did("did:exo:self-granter");
662        let verdict = k.adjudicate_self_grant(&actor, "escalate-self");
663        assert!(verdict.is_denied(), "expected Denied, got {verdict:?}");
664        match verdict {
665            KernelVerdict::Denied { violations } => {
666                assert!(
667                    violations
668                        .iter()
669                        .any(|violation| violation.starts_with("no-self-grant: ")),
670                    "violations must use stable invariant IDs: {violations:?}"
671                );
672            }
673            other => panic!("expected Denied, got {other:?}"),
674        }
675    }
676
677    #[test]
678    fn kernel_modification_denied() {
679        let k = signed_kernel();
680        let actor = did("did:exo:patcher");
681        let verdict = k.adjudicate_kernel_modification(&actor, "patch-kernel");
682        assert!(verdict.is_denied(), "expected Denied, got {verdict:?}");
683    }
684
685    #[test]
686    fn no_bailment_denied() {
687        let k = signed_kernel();
688        let actor = did("did:exo:unauth");
689        let verdict = k.adjudicate_without_bailment(&actor, "read-data");
690        assert!(verdict.is_denied(), "expected Denied, got {verdict:?}");
691    }
692
693    #[test]
694    fn sdk_violation_labels_do_not_depend_on_debug_formatting() {
695        let source = include_str!("kernel.rs")
696            .split("// ===========================================================================\n// Tests")
697            .next()
698            .expect("production section");
699        assert!(
700            !source.contains("format!(\"{:?}: {}\", v.invariant, v.description)"),
701            "SDK violation labels must use stable invariant IDs"
702        );
703    }
704
705    #[test]
706    fn verdict_helpers() {
707        assert!(KernelVerdict::Permitted.is_permitted());
708        assert!(!KernelVerdict::Permitted.is_denied());
709        let denied = KernelVerdict::Denied { violations: vec![] };
710        assert!(denied.is_denied());
711        assert!(!denied.is_permitted());
712        let esc = KernelVerdict::Escalated { reason: "r".into() };
713        assert!(esc.is_escalated());
714        assert!(!esc.is_permitted());
715    }
716
717    #[test]
718    fn verdict_serde_roundtrip() {
719        let v = KernelVerdict::Denied {
720            violations: vec!["NoSelfGrant: reason".into()],
721        };
722        let json = serde_json::to_string(&v).expect("ser");
723        let decoded: KernelVerdict = serde_json::from_str(&json).expect("de");
724        assert_eq!(v, decoded);
725    }
726
727    #[test]
728    fn debug_impl_smoke() {
729        let k = ConstitutionalKernel::new();
730        let dbg = format!("{k:?}");
731        assert!(dbg.contains("ConstitutionalKernel"));
732        assert!(dbg.contains("8"));
733    }
734}