Skip to main content

clawft_kernel/
boot.rs

1//! Kernel boot sequence and state machine.
2//!
3//! The [`Kernel`] struct is the central coordinator. It wraps
4//! [`AppContext`] and manages the process table, service registry,
5//! IPC, and health subsystem through a boot/shutdown lifecycle.
6
7use std::sync::Arc;
8use std::time::Instant;
9
10use serde::{Deserialize, Serialize};
11use tracing::{error, info, warn};
12
13use clawft_core::bootstrap::AppContext;
14use clawft_core::bus::MessageBus;
15use clawft_platform::Platform;
16use clawft_types::config::Config;
17
18#[cfg(feature = "native")]
19use crate::a2a::A2ARouter;
20use crate::app::AppManager;
21use crate::capability::AgentCapabilities;
22#[cfg(feature = "native")]
23use crate::capability::CapabilityChecker;
24use crate::cluster::{ClusterConfig, ClusterMembership};
25use crate::console::{BootEvent, BootLog, BootPhase, KernelEventLog};
26use crate::error::{KernelError, KernelResult};
27use crate::health::HealthSystem;
28use crate::ipc::KernelIpc;
29use crate::process::{ProcessEntry, ProcessState, ProcessTable, ResourceUsage};
30use crate::service::ServiceRegistry;
31use crate::supervisor::AgentSupervisor;
32#[cfg(feature = "native")]
33use crate::topic::TopicRouter;
34use clawft_types::config::KernelConfig;
35
36/// Boot-time warning emitted when `vector.backend` needs the `diskann`
37/// cargo feature but the kernel was built without it (WEFT-656). See
38/// `vector_diskann` module docs for the full mismatch story.
39#[cfg(feature = "ecc")]
40const DISKANN_FEATURE_MISMATCH_WARNING: &str = "vector.backend requires the \
41    `diskann` cargo feature (DiskAnn or Hybrid) but this kernel binary was \
42    built WITHOUT it — DiskAnnBackend is silently degrading to a \
43    brute-force O(n) linear-scan stub instead of real Vamana-graph ANN \
44    search. Rebuild with `--features diskann` for the real backend, or set \
45    `vector.strict = true` in kernel config to turn this into a boot error \
46    instead of a silent degrade.";
47
48/// Boot-time error emitted for the same mismatch when `vector.strict =
49/// true`: refuse to boot into a silently degraded vector backend.
50#[cfg(feature = "ecc")]
51const DISKANN_FEATURE_MISMATCH_ERROR: &str = "vector.backend requires the \
52    `diskann` cargo feature (DiskAnn or Hybrid) but this kernel binary was \
53    built WITHOUT it, and `vector.strict = true` refuses to silently fall \
54    back to the brute-force O(n) stub. Rebuild with `--features diskann`, \
55    or set `vector.strict = false` to accept the degraded stub.";
56
57/// Kernel lifecycle state.
58#[non_exhaustive]
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum KernelState {
61    /// Kernel is in the process of booting.
62    Booting,
63    /// Kernel is running and accepting commands.
64    Running,
65    /// Kernel is shutting down.
66    ShuttingDown,
67    /// Kernel has been halted.
68    Halted,
69}
70
71impl std::fmt::Display for KernelState {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            KernelState::Booting => write!(f, "booting"),
75            KernelState::Running => write!(f, "running"),
76            KernelState::ShuttingDown => write!(f, "shutting_down"),
77            KernelState::Halted => write!(f, "halted"),
78        }
79    }
80}
81
82// ── Feature-gated subsystem bundles ──────────────────────────────
83//
84// These group related feature-gated fields so the Kernel struct
85// has fewer conditional fields (3 instead of 18) and init code
86// is easier to read.
87
88/// Exochain subsystem: chain manager, resource tree, governance gate, and
89/// optional external chain-head anchor (WEFT-137 / ADR-041).
90#[cfg(feature = "exochain")]
91pub struct ChainSubsystem {
92    pub(crate) chain_manager: Option<Arc<crate::chain::ChainManager>>,
93    pub(crate) tree_manager: Option<Arc<crate::tree_manager::TreeManager>>,
94    pub(crate) governance_gate: Option<Arc<dyn crate::gate::GateBackend>>,
95    /// External chain-head anchoring controller (file ledger / external stub).
96    pub(crate) chain_anchor: Option<Arc<crate::chain_anchor::AnchoringController>>,
97    /// Project governance (profile `project` only): overlay reload and
98    /// parent-policy updates (ADR-103 D8).
99    pub(crate) overlay: Option<Arc<crate::overlay_runtime::OverlayRuntime>>,
100    /// Exclusive lock on the chain in use; released when the kernel drops.
101    pub(crate) _chain_lock: Option<crate::chain_storage::ChainLock>,
102}
103
104/// ECC cognitive substrate: HNSW, causal graph, cognitive tick,
105/// cross-references, impulse queue, and boot-time calibration.
106#[cfg(feature = "ecc")]
107pub struct EccSubsystem {
108    pub(crate) hnsw: Option<Arc<crate::hnsw_service::HnswService>>,
109    pub(crate) causal: Option<Arc<crate::causal::CausalGraph>>,
110    pub(crate) tick: Option<Arc<crate::cognitive_tick::CognitiveTick>>,
111    pub(crate) crossrefs: Option<Arc<crate::crossref::CrossRefStore>>,
112    pub(crate) impulses: Option<Arc<crate::impulse::ImpulseQueue>>,
113    pub(crate) calibration: Option<crate::calibration::EccCalibration>,
114    pub(crate) vector_backend: Option<Arc<dyn crate::vector_backend::VectorBackend>>,
115    pub(crate) eml_coherence:
116        Option<Arc<std::sync::Mutex<crate::eml_coherence::EmlCoherenceModel>>>,
117    /// Spatial BVH service (ADR-056 / WEFT-718). Present when
118    /// `[kernel.spatial].enabled = true`.
119    pub(crate) spatial: Option<Arc<crate::spatial_service::SpatialService>>,
120}
121
122/// OS-patterns observability: metrics, structured logging, timers,
123/// and the dead-letter queue.
124#[cfg(feature = "os-patterns")]
125pub struct ObservabilitySubsystem {
126    pub(crate) metrics_registry: Option<Arc<crate::metrics::MetricsRegistry>>,
127    pub(crate) log_service: Option<Arc<crate::log_service::LogService>>,
128    pub(crate) timer_service: Option<Arc<crate::timer::TimerService>>,
129    pub(crate) dead_letter_queue: Option<Arc<crate::dead_letter::DeadLetterQueue>>,
130}
131
132/// Where a booting kernel gets its node identity from.
133enum NodeBinding {
134    /// A persisted key seed (`None`: ephemeral, tests and one-shot boots).
135    Key(Option<[u8; 32]>),
136    /// The machine mesh service's node id (P3-U); no key is held here.
137    Service(String),
138}
139
140/// The WeftOS kernel.
141///
142/// Wraps `AppContext<P>` in a managed boot sequence with process
143/// tracking, service lifecycle, IPC, and health monitoring.
144///
145/// # Lifecycle
146///
147/// 1. Call [`Kernel::boot`] to initialize all subsystems.
148/// 2. The kernel transitions from `Booting` -> `Running`.
149/// 3. Call [`Kernel::shutdown`] to gracefully stop everything.
150/// 4. The kernel transitions from `Running` -> `ShuttingDown` -> `Halted`.
151pub struct Kernel<P: Platform> {
152    state: KernelState,
153    config: KernelConfig,
154    app_context: Option<AppContext<P>>,
155    bus: Arc<MessageBus>,
156    process_table: Arc<ProcessTable>,
157    service_registry: Arc<ServiceRegistry>,
158    ipc: Arc<KernelIpc>,
159    #[cfg(feature = "native")]
160    a2a_router: Arc<A2ARouter>,
161    #[cfg(feature = "native")]
162    cron_service: Arc<crate::cron::CronService>,
163    #[cfg(feature = "native")]
164    assessment_service: Arc<crate::assessment::AssessmentService>,
165    health: HealthSystem,
166    supervisor: AgentSupervisor<P>,
167    boot_log: BootLog,
168    event_log: Arc<KernelEventLog>,
169    boot_time: Instant,
170    cluster_membership: Arc<ClusterMembership>,
171    /// Installed application catalog; rehydrated from disk at boot.
172    app_manager: Arc<AppManager>,
173    revocation_list: Arc<crate::revocation::RevocationList>,
174    #[cfg(feature = "native")]
175    agent_registry: crate::agent_registry::AgentRegistry,
176    #[cfg(feature = "native")]
177    node_registry: crate::node_registry::NodeRegistry,
178    #[cfg(feature = "native")]
179    substrate_service: crate::substrate_service::SubstrateService,
180    #[cfg(feature = "exochain")]
181    chain: ChainSubsystem,
182    #[cfg(feature = "ecc")]
183    ecc: EccSubsystem,
184    #[cfg(feature = "os-patterns")]
185    observability: ObservabilitySubsystem,
186}
187
188impl<P: Platform> Kernel<P> {
189    /// Boot the kernel from configuration and platform.
190    ///
191    /// This is the primary entry point. It:
192    /// 1. Creates subsystems (process table, service registry, IPC, health)
193    /// 2. Creates AppContext (reuses existing bootstrap)
194    /// 3. Registers the kernel process (PID 0)
195    /// 4. Starts all registered services
196    /// 5. Transitions to Running state
197    ///
198    /// # Errors
199    ///
200    /// Returns [`KernelError::Boot`] if any critical subsystem fails
201    /// to initialize.
202    pub async fn boot(
203        config: Config,
204        kernel_config: KernelConfig,
205        platform: Arc<P>,
206    ) -> KernelResult<Self> {
207        Self::boot_with_node_key(config, kernel_config, platform, None).await
208    }
209
210    /// Boot the kernel with a persisted node identity.
211    ///
212    /// `node_key_seed` is the 32-byte Ed25519 seed of this node's key
213    /// (the daemon's `<runtime>/node.key`). The mesh, cluster and status
214    /// node id is derived from it with
215    /// [`crate::node_id::node_id_from_pubkey`], so the id is stable across
216    /// restarts. With `None` (tests, one-shot CLI boots) an ephemeral
217    /// key is generated and the id lasts for this process only.
218    ///
219    /// # Errors
220    ///
221    /// Same as [`Kernel::boot`]; additionally fails when mesh is enabled
222    /// and its listener cannot bind (ADR-103 D2).
223    pub async fn boot_with_node_key(
224        config: Config,
225        kernel_config: KernelConfig,
226        platform: Arc<P>,
227        node_key_seed: Option<[u8; 32]>,
228    ) -> KernelResult<Self> {
229        Self::boot_bound(config, kernel_config, platform, NodeBinding::Key(node_key_seed)).await
230    }
231
232    /// Boot as a client of the machine mesh service (ADR-103 P3-U).
233    ///
234    /// The node id is the service's (`hello_ack.node_id`); this kernel holds
235    /// no node key, binds no mesh listener and starts no mesh runtime. There
236    /// is no ephemeral-identity fallback: a malformed id fails the boot.
237    ///
238    /// # Errors
239    ///
240    /// [`KernelError::Boot`] when `service_node_id` is not a node id, plus
241    /// everything [`Kernel::boot`] can return.
242    pub async fn boot_in_service_mode(
243        config: Config,
244        kernel_config: KernelConfig,
245        platform: Arc<P>,
246        service_node_id: String,
247    ) -> KernelResult<Self> {
248        if !crate::node_id::is_node_id(&service_node_id) {
249            return Err(KernelError::Boot(format!(
250                "service mode needs the mesh service's node id, got {service_node_id:?}"
251            )));
252        }
253        Self::boot_bound(
254            config,
255            kernel_config,
256            platform,
257            NodeBinding::Service(service_node_id),
258        )
259        .await
260    }
261
262    async fn boot_bound(
263        config: Config,
264        mut kernel_config: KernelConfig,
265        platform: Arc<P>,
266        binding: NodeBinding,
267    ) -> KernelResult<Self> {
268        let (node_key_seed, service_node_id) = match binding {
269            NodeBinding::Key(seed) => (seed, None),
270            NodeBinding::Service(id) => (None, Some(id)),
271        };
272        // `required` means a machine mesh service must carry the mesh. A boot
273        // path that did not link one (one-shot CLI boot, foreground boot,
274        // tests) cannot honour it and must not quietly run its own listener.
275        #[cfg(feature = "mesh")]
276        if service_node_id.is_none()
277            && let Some(m) = kernel_config.mesh.as_ref()
278            && m.enabled
279            && m.service == clawft_types::config::MeshServicePolicy::Required
280        {
281            return Err(KernelError::Boot(
282                "kernel.mesh.service = \"required\" but this boot did not link a machine mesh \
283                 service (start the user daemon with the service running, or set \
284                 kernel.mesh.service = \"auto\")"
285                    .into(),
286            ));
287        }
288        let boot_time = Instant::now();
289        let mut boot_log = BootLog::new();
290        // Resolve the chain location once (explicit config, else
291        // runtime root: $WEFTOS_RUNTIME_DIR, project, or ~/.clawft) so restore, verify and
292        // shutdown persistence all use the same, possibly isolated, files.
293        let pin = crate::chain_storage::pin_chain_storage_noted(&mut kernel_config);
294        // An older, lock-unaware kernel may still be writing the legacy
295        // chain: refuse before taking any lock or touching the chain.
296        if let Some(msg) = pin.refusal {
297            // A refusal that clears by itself is a plain boot error (exit 1,
298            // a service manager retries); the rest are permanent (exit 78).
299            return Err(if pin.refusal_transient {
300                KernelError::Boot(msg)
301            } else {
302                KernelError::BootRefused(msg)
303            });
304        }
305        let (pinned_chain, chain_note) = (pin.path, pin.warning);
306        // One kernel per chain: hold chain.lock beside the chain in use for
307        // the kernel's lifetime (released when the kernel is dropped).
308        #[cfg(feature = "exochain")]
309        let chain_lock = match pinned_chain.as_deref() {
310            Some(ckpt) => Some(
311                crate::chain_storage::ChainLock::try_acquire(ckpt).map_err(|e| match e {
312                    crate::chain_storage::ChainLockError::InUse(m) => KernelError::BootRefused(m),
313                    crate::chain_storage::ChainLockError::Other(m) => KernelError::Boot(m),
314                })?,
315            ),
316            None => None,
317        };
318
319        info!("WeftOS kernel booting");
320        boot_log.push(BootEvent::info(
321            BootPhase::Init,
322            format!("WeftOS v{} booting...", env!("CARGO_PKG_VERSION")),
323        ));
324        if let Some(ref p) = pinned_chain {
325            boot_log.push(BootEvent::info(
326                BootPhase::Init,
327                format!("Chain storage: {}", p.display()),
328            ));
329        }
330        // One resolver for every runtime file (cluster peers, apps, revoked
331        // hosts); the chain, socket and node key use the same root.
332        let runtime_paths = crate::chain_storage::boot_runtime_paths(pinned_chain.as_deref());
333        boot_log.push(BootEvent::info(
334            BootPhase::Init,
335            format!("Runtime dir: {}", runtime_paths.root().display()),
336        ));
337        // ADR-103 D8: a `project` kernel's rules are the user's signed parent
338        // policy plus the project overlay; any failure refuses the boot.
339        let project_profile = kernel_config.profile.is_some();
340        #[cfg(feature = "exochain")]
341        let mut overlay_prepared = if project_profile {
342            let p = crate::overlay_runtime::prepare(&runtime_paths)
343                .map_err(|e| KernelError::BootRefused(e.boot_message()))?;
344            p.apply_limits(&mut kernel_config);
345            Some(p)
346        } else {
347            let p = crate::overlay_runtime::prepare_nested()
348                .map_err(|e| KernelError::BootRefused(e.boot_message()))?;
349            if let Some(p) = &p { p.apply_limits(&mut kernel_config); }
350            p
351        };
352        #[cfg(not(feature = "exochain"))]
353        if project_profile {
354            return Err(KernelError::BootRefused(
355                "profile `project` needs the exochain feature (governance overlay)".into(),
356            ));
357        }
358        #[cfg(feature = "exochain")]
359        let mut overlay_runtime: Option<Arc<crate::overlay_runtime::OverlayRuntime>> = None;
360        if let Some(w) = chain_note {
361            warn!("{w}");
362            boot_log.push(BootEvent::warn(BootPhase::Init, w));
363        }
364        boot_log.push(BootEvent::info(BootPhase::Init, "PID 0 (kernel)"));
365        // WEFT-70: surface macOS / non-Linux OS-sandbox downgrade in boot
366        // log (banner also prints this via console::boot_banner).
367        if let Some(msg) = clawft_plugin::sandbox::sandbox_platform_banner_warning() {
368            warn!(%msg, "sandbox platform restriction");
369            boot_log.push(BootEvent::warn(BootPhase::Init, msg));
370        }
371
372        // 1. Create subsystems
373        let process_table = Arc::new(ProcessTable::new(kernel_config.max_processes));
374        let service_registry = Arc::new(ServiceRegistry::new());
375
376        boot_log.push(BootEvent::info(
377            BootPhase::Config,
378            format!("Max processes: {}", kernel_config.max_processes),
379        ));
380        boot_log.push(BootEvent::info(
381            BootPhase::Config,
382            format!(
383                "Health check interval: {}s",
384                kernel_config.health_check_interval_secs
385            ),
386        ));
387
388        // 2. Create AppContext
389        let app_context = AppContext::new(config, platform)
390            .await
391            .map_err(|e| KernelError::Boot(format!("AppContext init failed: {e}")))?;
392
393        // 3. Create IPC from the AppContext's MessageBus
394        let bus = app_context.bus().clone();
395        let ipc = Arc::new(KernelIpc::new(bus.clone()));
396
397        // 4. Create health system
398        let health = HealthSystem::new(kernel_config.health_check_interval_secs);
399
400        // 5. Register kernel process (PID 0)
401        let kernel_entry = ProcessEntry {
402            pid: 0,
403            agent_id: "kernel".to_owned(),
404            state: ProcessState::Running,
405            capabilities: AgentCapabilities::default(),
406            resource_usage: ResourceUsage::default(),
407            cancel_token: crate::process::CancellationToken::new(),
408            parent_pid: None,
409        };
410        process_table
411            .insert_with_pid(kernel_entry)
412            .map_err(|e| KernelError::Boot(format!("failed to register kernel process: {e}")))?;
413
414        boot_log.push(BootEvent::info(
415            BootPhase::Services,
416            "Service registry ready",
417        ));
418
419        // 5a. Construct A2ARouter (per-PID inboxes, capability-checked routing)
420        #[cfg(feature = "native")]
421        let capability_checker = Arc::new(CapabilityChecker::new(process_table.clone()));
422        #[cfg(feature = "native")]
423        let topic_router = Arc::new(TopicRouter::new(process_table.clone()));
424        #[cfg(feature = "native")]
425        let a2a_router = Arc::new(A2ARouter::new(
426            process_table.clone(),
427            capability_checker,
428            topic_router,
429        ));
430
431        #[cfg(feature = "native")]
432        boot_log.push(BootEvent::info(BootPhase::Services, "A2A router ready"));
433
434        // 5b. Register cron service (K0 gate requirement)
435        #[cfg(feature = "native")]
436        let cron_svc = Arc::new(crate::cron::CronService::new());
437        #[cfg(feature = "native")]
438        if let Err(e) = service_registry.register(cron_svc.clone()) {
439            error!(error = %e, "failed to register cron service");
440        } else {
441            boot_log.push(BootEvent::info(
442                BootPhase::Services,
443                "Cron service registered",
444            ));
445        }
446
447        // Node identity (ADR-025 / ADR-103 D11): the node id is derived from
448        // the Ed25519 node key, never allocated per boot. Mesh handshake,
449        // cluster membership, heartbeats and status all use this one id.
450        let node_id: String = if let Some(id) = service_node_id.clone() {
451            // Service mode: the id belongs to the machine mesh service; this
452            // kernel never derives or generates one (no silent fallback).
453            id
454        } else {
455            #[cfg(any(feature = "mesh", feature = "exochain"))]
456            {
457                let identity = match node_key_seed {
458                    Some(seed) => crate::cluster::NodeIdentity::from_signing_key(
459                        ed25519_dalek::SigningKey::from_bytes(&seed),
460                    ),
461                    None => {
462                        warn!("no persisted node key supplied; using an ephemeral node identity");
463                        crate::cluster::NodeIdentity::generate()
464                    }
465                };
466                identity.node_id().to_owned()
467            }
468            #[cfg(not(any(feature = "mesh", feature = "exochain")))]
469            {
470                // No ed25519 in this build: hash the seed (or fresh random
471                // bytes) with the same SHA-256 derivation so the id shape
472                // is identical.
473                let seed = node_key_seed.unwrap_or_else(|| {
474                    let mut b = [0u8; 32];
475                    b[..16].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
476                    b[16..].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
477                    b
478                });
479                crate::node_id::node_id_from_pubkey(&seed)
480            }
481        };
482
483        // 5b½ / 5d prep — mesh-aware assessment service (WEFT-117).
484        //
485        // When mesh is enabled we share the key-derived node_id up front and
486        // share one MeshCoordinator between AssessmentService and
487        // AssessmentTransport. Assessment is registered after that
488        // decision so `assess.mesh.status` sees a live coordinator.
489        #[cfg(all(feature = "native", feature = "mesh"))]
490        let mesh_config_early = kernel_config.mesh.clone().unwrap_or_default();
491        #[cfg(all(feature = "native", feature = "mesh"))]
492        let mesh_node_id: Option<String> = if mesh_config_early.enabled {
493            Some(node_id.clone())
494        } else {
495            None
496        };
497        #[cfg(all(feature = "native", feature = "mesh"))]
498        let mesh_project_name = kernel_config
499            .cluster
500            .as_ref()
501            .and_then(|c| c.node_name.clone())
502            .unwrap_or_else(|| "local".into());
503        #[cfg(all(feature = "native", feature = "mesh"))]
504        let assess_mesh_coord: Option<Arc<crate::assessment::mesh::MeshCoordinator>> =
505            mesh_node_id.as_ref().map(|id| {
506                Arc::new(crate::assessment::mesh::MeshCoordinator::new(
507                    id.clone(),
508                    mesh_project_name.clone(),
509                ))
510            });
511
512        // 5b½. Register assessment service
513        #[cfg(feature = "native")]
514        let assessment_svc = {
515            #[cfg(feature = "mesh")]
516            {
517                if let Some(ref coord) = assess_mesh_coord {
518                    Arc::new(crate::assessment::AssessmentService::with_mesh_coordinator(
519                        Arc::clone(coord),
520                    ))
521                } else {
522                    Arc::new(crate::assessment::AssessmentService::new())
523                }
524            }
525            #[cfg(not(feature = "mesh"))]
526            {
527                Arc::new(crate::assessment::AssessmentService::new())
528            }
529        };
530        #[cfg(feature = "native")]
531        if let Err(e) = service_registry.register(assessment_svc.clone()) {
532            error!(error = %e, "failed to register assessment service");
533        } else {
534            let mesh_note = {
535                #[cfg(feature = "mesh")]
536                {
537                    if assess_mesh_coord.is_some() {
538                        " (mesh coordination enabled)"
539                    } else {
540                        ""
541                    }
542                }
543                #[cfg(not(feature = "mesh"))]
544                {
545                    ""
546                }
547            };
548            boot_log.push(BootEvent::info(
549                BootPhase::Services,
550                format!("Assessment service registered{mesh_note}"),
551            ));
552        }
553
554        // 5c. Register container service (K4)
555        let container_manager = std::sync::Arc::new(crate::container::ContainerManager::new(
556            crate::container::ContainerConfig::default(),
557        ));
558        let container_service = std::sync::Arc::new(crate::container::ContainerService::new(
559            container_manager.clone(),
560        ));
561        if let Err(e) = service_registry.register(container_service) {
562            error!(error = %e, "failed to register container service");
563        } else {
564            boot_log.push(BootEvent::info(
565                BootPhase::Services,
566                "Container service registered",
567            ));
568        }
569
570        // Host revocation list: loaded here (not at step 6b) so the mesh
571        // listener's admission gate and the kernel share one instance.
572        let revocation_list = Arc::new(crate::revocation::RevocationList::load(
573            runtime_paths.revoked_hosts(),
574        ));
575
576        // Collapsed-mode verdict source for mesh admission; bound to the
577        // governance gate once that exists (step 9, below the mesh block).
578        #[cfg(all(feature = "native", feature = "mesh", feature = "exochain"))]
579        let mesh_verdicts = Arc::new(crate::mesh_admit::GateVerdictSource::late());
580        #[cfg(all(feature = "native", feature = "mesh", feature = "exochain"))]
581        let mesh_closed_without_gate = mesh_config_early.admission
582            == clawft_types::config::MeshAdmissionMode::Enforce
583            && !mesh_config_early.admission_open_membership;
584
585        // 5d. Initialize mesh transport (K6) if configured.
586        //     Must happen before cluster (step 7) because cluster needs mesh
587        //     to reach peer nodes.
588        #[cfg(all(feature = "native", feature = "mesh"))]
589        let mesh_runtime = {
590            let mesh_config = mesh_config_early;
591            if service_node_id.is_none()
592                && crate::mesh_mode::select(&mesh_config) == crate::mesh_mode::MeshMode::Collapsed
593            {
594                let node_id = mesh_node_id
595                    .clone()
596                    .expect("mesh enabled implies mesh_node_id");
597                let mut runtime = if mesh_config.discovery {
598                    let kad_id = blake3::hash(node_id.as_bytes());
599                    crate::mesh_runtime::MeshRuntime::with_discovery(
600                        node_id.clone(),
601                        *kad_id.as_bytes(),
602                    )
603                } else {
604                    crate::mesh_runtime::MeshRuntime::new(node_id.clone())
605                };
606
607                // Wire mesh into the A2A router for incoming message injection.
608                runtime.set_local_router(Arc::clone(&a2a_router));
609
610                let runtime = Arc::new(runtime);
611                // ADR-103 leaf admission is opt-in until a service-owned
612                // registry exists. An ephemeral node key cannot sign durable
613                // acknowledgments, so only a persisted node key may serve it.
614                if let Some(seed) = node_key_seed {
615                    let leaf_root = runtime_paths.root().join("leaf");
616                    if leaf_root.join("registry").is_dir() {
617                        let ingress = crate::mesh_leaf::LeafIngress::open(
618                            &leaf_root,
619                            ed25519_dalek::SigningKey::from_bytes(&seed),
620                        ).map_err(|e| KernelError::Boot(format!("leaf ingress: {e}")))?;
621                        runtime.set_leaf_ingress(Arc::new(ingress)).map_err(|_|
622                            KernelError::Boot("leaf ingress already installed".into()))?;
623                    }
624                }
625
626                // WEFT-117: register AssessmentTransport so the mesh
627                // accept/event loop demuxes FrameType::AssessmentSync
628                // and can drain FindingDiff / gossip pushes to peers.
629                if let Some(ref coord) = assess_mesh_coord {
630                    let assess_transport =
631                        Arc::new(crate::mesh_assess::AssessmentTransport::new(Arc::clone(
632                            coord,
633                        )));
634                    runtime.set_assessment_transport(Arc::clone(&assess_transport));
635
636                    // Periodic drain of pending assessment broadcasts
637                    // (diff-only when available) + lightweight gossip.
638                    let rt_tick = Arc::clone(&runtime);
639                    let assess_for_tick = Arc::clone(&assessment_svc);
640                    let transport_for_tick = Arc::clone(&assess_transport);
641                    tokio::spawn(async move {
642                        let mut interval =
643                            tokio::time::interval(std::time::Duration::from_secs(15));
644                        loop {
645                            interval.tick().await;
646                            // Keep transport's published report in sync so
647                            // RequestReport and gossip_tick have data.
648                            if let Some(report) = assess_for_tick.get_latest() {
649                                transport_for_tick.publish_report(report);
650                            }
651                            // Prefer pending (FindingDiff / gossip from run).
652                            let pushed = rt_tick.push_pending_assessment().await;
653                            if pushed == 0 {
654                                let _ = rt_tick.assessment_gossip_tick().await;
655                            }
656                        }
657                    });
658
659                    boot_log.push(BootEvent::info(
660                        BootPhase::Network,
661                        "AssessmentTransport wired into mesh event loop",
662                    ));
663                }
664
665                // Wire the A2A router BACK to the mesh runtime — the
666                // counterpart of `set_local_router` above. Without
667                // this, the A2A router's `Topic` handler has no mesh
668                // runtime to consult, so locally-published topics
669                // (e.g. via `ipc.publish` / `weaver leaf push`) are
670                // never forwarded to mesh-subscribed peers like leaf
671                // devices. `set_local_router` is mesh→A2A (inbound);
672                // this is A2A→mesh (outbound). Both directions are
673                // required for the leaf-push topic forwarding path
674                // (`peers_for_topic` → `send_to_peer`) to work.
675                a2a_router.set_mesh_runtime(Arc::clone(&runtime));
676                runtime.set_enforcing(
677                    mesh_config.admission == clawft_types::config::MeshAdmissionMode::Enforce,
678                );
679
680                // Clone values needed by seed-peer loop before moving mesh_config.
681                let seed_peers = mesh_config.seed_peers.clone();
682                let transport_name_for_seeds = mesh_config.transport.clone();
683                let transport_display = mesh_config.transport.clone();
684                let listen_display = mesh_config.listen_addr.clone();
685                let noise_enabled = mesh_config.noise;
686
687                // Generate or load Noise keypair.
688                let noise_config = if noise_enabled {
689                    let private_key: [u8; 32] = if let Some(ref key_path) =
690                        mesh_config.noise_key_path
691                    {
692                        let key_bytes = std::fs::read(key_path).unwrap_or_else(|_| {
693                            tracing::warn!("noise key file not found, generating ephemeral key");
694                            let mut key = [0u8; 32];
695                            use rand::RngCore;
696                            rand::thread_rng().fill_bytes(&mut key);
697                            key.to_vec()
698                        });
699                        let mut arr = [0u8; 32];
700                        arr.copy_from_slice(&key_bytes[..32.min(key_bytes.len())]);
701                        arr
702                    } else {
703                        let mut key = [0u8; 32];
704                        use rand::RngCore;
705                        rand::thread_rng().fill_bytes(&mut key);
706                        key
707                    };
708                    Some(Arc::new(crate::mesh_noise::NoiseConfig {
709                        pattern: crate::mesh_noise::NoisePattern::XX,
710                        local_private_key: private_key,
711                        remote_static_key: None,
712                    }))
713                } else {
714                    None
715                };
716                let noise_for_seeds = noise_config.clone();
717
718                // Bind the configured transport; the accept loop and seed
719                // dialing live in `mesh_serve` (P3-K0).
720                let listen_addr = mesh_config.listen_addr.clone();
721                let transport = crate::mesh_serve::transport_for(&mesh_config.transport, None);
722
723                // Bind synchronously so a taken port fails boot loudly instead of
724                // leaving a kernel that silently has no mesh (ADR-103 D1/D2).
725                // Disabled mesh never reaches this block, so mesh-off cannot fail here.
726                let listener = transport.listen(&listen_addr).await.map_err(|e| {
727                    KernelError::Boot(format!(
728                        "mesh enabled but the listener could not bind {listen_addr} ({e}); \
729                         another kernel on this machine is probably already using that port. \
730                         Stop it, set kernel.mesh.listen_addr to a free address, or set \
731                         kernel.mesh.enabled = false"
732                    ))
733                })?;
734
735                // Admission (P3-K1): AllowAll unless a genesis hash is pinned
736                // (observe) or enforcement is requested (needs the pin).
737                let pinned = mesh_config
738                    .genesis_hash
739                    .as_deref()
740                    .map(|h| {
741                        crate::mesh_admit::hex_decode(h)
742                            .and_then(|b| <[u8; 32]>::try_from(b).ok())
743                            .ok_or_else(|| {
744                                KernelError::Boot(
745                                    "kernel.mesh.genesis_hash must be 64 hex characters".into(),
746                                )
747                            })
748                    })
749                    .transpose()?;
750                let dial_identity: Option<Arc<crate::mesh_admit::DialIdentity>> =
751                    match (mesh_config.admission, pinned, node_key_seed) {
752                        (clawft_types::config::MeshAdmissionMode::Off, _, _) => None,
753                        (_, Some(genesis), Some(seed)) => {
754                            Some(Arc::new(crate::mesh_admit::DialIdentity {
755                                key: ed25519_dalek::SigningKey::from_bytes(&seed),
756                                genesis,
757                                platform: std::env::consts::OS.to_owned(),
758                                capabilities: vec![],
759                            }))
760                        }
761                        _ => None,
762                    };
763                let admission_gate: Arc<dyn crate::mesh_admit::AdmissionGate> = {
764                    use clawft_types::config::MeshAdmissionMode as Adm;
765                    match (mesh_config.admission, pinned) {
766                        (Adm::Off, _) | (Adm::Observe, None) => {
767                            Arc::new(crate::mesh_admit::AllowAll)
768                        }
769                        (Adm::Enforce, None) => {
770                            return Err(KernelError::Boot(
771                                "kernel.mesh.admission = \"enforce\" requires kernel.mesh.genesis_hash"
772                                    .into(),
773                            ));
774                        }
775                        (mode, Some(genesis)) => {
776                            #[cfg(feature = "exochain")]
777                            let verdicts: Arc<dyn crate::mesh_admit::VerdictSource> =
778                                mesh_verdicts.clone();
779                            #[cfg(not(feature = "exochain"))]
780                            let verdicts: Arc<dyn crate::mesh_admit::VerdictSource> = {
781                                if mode == Adm::Enforce && !mesh_config.admission_open_membership {
782                                    return Err(KernelError::Boot(
783                                        "kernel.mesh.admission = \"enforce\" needs a governance \
784                                         gate; this build has none. Set \
785                                         kernel.mesh.admission_open_membership = true to admit \
786                                         any valid hello"
787                                            .into(),
788                                    ));
789                                }
790                                Arc::new(crate::mesh_admit::OpenVerdicts)
791                            };
792                            Arc::new(crate::mesh_admit::CryptoGate::new(
793                                genesis,
794                                Arc::clone(&revocation_list),
795                                verdicts,
796                                mode,
797                            ))
798                        }
799                    }
800                };
801
802                let require_authenticated_seeds = crate::mesh_admit_gate::nested_peer_ceiling_active();
803                if let (Some(identity), Some(noise)) = (dial_identity.as_ref(), noise_config.as_ref()) {
804                    let noise_static = crate::mesh_noise::noise_static_public(&noise.local_private_key)
805                        .ok_or_else(|| KernelError::Boot("cannot derive mesh Noise key".into()))?;
806                    runtime.set_authentication(crate::mesh_runtime::MeshAuthentication {
807                        gate: Arc::clone(&admission_gate), identity: Arc::clone(identity),
808                        noise_static, require_authenticated_seeds,
809                    });
810                } else if require_authenticated_seeds {
811                    return Err(KernelError::Boot("nested mesh requires reciprocal authenticated admission".into()));
812                }
813                let rt = Arc::clone(&runtime);
814                let conn_limits = crate::mesh_limits::Limits {
815                    first_frame: std::time::Duration::from_secs(
816                        mesh_config.first_frame_timeout_secs.max(1),
817                    ),
818                    per_ip: mesh_config.max_connections_per_ip.max(1),
819                    ..crate::mesh_limits::Limits::default()
820                };
821                // Held by MeshService so `stop` ends the accept loop and the
822                // seed dial loops instead of leaving them running.
823                let listener_task = tokio::spawn(async move {
824                    crate::mesh_serve::serve_listener_with(
825                        rt,
826                        listener,
827                        noise_config,
828                        transport.name(),
829                        &listen_addr,
830                        admission_gate,
831                        conn_limits,
832                    )
833                    .await;
834                });
835
836                // Connect to seed peers.
837                let mut mesh_tasks = crate::mesh_serve::connect_seeds(
838                    &runtime,
839                    &seed_peers,
840                    &transport_name_for_seeds,
841                    noise_for_seeds,
842                    dial_identity,
843                );
844                mesh_tasks.push(listener_task);
845
846                // WEFT-119: register Mesh as a SystemService so start/stop/
847                // health_check are available via the kernel service surface.
848                // start_all() (phase 8) flips the service to started; stop_all()
849                // on shutdown disconnects peers.
850                let mesh_svc = Arc::new(crate::mesh_system_service::MeshService::with_endpoints(
851                    Arc::clone(&runtime),
852                    listen_display.clone(),
853                    transport_display.clone(),
854                ));
855                mesh_svc.adopt_tasks(mesh_tasks);
856                if let Err(e) = service_registry.register(mesh_svc) {
857                    error!(error = %e, "failed to register mesh service");
858                } else {
859                    boot_log.push(BootEvent::info(
860                        BootPhase::Network,
861                        "Mesh service registered (SystemService lifecycle)",
862                    ));
863                }
864
865                boot_log.push(BootEvent::info(
866                    BootPhase::Network,
867                    format!(
868                        "Mesh transport started ({} on {}, {} seed peers)",
869                        transport_display,
870                        listen_display,
871                        seed_peers.len(),
872                    ),
873                ));
874
875                Some(runtime)
876            } else {
877                boot_log.push(BootEvent::info(
878                    BootPhase::Network,
879                    if service_node_id.is_some() {
880                        "Mesh transport owned by the machine mesh service"
881                    } else {
882                        "Mesh transport disabled"
883                    },
884                ));
885                None
886            }
887        };
888
889        // 6. Create cluster membership (universal, always present)
890        let cluster_config = ClusterConfig {
891            node_id: node_id.clone(),
892            node_name: kernel_config
893                .cluster
894                .as_ref()
895                .and_then(|c| c.node_name.clone())
896                .unwrap_or_else(|| "local".into()),
897            heartbeat_interval_secs: kernel_config
898                .cluster
899                .as_ref()
900                .map(|c| c.heartbeat_interval_secs)
901                .unwrap_or(5),
902            ..ClusterConfig::default()
903        };
904        // Cluster peer membership persists to disk so joins survive restarts.
905        let cluster_peers_path = runtime_paths.cluster_peers();
906        let cluster_membership =
907            Arc::new(ClusterMembership::new(cluster_config).with_persist_path(&cluster_peers_path));
908
909        boot_log.push(BootEvent::info(
910            BootPhase::Network,
911            format!(
912                "Cluster membership ready (node {}, {} peer(s) rehydrated)",
913                cluster_membership.local_node_id(),
914                cluster_membership.len(),
915            ),
916        ));
917
918        // 6a. AppManager with on-disk manifest store (WEFT-136).
919        // Installs survive kernel restarts via atomic apps.json (mirrors cluster_peers).
920        let apps_persist_path =
921            runtime_paths.apps();
922        let app_manager = Arc::new(AppManager::new().with_persist_path(&apps_persist_path));
923        boot_log.push(BootEvent::info(
924            BootPhase::Services,
925            format!(
926                "AppManager ready ({} app(s) rehydrated from {})",
927                app_manager.len(),
928                apps_persist_path.display(),
929            ),
930        ));
931
932        // 6b. Host revocation list (persistent ban list), loaded before the
933        //     mesh block (5d) so the listener's admission gate shares it.
934        {
935            let count = revocation_list.len();
936            if count > 0 {
937                boot_log.push(BootEvent::info(
938                    BootPhase::Network,
939                    format!("Host revocation list loaded ({count} banned hosts)"),
940                ));
941            } else {
942                boot_log.push(BootEvent::info(
943                    BootPhase::Network,
944                    "Host revocation list ready (empty)",
945                ));
946            }
947        }
948
949        // 7. Register cluster service (when feature-gated ruvector integration is enabled)
950        //    WEFT-120: when mesh is live, subscribe ClusterService to the mesh
951        //    peer-event stream so membership tracks joins/leaves/partitions.
952        #[cfg(feature = "cluster")]
953        {
954            use crate::cluster::ClusterService;
955            use ruvector_cluster::StaticDiscovery;
956
957            let net_config = kernel_config.cluster.clone().unwrap_or_default();
958            let seed_addrs: Vec<std::net::SocketAddr> = net_config
959                .seed_nodes
960                .iter()
961                .filter_map(|s| s.parse().ok())
962                .collect();
963            let seed_nodes: Vec<ruvector_cluster::ClusterNode> = seed_addrs
964                .into_iter()
965                .map(|addr| ruvector_cluster::ClusterNode::new(addr.to_string(), addr))
966                .collect();
967            let discovery = Box::new(StaticDiscovery::new(seed_nodes));
968            let node_id = cluster_membership.local_node_id().to_owned();
969
970            match ClusterService::new(
971                net_config,
972                node_id,
973                discovery,
974                Arc::clone(&cluster_membership),
975            ) {
976                Ok(cluster_svc) => {
977                    let svc = Arc::new(cluster_svc);
978
979                    // WEFT-120: ClusterService ← mesh peer-event bus.
980                    #[cfg(all(feature = "native", feature = "mesh"))]
981                    {
982                        if let Some(ref mr) = mesh_runtime {
983                            let rx = mr.subscribe_peer_events();
984                            svc.spawn_mesh_peer_listener(rx);
985                            boot_log.push(BootEvent::info(
986                                BootPhase::Network,
987                                "ClusterService subscribed to mesh peer-event stream",
988                            ));
989                        }
990                    }
991
992                    if let Err(e) = service_registry.register(svc) {
993                        error!(error = %e, "failed to register cluster service");
994                    } else {
995                        boot_log.push(BootEvent::info(
996                            BootPhase::Network,
997                            "Cluster service registered (ruvector)",
998                        ));
999                    }
1000                }
1001                Err(e) => {
1002                    error!(error = %e, "failed to create cluster service");
1003                    boot_log.push(BootEvent::info(
1004                        BootPhase::Network,
1005                        format!("Cluster service failed: {e}"),
1006                    ));
1007                }
1008            }
1009        }
1010
1011        // WEFT-120 fallback: mesh without cluster feature — membership still
1012        // tracks live peers via the peer-event bus.
1013        #[cfg(all(feature = "native", feature = "mesh", not(feature = "cluster")))]
1014        {
1015            if let Some(ref mr) = mesh_runtime {
1016                let rx = mr.subscribe_peer_events();
1017                cluster_membership.spawn_mesh_peer_listener(rx);
1018                boot_log.push(BootEvent::info(
1019                    BootPhase::Network,
1020                    "ClusterMembership subscribed to mesh peer-event stream",
1021                ));
1022            }
1023        }
1024
1025        // 8. Start services (none registered by default at boot, unless cluster feature added one)
1026        service_registry
1027            .start_all()
1028            .await
1029            .map_err(|e| KernelError::Boot(format!("service start failed: {e}")))?;
1030
1031        // 8b. Initialize local exochain (when exochain feature is enabled)
1032        //     Restores from checkpoint file if available; otherwise fresh genesis.
1033        #[cfg(feature = "exochain")]
1034        let chain_manager = {
1035            let chain_config = kernel_config.chain.clone().unwrap_or_default();
1036            if chain_config.enabled {
1037                // Load or generate Ed25519 signing key for chain integrity.
1038                // ADR-103 A7, one project key: a project kernel's chain is
1039                // signed by the project key `pre_boot` loaded (the node key
1040                // seed), confirmed against `project.key`; never a
1041                // `chain.key` beside the checkpoint. Anything else refuses.
1042                let signing_key = if project_profile {
1043                    let child = crate::overlay_runtime::child_paths(&runtime_paths)
1044                        .map_err(|e| KernelError::BootRefused(e.boot_message()))?;
1045                    let path = child.project_key().ok_or_else(|| {
1046                        KernelError::BootRefused("profile `project` has no project key path".into())
1047                    })?;
1048                    let key = crate::project_identity::project_chain_key(&path, node_key_seed)
1049                        .map_err(|e| KernelError::BootRefused(e.to_string()))?;
1050                    boot_log.push(BootEvent::info(
1051                        BootPhase::Services,
1052                        format!("Ed25519 signing key is the project key: {}", path.display()),
1053                    ));
1054                    Some(key)
1055                } else if let Some(ref ckpt_path) =
1056                    chain_config.effective_checkpoint_path()
1057                {
1058                    // `~/.weftos/user.key` wins over `chain.key` for the user
1059                    // chain (ADR-103 D-5); every other chain keeps its own key.
1060                    let key_path = clawft_types::config::chain_paths::chain_key_for_checkpoint(
1061                        std::path::Path::new(ckpt_path),
1062                    );
1063                    // The user key is held to the same standard as the mesh
1064                    // client's reader: a symlink, a non-regular file or a key
1065                    // readable beyond its owner is refused, not followed.
1066                    #[cfg(unix)]
1067                    if key_path.file_name().is_some_and(|n| n == "user.key") {
1068                        use std::os::unix::fs::PermissionsExt;
1069                        if let Ok(m) = std::fs::symlink_metadata(&key_path)
1070                            && (!m.file_type().is_file() || m.permissions().mode() & 0o077 != 0)
1071                        {
1072                            return Err(KernelError::Boot(format!(
1073                                "{} must be a regular file readable only by its owner (chmod 600); refusing to sign the chain with it",
1074                                key_path.display()
1075                            )));
1076                        }
1077                    }
1078                    match crate::chain::ChainManager::load_or_create_key(&key_path) {
1079                        Ok(key) => {
1080                            boot_log.push(BootEvent::info(
1081                                BootPhase::Services,
1082                                format!("Ed25519 signing key loaded: {}", key_path.display()),
1083                            ));
1084                            Some(key)
1085                        }
1086                        Err(e) => {
1087                            warn!(error = %e, "failed to load/create signing key");
1088                            boot_log.push(BootEvent::info(
1089                                BootPhase::Services,
1090                                format!("Signing key unavailable: {e} — chain will be unsigned"),
1091                            ));
1092                            None
1093                        }
1094                    }
1095                } else {
1096                    None
1097                };
1098
1099                let cm = if let Some(ref ckpt_path) = chain_config.effective_checkpoint_path() {
1100                    let json_path = std::path::PathBuf::from(ckpt_path);
1101                    // Derive RVF path from JSON path: same directory, `.rvf` extension
1102                    let rvf_path = json_path.with_extension("rvf");
1103
1104                    if rvf_path.exists() {
1105                        // Prefer RVF format (cryptographic integrity verification)
1106                        match crate::chain::ChainManager::load_from_rvf(
1107                            &rvf_path,
1108                            chain_config.checkpoint_interval,
1109                        ) {
1110                            Ok(restored) => {
1111                                let seq = restored.sequence();
1112                                boot_log.push(BootEvent::info(
1113                                    BootPhase::Services,
1114                                    format!(
1115                                        "Chain restored from RVF (seq={}, chain_id={})",
1116                                        seq, chain_config.chain_id,
1117                                    ),
1118                                ));
1119                                Arc::new(restored)
1120                            }
1121                            Err(e) => {
1122                                error!(error = %e, "failed to restore RVF chain, trying JSON fallback");
1123                                // Fall back to JSON
1124                                if json_path.exists() {
1125                                    match crate::chain::ChainManager::load_from_file(
1126                                        &json_path,
1127                                        chain_config.checkpoint_interval,
1128                                    ) {
1129                                        Ok(restored) => {
1130                                            let seq = restored.sequence();
1131                                            boot_log.push(BootEvent::info(
1132                                                BootPhase::Services,
1133                                                format!(
1134                                                    "Chain restored from JSON fallback (seq={}, chain_id={})",
1135                                                    seq, chain_config.chain_id,
1136                                                ),
1137                                            ));
1138                                            Arc::new(restored)
1139                                        }
1140                                        Err(e2) => {
1141                                            error!(error = %e2, "JSON fallback also failed; refusing to boot");
1142                                            return Err(KernelError::Boot(unrestorable_chain_msg(
1143                                                &rvf_path,
1144                                                &format!("RVF: {e}; JSON: {e2}"),
1145                                            )));
1146                                        }
1147                                    }
1148                                } else {
1149                                    return Err(KernelError::Boot(unrestorable_chain_msg(
1150                                        &rvf_path,
1151                                        &e.to_string(),
1152                                    )));
1153                                }
1154                            }
1155                        }
1156                    } else if json_path.exists() {
1157                        // Legacy JSON format
1158                        match crate::chain::ChainManager::load_from_file(
1159                            &json_path,
1160                            chain_config.checkpoint_interval,
1161                        ) {
1162                            Ok(restored) => {
1163                                let seq = restored.sequence();
1164                                boot_log.push(BootEvent::info(
1165                                    BootPhase::Services,
1166                                    format!(
1167                                        "Chain restored from JSON (seq={}, chain_id={}, will migrate to RVF)",
1168                                        seq, chain_config.chain_id,
1169                                    ),
1170                                ));
1171                                Arc::new(restored)
1172                            }
1173                            Err(e) => {
1174                                error!(error = %e, "failed to restore chain; refusing to boot");
1175                                return Err(KernelError::Boot(unrestorable_chain_msg(
1176                                    &json_path,
1177                                    &e.to_string(),
1178                                )));
1179                            }
1180                        }
1181                    } else {
1182                        Arc::new(crate::chain::ChainManager::new(
1183                            chain_config.chain_id,
1184                            chain_config.checkpoint_interval,
1185                        ))
1186                    }
1187                } else {
1188                    Arc::new(crate::chain::ChainManager::new(
1189                        chain_config.chain_id,
1190                        chain_config.checkpoint_interval,
1191                    ))
1192                };
1193
1194                // Attach Ed25519 signing key if available.
1195                let mut cm = cm;
1196                if let Some(key) = signing_key
1197                    && let Some(inner) = Arc::get_mut(&mut cm)
1198                {
1199                    // Generate ML-DSA key from Ed25519 key bytes for dual signing.
1200                    let ml_dsa_seed = key.to_bytes();
1201                    let (ml_key, _ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(&ml_dsa_seed);
1202                    inner.set_signing_key(key);
1203                    inner.set_ml_dsa_key(ml_key);
1204                    boot_log.push(BootEvent::info(
1205                        BootPhase::Services,
1206                        "Dual signing enabled (Ed25519 + ML-DSA-65)",
1207                    ));
1208                }
1209
1210                boot_log.push(BootEvent::info(
1211                    BootPhase::Services,
1212                    format!(
1213                        "Local chain ready (chain_id={}, seq={}, signed={})",
1214                        chain_config.chain_id,
1215                        cm.sequence(),
1216                        cm.has_signing_key(),
1217                    ),
1218                ));
1219
1220                // ADR-103 A7: a project kernel stamps its effective rule hash on
1221                // every chain event, boot events included.
1222                if let Some(p) = overlay_prepared.as_ref() {
1223                    p.commit(&cm)
1224                        .map_err(|e| KernelError::BootRefused(e.boot_message()))?;
1225                    p.install_provider(&cm);
1226                }
1227
1228                // Log boot phases to chain
1229                cm.append(
1230                    "kernel",
1231                    "boot.init",
1232                    Some(serde_json::json!({"version": "0.1.0"})),
1233                );
1234                cm.append(
1235                    "kernel",
1236                    "boot.config",
1237                    Some(serde_json::json!({
1238                        "max_processes": kernel_config.max_processes,
1239                        "health_interval": kernel_config.health_check_interval_secs,
1240                    })),
1241                );
1242                cm.append(
1243                    "kernel",
1244                    "boot.services",
1245                    Some(serde_json::json!({
1246                        "count": service_registry.len(),
1247                    })),
1248                );
1249
1250                Some(cm)
1251            } else {
1252                boot_log.push(BootEvent::info(BootPhase::Services, "Local chain disabled"));
1253                None
1254            }
1255        };
1256
1257        // Every subject revocation and un-revocation the list takes, from the
1258        // operator verb or a mesh notice, is chained (`workload.revoke`,
1259        // `workload.unrevoke`): the list records it itself, so no caller can
1260        // skip it.
1261        #[cfg(feature = "exochain")]
1262        if let Some(ref cm) = chain_manager {
1263            crate::workload_governance::chain_revocations(&revocation_list, Arc::clone(cm));
1264        }
1265
1266        // 8b′. External chain-head anchoring controller (WEFT-137 / ADR-041).
1267        // Built from `kernel.chain.external_anchor` when present. Backend
1268        // `file` is the useful default; `external` wires an intent-log stub.
1269        #[cfg(feature = "exochain")]
1270        let chain_anchor = {
1271            let chain_config = kernel_config.chain.clone().unwrap_or_default();
1272            if !chain_config.enabled {
1273                None
1274            } else if let Some(ref acfg) = chain_config.external_anchor {
1275                match crate::chain_anchor::AnchoringController::from_config(acfg) {
1276                    Ok(Some(ctrl)) => {
1277                        boot_log.push(BootEvent::info(
1278                            BootPhase::Services,
1279                            format!(
1280                                "Chain external anchor ready (backend={}, min_interval={}s, min_events={})",
1281                                ctrl.backend_name(),
1282                                acfg.min_interval_secs,
1283                                acfg.min_events_between,
1284                            ),
1285                        ));
1286                        // Best-effort initial head anchor when a chain exists.
1287                        if let Some(ref cm) = chain_manager {
1288                            match ctrl.try_anchor_chain_head(cm) {
1289                                Ok(Some(receipt)) => {
1290                                    boot_log.push(BootEvent::info(
1291                                        BootPhase::Services,
1292                                        format!(
1293                                            "Chain head anchored at boot (tx_id={})",
1294                                            receipt.tx_id
1295                                        ),
1296                                    ));
1297                                }
1298                                Ok(None) => {
1299                                    // Policy suppressed (e.g. zero-event fresh chain
1300                                    // still anchors when gates are zero; otherwise skip).
1301                                }
1302                                Err(e) => {
1303                                    warn!(error = %e, "boot-time chain head anchor failed");
1304                                }
1305                            }
1306                        }
1307                        Some(Arc::new(ctrl))
1308                    }
1309                    Ok(None) => {
1310                        boot_log.push(BootEvent::info(
1311                            BootPhase::Services,
1312                            "Chain external anchor configured as none — skipped",
1313                        ));
1314                        None
1315                    }
1316                    Err(e) => {
1317                        warn!(error = %e, "failed to build chain external anchor");
1318                        boot_log.push(BootEvent::info(
1319                            BootPhase::Services,
1320                            format!("Chain external anchor unavailable: {e}"),
1321                        ));
1322                        None
1323                    }
1324                }
1325            } else {
1326                None
1327            }
1328        };
1329
1330        // 8b-mesh. Wire the chain manager into the mesh runtime so every
1331        //          successful `handle_incoming` appends a `peer.envelope`
1332        //          event to the ExoChain. Makes mesh activity auditable
1333        //          via `weaver chain local` without adding a topic
1334        //          subscriber.
1335        #[cfg(all(feature = "native", feature = "mesh", feature = "exochain"))]
1336        if let (Some(mr), Some(cm)) = (mesh_runtime.as_ref(), chain_manager.as_ref()) {
1337            mr.set_chain_manager(Arc::clone(cm));
1338        }
1339
1340        // 8c. Bootstrap resource tree via TreeManager (when exochain feature is enabled)
1341        //     First attempt to restore from checkpoint; fall back to fresh bootstrap.
1342        #[cfg(feature = "exochain")]
1343        let tree_manager = {
1344            let rt_config = kernel_config.resource_tree.clone().unwrap_or_default();
1345            if rt_config.enabled {
1346                if let Some(ref cm) = chain_manager {
1347                    let tm = Arc::new(crate::tree_manager::TreeManager::new(Arc::clone(cm)));
1348
1349                    // Derive tree checkpoint path from chain checkpoint path
1350                    let chain_cfg = kernel_config.chain.clone().unwrap_or_default();
1351                    let tree_ckpt_path = chain_cfg
1352                        .effective_checkpoint_path()
1353                        .map(|p| std::path::PathBuf::from(p).with_extension("tree.json"));
1354
1355                    let mut restored_from_checkpoint = false;
1356                    if let Some(ref tree_path) = tree_ckpt_path
1357                        && tree_path.exists()
1358                    {
1359                        match tm.load_checkpoint(tree_path) {
1360                            Ok(()) => {
1361                                let stats = tm.stats();
1362                                // Verify tree root hash against the chain's last recorded hash.
1363                                let chain_tree_hash = cm.last_tree_root_hash();
1364                                if let Some(ref expected) = chain_tree_hash {
1365                                    if stats.root_hash == *expected {
1366                                        boot_log.push(BootEvent::info(
1367                                            BootPhase::ResourceTree,
1368                                            format!(
1369                                                "Resource tree restored from checkpoint ({} nodes, root={}..., hash verified)",
1370                                                stats.node_count,
1371                                                &stats.root_hash[..12],
1372                                            ),
1373                                        ));
1374                                        restored_from_checkpoint = true;
1375                                    } else {
1376                                        warn!(
1377                                            expected = %expected,
1378                                            actual = %stats.root_hash,
1379                                            "tree checkpoint root hash mismatch — falling back to fresh bootstrap"
1380                                        );
1381                                        boot_log.push(BootEvent::info(
1382                                            BootPhase::ResourceTree,
1383                                            format!(
1384                                                "Tree checkpoint hash mismatch (expected={}..., got={}...), bootstrapping fresh",
1385                                                &expected[..std::cmp::min(12, expected.len())],
1386                                                &stats.root_hash[..12],
1387                                            ),
1388                                        ));
1389                                        // Don't set restored_from_checkpoint — falls through to bootstrap.
1390                                    }
1391                                } else {
1392                                    // No hash in chain to verify against — accept as-is.
1393                                    boot_log.push(BootEvent::info(
1394                                        BootPhase::ResourceTree,
1395                                        format!(
1396                                            "Resource tree restored from checkpoint ({} nodes, root={}...)",
1397                                            stats.node_count,
1398                                            &stats.root_hash[..12],
1399                                        ),
1400                                    ));
1401                                    restored_from_checkpoint = true;
1402                                }
1403                            }
1404                            Err(e) => {
1405                                error!(error = %e, "failed to restore tree checkpoint, bootstrapping fresh");
1406                            }
1407                        }
1408                    }
1409
1410                    if !restored_from_checkpoint {
1411                        if let Err(e) = tm.bootstrap() {
1412                            error!(error = %e, "failed to bootstrap resource tree");
1413                            // Still allow boot to proceed without tree
1414                        } else {
1415                            let stats = tm.stats();
1416                            boot_log.push(BootEvent::info(
1417                                BootPhase::ResourceTree,
1418                                format!(
1419                                    "Resource tree bootstrapped ({} nodes, root={}...)",
1420                                    stats.node_count,
1421                                    &stats.root_hash[..12],
1422                                ),
1423                            ));
1424                        }
1425                    }
1426
1427                    // Register cron service in tree with manifest (5b wiring)
1428                    if let Err(e) = tm.register_service_with_manifest("cron", "scheduler") {
1429                        tracing::debug!(error = %e, "failed to register cron in tree (may already exist)");
1430                    }
1431
1432                    // K4: Register container namespace in the resource tree
1433                    {
1434                        use exo_resource_tree::model::{ResourceId, ResourceKind};
1435                        if let Err(e) = tm.insert(
1436                            ResourceId::new("/kernel/containers"),
1437                            ResourceKind::Namespace,
1438                            ResourceId::new("/kernel"),
1439                        ) {
1440                            tracing::debug!(
1441                                error = %e,
1442                                "failed to register containers namespace (may already exist)"
1443                            );
1444                        }
1445                    }
1446
1447                    // K3c: Register ECC namespaces in the resource tree
1448                    #[cfg(feature = "ecc")]
1449                    {
1450                        use exo_resource_tree::model::{ResourceId, ResourceKind};
1451                        let ecc_namespaces = [
1452                            ("/kernel/services/ecc", "/kernel/services"),
1453                            ("/kernel/services/ecc/hnsw", "/kernel/services/ecc"),
1454                            ("/kernel/services/ecc/causal", "/kernel/services/ecc"),
1455                            ("/kernel/services/ecc/tick", "/kernel/services/ecc"),
1456                            ("/kernel/services/ecc/calibration", "/kernel/services/ecc"),
1457                            ("/kernel/services/ecc/crossrefs", "/kernel/services/ecc"),
1458                        ];
1459                        for (path, parent) in &ecc_namespaces {
1460                            if let Err(e) = tm.insert(
1461                                ResourceId::new(*path),
1462                                ResourceKind::Namespace,
1463                                ResourceId::new(*parent),
1464                            ) {
1465                                tracing::debug!(
1466                                    error = %e, path = *path,
1467                                    "failed to register ECC namespace (may already exist)"
1468                                );
1469                            }
1470                        }
1471                        boot_log.push(BootEvent::info(
1472                            BootPhase::Ecc,
1473                            "ECC resource tree namespaces registered",
1474                        ));
1475                    }
1476
1477                    // K3: Register tool namespaces in the resource tree
1478                    {
1479                        use exo_resource_tree::model::{ResourceId, ResourceKind};
1480                        let tool_namespaces = [
1481                            ("/kernel/tools", "/kernel"),
1482                            ("/kernel/tools/fs", "/kernel/tools"),
1483                            ("/kernel/tools/agent", "/kernel/tools"),
1484                            ("/kernel/tools/sys", "/kernel/tools"),
1485                            ("/kernel/tools/ipc", "/kernel/tools"),
1486                            #[cfg(feature = "ecc")]
1487                            ("/kernel/tools/ecc", "/kernel/tools"),
1488                        ];
1489                        for (path, parent) in &tool_namespaces {
1490                            if let Err(e) = tm.insert(
1491                                ResourceId::new(*path),
1492                                ResourceKind::Namespace,
1493                                ResourceId::new(*parent),
1494                            ) {
1495                                tracing::debug!(
1496                                    error = %e, path = *path,
1497                                    "failed to register tool namespace (may already exist)"
1498                                );
1499                            }
1500                        }
1501                        // Register each built-in tool as a Tool node
1502                        let catalog = crate::wasm_runner::builtin_tool_catalog();
1503                        for spec in &catalog {
1504                            let cat_path = match spec.category {
1505                                crate::wasm_runner::ToolCategory::Filesystem => "/kernel/tools/fs",
1506                                crate::wasm_runner::ToolCategory::Agent => "/kernel/tools/agent",
1507                                crate::wasm_runner::ToolCategory::System => "/kernel/tools/sys",
1508                                crate::wasm_runner::ToolCategory::Ecc => "/kernel/tools/ecc",
1509                                crate::wasm_runner::ToolCategory::User => "/kernel/tools",
1510                            };
1511                            let tool_path = format!("{}/{}", cat_path, spec.name.replace('.', "/"));
1512                            if let Err(e) = tm.insert(
1513                                ResourceId::new(&tool_path),
1514                                ResourceKind::Tool,
1515                                ResourceId::new(cat_path),
1516                            ) {
1517                                tracing::debug!(
1518                                    error = %e, tool = %spec.name,
1519                                    "failed to register tool node (may already exist)"
1520                                );
1521                            }
1522                        }
1523                        boot_log.push(BootEvent::info(
1524                            BootPhase::ResourceTree,
1525                            format!(
1526                                "Registered {} built-in tools in resource tree",
1527                                catalog.len()
1528                            ),
1529                        ));
1530                    }
1531
1532                    Some(tm)
1533                } else {
1534                    boot_log.push(BootEvent::info(
1535                        BootPhase::ResourceTree,
1536                        "Resource tree requires chain — skipped",
1537                    ));
1538                    None
1539                }
1540            } else {
1541                boot_log.push(BootEvent::info(
1542                    BootPhase::ResourceTree,
1543                    "Resource tree disabled",
1544                ));
1545                None
1546            }
1547        };
1548
1549        // 8f. Initialize governance engine with chain-anchored genesis rules
1550        //
1551        // The governance gate is the constitutional layer of the kernel.
1552        // At boot, genesis rules are written to the chain as immutable
1553        // entries. Once anchored, they cannot be modified — only superseded
1554        // by a `governance.root.supersede` event that references the
1555        // original genesis sequence.
1556        #[cfg(feature = "exochain")]
1557        let governance_gate: Option<Arc<dyn crate::gate::GateBackend>> = {
1558            if let Some(ref cm) = chain_manager {
1559                use crate::gate::GovernanceGate;
1560                use crate::governance::{GovernanceBranch, GovernanceRule, RuleSeverity};
1561
1562                // Default risk threshold (0.7 for production safety)
1563                let (risk_threshold, human_approval) = overlay_prepared
1564                    .as_ref()
1565                    .map_or((0.7, false), |p| p.engine_params());
1566
1567                let mut gate =
1568                    GovernanceGate::new(risk_threshold, human_approval).with_chain(Arc::clone(cm));
1569
1570                // ── Genesis governance rules (immutable chain entries) ────
1571                // These are the constitutional rules that govern all agent
1572                // behavior. Once written to the chain, they cannot be
1573                // modified — only superseded by a governance.root.supersede
1574                // event.
1575
1576                let genesis_rules = vec![
1577                    // ── Core constitutional rules (GOV-001 .. GOV-007) ──────
1578                    GovernanceRule {
1579                        id: "GOV-001".into(),
1580                        description: "High-risk operations require elevated review".into(),
1581                        branch: GovernanceBranch::Judicial,
1582                        severity: RuleSeverity::Blocking,
1583                        active: true,
1584                        reference_url: None,
1585                        sop_category: None,
1586                        rule_type: Default::default(),
1587                        action_selector: None,
1588                        tool_selector: None,
1589                        force_on_match: false,
1590                    },
1591                    GovernanceRule {
1592                        id: "GOV-002".into(),
1593                        description: "Security-sensitive actions must not exceed security threshold".into(),
1594                        branch: GovernanceBranch::Judicial,
1595                        severity: RuleSeverity::Blocking,
1596                        active: true,
1597                        reference_url: None,
1598                        sop_category: None,
1599                        rule_type: Default::default(),
1600                        action_selector: None,
1601                        tool_selector: None,
1602                        force_on_match: false,
1603                    },
1604                    GovernanceRule {
1605                        id: "GOV-003".into(),
1606                        description: "Privacy-impacting operations flagged for review".into(),
1607                        branch: GovernanceBranch::Legislative,
1608                        severity: RuleSeverity::Warning,
1609                        active: true,
1610                        reference_url: None,
1611                        sop_category: None,
1612                        rule_type: Default::default(),
1613                        action_selector: None,
1614                        tool_selector: None,
1615                        force_on_match: false,
1616                    },
1617                    GovernanceRule {
1618                        id: "GOV-004".into(),
1619                        description: "Novel/unprecedented actions require advisory logging".into(),
1620                        branch: GovernanceBranch::Executive,
1621                        severity: RuleSeverity::Advisory,
1622                        active: true,
1623                        reference_url: None,
1624                        sop_category: None,
1625                        rule_type: Default::default(),
1626                        action_selector: None,
1627                        tool_selector: None,
1628                        force_on_match: false,
1629                    },
1630                    GovernanceRule {
1631                        id: "GOV-005".into(),
1632                        description: "Filesystem write operations scored for risk".into(),
1633                        branch: GovernanceBranch::Legislative,
1634                        severity: RuleSeverity::Warning,
1635                        active: true,
1636                        reference_url: None,
1637                        sop_category: None,
1638                        rule_type: Default::default(),
1639                        action_selector: None,
1640                        tool_selector: None,
1641                        force_on_match: false,
1642                    },
1643                    GovernanceRule {
1644                        id: "GOV-006".into(),
1645                        description: "Agent spawn operations require governance clearance".into(),
1646                        branch: GovernanceBranch::Executive,
1647                        severity: RuleSeverity::Blocking,
1648                        active: true,
1649                        reference_url: None,
1650                        sop_category: None,
1651                        rule_type: Default::default(),
1652                        action_selector: None,
1653                        tool_selector: None,
1654                        force_on_match: false,
1655                    },
1656                    GovernanceRule {
1657                        id: "GOV-007".into(),
1658                        description: "IPC messages between agents logged for audit trail".into(),
1659                        branch: GovernanceBranch::Judicial,
1660                        severity: RuleSeverity::Advisory,
1661                        active: true,
1662                        reference_url: None,
1663                        sop_category: None,
1664                        rule_type: Default::default(),
1665                        action_selector: None,
1666                        tool_selector: None,
1667                        force_on_match: false,
1668                    },
1669                    // ── AI-SDLC SOP rules: Legislative (6) ──────────────────
1670                    GovernanceRule {
1671                        id: "SOP-L001".into(),
1672                        description: "AI-IRB approval required before high-impact deployments".into(),
1673                        branch: GovernanceBranch::Legislative,
1674                        severity: RuleSeverity::Blocking,
1675                        active: true,
1676                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1300-01-AI_IRB_Approval.md".into()),
1677                        sop_category: Some("governance".into()),
1678                        rule_type: Default::default(),
1679                        action_selector: None,
1680                        tool_selector: None,
1681                        force_on_match: false,
1682                    },
1683                    GovernanceRule {
1684                        id: "SOP-L002".into(),
1685                        description: "Version control and branching policies must be enforced".into(),
1686                        branch: GovernanceBranch::Legislative,
1687                        severity: RuleSeverity::Warning,
1688                        active: true,
1689                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1003-01-AI_Version_Control.md".into()),
1690                        sop_category: Some("governance".into()),
1691                        rule_type: Default::default(),
1692                        action_selector: None,
1693                        tool_selector: None,
1694                        force_on_match: false,
1695                    },
1696                    GovernanceRule {
1697                        id: "SOP-L003".into(),
1698                        description: "Requirements must include AI-IRB ethical review".into(),
1699                        branch: GovernanceBranch::Legislative,
1700                        severity: RuleSeverity::Warning,
1701                        active: true,
1702                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1040-01-AI_Requirements.md".into()),
1703                        sop_category: Some("engineering".into()),
1704                        rule_type: Default::default(),
1705                        action_selector: None,
1706                        tool_selector: None,
1707                        force_on_match: false,
1708                    },
1709                    GovernanceRule {
1710                        id: "SOP-L004".into(),
1711                        description: "Release planning must follow structured lifecycle gates".into(),
1712                        branch: GovernanceBranch::Legislative,
1713                        severity: RuleSeverity::Advisory,
1714                        active: true,
1715                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1005-01-AI_Release_Planning.md".into()),
1716                        sop_category: Some("lifecycle".into()),
1717                        rule_type: Default::default(),
1718                        action_selector: None,
1719                        tool_selector: None,
1720                        force_on_match: false,
1721                    },
1722                    GovernanceRule {
1723                        id: "SOP-L005".into(),
1724                        description: "Data protection and PII handling must comply with policy".into(),
1725                        branch: GovernanceBranch::Legislative,
1726                        severity: RuleSeverity::Blocking,
1727                        active: true,
1728                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1303-01-AI_Data_Protection.md".into()),
1729                        sop_category: Some("ethics".into()),
1730                        rule_type: Default::default(),
1731                        action_selector: None,
1732                        tool_selector: None,
1733                        force_on_match: false,
1734                    },
1735                    GovernanceRule {
1736                        id: "SOP-L006".into(),
1737                        description: "Risk register must be maintained and reviewed".into(),
1738                        branch: GovernanceBranch::Legislative,
1739                        severity: RuleSeverity::Warning,
1740                        active: true,
1741                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1062-01-AI_Risk_Register.md".into()),
1742                        sop_category: Some("governance".into()),
1743                        rule_type: Default::default(),
1744                        action_selector: None,
1745                        tool_selector: None,
1746                        force_on_match: false,
1747                    },
1748                    // ── AI-SDLC SOP rules: Executive (5) ────────────────────
1749                    GovernanceRule {
1750                        id: "SOP-E001".into(),
1751                        description: "Secure coding standards must be followed".into(),
1752                        branch: GovernanceBranch::Executive,
1753                        severity: RuleSeverity::Warning,
1754                        active: true,
1755                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1200-01-AI_Secure_Coding.md".into()),
1756                        sop_category: Some("engineering".into()),
1757                        rule_type: Default::default(),
1758                        action_selector: None,
1759                        tool_selector: None,
1760                        force_on_match: false,
1761                    },
1762                    GovernanceRule {
1763                        id: "SOP-E002".into(),
1764                        description: "Deployment requires governance clearance checkpoint".into(),
1765                        branch: GovernanceBranch::Executive,
1766                        severity: RuleSeverity::Blocking,
1767                        active: true,
1768                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1220-01-AI_Deployment_Clearance.md".into()),
1769                        sop_category: Some("lifecycle".into()),
1770                        rule_type: Default::default(),
1771                        action_selector: None,
1772                        tool_selector: None,
1773                        force_on_match: false,
1774                    },
1775                    GovernanceRule {
1776                        id: "SOP-E003".into(),
1777                        description: "Incident response procedures must be documented and followed".into(),
1778                        branch: GovernanceBranch::Executive,
1779                        severity: RuleSeverity::Warning,
1780                        active: true,
1781                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1008-01-AI_Incident_Response.md".into()),
1782                        sop_category: Some("security".into()),
1783                        rule_type: Default::default(),
1784                        action_selector: None,
1785                        tool_selector: None,
1786                        force_on_match: false,
1787                    },
1788                    GovernanceRule {
1789                        id: "SOP-E004".into(),
1790                        description: "Decommissioning must follow structured teardown procedure".into(),
1791                        branch: GovernanceBranch::Executive,
1792                        severity: RuleSeverity::Advisory,
1793                        active: true,
1794                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1011-01-AI_Decommissioning.md".into()),
1795                        sop_category: Some("lifecycle".into()),
1796                        rule_type: Default::default(),
1797                        action_selector: None,
1798                        tool_selector: None,
1799                        force_on_match: false,
1800                    },
1801                    GovernanceRule {
1802                        id: "SOP-E005".into(),
1803                        description: "Third-party AI procurement requires screening".into(),
1804                        branch: GovernanceBranch::Executive,
1805                        severity: RuleSeverity::Advisory,
1806                        active: true,
1807                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1004-01-AI_Procurement_Screening.md".into()),
1808                        sop_category: Some("governance".into()),
1809                        rule_type: Default::default(),
1810                        action_selector: None,
1811                        tool_selector: None,
1812                        force_on_match: false,
1813                    },
1814                    // ── AI-SDLC SOP rules: Judicial (4) ─────────────────────
1815                    GovernanceRule {
1816                        id: "SOP-J001".into(),
1817                        description: "Bias and fairness assessments required for model outputs".into(),
1818                        branch: GovernanceBranch::Judicial,
1819                        severity: RuleSeverity::Blocking,
1820                        active: true,
1821                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1301-01-AI_Bias_Fairness.md".into()),
1822                        sop_category: Some("ethics".into()),
1823                        rule_type: Default::default(),
1824                        action_selector: None,
1825                        tool_selector: None,
1826                        force_on_match: false,
1827                    },
1828                    GovernanceRule {
1829                        id: "SOP-J002".into(),
1830                        description: "Explainability documentation required for decision systems".into(),
1831                        branch: GovernanceBranch::Judicial,
1832                        severity: RuleSeverity::Warning,
1833                        active: true,
1834                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1302-01-AI_Explainability.md".into()),
1835                        sop_category: Some("ethics".into()),
1836                        rule_type: Default::default(),
1837                        action_selector: None,
1838                        tool_selector: None,
1839                        force_on_match: false,
1840                    },
1841                    GovernanceRule {
1842                        id: "SOP-J003".into(),
1843                        description: "Model drift detection and monitoring must be active".into(),
1844                        branch: GovernanceBranch::Judicial,
1845                        severity: RuleSeverity::Warning,
1846                        active: true,
1847                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-1009-01-AI_Drift_Detection.md".into()),
1848                        sop_category: Some("lifecycle".into()),
1849                        rule_type: Default::default(),
1850                        action_selector: None,
1851                        tool_selector: None,
1852                        force_on_match: false,
1853                    },
1854                    GovernanceRule {
1855                        id: "SOP-J004".into(),
1856                        description: "Quality records must be maintained for audit compliance".into(),
1857                        branch: GovernanceBranch::Judicial,
1858                        severity: RuleSeverity::Advisory,
1859                        active: true,
1860                        reference_url: Some("https://github.com/AISDLC/AI-SDLC-SOPs/blob/main/sops/SOP-2002-01-AI_Quality_Records.md".into()),
1861                        sop_category: Some("quality".into()),
1862                        rule_type: Default::default(),
1863                        action_selector: None,
1864                        tool_selector: None,
1865                        force_on_match: false,
1866                    },
1867                    // ── Browser policy rules (S7 / WEFT-108) ────────────────
1868                    // Browser nodes are untrusted; these rules constrain IPC
1869                    // topics, spawn, and network without relying on effect
1870                    // magnitude. Operators may supersede via governance.root.
1871                ]
1872                .into_iter()
1873                .chain(crate::governance::browser_policy_default_rules())
1874                // WEFT-342: soul.binding_thread_intact (binding-thread integrity)
1875                .chain(crate::governance::binding_thread_default_rules())
1876                // ADR-099 s4: default-deny for workload.* actions.
1877                .chain(crate::workload_governance::default_rules())
1878                .collect::<Vec<_>>();
1879                // ADR-103 D8 (project profile): the effective rules replace the
1880                // default set.
1881                let genesis_rules = match overlay_prepared.as_ref() {
1882                    Some(p) => p.rules().to_vec(),
1883                    None => genesis_rules,
1884                };
1885
1886                // Anchor genesis rules to chain
1887                let genesis_seq = cm.sequence();
1888                let rules_json: Vec<serde_json::Value> = genesis_rules
1889                    .iter()
1890                    .map(|r| {
1891                        serde_json::json!({
1892                            "id": r.id,
1893                            "description": r.description,
1894                            "branch": format!("{}", r.branch),
1895                            "severity": format!("{}", r.severity),
1896                            "reference_url": r.reference_url,
1897                            "sop_category": r.sop_category,
1898                            "rule_type": format!("{}", r.rule_type),
1899                        })
1900                    })
1901                    .collect();
1902
1903                // The governance.genesis event is the ROOT — it establishes the
1904                // constitutional rules. Its chain sequence is the governance root.
1905                cm.append(
1906                    "governance",
1907                    "governance.genesis",
1908                    Some(serde_json::json!({
1909                        "version": "2.0.0",
1910                        "risk_threshold": risk_threshold,
1911                        "human_approval_required": human_approval,
1912                        "rules": rules_json,
1913                        "rule_count": genesis_rules.len(),
1914                        "genesis_seq": genesis_seq,
1915                    })),
1916                );
1917
1918                // Anchor each rule individually for granular verification
1919                for rule in &genesis_rules {
1920                    cm.append(
1921                        "governance",
1922                        "governance.rule",
1923                        Some(serde_json::json!({
1924                            "rule_id": rule.id,
1925                            "branch": format!("{}", rule.branch),
1926                            "severity": format!("{}", rule.severity),
1927                            "genesis_seq": genesis_seq,
1928                        })),
1929                    );
1930                }
1931
1932                // Add rules to the gate
1933                for rule in genesis_rules {
1934                    gate = gate.add_rule(rule);
1935                }
1936
1937                boot_log.push(BootEvent::info(
1938                    BootPhase::Services,
1939                    format!(
1940                        "Governance genesis anchored (seq={}, {} rules, threshold={:.1})",
1941                        genesis_seq,
1942                        gate.engine().rule_count(),
1943                        risk_threshold,
1944                    ),
1945                ));
1946
1947                match overlay_prepared.take() {
1948                    Some(p) => {
1949                        let (gate, rt) = p.into_runtime(gate, Arc::clone(cm));
1950                        overlay_runtime = Some(rt);
1951                        Some(gate)
1952                    }
1953                    None => Some(Arc::new(gate) as Arc<dyn crate::gate::GateBackend>),
1954                }
1955            } else {
1956                if overlay_prepared.is_some() {
1957                    return Err(KernelError::BootRefused(
1958                        "profile `project` needs a chain: governance cannot run open".into(),
1959                    ));
1960                }
1961                boot_log.push(BootEvent::info(
1962                    BootPhase::Services,
1963                    "Governance: no chain available, using open governance",
1964                ));
1965                None
1966            }
1967        };
1968
1969        #[cfg(all(feature = "native", feature = "mesh", feature = "exochain"))]
1970        match &governance_gate {
1971            Some(g) => mesh_verdicts.bind(Arc::clone(g)),
1972            None if mesh_closed_without_gate => {
1973                error!(
1974                    "mesh admission = enforce but no governance gate exists: every peer will be \
1975                     refused. Enable the chain/governance, or set \
1976                     kernel.mesh.admission_open_membership = true to admit any valid hello"
1977                );
1978                mesh_verdicts.bind_closed();
1979            }
1980            None => {
1981                warn!("mesh admission_open_membership: admitting any peer with a valid hello");
1982                mesh_verdicts.bind_open();
1983            }
1984        }
1985
1986        // Wire governance gate into A2A router for dual-layer enforcement.
1987        // The A2ARouter is already behind Arc, so we use set_gate() which
1988        // relies on OnceLock interior mutability.
1989        #[cfg(feature = "exochain")]
1990        if let Some(ref gate) = governance_gate {
1991            a2a_router.set_gate(Arc::clone(gate));
1992        }
1993
1994        // 8d. Log cluster and boot.ready chain events
1995        #[cfg(feature = "exochain")]
1996        if let Some(ref cm) = chain_manager {
1997            cm.append(
1998                "kernel",
1999                "boot.cluster",
2000                Some(serde_json::json!({
2001                    "node_id": cluster_membership.local_node_id(),
2002                })),
2003            );
2004
2005            let elapsed_ms = boot_time.elapsed().as_millis() as u64;
2006            let mut ready_payload = serde_json::json!({
2007                "elapsed_ms": elapsed_ms,
2008                "processes": process_table.len(),
2009                "services": service_registry.len(),
2010            });
2011
2012            if let Some(ref tm) = tree_manager {
2013                let root_hash = tm.stats().root_hash;
2014                ready_payload
2015                    .as_object_mut()
2016                    .unwrap()
2017                    .insert("tree_root_hash".to_string(), serde_json::json!(root_hash));
2018            }
2019
2020            cm.append("kernel", "boot.ready", Some(ready_payload));
2021
2022            // Emit boot manifest as a chain event (becomes an ExochainCheckpoint
2023            // segment in RVF persistence, capturing the complete boot state).
2024            let mut manifest = serde_json::json!({
2025                "version": env!("CARGO_PKG_VERSION"),
2026                "node_id": cluster_membership.local_node_id(),
2027                "process_count": process_table.len(),
2028                "service_count": service_registry.len(),
2029                "chain_sequence": cm.sequence(),
2030                "boot_elapsed_ms": boot_time.elapsed().as_millis() as u64,
2031            });
2032            if let Some(ref tm) = tree_manager {
2033                let stats = tm.stats();
2034                manifest.as_object_mut().unwrap().insert(
2035                    "tree_root_hash".to_string(),
2036                    serde_json::json!(stats.root_hash),
2037                );
2038                manifest.as_object_mut().unwrap().insert(
2039                    "tree_node_count".to_string(),
2040                    serde_json::json!(stats.node_count),
2041                );
2042            }
2043            cm.append("kernel", "boot.manifest", Some(manifest));
2044        }
2045
2046        // 8e. Initialize ECC cognitive substrate (when ecc feature is enabled)
2047        #[cfg(feature = "ecc")]
2048        let (
2049            ecc_hnsw,
2050            ecc_causal,
2051            ecc_tick,
2052            ecc_crossrefs,
2053            ecc_impulses,
2054            ecc_calibration,
2055            ecc_vector_backend,
2056            ecc_eml_model,
2057            ecc_spatial,
2058        ) = {
2059            use crate::calibration::{EccCalibrationConfig, run_calibration};
2060            use crate::causal::CausalGraph;
2061            use crate::cognitive_tick::{CognitiveTick, CognitiveTickConfig};
2062            use crate::crossref::CrossRefStore;
2063            use crate::hnsw_service::{HnswService, HnswServiceConfig};
2064            use crate::impulse::ImpulseQueue;
2065            use crate::service::SystemService;
2066            use crate::spatial_service::SpatialService;
2067            use crate::vector_backend::VectorBackend;
2068            use crate::vector_diskann::{
2069                DiskAnnBackend, DiskAnnConfig, DiskAnnFeatureStatus, diskann_feature_status,
2070            };
2071            use crate::vector_hnsw::HnswBackend;
2072            use crate::vector_hybrid::{HybridBackend, HybridConfig};
2073
2074            boot_log.push(BootEvent::info(
2075                BootPhase::Ecc,
2076                "Initializing ECC cognitive substrate",
2077            ));
2078
2079            let hnsw = Arc::new(HnswService::new(HnswServiceConfig::default()));
2080            let causal = Arc::new(CausalGraph::new());
2081            let crossrefs = Arc::new(CrossRefStore::new());
2082            let impulses = Arc::new(ImpulseQueue::new());
2083
2084            // Construct the vector backend based on kernel config.
2085            let vector_config = kernel_config.vector.clone();
2086            let vector_strict = vector_config.as_ref().map(|v| v.strict).unwrap_or(false);
2087            let vector_backend: Arc<dyn VectorBackend> =
2088                match vector_config.as_ref().map(|v| v.backend) {
2089                    Some(clawft_types::config::VectorBackendKind::DiskAnn) => {
2090                        match diskann_feature_status(
2091                            clawft_types::config::VectorBackendKind::DiskAnn,
2092                            cfg!(feature = "diskann"),
2093                            vector_strict,
2094                        ) {
2095                            DiskAnnFeatureStatus::StubRejected => {
2096                                return Err(KernelError::BootRefused(
2097                                    DISKANN_FEATURE_MISMATCH_ERROR.to_string(),
2098                                ));
2099                            }
2100                            DiskAnnFeatureStatus::StubDegraded => {
2101                                boot_log.push(BootEvent::warn(
2102                                    BootPhase::Ecc,
2103                                    DISKANN_FEATURE_MISMATCH_WARNING,
2104                                ));
2105                            }
2106                            DiskAnnFeatureStatus::Ok => {}
2107                        }
2108                        let da_cfg = vector_config
2109                            .as_ref()
2110                            .and_then(|v| v.diskann.as_ref())
2111                            .map(|d| DiskAnnConfig {
2112                                max_points: d.max_points,
2113                                dimensions: d.dimensions,
2114                                num_neighbors: d.num_neighbors,
2115                                search_list_size: d.search_list_size,
2116                                data_path: d.data_path.clone(),
2117                                use_pq: d.use_pq,
2118                                pq_num_chunks: d.pq_num_chunks,
2119                            })
2120                            .unwrap_or_default();
2121                        boot_log.push(BootEvent::info(
2122                            BootPhase::Ecc,
2123                            if cfg!(feature = "diskann") {
2124                                "Vector backend: DiskANN (real: Vamana graph + PQ)"
2125                            } else {
2126                                "Vector backend: DiskANN (STUB: brute-force O(n) linear scan)"
2127                            },
2128                        ));
2129                        Arc::new(DiskAnnBackend::new(da_cfg))
2130                    }
2131                    Some(clawft_types::config::VectorBackendKind::Hybrid) => {
2132                        match diskann_feature_status(
2133                            clawft_types::config::VectorBackendKind::Hybrid,
2134                            cfg!(feature = "diskann"),
2135                            vector_strict,
2136                        ) {
2137                            DiskAnnFeatureStatus::StubRejected => {
2138                                return Err(KernelError::BootRefused(
2139                                    DISKANN_FEATURE_MISMATCH_ERROR.to_string(),
2140                                ));
2141                            }
2142                            DiskAnnFeatureStatus::StubDegraded => {
2143                                boot_log.push(BootEvent::warn(
2144                                    BootPhase::Ecc,
2145                                    DISKANN_FEATURE_MISMATCH_WARNING,
2146                                ));
2147                            }
2148                            DiskAnnFeatureStatus::Ok => {}
2149                        }
2150                        let hnsw_cfg = vector_config
2151                            .as_ref()
2152                            .and_then(|v| v.hnsw.as_ref())
2153                            .map(|h| HnswServiceConfig {
2154                                ef_construction: h.ef_construction,
2155                                ef_search: 100,
2156                                default_dimensions: 384,
2157                            })
2158                            .unwrap_or_default();
2159                        let da_cfg = vector_config
2160                            .as_ref()
2161                            .and_then(|v| v.diskann.as_ref())
2162                            .map(|d| DiskAnnConfig {
2163                                max_points: d.max_points,
2164                                dimensions: d.dimensions,
2165                                num_neighbors: d.num_neighbors,
2166                                search_list_size: d.search_list_size,
2167                                data_path: d.data_path.clone(),
2168                                use_pq: d.use_pq,
2169                                pq_num_chunks: d.pq_num_chunks,
2170                            })
2171                            .unwrap_or_default();
2172                        let hybrid_cfg = vector_config
2173                            .as_ref()
2174                            .and_then(|v| v.hybrid.as_ref())
2175                            .map(|h| HybridConfig {
2176                                hot_capacity: h.hot_capacity,
2177                                promotion_threshold: h.promotion_threshold,
2178                                eviction_policy: crate::vector_hybrid::EvictionPolicy::Lru,
2179                            })
2180                            .unwrap_or_default();
2181                        boot_log.push(BootEvent::info(
2182                            BootPhase::Ecc,
2183                            format!(
2184                                "Vector backend: Hybrid (hot={}, threshold={}, cold={})",
2185                                hybrid_cfg.hot_capacity,
2186                                hybrid_cfg.promotion_threshold,
2187                                if cfg!(feature = "diskann") {
2188                                    "DiskANN real"
2189                                } else {
2190                                    "DiskANN STUB"
2191                                },
2192                            ),
2193                        ));
2194                        Arc::new(HybridBackend::new(hnsw_cfg, da_cfg, hybrid_cfg))
2195                    }
2196                    _ => {
2197                        // Default: HNSW only.
2198                        boot_log.push(BootEvent::info(
2199                            BootPhase::Ecc,
2200                            "Vector backend: HNSW (in-memory)",
2201                        ));
2202                        Arc::new(HnswBackend::with_defaults())
2203                    }
2204                };
2205
2206            // Run boot-time calibration
2207            let cal_config = EccCalibrationConfig::default();
2208            let calibration = run_calibration(&hnsw, &causal, &cal_config);
2209
2210            boot_log.push(BootEvent::info(
2211                BootPhase::Ecc,
2212                format!(
2213                    "ECC calibration complete (p50={}us, p95={}us, tick={}ms, spectral={})",
2214                    calibration.compute_p50_us,
2215                    calibration.compute_p95_us,
2216                    calibration.tick_interval_ms,
2217                    calibration.spectral_capable,
2218                ),
2219            ));
2220
2221            // Create cognitive tick with calibrated interval
2222            let tick_config = CognitiveTickConfig {
2223                tick_interval_ms: calibration.tick_interval_ms,
2224                ..CognitiveTickConfig::default()
2225            };
2226            let tick = Arc::new(CognitiveTick::new(tick_config));
2227
2228            // Register ECC services
2229            if let Err(e) = service_registry.register(hnsw.clone()) {
2230                tracing::debug!(error = %e, "failed to register HNSW service");
2231            }
2232            if let Err(e) = service_registry.register(tick.clone()) {
2233                tracing::debug!(error = %e, "failed to register cognitive tick service");
2234            }
2235
2236            // Start the cognitive tick service explicitly -- start_all() ran
2237            // before ECC init so this service was never started, which caused
2238            // health checks to report "degraded - cognitive tick not running".
2239            if let Err(e) = tick.start().await {
2240                tracing::warn!(error = %e, "failed to start cognitive tick service");
2241            }
2242
2243            // Spawn the DEMOCRITUS two-tier coherence tick loop.
2244            // This runs in the background, using the EML model for O(1)
2245            // predictions on every tick and falling back to exact Lanczos
2246            // spectral analysis when drift is detected.
2247            let eml_model = Arc::new(std::sync::Mutex::new(
2248                crate::eml_coherence::EmlCoherenceModel::new(),
2249            ));
2250            {
2251                let tick_clone = tick.clone();
2252                let causal_clone = causal.clone();
2253                let hnsw_clone = hnsw.clone();
2254                let eml_clone = eml_model.clone();
2255                tokio::spawn(async move {
2256                    crate::cognitive_tick::run_democritus_loop(
2257                        tick_clone,
2258                        causal_clone,
2259                        hnsw_clone,
2260                        eml_clone,
2261                    )
2262                    .await;
2263                });
2264            }
2265            boot_log.push(BootEvent::info(
2266                BootPhase::Ecc,
2267                "DEMOCRITUS two-tier coherence loop spawned",
2268            ));
2269
2270            // Log calibration to chain
2271            #[cfg(feature = "exochain")]
2272            if let Some(ref cm) = chain_manager {
2273                cm.append(
2274                    "ecc",
2275                    "ecc.boot.calibration",
2276                    Some(serde_json::json!({
2277                        "compute_p50_us": calibration.compute_p50_us,
2278                        "compute_p95_us": calibration.compute_p95_us,
2279                        "tick_interval_ms": calibration.tick_interval_ms,
2280                        "spectral_capable": calibration.spectral_capable,
2281                    })),
2282                );
2283            }
2284
2285            // Spatial BVH (opt-in via [kernel.spatial].enabled)
2286            let spatial_svc: Option<Arc<SpatialService>> = {
2287                let spatial_cfg = kernel_config.spatial.clone().unwrap_or_default();
2288                if spatial_cfg.enabled {
2289                    let svc = Arc::new(SpatialService::from_spatial_config(&spatial_cfg));
2290                    #[cfg(feature = "exochain")]
2291                    if let Some(ref cm) = chain_manager {
2292                        svc.attach_chain(cm.clone());
2293                    }
2294                    if let Err(e) = service_registry.register(svc.clone()) {
2295                        tracing::debug!(error = %e, "failed to register spatial service");
2296                    }
2297                    if let Err(e) = svc.start().await {
2298                        tracing::warn!(error = %e, "failed to start spatial service");
2299                    }
2300                    boot_log.push(BootEvent::info(
2301                        BootPhase::Ecc,
2302                        format!(
2303                            "Spatial BVH ready (backend={}, max_leaves={})",
2304                            svc.backend().backend_name(),
2305                            spatial_cfg.max_leaves,
2306                        ),
2307                    ));
2308                    Some(svc)
2309                } else {
2310                    boot_log.push(BootEvent::info(
2311                        BootPhase::Ecc,
2312                        "Spatial BVH disabled (set kernel.spatial.enabled = true)",
2313                    ));
2314                    None
2315                }
2316            };
2317
2318            boot_log.push(BootEvent::info(
2319                BootPhase::Ecc,
2320                format!(
2321                    "ECC ready (hnsw={}, causal={} nodes, tick={}ms, vector={}, spatial={})",
2322                    hnsw.len(),
2323                    causal.node_count(),
2324                    calibration.tick_interval_ms,
2325                    vector_backend.backend_name(),
2326                    spatial_svc
2327                        .as_ref()
2328                        .map(|s| s.backend().backend_name())
2329                        .unwrap_or("off"),
2330                ),
2331            ));
2332
2333            (
2334                Some(hnsw),
2335                Some(causal),
2336                Some(tick),
2337                Some(crossrefs),
2338                Some(impulses),
2339                Some(calibration),
2340                Some(vector_backend),
2341                Some(eml_model),
2342                spatial_svc,
2343            )
2344        };
2345
2346        // 8g. Initialize os-patterns observability modules
2347        #[cfg(feature = "os-patterns")]
2348        let (metrics_registry, log_svc, timer_svc, dead_letter_queue) = {
2349            use crate::dead_letter::DeadLetterQueue;
2350            use crate::log_service::LogService;
2351            use crate::metrics::MetricsRegistry;
2352            use crate::timer::TimerService;
2353
2354            boot_log.push(BootEvent::info(
2355                BootPhase::Services,
2356                "Initializing os-patterns observability modules",
2357            ));
2358
2359            // MetricsRegistry with built-in + boot-specific gauges
2360            let registry = Arc::new(MetricsRegistry::with_builtins());
2361            // Seed additional kernel gauges requested by W3
2362            registry.gauge_set("kernel.process.count", process_table.len() as i64);
2363            registry.gauge_set("kernel.agent.count", 0);
2364            registry.gauge_set("kernel.ipc.messages_sent", 0);
2365            registry.counter_add("kernel.ipc.messages_failed", 0);
2366            registry.gauge_set("kernel.chain.height", 0);
2367            registry.gauge_set("kernel.uptime_secs", 0);
2368
2369            boot_log.push(BootEvent::info(
2370                BootPhase::Services,
2371                "MetricsRegistry ready (built-in + kernel gauges)",
2372            ));
2373
2374            // LogService (ring-buffer structured logs)
2375            let log_svc = Arc::new(LogService::with_default_capacity());
2376            boot_log.push(BootEvent::info(BootPhase::Services, "LogService ready"));
2377
2378            // TimerService (sub-second precision timers)
2379            let timer_svc = Arc::new(TimerService::new());
2380            boot_log.push(BootEvent::info(BootPhase::Services, "TimerService ready"));
2381
2382            // DeadLetterQueue (undeliverable message sink)
2383            let dlq = Arc::new(DeadLetterQueue::with_default_capacity());
2384            boot_log.push(BootEvent::info(
2385                BootPhase::Services,
2386                "DeadLetterQueue ready",
2387            ));
2388
2389            // Wire DLQ into the A2A router
2390            a2a_router.set_dead_letter_queue(Arc::clone(&dlq));
2391
2392            (Some(registry), Some(log_svc), Some(timer_svc), Some(dlq))
2393        };
2394
2395        let elapsed = boot_time.elapsed();
2396        boot_log.push(BootEvent::info(
2397            BootPhase::Ready,
2398            format!(
2399                "Boot complete in {:.1}s ({} processes, {} services)",
2400                elapsed.as_secs_f64(),
2401                process_table.len(),
2402                service_registry.len(),
2403            ),
2404        ));
2405
2406        info!(
2407            elapsed_ms = elapsed.as_millis(),
2408            processes = process_table.len(),
2409            services = service_registry.len(),
2410            "kernel boot complete"
2411        );
2412
2413        // 9. Create agent supervisor
2414        let supervisor = AgentSupervisor::new(
2415            process_table.clone(),
2416            ipc.clone(),
2417            AgentCapabilities::default(),
2418        );
2419
2420        // 9b. Wire A2ARouter and cron into supervisor
2421        #[cfg(feature = "native")]
2422        let supervisor = supervisor.with_a2a_router(a2a_router.clone(), cron_svc.clone());
2423
2424        // 9c. Wire exochain managers into supervisor
2425        #[cfg(feature = "exochain")]
2426        let supervisor = supervisor.with_exochain(tree_manager.clone(), chain_manager.clone());
2427
2428        // 10. Seed the event ring buffer with boot events
2429        let event_log = Arc::new(KernelEventLog::new());
2430        event_log.ingest_boot_log(&boot_log);
2431
2432        Ok(Self {
2433            state: KernelState::Running,
2434            config: kernel_config,
2435            app_context: Some(app_context),
2436            bus,
2437            process_table,
2438            service_registry,
2439            ipc,
2440            #[cfg(feature = "native")]
2441            a2a_router,
2442            #[cfg(feature = "native")]
2443            cron_service: cron_svc,
2444            #[cfg(feature = "native")]
2445            assessment_service: assessment_svc,
2446            health,
2447            supervisor,
2448            boot_log,
2449            event_log,
2450            boot_time,
2451            cluster_membership,
2452            app_manager,
2453            revocation_list,
2454            #[cfg(feature = "native")]
2455            agent_registry: crate::agent_registry::AgentRegistry::new(),
2456            #[cfg(feature = "native")]
2457            node_registry: crate::node_registry::NodeRegistry::new(),
2458            #[cfg(feature = "native")]
2459            substrate_service: crate::substrate_service::SubstrateService::new(),
2460            #[cfg(feature = "exochain")]
2461            chain: ChainSubsystem {
2462                chain_manager,
2463                tree_manager,
2464                governance_gate,
2465                chain_anchor,
2466                overlay: overlay_runtime,
2467                _chain_lock: chain_lock,
2468            },
2469            #[cfg(feature = "ecc")]
2470            ecc: EccSubsystem {
2471                hnsw: ecc_hnsw,
2472                causal: ecc_causal,
2473                tick: ecc_tick,
2474                crossrefs: ecc_crossrefs,
2475                impulses: ecc_impulses,
2476                calibration: ecc_calibration,
2477                vector_backend: ecc_vector_backend,
2478                eml_coherence: ecc_eml_model,
2479                spatial: ecc_spatial,
2480            },
2481            #[cfg(feature = "os-patterns")]
2482            observability: ObservabilitySubsystem {
2483                metrics_registry,
2484                log_service: log_svc,
2485                timer_service: timer_svc,
2486                dead_letter_queue,
2487            },
2488        })
2489    }
2490
2491    /// Shut down the kernel gracefully.
2492    ///
2493    /// Stops all services, cancels all processes, and transitions
2494    /// to the `Halted` state.
2495    pub async fn shutdown(&mut self) -> KernelResult<()> {
2496        if self.state != KernelState::Running {
2497            return Err(KernelError::WrongState {
2498                expected: "Running".into(),
2499                actual: self.state.to_string(),
2500            });
2501        }
2502
2503        info!("kernel shutting down");
2504        self.state = KernelState::ShuttingDown;
2505        self.event_log.info("kernel", "shutdown initiated");
2506
2507        // Stop all services
2508        if let Err(e) = self.service_registry.stop_all().await {
2509            error!(error = %e, "error stopping services during shutdown");
2510        }
2511
2512        // Checkpoint tree+chain before shutting down
2513        #[cfg(feature = "exochain")]
2514        if let Some(ref tm) = self.chain.tree_manager
2515            && let Some(ref cm) = self.chain.chain_manager
2516        {
2517            let stats = tm.stats();
2518            cm.append(
2519                "kernel",
2520                "shutdown",
2521                Some(serde_json::json!({
2522                    "tree_root_hash": stats.root_hash,
2523                    "chain_seq": cm.sequence(),
2524                    "tree_nodes": stats.node_count,
2525                })),
2526            );
2527        }
2528
2529        // Abort all running agent tasks
2530        #[cfg(feature = "native")]
2531        self.supervisor.abort_all();
2532
2533        // Cancel all processes
2534        for entry in self.process_table.list() {
2535            if entry.pid == 0 {
2536                continue; // Don't cancel the kernel process
2537            }
2538            entry.cancel_token.cancel();
2539
2540            // Log agent stop in tree/chain
2541            #[cfg(feature = "exochain")]
2542            if let Some(ref tm) = self.chain.tree_manager {
2543                let _ = tm.unregister_agent(&entry.agent_id, entry.pid, 0);
2544            }
2545
2546            let _ = self
2547                .process_table
2548                .update_state(entry.pid, ProcessState::Exited(0));
2549        }
2550
2551        // Persist chain to RVF checkpoint (primary), JSON as fallback
2552        #[cfg(feature = "exochain")]
2553        if let Some(ref cm) = self.chain.chain_manager {
2554            let chain_config = self.config.chain.clone().unwrap_or_default();
2555            if let Some(ref ckpt_path) = chain_config.effective_checkpoint_path() {
2556                let json_path = std::path::PathBuf::from(ckpt_path);
2557                let rvf_path = json_path.with_extension("rvf");
2558
2559                // Save RVF format (primary)
2560                match cm.save_to_rvf(&rvf_path) {
2561                    Ok(()) => info!(path = %rvf_path.display(), "chain saved to RVF checkpoint"),
2562                    Err(e) => {
2563                        error!(error = %e, "failed to save RVF checkpoint, falling back to JSON");
2564                        // Fallback: save JSON
2565                        match cm.save_to_file(&json_path) {
2566                            Ok(()) => {
2567                                info!(path = %json_path.display(), "chain saved to JSON checkpoint (fallback)")
2568                            }
2569                            Err(e2) => {
2570                                error!(error = %e2, "failed to save JSON checkpoint fallback")
2571                            }
2572                        }
2573                    }
2574                }
2575
2576                // Save tree checkpoint alongside chain
2577                if let Some(ref tm) = self.chain.tree_manager {
2578                    let tree_path = json_path.with_extension("tree.json");
2579                    match tm.save_checkpoint(&tree_path) {
2580                        Ok(()) => {
2581                            info!(path = %tree_path.display(), "tree checkpoint saved");
2582                            cm.append(
2583                                "tree",
2584                                "tree.checkpoint",
2585                                Some(serde_json::json!({
2586                                    "path": tree_path.display().to_string(),
2587                                    "root_hash": tm.stats().root_hash,
2588                                })),
2589                            );
2590                        }
2591                        Err(e) => error!(error = %e, "failed to save tree checkpoint"),
2592                    }
2593                }
2594            }
2595        }
2596
2597        self.state = KernelState::Halted;
2598        self.event_log.info("kernel", "halted");
2599        info!("kernel halted");
2600        Ok(())
2601    }
2602
2603    /// Get the current kernel state.
2604    pub fn state(&self) -> &KernelState {
2605        &self.state
2606    }
2607
2608    /// Get the kernel configuration.
2609    pub fn kernel_config(&self) -> &KernelConfig {
2610        &self.config
2611    }
2612
2613    /// Get the process table.
2614    pub fn process_table(&self) -> &Arc<ProcessTable> {
2615        &self.process_table
2616    }
2617
2618    /// Get the service registry.
2619    pub fn services(&self) -> &Arc<ServiceRegistry> {
2620        &self.service_registry
2621    }
2622
2623    /// Get the IPC subsystem.
2624    pub fn ipc(&self) -> &Arc<KernelIpc> {
2625        &self.ipc
2626    }
2627
2628    /// Get the message bus.
2629    pub fn bus(&self) -> &Arc<MessageBus> {
2630        &self.bus
2631    }
2632
2633    /// Get the A2A router.
2634    #[cfg(feature = "native")]
2635    pub fn a2a_router(&self) -> &Arc<A2ARouter> {
2636        &self.a2a_router
2637    }
2638
2639    /// Get the agent identity registry.
2640    ///
2641    /// External clients call `agent.register` to obtain an `agent_id`
2642    /// bound to an Ed25519 public key. The registry is then consulted
2643    /// when verifying signatures on `ipc.publish` and
2644    /// `ipc.subscribe_stream` requests.
2645    #[cfg(feature = "native")]
2646    pub fn agent_registry(&self) -> &crate::agent_registry::AgentRegistry {
2647        &self.agent_registry
2648    }
2649
2650    /// Get the node identity registry.
2651    ///
2652    /// Maps node-ids to their ed25519 public keys. The daemon
2653    /// registers its own node here at boot; remote nodes (ESP32
2654    /// leaves, future kernel-class peers) register via a future
2655    /// `node.register` RPC. Consulted by the substrate publish gate
2656    /// to verify node signatures and by the Explorer's tree to
2657    /// resolve labels.
2658    #[cfg(feature = "native")]
2659    pub fn node_registry(&self) -> &crate::node_registry::NodeRegistry {
2660        &self.node_registry
2661    }
2662
2663    /// Get the substrate RPC service.
2664    ///
2665    /// Backs the `substrate.read`, `substrate.publish`,
2666    /// `substrate.subscribe`, and `substrate.notify` RPCs.
2667    #[cfg(feature = "native")]
2668    pub fn substrate_service(&self) -> &crate::substrate_service::SubstrateService {
2669        &self.substrate_service
2670    }
2671
2672    /// Get the cron service.
2673    #[cfg(feature = "native")]
2674    pub fn cron_service(&self) -> &Arc<crate::cron::CronService> {
2675        &self.cron_service
2676    }
2677
2678    /// Get the assessment service.
2679    #[cfg(feature = "native")]
2680    pub fn assessment_service(&self) -> &Arc<crate::assessment::AssessmentService> {
2681        &self.assessment_service
2682    }
2683
2684    /// Get the health system.
2685    pub fn health(&self) -> &HealthSystem {
2686        &self.health
2687    }
2688
2689    /// Get the agent supervisor.
2690    pub fn supervisor(&self) -> &AgentSupervisor<P> {
2691        &self.supervisor
2692    }
2693
2694    /// Get the boot log.
2695    pub fn boot_log(&self) -> &BootLog {
2696        &self.boot_log
2697    }
2698
2699    /// Get the runtime event log (ring buffer).
2700    pub fn event_log(&self) -> &Arc<KernelEventLog> {
2701        &self.event_log
2702    }
2703
2704    /// Get kernel uptime.
2705    pub fn uptime(&self) -> std::time::Duration {
2706        self.boot_time.elapsed()
2707    }
2708
2709    /// Get the cluster membership tracker.
2710    pub fn cluster_membership(&self) -> &Arc<ClusterMembership> {
2711        &self.cluster_membership
2712    }
2713
2714    /// Get the application manager (installed apps rehydrated from disk).
2715    pub fn app_manager(&self) -> &Arc<AppManager> {
2716        &self.app_manager
2717    }
2718
2719    /// Get the host revocation list.
2720    pub fn revocation_list(&self) -> &Arc<crate::revocation::RevocationList> {
2721        &self.revocation_list
2722    }
2723
2724    /// Get the local chain manager (when exochain feature is enabled).
2725    #[cfg(feature = "exochain")]
2726    pub fn chain_manager(&self) -> Option<&Arc<crate::chain::ChainManager>> {
2727        self.chain.chain_manager.as_ref()
2728    }
2729
2730    /// The project governance runtime (profile `project` only).
2731    #[cfg(feature = "exochain")]
2732    pub fn governance_overlay(&self) -> Option<&Arc<crate::overlay_runtime::OverlayRuntime>> {
2733        self.chain.overlay.as_ref()
2734    }
2735
2736    /// Get the tree manager (when exochain feature is enabled).
2737    #[cfg(feature = "exochain")]
2738    pub fn tree_manager(&self) -> Option<&Arc<crate::tree_manager::TreeManager>> {
2739        self.chain.tree_manager.as_ref()
2740    }
2741
2742    /// Get the governance gate backend (if configured).
2743    #[cfg(feature = "exochain")]
2744    pub fn governance_gate(&self) -> Option<&Arc<dyn crate::gate::GateBackend>> {
2745        self.chain.governance_gate.as_ref()
2746    }
2747
2748    /// Get the external chain-head anchoring controller (WEFT-137), when configured.
2749    #[cfg(feature = "exochain")]
2750    pub fn chain_anchor(&self) -> Option<&Arc<crate::chain_anchor::AnchoringController>> {
2751        self.chain.chain_anchor.as_ref()
2752    }
2753
2754    /// Get the ECC HNSW service (if ecc feature enabled).
2755    #[cfg(feature = "ecc")]
2756    pub fn ecc_hnsw(&self) -> Option<&Arc<crate::hnsw_service::HnswService>> {
2757        self.ecc.hnsw.as_ref()
2758    }
2759
2760    /// Get the ECC vector backend (if ecc feature enabled).
2761    ///
2762    /// Returns the configured vector search backend (HNSW, DiskANN,
2763    /// or Hybrid) based on the `[kernel.vector]` configuration.
2764    #[cfg(feature = "ecc")]
2765    pub fn ecc_vector_backend(&self) -> Option<&Arc<dyn crate::vector_backend::VectorBackend>> {
2766        self.ecc.vector_backend.as_ref()
2767    }
2768
2769    /// Get the ECC spatial BVH service (if enabled via `[kernel.spatial]`).
2770    #[cfg(feature = "ecc")]
2771    pub fn ecc_spatial(&self) -> Option<&Arc<crate::spatial_service::SpatialService>> {
2772        self.ecc.spatial.as_ref()
2773    }
2774
2775    /// Get the ECC causal graph (if ecc feature enabled).
2776    #[cfg(feature = "ecc")]
2777    pub fn ecc_causal(&self) -> Option<&Arc<crate::causal::CausalGraph>> {
2778        self.ecc.causal.as_ref()
2779    }
2780
2781    /// Get the ECC cognitive tick (if ecc feature enabled).
2782    #[cfg(feature = "ecc")]
2783    pub fn ecc_tick(&self) -> Option<&Arc<crate::cognitive_tick::CognitiveTick>> {
2784        self.ecc.tick.as_ref()
2785    }
2786
2787    /// Get the ECC calibration results (if ecc feature enabled).
2788    #[cfg(feature = "ecc")]
2789    pub fn ecc_calibration(&self) -> Option<&crate::calibration::EccCalibration> {
2790        self.ecc.calibration.as_ref()
2791    }
2792
2793    /// Get the ECC cross-reference store (if ecc feature enabled).
2794    #[cfg(feature = "ecc")]
2795    pub fn ecc_crossrefs(&self) -> Option<&Arc<crate::crossref::CrossRefStore>> {
2796        self.ecc.crossrefs.as_ref()
2797    }
2798
2799    /// Get the ECC impulse queue (if ecc feature enabled).
2800    #[cfg(feature = "ecc")]
2801    pub fn ecc_impulses(&self) -> Option<&Arc<crate::impulse::ImpulseQueue>> {
2802        self.ecc.impulses.as_ref()
2803    }
2804
2805    /// Get the EML coherence model (if ecc feature enabled).
2806    #[cfg(feature = "ecc")]
2807    pub fn ecc_eml_coherence(
2808        &self,
2809    ) -> Option<&Arc<std::sync::Mutex<crate::eml_coherence::EmlCoherenceModel>>> {
2810        self.ecc.eml_coherence.as_ref()
2811    }
2812
2813    /// Get the os-patterns metrics registry (if os-patterns feature enabled).
2814    #[cfg(feature = "os-patterns")]
2815    pub fn metrics_registry(&self) -> Option<&Arc<crate::metrics::MetricsRegistry>> {
2816        self.observability.metrics_registry.as_ref()
2817    }
2818
2819    /// Get the os-patterns log service (if os-patterns feature enabled).
2820    #[cfg(feature = "os-patterns")]
2821    pub fn log_service(&self) -> Option<&Arc<crate::log_service::LogService>> {
2822        self.observability.log_service.as_ref()
2823    }
2824
2825    /// Get the os-patterns timer service (if os-patterns feature enabled).
2826    #[cfg(feature = "os-patterns")]
2827    pub fn timer_service(&self) -> Option<&Arc<crate::timer::TimerService>> {
2828        self.observability.timer_service.as_ref()
2829    }
2830
2831    /// Get the os-patterns dead letter queue (if os-patterns feature enabled).
2832    #[cfg(feature = "os-patterns")]
2833    pub fn dead_letter_queue(&self) -> Option<&Arc<crate::dead_letter::DeadLetterQueue>> {
2834        self.observability.dead_letter_queue.as_ref()
2835    }
2836
2837    /// Take ownership of the AppContext for agent loop consumption.
2838    ///
2839    /// This is a one-shot operation: after calling this, the kernel
2840    /// no longer holds the AppContext. Use this before calling
2841    /// `AppContext::into_agent_loop()`.
2842    pub fn take_app_context(&mut self) -> Option<AppContext<P>> {
2843        self.app_context.take()
2844    }
2845}
2846
2847/// Boot refusal text for an existing chain file that failed to restore.
2848///
2849/// A chain that fails verification (corruption, or events written by a
2850/// newer binary, e.g. carrying a `rule_hash` this build cannot verify) must
2851/// never be replaced by a fresh genesis: the next checkpoint save would
2852/// overwrite the only copy. The operator moves it aside explicitly
2853/// (`--new-chain` picks a different location and does not touch this file).
2854#[cfg(feature = "exochain")]
2855fn unrestorable_chain_msg(path: &std::path::Path, cause: &str) -> String {
2856    format!(
2857        "chain at {} failed to restore ({cause}); refusing to start a fresh chain over it. \
2858         Move or rename {} (and its .rvf/.json sibling) aside, then restart; \
2859         if a newer binary wrote it, upgrade",
2860        path.display(),
2861        path.display()
2862    )
2863}
2864
2865#[cfg(test)]
2866mod tests {
2867    use super::*;
2868    use clawft_platform::NativePlatform;
2869    use clawft_types::config::{AgentDefaults, AgentsConfig};
2870
2871    fn test_config() -> Config {
2872        Config {
2873            agents: AgentsConfig {
2874                defaults: AgentDefaults {
2875                    workspace: "~/.clawft/workspace".into(),
2876                    model: "test/model".into(),
2877                    max_tokens: 1024,
2878                    temperature: 0.5,
2879                    max_tool_iterations: 5,
2880                    memory_window: 10,
2881                },
2882                ..AgentsConfig::default()
2883            },
2884            ..Config::default()
2885        }
2886    }
2887
2888    fn test_kernel_config() -> KernelConfig {
2889        KernelConfig {
2890            governance: Default::default(),
2891            profile: None,
2892            shared_services: Default::default(),
2893            enabled: true,
2894            max_processes: 16,
2895            health_check_interval_secs: 5,
2896            cluster: None,
2897            chain: None,
2898            resource_tree: None,
2899            vector: None,
2900            spatial: None,
2901            profiles: None,
2902            pairing: None,
2903            mesh: None,
2904            anchor: None,
2905            ipc_tcp: None,
2906            llm: None,
2907            agent: None,
2908            brand: ::clawft_types::config::DEFAULT_BRAND.to_string(),
2909        }
2910    }
2911
2912    /// WEFT-119: mesh.enabled registers `mesh` SystemService and starts it.
2913    #[cfg(all(feature = "native", feature = "mesh"))]
2914    #[tokio::test]
2915    async fn boot_registers_mesh_system_service() {
2916        use clawft_types::config::MeshConfig;
2917        use crate::health::HealthStatus;
2918
2919        let platform = Arc::new(NativePlatform::new());
2920        let mut kconfig = test_kernel_config();
2921        kconfig.mesh = Some(MeshConfig {
2922            enabled: true,
2923            transport: "tcp".into(),
2924            // Ephemeral port so parallel tests do not clash.
2925            listen_addr: "127.0.0.1:0".into(),
2926            discovery: false,
2927            seed_peers: vec![],
2928            noise: false,
2929            noise_key_path: None,
2930            ..MeshConfig::default()
2931        });
2932
2933        let mut kernel = Kernel::boot(test_config(), kconfig, platform)
2934            .await
2935            .expect("boot with mesh");
2936
2937        let mesh_svc = kernel
2938            .services()
2939            .get("mesh")
2940            .expect("mesh SystemService should be registered when mesh.enabled");
2941        assert_eq!(mesh_svc.name(), "mesh");
2942        assert_eq!(
2943            mesh_svc.health_check().await,
2944            HealthStatus::Healthy,
2945            "start_all should have started mesh"
2946        );
2947
2948        kernel.shutdown().await.unwrap();
2949        // After stop_all, mesh reports not started.
2950        assert!(
2951            matches!(
2952                mesh_svc.health_check().await,
2953                HealthStatus::Unhealthy(_)
2954            ),
2955            "mesh should be unhealthy after shutdown stop"
2956        );
2957    }
2958
2959    /// ADR-103 D2: enabled mesh + unbindable listener fails boot, naming the address.
2960    #[cfg(all(feature = "native", feature = "mesh"))]
2961    #[tokio::test]
2962    async fn boot_fails_when_mesh_listener_cannot_bind() {
2963        use clawft_types::config::MeshConfig;
2964
2965        let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2966        let addr = taken.local_addr().unwrap().to_string();
2967
2968        let mut kconfig = test_kernel_config();
2969        kconfig.mesh = Some(MeshConfig {
2970            enabled: true,
2971            listen_addr: addr.clone(),
2972            ..MeshConfig::default()
2973        });
2974        let err = Kernel::boot(test_config(), kconfig, Arc::new(NativePlatform::new()))
2975            .await
2976            .err()
2977            .expect("boot must fail when the mesh port is taken");
2978        let msg = err.to_string();
2979        assert!(msg.contains(&addr), "error should name the address: {msg}");
2980        assert!(msg.contains("another kernel"), "error should name the cause: {msg}");
2981    }
2982
2983    /// P3-U: a symlinked or loose `~/.weftos/user.key` is refused as the chain
2984    /// signing key instead of being followed.
2985    #[cfg(all(feature = "native", feature = "exochain", unix))]
2986    #[tokio::test]
2987    async fn user_key_symlink_or_loose_mode_is_refused_at_boot() {
2988        use clawft_types::config::ChainConfig;
2989        use std::os::unix::fs::PermissionsExt;
2990
2991        let home = tempfile::tempdir().unwrap();
2992        let weftos = home.path().join(".weftos");
2993        let chain_dir = weftos.join("chain");
2994        std::fs::create_dir_all(&chain_dir).unwrap();
2995        let mk = |ckpt: &std::path::Path| {
2996            let mut kc = test_kernel_config();
2997            kc.chain = Some(ChainConfig {
2998                enabled: true,
2999                checkpoint_path: Some(ckpt.display().to_string()),
3000                ..ChainConfig::default()
3001            });
3002            kc
3003        };
3004        let ckpt = chain_dir.join("chain.json");
3005
3006        // Loose permissions.
3007        let user_key = weftos.join("user.key");
3008        std::fs::write(&user_key, [3u8; 32]).unwrap();
3009        std::fs::set_permissions(&user_key, std::fs::Permissions::from_mode(0o644)).unwrap();
3010        let err = Kernel::boot(test_config(), mk(&ckpt), Arc::new(NativePlatform::new()))
3011            .await
3012            .err()
3013            .expect("loose user.key must refuse boot");
3014        assert!(err.to_string().contains("user.key"), "{err}");
3015
3016        // A symlink, even to a private file.
3017        std::fs::remove_file(&user_key).unwrap();
3018        let target = home.path().join("elsewhere.key");
3019        std::fs::write(&target, [3u8; 32]).unwrap();
3020        std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
3021        std::os::unix::fs::symlink(&target, &user_key).unwrap();
3022        let err = Kernel::boot(test_config(), mk(&ckpt), Arc::new(NativePlatform::new()))
3023            .await
3024            .err()
3025            .expect("symlinked user.key must refuse boot");
3026        assert!(err.to_string().contains("user.key"), "{err}");
3027    }
3028
3029    /// P3-U: service mode takes the service's node id, binds no listener (the
3030    /// taken port is not touched) and starts no mesh runtime.
3031    #[cfg(all(feature = "native", feature = "mesh"))]
3032    #[tokio::test]
3033    async fn service_mode_uses_the_service_node_id_and_binds_nothing() {
3034        use clawft_types::config::MeshConfig;
3035
3036        let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
3037        let addr = taken.local_addr().unwrap().to_string();
3038        let service_id = crate::node_id::node_id_from_pubkey(&[42u8; 32]);
3039
3040        let mut kconfig = test_kernel_config();
3041        kconfig.mesh = Some(MeshConfig {
3042            enabled: true,
3043            listen_addr: addr,
3044            ..MeshConfig::default()
3045        });
3046        let mut kernel = Kernel::boot_in_service_mode(
3047            test_config(),
3048            kconfig,
3049            Arc::new(NativePlatform::new()),
3050            service_id.clone(),
3051        )
3052        .await
3053        .expect("service mode must not try to bind the taken port");
3054        assert_eq!(kernel.cluster_membership().local_node_id(), service_id);
3055        assert!(kernel.services().get("mesh").is_none(), "no in-kernel mesh service");
3056        kernel.shutdown().await.unwrap();
3057    }
3058
3059    /// P3-U: a malformed service node id fails the boot; there is no
3060    /// ephemeral-identity fallback.
3061    #[cfg(all(feature = "native", feature = "mesh"))]
3062    #[tokio::test]
3063    async fn service_mode_refuses_a_malformed_node_id() {
3064        let err = Kernel::boot_in_service_mode(
3065            test_config(),
3066            test_kernel_config(),
3067            Arc::new(NativePlatform::new()),
3068            "nope".into(),
3069        )
3070        .await
3071        .err()
3072        .expect("must fail");
3073        assert!(err.to_string().contains("node id"), "{err}");
3074    }
3075
3076    /// P3-U (R2): `kernel.mesh.service = required` fails any boot path that
3077    /// did not link a service, instead of running its own listener.
3078    #[cfg(all(feature = "native", feature = "mesh"))]
3079    #[tokio::test]
3080    async fn required_service_fails_a_boot_without_a_link() {
3081        use clawft_types::config::{MeshConfig, MeshServicePolicy};
3082
3083        let mut kconfig = test_kernel_config();
3084        kconfig.mesh = Some(MeshConfig {
3085            enabled: true,
3086            listen_addr: "127.0.0.1:0".into(),
3087            service: MeshServicePolicy::Required,
3088            ..MeshConfig::default()
3089        });
3090        let err = Kernel::boot(test_config(), kconfig, Arc::new(NativePlatform::new()))
3091            .await
3092            .err()
3093            .expect("required without a service link must fail");
3094        assert!(err.to_string().contains("required"), "{err}");
3095    }
3096
3097    /// Mesh off means no bind and no error, even if the address is taken.
3098    #[cfg(all(feature = "native", feature = "mesh"))]
3099    #[tokio::test]
3100    async fn boot_with_mesh_disabled_never_binds() {
3101        use clawft_types::config::MeshConfig;
3102
3103        let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
3104        let mut kconfig = test_kernel_config();
3105        kconfig.mesh = Some(MeshConfig {
3106            enabled: false,
3107            listen_addr: taken.local_addr().unwrap().to_string(),
3108            ..MeshConfig::default()
3109        });
3110        let mut kernel = Kernel::boot(test_config(), kconfig, Arc::new(NativePlatform::new()))
3111            .await
3112            .expect("mesh-off boot must not touch the listener");
3113        kernel.shutdown().await.unwrap();
3114    }
3115
3116    /// The node id comes from the node key: same key, same id across boots.
3117    #[cfg(all(feature = "native", feature = "mesh"))]
3118    #[tokio::test]
3119    async fn node_id_derives_from_node_key_and_survives_reboot() {
3120        let seed = [9u8; 32];
3121        let expected = crate::node_id::node_id_from_pubkey(
3122            &ed25519_dalek::SigningKey::from_bytes(&seed)
3123                .verifying_key()
3124                .to_bytes(),
3125        );
3126        for _ in 0..2 {
3127            let mut kernel = Kernel::boot_with_node_key(
3128                test_config(),
3129                test_kernel_config(),
3130                Arc::new(NativePlatform::new()),
3131                Some(seed),
3132            )
3133            .await
3134            .expect("boot with node key");
3135            assert_eq!(kernel.cluster_membership().local_node_id(), expected);
3136            kernel.shutdown().await.unwrap();
3137        }
3138    }
3139
3140    /// The mesh runtime id (handshake) equals the cluster membership id.
3141    #[cfg(all(feature = "native", feature = "mesh"))]
3142    #[tokio::test]
3143    async fn mesh_node_id_equals_cluster_node_id() {
3144        use clawft_types::config::MeshConfig;
3145
3146        let mut kconfig = test_kernel_config();
3147        kconfig.mesh = Some(MeshConfig {
3148            enabled: true,
3149            listen_addr: "127.0.0.1:0".into(),
3150            ..MeshConfig::default()
3151        });
3152        let mut kernel = Kernel::boot_with_node_key(
3153            test_config(),
3154            kconfig,
3155            Arc::new(NativePlatform::new()),
3156            Some([3u8; 32]),
3157        )
3158        .await
3159        .expect("boot with mesh");
3160        let mesh_id = kernel
3161            .a2a_router()
3162            .mesh_runtime()
3163            .expect("mesh runtime attached")
3164            .node_id()
3165            .to_owned();
3166        assert_eq!(mesh_id, kernel.cluster_membership().local_node_id());
3167        assert!(crate::node_id::is_node_id(&mesh_id));
3168        kernel.shutdown().await.unwrap();
3169    }
3170
3171    #[tokio::test]
3172    async fn boot_and_shutdown() {
3173        let platform = Arc::new(NativePlatform::new());
3174        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3175            .await
3176            .unwrap();
3177
3178        assert_eq!(*kernel.state(), KernelState::Running);
3179        // Uptime should be non-negative (boot_time is set before boot completes)
3180        let _uptime = kernel.uptime();
3181
3182        // Kernel process should be PID 0
3183        let kernel_proc = kernel.process_table().get(0).unwrap();
3184        assert_eq!(kernel_proc.agent_id, "kernel");
3185        assert_eq!(kernel_proc.state, ProcessState::Running);
3186
3187        kernel.shutdown().await.unwrap();
3188        assert_eq!(*kernel.state(), KernelState::Halted);
3189    }
3190
3191    #[tokio::test]
3192    async fn double_shutdown_fails() {
3193        let platform = Arc::new(NativePlatform::new());
3194        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3195            .await
3196            .unwrap();
3197
3198        kernel.shutdown().await.unwrap();
3199        let result = kernel.shutdown().await;
3200        assert!(result.is_err());
3201    }
3202
3203    #[tokio::test]
3204    async fn boot_log_has_events() {
3205        let platform = Arc::new(NativePlatform::new());
3206        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3207            .await
3208            .unwrap();
3209
3210        let log = kernel.boot_log();
3211        assert!(!log.is_empty());
3212
3213        let formatted = log.format_all();
3214        assert!(formatted.contains(&format!("WeftOS v{}", env!("CARGO_PKG_VERSION"))));
3215        assert!(formatted.contains("Boot complete"));
3216    }
3217
3218    #[tokio::test]
3219    async fn process_table_accessible() {
3220        let platform = Arc::new(NativePlatform::new());
3221        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3222            .await
3223            .unwrap();
3224
3225        let pt = kernel.process_table();
3226        assert_eq!(pt.len(), 1); // kernel process only
3227        assert_eq!(pt.max_processes(), 16);
3228    }
3229
3230    #[tokio::test]
3231    async fn services_accessible() {
3232        let platform = Arc::new(NativePlatform::new());
3233        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3234            .await
3235            .unwrap();
3236
3237        // Base: cron + containers; +cluster; +hnsw +cognitive_tick (ecc); +assess (native)
3238        let count = kernel.services().len();
3239        #[cfg(all(feature = "ecc", feature = "cluster"))]
3240        assert_eq!(
3241            count, 6,
3242            "expected cron+containers+assess+cluster+hnsw+cognitive_tick"
3243        );
3244        #[cfg(all(feature = "ecc", not(feature = "cluster")))]
3245        assert_eq!(
3246            count, 5,
3247            "expected cron+containers+assess+hnsw+cognitive_tick"
3248        );
3249        #[cfg(all(not(feature = "ecc"), feature = "cluster"))]
3250        assert_eq!(count, 4, "expected cron+containers+assess+cluster");
3251        #[cfg(all(not(feature = "ecc"), not(feature = "cluster")))]
3252        assert_eq!(count, 3, "expected cron+containers+assess");
3253    }
3254
3255    #[tokio::test]
3256    async fn take_app_context() {
3257        let platform = Arc::new(NativePlatform::new());
3258        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3259            .await
3260            .unwrap();
3261
3262        let ctx = kernel.take_app_context();
3263        assert!(ctx.is_some());
3264
3265        // Second take returns None
3266        let ctx2 = kernel.take_app_context();
3267        assert!(ctx2.is_none());
3268    }
3269
3270    #[tokio::test]
3271    async fn ipc_accessible() {
3272        let platform = Arc::new(NativePlatform::new());
3273        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3274            .await
3275            .unwrap();
3276
3277        let ipc = kernel.ipc();
3278        assert!(Arc::ptr_eq(ipc.bus(), kernel.bus()));
3279    }
3280
3281    #[test]
3282    fn kernel_state_display() {
3283        assert_eq!(KernelState::Booting.to_string(), "booting");
3284        assert_eq!(KernelState::Running.to_string(), "running");
3285        assert_eq!(KernelState::ShuttingDown.to_string(), "shutting_down");
3286        assert_eq!(KernelState::Halted.to_string(), "halted");
3287    }
3288
3289    // ── Full-stack integration helpers ─────────────────────────────
3290
3291    /// Kernel config with exochain + resource tree enabled. No explicit
3292    /// checkpoint path: `chain_storage` pins a fresh temp dir per boot in
3293    /// unit tests, so the operator chain is never touched.
3294    #[cfg(all(feature = "exochain", feature = "ecc", feature = "wasm-sandbox"))]
3295    fn test_kernel_config_full_stack() -> KernelConfig {
3296        use clawft_types::config::{ChainConfig, ResourceTreeConfig};
3297        KernelConfig {
3298            enabled: true,
3299            max_processes: 32,
3300            health_check_interval_secs: 5,
3301            cluster: None,
3302            chain: Some(ChainConfig {
3303                enabled: true,
3304                checkpoint_interval: 10_000,
3305                chain_id: 0,
3306                checkpoint_path: None,
3307                external_anchor: None,
3308            }),
3309            resource_tree: Some(ResourceTreeConfig {
3310                enabled: true,
3311                checkpoint_path: None,
3312            }),
3313            vector: None,
3314            spatial: None,
3315            profiles: None,
3316            pairing: None,
3317            mesh: None,
3318            anchor: None,
3319            ipc_tcp: None,
3320            llm: None,
3321            agent: None,
3322            brand: ::clawft_types::config::DEFAULT_BRAND.to_string(),
3323            ..KernelConfig::default()
3324        }
3325    }
3326
3327    // ── Integration: full-stack kernel ──────────────────────────────
3328
3329    /// K0-K5 integration: native agent + WASM tool + container service + ECC
3330    /// all connected in a single kernel with chain witnessing.
3331    #[tokio::test]
3332    #[cfg(all(feature = "exochain", feature = "ecc", feature = "wasm-sandbox"))]
3333    async fn integration_full_stack_kernel() {
3334        let platform = Arc::new(NativePlatform::new());
3335        let kernel = Kernel::boot(test_config(), test_kernel_config_full_stack(), platform)
3336            .await
3337            .unwrap();
3338
3339        // ── K0: Kernel is running ──────────────────────────────────
3340        assert_eq!(*kernel.state(), KernelState::Running);
3341
3342        // ── K0: Services registered at boot ────────────────────────
3343        // cron + container + hnsw + cognitive_tick = 4 with ecc
3344        let svc_count = kernel.services().len();
3345        assert!(svc_count >= 4, "expected >= 4 services, got {svc_count}");
3346
3347        // ── K1: Spawn a native agent ───────────────────────────────
3348        let spawn_result = kernel.supervisor().spawn(crate::supervisor::SpawnRequest {
3349            agent_id: "integration-agent-1".into(),
3350            capabilities: None,
3351            parent_pid: None,
3352            env: std::collections::HashMap::new(),
3353            backend: None, // defaults to Native
3354        });
3355        assert!(
3356            spawn_result.is_ok(),
3357            "native agent spawn failed: {:?}",
3358            spawn_result.err()
3359        );
3360        let agent_pid = spawn_result.unwrap().pid;
3361        assert!(agent_pid > 0, "agent should get PID > 0");
3362
3363        // ── K1: Agent appears in process table ─────────────────────
3364        let processes = kernel.process_table().list();
3365        assert!(
3366            processes.iter().any(|p| p.pid == agent_pid),
3367            "agent not in process table"
3368        );
3369
3370        // ── K2: A2A messaging between kernel and agent ─────────────
3371        let a2a = kernel.a2a_router();
3372        let _inbox = a2a.create_inbox(agent_pid);
3373
3374        // Send a message from kernel (PID 0) to agent
3375        let msg = crate::ipc::KernelMessage::new(
3376            0, // from kernel
3377            crate::ipc::MessageTarget::Process(agent_pid),
3378            crate::ipc::MessagePayload::Json(serde_json::json!({"cmd": "ping"})),
3379        );
3380        let send_result = a2a.send(msg).await;
3381        // This should succeed: PID 0 exists, agent inbox exists
3382        assert!(
3383            send_result.is_ok(),
3384            "A2A send failed: {:?}",
3385            send_result.err()
3386        );
3387
3388        // ── K3: WASM tool execution ────────────────────────────────
3389        let wasm_config = crate::wasm_runner::WasmSandboxConfig::default();
3390        let wasm_runner = crate::wasm_runner::WasmToolRunner::new(wasm_config);
3391
3392        // Minimal WAT module that exports _start and immediately returns
3393        let noop_wat = r#"(module (func (export "_start")))"#;
3394        let result = wasm_runner
3395            .execute_bytes(
3396                "integration-tool",
3397                noop_wat.as_bytes(),
3398                serde_json::json!({}),
3399            )
3400            .await;
3401        assert!(result.is_ok(), "WASM execution failed: {:?}", result.err());
3402        let wasm_result = result.unwrap();
3403        assert_eq!(wasm_result.exit_code, 0, "WASM tool should exit cleanly");
3404        assert!(wasm_result.fuel_consumed > 0, "WASM should consume fuel");
3405
3406        // ── K3: WASM tool in registry ──────────────────────────────
3407        let wasm_runner_arc = Arc::new(crate::wasm_runner::WasmToolRunner::new(
3408            crate::wasm_runner::WasmSandboxConfig::default(),
3409        ));
3410        let mut registry = crate::wasm_runner::ToolRegistry::new();
3411        registry
3412            .register_wasm_tool(
3413                "demo-tool",
3414                "A demo WASM tool for integration testing",
3415                noop_wat.as_bytes().to_vec(),
3416                wasm_runner_arc,
3417            )
3418            .unwrap();
3419        assert!(
3420            registry.get("demo-tool").is_some(),
3421            "WASM tool should be in registry"
3422        );
3423        let tool_list = registry.list();
3424        assert!(
3425            tool_list.contains(&"demo-tool".to_string()),
3426            "WASM tool should appear in listing"
3427        );
3428
3429        // ── K4: Container service registered ───────────────────────
3430        let svc_list = kernel.services().list();
3431        assert!(
3432            svc_list.iter().any(|(name, _)| name == "containers"),
3433            "container service should be registered"
3434        );
3435
3436        // ── K3c: ECC cognitive substrate active ────────────────────
3437        let ecc_hnsw = kernel.ecc_hnsw().expect("HNSW service should be present");
3438        // After calibration the store is cleared, so it should be empty
3439        assert_eq!(
3440            ecc_hnsw.len(),
3441            0,
3442            "HNSW should be empty after calibration cleanup"
3443        );
3444
3445        let ecc_causal = kernel.ecc_causal().expect("causal graph should be present");
3446        assert_eq!(
3447            ecc_causal.node_count(),
3448            0,
3449            "causal graph should be empty after cleanup"
3450        );
3451
3452        let _ecc_tick = kernel.ecc_tick().expect("cognitive tick should be present");
3453
3454        let calibration = kernel.ecc_calibration().expect("calibration should exist");
3455        assert!(
3456            calibration.compute_p95_us > 0,
3457            "calibration should have run"
3458        );
3459        assert!(
3460            calibration.tick_interval_ms > 0,
3461            "tick interval should be auto-calibrated"
3462        );
3463
3464        // ── ECC: Insert a vector and search ────────────────────────
3465        ecc_hnsw.insert(
3466            "test-doc".into(),
3467            vec![1.0, 0.0, 0.0, 0.0],
3468            serde_json::json!({"text": "hello"}),
3469        );
3470        assert_eq!(ecc_hnsw.len(), 1);
3471        let results = ecc_hnsw.search(&[1.0, 0.0, 0.0, 0.0], 5);
3472        assert_eq!(results.len(), 1);
3473        assert_eq!(results[0].id, "test-doc");
3474
3475        // ── ECC: Causal graph operations ───────────────────────────
3476        let node1 = ecc_causal.add_node("concept-A".into(), serde_json::json!({}));
3477        let node2 = ecc_causal.add_node("concept-B".into(), serde_json::json!({}));
3478        ecc_causal.link(
3479            node1,
3480            node2,
3481            crate::causal::CausalEdgeType::Causes,
3482            1.0,
3483            0,
3484            0,
3485        );
3486        assert_eq!(ecc_causal.node_count(), 2);
3487        assert_eq!(ecc_causal.edge_count(), 1);
3488        let path = ecc_causal.find_path(node1, node2, 5);
3489        assert!(path.is_some(), "should find path between linked nodes");
3490
3491        // ── ExoChain: Chain has witnessed boot events ──────────────
3492        let chain = kernel.chain_manager().expect("chain should exist");
3493        assert!(
3494            chain.sequence() > 5,
3495            "chain should have boot events, got seq={}",
3496            chain.sequence()
3497        );
3498
3499        // Verify chain integrity
3500        let verify_result = chain.verify_integrity();
3501        assert!(
3502            verify_result.valid,
3503            "chain should be valid: {:?}",
3504            verify_result.errors
3505        );
3506        assert!(verify_result.errors.is_empty(), "no integrity errors");
3507
3508        // ── Governance: genesis rules anchored to chain ────────────
3509        let _governance = kernel
3510            .governance_gate()
3511            .expect("governance gate should exist");
3512        let all_events = chain.tail(0);
3513        let genesis_events: Vec<_> = all_events
3514            .iter()
3515            .filter(|e| e.kind == "governance.genesis")
3516            .collect();
3517        assert!(
3518            !genesis_events.is_empty(),
3519            "governance genesis should be on chain"
3520        );
3521
3522        // Verify the genesis payload contains the correct rule count
3523        // Find the kernel's own genesis event (version 2.0.0)
3524        let genesis_payload = genesis_events
3525            .iter()
3526            .filter_map(|e| e.payload.as_ref())
3527            .find(|p| p.get("version").and_then(|v| v.as_str()) == Some("2.0.0"))
3528            .expect("should find v2.0.0 governance genesis on chain");
3529        assert_eq!(
3530            genesis_payload["rule_count"].as_u64().unwrap(),
3531            26,
3532            "genesis should contain 26 rules (22 constitutional/SOP + 3 browser_policy + 1 binding_thread)"
3533        );
3534        assert_eq!(
3535            genesis_payload["version"].as_str().unwrap(),
3536            "2.0.0",
3537            "genesis version should be 2.0.0"
3538        );
3539
3540        // Each rule should be individually anchored (at least 26)
3541        let rule_events: Vec<_> = all_events
3542            .iter()
3543            .filter(|e| e.kind == "governance.rule")
3544            .collect();
3545        assert!(
3546            rule_events.len() >= 26,
3547            "at least 26 genesis rules should be individually anchored, got {}",
3548            rule_events.len(),
3549        );
3550
3551        // Verify all rule IDs are present (GOV + SOP + BP + binding_thread)
3552        let rule_ids: Vec<&str> = rule_events
3553            .iter()
3554            .filter_map(|e| e.payload.as_ref()?.get("rule_id")?.as_str())
3555            .collect();
3556        for expected_id in &[
3557            "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006", "GOV-007",
3558            "SOP-L001", "SOP-L002", "SOP-L003", "SOP-L004", "SOP-L005", "SOP-L006", "SOP-E001",
3559            "SOP-E002", "SOP-E003", "SOP-E004", "SOP-E005", "SOP-J001", "SOP-J002", "SOP-J003",
3560            "SOP-J004", "BP-001", "BP-002", "BP-003", "soul.binding_thread_intact",
3561        ] {
3562            assert!(
3563                rule_ids.contains(expected_id),
3564                "{expected_id} should be anchored on chain"
3565            );
3566        }
3567
3568        // ── ExoChain: ECC calibration event logged ─────────────────
3569        let ecc_events: Vec<_> = all_events
3570            .iter()
3571            .filter(|e| e.kind.starts_with("ecc."))
3572            .collect();
3573        assert!(
3574            !ecc_events.is_empty(),
3575            "ECC boot calibration should be logged to chain"
3576        );
3577
3578        // ── Resource Tree: namespaces exist ────────────────────────
3579        let tree = kernel.tree_manager().expect("tree should exist");
3580        let stats = tree.stats();
3581        assert!(
3582            stats.node_count > 20,
3583            "tree should have many nodes (got {})",
3584            stats.node_count
3585        );
3586
3587        // ── K2: ServiceApi + adapters compile ──────────────────────
3588        // (validated by service::tests; here we just confirm registry is queryable)
3589        assert!(
3590            kernel.services().get("cron").is_some(),
3591            "cron service should be accessible"
3592        );
3593        assert!(
3594            kernel.services().get("containers").is_some(),
3595            "container service should be accessible"
3596        );
3597
3598        // ── Graceful shutdown ──────────────────────────────────────
3599        let mut kernel = kernel;
3600        kernel.shutdown().await.unwrap();
3601        assert_eq!(*kernel.state(), KernelState::Halted);
3602    }
3603
3604    // ── Integration: cross-backend tools ───────────────────────────
3605
3606    /// Demonstrates that native (built-in) tools and WASM tools coexist
3607    /// in a single ToolRegistry and can be dispatched through the same
3608    /// lookup interface.
3609    #[test]
3610    #[cfg(feature = "wasm-sandbox")]
3611    fn integration_cross_backend_tools() {
3612        use crate::governance::EffectVector;
3613        use crate::wasm_runner::{
3614            BuiltinTool, BuiltinToolSpec, ToolCategory, ToolError, ToolRegistry, WasmSandboxConfig,
3615            WasmToolRunner,
3616        };
3617
3618        // ── A native (Rust) tool ───────────────────────────────────
3619        struct EchoTool;
3620
3621        impl BuiltinTool for EchoTool {
3622            fn name(&self) -> &str {
3623                "native.echo"
3624            }
3625            fn spec(&self) -> &BuiltinToolSpec {
3626                // Leak a static spec for test simplicity
3627                static SPEC: std::sync::OnceLock<BuiltinToolSpec> = std::sync::OnceLock::new();
3628                SPEC.get_or_init(|| BuiltinToolSpec {
3629                    name: "native.echo".into(),
3630                    category: ToolCategory::System,
3631                    description: "Echoes input back".into(),
3632                    parameters: serde_json::json!({}),
3633                    gate_action: "tool.native.echo".into(),
3634                    effect: EffectVector::default(),
3635                    native: true,
3636                })
3637            }
3638            fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
3639                Ok(serde_json::json!({"echo": args}))
3640            }
3641        }
3642
3643        // ── Build a mixed registry ─────────────────────────────────
3644        let mut registry = ToolRegistry::new();
3645
3646        // Register native tool
3647        registry.register(Arc::new(EchoTool));
3648
3649        // Register WASM tool
3650        let runner = Arc::new(WasmToolRunner::new(WasmSandboxConfig::default()));
3651        let noop_wat = r#"(module (func (export "_start")))"#;
3652        registry
3653            .register_wasm_tool(
3654                "wasm.noop",
3655                "A no-op WASM tool",
3656                noop_wat.as_bytes().to_vec(),
3657                runner,
3658            )
3659            .unwrap();
3660
3661        // ── Both tools accessible through one registry ─────────────
3662        assert_eq!(registry.list().len(), 2);
3663
3664        let native = registry
3665            .get("native.echo")
3666            .expect("native tool should exist");
3667        assert!(native.spec().native, "native tool should be marked native");
3668
3669        let wasm = registry.get("wasm.noop").expect("wasm tool should exist");
3670        assert!(!wasm.spec().native, "wasm tool should NOT be marked native");
3671
3672        // ── Native tool executes synchronously ─────────────────────
3673        let result = native
3674            .execute(serde_json::json!({"hello": "world"}))
3675            .unwrap();
3676        assert_eq!(result["echo"]["hello"], "world");
3677
3678        // ── Hierarchical registry ──────────────────────────────────
3679        // Child registry overlays parent; WASM tools from parent visible
3680        let parent = Arc::new(registry);
3681        let mut child = ToolRegistry::with_parent(parent);
3682
3683        // Child sees parent tools
3684        assert!(
3685            child.get("native.echo").is_some(),
3686            "child sees parent native tool"
3687        );
3688        assert!(
3689            child.get("wasm.noop").is_some(),
3690            "child sees parent wasm tool"
3691        );
3692
3693        // Child can shadow
3694        struct OverrideTool;
3695        impl BuiltinTool for OverrideTool {
3696            fn name(&self) -> &str {
3697                "native.echo"
3698            }
3699            fn spec(&self) -> &BuiltinToolSpec {
3700                static SPEC: std::sync::OnceLock<BuiltinToolSpec> = std::sync::OnceLock::new();
3701                SPEC.get_or_init(|| BuiltinToolSpec {
3702                    name: "native.echo".into(),
3703                    category: ToolCategory::System,
3704                    description: "Overridden echo".into(),
3705                    parameters: serde_json::json!({}),
3706                    gate_action: "tool.native.echo".into(),
3707                    effect: EffectVector::default(),
3708                    native: true,
3709                })
3710            }
3711            fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
3712                Ok(serde_json::json!({"overridden": true}))
3713            }
3714        }
3715
3716        child.register(Arc::new(OverrideTool));
3717        let result = child
3718            .get("native.echo")
3719            .unwrap()
3720            .execute(serde_json::json!({}))
3721            .unwrap();
3722        assert_eq!(
3723            result["overridden"], true,
3724            "child should shadow parent tool"
3725        );
3726
3727        // Parent WASM tool still reachable from child
3728        assert!(
3729            child.get("wasm.noop").is_some(),
3730            "WASM tool still reachable via parent"
3731        );
3732    }
3733
3734    // ── Boot path coverage tests ─────────────────────────────────
3735
3736    #[tokio::test]
3737    async fn boot_reaches_running_state() {
3738        let platform = Arc::new(NativePlatform::new());
3739        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3740            .await
3741            .unwrap();
3742        assert_eq!(*kernel.state(), KernelState::Running);
3743    }
3744
3745    #[tokio::test]
3746    async fn boot_registers_cron_service() {
3747        let platform = Arc::new(NativePlatform::new());
3748        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3749            .await
3750            .unwrap();
3751        assert!(
3752            kernel.services().get("cron").is_some(),
3753            "cron service must be registered at boot"
3754        );
3755    }
3756
3757    #[tokio::test]
3758    async fn boot_registers_container_service() {
3759        let platform = Arc::new(NativePlatform::new());
3760        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3761            .await
3762            .unwrap();
3763        assert!(
3764            kernel.services().get("containers").is_some(),
3765            "container service must be registered at boot"
3766        );
3767    }
3768
3769    #[cfg(feature = "exochain")]
3770    fn test_kernel_config_exochain() -> KernelConfig {
3771        use clawft_types::config::{ChainConfig, ResourceTreeConfig};
3772        KernelConfig {
3773            governance: Default::default(),
3774            profile: None,
3775            shared_services: Default::default(),
3776            enabled: true,
3777            max_processes: 16,
3778            health_check_interval_secs: 5,
3779            cluster: None,
3780            chain: Some(ChainConfig {
3781                enabled: true,
3782                checkpoint_interval: 10_000,
3783                chain_id: 0,
3784                checkpoint_path: None,
3785                external_anchor: None,
3786            }),
3787            resource_tree: Some(ResourceTreeConfig {
3788                enabled: true,
3789                checkpoint_path: None,
3790            }),
3791            vector: None,
3792            spatial: None,
3793            profiles: None,
3794            pairing: None,
3795            mesh: None,
3796            anchor: None,
3797            ipc_tcp: None,
3798            llm: None,
3799            agent: None,
3800            brand: ::clawft_types::config::DEFAULT_BRAND.to_string(),
3801        }
3802    }
3803
3804    /// A second kernel on a chain another kernel holds is refused at boot.
3805    #[cfg(all(feature = "exochain", unix))]
3806    #[tokio::test]
3807    async fn boot_refuses_a_chain_held_by_another_kernel() {
3808        let dir = tempfile::tempdir().unwrap();
3809        let ckpt = dir.path().join("chain.json");
3810        let _holder = crate::chain_storage::ChainLock::acquire(&ckpt).unwrap();
3811        let mut kc = test_kernel_config_exochain();
3812        kc.chain.as_mut().unwrap().checkpoint_path = Some(ckpt.to_string_lossy().into_owned());
3813        let platform = Arc::new(NativePlatform::new());
3814        let err = Kernel::boot(test_config(), kc, platform)
3815            .await
3816            .err()
3817            .expect("boot must refuse a held chain");
3818        let msg = err.to_string();
3819        assert!(msg.contains("in use by another kernel"), "{msg}");
3820        assert!(msg.contains("--new-chain"), "{msg}");
3821    }
3822
3823    /// A chain file that fails verification (corruption, or events a newer
3824    /// binary wrote that this build cannot verify) is never replaced by a
3825    /// fresh genesis: boot refuses and the file is left byte-identical.
3826    #[cfg(all(feature = "exochain", unix))]
3827    #[tokio::test]
3828    async fn boot_refuses_an_unrestorable_chain_and_keeps_the_file() {
3829        for rvf in [false, true] {
3830            let dir = tempfile::tempdir().unwrap();
3831            let ckpt = dir.path().join("chain.json");
3832            let chain_file = if rvf { ckpt.with_extension("rvf") } else { ckpt.clone() };
3833            let cm = crate::chain::ChainManager::new(0, 0);
3834            cm.append("s", "k", Some(serde_json::json!({"a": 1})));
3835            if rvf {
3836                cm.save_to_rvf(&chain_file).unwrap();
3837            } else {
3838                cm.save_to_file(&chain_file).unwrap();
3839            }
3840            let mut bytes = std::fs::read(&chain_file).unwrap();
3841            let mid = bytes.len() / 2;
3842            bytes[mid] ^= 0xFF; // corrupt
3843            std::fs::write(&chain_file, &bytes).unwrap();
3844
3845            let mut kc = test_kernel_config_exochain();
3846            kc.chain.as_mut().unwrap().checkpoint_path =
3847                Some(ckpt.to_string_lossy().into_owned());
3848            let platform = Arc::new(NativePlatform::new());
3849            let err = Kernel::boot(test_config(), kc, platform)
3850                .await
3851                .err()
3852                .expect("boot must refuse an unrestorable chain");
3853            let msg = err.to_string();
3854            assert!(msg.contains("refusing to start a fresh chain"), "rvf={rvf}: {msg}");
3855            assert!(
3856                msg.contains("Move or rename") && msg.contains(".rvf/.json sibling") && msg.contains("upgrade"),
3857                "rvf={rvf}: {msg}"
3858            );
3859            assert!(!msg.contains("--new-chain"), "rvf={rvf}: {msg}");
3860            assert_eq!(std::fs::read(&chain_file).unwrap(), bytes, "rvf={rvf}: file changed");
3861        }
3862    }
3863
3864    #[cfg(feature = "exochain")]
3865    #[tokio::test]
3866    async fn boot_exochain_creates_chain_manager() {
3867        let platform = Arc::new(NativePlatform::new());
3868        let kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
3869            .await
3870            .unwrap();
3871        assert!(
3872            kernel.chain_manager().is_some(),
3873            "chain manager must be present with exochain feature and enabled chain config"
3874        );
3875    }
3876
3877    #[cfg(feature = "exochain")]
3878    #[tokio::test]
3879    async fn boot_exochain_creates_tree_manager() {
3880        let platform = Arc::new(NativePlatform::new());
3881        let kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
3882            .await
3883            .unwrap();
3884        assert!(
3885            kernel.tree_manager().is_some(),
3886            "tree manager must be present with exochain feature and enabled config"
3887        );
3888    }
3889
3890    #[cfg(feature = "exochain")]
3891    #[tokio::test]
3892    async fn boot_exochain_chain_has_boot_events() {
3893        let platform = Arc::new(NativePlatform::new());
3894        let kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
3895            .await
3896            .unwrap();
3897        let chain = kernel.chain_manager().unwrap();
3898        // Chain should have boot.init, boot.config, boot.services, boot.cluster, boot.ready, boot.manifest at minimum
3899        assert!(
3900            chain.sequence() >= 6,
3901            "chain should have at least 6 boot events, got {}",
3902            chain.sequence()
3903        );
3904    }
3905
3906    #[cfg(feature = "exochain")]
3907    #[tokio::test]
3908    async fn boot_exochain_governance_gate_present() {
3909        let platform = Arc::new(NativePlatform::new());
3910        let kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
3911            .await
3912            .unwrap();
3913        assert!(
3914            kernel.governance_gate().is_some(),
3915            "governance gate should be present when chain is enabled"
3916        );
3917    }
3918
3919    #[cfg(feature = "ecc")]
3920    #[tokio::test]
3921    async fn boot_ecc_registers_hnsw_service() {
3922        let platform = Arc::new(NativePlatform::new());
3923        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3924            .await
3925            .unwrap();
3926        assert!(
3927            kernel.ecc_hnsw().is_some(),
3928            "HNSW service must be present with ecc feature"
3929        );
3930    }
3931
3932    #[cfg(feature = "ecc")]
3933    #[tokio::test]
3934    async fn boot_ecc_registers_cognitive_tick() {
3935        let platform = Arc::new(NativePlatform::new());
3936        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3937            .await
3938            .unwrap();
3939        assert!(
3940            kernel.ecc_tick().is_some(),
3941            "cognitive tick must be present with ecc feature"
3942        );
3943    }
3944
3945    #[cfg(feature = "ecc")]
3946    #[tokio::test]
3947    async fn boot_ecc_calibration_has_valid_results() {
3948        let platform = Arc::new(NativePlatform::new());
3949        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3950            .await
3951            .unwrap();
3952        let cal = kernel.ecc_calibration().unwrap();
3953        assert!(cal.tick_interval_ms > 0, "tick interval must be positive");
3954        assert!(cal.compute_p50_us > 0, "p50 latency must be measured");
3955        assert!(cal.compute_p95_us >= cal.compute_p50_us, "p95 >= p50");
3956    }
3957
3958    #[cfg(feature = "ecc")]
3959    #[tokio::test]
3960    async fn boot_ecc_causal_graph_accessible() {
3961        let platform = Arc::new(NativePlatform::new());
3962        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3963            .await
3964            .unwrap();
3965        assert!(
3966            kernel.ecc_causal().is_some(),
3967            "causal graph must be accessible"
3968        );
3969        assert_eq!(
3970            kernel.ecc_causal().unwrap().node_count(),
3971            0,
3972            "causal graph starts empty"
3973        );
3974    }
3975
3976    #[cfg(feature = "ecc")]
3977    #[tokio::test]
3978    async fn boot_ecc_crossrefs_and_impulses_accessible() {
3979        let platform = Arc::new(NativePlatform::new());
3980        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
3981            .await
3982            .unwrap();
3983        assert!(
3984            kernel.ecc_crossrefs().is_some(),
3985            "cross-ref store must be accessible"
3986        );
3987        assert!(
3988            kernel.ecc_impulses().is_some(),
3989            "impulse queue must be accessible"
3990        );
3991    }
3992
3993    // ── vector.strict / diskann feature mismatch (WEFT-656) ─────────────
3994    //
3995    // These assert against the actual compiled feature set (via
3996    // `#[cfg(not(feature = "diskann"))]`) rather than injecting a fake
3997    // mismatch, so they only run meaningfully in the default build (ecc
3998    // on, diskann off) where the mismatch is real.
3999
4000    #[cfg(all(feature = "ecc", not(feature = "diskann")))]
4001    #[tokio::test]
4002    async fn boot_vector_strict_diskann_without_feature_fails() {
4003        let platform = Arc::new(NativePlatform::new());
4004        let mut kconfig = test_kernel_config();
4005        kconfig.vector = Some(clawft_types::config::VectorConfig {
4006            backend: clawft_types::config::VectorBackendKind::DiskAnn,
4007            strict: true,
4008            ..Default::default()
4009        });
4010        let result = Kernel::boot(test_config(), kconfig, platform).await;
4011        assert!(
4012            result.is_err(),
4013            "boot must fail when vector.strict=true and the diskann feature \
4014             is not compiled in"
4015        );
4016    }
4017
4018    #[cfg(all(feature = "ecc", not(feature = "diskann")))]
4019    #[tokio::test]
4020    async fn boot_vector_strict_hybrid_without_feature_fails() {
4021        let platform = Arc::new(NativePlatform::new());
4022        let mut kconfig = test_kernel_config();
4023        kconfig.vector = Some(clawft_types::config::VectorConfig {
4024            backend: clawft_types::config::VectorBackendKind::Hybrid,
4025            strict: true,
4026            ..Default::default()
4027        });
4028        let result = Kernel::boot(test_config(), kconfig, platform).await;
4029        assert!(
4030            result.is_err(),
4031            "boot must fail for Hybrid too — it wraps DiskAnnBackend as its \
4032             cold tier"
4033        );
4034    }
4035
4036    #[cfg(all(feature = "ecc", not(feature = "diskann")))]
4037    #[tokio::test]
4038    async fn boot_vector_non_strict_diskann_without_feature_warns_and_boots() {
4039        let platform = Arc::new(NativePlatform::new());
4040        let mut kconfig = test_kernel_config();
4041        kconfig.vector = Some(clawft_types::config::VectorConfig {
4042            backend: clawft_types::config::VectorBackendKind::DiskAnn,
4043            strict: false,
4044            ..Default::default()
4045        });
4046        let kernel = Kernel::boot(test_config(), kconfig, platform)
4047            .await
4048            .unwrap();
4049        let warned = kernel.boot_log().events().iter().any(|e| {
4050            e.level == crate::console::LogLevel::Warn && e.message.contains("diskann")
4051        });
4052        assert!(
4053            warned,
4054            "expected a Warn boot event naming the diskann feature mismatch"
4055        );
4056    }
4057
4058    #[tokio::test]
4059    async fn boot_cluster_membership_accessible() {
4060        let platform = Arc::new(NativePlatform::new());
4061        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4062            .await
4063            .unwrap();
4064        let cm = kernel.cluster_membership();
4065        assert!(
4066            !cm.local_node_id().is_empty(),
4067            "cluster membership should have a node ID"
4068        );
4069    }
4070
4071    #[tokio::test]
4072    async fn shutdown_transitions_to_halted() {
4073        let platform = Arc::new(NativePlatform::new());
4074        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4075            .await
4076            .unwrap();
4077        assert_eq!(*kernel.state(), KernelState::Running);
4078        kernel.shutdown().await.unwrap();
4079        assert_eq!(*kernel.state(), KernelState::Halted);
4080    }
4081
4082    #[tokio::test]
4083    async fn shutdown_from_halted_fails() {
4084        let platform = Arc::new(NativePlatform::new());
4085        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4086            .await
4087            .unwrap();
4088        kernel.shutdown().await.unwrap();
4089        let err = kernel.shutdown().await.unwrap_err();
4090        match err {
4091            KernelError::WrongState { expected, actual } => {
4092                assert_eq!(expected, "Running");
4093                assert_eq!(actual, "halted");
4094            }
4095            other => panic!("expected WrongState, got: {other:?}"),
4096        }
4097    }
4098
4099    #[tokio::test]
4100    async fn process_table_has_kernel_pid_zero() {
4101        let platform = Arc::new(NativePlatform::new());
4102        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4103            .await
4104            .unwrap();
4105        let entry = kernel.process_table().get(0).expect("PID 0 should exist");
4106        assert_eq!(entry.agent_id, "kernel");
4107        assert_eq!(entry.state, ProcessState::Running);
4108        assert_eq!(entry.pid, 0);
4109    }
4110
4111    #[tokio::test]
4112    async fn a2a_router_accessible_after_boot() {
4113        let platform = Arc::new(NativePlatform::new());
4114        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4115            .await
4116            .unwrap();
4117        let a2a = kernel.a2a_router();
4118        // Should be able to create an inbox for a future PID
4119        // (A2ARouter is wired to the same process table)
4120        let _inbox = a2a.create_inbox(0); // kernel PID
4121    }
4122
4123    #[tokio::test]
4124    async fn boot_log_contains_expected_phases() {
4125        let platform = Arc::new(NativePlatform::new());
4126        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4127            .await
4128            .unwrap();
4129        let formatted = kernel.boot_log().format_all();
4130        assert!(
4131            formatted.contains(&format!("WeftOS v{}", env!("CARGO_PKG_VERSION"))),
4132            "should contain version"
4133        );
4134        assert!(
4135            formatted.contains("Service registry ready"),
4136            "should have service phase"
4137        );
4138        assert!(
4139            formatted.contains("A2A router ready"),
4140            "should have A2A phase"
4141        );
4142        assert!(
4143            formatted.contains("Boot complete"),
4144            "should have ready phase"
4145        );
4146    }
4147
4148    #[tokio::test]
4149    async fn event_log_populated_after_boot() {
4150        let platform = Arc::new(NativePlatform::new());
4151        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4152            .await
4153            .unwrap();
4154        // The event_log is populated from boot_log during boot
4155        let events = kernel.event_log();
4156        // At minimum there should be some events from boot ingestion
4157        assert!(
4158            !events.is_empty(),
4159            "event log should have entries after boot"
4160        );
4161    }
4162
4163    #[tokio::test]
4164    async fn health_system_accessible() {
4165        let platform = Arc::new(NativePlatform::new());
4166        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4167            .await
4168            .unwrap();
4169        let _health = kernel.health();
4170        // Health system should be constructable and accessible
4171    }
4172
4173    #[tokio::test]
4174    async fn uptime_is_positive_after_boot() {
4175        let platform = Arc::new(NativePlatform::new());
4176        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4177            .await
4178            .unwrap();
4179        let uptime = kernel.uptime();
4180        assert!(uptime.as_nanos() > 0, "uptime should be positive");
4181    }
4182
4183    #[tokio::test]
4184    async fn cron_service_accessible_and_empty() {
4185        let platform = Arc::new(NativePlatform::new());
4186        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4187            .await
4188            .unwrap();
4189        assert_eq!(kernel.cron_service().job_count(), 0, "no cron jobs at boot");
4190    }
4191
4192    #[tokio::test]
4193    async fn max_processes_matches_config() {
4194        let platform = Arc::new(NativePlatform::new());
4195        let mut kconfig = test_kernel_config();
4196        kconfig.max_processes = 42;
4197        let kernel = Kernel::boot(test_config(), kconfig, platform)
4198            .await
4199            .unwrap();
4200        assert_eq!(kernel.process_table().max_processes(), 42);
4201    }
4202
4203    // ── Sprint 09a: KernelState serde + display tests ────────────
4204
4205    #[test]
4206    fn kernel_state_serde_roundtrip_all_variants() {
4207        for state in [
4208            KernelState::Booting,
4209            KernelState::Running,
4210            KernelState::ShuttingDown,
4211            KernelState::Halted,
4212        ] {
4213            let json = serde_json::to_string(&state).unwrap();
4214            let restored: KernelState = serde_json::from_str(&json).unwrap();
4215            assert_eq!(restored, state);
4216        }
4217    }
4218
4219    #[test]
4220    fn kernel_state_equality() {
4221        assert_eq!(KernelState::Running, KernelState::Running);
4222        assert_ne!(KernelState::Running, KernelState::Halted);
4223    }
4224
4225    #[tokio::test]
4226    async fn kernel_state_is_running_after_boot() {
4227        let platform = Arc::new(NativePlatform::new());
4228        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4229            .await
4230            .unwrap();
4231        assert_eq!(kernel.state(), &KernelState::Running);
4232    }
4233
4234    #[tokio::test]
4235    async fn boot_log_has_entries() {
4236        let platform = Arc::new(NativePlatform::new());
4237        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4238            .await
4239            .unwrap();
4240        let log = kernel.boot_log();
4241        assert!(!log.events().is_empty(), "boot log should have events");
4242    }
4243
4244    #[tokio::test]
4245    async fn bus_is_accessible_after_boot() {
4246        let platform = Arc::new(NativePlatform::new());
4247        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4248            .await
4249            .unwrap();
4250        // Bus should exist (used for message passing)
4251        let _bus = kernel.bus();
4252    }
4253
4254    #[tokio::test]
4255    async fn a2a_router_has_inboxes_after_boot() {
4256        let platform = Arc::new(NativePlatform::new());
4257        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4258            .await
4259            .unwrap();
4260        let _router = kernel.a2a_router();
4261        // Router should be accessible after boot
4262    }
4263
4264    #[tokio::test]
4265    async fn ipc_accessible_after_boot() {
4266        let platform = Arc::new(NativePlatform::new());
4267        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4268            .await
4269            .unwrap();
4270        let _ipc = kernel.ipc();
4271    }
4272
4273    // ── W5: Test hardening — additional coverage ─────────────────
4274
4275    #[tokio::test]
4276    async fn supervisor_accessible_after_boot() {
4277        let platform = Arc::new(NativePlatform::new());
4278        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4279            .await
4280            .unwrap();
4281        let _supervisor = kernel.supervisor();
4282    }
4283
4284    #[tokio::test]
4285    async fn kernel_config_accessor_returns_boot_config() {
4286        let platform = Arc::new(NativePlatform::new());
4287        let mut kconfig = test_kernel_config();
4288        kconfig.max_processes = 99;
4289        kconfig.health_check_interval_secs = 42;
4290        let kernel = Kernel::boot(test_config(), kconfig, platform)
4291            .await
4292            .unwrap();
4293        assert_eq!(kernel.kernel_config().max_processes, 99);
4294        assert_eq!(kernel.kernel_config().health_check_interval_secs, 42);
4295    }
4296
4297    #[tokio::test]
4298    async fn shutdown_exits_all_agent_processes() {
4299        let platform = Arc::new(NativePlatform::new());
4300        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4301            .await
4302            .unwrap();
4303
4304        // Spawn a couple of agents
4305        let spawn1 = kernel.supervisor().spawn(crate::supervisor::SpawnRequest {
4306            agent_id: "test-agent-1".into(),
4307            capabilities: None,
4308            parent_pid: None,
4309            env: std::collections::HashMap::new(),
4310            backend: None,
4311        });
4312        let spawn2 = kernel.supervisor().spawn(crate::supervisor::SpawnRequest {
4313            agent_id: "test-agent-2".into(),
4314            capabilities: None,
4315            parent_pid: None,
4316            env: std::collections::HashMap::new(),
4317            backend: None,
4318        });
4319        assert!(spawn1.is_ok());
4320        assert!(spawn2.is_ok());
4321        let pid1 = spawn1.unwrap().pid;
4322        let pid2 = spawn2.unwrap().pid;
4323
4324        // Verify agents exist in process table
4325        assert!(kernel.process_table().get(pid1).is_some());
4326        assert!(kernel.process_table().get(pid2).is_some());
4327
4328        kernel.shutdown().await.unwrap();
4329
4330        // After shutdown, agent processes should no longer be Running.
4331        // They may be Exited (if the shutdown loop reached them) or still
4332        // Starting (if spawn() never transitioned them to Running because
4333        // no agent loop was attached in this test). Either is acceptable;
4334        // the key assertion is that shutdown completed without error.
4335        if let Some(entry1) = kernel.process_table().get(pid1) {
4336            assert!(
4337                !matches!(entry1.state, ProcessState::Running),
4338                "agent 1 should not be Running after shutdown, got: {}",
4339                entry1.state
4340            );
4341        }
4342        if let Some(entry2) = kernel.process_table().get(pid2) {
4343            assert!(
4344                !matches!(entry2.state, ProcessState::Running),
4345                "agent 2 should not be Running after shutdown, got: {}",
4346                entry2.state
4347            );
4348        }
4349    }
4350
4351    #[tokio::test]
4352    async fn shutdown_stops_services() {
4353        let platform = Arc::new(NativePlatform::new());
4354        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4355            .await
4356            .unwrap();
4357
4358        // Services should be running before shutdown
4359        let svc_count_before = kernel.services().len();
4360        assert!(svc_count_before > 0, "should have services before shutdown");
4361
4362        kernel.shutdown().await.unwrap();
4363        // After shutdown, services stop_all() was called — the registry is
4364        // still there but state is Halted
4365        assert_eq!(*kernel.state(), KernelState::Halted);
4366    }
4367
4368    #[tokio::test]
4369    async fn boot_with_custom_max_processes() {
4370        let platform = Arc::new(NativePlatform::new());
4371        let mut kconfig = test_kernel_config();
4372        kconfig.max_processes = 128;
4373        let kernel = Kernel::boot(test_config(), kconfig, platform)
4374            .await
4375            .unwrap();
4376        assert_eq!(kernel.process_table().max_processes(), 128);
4377    }
4378
4379    #[tokio::test]
4380    async fn boot_creates_a2a_router_with_topic_router() {
4381        let platform = Arc::new(NativePlatform::new());
4382        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4383            .await
4384            .unwrap();
4385        // A2A router should have a topic router wired in
4386        let _topic_router = kernel.a2a_router().topic_router();
4387    }
4388
4389    #[tokio::test]
4390    async fn event_log_has_shutdown_events() {
4391        let platform = Arc::new(NativePlatform::new());
4392        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4393            .await
4394            .unwrap();
4395
4396        let events_before = kernel.event_log().len();
4397        kernel.shutdown().await.unwrap();
4398
4399        // Shutdown should have added events
4400        let events_after = kernel.event_log().len();
4401        assert!(
4402            events_after > events_before,
4403            "event log should grow during shutdown (before={events_before}, after={events_after})"
4404        );
4405    }
4406
4407    #[tokio::test]
4408    async fn boot_log_contains_max_processes() {
4409        let platform = Arc::new(NativePlatform::new());
4410        let mut kconfig = test_kernel_config();
4411        kconfig.max_processes = 64;
4412        let kernel = Kernel::boot(test_config(), kconfig, platform)
4413            .await
4414            .unwrap();
4415        let formatted = kernel.boot_log().format_all();
4416        assert!(
4417            formatted.contains("Max processes: 64"),
4418            "boot log should contain max_processes config value"
4419        );
4420    }
4421
4422    #[tokio::test]
4423    async fn boot_log_contains_health_interval() {
4424        let platform = Arc::new(NativePlatform::new());
4425        let mut kconfig = test_kernel_config();
4426        kconfig.health_check_interval_secs = 15;
4427        let kernel = Kernel::boot(test_config(), kconfig, platform)
4428            .await
4429            .unwrap();
4430        let formatted = kernel.boot_log().format_all();
4431        assert!(
4432            formatted.contains("Health check interval: 15s"),
4433            "boot log should contain health check interval"
4434        );
4435    }
4436
4437    #[tokio::test]
4438    async fn take_app_context_returns_some_then_none() {
4439        let platform = Arc::new(NativePlatform::new());
4440        let mut kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4441            .await
4442            .unwrap();
4443
4444        assert!(
4445            kernel.take_app_context().is_some(),
4446            "first take should succeed"
4447        );
4448        assert!(
4449            kernel.take_app_context().is_none(),
4450            "second take should return None"
4451        );
4452        assert!(
4453            kernel.take_app_context().is_none(),
4454            "third take should also return None"
4455        );
4456    }
4457
4458    #[tokio::test]
4459    async fn kernel_pid_zero_has_default_capabilities() {
4460        let platform = Arc::new(NativePlatform::new());
4461        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4462            .await
4463            .unwrap();
4464        let entry = kernel.process_table().get(0).unwrap();
4465        assert_eq!(entry.agent_id, "kernel");
4466        assert!(entry.parent_pid.is_none(), "kernel should have no parent");
4467        // Default capabilities: can_ipc should be true, ipc_scope should be All
4468        assert!(entry.capabilities.can_ipc, "kernel should be able to IPC");
4469    }
4470
4471    #[tokio::test]
4472    async fn cluster_membership_has_nonempty_node_id() {
4473        let platform = Arc::new(NativePlatform::new());
4474        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4475            .await
4476            .unwrap();
4477        let node_id = kernel.cluster_membership().local_node_id();
4478        assert!(!node_id.is_empty(), "cluster node ID should not be empty");
4479        // UUID v4 format: 8-4-4-4-12 = 36 chars
4480        assert!(
4481            crate::node_id::is_node_id(node_id),
4482            "node ID should be hex(SHA-256(pubkey)[..16]), got {node_id}"
4483        );
4484    }
4485
4486    #[tokio::test]
4487    async fn boot_log_contains_cluster_info() {
4488        let platform = Arc::new(NativePlatform::new());
4489        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4490            .await
4491            .unwrap();
4492        let formatted = kernel.boot_log().format_all();
4493        assert!(
4494            formatted.contains("Cluster membership ready"),
4495            "boot log should mention cluster membership"
4496        );
4497    }
4498
4499    #[cfg(feature = "exochain")]
4500    #[tokio::test]
4501    async fn shutdown_with_exochain_completes() {
4502        let platform = Arc::new(NativePlatform::new());
4503        let mut kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
4504            .await
4505            .unwrap();
4506
4507        assert!(kernel.chain_manager().is_some());
4508        kernel.shutdown().await.unwrap();
4509        assert_eq!(*kernel.state(), KernelState::Halted);
4510    }
4511
4512    #[cfg(feature = "exochain")]
4513    #[tokio::test]
4514    async fn exochain_chain_integrity_valid_after_boot() {
4515        let platform = Arc::new(NativePlatform::new());
4516        let kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
4517            .await
4518            .unwrap();
4519        let chain = kernel.chain_manager().unwrap();
4520        let result = chain.verify_integrity();
4521        assert!(result.valid, "chain integrity should be valid after boot");
4522        assert!(result.errors.is_empty(), "no integrity errors expected");
4523    }
4524
4525    #[cfg(feature = "exochain")]
4526    #[tokio::test]
4527    async fn exochain_boot_manifest_event_present() {
4528        let platform = Arc::new(NativePlatform::new());
4529        let kernel = Kernel::boot(test_config(), test_kernel_config_exochain(), platform)
4530            .await
4531            .unwrap();
4532        let chain = kernel.chain_manager().unwrap();
4533        let all_events = chain.tail(0);
4534        let manifest_events: Vec<_> = all_events
4535            .iter()
4536            .filter(|e| e.kind == "boot.manifest")
4537            .collect();
4538        assert!(
4539            !manifest_events.is_empty(),
4540            "boot.manifest event should be on chain"
4541        );
4542    }
4543
4544    // ── os-patterns observability wiring tests (W3) ─────────────────
4545
4546    #[tokio::test]
4547    #[cfg(feature = "os-patterns")]
4548    async fn boot_creates_metrics_registry() {
4549        let platform = Arc::new(NativePlatform::new());
4550        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4551            .await
4552            .unwrap();
4553
4554        let registry = kernel.metrics_registry();
4555        assert!(
4556            registry.is_some(),
4557            "MetricsRegistry should be created at boot"
4558        );
4559        // Verify a built-in gauge was seeded
4560        let r = registry.unwrap();
4561        let val = r.gauge_get("kernel.process.count");
4562        assert!(val >= 1, "kernel.process.count gauge should be seeded");
4563    }
4564
4565    #[tokio::test]
4566    #[cfg(feature = "os-patterns")]
4567    async fn boot_creates_log_service() {
4568        let platform = Arc::new(NativePlatform::new());
4569        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4570            .await
4571            .unwrap();
4572
4573        assert!(
4574            kernel.log_service().is_some(),
4575            "LogService should be created at boot"
4576        );
4577    }
4578
4579    #[tokio::test]
4580    #[cfg(feature = "os-patterns")]
4581    async fn boot_creates_timer_service() {
4582        let platform = Arc::new(NativePlatform::new());
4583        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4584            .await
4585            .unwrap();
4586
4587        assert!(
4588            kernel.timer_service().is_some(),
4589            "TimerService should be created at boot"
4590        );
4591    }
4592
4593    #[tokio::test]
4594    #[cfg(feature = "os-patterns")]
4595    async fn boot_creates_dead_letter_queue() {
4596        let platform = Arc::new(NativePlatform::new());
4597        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4598            .await
4599            .unwrap();
4600
4601        let dlq = kernel.dead_letter_queue();
4602        assert!(dlq.is_some(), "DeadLetterQueue should be created at boot");
4603        assert!(dlq.unwrap().is_empty(), "DLQ should start empty");
4604    }
4605
4606    #[tokio::test]
4607    #[cfg(feature = "os-patterns")]
4608    async fn kernel_metrics_accessor_returns_registry() {
4609        let platform = Arc::new(NativePlatform::new());
4610        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4611            .await
4612            .unwrap();
4613
4614        let registry = kernel.metrics_registry().unwrap();
4615        // Verify counter_inc works on built-in metric
4616        registry.counter_inc(crate::metrics::METRIC_MESSAGES_SENT);
4617        assert_eq!(
4618            registry.counter_get(crate::metrics::METRIC_MESSAGES_SENT),
4619            1
4620        );
4621    }
4622
4623    #[tokio::test]
4624    #[cfg(feature = "os-patterns")]
4625    async fn dlq_accessible_via_kernel_accessor() {
4626        let platform = Arc::new(NativePlatform::new());
4627        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4628            .await
4629            .unwrap();
4630
4631        let dlq = kernel.dead_letter_queue().unwrap();
4632        assert_eq!(dlq.capacity(), crate::dead_letter::DEFAULT_DLQ_CAPACITY);
4633    }
4634
4635    #[tokio::test]
4636    #[cfg(feature = "os-patterns")]
4637    async fn failed_a2a_send_routes_to_dlq() {
4638        use crate::ipc::{KernelMessage, MessageTarget};
4639
4640        let platform = Arc::new(NativePlatform::new());
4641        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4642            .await
4643            .unwrap();
4644
4645        // Register a sender agent
4646        let sender_entry = ProcessEntry {
4647            pid: 0,
4648            agent_id: "dlq-sender".to_owned(),
4649            state: ProcessState::Running,
4650            capabilities: AgentCapabilities::default(),
4651            resource_usage: ResourceUsage::default(),
4652            cancel_token: crate::process::CancellationToken::new(),
4653            parent_pid: None,
4654        };
4655        let sender_pid = kernel.process_table().insert(sender_entry).unwrap();
4656        kernel.a2a_router().create_inbox(sender_pid);
4657
4658        // Send to a PID that has no inbox (PID 9999 doesn't exist)
4659        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(9999), "dead letter test");
4660        let result = kernel.a2a_router().send(msg).await;
4661        assert!(result.is_err(), "send to unknown PID should fail");
4662
4663        // Verify the message landed in the DLQ
4664        let dlq = kernel.dead_letter_queue().unwrap();
4665        assert_eq!(dlq.len(), 1, "DLQ should contain 1 dead letter");
4666    }
4667
4668    #[tokio::test]
4669    #[cfg(feature = "os-patterns")]
4670    async fn kernel_metrics_gauge_seeded_at_boot() {
4671        let platform = Arc::new(NativePlatform::new());
4672        let kernel = Kernel::boot(test_config(), test_kernel_config(), platform)
4673            .await
4674            .unwrap();
4675
4676        let registry = kernel.metrics_registry().unwrap();
4677        // kernel.process.count should reflect at least the kernel PID 0
4678        assert!(
4679            registry.gauge_get("kernel.process.count") >= 1,
4680            "kernel.process.count should be >= 1 at boot"
4681        );
4682        // Verify additional gauges exist (they were set to 0, so gauge_get returns 0 not None)
4683        assert_eq!(
4684            registry.gauge_get("kernel.uptime_secs"),
4685            0,
4686            "kernel.uptime_secs gauge should exist and be 0 at boot"
4687        );
4688        assert_eq!(
4689            registry.gauge_get("kernel.chain.height"),
4690            0,
4691            "kernel.chain.height gauge should exist and be 0 at boot"
4692        );
4693    }
4694}