Skip to main content

chio_kernel/kernel/
kernel_struct.rs

1use std::sync::atomic::{AtomicBool, AtomicU64};
2use std::sync::{Arc, Mutex};
3
4use arc_swap::ArcSwap;
5use dashmap::DashMap;
6
7use super::*;
8
9use std::collections::BTreeMap;
10use std::time::Duration;
11
12use super::construction::KernelBuildError;
13
14/// Wall-clock budgets for the mediation hot path. All values are milliseconds.
15/// `0` means "no deadline" (unbounded) for the opt-in guard and dispatch
16/// budgets, so a deployment that never sets one runs byte-for-byte as it did
17/// before deadlines existed. The receipt-append budget may not be `0`: an
18/// unbounded wedged-writer stall is never a valid posture, so it is rejected at
19/// load time and floor-clamped at read time as defense in depth.
20#[derive(Debug, Clone)]
21pub struct HotPathDeadlineConfig {
22    /// Budget for the whole guard pipeline, enforced around `run_guards`.
23    /// `0` disables (preserves the inline path).
24    pub guard_pipeline_budget_ms: u64,
25    /// Per-guard overrides keyed by `Guard::name()`. A named guard is enforced
26    /// against its own budget instead of the pipeline budget; `0` disables the
27    /// override for that guard. Any entry forces per-guard offload. A `BTreeMap`
28    /// keeps the canonical key order deterministic.
29    pub per_guard_budget_ms: BTreeMap<String, u64>,
30    /// Offload the guard pipeline to `spawn_blocking` even with no budget set,
31    /// so a blocking guard never pins an async worker. Default `false`.
32    pub always_offload_guards: bool,
33    /// Default per-dispatch budget, enforced around the tool-server call.
34    /// `0` disables. Default `0`.
35    pub dispatch_budget_ms: u64,
36    /// Per-tool-server dispatch overrides keyed by `ServerId` string.
37    pub per_server_dispatch_budget_ms: BTreeMap<String, u64>,
38    /// Watchdog bound on one receipt-append round trip through the commit
39    /// actor. Must be `>= MIN_RECEIPT_APPEND_BUDGET_MS`. Default 5000.
40    pub receipt_append_budget_ms: u64,
41    /// Writer-liveness watchdog poll cadence.
42    pub receipt_writer_poll_ms: u64,
43    /// Staleness threshold before a stuck writer is judged wedged.
44    pub receipt_writer_stall_ms: u64,
45}
46
47pub const DEFAULT_RECEIPT_APPEND_BUDGET_MS: u64 = 5_000;
48pub const MIN_RECEIPT_APPEND_BUDGET_MS: u64 = 250;
49pub const DEFAULT_RECEIPT_WRITER_POLL_MS: u64 = 1_000;
50pub const DEFAULT_RECEIPT_WRITER_STALL_MS: u64 = 10_000;
51
52impl Default for HotPathDeadlineConfig {
53    fn default() -> Self {
54        Self {
55            guard_pipeline_budget_ms: 0,
56            per_guard_budget_ms: BTreeMap::new(),
57            always_offload_guards: false,
58            dispatch_budget_ms: 0,
59            per_server_dispatch_budget_ms: BTreeMap::new(),
60            receipt_append_budget_ms: DEFAULT_RECEIPT_APPEND_BUDGET_MS,
61            receipt_writer_poll_ms: DEFAULT_RECEIPT_WRITER_POLL_MS,
62            receipt_writer_stall_ms: DEFAULT_RECEIPT_WRITER_STALL_MS,
63        }
64    }
65}
66
67impl HotPathDeadlineConfig {
68    /// Fail-closed load-time validation, run before kernel construction and
69    /// mirrored in the config-file validator.
70    pub fn validate(&self) -> Result<(), KernelBuildError> {
71        if self.receipt_append_budget_ms < MIN_RECEIPT_APPEND_BUDGET_MS {
72            return Err(KernelBuildError::InvalidDeadlineConfig(format!(
73                "receipt_append_budget_ms must be >= {MIN_RECEIPT_APPEND_BUDGET_MS}"
74            )));
75        }
76        if self.receipt_writer_poll_ms == 0 || self.receipt_writer_stall_ms == 0 {
77            return Err(KernelBuildError::InvalidDeadlineConfig(
78                "receipt writer poll and stall thresholds must be non-zero".to_string(),
79            ));
80        }
81        Ok(())
82    }
83
84    fn ms_to_budget(ms: u64) -> Option<Duration> {
85        match ms {
86            0 => None,
87            v => Some(Duration::from_millis(v)),
88        }
89    }
90
91    pub fn guard_pipeline_budget(&self) -> Option<Duration> {
92        Self::ms_to_budget(self.guard_pipeline_budget_ms)
93    }
94
95    pub fn guard_budget_for(&self, name: &str) -> Option<Duration> {
96        match self.per_guard_budget_ms.get(name) {
97            // A per-guard `0` disables only this guard's override, not the
98            // pipeline deadline: any per-guard entry forces the offloaded path,
99            // so falling through to the pipeline budget keeps an override-of-zero
100            // guard bounded instead of running unbounded.
101            None | Some(0) => self.guard_pipeline_budget(),
102            Some(ms) => Self::ms_to_budget(*ms),
103        }
104    }
105
106    pub fn dispatch_budget_for(&self, server_id: &str) -> Option<Duration> {
107        match self.per_server_dispatch_budget_ms.get(server_id) {
108            Some(ms) => Self::ms_to_budget(*ms),
109            None => Self::ms_to_budget(self.dispatch_budget_ms),
110        }
111    }
112
113    /// Effective append bound, clamped to the floor so a host that constructs a
114    /// `KernelConfig` without running validation still never gets an unbounded
115    /// (or below-floor) append.
116    pub fn receipt_append_budget(&self) -> Duration {
117        Duration::from_millis(
118            self.receipt_append_budget_ms
119                .max(MIN_RECEIPT_APPEND_BUDGET_MS),
120        )
121    }
122}
123
124#[cfg(test)]
125mod hot_path_deadline_config_tests {
126    use super::*;
127
128    #[test]
129    fn default_disables_guard_and_dispatch_but_bounds_append() {
130        let cfg = HotPathDeadlineConfig::default();
131        assert_eq!(cfg.guard_pipeline_budget(), None);
132        assert_eq!(cfg.dispatch_budget_for("any-server"), None);
133        assert_eq!(
134            cfg.receipt_append_budget(),
135            Duration::from_millis(DEFAULT_RECEIPT_APPEND_BUDGET_MS)
136        );
137    }
138
139    #[test]
140    fn validate_rejects_zero_append_budget() {
141        let cfg = HotPathDeadlineConfig {
142            receipt_append_budget_ms: 0,
143            ..HotPathDeadlineConfig::default()
144        };
145        assert!(cfg.validate().is_err());
146    }
147
148    #[test]
149    fn validate_rejects_below_floor_append_budget() {
150        let cfg = HotPathDeadlineConfig {
151            receipt_append_budget_ms: MIN_RECEIPT_APPEND_BUDGET_MS - 1,
152            ..HotPathDeadlineConfig::default()
153        };
154        assert!(cfg.validate().is_err());
155    }
156
157    #[test]
158    fn validate_rejects_zero_poll_or_stall() {
159        let cfg = HotPathDeadlineConfig {
160            receipt_writer_poll_ms: 0,
161            ..HotPathDeadlineConfig::default()
162        };
163        assert!(cfg.validate().is_err());
164    }
165
166    #[test]
167    fn append_budget_is_clamped_to_floor_even_without_validation() {
168        // A host that bypasses validation still never runs an unbounded (or
169        // below-floor) append.
170        let cfg = HotPathDeadlineConfig {
171            receipt_append_budget_ms: 1,
172            ..HotPathDeadlineConfig::default()
173        };
174        assert_eq!(
175            cfg.receipt_append_budget(),
176            Duration::from_millis(MIN_RECEIPT_APPEND_BUDGET_MS)
177        );
178    }
179
180    #[test]
181    fn per_guard_and_per_server_overrides_resolve() {
182        let mut per_guard = BTreeMap::new();
183        per_guard.insert("slow-guard".to_string(), 200u64);
184        let mut per_server = BTreeMap::new();
185        per_server.insert("slow-srv".to_string(), 300u64);
186        let cfg = HotPathDeadlineConfig {
187            guard_pipeline_budget_ms: 100,
188            per_guard_budget_ms: per_guard,
189            dispatch_budget_ms: 150,
190            per_server_dispatch_budget_ms: per_server,
191            ..HotPathDeadlineConfig::default()
192        };
193        assert_eq!(
194            cfg.guard_budget_for("slow-guard"),
195            Some(Duration::from_millis(200))
196        );
197        assert_eq!(
198            cfg.guard_budget_for("other-guard"),
199            Some(Duration::from_millis(100))
200        );
201        assert_eq!(
202            cfg.dispatch_budget_for("slow-srv"),
203            Some(Duration::from_millis(300))
204        );
205        assert_eq!(
206            cfg.dispatch_budget_for("other-srv"),
207            Some(Duration::from_millis(150))
208        );
209    }
210
211    #[test]
212    fn per_guard_zero_override_inherits_pipeline_budget() {
213        // A per-guard entry of `0` disables only that guard's own override; the
214        // guard must still inherit the overall pipeline budget rather than run
215        // unbounded (any per-guard entry forces the offloaded, timed path).
216        let mut per_guard = BTreeMap::new();
217        per_guard.insert("disabled-override".to_string(), 0u64);
218        let cfg = HotPathDeadlineConfig {
219            guard_pipeline_budget_ms: 1_000,
220            per_guard_budget_ms: per_guard,
221            ..HotPathDeadlineConfig::default()
222        };
223        assert_eq!(
224            cfg.guard_budget_for("disabled-override"),
225            Some(Duration::from_millis(1_000))
226        );
227    }
228
229    #[test]
230    fn per_guard_zero_override_stays_unbounded_without_pipeline_budget() {
231        // With no pipeline budget configured, a `0` override genuinely means no
232        // deadline for that guard.
233        let mut per_guard = BTreeMap::new();
234        per_guard.insert("disabled-override".to_string(), 0u64);
235        let cfg = HotPathDeadlineConfig {
236            guard_pipeline_budget_ms: 0,
237            per_guard_budget_ms: per_guard,
238            ..HotPathDeadlineConfig::default()
239        };
240        assert_eq!(cfg.guard_budget_for("disabled-override"), None);
241    }
242}
243
244/// Configuration for the Chio Runtime Kernel.
245pub struct KernelConfig {
246    /// Ed25519 keypair for signing receipts and issuing capabilities.
247    pub keypair: Keypair,
248
249    /// Public keys of trusted Capability Authorities.
250    pub ca_public_keys: Vec<chio_core::PublicKey>,
251
252    /// Maximum allowed delegation depth.
253    pub max_delegation_depth: u32,
254
255    /// SHA-256 hash of the active policy (embedded in receipts).
256    pub policy_hash: String,
257
258    /// Whether nested sampling requests are allowed at all.
259    pub allow_sampling: bool,
260
261    /// Whether sampling requests may include tool-use affordances.
262    pub allow_sampling_tool_use: bool,
263
264    /// Whether nested elicitation requests are allowed.
265    pub allow_elicitation: bool,
266
267    /// Maximum total wall-clock duration permitted for one streamed tool result.
268    pub max_stream_duration_secs: u64,
269
270    /// Maximum total canonical payload size permitted for one streamed tool result.
271    pub max_stream_total_bytes: u64,
272
273    /// Whether durable receipts and kernel-signed checkpoints are mandatory
274    /// prerequisites for this deployment.
275    pub require_web3_evidence: bool,
276
277    /// Allow process-local receipt logs when no durable receipt store is
278    /// installed. This is for tests and local scaffolds only; protocol
279    /// deployments should leave it false so successful dispatch requires
280    /// durable receipt persistence before any tool side effect.
281    pub allow_ephemeral_receipt_log: bool,
282
283    /// Allow a process-local (in-memory) revocation store when no durable or
284    /// remote revocation source is installed. This is for tests and local
285    /// scaffolds only; deployments should leave it false so a revoked
286    /// capability cannot be re-accepted after a restart drops the revocation
287    /// set.
288    pub allow_ephemeral_revocation_store: bool,
289
290    /// Number of receipts between Merkle checkpoint snapshots. Default: 100.
291    ///
292    /// Set to 0 to disable automatic checkpointing for deployments that do not
293    /// require web3 evidence.
294    pub checkpoint_batch_size: u64,
295
296    /// Optional receipt retention configuration.
297    ///
298    /// When `None` (default), retention is disabled and receipts accumulate
299    /// indefinitely. When `Some(config)`, the kernel will archive receipts
300    /// that exceed the time or size threshold.
301    pub retention_config: Option<crate::receipt_store::RetentionConfig>,
302
303    /// Per-process memory budget (bounded-structure caps + RSS soft ceiling).
304    pub memory_budget: MemoryBudgetConfig,
305
306    /// Wall-clock budgets for the mediation hot path. Construction input only,
307    /// not a wire payload, so this changes no signed or transmitted bytes.
308    pub deadlines: HotPathDeadlineConfig,
309}
310
311impl KernelConfig {
312    pub(crate) fn memory_budget_receipt_mirror_capacity(&self) -> usize {
313        self.memory_budget.receipt_mirror_capacity
314    }
315    pub(crate) fn memory_budget_federation_cache_capacity(&self) -> usize {
316        self.memory_budget.federation_cache_capacity
317    }
318    pub(crate) fn memory_budget_federation_cache_idle_ttl_secs(&self) -> u64 {
319        self.memory_budget.federation_cache_idle_ttl_secs
320    }
321}
322
323/// Boot-time configuration for the kernel-side hybrid signing path.
324///
325/// Mirrors the wire form of `chio_policy::CryptoFloor` (`allow_classical`,
326/// `allow_hybrid`, `pq_required`) and pairs the floor with the operator's
327/// 32-byte ML-DSA-65 keygen seed. Construct one of these from a parsed
328/// HushSpec policy plus the boot-loaded PQ seed and pass it to
329/// [`ChioKernel::with_hybrid_signing_backend`] with a verified self-quote
330/// port to obtain a `Box<dyn SigningBackend>` for hybrid receipt signing.
331///
332/// A separate input from [`KernelConfig`]: the hybrid fields are not folded
333/// into `KernelConfig`, so its wire form is unaffected.
334#[derive(Debug, Clone, Default)]
335pub struct HybridSigningConfig {
336    /// Minimum cryptographic posture enforced on receipts, capability
337    /// tokens, and compliance certificates. Default
338    /// [`KernelCryptoFloor::AllowClassical`].
339    pub crypto_floor: KernelCryptoFloor,
340
341    /// Optional 32-byte ML-DSA-65 keygen seed. Required when
342    /// `crypto_floor` is [`KernelCryptoFloor::AllowHybrid`] or
343    /// [`KernelCryptoFloor::PqRequired`]; ignored under
344    /// [`KernelCryptoFloor::AllowClassical`].
345    pub pq_signing_seed: Option<[u8; 32]>,
346}
347
348pub(crate) fn capability_crypto_floor(
349    floor: KernelCryptoFloor,
350) -> chio_core::capability::crypto_floor::CapabilityCryptoFloor {
351    match floor {
352        KernelCryptoFloor::AllowClassical => {
353            chio_core::capability::crypto_floor::CapabilityCryptoFloor::AllowClassical
354        }
355        KernelCryptoFloor::AllowHybrid => {
356            chio_core::capability::crypto_floor::CapabilityCryptoFloor::AllowHybrid
357        }
358        KernelCryptoFloor::PqRequired => {
359            chio_core::capability::crypto_floor::CapabilityCryptoFloor::PqRequired
360        }
361    }
362}
363
364pub(crate) fn receipt_crypto_floor(
365    floor: KernelCryptoFloor,
366) -> chio_core::receipt::crypto_floor::ReceiptCryptoFloor {
367    match floor {
368        KernelCryptoFloor::AllowClassical => {
369            chio_core::receipt::crypto_floor::ReceiptCryptoFloor::AllowClassical
370        }
371        KernelCryptoFloor::AllowHybrid => {
372            chio_core::receipt::crypto_floor::ReceiptCryptoFloor::AllowHybrid
373        }
374        KernelCryptoFloor::PqRequired => {
375            chio_core::receipt::crypto_floor::ReceiptCryptoFloor::PqRequired
376        }
377    }
378}
379
380pub const DEFAULT_MAX_STREAM_DURATION_SECS: u64 = 300;
381pub const DEFAULT_MAX_STREAM_TOTAL_BYTES: u64 = 256 * 1024 * 1024;
382/// Default cap on the number of chunks RETAINED from one streamed tool result.
383/// Bounds the accumulator `Vec<ToolCallChunk>` length and the per-chunk
384/// receipt-signing preimage even when every chunk is tiny and the byte cap is
385/// never reached. Generous for legitimate streams; `0` disables the cap.
386pub const DEFAULT_MAX_STREAM_CHUNKS: u64 = 1_048_576;
387pub const DEFAULT_CHECKPOINT_BATCH_SIZE: u64 = 100;
388pub const DEFAULT_RETENTION_DAYS: u64 = 90;
389pub const DEFAULT_MAX_SIZE_BYTES: u64 = 10_737_418_240;
390
391/// Per-process memory budget: bounded-structure capacities plus a process RSS
392/// soft ceiling. The soft ceiling is the in-process analog of the cgroup hard
393/// limit: the kernel sheds (Overloaded) before the OS OOM-kills it.
394#[derive(Debug, Clone)]
395pub struct MemoryBudgetConfig {
396    pub receipt_mirror_capacity: usize,
397    pub federation_cache_capacity: usize,
398    pub federation_cache_idle_ttl_secs: u64,
399    pub velocity_bucket_cap: usize,
400    pub admission_key_cap: usize,
401    pub journal_entry_cap: usize,
402    /// Max number of chunks retained from one streamed tool result. Bounds the
403    /// retained `Vec<ToolCallChunk>` and the per-chunk signing preimage so a flood
404    /// of tiny chunks that never trips `max_stream_total_bytes` still cannot grow
405    /// memory without bound. `0` disables the cap.
406    pub max_stream_chunks: u64,
407    /// Max number of DISTINCT tool names retained in each session journal's
408    /// cumulative `tool_counts` map. Unlike the `entries` and
409    /// `tool_sequence` rings, `tool_counts` is cumulative (it survives ring
410    /// eviction so the behavioral-sequence guard can answer "was this tool ever
411    /// invoked"), so a ring cannot bound it. Once a session reaches this many
412    /// distinct tool names a previously-unseen name is dropped fail-closed: a
413    /// dependent required-predecessor check then treats it as never-invoked and
414    /// denies. Sized well above any legitimate (registry-bounded) tool set.
415    pub journal_tool_counts_cap: usize,
416    /// Process RSS soft ceiling in bytes. When set and exceeded, new admissions
417    /// shed with Overloaded { Allocation }. Set to roughly 85-90% of the cgroup
418    /// memory.max so the graceful stop fires before the kill. Stage A ships None.
419    pub rss_soft_limit_bytes: Option<u64>,
420    /// How often the RSS sampler reads /proc/self/statm.
421    pub rss_sample_interval_secs: u64,
422}
423
424impl MemoryBudgetConfig {
425    pub fn defaults() -> Self {
426        Self {
427            receipt_mirror_capacity: 4096,
428            federation_cache_capacity: 8192,
429            federation_cache_idle_ttl_secs: 3600,
430            velocity_bucket_cap: 65_536,
431            admission_key_cap: 4096,
432            journal_entry_cap: 4096,
433            max_stream_chunks: DEFAULT_MAX_STREAM_CHUNKS,
434            journal_tool_counts_cap: 4096,
435            rss_soft_limit_bytes: None,
436            rss_sample_interval_secs: 30,
437        }
438    }
439}
440
441impl Default for MemoryBudgetConfig {
442    fn default() -> Self {
443        Self::defaults()
444    }
445}
446
447/// Owns the RSS sampler thread; signals stop and joins on drop. On non-Linux
448/// hosts the sampler is a no-op and the soft limit is inert (cgroup and
449/// try_reserve backstops still apply).
450pub(crate) struct RssSamplerHandle {
451    stop: Arc<AtomicBool>,
452    join: Option<std::thread::JoinHandle<()>>,
453}
454
455impl RssSamplerHandle {
456    pub(crate) fn spawn(shed: Arc<AtomicBool>, soft_limit_bytes: u64, interval_secs: u64) -> Self {
457        let stop = Arc::new(AtomicBool::new(false));
458        let worker_stop = Arc::clone(&stop);
459        let interval = std::time::Duration::from_secs(interval_secs.max(1));
460        let join = std::thread::spawn(move || {
461            use std::sync::atomic::Ordering;
462            while !worker_stop.load(Ordering::SeqCst) {
463                if let Some(rss) = read_process_rss_bytes() {
464                    shed.store(rss > soft_limit_bytes, Ordering::Relaxed);
465                }
466                let mut waited = std::time::Duration::ZERO;
467                let slice = std::time::Duration::from_millis(200);
468                while waited < interval && !worker_stop.load(Ordering::SeqCst) {
469                    std::thread::sleep(slice);
470                    waited += slice;
471                }
472            }
473        });
474        Self {
475            stop,
476            join: Some(join),
477        }
478    }
479}
480
481impl Drop for RssSamplerHandle {
482    fn drop(&mut self) {
483        self.stop.store(true, std::sync::atomic::Ordering::SeqCst);
484        if let Some(join) = self.join.take() {
485            let _ = join.join();
486        }
487    }
488}
489
490#[cfg(target_os = "linux")]
491fn read_process_rss_bytes() -> Option<u64> {
492    let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
493    let resident_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
494    Some(resident_pages.saturating_mul(linux_page_size()))
495}
496
497/// Real system page size in bytes, read once via `sysconf(_SC_PAGESIZE)` and
498/// cached. Hosts with non-4-KiB pages (for example common 64-KiB-page ARM
499/// deployments) would otherwise undercount RSS by the page-size ratio and shed
500/// far past the configured soft ceiling. Falls back to 4096 only if the query
501/// fails.
502#[cfg(target_os = "linux")]
503fn linux_page_size() -> u64 {
504    use std::sync::OnceLock;
505    static PAGE_SIZE: OnceLock<u64> = OnceLock::new();
506    *PAGE_SIZE.get_or_init(|| {
507        // SAFETY: `sysconf` with a compile-time constant name has no
508        // preconditions; it returns the configured page size, or -1 on failure.
509        let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
510        if raw > 0 {
511            raw as u64
512        } else {
513            4096
514        }
515    })
516}
517
518#[cfg(not(target_os = "linux"))]
519fn read_process_rss_bytes() -> Option<u64> {
520    None
521}
522
523/// The Chio Runtime Kernel.
524///
525/// This is the central component of the Chio protocol. It validates capabilities,
526/// runs guards, dispatches tool calls, and signs receipts.
527///
528/// The kernel is designed to be the sole trusted mediator. It never exposes its
529/// signing key, address, or internal state to the agent.
530pub struct ChioKernel {
531    pub(super) config: KernelConfig,
532    pub(super) durable_admission_mode: crate::admission_operation::DurableAdmissionMode,
533    pub(super) durable_admission_runtime: Option<DurableAdmissionRuntime>,
534    /// Explicit compatibility escape for development fixtures that exercise the
535    /// legacy non-durable financial lifecycle. Production construction leaves
536    /// this false, so a financial hold cannot cross a connector boundary without
537    /// durable recovery coverage.
538    pub(super) unsafe_ephemeral_financial_dispatch: bool,
539    /// Guards are stored behind `Arc` so a single guard can be cloned into a
540    /// `spawn_blocking` task without moving the whole pipeline, letting the
541    /// deadline wrapper bound a blocking guard off the async worker.
542    pub(super) guards: Arc<Vec<Arc<dyn Guard>>>,
543    pub(super) post_invocation_pipeline: crate::post_invocation::PostInvocationPipeline,
544    pub(super) budget_store: Arc<dyn BudgetStore>,
545    pub(super) budget_store_lock: Mutex<()>,
546    pub(super) revocation_store: Arc<dyn RevocationStore>,
547    pub(super) capability_authority: Box<dyn CapabilityAuthority>,
548    // Held behind `Arc` so a single connection can be cloned into a
549    // `spawn_blocking` task, letting the dispatch deadline drive the call off the
550    // async worker (a connection that blocks before its first `.await` cannot then
551    // pin the worker).
552    pub(super) tool_servers: HashMap<ServerId, Arc<dyn ToolServerConnection>>,
553    pub(super) resource_providers: Vec<Box<dyn ResourceProvider>>,
554    pub(super) prompt_providers: Vec<Box<dyn PromptProvider>>,
555    pub(super) sessions: DashMap<SessionId, Arc<Session>>,
556    pub(super) receipt_log: Mutex<ReceiptLog>,
557    pub(super) child_receipt_log: Mutex<ChildReceiptLog>,
558    /// Live entry-count gauges for the two receipt mirrors. Cloned from the
559    /// ring's gauge at construction so telemetry and the
560    /// bounded-structure registry (`bounded_structure_gauges`) can read the
561    /// count without locking the log.
562    pub(super) receipt_mirror_gauge: chio_bounded::SizeGauge,
563    pub(super) child_receipt_mirror_gauge: chio_bounded::SizeGauge,
564    pub(super) receipt_store: Option<Arc<dyn ReceiptStore>>,
565    pub(super) receipt_store_write_lock: Mutex<()>,
566    /// Retention maintenance worker, spawned at store attach when
567    /// `config.retention_config` is `Some`. Owns a dedicated OS thread that
568    /// calls `ReceiptStore::rotate_receipts` on `RetentionConfig.check_interval_secs`;
569    /// joined when this field is dropped (kernel drop). `None` when
570    /// retention is unconfigured or before a store is attached.
571    pub(super) retention_maintenance: Option<crate::receipt_store::RetentionMaintenanceHandle>,
572    pub(super) payment_adapter: Option<Box<dyn PaymentAdapter>>,
573    pub(super) price_oracle: Option<Box<dyn PriceOracle>>,
574    pub(super) runtime_admission_hook: Option<Arc<dyn RuntimeAdmissionHook>>,
575    pub(super) attestation_trust_policy: Option<AttestationTrustPolicy>,
576    pub(super) capability_crypto_floor: KernelCryptoFloor,
577    /// How many receipts per Merkle checkpoint batch. Default: 100.
578    pub(super) checkpoint_batch_size: u64,
579    /// Monotonic counter for checkpoint_seq values.
580    pub(super) checkpoint_seq_counter: AtomicU64,
581    /// seq of the last receipt included in the previous checkpoint batch.
582    pub(super) last_checkpoint_seq: AtomicU64,
583    /// Nonce replay store for DPoP proof verification. Required when any grant has dpop_required.
584    pub(super) dpop_nonce_store: Option<dpop::DpopNonceStore>,
585    /// Configuration for DPoP proof verification TTLs and clock skew.
586    pub(super) dpop_config: Option<dpop::DpopConfig>,
587    /// Execution-nonce config (TTL, capacity, strict-mode flag).
588    /// When `None`, no nonce is minted on allow and strict verification is
589    /// disabled (compatibility deployments keep working).
590    pub(super) execution_nonce_config: Option<crate::execution_nonce::ExecutionNonceConfig>,
591    /// Replay-prevention store for execution nonces. Shared with
592    /// any tool server that delegates verification to the kernel. Boxed
593    /// trait object so SQLite-backed stores can be plugged in.
594    pub(super) execution_nonce_store: Option<Box<dyn crate::execution_nonce::ExecutionNonceStore>>,
595    /// Replay store for governed approval tokens. Prevents a signed approval
596    /// from being consumed more than once. Uses the same LRU + TTL pattern as
597    /// DPoP nonce verification. Key: (request_id, governed_intent_hash).
598    pub(super) approval_replay_store: Option<dpop::DpopNonceStore>,
599    pub(super) threshold_approval_requirement_resolver:
600        Option<Arc<dyn crate::threshold_approval::ThresholdApprovalRequirementResolver>>,
601    pub(super) supplemental_quota_verifier:
602        Option<crate::supplemental_quota::SupplementalQuotaVerifierRuntime>,
603    /// Emergency kill switch. When `true`, every evaluate entry point returns
604    /// `Verdict::Deny` without performing capability validation or guard
605    /// evaluation. Flipped by `emergency_stop` / `emergency_resume`.
606    ///
607    /// Reads use `Ordering::SeqCst` even on the hot path. The emergency check
608    /// is a single atomic load per evaluate call (negligible cost relative to
609    /// the guard pipeline) and `SeqCst` is the safest default for a rarely
610    /// taken control path.
611    pub(super) emergency_stopped: AtomicBool,
612    /// Unix timestamp (seconds) at which the kill switch was last engaged.
613    /// `0` means "never engaged" or "currently resumed". Written with
614    /// `SeqCst` before `emergency_stopped` is set to `true`, cleared to `0`
615    /// after `emergency_stopped` is set to `false`.
616    pub(super) emergency_stopped_since: AtomicU64,
617    /// Operator-supplied reason for the most recent emergency stop. Set on
618    /// `emergency_stop`, cleared on `emergency_resume`. Stored behind
619    /// ArcSwap so health probes can read the current reason without blocking.
620    pub(super) emergency_stop_reason: ArcSwap<Option<String>>,
621    /// Persistent degraded flag for the trusted computing base's locks. A
622    /// poisoned budget-registry or session lock means a panic unwound
623    /// mid-mutation, so the state it guarded may be half-updated. Tripping this
624    /// flag makes the pre-dispatch gate fail evaluations closed until an
625    /// operator-visible recovery, rather than silently proceeding on the
626    /// recovered `into_inner` state. It is TCB-critical: any poison denies.
627    pub(super) lock_poison: chio_supervisor::HealthFlag,
628    /// Memory-provenance chain. When installed, every
629    /// governed `MemoryWrite` action appends an entry after the allow
630    /// receipt is signed, and every `MemoryRead` attaches the latest
631    /// entry (or an `Unverified` marker) to its receipt as
632    /// `memory_provenance` evidence metadata. `None` keeps the kernel
633    /// backward-compatible: memory-shaped tool calls behave exactly as
634    /// they do without a provenance chain installed.
635    pub(super) memory_provenance: Option<Arc<dyn crate::memory_provenance::MemoryProvenanceStore>>,
636    /// Cross-kernel federation peer set. When a request
637    /// carries a `federated_origin_kernel_id` and that peer is pinned
638    /// here (fresh), the kernel invokes `federation_cosigner` after
639    /// locally signing the receipt to obtain the origin kernel's
640    /// co-signature. Absent in non-federated deployments.
641    pub(super) federation_peers:
642        ArcSwap<HashMap<String, chio_federation::trust_establishment::FederationPeer>>,
643    /// `ArcSwap` so trust-root rotations can land without holding a
644    /// kernel mutex. Hex-keyed because `chio_core::PublicKey` does not
645    /// implement `Hash`.
646    pub(super) capability_trust_roots:
647        ArcSwap<HashMap<String, chio_core::capability::attenuation::ScopeHash>>,
648    /// Serializes read-modify-write updates to `capability_trust_roots`.
649    /// Snapshot reads remain lock-free through ArcSwap.
650    pub(super) capability_trust_roots_write_lock: Mutex<()>,
651    /// Bilateral co-signer. Separate from the peer set so
652    /// runtime can install it independently - for instance, a deployment
653    /// can declare peers while still using a mock cosigner in tests.
654    pub(super) federation_cosigner:
655        Option<Arc<dyn chio_federation::bilateral::BilateralCoSigningProtocol>>,
656    /// Locally-signed dual receipts, indexed by ChioReceipt.id.
657    /// Populated only when the post-sign hook fires successfully. Kept
658    /// in-memory; persistent storage plugs in via the federation-state
659    /// APIs already in chio-federation.
660    /// Capped, idle-swept, gauged instead of an unbounded DashMap: federated
661    /// calls no longer grow kernel RSS without bound.
662    pub(super) federation_dual_receipts:
663        Mutex<chio_bounded::BoundedMap<String, chio_federation::bilateral::DualSignedReceipt>>,
664    pub(super) federation_dual_receipts_gauge: chio_bounded::SizeGauge,
665    /// DSSE signature-slice envelopes, indexed by ChioReceipt.id.
666    /// These are emitted through the federation cosigner protocol rather than
667    /// by loading Org A private key material in the tool-host kernel.
668    pub(super) federation_dsse_envelopes:
669        Mutex<chio_bounded::BoundedMap<String, chio_federation::bilateral_dsse::DsseEnvelope>>,
670    pub(super) federation_dsse_envelopes_gauge: chio_bounded::SizeGauge,
671    /// Optional durable backing for bilateral co-sign artifacts. When set, the
672    /// co-sign hook writes through to it before caching and the
673    /// accessors fall through to it on a cache miss.
674    pub(super) federation_artifact_store:
675        Option<std::sync::Arc<dyn crate::federation_artifact_store::FederationArtifactStore>>,
676    /// Request-keyed tenant scope for receipts. Async evaluate futures
677    /// can resume on a different worker after dispatch, so the scope is
678    /// stored in this map rather than a thread-local.
679    pub(super) receipt_tenant_ids: Arc<DashMap<String, String>>,
680    /// Request-keyed copy of the receipt-version admission snapshot.
681    /// Async evaluate futures may resume on a different Tokio worker
682    /// after dispatch. This map keeps the admitted version and peer state
683    /// available until the evaluation future finishes.
684    pub(super) receipt_federation_admissions: Arc<DashMap<String, ReceiptFederationAdmission>>,
685    /// Operator-declared kernel identifier used as the
686    /// `org_b_kernel_id` in bilateral co-signing. Defaults to the hex
687    /// encoding of the kernel's signing public key, but operators can
688    /// override it to a stable DNS name via `with_federation_peers`.
689    pub(super) federation_local_kernel_id: ArcSwap<Option<String>>,
690    /// Mpsc-backed signing task handle. Owns a clone of `config.keypair` and
691    /// pulls signing requests from a bounded channel; producers `.await` on
692    /// backpressure rather than on a mutex. Spawned at [`ChioKernel::new`] and
693    /// joined by [`ChioKernel::shutdown`]. Wrapped in `Arc` so shared kernel
694    /// handles can pass the signing handle to in-flight evaluators without
695    /// cloning the whole kernel.
696    pub(super) signing_task: std::sync::Arc<signing_task::SigningTaskHandle>,
697    pub(super) settlement_observer: Option<crate::settlement_routing::SettlementObserverRuntime>,
698    /// Recursive-delegation oracle handle. When `Some`, the verifier consults this
699    /// arc-swap-backed snapshot on every delegated dispatch and denies
700    /// the capability if any link in the chain (or the leaf) is in the
701    /// revoked set. `None` falls back to the per-row
702    /// `RevocationStore` lookup. Field always present so the struct
703    /// shape stays feature-flag agnostic.
704    pub(super) revocation_view: Option<std::sync::Arc<chio_kernel_core::RevocationView>>,
705    pub(super) budget_registry: Mutex<chio_kernel_core::InMemoryBudgetRegistry>,
706    /// Sibling-sum shares held open by reserve-for-caller authorizations.
707    ///
708    /// A mediated authorization keeps its delegated child's admitted share in
709    /// `budget_registry` while the reserved hold is open, so an outstanding
710    /// reservation still counts against the parent and a sibling cannot
711    /// over-subscribe it. Keyed by budget hold id, each entry carries the
712    /// `(parent, child, share)` needed to release that headroom when the hold
713    /// closes (reconciled by nonce or forfeited by the TTL reaper).
714    pub(super) reserved_sibling_shares: Mutex<HashMap<String, ReservedSiblingShare>>,
715    /// Fail-closed gate over delegated reserve-for-caller holds carried across a
716    /// restart. A delegated reservation keeps its child's sibling-sum share
717    /// admitted in `budget_registry` while its durable hold stays open, but that
718    /// admission is in-memory only: a freshly built mediation kernel over a
719    /// populated budget store loses it, and the durable hold record does not
720    /// carry the parent capability id or the shares needed to rebuild it. Until
721    /// every such hold from a prior process closes, this kernel denies delegated
722    /// admission fail-closed so a sibling cannot be admitted against the parent as
723    /// if the still-open reservation consumed nothing. Armed by
724    /// [`ChioKernel::arm_restart_reserved_hold_gate`] at mediation-kernel startup.
725    pub(super) restart_reserved_hold_gate: Mutex<RestartReservedHoldGate>,
726    /// RSS soft-ceiling shed flag. Set by the sampler when process RSS exceeds
727    /// `memory_budget.rss_soft_limit_bytes`; read on the
728    /// admission fast path alongside the emergency stop.
729    pub(super) rss_shed: Arc<AtomicBool>,
730    /// Owns the sampler thread when a soft limit is configured; joins on drop.
731    pub(super) rss_sampler: Option<RssSamplerHandle>,
732    /// Receipt-writer liveness watchdog. Opt-in: the hosting edge spawns the
733    /// poll task, which publishes the latest verdict into an `ArcSwap` the
734    /// pre-dispatch readiness gate reads. Absent watchdog leaves the verdict
735    /// `Unknown` and the gate behaves as before.
736    pub(super) receipt_writer_watchdog:
737        std::sync::Arc<receipt_writer_watchdog::ReceiptWriterWatchdogHandle>,
738}
739
740/// The parent/child/share triple a reserve-for-caller hold keeps admitted in
741/// the sibling-sum `budget_registry` while its durable hold stays open. It is
742/// recorded when the reservation is stamped and consumed to release the
743/// parent's headroom once the hold is reconciled or reaped.
744#[derive(Debug, Clone)]
745pub(crate) struct ReservedSiblingShare {
746    pub(crate) parent_token_id: String,
747    pub(crate) child_token_id: String,
748    pub(crate) share_bps: u16,
749}
750
751/// State of the fail-closed gate over delegated reserve-for-caller holds carried
752/// across a restart. See [`super::ChioKernel::restart_reserved_hold_gate`].
753#[derive(Debug, Clone)]
754pub(crate) enum RestartReservedHoldGate {
755    /// No unaccounted reserve holds from a prior process; delegated admission
756    /// proceeds. Every kernel starts here and returns here once the durable open
757    /// holds observed at startup have closed.
758    Clear,
759    /// The listed holds were open delegated reserve-for-caller holds when this
760    /// kernel started and are not tracked in its in-memory sibling-share map.
761    /// Delegated admission denies until each has closed (reconciled or reaped),
762    /// re-queried per admission so the gate clears exactly when they settle.
763    PendingHolds(std::collections::HashSet<String>),
764    /// The budget store could not enumerate its reserved holds yet reported open
765    /// holds at startup. Delegated admission denies until the open-hold count
766    /// drains to zero; while denied this kernel opens no new holds, so the count
767    /// faithfully tracks the prior process's holds draining away.
768    PendingOpaqueCount,
769}
770
771impl ChioKernel {
772    /// Construct the hybrid signing backend the kernel would use under
773    /// `hybrid`'s configured floor and PQ key material after the kernel
774    /// self-quote gate has run.
775    ///
776    /// Threads the kernel's classical Ed25519 keypair into a
777    /// [`chio_core::crypto::Ed25519Backend`] under
778    /// [`KernelCryptoFloor::AllowClassical`], or composes it with an
779    /// [`chio_core::crypto::MlDsa65Backend`] derived from `hybrid.pq_signing_seed`
780    /// into a [`chio_core::crypto::HybridBackend`] under
781    /// [`KernelCryptoFloor::AllowHybrid`] or [`KernelCryptoFloor::PqRequired`],
782    /// but only after [`crate::boot::load_kernel_signing_backend_after_self_quote`]
783    /// accepts `self_quote_bytes`.
784    ///
785    /// Receipt body construction continues to flow through the existing
786    /// inline path (`build_and_sign_receipt`); callers that opt in to
787    /// hybrid signing pass the returned backend through
788    /// [`crate::sign_receipt_body_with_backend`] (along with the canonical
789    /// content preimage the body's `content_hash` was derived from) before
790    /// persistence, so the hybrid path recomputes `content_hash` inside the
791    /// trust boundary and is WYSIWYS fail-closed just like the inline
792    /// classical path.
793    ///
794    /// # Errors
795    ///
796    /// Returns [`crate::boot::KernelBootError::SelfQuoteRejected`] when the
797    /// self-quote verifier rejects a non-classical floor, or
798    /// [`crate::boot::KernelBootError::SigningBackend`] when the configured
799    /// floor needs a PQ key but `hybrid.pq_signing_seed` is `None`. Mirrors
800    /// the policy-level check in `chio_policy::CryptoFloor::validate_with_pq_key`
801    /// so the boot path catches the misconfiguration even when the policy crate
802    /// is bypassed.
803    pub fn with_hybrid_signing_backend(
804        &mut self,
805        hybrid: &HybridSigningConfig,
806        self_quote_bytes: &[u8],
807        verifier: &dyn crate::boot::KernelSelfQuoteVerifier,
808    ) -> Result<Box<dyn chio_core::crypto::SigningBackend>, crate::boot::KernelBootError> {
809        let backend = crate::boot::load_kernel_signing_backend_after_self_quote(
810            hybrid.crypto_floor,
811            self.config.keypair.clone(),
812            hybrid.pq_signing_seed.as_ref(),
813            self_quote_bytes,
814            verifier,
815        )?;
816        self.capability_crypto_floor = hybrid.crypto_floor;
817        Ok(backend)
818    }
819}