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}