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}