Skip to main content

chio_kernel/kernel/
construction.rs

1//! `ChioKernel` construction and configuration surface.
2//!
3//! Holds the kernel constructor, session/store accessors, and the
4//! `set_*` / `with_*` / `register_*` configuration setters, including
5//! federation, emergency-stop, DPoP, and execution-nonce wiring.
6
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9
10use arc_swap::ArcSwap;
11use chio_log_redact::redacted;
12use dashmap::DashMap;
13
14use super::*;
15
16/// Fail-closed kernel build error. Lets deadline config be validated at
17/// construction time without making the infallible `ChioKernel::new` fallible.
18#[derive(Debug, thiserror::Error)]
19pub enum KernelBuildError {
20    #[error("invalid hot-path deadline config: {0}")]
21    InvalidDeadlineConfig(String),
22}
23
24impl ChioKernel {
25    pub(crate) fn with_sessions_read<R>(
26        &self,
27        f: impl FnOnce(&DashMap<SessionId, Arc<Session>>) -> Result<R, KernelError>,
28    ) -> Result<R, KernelError> {
29        f(&self.sessions)
30    }
31
32    pub(crate) fn with_sessions_write<R>(
33        &self,
34        f: impl FnOnce(&DashMap<SessionId, Arc<Session>>) -> Result<R, KernelError>,
35    ) -> Result<R, KernelError> {
36        f(&self.sessions)
37    }
38
39    pub(crate) fn with_session<R>(
40        &self,
41        session_id: &SessionId,
42        f: impl FnOnce(&Arc<Session>) -> Result<R, KernelError>,
43    ) -> Result<R, KernelError> {
44        let session = self
45            .sessions
46            .get(session_id)
47            .map(|entry| Arc::clone(entry.value()))
48            .ok_or_else(|| KernelError::UnknownSession(session_id.clone()))?;
49        f(&session)
50    }
51
52    /// Resolve the tenant_id for a given session by walking its
53    /// authenticated auth context into the enterprise identity's tenant
54    /// claim. Returns `None` for single-tenant / anonymous sessions, for
55    /// unknown session IDs, or when the caller did not supply one.
56    ///
57    /// Tenant_id is taken from the OAuth bearer's `enterprise_identity`
58    /// (preferred, richer SSO claims) and falls back to the OAuth
59    /// `federated_claims.tenant_id` when the IdP surfaces only the minimal
60    /// federated claim set. Both sources originate from the authenticated
61    /// session -- never from caller-provided request fields, because
62    /// caller choice would defeat the isolation guarantee.
63    pub(crate) fn resolve_tenant_id_for_session(
64        &self,
65        session_id: Option<&SessionId>,
66    ) -> Option<String> {
67        let id = session_id?;
68        self.with_session(id, |session| {
69            let auth_context = session.auth_context();
70            Ok(extract_tenant_id_from_auth_context(&auth_context))
71        })
72        .ok()
73        .flatten()
74    }
75
76    pub(crate) fn scope_receipt_tenant_id_for_request(
77        &self,
78        request_id: &str,
79        tenant_id: Option<String>,
80    ) -> ScopedKernelReceiptTenantId {
81        let previous = match tenant_id {
82            Some(tenant_id) => self
83                .receipt_tenant_ids
84                .insert(request_id.to_string(), tenant_id),
85            None => self
86                .receipt_tenant_ids
87                .remove(request_id)
88                .map(|(_, previous)| previous),
89        };
90        ScopedKernelReceiptTenantId {
91            request_id: request_id.to_string(),
92            tenant_ids: Arc::clone(&self.receipt_tenant_ids),
93            previous,
94        }
95    }
96
97    pub(crate) fn receipt_tenant_id_for_request(&self, request_id: Option<&str>) -> Option<String> {
98        request_id.and_then(|request_id| {
99            self.receipt_tenant_ids
100                .get(request_id)
101                .map(|entry| entry.value().clone())
102        })
103    }
104
105    pub(crate) fn with_session_mut<R>(
106        &self,
107        session_id: &SessionId,
108        f: impl FnOnce(&Arc<Session>) -> Result<R, KernelError>,
109    ) -> Result<R, KernelError> {
110        self.with_session(session_id, f)
111    }
112
113    pub(crate) fn with_budget_store<R>(
114        &self,
115        f: impl FnOnce(&dyn BudgetStore) -> Result<R, KernelError>,
116    ) -> Result<R, KernelError> {
117        let _guard = match self.budget_store_lock.lock() {
118            Ok(guard) => guard,
119            Err(poisoned) => poisoned.into_inner(),
120        };
121        f(self.budget_store.as_ref())
122    }
123
124    pub(crate) fn with_revocation_store<R>(
125        &self,
126        f: impl FnOnce(&dyn RevocationStore) -> Result<R, KernelError>,
127    ) -> Result<R, KernelError> {
128        f(self.revocation_store.as_ref())
129    }
130
131    pub(crate) fn with_receipt_store<R>(
132        &self,
133        f: impl FnOnce(&dyn ReceiptStore) -> Result<R, KernelError>,
134    ) -> Result<Option<R>, KernelError> {
135        let Some(store) = self.receipt_store.as_ref() else {
136            return Ok(None);
137        };
138        f(store.as_ref()).map(Some)
139    }
140
141    /// Fallible constructor that runs fail-closed validation of the hot-path
142    /// deadline config before building. This is the documented entrypoint for
143    /// hosts that set `[deadlines]`. `new` stays infallible for existing
144    /// callers; the append bound is additionally floor-clamped at read time so
145    /// even a host that bypasses this path can never run an unbounded append.
146    pub fn try_new(config: KernelConfig) -> Result<Self, KernelBuildError> {
147        config.deadlines.validate()?;
148        Ok(Self::new(config))
149    }
150
151    /// Flush any queued receipt writes to durable storage, bounded by `timeout`.
152    ///
153    /// A shutdown drain calls this to prove the commit-actor queue is empty
154    /// before the process exits. `Ok(None)` means no receipt store is
155    /// configured; a configured store's flush error is surfaced rather than
156    /// swallowed, so a drain can exit non-zero when a receipt might not be
157    /// durable.
158    pub fn flush_receipt_writes_with_timeout(
159        &self,
160        timeout: std::time::Duration,
161    ) -> Result<Option<crate::ReceiptFlushReport>, KernelError> {
162        self.with_receipt_store(|store| {
163            store
164                .flush_receipt_writes_with_timeout(timeout)
165                .map_err(KernelError::from)
166        })
167    }
168
169    pub fn new(config: KernelConfig) -> Self {
170        info!("initializing Chio kernel");
171        let authority_keypair = config.keypair.clone();
172        let checkpoint_batch_size = config.checkpoint_batch_size;
173        // Build the mpsc-backed signing-task handle. The handle clones the
174        // signing keypair so the receipt-signing critical path no longer borrows
175        // from `self.config.keypair` while the evaluate pipeline is mid-flight.
176        // The tokio task is spawned LAZILY on first `signing_task.sign(_)` call
177        // so `ChioKernel::new` remains constructible from sync contexts; by the
178        // time any caller reaches `sign`, a tokio runtime is necessarily active.
179        let signing_keypair = config.keypair.clone();
180        // WYSIWYS: the async signer admits exactly what the inline
181        // signer admits, then bounds queue memory by an AGGREGATE byte budget.
182        //
183        // - Per-request cap = 0 (unlimited). The inline `build_and_sign_receipt`
184        //   path applies NO preimage cap, and `max_stream_total_bytes` limits
185        //   *raw stream bytes*, a different unit from the *preimage bytes* a
186        //   queued request holds (a stream receipt's preimage is the
187        //   concatenation of 64-char per-chunk digests, not the raw payload).
188        //   Comparing a preimage length against `max_stream_total_bytes` would
189        //   falsely reject stream receipts the inline signer accepts (case 3),
190        //   and a `max_stream_total_bytes == 0` ("unlimited") config must not
191        //   collapse to a 1-byte cap (case 2). So we disable the per-request
192        //   cap and let the aggregate budget bound memory instead.
193        // - Aggregate budget tracks the configured stream/output max (saturating
194        //   into `usize`), with a non-zero floor so a `0` ("unlimited") stream
195        //   config still bounds queued memory at the documented default rather
196        //   than growing unbounded (case 1). Always BOUNDED.
197        let configured_stream_max =
198            usize::try_from(config.max_stream_total_bytes).unwrap_or(usize::MAX);
199        let signing_queued_budget = if configured_stream_max == 0 {
200            signing_task::DEFAULT_MAX_SIGNING_QUEUED_BYTES
201        } else {
202            configured_stream_max
203        };
204        let signing_task = std::sync::Arc::new(
205            signing_task::SigningTaskHandle::with_capacity_max_content_and_queued_bytes(
206                signing_keypair,
207                signing_task::DEFAULT_SIGNING_CHANNEL_CAPACITY,
208                /* per-request cap */ 0,
209                signing_queued_budget,
210            ),
211        );
212        // Read the memory-budget-driven caps before `config` is moved into the
213        // struct literal.
214        let receipt_mirror_capacity = config.memory_budget_receipt_mirror_capacity();
215        let federation_cache_capacity = config.memory_budget_federation_cache_capacity();
216        let federation_cache_idle_ttl = config.memory_budget_federation_cache_idle_ttl_secs();
217        let rss_soft_limit_bytes = config.memory_budget.rss_soft_limit_bytes;
218        let rss_sample_interval_secs = config.memory_budget.rss_sample_interval_secs;
219        // Build the bounded-structure gauges first so they can be shared between
220        // each structure and the telemetry / registry readers.
221        let receipt_mirror_gauge;
222        let child_receipt_mirror_gauge;
223        let federation_dual_receipts_gauge;
224        let federation_dsse_envelopes_gauge;
225        let mut kernel = Self {
226            config,
227            durable_admission_mode: crate::admission_operation::DurableAdmissionMode::default(),
228            durable_admission_runtime: None,
229            unsafe_ephemeral_financial_dispatch: false,
230            guards: std::sync::Arc::new(Vec::new()),
231            post_invocation_pipeline: crate::post_invocation::PostInvocationPipeline::new(),
232            budget_store: Arc::new(InMemoryBudgetStore::new()),
233            budget_store_lock: Mutex::new(()),
234            revocation_store: Arc::new(InMemoryRevocationStore::new()),
235            capability_authority: Box::new(LocalCapabilityAuthority::new(authority_keypair)),
236            tool_servers: HashMap::new(),
237            resource_providers: Vec::new(),
238            prompt_providers: Vec::new(),
239            sessions: DashMap::new(),
240            receipt_log: {
241                let gauge = chio_bounded::SizeGauge::new();
242                receipt_mirror_gauge = gauge.clone();
243                Mutex::new(ReceiptLog::with_capacity(receipt_mirror_capacity, gauge))
244            },
245            child_receipt_log: {
246                let gauge = chio_bounded::SizeGauge::new();
247                child_receipt_mirror_gauge = gauge.clone();
248                Mutex::new(ChildReceiptLog::with_capacity(
249                    receipt_mirror_capacity,
250                    gauge,
251                ))
252            },
253            receipt_mirror_gauge,
254            child_receipt_mirror_gauge,
255            receipt_store: None,
256            receipt_store_write_lock: Mutex::new(()),
257            retention_maintenance: None,
258            payment_adapter: None,
259            price_oracle: None,
260            runtime_admission_hook: None,
261            attestation_trust_policy: None,
262            capability_crypto_floor: KernelCryptoFloor::AllowClassical,
263            checkpoint_batch_size,
264            checkpoint_seq_counter: AtomicU64::new(0),
265            last_checkpoint_seq: AtomicU64::new(0),
266            dpop_nonce_store: None,
267            dpop_config: None,
268            execution_nonce_config: None,
269            execution_nonce_store: None,
270            approval_replay_store: Some(dpop::DpopNonceStore::new(
271                8192,
272                std::time::Duration::from_secs(3600),
273            )),
274            threshold_approval_requirement_resolver: None,
275            supplemental_quota_verifier: None,
276            emergency_stopped: AtomicBool::new(false),
277            emergency_stopped_since: AtomicU64::new(0),
278            emergency_stop_reason: ArcSwap::from_pointee(Option::<String>::None),
279            lock_poison: chio_supervisor::HealthFlag::new(true),
280            memory_provenance: None,
281            federation_peers: ArcSwap::from_pointee(HashMap::new()),
282            capability_trust_roots: ArcSwap::from_pointee(HashMap::new()),
283            capability_trust_roots_write_lock: Mutex::new(()),
284            federation_cosigner: None,
285            federation_dual_receipts: {
286                let gauge = chio_bounded::SizeGauge::new();
287                federation_dual_receipts_gauge = gauge.clone();
288                Mutex::new(chio_bounded::BoundedMap::new(
289                    federation_cache_capacity,
290                    federation_cache_idle_ttl,
291                    gauge,
292                ))
293            },
294            federation_dual_receipts_gauge,
295            federation_dsse_envelopes: {
296                let gauge = chio_bounded::SizeGauge::new();
297                federation_dsse_envelopes_gauge = gauge.clone();
298                Mutex::new(chio_bounded::BoundedMap::new(
299                    federation_cache_capacity,
300                    federation_cache_idle_ttl,
301                    gauge,
302                ))
303            },
304            federation_dsse_envelopes_gauge,
305            federation_artifact_store: None,
306            receipt_tenant_ids: Arc::new(DashMap::new()),
307            receipt_federation_admissions: Arc::new(DashMap::new()),
308            federation_local_kernel_id: ArcSwap::from_pointee(Option::<String>::None),
309            signing_task,
310            settlement_observer: None,
311            revocation_view: None,
312            budget_registry: Mutex::new(chio_kernel_core::InMemoryBudgetRegistry::new()),
313            reserved_sibling_shares: Mutex::new(HashMap::new()),
314            restart_reserved_hold_gate: Mutex::new(kernel_struct::RestartReservedHoldGate::Clear),
315            rss_shed: Arc::new(AtomicBool::new(false)),
316            rss_sampler: None,
317            receipt_writer_watchdog: std::sync::Arc::new(
318                receipt_writer_watchdog::ReceiptWriterWatchdogHandle::new(),
319            ),
320        };
321        // Start the RSS soft-ceiling sampler only when a limit is configured; with
322        // no limit set, no thread is spawned.
323        if let Some(limit) = rss_soft_limit_bytes {
324            kernel.rss_sampler = Some(kernel_struct::RssSamplerHandle::spawn(
325                Arc::clone(&kernel.rss_shed),
326                limit,
327                rss_sample_interval_secs,
328            ));
329        }
330        kernel
331    }
332
333    pub(crate) fn ensure_federated_receipt_persistence_ready(
334        &self,
335        remote_kernel_id: Option<&str>,
336    ) -> Result<(), KernelError> {
337        if remote_kernel_id.is_none() {
338            return Ok(());
339        }
340        match &self.receipt_store {
341            None => Err(KernelError::Internal(
342                "federated receipt persistence unavailable: no durable receipt store configured"
343                    .to_string(),
344            )),
345            Some(store) if store.writer_serving_closed() => Err(KernelError::Internal(
346                "federated receipt persistence degraded: commit writer is not serving".to_string(),
347            )),
348            Some(_) => Ok(()),
349        }
350    }
351
352    /// Record that a trusted-computing-base lock was found poisoned. A poisoned
353    /// lock means a panic unwound while the lock was held, so the state it guards
354    /// may be half-mutated. Trip the persistent degraded flag so the pre-dispatch
355    /// gate fails subsequent evaluations closed rather than proceeding on the
356    /// recovered state.
357    pub(crate) fn record_tcb_lock_poison(&self, lock: &str) {
358        self.lock_poison.record_failure(
359            format!("{lock} lock poisoned by a prior panic"),
360            chio_supervisor::now_unix_ms(),
361            0,
362        );
363    }
364
365    /// Pre-dispatch gate: deny before dispatch once any TCB lock has been found
366    /// poisoned, so no evaluation proceeds on state a panicking thread may have
367    /// left half-mutated. Recovery is operator-visible only.
368    pub(crate) fn ensure_tcb_locks_healthy(&self) -> Result<(), KernelError> {
369        if self.lock_poison.is_serving_closed() {
370            return Err(KernelError::Internal(
371                "trusted state degraded: a TCB lock was poisoned by a prior panic".to_string(),
372            ));
373        }
374        Ok(())
375    }
376
377    pub(crate) fn ensure_receipt_persistence_ready(&self) -> Result<(), KernelError> {
378        if let Some(store) = &self.receipt_store {
379            // A known-dead or degraded commit writer denies at the door: the tool
380            // never runs, so no side effect occurs without a durable receipt path.
381            if store.writer_serving_closed() {
382                return Err(KernelError::Internal(
383                    "durable receipt persistence degraded: commit writer is not serving"
384                        .to_string(),
385                ));
386            }
387        }
388        if self.receipt_store.is_none() && !self.config.allow_ephemeral_receipt_log {
389            return Err(KernelError::Internal(
390                "durable receipt persistence unavailable: no receipt store configured".to_string(),
391            ));
392        }
393        // A configured store is not enough: if the commit writer is wedged,
394        // saturated, or dead, dispatching would run a tool side effect that
395        // could never be durably receipted. Deny before that happens.
396        // `receipt_writer_liveness` samples the store directly when no watchdog
397        // has published a verdict, so the gate fails closed on a wedged writer
398        // whether or not the host started the background watchdog.
399        let liveness = self.receipt_writer_liveness();
400        if liveness.healthy() {
401            Ok(())
402        } else {
403            Err(KernelError::ReceiptWriterUnavailable(format!(
404                "receipt commit writer is {liveness:?}; denying before dispatch"
405            )))
406        }
407    }
408
409    /// Receipt-writer liveness verdict the pre-dispatch gate reads. Prefers the
410    /// watchdog's most recent published verdict; when no watchdog is running the
411    /// published verdict stays `Unknown`, which the gate would otherwise treat as
412    /// permissive. Fall back to sampling the installed store directly in that
413    /// case so a durable store with a wedged commit writer is denied regardless
414    /// of whether the host started the watchdog. A store without an async writer
415    /// reports `Unknown` from the sample too, preserving the permissive behavior
416    /// for writer-less stores.
417    pub(crate) fn receipt_writer_liveness(&self) -> crate::receipt_store::ReceiptWriterLiveness {
418        let published = self.receipt_writer_watchdog.current();
419        if published != crate::receipt_store::ReceiptWriterLiveness::Unknown {
420            return published;
421        }
422        self.sample_receipt_writer_liveness()
423    }
424
425    /// Sample the installed store's commit-writer liveness against the configured
426    /// stall threshold. Reads the store's in-memory writer counters, so it is
427    /// cheap enough to run inline on the pre-dispatch path. Returns `Unknown`
428    /// when no store is installed or the store has no async writer.
429    fn sample_receipt_writer_liveness(&self) -> crate::receipt_store::ReceiptWriterLiveness {
430        let stall_threshold =
431            std::time::Duration::from_millis(self.config.deadlines.receipt_writer_stall_ms);
432        match self.with_receipt_store(|store| Ok(store.writer_liveness(stall_threshold))) {
433            Ok(Some(verdict)) => verdict,
434            _ => crate::receipt_store::ReceiptWriterLiveness::Unknown,
435        }
436    }
437
438    /// Sample the installed store's writer liveness once and publish it into the
439    /// gate's cell, without spawning the background poll task. Test-only shim so
440    /// the pre-dispatch gate can be exercised deterministically.
441    #[cfg(test)]
442    pub(crate) fn refresh_receipt_writer_liveness_for_test(&self) {
443        self.receipt_writer_watchdog
444            .publish(self.sample_receipt_writer_liveness());
445    }
446
447    /// Whether the background receipt-writer watchdog poll task is installed.
448    #[cfg(test)]
449    pub(crate) fn receipt_writer_watchdog_is_running(&self) -> bool {
450        self.receipt_writer_watchdog.is_running()
451    }
452
453    /// Start the receipt-writer liveness watchdog. Opt-in: the hosting edge
454    /// calls this in an async context. It polls the store's liveness on the
455    /// configured cadence and publishes the verdict the pre-dispatch gate reads.
456    pub fn spawn_receipt_writer_watchdog(self: &std::sync::Arc<Self>) {
457        // The watchdog polls on a `tokio::time::interval`, which panics in a
458        // runtime built without a time driver. Skip the background poll in that
459        // case rather than crash the host: the pre-dispatch gate already falls
460        // back to sampling the store's writer liveness directly when no verdict
461        // is published, so a timerless host stays fail-closed without the task.
462        if !super::dispatch::dispatch_timer_available() {
463            return;
464        }
465        let poll =
466            std::time::Duration::from_millis(self.config.deadlines.receipt_writer_poll_ms.max(1));
467        // Hold a weak reference between ticks. The kernel owns this task's join
468        // handle, so a strong reference here would form a cycle (kernel -> handle
469        // -> task -> kernel) that keeps the kernel and its receipt store alive
470        // forever when the last external Arc is dropped without calling
471        // shutdown(). Upgrade only to take a sample, then release before awaiting
472        // the next tick, and exit once the kernel is gone.
473        let kernel = std::sync::Arc::downgrade(self);
474        let handle = tokio::spawn(async move {
475            let mut ticker = tokio::time::interval(poll);
476            loop {
477                ticker.tick().await;
478                let Some(kernel) = kernel.upgrade() else {
479                    return;
480                };
481                let verdict = kernel.sample_receipt_writer_liveness();
482                kernel.receipt_writer_watchdog.publish(verdict);
483            }
484        });
485        self.receipt_writer_watchdog.set_join_handle(handle);
486    }
487
488    pub(crate) fn ensure_revocation_durability_ready(&self) -> Result<(), KernelError> {
489        // A remote revocation view (federation/oracle) is consulted on every
490        // delegated dispatch and is re-synced from its source after a restart, so
491        // an installed view is itself a durable revocation source: it satisfies
492        // the gate even when the local per-row store is the default in-memory one.
493        if self.revocation_view.is_some() {
494            return Ok(());
495        }
496        let ephemeral = self.with_revocation_store(|store| Ok(store.is_ephemeral()))?;
497        if !ephemeral || self.config.allow_ephemeral_revocation_store {
498            return Ok(());
499        }
500        Err(KernelError::Internal(
501            "durable revocation state unavailable: no revocation store configured".to_string(),
502        ))
503    }
504
505    pub(crate) fn scope_receipt_federation_admission_for_request(
506        &self,
507        request_id: &str,
508        admission: ReceiptFederationAdmission,
509    ) -> ScopedKernelReceiptFederationAdmission {
510        let previous = self
511            .receipt_federation_admissions
512            .insert(request_id.to_string(), admission);
513        ScopedKernelReceiptFederationAdmission {
514            request_id: request_id.to_string(),
515            admissions: Arc::clone(&self.receipt_federation_admissions),
516            previous,
517        }
518    }
519
520    pub(crate) fn receipt_federation_admission_for_request(
521        &self,
522        request_id: &str,
523        remote_kernel_id: Option<&str>,
524    ) -> Option<ReceiptFederationAdmission> {
525        let admission = self
526            .receipt_federation_admissions
527            .get(request_id)
528            .map(|entry| entry.value().clone())?;
529        if admission.remote_kernel_id.as_deref() == remote_kernel_id {
530            Some(admission)
531        } else {
532            None
533        }
534    }
535
536    /// Resolve and snapshot federation receipt admission at the boundary.
537    /// The returned snapshot must be carried through receipt persistence and
538    /// federation cosigning; persistence must not re-resolve peer freshness
539    /// after the tool has already executed.
540    pub(crate) fn kernel_receipt_admission_for_remote(
541        &self,
542        remote_kernel_id: Option<&str>,
543        now: u64,
544    ) -> Result<ReceiptFederationAdmission, KernelError> {
545        if let Some(remote) = remote_kernel_id {
546            if let Some(peer) = self.federation_peer(remote, now) {
547                return Ok(ReceiptFederationAdmission {
548                    remote_kernel_id: Some(remote.to_string()),
549                    peer: Some(peer),
550                });
551            }
552            return Err(KernelError::Internal(format!(
553                "named federation peer {remote} is not pinned fresh"
554            )));
555        }
556        Ok(ReceiptFederationAdmission {
557            remote_kernel_id: None,
558            peer: None,
559        })
560    }
561
562    /// Install (or replace) the recursive-delegation oracle handle.
563    /// Default deployments leave this `None` and rely on the compatibility
564    /// per-row `RevocationStore` lookup. Installing a
565    /// [`chio_kernel_core::RevocationView`] here causes the verifier to
566    /// consult it on every delegated dispatch.
567    ///
568    /// The handle is `Arc`-shared so federation gossip can install
569    /// monotone snapshot updates without holding a kernel mutex.
570    pub fn set_revocation_view(&mut self, view: std::sync::Arc<chio_kernel_core::RevocationView>) {
571        self.revocation_view = Some(view);
572    }
573
574    /// Borrow the currently-installed revocation view, if any.
575    #[must_use]
576    pub fn revocation_view(&self) -> Option<&std::sync::Arc<chio_kernel_core::RevocationView>> {
577        self.revocation_view.as_ref()
578    }
579
580    /// Borrow the kernel's mpsc-backed signing-task handle.
581    ///
582    /// Internal callers submit `ChioReceiptBody` payloads through this handle
583    /// and `.await` the signed `ChioReceipt`. The underlying tokio task is
584    /// spawned lazily on the first call.
585    ///
586    /// Not exposed publicly: callers should go through
587    /// [`Self::sign_receipt_via_channel`] or the `ToolEvaluator` trait so the
588    /// channel boundary stays an implementation detail of the kernel crate.
589    /// Crash-recovery tests reach this directly.
590    #[allow(dead_code)]
591    pub(crate) fn signing_task_handle(&self) -> &signing_task::SigningTaskHandle {
592        &self.signing_task
593    }
594
595    /// Sign a [`ChioReceiptBody`] off the kernel critical path via the
596    /// mpsc-backed signing task.
597    ///
598    /// Producers `.await` on bounded backpressure rather than on a
599    /// receipt-log mutex. Returns
600    /// `Err(KernelError::ReceiptSigningFailed)` if the signing step
601    /// itself rejected the body (e.g. the body's `kernel_key` does not
602    /// match the kernel's signing public key) and
603    /// `Err(KernelError::Internal)` if the signing task is no longer
604    /// running.
605    ///
606    /// This method is async because it `.await`s on the channel send
607    /// (backpressure) and on the oneshot reply. The caller must run
608    /// inside a tokio runtime; every kernel-internal call site already
609    /// does (the `ToolEvaluator` trait methods are async, and so is
610    /// the public `evaluate_tool_call` entrypoint).
611    ///
612    /// `canonical_content` is the exact byte preimage `body.content_hash` was
613    /// derived from. The signing task recomputes `sha256_hex(canonical_content)`
614    /// inside the trust boundary and refuses to sign on mismatch (WYSIWYS,
615    /// ), so this channel path is as fail-closed as the inline
616    /// `build_and_sign_receipt` path.
617    pub async fn sign_receipt_via_channel(
618        &self,
619        body: ChioReceiptBody,
620        canonical_content: Vec<u8>,
621    ) -> Result<ChioReceipt, KernelError> {
622        self.signing_task.sign(body, canonical_content).await
623    }
624
625    /// Drain the in-flight signing-task queue and join the task. After this
626    /// call, every signing request that successfully `.send().await`-ed before
627    /// shutdown has been signed and replied to; new sends surface
628    /// `KernelError::Internal`.
629    ///
630    /// Idempotent: safe to call more than once. Safe to call on a
631    /// kernel whose signing task was never spawned (no signing
632    /// happened); in that case shutdown is a no-op.
633    ///
634    /// Note: shutdown does NOT mark the kernel as terminally stopped
635    /// for other paths (capability validation, guard pipeline, store
636    /// lookups). Operators that want a hard stop should call
637    /// [`Self::emergency_stop`] in addition.
638    pub async fn shutdown(&self) {
639        self.signing_task.shutdown().await;
640        self.receipt_writer_watchdog.shutdown().await;
641    }
642
643    pub fn set_receipt_store(
644        &mut self,
645        receipt_store: Box<dyn ReceiptStore>,
646    ) -> Result<(), KernelError> {
647        self.set_receipt_store_handle(Arc::from(receipt_store))
648    }
649
650    pub fn configure_durable_admission(
651        &mut self,
652        mode: crate::admission_operation::DurableAdmissionMode,
653        unsafe_development: bool,
654    ) -> Result<(), crate::admission_operation::AdmissionOperationError> {
655        let receipts = if self.config.allow_ephemeral_receipt_log {
656            crate::admission_operation::AdmissionReceiptPersistence::Ephemeral
657        } else {
658            crate::admission_operation::AdmissionReceiptPersistence::Durable
659        };
660        self.durable_admission_mode = mode.validate_configuration(unsafe_development, receipts)?;
661        Ok(())
662    }
663
664    #[must_use]
665    pub fn durable_admission_mode(&self) -> crate::admission_operation::DurableAdmissionMode {
666        self.durable_admission_mode
667    }
668
669    pub fn set_durable_admission_store(
670        &mut self,
671        store: Arc<dyn crate::receipt_store::QualifiedAdmissionProjectionStore>,
672        outcome_store: Arc<dyn crate::tool_outcome::QualifiedToolOutcomeStore>,
673        fence: crate::admission_operation::StoreMutationFence,
674    ) -> Result<(), crate::admission_operation::AdmissionOperationError> {
675        self.durable_admission_runtime = Some(DurableAdmissionRuntime::new(
676            store,
677            outcome_store,
678            fence,
679            &self.config.keypair.public_key().to_hex(),
680        )?);
681        Ok(())
682    }
683
684    /// Enable legacy financial dispatch without durable admission recovery.
685    ///
686    /// This is an unsafe development compatibility switch. Ambiguous connector
687    /// outcomes can retain budget or rail holds permanently because no recovery
688    /// operation survives restart. Production deployments should install a
689    /// qualified durable admission store instead.
690    pub fn enable_unsafe_ephemeral_financial_dispatch_for_development(&mut self) {
691        self.unsafe_ephemeral_financial_dispatch = true;
692    }
693
694    pub fn set_channel_terminal_authority(
695        &mut self,
696        authority: Arc<dyn crate::admission_operation::QualifiedChannelTerminalAuthority>,
697    ) -> Result<(), KernelError> {
698        let runtime = self.durable_admission_runtime.as_mut().ok_or_else(|| {
699            KernelError::DurableAdmission(
700                "channel terminal authority requires a durable admission store".to_owned(),
701            )
702        })?;
703        runtime.set_channel_terminal_authority(authority);
704        Ok(())
705    }
706
707    pub fn set_receipt_store_handle(
708        &mut self,
709        receipt_store: Arc<dyn ReceiptStore>,
710    ) -> Result<(), KernelError> {
711        self.try_set_receipt_store_handle(receipt_store)
712    }
713
714    pub fn try_set_receipt_store_handle(
715        &mut self,
716        receipt_store: Arc<dyn ReceiptStore>,
717    ) -> Result<(), KernelError> {
718        if let Some(runtime) = self.settlement_observer.as_ref() {
719            Self::validate_settlement_receipt_store(receipt_store.as_ref(), runtime)?;
720        }
721        match receipt_store.load_latest_checkpoint() {
722            Ok(Some(checkpoint)) => {
723                self.checkpoint_seq_counter
724                    .store(checkpoint.body.checkpoint_seq, Ordering::SeqCst);
725                self.last_checkpoint_seq
726                    .store(checkpoint.body.batch_end_seq, Ordering::SeqCst);
727            }
728            Ok(None) => {
729                self.checkpoint_seq_counter.store(0, Ordering::SeqCst);
730                self.last_checkpoint_seq.store(0, Ordering::SeqCst);
731            }
732            Err(error) => {
733                return Err(KernelError::Internal(format!(
734                    "failed to hydrate checkpoint counters from receipt store: {error}"
735                )));
736            }
737        }
738        // Honor disabled checkpointing: KernelConfig
739        // documents `checkpoint_batch_size = 0` as DISABLING automatic
740        // checkpointing (non-web3 deployments). Only require a background signer
741        // when checkpointing is enabled (batch_size > 0); with 0 the store
742        // attaches with no checkpoint machinery. Web3-enabled deployments that
743        // MUST checkpoint are separately guarded by
744        // `validate_web3_evidence_prerequisites`, which rejects batch_size == 0.
745        if self.checkpoint_batch_size > 0 && receipt_store.supports_kernel_signed_checkpoints() {
746            let background_enabled = receipt_store
747                .enable_background_checkpoints(
748                    self.config.keypair.clone(),
749                    self.checkpoint_batch_size,
750                )
751                .map_err(|error| {
752                    KernelError::Internal(format!(
753                        "failed to enable background receipt checkpoints: {error}"
754                    ))
755                })?;
756            // Fail-closed: a store that claims checkpoint capability but did
757            // not install a background signer (default hook returns
758            // `Ok(false)`) would append forever without producing kernel-signed
759            // Web3 checkpoints now that the synchronous trigger is gone. Reject
760            // the attach rather than serve silently-checkpointless.
761            if !background_enabled {
762                return Err(KernelError::Internal(
763                    "receipt store reports kernel-signed checkpoint support but did not install a \
764                     background checkpoint signer; refusing to attach a store that would append \
765                     without producing checkpoints"
766                        .to_string(),
767                ));
768            }
769        }
770        // Spawn the retention maintenance worker only when retention is
771        // configured; an unconfigured deployment gets no background thread.
772        if let Some(config) = self.config.retention_config.clone() {
773            // Fail-closed: prefix retention can only archive receipts already
774            // covered by a kernel checkpoint (the archival watermark advances to
775            // checkpoint boundaries). With automatic checkpointing disabled
776            // (`checkpoint_batch_size == 0`) no background signer was installed
777            // above, so the checkpoint chain can never advance past 0 and the
778            // retention worker could never archive anything: the store would
779            // serve forever under a policy it can never honor, silently retaining
780            // every receipt. Reject the attach rather than run a retention policy
781            // that can never advance its watermark.
782            if self.checkpoint_batch_size == 0 {
783                return Err(KernelError::Internal(
784                    "KernelConfig.retention_config is set but automatic checkpointing is disabled \
785                     (checkpoint_batch_size == 0); prefix retention can only archive \
786                     checkpoint-covered receipts, so refusing to attach a store that could never \
787                     advance its retention watermark"
788                        .to_string(),
789                ));
790            }
791            // Fail-closed: configured retention is a storage/compliance control.
792            // A store whose rotate_receipts is the default unsupported implementation would
793            // attach and then only log "retention not supported" on every worker
794            // interval, silently never archiving. Reject the attach rather than
795            // serve traffic under a retention policy the store cannot honor.
796            if !receipt_store.supports_retention() {
797                return Err(KernelError::Internal(
798                    "receipt store does not support retention but KernelConfig.retention_config \
799                     is set; refusing to attach a store that cannot honor the configured \
800                     retention policy"
801                        .to_string(),
802                ));
803            }
804            // Fail-closed: a tenant-scoped retention policy cannot be honored by a
805            // prefix-watermark store (rotation archives a contiguous checkpointed
806            // prefix of the whole log, not one tenant's rows), so its rotate call
807            // fails closed and the worker would only log "tenant scope
808            // unsupported" every interval and never archive. Reject the attach
809            // rather than serve traffic under a policy the store cannot honor.
810            if config.tenant_id.is_some() && !receipt_store.supports_tenant_scoped_retention() {
811                return Err(KernelError::Internal(
812                    "KernelConfig.retention_config sets a tenant scope but the receipt store does \
813                     not support tenant-scoped retention; refusing to attach a store that cannot \
814                     honor the configured retention policy"
815                        .to_string(),
816                ));
817            }
818            self.retention_maintenance =
819                Some(crate::receipt_store::RetentionMaintenanceHandle::spawn(
820                    Arc::clone(&receipt_store),
821                    config,
822                ));
823        }
824        self.receipt_store = Some(receipt_store);
825        Ok(())
826    }
827
828    pub fn set_payment_adapter(&mut self, payment_adapter: Box<dyn PaymentAdapter>) {
829        self.payment_adapter = Some(payment_adapter);
830    }
831
832    pub fn set_price_oracle(&mut self, price_oracle: Box<dyn PriceOracle>) {
833        self.price_oracle = Some(price_oracle);
834    }
835
836    pub fn set_attestation_trust_policy(
837        &mut self,
838        attestation_trust_policy: AttestationTrustPolicy,
839    ) {
840        self.attestation_trust_policy = Some(attestation_trust_policy);
841    }
842
843    /// Accept the default in-memory revocation store instead of requiring a
844    /// durable one. A locally-run kernel with no durable or remote revocation
845    /// backend keeps its revocation set in memory, so the durability gate would
846    /// otherwise deny every dispatch. Opting in here relaxes the gate
847    /// only while the store is genuinely ephemeral: installing a durable store
848    /// (or a revocation view) satisfies the gate on its own regardless of this
849    /// flag. Intended for local, interactive runtimes; deployments that must
850    /// survive restart should wire a durable store instead of calling this.
851    pub fn opt_in_ephemeral_revocation_store(&mut self) {
852        self.config.allow_ephemeral_revocation_store = true;
853    }
854
855    pub fn set_revocation_store(&mut self, revocation_store: Box<dyn RevocationStore>) {
856        self.set_revocation_store_handle(Arc::from(revocation_store));
857    }
858
859    pub fn set_revocation_store_handle(&mut self, revocation_store: Arc<dyn RevocationStore>) {
860        self.revocation_store = revocation_store;
861    }
862
863    pub fn set_capability_authority(&mut self, capability_authority: Box<dyn CapabilityAuthority>) {
864        self.capability_authority = capability_authority;
865    }
866
867    pub fn set_budget_store(&mut self, budget_store: Box<dyn BudgetStore>) {
868        self.set_budget_store_handle(Arc::from(budget_store));
869    }
870
871    pub fn set_budget_store_handle(&mut self, budget_store: Arc<dyn BudgetStore>) {
872        self.budget_store = budget_store;
873    }
874
875    pub fn set_post_invocation_pipeline(
876        &mut self,
877        pipeline: crate::post_invocation::PostInvocationPipeline,
878    ) {
879        self.post_invocation_pipeline = pipeline;
880    }
881
882    pub fn add_post_invocation_hook(
883        &mut self,
884        hook: Box<dyn crate::post_invocation::PostInvocationHook>,
885    ) {
886        self.post_invocation_pipeline.add(hook);
887    }
888
889    /// Install a settlement hook with its durable outcome store and retry policy.
890    ///
891    /// Before dispatch, the kernel requires a receipt store that can atomically
892    /// seed pending settlement work within the configured receipt-append
893    /// deadline. The store and runtime may be configured in either order;
894    /// attaching an incompatible store fails without replacing the active one.
895    ///
896    /// The returned error must not be discarded: swallowing it leaves the kernel
897    /// dispatching charges with settlement uninstalled. Embedders porting from
898    /// `set_settlement_observer` should read
899    /// `docs/migrations/kernel-embedder-surface.md`, which lists the receipt-store
900    /// capabilities this requires and the configuration failure modes.
901    pub fn set_settlement_observer_runtime(
902        &mut self,
903        hook: Arc<dyn chio_settle::SettlementHook>,
904        outcome_store: Arc<dyn chio_settle::SettlementOutcomeStore>,
905        retry_policy: chio_settle::RetryPolicy,
906    ) -> Result<(), KernelError> {
907        let runtime = crate::settlement_routing::SettlementObserverRuntime::new(
908            hook,
909            outcome_store,
910            retry_policy,
911        )
912        .map_err(crate::SettlementRuntimeConfigError::from)?;
913        if let Some(receipt_store) = self.receipt_store.as_ref() {
914            Self::validate_settlement_receipt_store(receipt_store.as_ref(), &runtime)?;
915        }
916        self.settlement_observer = Some(runtime);
917        Ok(())
918    }
919
920    fn validate_settlement_receipt_store(
921        receipt_store: &dyn ReceiptStore,
922        runtime: &crate::settlement_routing::SettlementObserverRuntime,
923    ) -> Result<(), KernelError> {
924        if receipt_store.atomic_receipt_projection()
925            != crate::receipt_store::AtomicReceiptProjection::SettlementObservationV1
926            || !receipt_store.supports_atomic_receipt_projection_with_timeout()
927        {
928            return Err(crate::SettlementRuntimeConfigError::UnsupportedAtomicProjection.into());
929        }
930        let Some(receipt_binding) = receipt_store.settlement_store_binding() else {
931            return Err(crate::SettlementRuntimeConfigError::MissingStoreBinding.into());
932        };
933        if receipt_binding != runtime.store_binding() {
934            return Err(crate::SettlementRuntimeConfigError::StoreBindingMismatch.into());
935        }
936        Ok(())
937    }
938
939    /// Return the active settlement hook without exposing routing internals.
940    #[must_use]
941    pub fn settlement_observer(&self) -> Option<Arc<dyn chio_settle::SettlementHook>> {
942        self.settlement_observer
943            .as_ref()
944            .map(crate::settlement_routing::SettlementObserverRuntime::hook)
945    }
946
947    /// Invoke the registered settlement hook against a
948    /// freshly signed receipt. The receipt is observer-only relative
949    /// to this call: callers MUST pass a receipt that has already been
950    /// signed and stored, and the returned status NEVER feeds back
951    /// into the dispatch path.
952    #[must_use]
953    pub fn run_settlement_observer(
954        &self,
955        receipt: &chio_core::receipt::body::ChioReceipt,
956        idempotency_key: &chio_settle::SettlementIdempotencyKey,
957    ) -> settlement_observer::SettlementObserverStatus {
958        settlement_observer::run_observer(
959            self.settlement_observer
960                .as_ref()
961                .map(crate::settlement_routing::SettlementObserverRuntime::hook_ref),
962            receipt,
963            &[self.public_key()],
964            idempotency_key,
965        )
966    }
967
968    /// Install a memory-provenance chain.
969    ///
970    /// Once installed, every governed `MemoryWrite`-shaped tool call
971    /// appends an entry to the chain after the allow receipt is
972    /// signed. A chain-store failure on that path is fatal: the call
973    /// surfaces `KernelError::Internal(...)` so operators can detect
974    /// and repair the drift rather than silently shipping a write
975    /// without provenance evidence.
976    ///
977    /// Every `MemoryRead`-shaped tool call looks the entry up by
978    /// `(store, key)` and attaches the result to the receipt as
979    /// `memory_provenance` evidence metadata. Reads with no chain
980    /// entry or with a tampered chain surface as
981    /// [`crate::memory_provenance::ProvenanceVerification::Unverified`]
982    /// so the receipt unambiguously records the gap.
983    pub fn set_memory_provenance_store(
984        &mut self,
985        store: Arc<dyn crate::memory_provenance::MemoryProvenanceStore>,
986    ) {
987        self.memory_provenance = Some(store);
988    }
989
990    /// Return a clone of the active memory-provenance store handle,
991    /// or `None` when no provenance chain has been installed.
992    ///
993    /// Useful for integration tests that want to assert on the chain
994    /// state directly after driving `evaluate_tool_call`.
995    #[must_use]
996    pub fn memory_provenance_store(
997        &self,
998    ) -> Option<Arc<dyn crate::memory_provenance::MemoryProvenanceStore>> {
999        self.memory_provenance.as_ref().map(Arc::clone)
1000    }
1001
1002    /// Install a set of [`chio_federation::trust_establishment::FederationPeer`]s
1003    /// this kernel trusts for bilateral co-signing. Overwrites any
1004    /// previously declared set. Callers typically obtain these peers
1005    /// from [`chio_federation::trust_establishment::KernelTrustExchange::accept_envelope`]
1006    /// after a successful mTLS handshake.
1007    ///
1008    /// Builder-style so deployments can chain `.with_federation_peers(...)`
1009    /// onto `ChioKernel::new(config)`.
1010    #[must_use]
1011    pub fn with_federation_peers(
1012        self,
1013        peers: Vec<chio_federation::trust_establishment::FederationPeer>,
1014    ) -> Self {
1015        let mut next = HashMap::new();
1016        for peer in peers {
1017            next.insert(peer.kernel_id.clone(), peer);
1018        }
1019        self.federation_peers.store(Arc::new(next));
1020        self
1021    }
1022
1023    /// Builder-style so deployments can chain
1024    /// `.with_capability_trust_roots(...)` onto `ChioKernel::new(config)`.
1025    #[must_use]
1026    pub fn with_capability_trust_roots(
1027        self,
1028        roots: Vec<(
1029            chio_core::PublicKey,
1030            chio_core::capability::attenuation::ScopeHash,
1031        )>,
1032    ) -> Self {
1033        {
1034            let _guard = self
1035                .capability_trust_roots_write_lock
1036                .lock()
1037                .unwrap_or_else(|poisoned| poisoned.into_inner());
1038            let mut next: HashMap<String, chio_core::capability::attenuation::ScopeHash> =
1039                HashMap::new();
1040            for (issuer, root) in roots {
1041                next.insert(issuer.to_hex(), root);
1042            }
1043            self.capability_trust_roots.store(Arc::new(next));
1044        }
1045        self
1046    }
1047
1048    /// Install or update a single capability trust-root entry without
1049    /// requiring builder-style construction. Returns the previous root
1050    /// hash for that issuer, if any.
1051    pub fn set_capability_trust_root(
1052        &self,
1053        issuer: chio_core::PublicKey,
1054        root: chio_core::capability::attenuation::ScopeHash,
1055    ) -> Option<chio_core::capability::attenuation::ScopeHash> {
1056        let _guard = self
1057            .capability_trust_roots_write_lock
1058            .lock()
1059            .unwrap_or_else(|poisoned| poisoned.into_inner());
1060        let current = self.capability_trust_roots.load_full();
1061        let mut next: HashMap<String, chio_core::capability::attenuation::ScopeHash> =
1062            (*current).clone();
1063        let prev = next.insert(issuer.to_hex(), root);
1064        self.capability_trust_roots.store(Arc::new(next));
1065        prev
1066    }
1067
1068    /// Snapshot the currently registered capability trust-root registry.
1069    /// Hot-path callers should prefer the resolver returned by
1070    /// `capability_trust_root_resolver_snapshot`.
1071    pub fn capability_trust_roots_snapshot(
1072        &self,
1073    ) -> HashMap<String, chio_core::capability::attenuation::ScopeHash> {
1074        (*self.capability_trust_roots.load_full()).clone()
1075    }
1076
1077    /// Build an owned snapshot of the capability trust-root registry
1078    /// suitable for use as a `chio_kernel_core::TrustRootResolver`. The
1079    /// returned closure captures a frozen snapshot so concurrent
1080    /// rotations cannot tear an in-flight verification.
1081    pub(crate) fn capability_trust_root_resolver_snapshot(
1082        &self,
1083    ) -> impl Fn(&chio_core::PublicKey) -> Option<chio_core::capability::attenuation::ScopeHash>
1084           + Send
1085           + Sync
1086           + 'static {
1087        let snapshot: Arc<HashMap<String, chio_core::capability::attenuation::ScopeHash>> =
1088            self.capability_trust_roots.load_full();
1089        move |issuer: &chio_core::PublicKey| -> Option<chio_core::capability::attenuation::ScopeHash> {
1090            snapshot.get(&issuer.to_hex()).cloned()
1091        }
1092    }
1093
1094    pub(crate) fn capability_negotiation_for_remote(
1095        &self,
1096        remote_kernel_id: Option<&str>,
1097        now: u64,
1098    ) -> Result<chio_core::capability::features::CapabilityNegotiation, String> {
1099        if let Some(remote) = remote_kernel_id {
1100            if let Some(peer) = self.federation_peer(remote, now) {
1101                return Ok(peer.capabilities);
1102            }
1103            return Err(format!(
1104                "no fresh federation peer negotiation profile pinned for remote kernel {remote}"
1105            ));
1106        }
1107        let mut local = chio_core::capability::features::CapabilityNegotiation::t1_default();
1108        local.features.insert(
1109            chio_core::capability::features::AGGREGATE_INVOCATION_BUDGET.to_string(),
1110            true,
1111        );
1112        local.features.insert(
1113            chio_core::capability::features::CUMULATIVE_APPROVAL_BUDGET.to_string(),
1114            true,
1115        );
1116        Ok(local)
1117    }
1118
1119    /// Install the bilateral cosigner responsible for
1120    /// contacting a peer kernel to obtain a co-signature. Production
1121    /// deployments plug in an mTLS-backed RPC client; tests can use
1122    /// [`chio_federation::bilateral::InProcessCoSigner`].
1123    pub fn set_federation_cosigner(
1124        &mut self,
1125        cosigner: Arc<dyn chio_federation::bilateral::BilateralCoSigningProtocol>,
1126    ) {
1127        self.federation_cosigner = Some(cosigner);
1128    }
1129
1130    /// Install a durable [`crate::federation_artifact_store::FederationArtifactStore`]
1131    /// for bilateral co-sign artifacts.
1132    ///
1133    /// When set, [`Self::apply_federation_cosign`] writes each
1134    /// `DualSignedReceipt` / `DsseEnvelope` through to the store BEFORE inserting
1135    /// it into the bounded in-memory caches, and [`Self::dual_signed_receipt`] /
1136    /// [`Self::federation_dsse_envelope`] fall back to the store on a cache miss.
1137    /// This closes the evidence-loss window where a federated deployment
1138    /// producing more than `federation_cache_capacity` receipts would drop
1139    /// evicted co-sign artifacts while the durable receipt store keeps only the
1140    /// base receipt. Without a store installed, evicted co-sign artifacts are
1141    /// lossy by policy (the bounded caches drop-oldest at capacity).
1142    ///
1143    /// A deployment requiring durable bilateral evidence installs a
1144    /// database-backed impl; the bundled
1145    /// [`crate::federation_artifact_store::InMemoryFederationArtifactStore`] is a
1146    /// capped, idle-swept reference impl and test double.
1147    pub fn set_federation_artifact_store(
1148        &mut self,
1149        store: Arc<dyn crate::federation_artifact_store::FederationArtifactStore>,
1150    ) {
1151        self.federation_artifact_store = Some(store);
1152    }
1153
1154    /// Advertise this kernel's stable identifier as seen by
1155    /// remote federation peers. When unset, the hex encoding of the
1156    /// signing public key is used. Setting this is recommended in
1157    /// production so receipts reference DNS names rather than raw keys.
1158    pub fn set_federation_local_kernel_id(&self, kernel_id: impl Into<String>) {
1159        self.federation_local_kernel_id
1160            .store(Arc::new(Some(kernel_id.into())));
1161    }
1162
1163    /// Resolve the active federation peer for
1164    /// `remote_kernel_id`, refusing stale pins fail-closed.
1165    pub fn federation_peer(
1166        &self,
1167        remote_kernel_id: &str,
1168        now: u64,
1169    ) -> Option<chio_federation::trust_establishment::FederationPeer> {
1170        let peers = self.federation_peers.load();
1171        let peer = peers.get(remote_kernel_id)?.clone();
1172        if peer.is_fresh(now) {
1173            Some(peer)
1174        } else {
1175            None
1176        }
1177    }
1178
1179    /// Snapshot the currently-pinned federation peer set.
1180    pub fn federation_peers_snapshot(
1181        &self,
1182    ) -> Vec<chio_federation::trust_establishment::FederationPeer> {
1183        self.federation_peers.load().values().cloned().collect()
1184    }
1185
1186    /// Look up a dual-signed receipt by the underlying
1187    /// [`chio_core::receipt::body::ChioReceipt`] id. Returns `None` when the
1188    /// receipt did not cross a federation boundary or when the
1189    /// co-signing hook has not yet produced a dual-signed artifact
1190    /// for it.
1191    pub fn dual_signed_receipt(
1192        &self,
1193        receipt_id: &str,
1194    ) -> Option<chio_federation::bilateral::DualSignedReceipt> {
1195        let now = current_unix_timestamp();
1196        {
1197            let mut cache = match self.federation_dual_receipts.lock() {
1198                Ok(g) => g,
1199                Err(poisoned) => poisoned.into_inner(),
1200            };
1201            if let Some(hit) = cache.get(&receipt_id.to_string(), now) {
1202                return Some(hit.clone());
1203            }
1204        }
1205        self.federation_artifact_store.as_ref().and_then(|store| {
1206            // A store READ error is not an admission gate (writes fail closed via
1207            // `?` elsewhere), but collapsing Err to None silently makes a transient
1208            // read failure indistinguishable from an absent artifact. Log it so the
1209            // read-through fallback is observable.
1210            match store.get_dual_signed(receipt_id) {
1211                Ok(hit) => hit,
1212                Err(error) => {
1213                    debug!(
1214                        receipt_id = %receipt_id,
1215                        reason = %redacted!(&error.to_string()),
1216                        "federation artifact-store dual-signed read failed; treating as absent"
1217                    );
1218                    None
1219                }
1220            }
1221        })
1222    }
1223
1224    pub fn federation_dsse_envelope(
1225        &self,
1226        receipt_id: &str,
1227    ) -> Option<chio_federation::bilateral_dsse::DsseEnvelope> {
1228        let now = current_unix_timestamp();
1229        {
1230            let mut cache = match self.federation_dsse_envelopes.lock() {
1231                Ok(g) => g,
1232                Err(poisoned) => poisoned.into_inner(),
1233            };
1234            if let Some(hit) = cache.get(&receipt_id.to_string(), now) {
1235                return Some(hit.clone());
1236            }
1237        }
1238        self.federation_artifact_store.as_ref().and_then(|store| {
1239            // As with the dual-signed read above: log a swallowed store read error
1240            // so a transient failure is not silently reported as an absent DSSE
1241            // envelope.
1242            match store.get_dsse(receipt_id) {
1243                Ok(hit) => hit,
1244                Err(error) => {
1245                    debug!(
1246                        receipt_id = %receipt_id,
1247                        reason = %redacted!(&error.to_string()),
1248                        "federation artifact-store DSSE read failed; treating as absent"
1249                    );
1250                    None
1251                }
1252            }
1253        })
1254    }
1255
1256    /// Local kernel identifier used in bilateral co-signing. Falls back
1257    /// to the hex encoding of the signing public key.
1258    pub fn federation_local_kernel_id(&self) -> String {
1259        if let Some(id) = self.federation_local_kernel_id.load_full().as_ref() {
1260            return id.clone();
1261        }
1262        self.config.keypair.public_key().to_hex()
1263    }
1264
1265    fn treaty_dsse_extensions_from_receipt_metadata(
1266        &self,
1267        receipt: &chio_core::receipt::body::ChioReceipt,
1268    ) -> Result<Option<chio_federation::bilateral_dsse::BilateralPredicateExtensions>, KernelError>
1269    {
1270        let Some(metadata) = receipt.metadata.as_ref() else {
1271            return Ok(None);
1272        };
1273        let Some(value) = metadata
1274            .get("chio_runtime")
1275            .and_then(|runtime| runtime.get("federation_treaty_dsse"))
1276        else {
1277            return Ok(None);
1278        };
1279        let material: KernelFederationTreatyDsseMetadata = serde_json::from_value(value.clone())
1280            .map_err(|error| {
1281                KernelError::Internal(format!(
1282                    "federation treaty DSSE metadata is invalid: {error}"
1283                ))
1284            })?;
1285        let consistency_model = material.consistency_model.clone().ok_or_else(|| {
1286            KernelError::Internal(
1287                "federation treaty DSSE consistency model is missing from runtime material"
1288                    .to_string(),
1289            )
1290        })?;
1291        if consistency_model != material.treaty_binding_ref.consistency_model {
1292            return Err(KernelError::Internal(
1293                "federation treaty DSSE consistency model does not match treaty binding"
1294                    .to_string(),
1295            ));
1296        }
1297        let mut treaty_binding_ref = material.treaty_binding_ref;
1298        if treaty_binding_ref.request_sha256 != receipt.action.parameter_hash {
1299            return Err(KernelError::Internal(
1300                "federation treaty DSSE request hash does not match receipt action hash"
1301                    .to_string(),
1302            ));
1303        }
1304        treaty_binding_ref.outcome_sha256 = receipt.content_hash.clone();
1305        treaty_binding_ref.remote_receipt_sha256 = chio_core::crypto::sha256_hex(
1306            &chio_core::canonical::canonical_json_bytes(receipt).map_err(|error| {
1307                KernelError::Internal(format!(
1308                    "federation treaty DSSE receipt canonicalization failed: {error}"
1309                ))
1310            })?,
1311        );
1312        Ok(Some(
1313            chio_federation::bilateral_dsse::BilateralPredicateExtensions {
1314                capability_lease_ref: Some(material.capability_lease_ref),
1315                policy_evaluation_summary: Some(material.policy_evaluation_summary),
1316                governance_receipt_ref: material.governance_receipt_ref,
1317                consistency_anchor: material.consistency_anchor,
1318                consistency_model: Some(consistency_model),
1319                cross_org_visibility: material.cross_org_visibility,
1320                treaty_binding_ref: Some(treaty_binding_ref),
1321            },
1322        ))
1323    }
1324
1325    /// Post-sign hook. Invoked immediately after
1326    /// [`Self::build_and_sign_receipt`] so the local (tool-host)
1327    /// signature has already landed in the `ChioReceipt`. When
1328    /// `federated_origin_kernel_id` is set and the admission-time peer
1329    /// snapshot is available, this dispatches the receipt to the
1330    /// cosigner, assembles a [`chio_federation::bilateral::DualSignedReceipt`],
1331    /// and stashes it for retrieval via [`Self::dual_signed_receipt`].
1332    ///
1333    /// Fail-closed: any error from peer resolution or the cosigner is
1334    /// surfaced as a [`KernelError::Internal`] so operators see the
1335    /// federation drift rather than silently shipping a receipt without
1336    /// the remote signature. Production evaluate paths pass the
1337    /// admission-time snapshot; direct record callers still get a
1338    /// fresh-peer fallback. Non-federated requests (`None` origin) are a
1339    /// no-op.
1340    pub(crate) fn apply_federation_cosign(
1341        &self,
1342        request: &crate::runtime::ToolCallRequest,
1343        receipt: &chio_core::receipt::body::ChioReceipt,
1344        admitted_peer: Option<&chio_federation::trust_establishment::FederationPeer>,
1345    ) -> Result<(), KernelError> {
1346        let Some(origin_kernel_id) = request.federated_origin_kernel_id.as_ref() else {
1347            return Ok(());
1348        };
1349        let Some(cosigner) = self.federation_cosigner.as_ref() else {
1350            return Err(KernelError::Internal(format!(
1351                "federation cosigner missing for request {request_id} bound to origin kernel {origin_kernel_id}",
1352                request_id = request.request_id,
1353            )));
1354        };
1355        let peer = match admitted_peer {
1356            Some(peer) if peer.kernel_id == *origin_kernel_id => peer.clone(),
1357            _ => {
1358                let now = current_unix_timestamp();
1359                self.federation_peer(origin_kernel_id, now).ok_or_else(|| {
1360                    KernelError::Internal(format!(
1361                        "federation peer {origin_kernel_id} is not pinned fresh and no admission-time peer snapshot is in scope"
1362                    ))
1363                })?
1364            }
1365        };
1366
1367        let local_kernel_id = self.federation_local_kernel_id();
1368        let extensions = match self.treaty_dsse_extensions_from_receipt_metadata(receipt)? {
1369            Some(extensions) => extensions,
1370            None => {
1371                // A federated request rejected before runtime treaty admission
1372                // ran (capability verification, time bounds, revocation, subject
1373                // binding) never produced the bilateral treaty material the
1374                // dual-sign path binds, and dispatched no cross-org outcome to
1375                // co-sign. Record such a deny single-signed, with no dual-signed
1376                // or DSSE artifact, matching the pre-dispatch denial contract in
1377                // federated_request_without_receipt_store_denies_before_dispatch_or_cosign.
1378                // An allowed federated outcome always carries treaty material, so
1379                // any non-deny receipt still fails closed here.
1380                if matches!(
1381                    receipt.decision,
1382                    Some(chio_core::receipt::decision::Decision::Deny { .. })
1383                ) {
1384                    return Ok(());
1385                }
1386                return Err(KernelError::Internal(
1387                    "federation runtime treaty material missing; refusing treaty-bound DSSE"
1388                        .to_string(),
1389                ));
1390            }
1391        };
1392        let dual = chio_federation::bilateral::co_sign_with_origin(
1393            origin_kernel_id,
1394            &peer.public_key,
1395            &local_kernel_id,
1396            &self.config.keypair,
1397            receipt.clone(),
1398            cosigner.as_ref(),
1399        )
1400        .map_err(|e| KernelError::Internal(format!("bilateral co-sign failed: {e}")))?;
1401        let timestamp_unix_ms = current_unix_timestamp().saturating_mul(1000);
1402        let dsse_envelope =
1403            chio_federation::bilateral_dsse::sign_chio_bilateral_dsse_envelope_with_cosigner(
1404                receipt,
1405                &peer.public_key,
1406                &self.config.keypair,
1407                origin_kernel_id,
1408                &local_kernel_id,
1409                &request.tool_name,
1410                timestamp_unix_ms,
1411                extensions,
1412                cosigner.as_ref(),
1413            )
1414            .map_err(|e| KernelError::Internal(format!("bilateral DSSE co-sign failed: {e}")))?;
1415
1416        // Write through to the artifact store (if one is installed via
1417        // set_federation_artifact_store) BEFORE the bounded caches. An entry
1418        // evicted from a cache is only guaranteed resolvable when the store
1419        // DURABLY retains it: a bounded in-memory store can evict the same id the
1420        // caches did, so only a store that reports itself durable suppresses the
1421        // evidence-loss signal below.
1422        let durable_store_installed = self
1423            .federation_artifact_store
1424            .as_ref()
1425            .is_some_and(|store| store.is_durable());
1426        if let Some(store) = self.federation_artifact_store.as_ref() {
1427            store.put_dual_signed(&receipt.id, &dual)?;
1428            store.put_dsse(&receipt.id, &dsse_envelope)?;
1429        }
1430        let now = current_unix_timestamp();
1431        {
1432            let mut cache = match self.federation_dual_receipts.lock() {
1433                Ok(g) => g,
1434                Err(poisoned) => poisoned.into_inner(),
1435            };
1436            // An evicted entry survives only through a durable store. Without one
1437            // the drop is lossy by policy (bounded drop-oldest); log it so the
1438            // evidence loss is explicit for operators.
1439            if cache.insert(receipt.id.clone(), dual, now).is_some() && !durable_store_installed {
1440                debug!(
1441                    request_id = %request.request_id,
1442                    "dropping an evicted dual-signed co-sign artifact: bounded federation cache is full and no durable FederationArtifactStore is installed"
1443                );
1444            }
1445        }
1446        {
1447            let mut cache = match self.federation_dsse_envelopes.lock() {
1448                Ok(g) => g,
1449                Err(poisoned) => poisoned.into_inner(),
1450            };
1451            if cache
1452                .insert(receipt.id.clone(), dsse_envelope, now)
1453                .is_some()
1454                && !durable_store_installed
1455            {
1456                debug!(
1457                    request_id = %request.request_id,
1458                    "dropping an evicted co-sign DSSE envelope: bounded federation cache is full and no durable FederationArtifactStore is installed"
1459                );
1460            }
1461        }
1462        Ok(())
1463    }
1464
1465    /// Engage the emergency kill switch.
1466    ///
1467    /// After this call, every `evaluate_tool_call*` path returns a signed
1468    /// deny receipt with reason `"kernel emergency stop active"` before
1469    /// touching capability validation or the guard pipeline. The kernel
1470    /// remains running so orchestrators and health probes see a live
1471    /// process; it is inert.
1472    ///
1473    /// The active capability set is NOT purged from the revocation store:
1474    /// the current `RevocationStore` trait has no bulk revoke API and
1475    /// capability expiration plus the kill-switch flag together
1476    /// cover this surface. When a future revision adds `revoke_all`,
1477    /// this method should call it; until then, capability revocation is
1478    /// delegated to natural expiration.
1479    pub fn emergency_stop(&self, reason: &str) -> Result<(), KernelError> {
1480        let now_unix_ms = current_unix_timestamp_ms();
1481        let now = now_unix_ms / 1000;
1482        // Record the timestamp first so any concurrent reader that observes
1483        // `emergency_stopped == true` sees a non-zero `since` value.
1484        self.emergency_stopped_since.store(now, Ordering::SeqCst);
1485        self.emergency_stop_reason
1486            .store(Arc::new(Some(reason.to_string())));
1487        self.emergency_stopped.store(true, Ordering::SeqCst);
1488
1489        warn!(
1490            reason = %redacted!(reason),
1491            timestamp = now,
1492            "emergency stop engaged -- all evaluations will be denied"
1493        );
1494        Ok(())
1495    }
1496
1497    /// Disengage the emergency kill switch and resume normal operation.
1498    ///
1499    /// Subsequent `evaluate_tool_call*` calls follow the full validation
1500    /// pipeline again. Capabilities that naturally expired while the
1501    /// kernel was stopped remain expired; the kill switch does not
1502    /// retroactively grant anything.
1503    pub fn emergency_resume(&self) -> Result<(), KernelError> {
1504        self.emergency_stopped.store(false, Ordering::SeqCst);
1505        self.emergency_stopped_since.store(0, Ordering::SeqCst);
1506
1507        warn!("emergency stop disengaged -- evaluations will resume");
1508
1509        self.emergency_stop_reason
1510            .store(Arc::new(Option::<String>::None));
1511        Ok(())
1512    }
1513
1514    /// Return `true` when the emergency kill switch is engaged.
1515    #[must_use]
1516    pub fn is_emergency_stopped(&self) -> bool {
1517        self.emergency_stopped.load(Ordering::SeqCst)
1518    }
1519
1520    /// Return `true` when the RSS soft-ceiling sampler has flagged that process
1521    /// RSS crossed `memory_budget.rss_soft_limit_bytes`. A single relaxed atomic
1522    /// load on the admission fast path.
1523    #[must_use]
1524    pub fn is_rss_shedding(&self) -> bool {
1525        self.rss_shed.load(Ordering::Relaxed)
1526    }
1527
1528    /// Enumerate each long-lived bounded structure's telemetry label and its
1529    /// current live entry count. This is the registry the size-metric convention
1530    /// and the soak harness read; adding a
1531    /// new long-lived collection without a gauge here fails the registry test.
1532    #[must_use]
1533    pub fn bounded_structure_gauges(&self) -> Vec<(&'static str, usize)> {
1534        vec![
1535            ("receipt_mirror", self.receipt_mirror_gauge.get()),
1536            (
1537                "child_receipt_mirror",
1538                self.child_receipt_mirror_gauge.get(),
1539            ),
1540            (
1541                "federation_dual_receipts",
1542                self.federation_dual_receipts_gauge.get(),
1543            ),
1544            (
1545                "federation_dsse_envelopes",
1546                self.federation_dsse_envelopes_gauge.get(),
1547            ),
1548        ]
1549    }
1550
1551    #[cfg(test)]
1552    pub(crate) fn set_rss_shed_for_test(&self, on: bool) {
1553        self.rss_shed.store(on, Ordering::Relaxed);
1554    }
1555
1556    /// Return the unix timestamp (seconds) at which the kill switch was
1557    /// engaged, or `None` when the kernel is currently running normally.
1558    #[must_use]
1559    pub fn emergency_stopped_since(&self) -> Option<u64> {
1560        if !self.is_emergency_stopped() {
1561            return None;
1562        }
1563        let since = self.emergency_stopped_since.load(Ordering::SeqCst);
1564        if since == 0 {
1565            None
1566        } else {
1567            Some(since)
1568        }
1569    }
1570
1571    /// Return the operator-supplied reason for the current emergency stop,
1572    /// or `None` when the kernel is running normally.
1573    #[must_use]
1574    pub fn emergency_stop_reason(&self) -> Option<String> {
1575        if !self.is_emergency_stopped() {
1576            return None;
1577        }
1578        self.emergency_stop_reason.load_full().as_ref().clone()
1579    }
1580
1581    /// Install a DPoP nonce replay store and verification config.
1582    ///
1583    /// Once installed, any invocation whose matched grant has `dpop_required == Some(true)`
1584    /// must carry a valid `DpopProof` on the `ToolCallRequest`. Requests that lack a proof
1585    /// or whose proof fails verification are denied fail-closed.
1586    pub fn set_dpop_store(&mut self, nonce_store: dpop::DpopNonceStore, config: dpop::DpopConfig) {
1587        self.dpop_nonce_store = Some(nonce_store);
1588        self.dpop_config = Some(config);
1589    }
1590
1591    /// Install an execution-nonce config and replay store.
1592    ///
1593    /// Once installed, every `Verdict::Allow` carries a short-lived signed
1594    /// nonce on `ToolCallResponse::execution_nonce`. Tool servers re-present
1595    /// that nonce via `ToolCallRequest::execution_nonce` and the kernel's
1596    /// `verify_presented_execution_nonce` helper (or directly via the
1597    /// free-standing `verify_execution_nonce` function) before executing.
1598    ///
1599    /// Set `config.require_nonce = true` to put the kernel into strict mode:
1600    /// any call that reaches `require_presented_execution_nonce` without a
1601    /// nonce is denied. When `require_nonce == false` the feature is opt-in
1602    /// per tool server and non-nonce callers continue to work (backward
1603    /// compatibility).
1604    pub fn set_execution_nonce_store(
1605        &mut self,
1606        config: crate::execution_nonce::ExecutionNonceConfig,
1607        store: Box<dyn crate::execution_nonce::ExecutionNonceStore>,
1608    ) {
1609        self.execution_nonce_config = Some(config);
1610        self.execution_nonce_store = Some(store);
1611    }
1612
1613    /// Returns `true` when execution-nonce strict mode is active.
1614    ///
1615    /// Strict mode requires every presented tool call to carry a fresh,
1616    /// valid, single-use nonce. When `false` the kernel is either not
1617    /// minting nonces at all (no config installed) or is in opt-in mode
1618    /// where tool servers can verify presented nonces but non-nonce calls
1619    /// are not outright rejected.
1620    #[must_use]
1621    pub fn execution_nonce_required(&self) -> bool {
1622        self.execution_nonce_config
1623            .as_ref()
1624            .is_some_and(|cfg| cfg.require_nonce)
1625    }
1626
1627    /// Mint a signed execution nonce for an allow verdict.
1628    ///
1629    /// Returns `Ok(None)` when no config is installed (nonces disabled) or
1630    /// when this request already presented a nonce for execution. Otherwise
1631    /// returns `Ok(Some(nonce))` once configured. The nonce binding is
1632    /// derived from the capability subject, capability ID, target server/tool,
1633    /// and the canonical parameter hash embedded in the just-signed allow
1634    /// receipt so the verify-time check is always comparing apples to apples.
1635    pub(crate) fn mint_execution_nonce_for_allow(
1636        &self,
1637        request: &ToolCallRequest,
1638        cap: &CapabilityToken,
1639        receipt: &ChioReceipt,
1640    ) -> Result<Option<Box<crate::execution_nonce::SignedExecutionNonce>>, KernelError> {
1641        self.mint_execution_nonce_for_allow_reserving(request, cap, receipt, None)
1642    }
1643
1644    pub(crate) fn mint_execution_nonce_for_allow_reserving(
1645        &self,
1646        request: &ToolCallRequest,
1647        cap: &CapabilityToken,
1648        receipt: &ChioReceipt,
1649        reserved_hold_id: Option<&str>,
1650    ) -> Result<Option<Box<crate::execution_nonce::SignedExecutionNonce>>, KernelError> {
1651        if request.execution_nonce.is_some() {
1652            return Ok(None);
1653        }
1654        let Some(config) = self.execution_nonce_config.as_ref() else {
1655            return Ok(None);
1656        };
1657        let now = i64::try_from(current_unix_timestamp()).unwrap_or(i64::MAX);
1658        let binding = crate::execution_nonce::NonceBinding {
1659            subject_id: cap.subject.to_hex(),
1660            request_id: request.request_id.clone(),
1661            capability_id: cap.id.clone(),
1662            tool_server: request.server_id.clone(),
1663            tool_name: request.tool_name.clone(),
1664            parameter_hash: receipt.action.parameter_hash.clone(),
1665        };
1666        let reserving_request_id = reserved_hold_id.map(|_| request.request_id.clone());
1667        let signed = crate::execution_nonce::mint_execution_nonce_with_reservation(
1668            &self.config.keypair,
1669            binding,
1670            reserved_hold_id.map(str::to_string),
1671            reserving_request_id,
1672            config,
1673            now,
1674        )?;
1675        Ok(Some(Box::new(signed)))
1676    }
1677
1678    /// Verify a caller-presented execution nonce against the
1679    /// expected binding, consuming it in the replay store on success.
1680    ///
1681    /// Returns `Ok(())` when the nonce is fresh, correctly bound, signed
1682    /// by this kernel, and has not been consumed. Returns an error
1683    /// wrapping `ExecutionNonceError` on any failure (expired, tampered,
1684    /// replayed, binding mismatch, store unreachable).
1685    pub fn verify_presented_execution_nonce(
1686        &self,
1687        presented: &crate::execution_nonce::SignedExecutionNonce,
1688        expected: &crate::execution_nonce::NonceBinding,
1689    ) -> Result<(), crate::execution_nonce::ExecutionNonceError> {
1690        let store = self.execution_nonce_store.as_deref().ok_or_else(|| {
1691            crate::execution_nonce::ExecutionNonceError::Store(
1692                "execution nonce store is not installed".to_string(),
1693            )
1694        })?;
1695        let now = i64::try_from(current_unix_timestamp()).unwrap_or(i64::MAX);
1696        crate::execution_nonce::verify_execution_nonce(
1697            presented,
1698            &self.config.keypair.public_key(),
1699            expected,
1700            now,
1701            store,
1702        )
1703    }
1704
1705    /// Execution-nonce dispatch gate.
1706    ///
1707    /// Denies fail-closed when strict mode is configured and the request
1708    /// lacks a nonce. When strict mode is disabled, a request with no
1709    /// nonce remains backward-compatible. Any presented nonce is still
1710    /// verified and consumed so opt-in callers cannot bypass binding,
1711    /// expiry, signature, or replay checks.
1712    ///
1713    /// Returns `Ok(())` when:
1714    /// * no nonce is required and none was presented, OR
1715    /// * a nonce is presented, signed by this kernel, correctly bound,
1716    ///   non-expired, and has not been consumed.
1717    ///
1718    /// Returns `Err(KernelError::Internal(...))` fail-closed otherwise.
1719    pub fn require_presented_execution_nonce(
1720        &self,
1721        request: &ToolCallRequest,
1722        cap: &CapabilityToken,
1723    ) -> Result<(), KernelError> {
1724        self.validate_required_execution_nonce(request, cap)?;
1725        self.reserve_presented_execution_nonce(request)
1726    }
1727
1728    pub(crate) fn validate_required_execution_nonce(
1729        &self,
1730        request: &ToolCallRequest,
1731        cap: &CapabilityToken,
1732    ) -> Result<(), KernelError> {
1733        let presented = request.execution_nonce.as_ref();
1734        if !self.execution_nonce_required() && presented.is_none() {
1735            return Ok(());
1736        }
1737        let presented = presented.ok_or_else(|| {
1738            KernelError::Internal(
1739                "execution nonce required but not presented on tool call".to_string(),
1740            )
1741        })?;
1742        if self.execution_nonce_store.is_none() {
1743            return Err(KernelError::Internal(
1744                "execution nonce store is not installed".to_string(),
1745            ));
1746        }
1747        let parameter_hash = chio_core::receipt::decision::ToolCallAction::from_parameters(
1748            request.arguments.clone(),
1749        )
1750        .map_err(|e| KernelError::ReceiptSigningFailed(format!("failed to hash parameters: {e}")))?
1751        .parameter_hash;
1752        let expected = crate::execution_nonce::NonceBinding {
1753            subject_id: cap.subject.to_hex(),
1754            request_id: request.request_id.clone(),
1755            capability_id: cap.id.clone(),
1756            tool_server: request.server_id.clone(),
1757            tool_name: request.tool_name.clone(),
1758            parameter_hash,
1759        };
1760        let now = i64::try_from(current_unix_timestamp()).unwrap_or(i64::MAX);
1761        crate::execution_nonce::validate_execution_nonce(
1762            presented,
1763            &self.config.keypair.public_key(),
1764            &expected,
1765            now,
1766        )
1767        .map_err(|e| KernelError::Internal(format!("{e}")))
1768    }
1769
1770    pub(crate) fn reserve_presented_execution_nonce(
1771        &self,
1772        request: &ToolCallRequest,
1773    ) -> Result<(), KernelError> {
1774        let Some(presented) = request.execution_nonce.as_ref() else {
1775            return Ok(());
1776        };
1777        let store = self.execution_nonce_store.as_deref().ok_or_else(|| {
1778            KernelError::Internal("execution nonce store is not installed".to_string())
1779        })?;
1780        let now = i64::try_from(current_unix_timestamp()).unwrap_or(i64::MAX);
1781        crate::execution_nonce::reserve_execution_nonce(presented, store, now)
1782            .map_err(|e| KernelError::Internal(format!("{e}")))
1783    }
1784
1785    /// Strict-mode nonce issuance gate.
1786    ///
1787    /// In strict mode, a request that reaches evaluation without a presented
1788    /// nonce is an authorization preflight. It may receive a freshly signed
1789    /// nonce, but it must not execute the target tool. Actual execution
1790    /// presents that nonce on a later request and consumes it immediately
1791    /// before dispatch.
1792    #[must_use]
1793    pub(crate) fn execution_nonce_preflight_required(&self, request: &ToolCallRequest) -> bool {
1794        self.execution_nonce_required() && request.execution_nonce.is_none()
1795    }
1796
1797    pub fn requires_web3_evidence(&self) -> bool {
1798        self.config.require_web3_evidence
1799    }
1800
1801    pub fn validate_web3_evidence_prerequisites(&self) -> Result<(), KernelError> {
1802        if !self.requires_web3_evidence() {
1803            return Ok(());
1804        }
1805
1806        let Some(supports_kernel_signed_checkpoints) =
1807            self.with_receipt_store(|store| Ok(store.supports_kernel_signed_checkpoints()))?
1808        else {
1809            return Err(KernelError::Web3EvidenceUnavailable(
1810                "web3-enabled deployments require a durable receipt store".to_string(),
1811            ));
1812        };
1813
1814        if self.checkpoint_batch_size == 0 {
1815            return Err(KernelError::Web3EvidenceUnavailable(
1816                "web3-enabled deployments require checkpoint_batch_size > 0".to_string(),
1817            ));
1818        }
1819
1820        if !supports_kernel_signed_checkpoints {
1821            return Err(KernelError::Web3EvidenceUnavailable(
1822                "web3-enabled deployments require local receipt persistence with kernel-signed checkpoint support; append-only remote receipt mirrors are unsupported".to_string(),
1823            ));
1824        }
1825
1826        Ok(())
1827    }
1828
1829    /// Register a policy guard. Guards are evaluated in registration order.
1830    /// If any guard denies, the request is denied.
1831    pub fn add_guard(&mut self, guard: Box<dyn Guard>) {
1832        // `Box<dyn Guard>` converts directly to `Arc<dyn Guard>`, and
1833        // `Vec<Arc<dyn Guard>>` is `Clone`, so `make_mut` copies-on-write only
1834        // while guards are still being registered (the kernel is single-owner
1835        // at that point).
1836        std::sync::Arc::make_mut(&mut self.guards).push(std::sync::Arc::from(guard));
1837    }
1838
1839    /// Install a product-specific runtime admission hook. The hook runs after
1840    /// core authorization checks and guards, but before tool dispatch.
1841    pub fn set_runtime_admission_hook(&mut self, hook: Arc<dyn RuntimeAdmissionHook>) {
1842        self.runtime_admission_hook = Some(hook);
1843    }
1844
1845    pub fn set_threshold_approval_requirement_resolver(
1846        &mut self,
1847        resolver: Arc<dyn crate::threshold_approval::ThresholdApprovalRequirementResolver>,
1848    ) {
1849        self.threshold_approval_requirement_resolver = Some(resolver);
1850    }
1851
1852    /// Install the sole trusted parser and verifier for opaque supplemental
1853    /// authorization artifacts.
1854    pub fn set_supplemental_quota_verifier(
1855        &mut self,
1856        verifier: Arc<dyn crate::supplemental_quota::SupplementalQuotaVerifier>,
1857        binding: crate::supplemental_quota::SupplementalQuotaVerifierBinding,
1858    ) -> Result<(), crate::supplemental_quota::SupplementalQuotaError> {
1859        self.supplemental_quota_verifier = Some(
1860            crate::supplemental_quota::SupplementalQuotaVerifierRuntime::new(verifier, binding)?,
1861        );
1862        Ok(())
1863    }
1864
1865    /// Remove the product-specific runtime admission hook.
1866    pub fn clear_runtime_admission_hook(&mut self) {
1867        self.runtime_admission_hook = None;
1868    }
1869
1870    /// Register a tool server connection.
1871    pub fn register_tool_server(&mut self, connection: Box<dyn ToolServerConnection>) {
1872        let id = connection.server_id().to_owned();
1873        info!(server_id = %id, "registering tool server");
1874        self.tool_servers.insert(id, Arc::from(connection));
1875    }
1876
1877    /// Register a resource provider.
1878    pub fn register_resource_provider(&mut self, provider: Box<dyn ResourceProvider>) {
1879        info!("registering resource provider");
1880        self.resource_providers.push(provider);
1881    }
1882
1883    /// Register a prompt provider.
1884    pub fn register_prompt_provider(&mut self, provider: Box<dyn PromptProvider>) {
1885        info!("registering prompt provider");
1886        self.prompt_providers.push(provider);
1887    }
1888}